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}