code wiki / (root) / nx_batch_audit_sidecar.nx

nx_batch_audit_sidecar.nx source

↩ module page · 265 lines · 9725 B

1// nx_batch_audit_sidecar.nx -- READ-ONLY observability sidecar for 2// live-fire batch monitoring. 3// 4// Architecture: This is "Step C" of the surgical 3-step wedge. 5// Steps A (nx_world_state) and B (nx_trace_emit) are the foundation; 6// this sidecar threads them. It does NOT change picks, prompts, or 7// renders -- it observes the existing system's decisions and emits 8// trace spans + continuity verdicts so the user can SEE what's 9// happening instead of staring at a black box. 10// 11// Assumed input schema (one event per JSONL line; production host 12// converts to the in-memory AuditEvent struct via a thin reader, 13// can be NishiLang nx_json parser or Python boundary code): 14// 15// {"batch_id":42,"scene_seq":3,"trace_id":100, 16// "character_id":7,"arc_id":10,"stage_idx":1, 17// "outfit_id":200,"location_id":101,"pose_id":300, 18// "affect_axis":8,"camera_id":500, 19// "day_clock_min":420,"beat_armed":3} 20// 21// "beat_armed" maps to NX_WS_BEAT_* and tells the sidecar whether 22// the existing picker already authorised an outfit/location change 23// at this step. Omitting it -> NX_WS_BEAT_NONE -> any change flags 24// a continuity violation. 25// 26// Pool-utilization tracking: the sidecar maintains parallel 27// histograms (outfit_pool, pose_pool, location_pool, arc_pool) so 28// at batch close it can name the under-sampled IDs. 29// 30// genealogy_id: canvas_qa_verification + opentelemetry_traces + 31// helicone_2025_proxy_observability 32// lineage_id: batch_audit_sidecar_v1 33 34// nx_safety_envelope: 35// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 36// sil_target: SIL1 37// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 38// verdict: NOT_YET_EVALUATED 39 40import "nx_syscalls.nx" 41import "nx_runtime.nx" 42import "nx_tier.nx" 43import "nx_directors_note.nx" 44import "nx_world_state.nx" 45import "nx_trace_emit.nx" 46const NX_MAGIC_1024: i64 = 1024 47 48// ===== AuditEvent struct ========================================== 49 50struct AuditEvent { 51 batch_id: nx_int, 52 scene_seq: nx_int, 53 trace_id: nx_int, 54 character_id: nx_int, 55 arc_id: nx_int, 56 stage_idx: nx_int, 57 outfit_id: nx_int, 58 location_id: nx_int, 59 pose_id: nx_int, 60 affect_axis: nx_int, 61 camera_id: nx_int, 62 day_clock_min: nx_int, 63 beat_armed: nx_int 64} 65 66const NX_AE_BYTES: nx_int = 104 // 13 fields * 8 67 68func nx_audit_event_alloc() -> *AuditEvent { 69 let e: *AuditEvent = (sys_mmap(NX_AE_BYTES)) as *AuditEvent 70 e.batch_id = 0 71 e.scene_seq = 0 72 e.trace_id = 0 73 e.character_id = 0 74 e.arc_id = 0 75 e.stage_idx = 0 76 e.outfit_id = 0 77 e.location_id = 0 78 e.pose_id = 0 79 e.affect_axis = NX_AF_NEUTRAL 80 e.camera_id = 0 81 e.day_clock_min = 0 82 e.beat_armed = NX_WS_BEAT_NONE 83 return e 84} 85 86// ===== Pool-utilization histogram ================================= 87// 88// Open-addressed [id -> count] table per axis. Caller supplies one 89// table per pool kind (outfit / pose / location / arc / camera / 90// affect); we treat all uniformly. Cap must be power of two. 91 92const NX_POOL_EMPTY_KEY: nx_int = 0 93 94func nx_pool_hist_init(keys: *i64, counts: *i64, cap: nx_int) -> nx_int { 95 var i: nx_int = 0 96 while i < cap { 97 keys[i] = NX_POOL_EMPTY_KEY 98 counts[i] = 0 99 i = i + 1 100 } 101 return 0 102} 103 104func nx_pool_hist_bump(keys: *i64, counts: *i64, cap: nx_int, 105 id: nx_int) -> nx_int { 106 if id == NX_POOL_EMPTY_KEY { return 0 } 107 let mask: nx_int = cap - 1 108 var slot: nx_int = id & mask 109 if slot < 0 { slot = slot + cap } 110 var placed: nx_int = 0 111 while placed == 0 { 112 if keys[slot] == NX_POOL_EMPTY_KEY { 113 keys[slot] = id 114 counts[slot] = 1 115 placed = 1 116 } 117 if placed == 0 { 118 if keys[slot] == id { 119 counts[slot] = counts[slot] + 1 120 placed = 1 121 } 122 } 123 if placed == 0 { 124 slot = (slot + 1) & mask 125 if slot < 0 { slot = slot + cap } 126 } 127 } 128 return 0 129} 130 131func nx_pool_hist_distinct(keys: *i64, cap: nx_int) -> nx_int { 132 var n: nx_int = 0 133 var i: nx_int = 0 134 while i < cap { 135 if keys[i] != NX_POOL_EMPTY_KEY { n = n + 1 } 136 i = i + 1 137 } 138 return n 139} 140 141// ===== Sealed-enum: utilization band ============================== 142 143const NX_UTIL_EMPTY: nx_int = 0 // nothing picked 144const NX_UTIL_SEVERE_BIAS: nx_int = 1 // one id dominates >75% 145const NX_UTIL_MILD_BIAS: nx_int = 2 // 50%-75% from one id 146const NX_UTIL_UNIFORM: nx_int = 3 // close to uniform 147const NX_UTIL_N_BANDS: nx_int = 4 148 149// Classify based on max-share. Q10. 150func nx_util_classify_q10(max_share_q10: nx_int, total: nx_int) -> nx_int { 151 if total <= 0 { return NX_UTIL_EMPTY } 152 if max_share_q10 >= 768 { return NX_UTIL_SEVERE_BIAS } // >= 75% 153 if max_share_q10 >= 512 { return NX_UTIL_MILD_BIAS } // >= 50% 154 return NX_UTIL_UNIFORM 155} 156 157func nx_pool_hist_max_share_q10(counts: *i64, cap: nx_int, 158 total_out: *i64) -> nx_int { 159 var total: nx_int = 0 160 var max_c: nx_int = 0 161 var i: nx_int = 0 162 while i < cap { 163 let c: nx_int = counts[i] 164 total = total + c 165 if c > max_c { max_c = c } 166 i = i + 1 167 } 168 total_out[0] = total 169 if total <= 0 { return 0 } 170 return (max_c * NX_MAGIC_1024) / total 171} 172 173// ===== Event processing =========================================== 174// 175// One event -> apply to world_state -> run continuity_check -> emit 176// trace span(s) -> bump pool histograms. No prompt or pick changes. 177 178func nx_audit_process_event(cfg: *TraceCfg, ws: *WorldState, 179 evt: *AuditEvent, 180 outfit_keys: *i64, outfit_counts: *i64, outfit_cap: nx_int, 181 pose_keys: *i64, pose_counts: *i64, pose_cap: nx_int) -> nx_int { 182 // Materialise the event as a DirectorsNote so continuity_check 183 // works without a separate code path. 184 let dn: *DirectorsNote = nx_dn_alloc() 185 dn.character_id = evt.character_id 186 dn.body_archetype_id = 1 // unknown from event; sidecar doesn't model archetype 187 dn.outfit_key_id = evt.outfit_id 188 dn.location_id = evt.location_id 189 dn.pose_id = evt.pose_id 190 dn.affect_axis = evt.affect_axis 191 dn.camera_motivation_id = evt.camera_id 192 dn.activity_id = evt.pose_id // proxy until activity_id lands on event schema 193 194 // Apply armed beat (whatever the live picker said it authorised) 195 nx_world_arm_beat(ws, evt.beat_armed) 196 197 // Continuity check 198 let verdict: nx_int = nx_continuity_check(ws, dn) 199 200 // Emit per-event span: continuity verdict as the attribute. 201 let evt_span: nx_int = nx_trace_mint_span_id(cfg) 202 nx_trace_emit_one_attr(cfg, evt.trace_id, evt_span, 203 0 - 1, NX_SPAN_CONTINUITY_CHECK, 204 evt.scene_seq * 100, 205 evt.scene_seq * 100 + 1, 206 0xCC07, verdict) 207 208 // Commit ONLY if verdict was consistent -- otherwise the world 209 // state would chase a known-bad transition and propagate the 210 // breakage downstream. Pool bumps still fire either way because 211 // the live system DID pick those IDs. 212 if verdict == NX_CONT_CONSISTENT { 213 nx_world_commit_scene(ws, dn, evt.day_clock_min) 214 } 215 216 // Pool histograms 217 nx_pool_hist_bump(outfit_keys, outfit_counts, outfit_cap, evt.outfit_id) 218 nx_pool_hist_bump(pose_keys, pose_counts, pose_cap, evt.pose_id) 219 220 return verdict 221} 222 223// ===== Batch close: emit summary span ============================= 224// 225// Aggregates pool histograms into utilization bands and emits a 226// single summary span the user (or downstream dashboard) can read. 227// 228// outfit_attr_key = FNV-style key hash for the "outfit_util_band" 229// attribute (caller-supplied so names are reproducible across runs). 230 231func nx_audit_emit_batch_summary(cfg: *TraceCfg, trace_id: nx_int, 232 parent: nx_int, 233 n_scenes: nx_int, n_violations: nx_int, 234 outfit_keys: *i64, outfit_counts: *i64, outfit_cap: nx_int, 235 pose_keys: *i64, pose_counts: *i64, pose_cap: nx_int) -> nx_int { 236 let outfit_total: *i64 = (sys_mmap(8)) as *i64 237 let pose_total: *i64 = (sys_mmap(8)) as *i64 238 let outfit_share: nx_int = nx_pool_hist_max_share_q10(outfit_counts, outfit_cap, outfit_total) 239 let pose_share: nx_int = nx_pool_hist_max_share_q10(pose_counts, pose_cap, pose_total) 240 241 let outfit_band: nx_int = nx_util_classify_q10(outfit_share, outfit_total[0]) 242 let pose_band: nx_int = nx_util_classify_q10(pose_share, pose_total[0]) 243 244 let outfit_distinct: nx_int = nx_pool_hist_distinct(outfit_keys, outfit_cap) 245 let pose_distinct: nx_int = nx_pool_hist_distinct(pose_keys, pose_cap) 246 247 let attrs: *i64 = (sys_mmap(96)) as *i64 248 attrs[0] = 0x5C01 // "n_scenes" 249 attrs[1] = n_scenes 250 attrs[2] = 0xCC07 // "n_violations" 251 attrs[3] = n_violations 252 attrs[4] = 0x0FBA // "outfit_band" 253 attrs[5] = outfit_band 254 attrs[6] = 0xFBD1 // "outfit_distinct" 255 attrs[7] = outfit_distinct 256 attrs[8] = 0xB05E // "pose_band" 257 attrs[9] = pose_band 258 attrs[10] = 0xBD15 // "pose_distinct" 259 attrs[11] = pose_distinct 260 261 let close_span: nx_int = nx_trace_mint_span_id(cfg) 262 return nx_trace_emit_span(cfg, trace_id, close_span, parent, 263 NX_SPAN_BATCH_CLOSE, 0, n_scenes * 100, 264 attrs, 6) 265}