vital-core / src /lib /consensus.js
vital-core sync
Deploy snapshot for Hugging Face Spaces
50841a0
Raw
History Blame Contribute Delete
3.29 kB
import { canonicalRuleKey, actRuleIdsFor } from './act.js';
/**
* Cross-engine consensus. axe and Alfa both implement W3C ACT rules, so
* the same issue is often reported by both — once you'd naively sum axe
* + Alfa counts, you'd show ~2x the real number. This consolidates by:
*
* primary: the shared ACT rule (axe "image-alt" and Alfa "sia-r2"
* are ACT 23a2a8 -> one canonical issue), and
* secondary: the affected page URL.
*
* An issue = (canonical rule, page). It's "consensus" when both engines
* flagged that canonical rule on that page, else axe-only or alfa-only.
* Unmapped rules keep their own key, so they're never wrongly merged.
*
* Selectors differ between engines (each emits its own), so we correlate
* at canonical-rule + page granularity rather than forcing selector
* equality — that's the reliable signal that "both engines agree".
*/
/**
* Build a consensus summary from the per-rule affectedPages already
* collected in the weekly summary.
*
* axeRules / alfaRules: { ruleId: { affectedPages: [{url, instances}], ... } }
*
* Returns:
* {
* uniqueIssues, // distinct (canonical rule, page) pairs
* consensus, // flagged by BOTH engines
* axeOnly, alfaOnly,
* rawAxe, rawAlfa, // naive per-engine (rule,page) counts, for contrast
* byKey: { canonicalKey: { actRuleId, axeRules:Set, alfaRules:Set, pages:Set, engines } }
* }
*/
export function buildConsensus(axeRules = {}, alfaRules = {}) {
// canonicalKey -> { actRuleId, axeRules:Set, alfaRules:Set,
// pagesAxe:Set, pagesAlfa:Set }
const groups = new Map();
let rawAxe = 0;
let rawAlfa = 0;
const ingest = (rules, engineKey) => {
for (const [ruleId, rule] of Object.entries(rules)) {
const key = canonicalRuleKey(engineKey, ruleId);
const act = actRuleIdsFor(engineKey, ruleId)[0] ?? null;
let g = groups.get(key);
if (!g) {
g = { actRuleId: act, axeRules: new Set(), alfaRules: new Set(), pagesAxe: new Set(), pagesAlfa: new Set() };
groups.set(key, g);
}
if (engineKey === 'axe-core') g.axeRules.add(ruleId);
else g.alfaRules.add(ruleId);
for (const p of rule.affectedPages ?? []) {
if (engineKey === 'axe-core') { g.pagesAxe.add(p.url); rawAxe++; }
else { g.pagesAlfa.add(p.url); rawAlfa++; }
}
}
};
ingest(axeRules, 'axe-core');
ingest(alfaRules, 'alfa');
let consensus = 0;
let axeOnly = 0;
let alfaOnly = 0;
let uniqueIssues = 0;
const byKey = {};
for (const [key, g] of groups) {
const allPages = new Set([...g.pagesAxe, ...g.pagesAlfa]);
for (const page of allPages) {
uniqueIssues++;
const inAxe = g.pagesAxe.has(page);
const inAlfa = g.pagesAlfa.has(page);
if (inAxe && inAlfa) consensus++;
else if (inAxe) axeOnly++;
else alfaOnly++;
}
byKey[key] = {
actRuleId: g.actRuleId,
axeRules: [...g.axeRules],
alfaRules: [...g.alfaRules],
pages: allPages.size,
engines: g.axeRules.size && g.alfaRules.size ? 'both' : g.axeRules.size ? 'axe' : 'alfa',
};
}
return {
uniqueIssues,
consensus,
axeOnly,
alfaOnly,
rawAxe,
rawAlfa,
byKey,
};
}