@diegovelasquezweb/a11y-engine 0.1.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.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/assets/discovery/crawler-config.json +11 -0
- package/assets/discovery/stack-detection.json +33 -0
- package/assets/remediation/axe-check-maps.json +31 -0
- package/assets/remediation/code-patterns.json +109 -0
- package/assets/remediation/guardrails.json +24 -0
- package/assets/remediation/intelligence.json +4166 -0
- package/assets/remediation/source-boundaries.json +46 -0
- package/assets/reporting/compliance-config.json +173 -0
- package/assets/reporting/manual-checks.json +944 -0
- package/assets/reporting/wcag-reference.json +588 -0
- package/package.json +37 -0
- package/scripts/audit.mjs +326 -0
- package/scripts/core/asset-loader.mjs +54 -0
- package/scripts/core/toolchain.mjs +102 -0
- package/scripts/core/utils.mjs +105 -0
- package/scripts/engine/analyzer.mjs +1022 -0
- package/scripts/engine/dom-scanner.mjs +685 -0
- package/scripts/engine/source-scanner.mjs +300 -0
- package/scripts/reports/builders/checklist.mjs +307 -0
- package/scripts/reports/builders/html.mjs +766 -0
- package/scripts/reports/builders/md.mjs +96 -0
- package/scripts/reports/builders/pdf.mjs +259 -0
- package/scripts/reports/renderers/findings.mjs +188 -0
- package/scripts/reports/renderers/html.mjs +452 -0
- package/scripts/reports/renderers/md.mjs +595 -0
- package/scripts/reports/renderers/pdf.mjs +551 -0
- package/scripts/reports/renderers/utils.mjs +42 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file report-md.mjs
|
|
3
|
+
* @description Generates a Markdown-based remediation guide and audit summary.
|
|
4
|
+
* This report is optimized for developers and intended to be used as a
|
|
5
|
+
* backlog or README-style remediation roadmap.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readJson, log, getInternalPath, DEFAULTS } from "../../core/utils.mjs";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { normalizeFindings } from "../renderers/findings.mjs";
|
|
12
|
+
import { buildMarkdownSummary } from "../renderers/md.mjs";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prints the CLI usage instructions and available options for the Markdown report builder.
|
|
16
|
+
*/
|
|
17
|
+
function printUsage() {
|
|
18
|
+
log.info(`Usage:
|
|
19
|
+
node report-md.mjs [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--input <path> Findings JSON path (default: internal)
|
|
23
|
+
--output <path> Output Markdown path (default: internal)
|
|
24
|
+
--base-url <url> Target website URL
|
|
25
|
+
--target <text> Compliance target label (default: WCAG 2.2 AA)
|
|
26
|
+
-h, --help Show this help
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parses command-line arguments into a configuration object for the Markdown builder.
|
|
32
|
+
* @param {string[]} argv - Array of command-line arguments.
|
|
33
|
+
* @returns {Object} A configuration object containing input, output, and target settings.
|
|
34
|
+
*/
|
|
35
|
+
function parseArgs(argv) {
|
|
36
|
+
const args = {
|
|
37
|
+
input: getInternalPath("a11y-findings.json"),
|
|
38
|
+
output: getInternalPath("remediation.md"),
|
|
39
|
+
baseUrl: "",
|
|
40
|
+
target: DEFAULTS.complianceTarget,
|
|
41
|
+
framework: null,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
45
|
+
const key = argv[i];
|
|
46
|
+
const value = argv[i + 1];
|
|
47
|
+
if (key === "--help" || key === "-h") {
|
|
48
|
+
printUsage();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
if (!key.startsWith("--") || value === undefined) continue;
|
|
52
|
+
|
|
53
|
+
if (key === "--input") args.input = value;
|
|
54
|
+
if (key === "--output") args.output = value;
|
|
55
|
+
if (key === "--base-url") args.baseUrl = value;
|
|
56
|
+
if (key === "--target") args.target = value;
|
|
57
|
+
i += 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return args;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The main execution function for the Markdown report builder.
|
|
65
|
+
* Reads scan results, generates the Markdown string, and writes the output file.
|
|
66
|
+
* @throws {Error} If the input findings file is missing or invalid.
|
|
67
|
+
*/
|
|
68
|
+
function main() {
|
|
69
|
+
const args = parseArgs(process.argv.slice(2));
|
|
70
|
+
const inputPayload = readJson(args.input);
|
|
71
|
+
if (!inputPayload) {
|
|
72
|
+
throw new Error(`Input findings file not found or invalid: ${args.input}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const patternInput = getInternalPath("a11y-pattern-findings.json");
|
|
76
|
+
const patternPayload = fs.existsSync(patternInput) ? readJson(patternInput) : null;
|
|
77
|
+
|
|
78
|
+
const findings = normalizeFindings(inputPayload);
|
|
79
|
+
const md = buildMarkdownSummary(args, findings, {
|
|
80
|
+
...inputPayload.metadata,
|
|
81
|
+
incomplete_findings: inputPayload.incomplete_findings,
|
|
82
|
+
pattern_findings: patternPayload,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
fs.mkdirSync(path.dirname(args.output), { recursive: true });
|
|
86
|
+
fs.writeFileSync(args.output, md, "utf-8");
|
|
87
|
+
|
|
88
|
+
log.success(`Remediation guide written to ${args.output}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
main();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
log.error(`Markdown Generation Error: ${error.message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file report-pdf.mjs
|
|
3
|
+
* @description Generates a professional PDF audit report using Playwright.
|
|
4
|
+
* It renders an internal HTML template optimized for print and exports it
|
|
5
|
+
* as a formal A4 accessibility compliance document.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { chromium } from "playwright";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { readJson, log, getInternalPath, DEFAULTS } from "../../core/utils.mjs";
|
|
12
|
+
import {
|
|
13
|
+
normalizeFindings,
|
|
14
|
+
buildSummary,
|
|
15
|
+
computeComplianceScore,
|
|
16
|
+
wcagOverallStatus,
|
|
17
|
+
} from "../renderers/findings.mjs";
|
|
18
|
+
import {
|
|
19
|
+
scoreMetrics,
|
|
20
|
+
buildPdfTableOfContents,
|
|
21
|
+
buildPdfExecutiveSummary,
|
|
22
|
+
buildPdfRiskSection,
|
|
23
|
+
buildPdfRemediationRoadmap,
|
|
24
|
+
buildPdfMethodologySection,
|
|
25
|
+
buildPdfIssueSummaryTable,
|
|
26
|
+
buildPdfNextSteps,
|
|
27
|
+
buildPdfAuditLimitations,
|
|
28
|
+
buildPdfCoverPage,
|
|
29
|
+
} from "../renderers/pdf.mjs";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prints the CLI usage instructions and available options for the PDF report builder.
|
|
33
|
+
*/
|
|
34
|
+
function printUsage() {
|
|
35
|
+
log.info(`Usage:
|
|
36
|
+
node report-pdf.mjs [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--input <path> Findings JSON path (default: internal)
|
|
40
|
+
--output <path> Output PDF path (required)
|
|
41
|
+
--base-url <url> Target website URL
|
|
42
|
+
--target <text> Compliance target label (default: WCAG 2.2 AA)
|
|
43
|
+
-h, --help Show this help
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parses command-line arguments into a configuration object for the PDF builder.
|
|
49
|
+
* @param {string[]} argv - Array of command-line arguments.
|
|
50
|
+
* @returns {Object} A configuration object containing input, output, and target settings.
|
|
51
|
+
*/
|
|
52
|
+
function parseArgs(argv) {
|
|
53
|
+
const args = {
|
|
54
|
+
input: getInternalPath("a11y-findings.json"),
|
|
55
|
+
output: "",
|
|
56
|
+
baseUrl: "",
|
|
57
|
+
target: DEFAULTS.complianceTarget,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
61
|
+
const key = argv[i];
|
|
62
|
+
const value = argv[i + 1];
|
|
63
|
+
if (key === "--help" || key === "-h") {
|
|
64
|
+
printUsage();
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
if (!key.startsWith("--") || value === undefined) continue;
|
|
68
|
+
|
|
69
|
+
if (key === "--input") args.input = value;
|
|
70
|
+
if (key === "--output") args.output = value;
|
|
71
|
+
if (key === "--base-url") args.baseUrl = value;
|
|
72
|
+
if (key === "--target") args.target = value;
|
|
73
|
+
i += 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Constructs the HTML structure specifically tailored for PDF rendering.
|
|
81
|
+
* @param {Object} args - The parsed CLI arguments.
|
|
82
|
+
* @param {Object[]} findings - The normalized list of audit findings.
|
|
83
|
+
* @returns {string} The HTML document string fully prepared for PDF export.
|
|
84
|
+
*/
|
|
85
|
+
function buildPdfHtml(args, findings) {
|
|
86
|
+
const totals = buildSummary(findings);
|
|
87
|
+
const score = computeComplianceScore(totals);
|
|
88
|
+
|
|
89
|
+
let siteHostname = args.baseUrl;
|
|
90
|
+
try {
|
|
91
|
+
siteHostname = new URL(
|
|
92
|
+
args.baseUrl.startsWith("http")
|
|
93
|
+
? args.baseUrl
|
|
94
|
+
: `https://${args.baseUrl}`,
|
|
95
|
+
).hostname;
|
|
96
|
+
} catch {}
|
|
97
|
+
|
|
98
|
+
const coverDate = new Date().toLocaleDateString("en-US", {
|
|
99
|
+
year: "numeric",
|
|
100
|
+
month: "long",
|
|
101
|
+
day: "numeric",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return `<!doctype html>
|
|
105
|
+
<html lang="en">
|
|
106
|
+
<head>
|
|
107
|
+
<meta charset="utf-8">
|
|
108
|
+
<title>Accessibility Audit — ${siteHostname}</title>
|
|
109
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
110
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
111
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
|
|
112
|
+
<style>
|
|
113
|
+
@page { size: A4; margin: 2cm; }
|
|
114
|
+
|
|
115
|
+
body {
|
|
116
|
+
background: white;
|
|
117
|
+
color: black;
|
|
118
|
+
font-family: 'Libre Baskerville', serif;
|
|
119
|
+
font-size: 11pt;
|
|
120
|
+
line-height: 1.6;
|
|
121
|
+
margin: 0;
|
|
122
|
+
padding: 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
h1, h2, h3, h4 {
|
|
126
|
+
font-family: 'Inter', sans-serif;
|
|
127
|
+
color: black;
|
|
128
|
+
margin-top: 1.5rem;
|
|
129
|
+
margin-bottom: 1rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cover-page {
|
|
133
|
+
height: 25.5cm;
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
page-break-after: always;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.finding-entry {
|
|
140
|
+
border-top: 1pt solid black;
|
|
141
|
+
padding-top: 1.5rem;
|
|
142
|
+
margin-top: 2rem;
|
|
143
|
+
page-break-inside: avoid;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.severity-tag {
|
|
147
|
+
font-weight: 800;
|
|
148
|
+
text-transform: uppercase;
|
|
149
|
+
border: 1.5pt solid black;
|
|
150
|
+
padding: 2pt 6pt;
|
|
151
|
+
font-size: 9pt;
|
|
152
|
+
margin-bottom: 1rem;
|
|
153
|
+
display: inline-block;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.remediation-box {
|
|
157
|
+
background-color: #f3f4f6;
|
|
158
|
+
border-left: 4pt solid black;
|
|
159
|
+
padding: 1rem;
|
|
160
|
+
margin: 1rem 0;
|
|
161
|
+
font-style: italic;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pre {
|
|
165
|
+
background: #f9fafb;
|
|
166
|
+
border: 1pt solid #ddd;
|
|
167
|
+
padding: 10pt;
|
|
168
|
+
font-size: 8pt;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
white-space: pre-wrap;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.stats-table {
|
|
174
|
+
width: 100%;
|
|
175
|
+
border-collapse: collapse;
|
|
176
|
+
margin: 2rem 0;
|
|
177
|
+
}
|
|
178
|
+
.stats-table th, .stats-table td {
|
|
179
|
+
border: 1pt solid black;
|
|
180
|
+
padding: 10pt;
|
|
181
|
+
text-align: left;
|
|
182
|
+
font-size: 9pt;
|
|
183
|
+
font-family: 'Inter', sans-serif;
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
186
|
+
</head>
|
|
187
|
+
<body>
|
|
188
|
+
${buildPdfCoverPage({ siteHostname, target: args.target, score, wcagStatus: wcagOverallStatus(totals), coverDate })}
|
|
189
|
+
${buildPdfTableOfContents()}
|
|
190
|
+
${buildPdfExecutiveSummary(args, findings, totals)}
|
|
191
|
+
${buildPdfRiskSection(totals)}
|
|
192
|
+
${buildPdfRemediationRoadmap(findings)}
|
|
193
|
+
${buildPdfMethodologySection(args, findings)}
|
|
194
|
+
${buildPdfIssueSummaryTable(findings)}
|
|
195
|
+
${buildPdfNextSteps(findings, totals)}
|
|
196
|
+
${buildPdfAuditLimitations()}
|
|
197
|
+
</body>
|
|
198
|
+
</html>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The main execution function for the PDF report builder.
|
|
203
|
+
* Uses a headless browser (Playwright) to render the report and save it as a PDF file.
|
|
204
|
+
*/
|
|
205
|
+
async function main() {
|
|
206
|
+
const args = parseArgs(process.argv.slice(2));
|
|
207
|
+
if (!args.output) {
|
|
208
|
+
log.error("Missing required --output flag for PDF report location.");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const inputPayload = readJson(args.input);
|
|
213
|
+
if (!inputPayload) {
|
|
214
|
+
log.error(`Input findings file not found or invalid: ${args.input}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const findings = normalizeFindings(inputPayload).filter(
|
|
219
|
+
(f) => f.wcagClassification !== "AAA" && f.wcagClassification !== "Best Practice",
|
|
220
|
+
);
|
|
221
|
+
const html = buildPdfHtml(args, findings);
|
|
222
|
+
|
|
223
|
+
log.info("Generating professional PDF report...");
|
|
224
|
+
|
|
225
|
+
fs.mkdirSync(path.dirname(args.output), { recursive: true });
|
|
226
|
+
|
|
227
|
+
const browser = await chromium.launch();
|
|
228
|
+
const page = await browser.newPage();
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await page.setContent(html, { waitUntil: "load" });
|
|
232
|
+
await page.evaluate(() => document.fonts.ready);
|
|
233
|
+
|
|
234
|
+
await page.pdf({
|
|
235
|
+
path: args.output,
|
|
236
|
+
format: "A4",
|
|
237
|
+
printBackground: true,
|
|
238
|
+
margin: {
|
|
239
|
+
top: "1cm",
|
|
240
|
+
right: "1cm",
|
|
241
|
+
bottom: "1cm",
|
|
242
|
+
left: "1cm",
|
|
243
|
+
},
|
|
244
|
+
displayHeaderFooter: false,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
log.success(`PDF report generated successfully: ${args.output}`);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
log.error(`Failed to generate PDF: ${error.message}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
} finally {
|
|
252
|
+
await browser.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
main().catch((error) => {
|
|
257
|
+
log.error(`Unhandled PDF Generation Error: ${error.message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file findings.mjs
|
|
3
|
+
* @description Core data normalization and scoring logic for accessibility findings.
|
|
4
|
+
* Provides functions to process raw scanner results into a structured format used
|
|
5
|
+
* across all report types (HTML, Markdown, PDF).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ASSET_PATHS, loadAssetJson } from "../../core/asset-loader.mjs";
|
|
9
|
+
|
|
10
|
+
const SCORING_CONFIG = loadAssetJson(
|
|
11
|
+
ASSET_PATHS.reporting.complianceConfig,
|
|
12
|
+
"assets/reporting/compliance-config.json",
|
|
13
|
+
);
|
|
14
|
+
const RULE_METADATA = loadAssetJson(
|
|
15
|
+
ASSET_PATHS.reporting.wcagReference,
|
|
16
|
+
"assets/reporting/wcag-reference.json",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Defines the priority order for severity levels, where lower values indicate higher priority.
|
|
21
|
+
* @type {Object<string, number>}
|
|
22
|
+
*/
|
|
23
|
+
export const SEVERITY_ORDER = SCORING_CONFIG.severityOrder;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalizes raw scanner finding objects into a consistent structure for reporting.
|
|
28
|
+
* Sorts the result by severity and then by ID.
|
|
29
|
+
* @param {Object} payload - The raw scanner result payload.
|
|
30
|
+
* @param {Object[]} payload.findings - The array of findings to normalize.
|
|
31
|
+
* @returns {Object[]} An array of normalized and sorted finding objects.
|
|
32
|
+
* @throws {Error} If the payload structure is invalid.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeFindings(payload) {
|
|
35
|
+
if (
|
|
36
|
+
!payload ||
|
|
37
|
+
typeof payload !== "object" ||
|
|
38
|
+
!Array.isArray(payload.findings)
|
|
39
|
+
) {
|
|
40
|
+
throw new Error("Input must be a JSON object with a 'findings' array.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return payload.findings
|
|
44
|
+
.map((item, index) => ({
|
|
45
|
+
id: String(item.id ?? `A11Y-${String(index + 1).padStart(3, "0")}`),
|
|
46
|
+
ruleId: String(item.rule_id ?? ""),
|
|
47
|
+
category: item.category ?? null,
|
|
48
|
+
title: String(item.title ?? "Untitled finding"),
|
|
49
|
+
severity: String(item.severity ?? "Unknown"),
|
|
50
|
+
wcag: String(item.wcag ?? ""),
|
|
51
|
+
wcagClassification: item.wcag_classification ?? null,
|
|
52
|
+
area: String(item.area ?? ""),
|
|
53
|
+
url: String(item.url ?? ""),
|
|
54
|
+
selector: String(item.selector ?? ""),
|
|
55
|
+
primarySelector: String(item.primary_selector ?? item.selector ?? ""),
|
|
56
|
+
impactedUsers: String(
|
|
57
|
+
item.impacted_users ?? "Users relying on assistive technology",
|
|
58
|
+
),
|
|
59
|
+
actual: String(item.actual ?? ""),
|
|
60
|
+
primaryFailureMode: item.primary_failure_mode ?? null,
|
|
61
|
+
relationshipHint: item.relationship_hint ?? null,
|
|
62
|
+
failureChecks: Array.isArray(item.failure_checks) ? item.failure_checks : [],
|
|
63
|
+
relatedContext: Array.isArray(item.related_context) ? item.related_context : [],
|
|
64
|
+
expected: String(item.expected ?? ""),
|
|
65
|
+
mdn: item.mdn ?? null,
|
|
66
|
+
fixDescription: item.fix_description ?? null,
|
|
67
|
+
fixCode: item.fix_code ?? null,
|
|
68
|
+
recommendedFix: String(item.recommended_fix ?? item.recommendedFix ?? ""),
|
|
69
|
+
evidence: Array.isArray(item.evidence) ? item.evidence : [],
|
|
70
|
+
totalInstances:
|
|
71
|
+
typeof item.total_instances === "number" ? item.total_instances : null,
|
|
72
|
+
effort: item.effort ?? null,
|
|
73
|
+
relatedRules: Array.isArray(item.related_rules) ? item.related_rules : [],
|
|
74
|
+
fixCodeLang: item.fix_code_lang ?? "html",
|
|
75
|
+
screenshotPath: item.screenshot_path ?? null,
|
|
76
|
+
falsePositiveRisk: item.false_positive_risk ?? null,
|
|
77
|
+
guardrails: item.guardrails ?? null,
|
|
78
|
+
fixDifficultyNotes: item.fix_difficulty_notes ?? null,
|
|
79
|
+
frameworkNotes: item.framework_notes ?? null,
|
|
80
|
+
cmsNotes: item.cms_notes ?? null,
|
|
81
|
+
fileSearchPattern: item.file_search_pattern ?? null,
|
|
82
|
+
ownershipStatus: item.ownership_status ?? "unknown",
|
|
83
|
+
ownershipReason: item.ownership_reason ?? null,
|
|
84
|
+
primarySourceScope: Array.isArray(item.primary_source_scope)
|
|
85
|
+
? item.primary_source_scope
|
|
86
|
+
: [],
|
|
87
|
+
searchStrategy: item.search_strategy ?? "verify_ownership_before_search",
|
|
88
|
+
managedByLibrary: item.managed_by_library ?? null,
|
|
89
|
+
componentHint: item.component_hint ?? null,
|
|
90
|
+
verificationCommand: item.verification_command ?? null,
|
|
91
|
+
verificationCommandFallback: item.verification_command_fallback ?? null,
|
|
92
|
+
pagesAffected: typeof item.pages_affected === "number" ? item.pages_affected : null,
|
|
93
|
+
affectedUrls: Array.isArray(item.affected_urls) ? item.affected_urls : null,
|
|
94
|
+
checkData: item.check_data ?? null,
|
|
95
|
+
}))
|
|
96
|
+
.sort((a, b) => {
|
|
97
|
+
const sa = SEVERITY_ORDER[a.severity] ?? 99;
|
|
98
|
+
const sb = SEVERITY_ORDER[b.severity] ?? 99;
|
|
99
|
+
if (sa !== sb) return sa - sb;
|
|
100
|
+
return a.id.localeCompare(b.id);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Aggregates findings by their severity level.
|
|
106
|
+
* @param {Object[]} findings - The normalized list of findings.
|
|
107
|
+
* @returns {Object<string, number>} An object mapping severity labels to counts.
|
|
108
|
+
*/
|
|
109
|
+
export function buildSummary(findings) {
|
|
110
|
+
const totals = { Critical: 0, Serious: 0, Moderate: 0, Minor: 0 };
|
|
111
|
+
for (const finding of findings) {
|
|
112
|
+
if (finding.severity in totals) totals[finding.severity] += 1;
|
|
113
|
+
}
|
|
114
|
+
return totals;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Calculates a global compliance score (0-100) based on weighted severity counts.
|
|
119
|
+
* @param {Object<string, number>} totals - The summary of counts per severity.
|
|
120
|
+
* @returns {number} An integer compliance score from 0 to 100.
|
|
121
|
+
*/
|
|
122
|
+
export function computeComplianceScore(totals) {
|
|
123
|
+
const { baseScore, penalties } = SCORING_CONFIG.complianceScore;
|
|
124
|
+
const raw =
|
|
125
|
+
baseScore -
|
|
126
|
+
totals.Critical * penalties.Critical -
|
|
127
|
+
totals.Serious * penalties.Serious -
|
|
128
|
+
totals.Moderate * penalties.Moderate -
|
|
129
|
+
totals.Minor * penalties.Minor;
|
|
130
|
+
return Math.max(0, Math.min(100, Math.round(raw)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns a human-readable performance label (grade) based on the compliance score.
|
|
135
|
+
* @param {number} score - The calculated compliance score.
|
|
136
|
+
* @returns {string} A label like "Excellent", "Good", "Fair", "Poor", or "Critical".
|
|
137
|
+
*/
|
|
138
|
+
export function scoreLabel(score) {
|
|
139
|
+
for (const { min, label } of SCORING_CONFIG.gradeThresholds) {
|
|
140
|
+
if (score >= min) return label;
|
|
141
|
+
}
|
|
142
|
+
return "Critical";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function wcagOverallStatus(totals) {
|
|
146
|
+
if (totals.Critical > 0 || totals.Serious > 0) return "Fail";
|
|
147
|
+
if (totals.Moderate > 0 || totals.Minor > 0) return "Conditional Pass";
|
|
148
|
+
return "Pass";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Analyzes findings to determine the unique number of issues impacting specific user personas.
|
|
153
|
+
* @param {Object[]} findings - The normalized list of findings.
|
|
154
|
+
* @returns {Object} An object containing impact counts for screenReader, keyboard, vision, and cognitive personas.
|
|
155
|
+
*/
|
|
156
|
+
export function buildPersonaSummary(findings) {
|
|
157
|
+
const SMART_MAP = RULE_METADATA.personaMapping;
|
|
158
|
+
|
|
159
|
+
const uniqueIssues = {};
|
|
160
|
+
for (const persona of Object.keys(SMART_MAP)) {
|
|
161
|
+
uniqueIssues[persona] = new Set();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const f of findings) {
|
|
165
|
+
const users = (f.impactedUsers || "").toLowerCase();
|
|
166
|
+
const ruleId = (f.ruleId || "").toLowerCase();
|
|
167
|
+
const title = (f.title || "").toLowerCase();
|
|
168
|
+
const issueKey = f.ruleId || f.title;
|
|
169
|
+
|
|
170
|
+
Object.keys(SMART_MAP).forEach((persona) => {
|
|
171
|
+
const { rules, keywords } = SMART_MAP[persona];
|
|
172
|
+
const matchRule = rules.some((r) => ruleId.includes(r.toLowerCase()));
|
|
173
|
+
const matchKeyword = keywords.some(
|
|
174
|
+
(k) => users.includes(k) || title.includes(k),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (matchRule || matchKeyword) {
|
|
178
|
+
uniqueIssues[persona].add(issueKey);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = {};
|
|
184
|
+
for (const persona of Object.keys(SMART_MAP)) {
|
|
185
|
+
result[persona] = uniqueIssues[persona].size;
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|