code wiki / (root) / nx_scene_index.nx

nx_scene_index.nx source

↩ module page · 242 lines · 9264 B

1// nx_scene_index.nx -- companion-queryable surface over discovered anchors. 2// 3// Top of the ingestion stack. Turns each ArcAnchor (an abstract 4// "(role + role) recurs in this corpus") into a concrete 5// SceneCandidate the companion's autonomous-trigger layer can 6// query and surface as a surprise scene. 7// 8// Each SceneCandidate carries: 9// - anchor_kind (e.g. NX_ANCHOR_OUTFIT_LOCATION) 10// - registry slot ids for the two terms 11// - the BEST-FIT representative beat (the one that most strongly 12// instances this anchor in the corpus) 13// - the byte span (start, end) within the source media so the 14// companion can quote / re-render the scene verbatim 15// - first / last sighting beat ids (recency for ranking) 16// - sighting count (frequency for ranking) 17// - stage (STAGE1 SUPERVISED today; STAGE3 once user pins) 18// 19// Per cardinals: 20// feedback-self-surfacing-intelligence-staged-autonomy: 21// SceneCandidates default STAGE1 SUPERVISED -- the companion 22// PROPOSES to the user before surfacing. STAGE2 PROMPTED at 23// anchor count >= NX_ARC_ANCHOR_PROMOTE_N. STAGE3 once user 24// explicitly pins. 25// feedback-launching-content-must-be-one-command-easy: 26// SceneIndex is the substrate primitive a single one-command 27// surfacer (nx_companion_surprise.nx, future) wires through. 28// 29// nx_safety_envelope: 30// intended_use: "Companion-side queryable index of scene 31// candidates derived from discovered 32// ArcAnchors + source-beat back-references." 33// sil_target: SIL2 34// asil_target: QM 35// dal_target: DAL C 36// iec_62304_class: NONE 37// evidence: [no_floating_point, 38// bounded_loops_per_jpl_rule_2, 39// anchor_to_candidate_one_to_one, 40// source_span_preserved_for_audit] 41// hazard_register: [bug-tape-best-beat-tie-breaker-undefined, 42// bug-tape-stale-source-span-after-corpus-purge] 43// residual_risk: "Best-beat selection is highest matching- 44// role-id sum today; ties broken by lowest 45// beat_id. V2 incorporates recency + affect 46// + user-pin weights." 47// verdict: NOT_YET_EVALUATED 48 49import "nx_syscalls.nx" 50import "nx_storybeat.nx" 51import "nx_storydb.nx" 52import "nx_media_pool.nx" 53import "nx_term_registry.nx" 54import "nx_arc_anchor.nx" 55 56// ===== SceneCandidate record ====================================== 57// 58// Fixed 96 bytes (12 * i64). Stable schema; append-only. 59 60struct SceneCandidate { 61 anchor_kind: i64, // NX_ANCHOR_* 62 term_a: i64, // registry slot id 63 term_b: i64, // registry slot id 64 role_a: i64, // NX_TERM_ROLE_* 65 role_b: i64, 66 best_beat_id: i64, // index into StoryDb.beats 67 source_media: i64, // media_id pointing into StoryDb.media_pool 68 source_start: i64, // byte offset within source bytes 69 source_end: i64, // exclusive 70 sighting_count: i64, // anchor.cooccur_count (frequency rank) 71 stage: i64, // NX_TERM_STAGE_* 72 flags: i64, // bit 0 = user_pinned 73} 74 75const NX_SCENE_CAND_BYTES: i64 = 96 76 77const NX_SCENE_FLAG_USER_PINNED: i64 = 1 78 79// ===== SceneIndex container ======================================= 80 81const NX_SCENE_INDEX_DEFAULT_CAP: i64 = 256 82 83struct SceneIndex { 84 candidates: *SceneCandidate, 85 n_candidates: i64, 86 cap: i64, 87} 88 89const NX_SCENE_INDEX_BYTES: i64 = 24 90 91// ===== allocation ================================================= 92 93func nx_scene_index_alloc_sized(cap: i64) -> *SceneIndex { 94 var c: i64 = cap 95 if c <= 0 { c = NX_SCENE_INDEX_DEFAULT_CAP } 96 let raw: *u8 = sys_mmap(NX_SCENE_INDEX_BYTES) 97 let s: *SceneIndex = raw as *SceneIndex 98 s.candidates = (sys_mmap(c * NX_SCENE_CAND_BYTES)) as *SceneCandidate 99 s.n_candidates = 0 100 s.cap = c 101 return s 102} 103 104func nx_scene_index_alloc() -> *SceneIndex { 105 return nx_scene_index_alloc_sized(0) 106} 107 108func nx_scene_candidate_at(s: *SceneIndex, i: i64) -> *SceneCandidate { 109 return (((s.candidates as i64) + i * NX_SCENE_CAND_BYTES) 110 as *SceneCandidate) 111} 112 113// ===== find best beat for an anchor ============================== 114// 115// Scan db.beats and return the index of the beat whose (outfit_id, 116// location_id, character_id) most strongly matches the anchor's 117// (term_a, term_b). Score = number of matched slots. Ties broken 118// by lowest beat_id (earliest in corpus). 119 120func nx_scene_index_match_beat(db: *StoryDb, 121 anchor_term_a: i64, 122 anchor_term_b: i64) -> i64 { 123 var best: i64 = -1 124 var best_score: i64 = 0 125 let BUDGET: i64 = db.n_beats + 1 126 var iter: i64 = 0 127 var i: i64 = 0 128 while i < db.n_beats { 129 if iter >= BUDGET { return best } 130 let b: *StoryBeat = nx_storydb_beat_at(db, i) 131 var score: i64 = 0 132 if b.outfit_id == anchor_term_a { score = score + 1 } 133 if b.outfit_id == anchor_term_b { score = score + 1 } 134 if b.location_id == anchor_term_a { score = score + 1 } 135 if b.location_id == anchor_term_b { score = score + 1 } 136 if b.character_id == anchor_term_a { score = score + 1 } 137 if b.character_id == anchor_term_b { score = score + 1 } 138 if score > best_score { 139 best = i 140 best_score = score 141 } 142 i = i + 1 143 iter = iter + 1 144 } 145 return best 146} 147 148// ===== build index from anchors =================================== 149// 150// For each ArcAnchor, find the best representative beat and emit 151// a SceneCandidate. Skips anchors whose best beat resolves to -1 152// (anchor doesn't actually appear in this StoryDb -- shouldn't 153// happen but defensive). 154 155func nx_scene_index_build(idx: *SceneIndex, db: *StoryDb, 156 anchors: *ArcAnchor, n_anchors: i64) -> i64 { 157 if n_anchors <= 0 { return 0 } 158 var emitted: i64 = 0 159 let BUDGET: i64 = n_anchors + 1 160 var iter: i64 = 0 161 var i: i64 = 0 162 while i < n_anchors { 163 if iter >= BUDGET { return emitted } 164 if idx.n_candidates >= idx.cap { return emitted } 165 let a: *ArcAnchor = 166 (((anchors as i64) + i * NX_ARC_ANCHOR_BYTES) as *ArcAnchor) 167 168 let best_beat: i64 = nx_scene_index_match_beat(db, a.term_a, a.term_b) 169 if best_beat >= 0 { 170 let b: *StoryBeat = nx_storydb_beat_at(db, best_beat) 171 let dst: *SceneCandidate = nx_scene_candidate_at(idx, 172 idx.n_candidates) 173 dst.anchor_kind = a.anchor_kind 174 dst.term_a = a.term_a 175 dst.term_b = a.term_b 176 dst.role_a = a.role_a 177 dst.role_b = a.role_b 178 dst.best_beat_id = best_beat 179 dst.source_media = b.media_id 180 dst.source_start = b.span_start 181 dst.source_end = b.span_end 182 dst.sighting_count = a.cooccur_count 183 dst.stage = a.stage 184 dst.flags = 0 185 if a.user_pinned != 0 { dst.flags = NX_SCENE_FLAG_USER_PINNED } 186 idx.n_candidates = idx.n_candidates + 1 187 emitted = emitted + 1 188 } 189 i = i + 1 190 iter = iter + 1 191 } 192 return emitted 193} 194 195// ===== query helpers ============================================== 196// 197// Two query patterns matter for the companion-side selector: 198// 199// (1) "Give me a candidate matching anchor_kind K." Used when the 200// companion wants a specific scene archetype (e.g. autonomous 201// surprise pulls from OUTFIT_LOCATION). 202// 203// (2) "Give me the top-N candidates by sighting_count." Used when 204// the companion is browsing what the corpus has yielded. 205 206func nx_scene_index_first_of_kind(idx: *SceneIndex, kind: i64) -> i64 { 207 let BUDGET: i64 = idx.n_candidates + 1 208 var iter: i64 = 0 209 var i: i64 = 0 210 while i < idx.n_candidates { 211 if iter >= BUDGET { return -1 } 212 let c: *SceneCandidate = nx_scene_candidate_at(idx, i) 213 if c.anchor_kind == kind { return i } 214 i = i + 1 215 iter = iter + 1 216 } 217 return -1 218} 219 220// Returns the byte span of the source for a candidate, via outparams. 221// Caller looks up MediaPool to get the actual bytes pointer. 222func nx_scene_index_source_span(idx: *SceneIndex, cand_id: i64, 223 out_media: *i64, 224 out_start: *i64, out_end: *i64) -> i64 { 225 if cand_id < 0 { return -1 } 226 if cand_id >= idx.n_candidates { return -1 } 227 let c: *SceneCandidate = nx_scene_candidate_at(idx, cand_id) 228 *out_media = c.source_media 229 *out_start = c.source_start 230 *out_end = c.source_end 231 return 0 232} 233 234// User-pin a candidate (promotes its stage to STAGE3 UNSUPERVISED). 235func nx_scene_index_pin(idx: *SceneIndex, cand_id: i64) -> i64 { 236 if cand_id < 0 { return -1 } 237 if cand_id >= idx.n_candidates { return -1 } 238 let c: *SceneCandidate = nx_scene_candidate_at(idx, cand_id) 239 c.flags = c.flags | NX_SCENE_FLAG_USER_PINNED 240 c.stage = 3 // NX_TERM_STAGE_UNSUPERVISED 241 return 0 242}