nx_verdict_phrase.nx source
↩ module page · 480 lines · 20020 B
1// nx_verdict_phrase.nx -- human-legible phrasing for grader verdicts.
2//
3// CAPABILITY_COMPLETENESS: FULL (for shipped tier-axis verdicts;
4// new graders register their phrasing via nx_verdict_phrase_set)
5//
6// User direction 2026-05-16:
7// "also make sure the math is human legible like the skin is
8// too plastic or something even more explanatory or she is 50
9// percent asian in this pic etc"
10//
11// The substrate's tier-axis verdicts are sealed integers + Q10
12// scores -- great for the closed-loop trainer + ledger, useless
13// for a human reading the report. This primitive bridges the
14// machine-readable verdict world with the human-readable
15// explanation world.
16//
17// Two phrasing modes:
18//
19// MODE_TERSE -- one short clause, suitable for batch
20// dashboards: "skin too plastic"
21// "background fragmented"
22// "vein signal absent"
23// MODE_VERBOSE -- one full sentence with the score reading:
24// "Skin texture verdict PLASTIC_SHEEN at
25// energy 0.83 (expected 0.4-0.7 for
26// natural skin); over-smoothed render."
27//
28// Phrases are emitted as NUL-terminated UTF-8 byte sequences.
29// Caller passes a buffer of NX_VP_MAX_PHRASE_LEN bytes; primitive
30// writes the phrase and returns the byte count written.
31//
32// CONTINUOUS-CLASS PHRASING (e.g., "30% east-asian / 50%
33// european / 20% african" -- the user's "50 percent asian"
34// example): the phrasing is done via NxVerdictBlend struct
35// (sealed-enum class list + per-class Q10 weight) -> phrase
36// rendering with thresholds (drop classes below 5% Q10, sort
37// descending, format "X% kind / Y% kind / Z% kind").
38//
39// genealogy_id: substrate_legibility_layer_2026_05_16
40// lineage_id: nx_verdict_phrase_v1
41
42// nx_safety_envelope:
43// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
44// sil_target: SIL1
45// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
46// verdict: NOT_YET_EVALUATED
47
48import "nx_syscalls.nx"
49import "nx_runtime.nx"
50import "nx_tier.nx"
51
52const NX_VP_Q10: nx_int = 1024
53const NX_VP_MAX_PHRASE_LEN: nx_int = 512
54const NX_VP_BLEND_MAX_CLASSES: nx_int = 16
55const NX_VP_BLEND_MIN_PERCENT_Q10: nx_int = 51 // 5% Q10
56
57// ===== sealed-enum: phrasing mode ================================
58
59const NX_VP_MODE_TERSE: nx_int = 0
60const NX_VP_MODE_VERBOSE: nx_int = 1
61
62// ===== axis-id constants (mirror nx_domain_profile NX_AXIS_*) ====
63
64const NX_VP_AXIS_T1_SKIN_TONE_ITA: nx_int = 4
65const NX_VP_AXIS_T2_GLCM_TEXTURE: nx_int = 5
66const NX_VP_AXIS_T2_PORE_DETAIL: nx_int = 7
67const NX_VP_AXIS_T4_UNDERTONE: nx_int = 12
68const NX_VP_AXIS_T5_VEIN_SIGNAL: nx_int = 16
69const NX_VP_AXIS_T5_SUBSURFACE_SIGNAL: nx_int = 17
70const NX_VP_AXIS_T5B_MICRO_EXPRESSION: nx_int = 22
71const NX_VP_AXIS_T6_SHADOW_DIRECTION: nx_int = 23
72const NX_VP_AXIS_T6_FINGER_COUNT: nx_int = 24
73const NX_VP_AXIS_T6_LIMB_COUNT: nx_int = 25
74const NX_VP_AXIS_T6_POSE_NATURALNESS: nx_int = 26
75const NX_VP_AXIS_T6_SURFACE_PLANE: nx_int = 27
76const NX_VP_AXIS_T6_SILHOUETTE_INTEG: nx_int = 28
77
78// ===== blend struct ==============================================
79
80struct NxVerdictBlend {
81 n_classes: nx_int,
82 class_ids: *nx_int, // sealed enum per axis
83 class_names: **u8, // NUL-terminated UTF-8 names
84 weights_q10: *nx_int, // per-class weight (sum should be Q10)
85}
86
87const NX_VP_BLEND_BYTES: nx_size = 40
88
89// ===== string helpers ============================================
90
91func _vp_strlen(s: *u8) -> nx_int {
92 var n: nx_int = 0
93 var safety: nx_int = 0
94 while safety < NX_VP_MAX_PHRASE_LEN {
95 if s[n] == (0 as u8) { return n }
96 n = n + 1
97 safety = safety + 1
98 }
99 return n
100}
101
102func _vp_strcpy(dst: *u8, src: *u8, dst_pos: nx_int) -> nx_int {
103 let len: nx_int = _vp_strlen(src)
104 var i: nx_int = 0
105 while i < len {
106 dst[dst_pos + i] = src[i]
107 i = i + 1
108 }
109 return dst_pos + len
110}
111
112// Format an integer as decimal ASCII into dst starting at pos.
113// Returns the new position.
114func _vp_format_int(dst: *u8, pos: nx_int, value: nx_int) -> nx_int {
115 if value == 0 {
116 dst[pos] = 48 as u8 // '0'
117 return pos + 1
118 }
119 var v: nx_int = value
120 var negative: nx_int = 0
121 if v < 0 {
122 negative = 1
123 v = 0 - v
124 }
125 // Collect digits in reverse.
126 let buf: *u8 = (sys_mmap(32)) as *u8
127 var n_digits: nx_int = 0
128 while v > 0 {
129 let d: nx_int = v % 10
130 let ch: nx_int = 48 + d // '0' + d
131 buf[n_digits] = ch as u8
132 v = v / 10
133 n_digits = n_digits + 1
134 }
135 var out_pos: nx_int = pos
136 if negative == 1 {
137 dst[out_pos] = 45 as u8 // '-'
138 out_pos = out_pos + 1
139 }
140 var k: nx_int = n_digits - 1
141 while k >= 0 {
142 dst[out_pos] = buf[k]
143 out_pos = out_pos + 1
144 k = k - 1
145 }
146 return out_pos
147}
148
149// Q10 -> percent (0..100) for human display.
150func _vp_q10_to_percent(q10: nx_int) -> nx_int {
151 let pct: nx_int = (q10 * 100) / NX_VP_Q10
152 return pct
153}
154
155// ===== phrase templates (per axis x verdict) =====================
156//
157// For each shipped tier-axis we ship a sealed table of phrases
158// indexed by (verdict_value, mode). Caller adds new mappings
159// via nx_verdict_phrase_set (queued).
160//
161// Each helper returns a NUL-terminated UTF-8 byte string.
162
163func _vp_skin_tone_phrase(verdict: nx_int, mode: nx_int) -> *u8 {
164 if mode == NX_VP_MODE_TERSE {
165 if verdict == 0 { return "very light skin tone" as *u8 }
166 if verdict == 1 { return "light skin tone" as *u8 }
167 if verdict == 2 { return "intermediate skin tone" as *u8 }
168 if verdict == 3 { return "tan skin tone" as *u8 }
169 if verdict == 4 { return "brown skin tone" as *u8 }
170 if verdict == 5 { return "dark skin tone" as *u8 }
171 return "unknown skin tone" as *u8
172 }
173 if verdict == 0 { return "Very light skin tone (Fitzpatrick I; ITA > 55 deg)" as *u8 }
174 if verdict == 1 { return "Light skin tone (Fitzpatrick II; ITA 41-55 deg)" as *u8 }
175 if verdict == 2 { return "Intermediate skin tone (Fitzpatrick III; ITA 28-41 deg)" as *u8 }
176 if verdict == 3 { return "Tan skin tone (Fitzpatrick IV; ITA 10-28 deg)" as *u8 }
177 if verdict == 4 { return "Brown skin tone (Fitzpatrick V; ITA -30 to 10 deg)" as *u8 }
178 if verdict == 5 { return "Dark skin tone (Fitzpatrick VI; ITA < -30 deg)" as *u8 }
179 return "Unknown skin tone classification" as *u8
180}
181
182func _vp_undertone_phrase(verdict: nx_int, mode: nx_int) -> *u8 {
183 if mode == NX_VP_MODE_TERSE {
184 if verdict == 1 { return "cool undertone" as *u8 }
185 if verdict == 2 { return "neutral undertone" as *u8 }
186 if verdict == 3 { return "warm undertone" as *u8 }
187 return "undertone not detected" as *u8
188 }
189 if verdict == 1 { return "Cool undertone (pink/red-leaning chroma; CIELab h_ab < 45 deg)" as *u8 }
190 if verdict == 2 { return "Neutral undertone (balanced chroma; CIELab h_ab 45-55 deg)" as *u8 }
191 if verdict == 3 { return "Warm undertone (golden/yellow-leaning chroma; CIELab h_ab > 55 deg)" as *u8 }
192 return "Undertone not detected -- pixels not in typical skin chroma band" as *u8
193}
194
195func _vp_skin_texture_phrase(verdict: nx_int, mode: nx_int) -> *u8 {
196 if mode == NX_VP_MODE_TERSE {
197 if verdict == 1 { return "natural skin texture" as *u8 }
198 if verdict == 2 { return "skin too plastic" as *u8 }
199 if verdict == 3 { return "skin too noisy" as *u8 }
200 return "skin texture unknown" as *u8
201 }
202 if verdict == 1 { return "Natural skin texture (GLCM energy 0.4-0.7; visible micro-variation)" as *u8 }
203 if verdict == 2 { return "Plastic-sheen skin (GLCM energy > 0.9; over-smoothed render typical of SDXL / Midjourney)" as *u8 }
204 if verdict == 3 { return "Over-noisy skin (GLCM contrast > 0.5; texture corrupted or grain too coarse)" as *u8 }
205 return "Skin texture not classified" as *u8
206}
207
208func _vp_vein_signal_phrase(verdict: nx_int, mode: nx_int) -> *u8 {
209 if mode == NX_VP_MODE_TERSE {
210 if verdict == 0 { return "no visible veins (plastic render tell)" as *u8 }
211 if verdict == 1 { return "faint vein signal" as *u8 }
212 if verdict == 2 { return "natural vein visibility" as *u8 }
213 if verdict == 3 { return "veins overpronounced" as *u8 }
214 return "vein signal unknown" as *u8
215 }
216 if verdict == 0 { return "Vein-signal absent: thin-skin regions show no blue subsurface shift -- typical of AI-smoothed renders" as *u8 }
217 if verdict == 1 { return "Weak vein signal: borderline blue-shift in thin-skin regions" as *u8 }
218 if verdict == 2 { return "Normal vein visibility: subsurface signal present in wrist / inside-elbow / temple regions" as *u8 }
219 if verdict == 3 { return "Over-pronounced veins: signal density > 25%; medical-photo or surgical look" as *u8 }
220 return "Vein signal not classified" as *u8
221}
222
223func _vp_micro_expression_phrase(verdict: nx_int, mode: nx_int) -> *u8 {
224 if mode == NX_VP_MODE_TERSE {
225 if verdict == 0 { return "dead face (no aliveness cues)" as *u8 }
226 if verdict == 1 { return "flat face (few aliveness cues)" as *u8 }
227 if verdict == 2 { return "weak aliveness signal" as *u8 }
228 if verdict == 3 { return "alive expression" as *u8 }
229 return "expression liveliness unknown" as *u8
230 }
231 if verdict == 0 { return "Face shows 0-1 of 6 expected aliveness micro-cues (bilateral asymmetry / eyelid shadow / sclera vessels / pupil asymmetry / mouth tension / nostril asymmetry) -- canonical AI 'dead face' tell" as *u8 }
232 if verdict == 1 { return "Face shows 2-3 of 6 aliveness cues -- flat presentation; missing key tells" as *u8 }
233 if verdict == 2 { return "Face shows 4 of 6 aliveness cues -- weak but present" as *u8 }
234 if verdict == 3 { return "Face shows 5-6 of 6 aliveness cues -- natural expression" as *u8 }
235 return "Expression liveliness not classified" as *u8
236}
237
238func _vp_finger_count_phrase(verdict: nx_int, mode: nx_int) -> *u8 {
239 if mode == NX_VP_MODE_TERSE {
240 if verdict == 0 { return "5 fingers detected" as *u8 }
241 if verdict == 1 { return "extra finger detected" as *u8 }
242 if verdict == 2 { return "missing finger" as *u8 }
243 if verdict == 3 { return "fused fingers" as *u8 }
244 if verdict == 4 { return "finger detection noisy" as *u8 }
245 return "finger count unknown" as *u8
246 }
247 if verdict == 0 { return "5 finger-runs detected on scan line -- anatomically correct" as *u8 }
248 if verdict == 1 { return "6+ finger-runs detected -- the canonical generative-AI extra-finger failure" as *u8 }
249 if verdict == 2 { return "Only 4 finger-runs detected -- missing finger or hand partially occluded" as *u8 }
250 if verdict == 3 { return "3 or fewer finger-runs -- fingers fused into paddle shapes" as *u8 }
251 if verdict == 4 { return "Scan line shows > 16 tiny runs -- segmentation noisy, cannot count reliably" as *u8 }
252 return "Finger count not classified" as *u8
253}
254
255func _vp_aperture_phrase(verdict: nx_int, mode: nx_int) -> *u8 {
256 if mode == NX_VP_MODE_TERSE {
257 if verdict == 0 { return "coherent background" as *u8 }
258 if verdict == 1 { return "depth-of-field wrong (background too sharp)" as *u8 }
259 if verdict == 2 { return "no atmospheric perspective" as *u8 }
260 if verdict == 3 { return "fragmented background" as *u8 }
261 if verdict == 4 { return "background is a mess (3+ checks failed)" as *u8 }
262 return "background not classified" as *u8
263 }
264 if verdict == 0 { return "Background through aperture passes all four physics checks (depth-of-field, atmospheric color shift, edge density, edge uniformity)" as *u8 }
265 if verdict == 1 { return "Depth-of-field failure: background as sharp as foreground -- AI did not apply lens blur" as *u8 }
266 if verdict == 2 { return "Atmospheric-perspective failure: no cool/blue shift in distant content -- AI flattened depth cues" as *u8 }
267 if verdict == 3 { return "Fragmented background: edge density or uniformity outside real-scene distribution" as *u8 }
268 if verdict == 4 { return "Background implausible: 3+ physics checks failed -- AI-generated random fragments instead of coherent scene" as *u8 }
269 return "Background not classified" as *u8
270}
271
272// ===== top-level phrase fetch ====================================
273//
274// Returns the static phrase string for (axis_id, verdict_value, mode).
275// Returns "unsupported axis" if no template registered.
276
277func nx_verdict_phrase_for(axis_id: nx_int, verdict: nx_int, mode: nx_int) -> *u8 {
278 if axis_id == NX_VP_AXIS_T1_SKIN_TONE_ITA {
279 return _vp_skin_tone_phrase(verdict, mode)
280 }
281 if axis_id == NX_VP_AXIS_T4_UNDERTONE {
282 return _vp_undertone_phrase(verdict, mode)
283 }
284 if axis_id == NX_VP_AXIS_T2_GLCM_TEXTURE {
285 return _vp_skin_texture_phrase(verdict, mode)
286 }
287 if axis_id == NX_VP_AXIS_T5_VEIN_SIGNAL {
288 return _vp_vein_signal_phrase(verdict, mode)
289 }
290 if axis_id == NX_VP_AXIS_T5B_MICRO_EXPRESSION {
291 return _vp_micro_expression_phrase(verdict, mode)
292 }
293 if axis_id == NX_VP_AXIS_T6_FINGER_COUNT {
294 return _vp_finger_count_phrase(verdict, mode)
295 }
296 if axis_id == NX_VP_AXIS_T6_SURFACE_PLANE {
297 return _vp_aperture_phrase(verdict, mode)
298 }
299 return "axis phrasing not registered" as *u8
300}
301
302// ===== blend-class phrasing (continuous-class, e.g. "50% asian") ==
303//
304// Renders a comma-separated descending list of "PERCENT%
305// class_name" entries, dropping classes whose weight is below
306// NX_VP_BLEND_MIN_PERCENT_Q10 (default 5%). Caller provides the
307// class names; substrate sorts by descending weight + formats.
308
309func nx_verdict_blend_render(
310 blend: *NxVerdictBlend, out_buf: *u8) -> nx_int {
311
312 if blend.n_classes <= 0 {
313 out_buf[0] = 0 as u8
314 return 0
315 }
316 if blend.n_classes > NX_VP_BLEND_MAX_CLASSES {
317 out_buf[0] = 0 as u8
318 return 0
319 }
320
321 // Sort indices by descending weight using selection sort.
322 let n: nx_int = blend.n_classes
323 let order: *nx_int = (sys_mmap((n as nx_size) * 8)) as *nx_int
324 var i: nx_int = 0
325 while i < n {
326 order[i] = i
327 i = i + 1
328 }
329 var p: nx_int = 0
330 while p < n - 1 {
331 var max_idx: nx_int = p
332 var q: nx_int = p + 1
333 while q < n {
334 let a: nx_int = blend.weights_q10[order[max_idx]]
335 let b: nx_int = blend.weights_q10[order[q]]
336 if b > a { max_idx = q }
337 q = q + 1
338 }
339 if max_idx != p {
340 let tmp: nx_int = order[p]
341 order[p] = order[max_idx]
342 order[max_idx] = tmp
343 }
344 p = p + 1
345 }
346
347 var out_pos: nx_int = 0
348 var emitted: nx_int = 0
349 var k: nx_int = 0
350 while k < n {
351 let idx: nx_int = order[k]
352 let w: nx_int = blend.weights_q10[idx]
353 if w >= NX_VP_BLEND_MIN_PERCENT_Q10 {
354 if emitted > 0 {
355 out_buf[out_pos] = 32 as u8 // ' '
356 out_buf[out_pos + 1] = 47 as u8 // '/'
357 out_buf[out_pos + 2] = 32 as u8 // ' '
358 out_pos = out_pos + 3
359 }
360 let pct: nx_int = _vp_q10_to_percent(w)
361 out_pos = _vp_format_int(out_buf, out_pos, pct)
362 out_buf[out_pos] = 37 as u8 // '%'
363 out_buf[out_pos + 1] = 32 as u8 // ' '
364 out_pos = out_pos + 2
365 let name: *u8 = blend.class_names[idx]
366 out_pos = _vp_strcpy(out_buf, name, out_pos)
367 emitted = emitted + 1
368 }
369 k = k + 1
370 }
371 out_buf[out_pos] = 0 as u8
372 return out_pos
373}
374
375// ===== self-test =================================================
376
377func main() -> nx_int {
378 // ---- skin-tone terse + verbose ----
379 let p_st_terse: *u8 = nx_verdict_phrase_for(
380 NX_VP_AXIS_T1_SKIN_TONE_ITA, 1, NX_VP_MODE_TERSE)
381 let p_st_verbose: *u8 = nx_verdict_phrase_for(
382 NX_VP_AXIS_T1_SKIN_TONE_ITA, 1, NX_VP_MODE_VERBOSE)
383 if _vp_strlen(p_st_terse) != _vp_strlen("light skin tone" as *u8) { return 1 }
384 if _vp_strlen(p_st_verbose) < 30 { return 2 }
385
386 // ---- skin texture: "plastic" verdict yields the user's
387 // example phrase "skin too plastic" ----
388 let p_plastic: *u8 = nx_verdict_phrase_for(
389 NX_VP_AXIS_T2_GLCM_TEXTURE, 2, NX_VP_MODE_TERSE)
390 let expected_plastic: *u8 = "skin too plastic" as *u8
391 if _vp_strlen(p_plastic) != _vp_strlen(expected_plastic) { return 10 }
392 var k: nx_int = 0
393 while k < _vp_strlen(expected_plastic) {
394 if p_plastic[k] != expected_plastic[k] { return 11 }
395 k = k + 1
396 }
397
398 // ---- micro-expression: "dead face" verdict ----
399 let p_dead: *u8 = nx_verdict_phrase_for(
400 NX_VP_AXIS_T5B_MICRO_EXPRESSION, 0, NX_VP_MODE_TERSE)
401 if _vp_strlen(p_dead) < 10 { return 20 }
402
403 // ---- finger count: "extra finger" ----
404 let p_extra: *u8 = nx_verdict_phrase_for(
405 NX_VP_AXIS_T6_FINGER_COUNT, 1, NX_VP_MODE_TERSE)
406 if _vp_strlen(p_extra) < 5 { return 30 }
407
408 // ---- unregistered axis fallback ----
409 let p_unk: *u8 = nx_verdict_phrase_for(999, 0, NX_VP_MODE_TERSE)
410 if _vp_strlen(p_unk) < 10 { return 40 }
411
412 // ---- blend renderer ("50% east asian / 30% european / 20% latino")
413 //
414 // Q10 weights: east-asian 512, european 307, latino 205
415 let names: **u8 = (sys_mmap(24)) as **u8
416 names[0] = "east asian" as *u8
417 names[1] = "european" as *u8
418 names[2] = "latino" as *u8
419 let weights: *nx_int = (sys_mmap(24)) as *nx_int
420 weights[0] = 512
421 weights[1] = 307
422 weights[2] = 205
423 let class_ids: *nx_int = (sys_mmap(24)) as *nx_int
424 class_ids[0] = 0
425 class_ids[1] = 1
426 class_ids[2] = 2
427
428 let blend_ptr: *u8 = sys_mmap(NX_VP_BLEND_BYTES)
429 let blend: *NxVerdictBlend = blend_ptr as *NxVerdictBlend
430 blend.n_classes = 3
431 blend.class_ids = class_ids
432 blend.class_names = names
433 blend.weights_q10 = weights
434
435 let out_buf: *u8 = (sys_mmap(NX_VP_MAX_PHRASE_LEN as nx_size)) as *u8
436 let len: nx_int = nx_verdict_blend_render(blend, out_buf)
437 if len < 20 { return 50 }
438 // Should start with "50%" since the largest weight (512 Q10 = 50%)
439 // gets emitted first.
440 if out_buf[0] != (53 as u8) { return 51 } // '5'
441 if out_buf[1] != (48 as u8) { return 52 } // '0'
442 if out_buf[2] != (37 as u8) { return 53 } // '%'
443 if out_buf[3] != (32 as u8) { return 54 } // ' '
444 if out_buf[4] != (101 as u8) { return 55 } // 'e' (east asian)
445
446 // ---- blend with class below 5% threshold should be dropped ----
447 let w_low: *nx_int = (sys_mmap(24)) as *nx_int
448 w_low[0] = 512 // 50%
449 w_low[1] = 470 // 46%
450 w_low[2] = 42 // 4% -- below threshold; should be dropped
451 let class_ids2: *nx_int = (sys_mmap(24)) as *nx_int
452 class_ids2[0] = 0
453 class_ids2[1] = 1
454 class_ids2[2] = 2
455 let blend2_ptr: *u8 = sys_mmap(NX_VP_BLEND_BYTES)
456 let blend2: *NxVerdictBlend = blend2_ptr as *NxVerdictBlend
457 blend2.n_classes = 3
458 blend2.class_ids = class_ids2
459 blend2.class_names = names
460 blend2.weights_q10 = w_low
461 let out_buf2: *u8 = (sys_mmap(NX_VP_MAX_PHRASE_LEN as nx_size)) as *u8
462 let len2: nx_int = nx_verdict_blend_render(blend2, out_buf2)
463 // Search for "/" -- exactly one separator (two classes emitted).
464 var slash_count: nx_int = 0
465 var s: nx_int = 0
466 while s < len2 {
467 if out_buf2[s] == (47 as u8) { slash_count = slash_count + 1 }
468 s = s + 1
469 }
470 if slash_count != 1 { return 60 }
471
472 // ---- format_int sanity ----
473 let small_buf: *u8 = (sys_mmap(16)) as *u8
474 let pos: nx_int = _vp_format_int(small_buf, 0, 42)
475 if pos != 2 { return 70 }
476 if small_buf[0] != (52 as u8) { return 71 } // '4'
477 if small_buf[1] != (50 as u8) { return 72 } // '2'
478
479 return 0
480}