code wiki / _hdl_build / nx_ux_study.nx

nx_ux_study.nx source

↩ module page · 267 lines · 12135 B

1// nx_ux_study.nx -- the Nielsen Norman Group "Usability Testing 101" method encoded as a sovereign DATA 2// STRUCTURE + an HONEST scoring engine. This is the anti-navel-gazing spine the operator asked for: it 3// turns "is the feature PRESENT?" (a sticker) into "can a representative user complete a realistic TASK, 4// and how SEVERE are the findings?" (rigor). A study cannot grade PASS if any task FAILED or any finding 5// is major-or-worse -- feature presence is irrelevant to the grade. 6// 7// GROUNDING (rule 4): the method is NN/g's Usability (User) Testing 101 by Kate Moran 8// (nngroup.com/articles/usability-testing-101, pub 2019-12-01; title/author/description verbatim-captured 9// this session). The three components (facilitator, tasks, participant), the quantitative metrics 10// (task success rate, time-on-task, error rate), the 10 Nielsen heuristics, and the 0..4 severity scale 11// are the canonical, stable NN/g framework. license_tier: ORIGINAL 12import "nx_syscalls.nx" 13 14// ---- struct byte sizes (no sizeof in NishiLang; count fields * 8) ---- 15const UX_TASK_BYTES: i64 = 48 // 6 i64/ptr fields 16const UX_FINDING_BYTES: i64 = 40 // 5 i64/ptr fields 17const UX_STUDY_BYTES: i64 = 72 // 9 i64/ptr fields 18 19// ---- task outcome (a participant either completes the task or does not) ---- 20const UX_FAIL: i64 = 0 21const UX_PARTIAL: i64 = 1 22const UX_SUCCESS: i64 = 2 23 24// ---- NN/g finding severity 0..4: 0 not-a-problem, 1 cosmetic, 2 minor, 3 major, 4 catastrophe. 25// Rated by frequency x impact x persistence. >= UX_SEV_MAJOR blocks a PASS grade. ---- 26const UX_SEV_MINOR: i64 = 2 27const UX_SEV_MAJOR: i64 = 3 28 29// ---- grade codes ---- 30const UX_GRADE_FAIL: i64 = 0 31const UX_GRADE_COND: i64 = 1 // conditional: usable but defects remain 32const UX_GRADE_PASS: i64 = 2 33 34// ---- test surfaces (page/game/media rendered where + how): extensible ---- 35const UX_SURF_NISHI_DESKTOP: i64 = 0 36const UX_SURF_NISHI_MOBILE: i64 = 1 37const UX_SURF_CHROME_DESKTOP: i64 = 2 38const UX_SURF_CHROME_MOBILE: i64 = 3 39 40// ---- a realistic user TASK + the OBSERVED outcome (the heart of usability testing) ---- 41struct UxTask { 42 id: i64, 43 desc: *u8, // the realistic user goal ("read the verdict", "reach the Play button") 44 crit: *u8, // success criterion (what "done" objectively means) 45 outcome: i64, // UX_FAIL / UX_PARTIAL / UX_SUCCESS -- observed, not assumed 46 time_frames: i64, // time-on-task, measured in recording frames (grounded, not guessed) 47 n_errors: i64, // errors the participant hit during the task 48} 49 50// ---- a FINDING: a usability problem tied to a heuristic + severity + FRAME evidence + recommendation ---- 51struct UxFinding { 52 heuristic_id: i64, // which of Nielsen's 10 heuristics it violates 53 severity: i64, // 0..4 NN/g scale 54 desc: *u8, // what is wrong 55 evidence: i64, // recording frame # that shows it (so a finding is never a claim, it is a pixel) 56 rec: *u8, // recommendation (what to do about it) 57} 58 59// ---- the STUDY: goal + artifact + surface + study-type + the tasks + the findings ---- 60struct UxStudy { 61 goal: *u8, 62 artifact: *u8, // URL / path of the thing under test 63 surface: i64, // UX_SURF_* 64 quantitative: i64, // 1 quantitative (metrics), 0 qualitative 65 moderated: i64, // 1 moderated (facilitator present), 0 unmoderated (self-driven bot) 66 tasks: *UxTask, 67 n_tasks: i64, 68 findings: *UxFinding, 69 n_findings: i64, 70} 71 72// ---------- small print helpers ---------- 73func ux_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 74func ux_n(v: i64) -> i64 { 75 let t: *u8 = sys_mmap(24 as nx_size) 76 var m: i64 = v 77 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 78 var k: i64 = 0 79 if m == 0 { t[0] = 48 as u8; k = 1 } 80 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 81 let b: *u8 = sys_mmap(24 as nx_size) 82 var j: i64 = 0 83 while j < k { b[j] = t[k - 1 - j]; j = j + 1 } 84 sys_write(1, b, k) 85 return 0 86} 87 88// ---------- array accessors ---------- 89func ux_task(s: *UxStudy, i: i64) -> *UxTask { 90 return (s.tasks as *u8 + (i as nx_size) * (UX_TASK_BYTES as nx_size)) as *UxTask 91} 92func ux_finding(s: *UxStudy, i: i64) -> *UxFinding { 93 return (s.findings as *u8 + (i as nx_size) * (UX_FINDING_BYTES as nx_size)) as *UxFinding 94} 95 96// ---------- builders ---------- 97func ux_set_task(s: *UxStudy, i: i64, desc: *u8, crit: *u8, outcome: i64, tf: i64, ne: i64) -> i64 { 98 let t: *UxTask = ux_task(s, i) 99 t.id = i + 1 100 t.desc = desc 101 t.crit = crit 102 t.outcome = outcome 103 t.time_frames = tf 104 t.n_errors = ne 105 return 0 106} 107func ux_set_finding(s: *UxStudy, i: i64, h: i64, sev: i64, desc: *u8, ev: i64, rec: *u8) -> i64 { 108 let f: *UxFinding = ux_finding(s, i) 109 f.heuristic_id = h 110 f.severity = sev 111 f.desc = desc 112 f.evidence = ev 113 f.rec = rec 114 return 0 115} 116 117// ---------- Nielsen's 10 heuristics (the evaluation rubric; names verbatim) ---------- 118func ux_heuristic_name(id: i64) -> i64 { 119 if id == 1 { ux_w("Visibility of system status" as *u8); return 0 } 120 if id == 2 { ux_w("Match between system and the real world" as *u8); return 0 } 121 if id == 3 { ux_w("User control and freedom" as *u8); return 0 } 122 if id == 4 { ux_w("Consistency and standards" as *u8); return 0 } 123 if id == 5 { ux_w("Error prevention" as *u8); return 0 } 124 if id == 6 { ux_w("Recognition rather than recall" as *u8); return 0 } 125 if id == 7 { ux_w("Flexibility and efficiency of use" as *u8); return 0 } 126 if id == 8 { ux_w("Aesthetic and minimalist design" as *u8); return 0 } 127 if id == 9 { ux_w("Help users recognize, diagnose, and recover from errors" as *u8); return 0 } 128 if id == 10 { ux_w("Help and documentation" as *u8); return 0 } 129 ux_w("(unknown heuristic)" as *u8) 130 return 0 131} 132 133// ---------- scoring engine (metrics) ---------- 134// task success rate as a percentage (0..100). Only full SUCCESS counts -- PARTIAL is not success. 135func ux_task_success_rate(s: *UxStudy) -> i64 { 136 if s.n_tasks == 0 { return 0 } 137 var ok: i64 = 0 138 var i: i64 = 0 139 while i < s.n_tasks { 140 let t: *UxTask = ux_task(s, i) 141 if t.outcome == UX_SUCCESS { ok = ok + 1 } 142 i = i + 1 143 } 144 return (ok * 100) / s.n_tasks 145} 146func ux_any_task_fail(s: *UxStudy) -> i64 { 147 var i: i64 = 0 148 while i < s.n_tasks { 149 let t: *UxTask = ux_task(s, i) 150 if t.outcome == UX_FAIL { return 1 } 151 i = i + 1 152 } 153 return 0 154} 155func ux_max_severity(s: *UxStudy) -> i64 { 156 var m: i64 = 0 157 var i: i64 = 0 158 while i < s.n_findings { 159 let f: *UxFinding = ux_finding(s, i) 160 if f.severity > m { m = f.severity } 161 i = i + 1 162 } 163 return m 164} 165func ux_count_severity(s: *UxStudy, lvl: i64) -> i64 { 166 var c: i64 = 0 167 var i: i64 = 0 168 while i < s.n_findings { 169 let f: *UxFinding = ux_finding(s, i) 170 if f.severity == lvl { c = c + 1 } 171 i = i + 1 172 } 173 return c 174} 175func ux_total_errors(s: *UxStudy) -> i64 { 176 var e: i64 = 0 177 var i: i64 = 0 178 while i < s.n_tasks { 179 let t: *UxTask = ux_task(s, i) 180 e = e + t.n_errors 181 i = i + 1 182 } 183 return e 184} 185 186// THE STICKER-KILLER. A study PASSES only if EVERY realistic task succeeded AND no finding is major 187// or worse (>= 3). "Feature present" earns nothing. This is the whole point of grounding compare in 188// usability testing: you cannot give yourself a fake win because the grade is bound to task outcomes 189// and severity, not to whether a thing exists on disk. 190func ux_grade(s: *UxStudy) -> i64 { 191 if ux_any_task_fail(s) == 1 { return UX_GRADE_FAIL } 192 if ux_max_severity(s) >= UX_SEV_MAJOR { return UX_GRADE_FAIL } 193 if ux_task_success_rate(s) < 100 { return UX_GRADE_COND } 194 if ux_max_severity(s) >= UX_SEV_MINOR { return UX_GRADE_COND } 195 return UX_GRADE_PASS 196} 197 198// ---------- labels ---------- 199func ux_print_outcome(o: i64) -> i64 { 200 if o == UX_SUCCESS { ux_w("SUCCESS" as *u8); return 0 } 201 if o == UX_PARTIAL { ux_w("PARTIAL" as *u8); return 0 } 202 ux_w("FAIL " as *u8); return 0 203} 204func ux_print_severity(sv: i64) -> i64 { 205 if sv == 0 { ux_w("0 none " as *u8); return 0 } 206 if sv == 1 { ux_w("1 cosm " as *u8); return 0 } 207 if sv == 2 { ux_w("2 minor" as *u8); return 0 } 208 if sv == 3 { ux_w("3 MAJOR" as *u8); return 0 } 209 ux_w("4 CATAS" as *u8); return 0 210} 211func ux_print_surface(sf: i64) -> i64 { 212 if sf == UX_SURF_NISHI_DESKTOP { ux_w("Nishi browser (desktop)" as *u8); return 0 } 213 if sf == UX_SURF_NISHI_MOBILE { ux_w("Nishi browser (mobile)" as *u8); return 0 } 214 if sf == UX_SURF_CHROME_DESKTOP { ux_w("Chrome (desktop, oracle)" as *u8); return 0 } 215 if sf == UX_SURF_CHROME_MOBILE { ux_w("Chrome (mobile, oracle)" as *u8); return 0 } 216 ux_w("(unknown surface)" as *u8); return 0 217} 218func ux_print_grade(g: i64) -> i64 { 219 if g == UX_GRADE_PASS { ux_w("PASS" as *u8); return 0 } 220 if g == UX_GRADE_COND { ux_w("CONDITIONAL" as *u8); return 0 } 221 ux_w("FAIL" as *u8); return 0 222} 223 224// ---------- the report (WHAT happened, WHY it matters, WHAT to do) ---------- 225func ux_report(s: *UxStudy) -> i64 { 226 ux_w("================ NISHI USABILITY STUDY (NN/g Usability Testing 101) ================\n" as *u8) 227 ux_w("goal : " as *u8); ux_w(s.goal); ux_w("\n" as *u8) 228 ux_w("artifact : " as *u8); ux_w(s.artifact); ux_w("\n" as *u8) 229 ux_w("surface : " as *u8); ux_print_surface(s.surface); ux_w("\n" as *u8) 230 ux_w("study type: " as *u8) 231 if s.quantitative == 1 { ux_w("quantitative" as *u8) } else { ux_w("qualitative" as *u8) } 232 if s.moderated == 1 { ux_w(", moderated\n" as *u8) } else { ux_w(", unmoderated (self-driven participant)\n" as *u8) } 233 234 ux_w("\n-- TASKS (a representative user attempts realistic goals) --\n" as *u8) 235 var i: i64 = 0 236 while i < s.n_tasks { 237 let t: *UxTask = ux_task(s, i) 238 ux_w(" T" as *u8); ux_n(t.id); ux_w(" [" as *u8); ux_print_outcome(t.outcome); ux_w("] " as *u8); ux_w(t.desc); ux_w("\n" as *u8) 239 ux_w(" criterion: " as *u8); ux_w(t.crit); ux_w("\n" as *u8) 240 ux_w(" time-on-task=" as *u8); ux_n(t.time_frames); ux_w(" frames errors=" as *u8); ux_n(t.n_errors); ux_w("\n" as *u8) 241 i = i + 1 242 } 243 244 ux_w("\n-- FINDINGS (heuristic-tagged, severity-rated, FRAME-grounded) --\n" as *u8) 245 i = 0 246 while i < s.n_findings { 247 let f: *UxFinding = ux_finding(s, i) 248 ux_w(" [sev " as *u8); ux_print_severity(f.severity); ux_w("] H" as *u8); ux_n(f.heuristic_id); ux_w(" " as *u8); ux_heuristic_name(f.heuristic_id); ux_w("\n" as *u8) 249 ux_w(" problem : " as *u8); ux_w(f.desc); ux_w("\n" as *u8) 250 ux_w(" evidence: recording frame " as *u8); ux_n(f.evidence); ux_w("\n" as *u8) 251 ux_w(" fix : " as *u8); ux_w(f.rec); ux_w("\n" as *u8) 252 i = i + 1 253 } 254 255 ux_w("\n-- METRICS --\n" as *u8) 256 ux_w(" task success rate : " as *u8); ux_n(ux_task_success_rate(s)); ux_w("% (" as *u8); ux_n(s.n_tasks); ux_w(" tasks)\n" as *u8) 257 ux_w(" total errors : " as *u8); ux_n(ux_total_errors(s)); ux_w("\n" as *u8) 258 ux_w(" severity histogram: catas=" as *u8); ux_n(ux_count_severity(s, 4)); ux_w(" major=" as *u8); ux_n(ux_count_severity(s, 3)); ux_w(" minor=" as *u8); ux_n(ux_count_severity(s, 2)); ux_w(" cosm=" as *u8); ux_n(ux_count_severity(s, 1)); ux_w("\n" as *u8) 259 260 let g: i64 = ux_grade(s) 261 ux_w("\n ==> GRADE: " as *u8); ux_print_grade(g); ux_w("\n" as *u8) 262 if g == UX_GRADE_FAIL { ux_w(" (a task FAILED or a MAJOR+ defect exists -> NOT done. No sticker.)\n" as *u8) } 263 if g == UX_GRADE_COND { ux_w(" (usable, but partial tasks or minor defects remain -> not yet PASS.)\n" as *u8) } 264 if g == UX_GRADE_PASS { ux_w(" (every task succeeded, no minor-or-worse defect -> genuinely usable.)\n" as *u8) } 265 ux_w("====================================================================================\n" as *u8) 266 return g 267}