/** * Human-impact modeling for accessibility findings. * * Maps each WCAG 2.x success criterion to the Section 508 Functional * Performance Criteria (FPC) it relates to, and each FPC to a disability * group with a US population prevalence estimate. This lets a finding say * *who* it affects and roughly *how many people*, instead of an opaque * "requires manual testing". * * Sources: * - WCAG -> FPC mapping: GSA Section 508 program, * https://www.section508.gov/develop/mapping-wcag-to-fpc/ (extended to * WCAG 2.2 criteria here). FPC categories also align with EU EN 301 549 * Table B.2. * - Prevalence: US Census Bureau American Community Survey (ACS) 2022 * 1-Year Estimates (disability tables), with supplemental CDC/NIDCD/NEI * figures. These are population-level estimates, not site analytics — * use them to convey scale, not as precise measurements. * * Approach borrowed from mgifford/daily-dap. */ // FPC code -> disability group + US prevalence (fraction of population). export const FPC = { WV: { code: 'WV', group: 'Without vision', prevalence: 0.010 }, LV: { code: 'LV', group: 'Limited vision', prevalence: 0.024 }, WPC: { code: 'WPC', group: 'Without color perception', prevalence: 0.045 }, WH: { code: 'WH', group: 'Without hearing', prevalence: 0.003 }, LH: { code: 'LH', group: 'Limited hearing', prevalence: 0.035 }, WS: { code: 'WS', group: 'Without speech', prevalence: 0.005 }, LM: { code: 'LM', group: 'Limited manipulation/strength', prevalence: 0.130 }, LR: { code: 'LR', group: 'Limited reach', prevalence: 0.020 }, LCLA: { code: 'LCLA', group: 'Limited cognitive, language, or learning ability', prevalence: 0.110 }, }; // WCAG success criterion -> the FPC codes it most directly affects. // Covers WCAG 2.0/2.1/2.2 Level A and AA. A criterion with no entry // returns an empty impact (treated as undetermined rather than guessed). const SC_TO_FPC = { // 1.1 Text Alternatives '1.1.1': ['WV', 'LV', 'LCLA'], // 1.2 Time-based Media '1.2.1': ['WV', 'WH'], '1.2.2': ['WH', 'LH'], '1.2.3': ['WV', 'LV'], '1.2.4': ['WH', 'LH'], '1.2.5': ['WV', 'LV'], // 1.3 Adaptable '1.3.1': ['WV', 'LV', 'LCLA'], '1.3.2': ['WV', 'LCLA'], '1.3.3': ['WV', 'LV', 'LCLA'], '1.3.4': ['LV', 'LM'], '1.3.5': ['LCLA', 'LM'], // 1.4 Distinguishable '1.4.1': ['WPC', 'LV'], '1.4.2': ['WV', 'LV'], '1.4.3': ['LV', 'WPC'], '1.4.4': ['LV'], '1.4.5': ['LV', 'WPC'], '1.4.10': ['LV', 'LM'], '1.4.11': ['LV', 'WPC'], '1.4.12': ['LV', 'LCLA'], '1.4.13': ['LV', 'LM', 'LCLA'], // 2.1 Keyboard Accessible '2.1.1': ['WV', 'LM', 'LR'], '2.1.2': ['WV', 'LM', 'LR'], '2.1.4': ['WV', 'LM'], // 2.2 Enough Time '2.2.1': ['LM', 'LR', 'LCLA'], '2.2.2': ['LV', 'LCLA'], // 2.3 Seizures '2.3.1': ['LCLA'], // 2.4 Navigable '2.4.1': ['WV', 'LM'], '2.4.2': ['WV', 'LV', 'LCLA'], '2.4.3': ['WV', 'LM'], '2.4.4': ['WV', 'LV', 'LCLA'], '2.4.5': ['WV', 'LV', 'LCLA'], '2.4.6': ['WV', 'LV', 'LCLA'], '2.4.7': ['LV', 'LM'], '2.4.11': ['LV', 'LM'], // 2.5 Input Modalities '2.5.1': ['LM', 'LR'], '2.5.2': ['LM', 'LR'], '2.5.3': ['WV', 'WS'], '2.5.4': ['LM', 'LR'], '2.5.7': ['LM', 'LR'], '2.5.8': ['LM', 'LR', 'LV'], // 3.1 Readable '3.1.1': ['WV', 'LCLA'], '3.1.2': ['WV', 'LCLA'], // 3.2 Predictable '3.2.1': ['WV', 'LV', 'LCLA'], '3.2.2': ['WV', 'LV', 'LCLA'], '3.2.3': ['LV', 'LCLA'], '3.2.4': ['LV', 'LCLA'], '3.2.6': ['LCLA'], // 3.3 Input Assistance '3.3.1': ['WV', 'LV', 'LCLA'], '3.3.2': ['WV', 'LV', 'LCLA'], '3.3.3': ['WV', 'LV', 'LCLA'], '3.3.4': ['LCLA'], '3.3.7': ['LCLA', 'LM'], '3.3.8': ['LCLA', 'LM'], // 4.1 Compatible '4.1.1': ['WV', 'LV'], '4.1.2': ['WV', 'LV', 'LM'], '4.1.3': ['WV', 'LV'], }; /** * Impact for a WCAG success criterion: the disability groups affected, * each with prevalence. Returns { groups: [{code, group, prevalence}], * maxPrevalence } or null when the SC is unmapped/unknown. */ export function impactFor(sc) { const codes = SC_TO_FPC[sc]; if (!codes || codes.length === 0) return null; const groups = codes.map((c) => FPC[c]).filter(Boolean); if (groups.length === 0) return null; return { groups, // The single most-affected group's prevalence — a conservative // headline figure (groups overlap, so we don't sum them). maxPrevalence: Math.max(...groups.map((g) => g.prevalence)), }; } /** * Optional people-excluded estimate, when page-load data is supplied. * prevalence: fraction (0-1) * pageLoads: loads attributable to the affected pages * Rough by design — meant to convey scale, not precision. */ export function estimateExcluded(prevalence, pageLoads) { if (!pageLoads || pageLoads <= 0) return null; return Math.round(prevalence * pageLoads); } /** Format a prevalence fraction as a percent string, e.g. 0.024 -> "2.4%". */ export function pct(fraction) { return `${(fraction * 100).toFixed(1)}%`; }