code wiki / (root) / nx_term_extract_test.nx

nx_term_extract_test.nx source

↩ module page · 172 lines · 6431 B

1// nx_term_extract_test.nx -- end-to-end smoke for the learning ingestion. 2// 3// Proves the discovery pipeline learns "apron" / "kitchen" / etc. 4// from ingested prose WITHOUT any pre-seeded outfit/location word 5// list. The registry grows itself. 6// 7// Exit code: 8// 0 all assertions pass 9// N assertion N failed 10 11import "syscalls.nx" 12import "nx_media_pool.nx" 13import "nx_storybeat.nx" 14import "nx_storydb.nx" 15import "nx_text_ingest.nx" 16import "nx_phrase_match.nx" 17import "nx_term_registry.nx" 18import "nx_noun_phrase_scan.nx" 19import "nx_term_extract.nx" 20 21// Compare a canonical-arena entry to a literal lowercase string. 22// Returns 1 on equal, 0 otherwise. 23func eq_canon(reg: *TermRegistry, slot: i64, lit: *u8, llen: i64) -> i64 { 24 if slot < 0 { return 0 } 25 let t: *Term = nx_term_at(reg, slot) 26 if t.canon_len != llen { return 0 } 27 var i: i64 = 0 28 while i < llen { 29 let a: i64 = ((reg.str_arena as i64) + t.canon_off + i) as *u8 30 let lit_p: *u8 = ((lit as i64) + i) as *u8 31 if *(a as *u8) != *lit_p { return 0 } 32 i = i + 1 33 } 34 return 1 35} 36 37func main() -> i64 { 38 39 // ---------- Block 10: tiny phrase scan ------------------------- 40 // 41 // "She walked into the kitchen wearing an apron and smiled." 42 // Expected candidates: 43 // - "into the " cue -> "kitchen" (LOCATION, conf 6500) 44 // - "wearing " cue -> "apron" (OUTFIT, conf 8500) 45 // - "she " cue -> "walked" (ACTION, conf 5000) 46 let buf: *u8 = sys_mmap(256) 47 var k: i64 = 0 48 while k < 256 { buf[k] = 0; k = k + 1 } 49 let prose: *u8 = "She walked into the kitchen wearing an apron and smiled." 50 var ci: i64 = 0 51 while prose[ci] != 0 { 52 buf[ci] = prose[ci] 53 ci = ci + 1 54 } 55 let plen: i64 = ci 56 57 let cand_buf: *u8 = sys_mmap(32 * NX_NP_CAND_BYTES) 58 let cands: *PhraseCandidate = cand_buf as *PhraseCandidate 59 let n: i64 = nx_noun_phrase_scan(buf, 0, plen, cands, 32) 60 if n < 2 { return 10 } // expect at least kitchen + apron 61 62 // Helper: confirm at least one candidate has the lowercase 63 // canonical bytes matching the literal AND role_hint matches. 64 var saw_kitchen_loc: i64 = 0 65 var saw_apron_outfit: i64 = 0 66 var ki: i64 = 0 67 while ki < n { 68 let c: *PhraseCandidate = 69 (((cand_buf as i64) + ki * NX_NP_CAND_BYTES) as *PhraseCandidate) 70 if c.role_hint == NX_TERM_ROLE_LOCATION { 71 // "kitchen" lowercase = bytes 0x6B 0x69 0x74 0x63 0x68 0x65 0x6E (7 bytes) 72 if c.span_end - c.span_start == 7 { 73 let p0: i64 = nx_pm_to_lower(buf[c.span_start]) 74 if p0 == 0x6B { saw_kitchen_loc = 1 } 75 } 76 } 77 if c.role_hint == NX_TERM_ROLE_OUTFIT { 78 // "apron" lowercase = 0x61 0x70 0x72 0x6F 0x6E (5 bytes) 79 if c.span_end - c.span_start == 5 { 80 let p0: i64 = nx_pm_to_lower(buf[c.span_start]) 81 if p0 == 0x61 { saw_apron_outfit = 1 } 82 } 83 } 84 ki = ki + 1 85 } 86 if saw_kitchen_loc != 1 { return 11 } 87 if saw_apron_outfit != 1 { return 12 } 88 89 // ---------- Block 20: full ingest + extract pipeline ---------- 90 let big_buf: *u8 = sys_mmap(1024) 91 var bk: i64 = 0 92 while bk < 1024 { big_buf[bk] = 0; bk = bk + 1 } 93 let story: *u8 = "Sarah moved into the bedroom and slipped into the silk robe.\n\nLater she walked into the kitchen wearing an apron.\n" 94 var si: i64 = 0 95 while story[si] != 0 { 96 big_buf[si] = story[si] 97 si = si + 1 98 } 99 let slen: i64 = si 100 101 let db: *StoryDb = nx_storydb_alloc() 102 let mid: i64 = nx_media_pool_add(db.media_pool, NX_MEDIA_TYPE_TEXT, 103 NX_MEDIA_SRC_USER_PASTE, 104 big_buf, slen, 105 0 as *u8, 0, 106 0 as *u8, 0, 107 1700000000) 108 if mid < 0 { return 20 } 109 if nx_text_ingest(db, mid) != NX_TEXT_INGEST_OK { return 21 } 110 if db.n_beats < 2 { return 22 } 111 112 let reg: *TermRegistry = nx_term_registry_alloc() 113 if reg.n_terms != 0 { return 23 } 114 if nx_term_extract_all(db, reg) != NX_EXTR_OK { return 24 } 115 116 // Registry must have learned "bedroom" + "kitchen" (LOCATION) 117 // and "robe" + "apron" (OUTFIT) from context. We don't know 118 // their slot ids in advance because the registry assigns them 119 // by hash, so we walk the registry and count role coverage. 120 if reg.n_terms < 4 { return 25 } 121 var n_outfit: i64 = 0 122 var n_loc: i64 = 0 123 var ti: i64 = 0 124 let TBUDGET: i64 = reg.cap + 1 125 var titer: i64 = 0 126 while ti < reg.cap { 127 if titer >= TBUDGET { return 26 } 128 let t: *Term = nx_term_at(reg, ti) 129 if t.canon_hash != 0 { 130 if t.role == NX_TERM_ROLE_OUTFIT { n_outfit = n_outfit + 1 } 131 if t.role == NX_TERM_ROLE_LOCATION { n_loc = n_loc + 1 } 132 } 133 ti = ti + 1 134 titer = titer + 1 135 } 136 if n_outfit < 1 { return 27 } // robe or apron 137 if n_loc < 1 { return 28 } // bedroom or kitchen 138 139 // ---------- Block 30: beat back-fill --------------------------- 140 // 141 // The second beat (kitchen + apron paragraph) should have 142 // outfit_id and location_id populated to non-zero registry 143 // slots. (Slot 0 is the first inserted term, also valid; 144 // we just check >= 0 with non-default.) 145 // 146 // To find the right beat, we look for the one whose span 147 // contains "kitchen". Whichever beat index that is, it should 148 // have both outfit_id and location_id set. 149 var found_beat: i64 = -1 150 var bi: i64 = 0 151 while bi < db.n_beats { 152 let b: *StoryBeat = nx_storydb_beat_at(db, bi) 153 if b.beat_kind != NX_BEAT_HEADING { 154 // Check span contains lowercase "kitchen". 155 let hit: i64 = nx_phrase_find_ci(big_buf, slen, 156 "kitchen" as *u8, 7, 0) 157 if hit >= b.span_start { 158 if hit < b.span_end { found_beat = bi } 159 } 160 } 161 bi = bi + 1 162 } 163 if found_beat < 0 { return 30 } 164 let beat_k: *StoryBeat = nx_storydb_beat_at(db, found_beat) 165 // At least one of outfit_id or location_id must be set; we 166 // require at least one for V1. 167 if beat_k.outfit_id == 0 { 168 if beat_k.location_id == 0 { return 31 } 169 } 170 171 return 0 172}