nx_batch_audit_sidecar_test.nx source
↩ module page · 169 lines · 7256 B
1// nx_batch_audit_sidecar_test.nx -- end-to-end live-fire shape.
2//
3// Simulates a 4-scene batch (same character, morning routine, one
4// deliberate continuity violation, biased outfit picks). Confirms:
5// - per-event continuity verdicts route correctly
6// - pool-utilization classifies as SEVERE_BIAS when one outfit dominates
7// - batch_close summary span carries the right shape
8
9import "nx_syscalls.nx"
10import "nx_runtime.nx"
11import "nx_tier.nx"
12import "nx_directors_note.nx"
13import "nx_world_state.nx"
14import "nx_trace_emit.nx"
15import "nx_batch_audit_sidecar.nx"
16
17func main() -> nx_int {
18 let cfg: *TraceCfg = nx_trace_cfg_alloc()
19
20 // Disable real fd writes during test so qemu's stderr stays clean;
21 // we exercise pure logic + return-code paths. Re-enable would
22 // emit the JSONL spans we saw in the trace_emit smoke.
23 nx_trace_disable(cfg)
24
25 let ws: *WorldState = nx_world_alloc(100, 0 - 1, 4, 4)
26 nx_world_register_character(ws, 7, 0xCAFE, 9)
27 nx_world_register_location(ws, 100, 0xDEAD)
28 nx_world_register_location(ws, 101, 0xBEEF)
29
30 let outfit_keys: *i64 = (sys_mmap(64)) as *i64
31 let outfit_counts: *i64 = (sys_mmap(64)) as *i64
32 let pose_keys: *i64 = (sys_mmap(64)) as *i64
33 let pose_counts: *i64 = (sys_mmap(64)) as *i64
34 nx_pool_hist_init(outfit_keys, outfit_counts, 8)
35 nx_pool_hist_init(pose_keys, pose_counts, 8)
36
37 var n_violations: nx_int = 0
38
39 // ===== Scene 1: legitimate first scene ========================
40 let e1: *AuditEvent = nx_audit_event_alloc()
41 e1.batch_id = 42
42 e1.scene_seq = 1
43 e1.trace_id = 1000
44 e1.character_id = 7
45 e1.arc_id = 10
46 e1.stage_idx = 0
47 e1.outfit_id = 200
48 e1.location_id = 100
49 e1.pose_id = 300
50 e1.affect_axis = NX_AF_FOCUSED
51 e1.camera_id = 500
52 e1.day_clock_min = 420
53 e1.beat_armed = NX_WS_BEAT_NONE
54
55 let v1: nx_int = nx_audit_process_event(cfg, ws, e1,
56 outfit_keys, outfit_counts, 8,
57 pose_keys, pose_counts, 8)
58 if v1 != NX_CONT_CONSISTENT { return 1 }
59
60 // ===== Scene 2: same outfit + location + later clock -> OK ====
61 let e2: *AuditEvent = nx_audit_event_alloc()
62 e2.batch_id = 42
63 e2.scene_seq = 2
64 e2.trace_id = 1000
65 e2.character_id = 7
66 e2.outfit_id = 200
67 e2.location_id = 100
68 e2.pose_id = 300 // same pose -- bias-building
69 e2.day_clock_min = 450
70 let v2: nx_int = nx_audit_process_event(cfg, ws, e2,
71 outfit_keys, outfit_counts, 8,
72 pose_keys, pose_counts, 8)
73 if v2 != NX_CONT_CONSISTENT { return 2 }
74
75 // ===== Scene 3: TELEPORT to kitchen with NO beat -> violation =
76 let e3: *AuditEvent = nx_audit_event_alloc()
77 e3.batch_id = 42
78 e3.scene_seq = 3
79 e3.trace_id = 1000
80 e3.character_id = 7
81 e3.outfit_id = 200
82 e3.location_id = 101 // teleport
83 e3.pose_id = 300
84 e3.day_clock_min = 480
85 e3.beat_armed = NX_WS_BEAT_NONE
86 let v3: nx_int = nx_audit_process_event(cfg, ws, e3,
87 outfit_keys, outfit_counts, 8,
88 pose_keys, pose_counts, 8)
89 if v3 != NX_CONT_VIOLATES_LOCATION_TELEPORT { return 3 }
90 n_violations = n_violations + 1
91
92 // World state must NOT have advanced past the violation -- still 2 commits
93 if ws.scene_count != 2 { return 4 }
94
95 // ===== Scene 4: armed beat + new outfit -> OK ================
96 let e4: *AuditEvent = nx_audit_event_alloc()
97 e4.batch_id = 42
98 e4.scene_seq = 4
99 e4.trace_id = 1000
100 e4.character_id = 7
101 e4.outfit_id = 200 // same outfit
102 e4.location_id = 101 // change location with proper beat
103 e4.pose_id = 300
104 e4.day_clock_min = 480
105 e4.beat_armed = NX_WS_BEAT_LOCATION_CHANGE_OK
106 let v4: nx_int = nx_audit_process_event(cfg, ws, e4,
107 outfit_keys, outfit_counts, 8,
108 pose_keys, pose_counts, 8)
109 if v4 != NX_CONT_CONSISTENT { return 5 }
110
111 // ===== Pool utilization: outfit_id=200 across 4 scenes -> 100% =
112 let outfit_total: *i64 = (sys_mmap(8)) as *i64
113 let outfit_share: nx_int = nx_pool_hist_max_share_q10(outfit_counts, 8, outfit_total)
114 if outfit_total[0] != 4 { return 10 }
115 // 4/4 = 1024 Q10
116 if outfit_share != 1024 { return 11 }
117 if nx_util_classify_q10(outfit_share, outfit_total[0]) != NX_UTIL_SEVERE_BIAS { return 12 }
118
119 let pose_total: *i64 = (sys_mmap(8)) as *i64
120 let pose_share: nx_int = nx_pool_hist_max_share_q10(pose_counts, 8, pose_total)
121 if pose_total[0] != 4 { return 13 }
122 if pose_share != 1024 { return 14 }
123 if nx_util_classify_q10(pose_share, pose_total[0]) != NX_UTIL_SEVERE_BIAS { return 15 }
124
125 // ===== Distinct counts ========================================
126 if nx_pool_hist_distinct(outfit_keys, 8) != 1 { return 16 }
127 if nx_pool_hist_distinct(pose_keys, 8) != 1 { return 17 }
128
129 // ===== Verdict-band coverage =================================
130 //
131 // Build a synthetic 2-id case to exercise MILD_BIAS + UNIFORM
132 // without rebuilding the world.
133 let alt_keys: *i64 = (sys_mmap(64)) as *i64
134 let alt_counts: *i64 = (sys_mmap(64)) as *i64
135 nx_pool_hist_init(alt_keys, alt_counts, 8)
136 // 60% / 40% split
137 nx_pool_hist_bump(alt_keys, alt_counts, 8, 100); nx_pool_hist_bump(alt_keys, alt_counts, 8, 100)
138 nx_pool_hist_bump(alt_keys, alt_counts, 8, 100); nx_pool_hist_bump(alt_keys, alt_counts, 8, 100)
139 nx_pool_hist_bump(alt_keys, alt_counts, 8, 100); nx_pool_hist_bump(alt_keys, alt_counts, 8, 100)
140 nx_pool_hist_bump(alt_keys, alt_counts, 8, 200); nx_pool_hist_bump(alt_keys, alt_counts, 8, 200)
141 nx_pool_hist_bump(alt_keys, alt_counts, 8, 200); nx_pool_hist_bump(alt_keys, alt_counts, 8, 200)
142 let alt_total: *i64 = (sys_mmap(8)) as *i64
143 let alt_share: nx_int = nx_pool_hist_max_share_q10(alt_counts, 8, alt_total)
144 if nx_util_classify_q10(alt_share, alt_total[0]) != NX_UTIL_MILD_BIAS { return 20 }
145
146 // Uniform case: 4 ids, 1 each
147 nx_pool_hist_init(alt_keys, alt_counts, 8)
148 nx_pool_hist_bump(alt_keys, alt_counts, 8, 100)
149 nx_pool_hist_bump(alt_keys, alt_counts, 8, 200)
150 nx_pool_hist_bump(alt_keys, alt_counts, 8, 300)
151 nx_pool_hist_bump(alt_keys, alt_counts, 8, 400)
152 let unif_share: nx_int = nx_pool_hist_max_share_q10(alt_counts, 8, alt_total)
153 if nx_util_classify_q10(unif_share, alt_total[0]) != NX_UTIL_UNIFORM { return 21 }
154
155 // Empty case
156 nx_pool_hist_init(alt_keys, alt_counts, 8)
157 let empty_share: nx_int = nx_pool_hist_max_share_q10(alt_counts, 8, alt_total)
158 if nx_util_classify_q10(empty_share, alt_total[0]) != NX_UTIL_EMPTY { return 22 }
159
160 // ===== Summary span emission (re-enable trace so emit fires) ==
161 nx_trace_enable(cfg)
162 nx_audit_emit_batch_summary(cfg, 1000, 0 - 1,
163 4, n_violations,
164 outfit_keys, outfit_counts, 8,
165 pose_keys, pose_counts, 8)
166
167 println("nx_batch_audit_sidecar: live-fire shape PASS" as *u8)
168 return 0
169}