const DEFAULT_REPORTING = {
max_html_issues: 50,
moderate_issue_threshold_percent: 5,
include_key_page_issues: true,
};
const SEVERITY_RANK = { Critical: 0, High: 1, Medium: 2, Low: 3 };
export function normalizeAccessibilityReporting(reporting = {}) {
return {
...DEFAULT_REPORTING,
...reporting,
max_html_issues: Math.max(0, Number(reporting.max_html_issues ?? DEFAULT_REPORTING.max_html_issues) || 0),
moderate_issue_threshold_percent: Math.max(
0,
Number(reporting.moderate_issue_threshold_percent ?? DEFAULT_REPORTING.moderate_issue_threshold_percent) || 0
),
include_key_page_issues: reporting.include_key_page_issues ?? DEFAULT_REPORTING.include_key_page_issues,
};
}
export function prioritizeAccessibilityBugs(summary, bugs, { keyPages = [], reporting = {} } = {}) {
const cfg = normalizeAccessibilityReporting(reporting);
const keyPageSet = cfg.include_key_page_issues ? new Set(keyPages) : new Set();
const totalPages = summary.pagesScanned || bugs[0]?.frequency.total_pages_scanned || 0;
const threshold = cfg.moderate_issue_threshold_percent;
const decorated = bugs.map((bug) => {
const pagesAffected = bug.frequency?.pages_affected ?? 0;
const prevalencePercent = totalPages > 0 ? (100 * pagesAffected) / totalPages : 0;
const keyPageHit = keyPageSet.size > 0 && bugHasKeyPageHit(summary, bug, keyPageSet);
const tier = priorityTier(bug, prevalencePercent, keyPageHit, threshold, cfg.include_key_page_issues);
return {
...bug,
priority_tier: tier,
priority_key_page: keyPageHit,
priority_prevalence_percent: Math.round(prevalencePercent * 100) / 100,
};
});
decorated.sort((a, b) =>
a.priority_tier - b.priority_tier ||
(b.frequency?.pages_affected ?? 0) - (a.frequency?.pages_affected ?? 0) ||
(SEVERITY_RANK[a.severity] ?? 99) - (SEVERITY_RANK[b.severity] ?? 99) ||
(b.frequency?.instances ?? 0) - (a.frequency?.instances ?? 0) ||
String(a.summary ?? '').localeCompare(String(b.summary ?? ''))
);
const visible = [];
for (const bug of decorated) {
if (bug.priority_tier <= 1) visible.push(bug);
}
for (const bug of decorated) {
if (bug.priority_tier <= 1) continue;
if (cfg.max_html_issues > 0 && visible.length >= cfg.max_html_issues) break;
visible.push(bug);
}
const visibleSet = new Set(visible.map((b) => b.instance_id));
return {
bugs: decorated.map((bug) => ({
...bug,
default_visible: visibleSet.has(bug.instance_id),
})),
visibleCount: visibleSet.size,
totalCount: bugs.length,
reporting: cfg,
};
}
function priorityTier(bug, prevalencePercent, keyPageHit, thresholdPercent, includeKeyPages) {
if (bug.severity === 'Critical' || bug.severity === 'High') return 0;
if (includeKeyPages && keyPageHit && prevalencePercent > thresholdPercent) return 1;
if (isWcagAorAa(bug.wcag_category)) return 2;
if (bug.wcag_category === 'Best Practice') return 3;
if (bug.wcag_category === 'WCAG 2.x AAA') return 4;
return 5;
}
function isWcagAorAa(category) {
return /^WCAG \d\.\d [A]{1,2}$/.test(category ?? '');
}
function bugHasKeyPageHit(summary, bug, keyPageSet) {
const rule = ruleForBug(summary, bug);
const affected = rule?.affectedPages?.map((p) => p.url).filter(Boolean) ?? bug.affected_pages ?? [];
return affected.some((url) => keyPageSet.has(url));
}
function ruleForBug(summary, bug) {
const rules = {
'axe-core': summary.axe?.rules,
alfa: summary.alfa?.rules,
'deprecated-html': summary.deprecatedHtml?.rules,
}[bug.engine_key];
return rules?.[bug.rule_id] ?? null;
}