code wiki / (root) / nx_arc_anchor.nx

nx_arc_anchor.nx source

↩ module page · 155 lines · 6235 B

1// nx_arc_anchor.nx -- discover recurring scene-motif tuples. 2// 3// An "arc anchor" is a tuple of co-occurring terms that recur 4// across the ingested corpus and together identify a scene 5// archetype: e.g. (OUTFIT=apron, LOCATION=kitchen) -> "domestic 6// intimacy" anchor; (OUTFIT=robe, LOCATION=bedroom) -> "morning 7// intimate" anchor. 8// 9// Discovery is BOTTOM-UP: nx_term_cooccur counts pairs across 10// beats, and we surface the top-K pairs whose role combination 11// matches one of the anchor templates. No hand-written motif 12// list -- the templates are STRUCTURAL (which role-pairs are 13// scene-archetypal) and the actual terms come from the registry. 14// 15// Per cardinals: 16// feedback-self-surfacing-intelligence: 17// anchors enter at STAGE1 SUPERVISED. Promotion happens 18// when the anchor recurs in a fresh corpus, with sighting 19// count crossing NX_ARC_ANCHOR_PROMOTE_N (3) and zero 20// user-rejections. 21// feedback-build-intelligence-never-strip-features: 22// anchors with weak signals stay surfaced as STAGE1 23// candidates rather than getting filtered out; the user 24// reviews and pins / dismisses. 25// 26// nx_safety_envelope: 27// intended_use: "Surface recurring (outfit + location + 28// character?) scene-archetype tuples from 29// co-occurrence counts." 30// sil_target: SIL2 31// asil_target: QM 32// dal_target: DAL C 33// iec_62304_class: NONE 34// evidence: [no_floating_point, 35// bounded_loops_per_jpl_rule_2, 36// sealed_anchor_kind_enum, 37// anchor_template_is_structural_not_topical] 38// hazard_register: [bug-tape-anchor-spurious-on-tiny-corpus, 39// bug-tape-anchor-misses-three-term-tuples] 40// residual_risk: "V1 surfaces only 2-term anchors (the most 41// common pair role). 3-term (outfit + 42// location + character) anchors are V2." 43// verdict: NOT_YET_EVALUATED 44 45import "nx_syscalls.nx" 46import "nx_term_registry.nx" 47import "nx_term_cooccur.nx" 48 49// ===== sealed anchor kinds ======================================== 50// 51// What KIND of role-pair this anchor represents. Caller dispatches 52// on this to decide which director-initiative pipeline to feed. 53 54const NX_ANCHOR_UNKNOWN: i64 = 0 55const NX_ANCHOR_OUTFIT_LOCATION: i64 = 1 // apron + kitchen 56const NX_ANCHOR_OUTFIT_CHARACTER: i64 = 2 // suit + boss 57const NX_ANCHOR_LOCATION_CHARACTER: i64 = 3 // bedroom + spouse 58const NX_ANCHOR_OUTFIT_OUTFIT: i64 = 4 // layered clothing 59const NX_ANCHOR_LOCATION_LOCATION: i64 = 5 // adjacent rooms 60 61// ===== promotion threshold ======================================== 62 63const NX_ARC_ANCHOR_PROMOTE_N: i64 = 3 64 65// ===== ArcAnchor record =========================================== 66 67struct ArcAnchor { 68 anchor_kind: i64, // NX_ANCHOR_* 69 term_a: i64, // registry slot 70 term_b: i64, // registry slot 71 role_a: i64, // copy of registry.term[term_a].role 72 role_b: i64, 73 cooccur_count: i64, // joint sightings (= cooccur pair count) 74 stage: i64, // NX_TERM_STAGE_* 75 user_pinned: i64, 76} 77 78const NX_ARC_ANCHOR_BYTES: i64 = 64 79 80// ===== classify a pair to anchor kind ============================= 81 82func nx_arc_anchor_classify(role_a: i64, role_b: i64) -> i64 { 83 // Normalize so the lower role is first. 84 var ra: i64 = role_a 85 var rb: i64 = role_b 86 if ra > rb { 87 let tmp: i64 = ra 88 ra = rb 89 rb = tmp 90 } 91 if ra == NX_TERM_ROLE_OUTFIT { 92 if rb == NX_TERM_ROLE_LOCATION { return NX_ANCHOR_OUTFIT_LOCATION } 93 if rb == NX_TERM_ROLE_CHARACTER { return NX_ANCHOR_OUTFIT_CHARACTER } 94 if rb == NX_TERM_ROLE_OUTFIT { return NX_ANCHOR_OUTFIT_OUTFIT } 95 } 96 if ra == NX_TERM_ROLE_LOCATION { 97 if rb == NX_TERM_ROLE_CHARACTER { return NX_ANCHOR_LOCATION_CHARACTER } 98 if rb == NX_TERM_ROLE_LOCATION { return NX_ANCHOR_LOCATION_LOCATION } 99 } 100 return NX_ANCHOR_UNKNOWN 101} 102 103// ===== discover anchors =========================================== 104// 105// Walks the cooccur table's top-K pairs. For each pair whose 106// roles classify to a known anchor kind, emits an ArcAnchor. 107// Caller passes (out, out_cap) and a scratch top-K buffer. 108// 109// Returns count of anchors emitted. 110 111func nx_arc_anchor_discover(co: *Cooccur, reg: *TermRegistry, 112 scratch: *CooccurPair, scratch_cap: i64, 113 out: *ArcAnchor, out_cap: i64) -> i64 { 114 if out_cap <= 0 { return 0 } 115 let n: i64 = nx_cooccur_top_k(co, scratch, scratch_cap) 116 if n <= 0 { return 0 } 117 var emitted: i64 = 0 118 let BUDGET: i64 = n + 1 119 var iter: i64 = 0 120 var i: i64 = 0 121 while i < n { 122 if iter >= BUDGET { return emitted } 123 if emitted >= out_cap { return emitted } 124 let p: *CooccurPair = 125 (((scratch as i64) + i * NX_COOCCUR_PAIR_BYTES) as *CooccurPair) 126 if p.pair_key == 0 { i = n } 127 if i < n { 128 let term_a: *Term = nx_term_at(reg, p.term_a) 129 let term_b: *Term = nx_term_at(reg, p.term_b) 130 let kind: i64 = nx_arc_anchor_classify(term_a.role, term_b.role) 131 if kind != NX_ANCHOR_UNKNOWN { 132 let dst: *ArcAnchor = 133 (((out as i64) + emitted * NX_ARC_ANCHOR_BYTES) 134 as *ArcAnchor) 135 dst.anchor_kind = kind 136 dst.term_a = p.term_a 137 dst.term_b = p.term_b 138 dst.role_a = term_a.role 139 dst.role_b = term_b.role 140 dst.cooccur_count = p.count 141 dst.user_pinned = 0 142 if p.count >= NX_ARC_ANCHOR_PROMOTE_N { 143 dst.stage = NX_TERM_STAGE_PROMPTED 144 } 145 if p.count < NX_ARC_ANCHOR_PROMOTE_N { 146 dst.stage = NX_TERM_STAGE_SUPERVISED 147 } 148 emitted = emitted + 1 149 } 150 i = i + 1 151 } 152 iter = iter + 1 153 } 154 return emitted 155}