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}