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}