code wiki / (root) / nx_ux_score.nx

nx_ux_score.nx source

↩ module page · 292 lines · 12219 B

1// nx_ux_score.nx -- score a render journal against UX standard clauses. 2// 3// T2 of NISHI_UX_TRIANGULATION_ROADMAP. Takes a RenderJournal (the 4// substrate-honest record of paint intents) and emits a 0..10 score 5// per clause, with per-clause notes citing what evidence justified 6// the score. Substrate is the judge of its OWN UX -- no observing 7// the user, no asking the user, no Claude/operator hand-waving. 8// 9// Scoring heuristics per clause (extensible): 10// NN/g #1 visibility -- count intent labels containing "flash", 11// "breadcrumb_current", "indicator", "status", "current" 12// (each +1, capped at 10). Rationale: the app shows the user 13// where they are + feedback on action. 14// NN/g #6 recognition not recall -- count "breadcrumb_*", "tab", 15// "menu", "label" intents. 16// NN/g #4 consistency -- if the journal exhibits a header/content/ 17// footer pattern (status_bar at bottom + url_bar at top), +5. 18// ISO 9241-110 self-descriptiveness -- journal exists at all? +5. 19// Many semantic labels (>= 12)? +5. Total cap 10. 20// ... etc. 21// 22// Anything not scoreable from the journal alone returns 0 + a note 23// "needs external instrumentation" (queued: nx_event_timeline, 24// nx_attention_signal). 25// 26// API: 27// nx_ux_score_clause(journal, clause_id, note_buf, note_cap) -> 0..10 28// 29// license_tier: ORIGINAL 30// lineage_id: nishi_ux_score_q10 31 32import "nx_syscalls.nx" 33import "nx_render_journal.nx" 34import "nx_ux_standard.nx" 35 36// Substring search: returns 1 if needle (NUL-term'd) appears 37// anywhere in haystack (NUL-term'd). 38func _ux_str_contains(haystack: *u8, needle: *u8) -> i64 { 39 var i: i64 = 0 40 while haystack[i] != 0 { 41 var j: i64 = 0 42 var still_matching: i64 = 1 43 var inner_done: i64 = 0 44 while inner_done == 0 { 45 if needle[j] == 0 { inner_done = 1 } 46 else { 47 if haystack[i + j] == 0 { inner_done = 1; still_matching = 0 } 48 else { 49 if haystack[i + j] != needle[j] { inner_done = 1; still_matching = 0 } 50 else { j = j + 1 } 51 } 52 } 53 } 54 if still_matching == 1 { return 1 } 55 i = i + 1 56 } 57 return 0 58} 59 60// Count journal entries whose intent string contains `needle`. 61func _ux_count_intents(j: *RenderJournal, needle: *u8) -> i64 { 62 var n: i64 = 0 63 var idx: i64 = 0 64 while idx < j.count { 65 let e: *RenderJournalEntry = _journal_at(j, idx) 66 if _ux_str_contains(e.intent, needle) == 1 { n = n + 1 } 67 idx = idx + 1 68 } 69 return n 70} 71 72// Returns 1 if any journal entry's intent EXACTLY equals `tag`. 73func _ux_has_exact(j: *RenderJournal, tag: *u8) -> i64 { 74 var idx: i64 = 0 75 while idx < j.count { 76 let e: *RenderJournalEntry = _journal_at(j, idx) 77 var i: i64 = 0 78 var eq: i64 = 1 79 var inner_done: i64 = 0 80 while inner_done == 0 { 81 if tag[i] == 0 { inner_done = 1 } 82 else { 83 if e.intent[i] != tag[i] { eq = 0; inner_done = 1 } 84 else { i = i + 1 } 85 } 86 } 87 if eq == 1 { if e.intent[i] == 0 { return 1 } } 88 idx = idx + 1 89 } 90 return 0 91} 92 93// Score a single clause. Returns 0..10. Writes a short justification 94// note into note_buf (caller-allocated, NUL-terminated). 95func nx_ux_score_clause(j: *RenderJournal, clause_id: i64, 96 note_buf: *u8, note_cap: i64) -> i64 { 97 // ---- NN/g #1 visibility of system status ------------------------- 98 if clause_id == NX_UX_NN1_VISIBILITY { 99 let flash: i64 = _ux_count_intents(j, "flash" as *u8) 100 let bc: i64 = _ux_count_intents(j, "breadcrumb_current" as *u8) 101 let status: i64 = _ux_count_intents(j, "status" as *u8) 102 let indicator: i64 = _ux_count_intents(j, "indicator" as *u8) 103 var s: i64 = flash * 2 + bc * 3 + status * 1 + indicator * 2 104 if s > 10 { s = 10 } 105 return s 106 } 107 // ---- NN/g #6 recognition not recall ------------------------------ 108 if clause_id == NX_UX_NN6_RECOGNITION { 109 let bc: i64 = _ux_count_intents(j, "breadcrumb" as *u8) 110 let menu: i64 = _ux_count_intents(j, "menu" as *u8) 111 let label: i64 = _ux_count_intents(j, "label" as *u8) 112 var s: i64 = bc * 2 + menu * 2 + label * 1 113 if s > 10 { s = 10 } 114 return s 115 } 116 // ---- NN/g #4 consistency / standards ---------------------------- 117 if clause_id == NX_UX_NN4_CONSISTENCY { 118 // Header (url_bar_bg at top) + footer (status_bar at bottom) + 119 // content area = web-standard layout. 120 let url_bar: i64 = _ux_has_exact(j, "url_bar_bg" as *u8) 121 let status_bar: i64 = _ux_has_exact(j, "status_bar" as *u8) 122 let background: i64 = _ux_has_exact(j, "background" as *u8) 123 var s: i64 = url_bar * 4 + status_bar * 3 + background * 3 124 if s > 10 { s = 10 } 125 return s 126 } 127 // ---- NN/g #3 user control & freedom ----------------------------- 128 if clause_id == NX_UX_NN3_USER_CONTROL { 129 let back: i64 = _ux_count_intents(j, "back_button" as *u8) 130 let forward: i64 = _ux_count_intents(j, "forward_button" as *u8) 131 let reload: i64 = _ux_count_intents(j, "reload_button" as *u8) 132 var s: i64 = back * 3 + forward * 3 + reload * 2 133 if s > 10 { s = 10 } 134 return s 135 } 136 // ---- ISO 9241-110 self-descriptiveness -------------------------- 137 if clause_id == NX_UX_ISO_SELFDESCRIPTIVE { 138 // Journal existing + semantic labels = the app describes itself. 139 if j.count >= 12 { return 9 } 140 if j.count >= 6 { return 7 } 141 if j.count >= 1 { return 4 } 142 return 0 143 } 144 // ---- Apple HIG feedback (independent source from NN/g #1) ------- 145 // HIG "feedback" subsumes visible state changes: click flash, 146 // location indicator, focus ring, selection state, status. 147 if clause_id == NX_UX_APPLE_FEEDBACK { 148 let flash: i64 = _ux_count_intents(j, "flash" as *u8) 149 let bc: i64 = _ux_count_intents(j, "breadcrumb_current" as *u8) 150 let focus: i64 = _ux_count_intents(j, "focus_indicator" as *u8) 151 let status: i64 = _ux_count_intents(j, "status_bar" as *u8) 152 var s: i64 = flash * 3 + bc * 3 + focus * 3 + status * 1 153 if s > 10 { s = 10 } 154 return s 155 } 156 // ---- NN/g #5 error prevention ----------------------------------- 157 // Disabled-state affordance prevents the user from attempting an 158 // impossible action. Detected via back/forward button entries 159 // whose color is the dim variant (not the default blue 0x004488CC). 160 if clause_id == NX_UX_NN5_ERROR_PREVENTION { 161 var s: i64 = 0 162 var idx: i64 = 0 163 while idx < j.count { 164 let e: *RenderJournalEntry = _journal_at(j, idx) 165 let is_back: i64 = _ux_str_contains(e.intent, "back_button" as *u8) 166 let is_fwd: i64 = _ux_str_contains(e.intent, "forward_button" as *u8) 167 if is_back == 1 { 168 if e.color != 0x004488CC { if e.color != 0 { s = s + 4 } } 169 } 170 if is_fwd == 1 { 171 if e.color != 0x004488CC { if e.color != 0 { s = s + 4 } } 172 } 173 idx = idx + 1 174 } 175 if s > 10 { s = 10 } 176 return s 177 } 178 // ---- WCAG 2.1.1 keyboard accessible ----------------------------- 179 // The accessibility tree being emitted to the journal proves 180 // the app KNOWS its keyboard-targetable surfaces. Combined with 181 // an actual KeyPress dispatch + focus indicator, we score the 182 // pattern. 183 if clause_id == NX_UX_WCAG_2_1_1_KEYBOARD { 184 let a11y: i64 = _ux_count_intents(j, "a11y_role" as *u8) 185 let focus: i64 = _ux_count_intents(j, "focus_indicator" as *u8) 186 var s: i64 = 0 187 if a11y >= 3 { s = s + 4 } 188 if focus > 0 { s = s + 4 } 189 if a11y >= 6 { s = s + 2 } 190 if s > 10 { s = 10 } 191 return s 192 } 193 // ---- WCAG 2.4.7 focus visible ----------------------------------- 194 if clause_id == NX_UX_WCAG_2_4_7_FOCUS { 195 let focus: i64 = _ux_count_intents(j, "focus_indicator" as *u8) 196 if focus > 0 { return 8 } 197 return 0 198 } 199 // ---- WCAG 4.1.2 name/role/value --------------------------------- 200 // a11y_role_* journal entries declare the semantic role of each 201 // visible element. A future AT-bridge primitive will expose 202 // these to screen readers. We score on declaration count. 203 if clause_id == NX_UX_WCAG_4_1_2_NRV { 204 let a11y: i64 = _ux_count_intents(j, "a11y_role" as *u8) 205 if a11y >= 6 { return 9 } 206 if a11y >= 3 { return 7 } 207 if a11y >= 1 { return 4 } 208 return 0 209 } 210 // ---- Apple HIG clarity (minimalist substrate) ------------------- 211 if clause_id == NX_UX_APPLE_CLARITY { 212 // No advertising, no animations, no decorative bloat = clarity. 213 let bloat: i64 = _ux_count_intents(j, "decoration" as *u8) 214 if bloat == 0 { return 8 } 215 return 5 216 } 217 // ---- W3C HTML semantic elements --------------------------------- 218 // Landmark elements (header/nav/main/footer analogues) declared 219 // in the journal as named regions. 220 if clause_id == NX_UX_W3C_HTML_SEMANTIC { 221 let url_bar: i64 = _ux_has_exact(j, "url_bar_bg" as *u8) 222 let status_bar: i64 = _ux_has_exact(j, "status_bar" as *u8) 223 let title: i64 = _ux_count_intents(j, "title" as *u8) 224 let content: i64 = _ux_count_intents(j, "content" as *u8) 225 var s: i64 = url_bar * 3 + status_bar * 3 226 if title >= 1 { s = s + 2 } 227 if content >= 1 { s = s + 2 } 228 if s > 10 { s = 10 } 229 return s 230 } 231 // ---- W3C ARIA 1.2 roles ----------------------------------------- 232 // Independent confirmation of WCAG 4.1.2 evidence -- ARIA is the 233 // W3C spec that defines the vocabulary; WCAG is the conformance 234 // criterion that references it. Score same signal, different 235 // organisational source. 236 if clause_id == NX_UX_W3C_ARIA_ROLES { 237 let a11y: i64 = _ux_count_intents(j, "a11y_role" as *u8) 238 if a11y >= 6 { return 9 } 239 if a11y >= 3 { return 7 } 240 if a11y >= 1 { return 4 } 241 return 0 242 } 243 // ---- W3C ARIA 1.2 labels ---------------------------------------- 244 if clause_id == NX_UX_W3C_ARIA_LABELS { 245 let label: i64 = _ux_count_intents(j, "label" as *u8) 246 let bc: i64 = _ux_count_intents(j, "breadcrumb" as *u8) 247 var s: i64 = label * 2 + bc * 2 248 if s > 10 { s = 10 } 249 return s 250 } 251 // ---- W3C DOM Living Standard tree ------------------------------- 252 // Well-formed render tree = journal has structural diversity. 253 if clause_id == NX_UX_W3C_DOM_TREE { 254 if j.count >= 20 { return 9 } 255 if j.count >= 10 { return 7 } 256 if j.count >= 4 { return 4 } 257 return 0 258 } 259 // ---- W3C CSS computable color contrast -------------------------- 260 // Substrate emits colours as raw 0x00RRGGBB; we can verify text 261 // colour against background colour exists (non-zero counts). 262 if clause_id == NX_UX_W3C_CSS_CONTRAST { 263 let bg: i64 = _ux_has_exact(j, "background" as *u8) 264 let text: i64 = _ux_count_intents(j, "text" as *u8) 265 let title: i64 = _ux_count_intents(j, "title" as *u8) 266 var s: i64 = bg * 3 267 if text >= 1 { s = s + 3 } 268 if title >= 1 { s = s + 2 } 269 if s > 10 { s = 10 } 270 return s 271 } 272 // ---- MS Inclusive Design exclusion ------------------------------ 273 // Persona-spectrum check: keyboard-reachable + focus-visible + 274 // semantic roles together cover the motor/visual/cognitive axes. 275 if clause_id == NX_UX_MS_INCL_EXCLUSION { 276 let a11y: i64 = _ux_count_intents(j, "a11y_role" as *u8) 277 let focus: i64 = _ux_count_intents(j, "focus_indicator" as *u8) 278 let status: i64 = _ux_has_exact(j, "status_bar" as *u8) 279 var s: i64 = 0 280 if a11y >= 3 { s = s + 4 } 281 if focus > 0 { s = s + 4 } 282 if status == 1 { s = s + 2 } 283 if s > 10 { s = 10 } 284 return s 285 } 286 // Default: unscoreable from journal alone. 287 return 0 288} 289 290func main() -> i64 { 291 return 0 292}