vital-core / src /lib /fpc.js
vital-core sync
Deploy snapshot for Hugging Face Spaces
50841a0
Raw
History Blame Contribute Delete
5.08 kB
/**
* 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)}%`;
}