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}