code wiki / (root) / nx_a11y_check.nx

nx_a11y_check.nx source

↩ module page · 329 lines · 11626 B

1// nx_a11y_check.nx -- Layer-3 WCAG 2.2 AA accessibility grader. 2// 3// Enforces the ADA-compliance cardinal 4// feedback-ada-compliance-equal-experiences (2026-05-15): 5// 6// "the adc [ADA] is another thing we want compliance on so disabled 7// people have equal experiences" 8// 9// Scans HTML source (either a standalone .html file or the HTML 10// payload string inside a NishiLang web file) for the most 11// frequently-violated WCAG 2.2 success criteria. This first cut 12// covers three high-impact criteria that are reliably detectable via 13// lexical scan: 14// 15// 1.1.1 Non-text Content -- every <img> must carry an alt= attr 16// 1.4.4 Resize Text -- font-size must not use px 17// 2.4.4 Link Purpose -- avoid vague link text ("click here", 18// "here", "read more", "more") 19// 20// Future criteria (queued for IR + state-machine scan): 21// 1.4.3 contrast (needs CSS resolution) 22// 2.1.1 keyboard nav (needs JS + handler scan) 23// 3.3.2 form labels (needs cross-element pairing) 24// 4.1.2 name/role/value (needs ARIA-aware scan) 25// 26// Verdict shape mirrors nx_license_check: per-criterion violation 27// count, per-criterion sealed verdict (WIN/LOSE/UNMEASURED), and an 28// overall WIN/LOSE that the polyangulated grader can consume as an 29// 8th perspective. 30// 31// license_tier: ORIGINAL 32// genealogy_id: international-research-sources/w3c/wcag22_aa 33// 34// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 35// intended_use: "Layer-3 WCAG 2.2 AA accessibility grader -- 36// enforces ADA-compliance cardinal so disabled 37// users have equal experiences across substrate- 38// emitted HTML / web frontends." 39// sil_target: SIL2 (accessibility-as-safety: ADA non- 40// compliance can prevent disabled 41// users from operating safety-critical 42// systems; classifies as injury under 43// the cardinal interpretation) 44// asil_target: QM 45// dal_target: NONE 46// iec_62304_class: NONE 47// evidence: [no_floating_point, sealed_NX_A11Y_VERDICT, 48// sealed_NX_WCAG_criterion_enum_complete_3, 49// per_criterion_violation_count_tracked, 50// W3C_WCAG_2_2_AA_open_spec_basis] 51// hazard_register: [bug-tape-WCAG_1_1_1_img_no_alt, 52// bug-tape-WCAG_1_4_4_px_font_size, 53// bug-tape-WCAG_2_4_4_vague_link_text, 54// bug-tape-lexical-scan-misses-DOM-context] 55// residual_risk: "First-cut covers 3 of 50+ WCAG 2.2 AA 56// success criteria. Contrast (1.4.3), 57// keyboard nav (2.1.1), form labels (3.3.2), 58// ARIA (4.1.2) require IR + state-machine scan 59// which is queued. GRADER reports UNMEASURED 60// for those, never silently PASS." 61// verdict: NOT_YET_EVALUATED 62 63import "nx_syscalls.nx" 64import "nx_runtime.nx" 65import "nx_types.nx" 66import "nx_tier.nx" 67 68// ---- Sealed verdict (mirror of nx_license_verdict) ------------------ 69 70const NX_A11Y_VERDICT_UNMEASURED: nx_int = 0 71const NX_A11Y_VERDICT_WIN: nx_int = 1 72const NX_A11Y_VERDICT_LOSE: nx_int = 2 73const NX_A11Y_VERDICT_TIE: nx_int = 3 74 75// ---- Sealed enum: WCAG 2.2 success criterion ------------------------ 76 77const NX_WCAG_1_1_1_TEXT_ALT: nx_int = 0 78const NX_WCAG_1_4_4_RESIZE: nx_int = 1 79const NX_WCAG_2_4_4_LINK_PURPOSE: nx_int = 2 80const NX_WCAG_CRIT_N: nx_int = 3 81 82func nx_wcag_criterion_is_valid(c: nx_int) -> nx_int { 83 if c < 0 { return 0 } 84 if c >= NX_WCAG_CRIT_N { return 0 } 85 return 1 86} 87 88// ---- Per-criterion report struct ------------------------------------ 89// 90// Fixed-stride so the polyangulated grader can pack an array. 91 92struct A11yCritReport { 93 criterion: nx_int, 94 violation_count: nx_int, 95 verdict: nx_int, 96} 97 98struct A11yCard { 99 crit_text_alt: A11yCritReport, 100 crit_resize: A11yCritReport, 101 crit_link_purpose: A11yCritReport, 102 n_violations_total: nx_int, 103 n_lose: nx_int, 104 n_win: nx_int, 105 overall_verdict: nx_int, 106} 107 108// ---- Byte helpers (start-of-line not required; HTML scan is global) - 109 110func _a_byte_at(src: *u8, i: nx_int) -> nx_int { 111 let p: *u8 = (src as nx_int + i) as *u8 112 let b: nx_int = p[0] as nx_int 113 return b & 255 114} 115 116func _a_lower(c: nx_int) -> nx_int { 117 if c >= 65 { 118 if c <= 90 { return c + 32 } 119 } 120 return c 121} 122 123func _a_eq_ci(src: *u8, src_len: nx_int, off: nx_int, needle: *u8, needle_len: nx_int) -> nx_int { 124 var k: nx_int = 0 125 if off + needle_len > src_len { return 0 } 126 while k < needle_len { 127 let a: nx_int = _a_lower(_a_byte_at(src, off + k)) 128 let b: nx_int = _a_lower(_a_byte_at(needle, k)) 129 if a != b { return 0 } 130 k = k + 1 131 } 132 return 1 133} 134 135func _a_is_digit(c: nx_int) -> nx_int { 136 if c < 48 { return 0 } 137 if c > 57 { return 0 } 138 return 1 139} 140 141// ---- WCAG 1.1.1: <img> without alt= --------------------------------- 142// 143// Scan strategy: for each `<img` occurrence, look forward to the next 144// `>` and check that an `alt=` attribute appears within the tag. 145 146func _count_img_without_alt(src: *u8, src_len: nx_int) -> nx_int { 147 var i: nx_int = 0 148 var bad: nx_int = 0 149 let img_tag: *u8 = "<img" as *u8 150 let alt_attr: *u8 = "alt=" as *u8 151 while i < src_len - 4 { 152 if _a_eq_ci(src, src_len, i, img_tag, 4) == 1 { 153 // Find tag end (next `>`). 154 var j: nx_int = i + 4 155 var has_alt: nx_int = 0 156 while j < src_len { 157 let c: nx_int = _a_byte_at(src, j) 158 if c == 62 { j = src_len + 1; } 159 else { 160 if _a_eq_ci(src, src_len, j, alt_attr, 4) == 1 { has_alt = 1 } 161 j = j + 1 162 } 163 } 164 if has_alt == 0 { bad = bad + 1 } 165 i = i + 4 166 } else { 167 i = i + 1 168 } 169 } 170 return bad 171} 172 173// ---- WCAG 1.4.4: font-size in px ------------------------------------ 174// 175// Scan for `font-size:` followed by a number and `px`. 176 177func _skip_ws_and_digits(src: *u8, src_len: nx_int, off: nx_int) -> nx_int { 178 var j: nx_int = off 179 var saw_digit: nx_int = 0 180 while j < src_len { 181 let c: nx_int = _a_byte_at(src, j) 182 if c == 32 { 183 j = j + 1 184 } else { 185 if _a_is_digit(c) == 1 { 186 saw_digit = 1 187 j = j + 1 188 } else { 189 if saw_digit == 1 { return j } 190 return -1 191 } 192 } 193 } 194 return -1 195} 196 197func _count_px_font(src: *u8, src_len: nx_int) -> nx_int { 198 var i: nx_int = 0 199 var bad: nx_int = 0 200 let fs_tag: *u8 = "font-size:" as *u8 201 let px_str: *u8 = "px" as *u8 202 while i < src_len - 10 { 203 if _a_eq_ci(src, src_len, i, fs_tag, 10) == 1 { 204 let after: nx_int = _skip_ws_and_digits(src, src_len, i + 10) 205 if after >= 0 { 206 if _a_eq_ci(src, src_len, after, px_str, 2) == 1 { 207 bad = bad + 1 208 } 209 } 210 i = i + 10 211 } else { 212 i = i + 1 213 } 214 } 215 return bad 216} 217 218// ---- WCAG 2.4.4: vague link text ------------------------------------ 219// 220// Scan for the literal phrases ">click here<", ">here<", ">read more<", 221// ">more<". Conservative -- catches the obvious offenders, may miss 222// case-variants with whitespace inside the > < bracket pair (left for 223// a future refinement). 224 225func _count_vague_link(src: *u8, src_len: nx_int) -> nx_int { 226 var i: nx_int = 0 227 var bad: nx_int = 0 228 let p1: *u8 = ">click here<" as *u8 229 let p2: *u8 = ">here<" as *u8 230 let p3: *u8 = ">read more<" as *u8 231 let p4: *u8 = ">more<" as *u8 232 while i < src_len { 233 if _a_eq_ci(src, src_len, i, p1, 12) == 1 { bad = bad + 1; i = i + 12 } 234 else { 235 if _a_eq_ci(src, src_len, i, p2, 6) == 1 { bad = bad + 1; i = i + 6 } 236 else { 237 if _a_eq_ci(src, src_len, i, p3, 11) == 1 { bad = bad + 1; i = i + 11 } 238 else { 239 if _a_eq_ci(src, src_len, i, p4, 6) == 1 { bad = bad + 1; i = i + 6 } 240 else { i = i + 1 } 241 } 242 } 243 } 244 } 245 return bad 246} 247 248// ---- Verdict per criterion ----------------------------------------- 249 250func _verdict_from_count(n: nx_int) -> nx_int { 251 if n == 0 { return NX_A11Y_VERDICT_WIN } 252 return NX_A11Y_VERDICT_LOSE 253} 254 255// ---- Scan entry: bytes -> A11yCard --------------------------------- 256 257func nx_a11y_scan(src: *u8, src_len: nx_int, card: *A11yCard) -> nx_int { 258 let n1: nx_int = _count_img_without_alt(src, src_len) 259 let n2: nx_int = _count_px_font(src, src_len) 260 let n3: nx_int = _count_vague_link(src, src_len) 261 262 card.crit_text_alt.criterion = NX_WCAG_1_1_1_TEXT_ALT 263 card.crit_text_alt.violation_count = n1 264 card.crit_text_alt.verdict = _verdict_from_count(n1) 265 266 card.crit_resize.criterion = NX_WCAG_1_4_4_RESIZE 267 card.crit_resize.violation_count = n2 268 card.crit_resize.verdict = _verdict_from_count(n2) 269 270 card.crit_link_purpose.criterion = NX_WCAG_2_4_4_LINK_PURPOSE 271 card.crit_link_purpose.violation_count = n3 272 card.crit_link_purpose.verdict = _verdict_from_count(n3) 273 274 let total: nx_int = n1 + n2 + n3 275 card.n_violations_total = total 276 277 var lose: nx_int = 0 278 var win: nx_int = 0 279 if card.crit_text_alt.verdict == NX_A11Y_VERDICT_LOSE { lose = lose + 1 } 280 if card.crit_resize.verdict == NX_A11Y_VERDICT_LOSE { lose = lose + 1 } 281 if card.crit_link_purpose.verdict == NX_A11Y_VERDICT_LOSE { lose = lose + 1 } 282 if card.crit_text_alt.verdict == NX_A11Y_VERDICT_WIN { win = win + 1 } 283 if card.crit_resize.verdict == NX_A11Y_VERDICT_WIN { win = win + 1 } 284 if card.crit_link_purpose.verdict == NX_A11Y_VERDICT_WIN { win = win + 1 } 285 card.n_lose = lose 286 card.n_win = win 287 if lose == 0 { 288 card.overall_verdict = NX_A11Y_VERDICT_WIN 289 } else { 290 card.overall_verdict = NX_A11Y_VERDICT_LOSE 291 } 292 return card.overall_verdict 293} 294 295// ---- File-level helper --------------------------------------------- 296 297func nx_a11y_scan_file(path: *u8, card: *A11yCard) -> nx_int { 298 let len_raw: *u8 = sys_mmap(16) 299 let len_p: *i64 = len_raw as *i64 300 let src: *u8 = sys_read_file(path, len_p) 301 if src == (0 as *u8) { 302 card.n_violations_total = 0 303 card.overall_verdict = NX_A11Y_VERDICT_UNMEASURED 304 return NX_A11Y_VERDICT_UNMEASURED 305 } 306 let src_len: nx_int = len_p[0] 307 return nx_a11y_scan(src, src_len, card) 308} 309 310// ---- Named-improvement string per criterion ------------------------ 311 312func nx_a11y_named_improvement(c: nx_int) -> *u8 { 313 if c == NX_WCAG_1_1_1_TEXT_ALT { 314 return "WCAG 1.1.1: every <img> must carry alt= -- generate alt text from scene-graph semantics" as *u8 315 } 316 if c == NX_WCAG_1_4_4_RESIZE { 317 return "WCAG 1.4.4: replace font-size px with rem/em so users can resize text" as *u8 318 } 319 if c == NX_WCAG_2_4_4_LINK_PURPOSE { 320 return "WCAG 2.4.4: replace vague link text (\"click here\" / \"more\") with descriptive text" as *u8 321 } 322 return "" as *u8 323} 324 325func nx_a11y_card_alloc() -> *A11yCard { 326 let raw: *u8 = sys_mmap(256) 327 let c: *A11yCard = raw as *A11yCard 328 return c 329}