code wiki / _hdl_build / nx_wiki_answer.nx
nx_wiki_answer.nx source
↩ module page · 439 lines · 23412 B
1// nx_wiki_answer.nx -- THE INSTANT-ANSWER organ for the live 12-page wiki corpus (IMS Thrust B rung 1).
2// answer(query) returns THE ANSWER -- a direct snippet + its source page -- in ONE step, NOT a results list.
3// This is the operator's "<10s / 1-click answer" surface: type a question, get the passage that answers it
4// with a source link, no second click into a SERP.
5//
6// FLOW (compose proven engines, ZERO ranking/snippet math reinvented):
7// 1. registry-driven site lookup (site=wiki -> idx + manifest + base_url) over the committed onsite registry
8// knowledge/index/onsite_sites.tsv -- the SAME data-driven model nx_onsite_search uses (per-site isolation
9// by construction: the wiki index file physically cannot return another site's docs).
10// 2. nx_inv_load (nx_search_inverted_persist) -> the durable inverted index; parse the manifest into
11// url/title(=slug)/body slices (docid = line), EXACTLY as nx_onsite_search does.
12// 3. BM25F top page: shortlist via nx_inv_query_term, score every candidate with bmf_score (nx_bm25f --
13// title-field=slug boost + body), pick top-1. Title hits (the slug) outrank body hits.
14// 4. best answer passage from that top page via nx_search_snippet_extract (densest-cluster window picker
15// below) + <mark> highlight over the query terms.
16// 5. threshold gate: if the top-1 BM25F score (x1e6) does NOT exceed wiki_answer_min_score (READ from the
17// params.tsv ledger -- no magic number, rule 11) -> WA_NO_ANSWER. We REFUSE rather than fabricate.
18//
19// REUSE: nx_search_inverted_persist (nx_inv_load + nx_inv_query_term), nx_bm25f (bmf_score/bm_token_count),
20// nx_search_snippet_extract (nx_search_snippet_extract + NxSearchQuery via nx_search_query_parser), nx_syscalls.
21// Library organ (no main when imported by the gate; the self-test main below makes it runnable standalone).
22// ADDITIVE-ONLY: reads index+manifest+params; writes nothing. license_tier: ORIGINAL
23import "nx_search_inverted_persist.nx"
24import "nx_bm25f.nx"
25import "nx_search_snippet_extract.nx"
26const WA_MAGIC_1000000: i64 = 1000000
27const WA_MAGIC_10000: i64 = 10000
28
29// ===== sealed verdict surface (codes 5200-5209; distinct from NX_INV_*/BMF_*/NX_SSE_*/VP_*) =========
30const WA_OK: i64 = 0
31const WA_NO_ANSWER: i64 = 5200 // top-1 below threshold OR no candidate matched -> refuse (no fabrication)
32const WA_BAD_INPUT: i64 = 5201
33const WA_NO_REGISTRY: i64 = 5202 // onsite registry missing / site row absent
34const WA_NO_INDEX: i64 = 5203 // index or manifest missing / stale pair
35const WA_QUERY_FAIL: i64 = 5204 // query parse failed
36
37// ===== named sizing constants (M7 no magic numbers) ================================================
38const WA_REG_PATH: *u8 = "knowledge/index/onsite_sites.tsv" // committed production onsite registry
39const WA_PARAMS_PATH: *u8 = "knowledge/registry/params.tsv" // measured-parameters ledger
40const WA_PARAM_NAME: *u8 = "wiki_answer_min_score" // the NO_ANSWER floor (BM25F x1e6)
41const WA_MIN_FALLBACK: i64 = 100000 // used ONLY if the params row is unreadable
42const WA_SITE: *u8 = "wiki"
43const WA_ROWID_CAP: i64 = 16384 // matches NX_INV_MAX_POSTINGS_PER
44const WA_MAX_TERMS: i64 = 16 // query terms considered for BM25F (parser caps at 32)
45const WA_SNIP_CAP: i64 = 2048 // snippet output buffer (>= NX_SSE_DEFAULT_OUTPUT_CAP)
46const WA_TERMS_CAP: i64 = 1024 // NxSearchQuery terms buffer (NX_SQP_TERMS_BUF_MIN_CAP)
47const WA_SLUG_CAP: i64 = 256
48const WA_DENSE_WIN: i64 = 240 // densest-cluster scan window (bytes) for the answer passage
49
50// ===== the answer result (caller-allocated) ========================================================
51// snippet : NUL-terminated <mark>-highlighted answer passage (densest-cluster window of query terms)
52// slug : NUL-terminated source slug (e.g. "access_wall") -- also the page title here
53// base_url : NUL-terminated site base (https://nishifamily.com/wiki/) so the caller forms slug.html link
54// score : BM25F top-1 score, x1e6 (the same scale onsite_search prints as score/1e6)
55// verdict : WA_OK or a WA_* code (WA_NO_ANSWER => snippet/slug are empty; do NOT render an answer)
56struct WaAnswer {
57 snippet: *u8,
58 slug: *u8,
59 base_url: *u8,
60 score: i64,
61 verdict: i64,
62}
63
64// ---- tiny local helpers (wa_ namespace) ----
65func wa_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
66func wa_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 }
67func wa_fold(s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { let c: i64 = s[i] as i64; if c >= 65 { if c <= 90 { s[i] = (c + 32) as u8 } } i = i + 1 } return 0 }
68
69// registry lookup: NUL-terminate the wiki row's 4 fields IN PLACE; return field pointers via out-params.
70// 1=found,0=not. Mirrors nx_onsite_search os_lookup_site (registry must end with a trailing newline).
71func wa_lookup_site(reg: *u8, n: i64, site: *u8, out_idx: *i64, out_mf: *i64, out_base: *i64) -> i64 {
72 var ls: i64 = 0; var i: i64 = 0
73 while i <= n {
74 var eol: i64 = 0
75 if i == n { eol = 1 } else { if reg[i] == (10 as u8) { eol = 1 } }
76 if eol == 1 {
77 if i > ls {
78 if reg[ls] != (35 as u8) { // skip '#' comment rows
79 var t1: i64 = 0 - 1; var t2: i64 = 0 - 1; var t3: i64 = 0 - 1
80 var k: i64 = ls
81 while k < i {
82 if reg[k] == (9 as u8) { if t1 < 0 { t1 = k } else { if t2 < 0 { t2 = k } else { if t3 < 0 { t3 = k } } } }
83 k = k + 1
84 }
85 if t1 > ls { if t2 > t1 { if t3 > t2 {
86 reg[t1] = 0 as u8; reg[t2] = 0 as u8; reg[t3] = 0 as u8
87 if i < n { reg[i] = 0 as u8 }
88 let sp: *u8 = ((reg as i64) + ls) as *u8
89 if wa_streq(sp, site) == 1 {
90 out_idx[0] = (reg as i64) + t1 + 1
91 out_mf[0] = (reg as i64) + t2 + 1
92 out_base[0] = (reg as i64) + t3 + 1
93 return 1
94 }
95 } } }
96 }
97 }
98 ls = i + 1
99 }
100 i = i + 1
101 }
102 return 0
103}
104
105// read the NO_ANSWER threshold from the params.tsv ledger: find the row whose first TAB-field == WA_PARAM_NAME,
106// parse the THIRD column (value) as a decimal. Returns the threshold, or WA_MIN_FALLBACK if unreadable.
107// Data-driven (rule 11) -- the floor lives in config, not in code.
108func wa_read_threshold() -> i64 {
109 let nbox: *i64 = sys_mmap(16) as *i64
110 nbox[0] = 0
111 let buf: *u8 = sys_read_file(WA_PARAMS_PATH, nbox)
112 if (buf as i64) == 0 { return WA_MIN_FALLBACK }
113 let n: i64 = nbox[0]
114 let namelen: i64 = wa_strlen(WA_PARAM_NAME)
115 var ls: i64 = 0; var i: i64 = 0
116 while i <= n {
117 var eol: i64 = 0
118 if i == n { eol = 1 } else { if buf[i] == (10 as u8) { eol = 1 } }
119 if eol == 1 {
120 if i > ls { if buf[ls] != (35 as u8) {
121 // first field = up to first TAB
122 var t1: i64 = 0 - 1; var k: i64 = ls
123 while k < i { if buf[k] == (9 as u8) { t1 = k; k = i } else { k = k + 1 } }
124 if t1 > ls {
125 var eq: i64 = 0
126 if t1 - ls == namelen {
127 eq = 1; var c: i64 = 0
128 // LM-030 FIX (2026-07-30): this loop indexed the const *u8 DIRECTLY, which
129 // compiles clean and reads GARBAGE -- so this comparison could never match and the
130 // params.tsv lookup silently fell through to WA_MIN_FALLBACK forever. Bind first.
131 let pname: *u8 = WA_PARAM_NAME
132 while c < namelen { if buf[ls + c] != pname[c] { eq = 0; c = namelen } else { c = c + 1 } }
133 }
134 if eq == 1 {
135 // find 2nd + 3rd TAB -> value field is between them
136 var t2: i64 = 0 - 1; var t3: i64 = 0 - 1; var p: i64 = t1 + 1
137 while p < i { if buf[p] == (9 as u8) { if t2 < 0 { t2 = p } else { if t3 < 0 { t3 = p; p = i } } } p = p + 1 }
138 if t2 > t1 { if t3 > t2 {
139 var v: i64 = 0; var q: i64 = t2 + 1; var any: i64 = 0
140 while q < t3 { let ch: i64 = buf[q] as i64; if ch >= 48 { if ch <= 57 { v = v * 10 + (ch - 48); any = 1 } } q = q + 1 }
141 if any == 1 { return v }
142 } }
143 }
144 }
145 } }
146 ls = i + 1
147 }
148 i = i + 1
149 }
150 return WA_MIN_FALLBACK
151}
152
153// densest-cluster answer passage: scan the doc body for the WA_DENSE_WIN-byte window containing the MOST
154// distinct query-term hits (Lucene-style, the V2 the snippet extractor's header flagged). Returns the byte
155// offset of the best window's START (clamped >=0), or the first-match offset, or 0. We then re-center the
156// snippet extractor on that offset by passing a body slice that begins there.
157func wa_best_window(body: *u8, blen: i64, qterms: *i64, qlens: *i64, nq: i64) -> i64 {
158 if blen <= WA_DENSE_WIN { return 0 }
159 var best_off: i64 = 0; var best_hits: i64 = 0 - 1
160 // step the window start by a stride (cheap; exact enough for short docs)
161 var s: i64 = 0
162 while s < blen {
163 let e0: i64 = s + WA_DENSE_WIN
164 var e: i64 = e0; if e > blen { e = blen }
165 var hits: i64 = 0
166 var t: i64 = 0
167 while t < nq {
168 let term: *u8 = qterms[t] as *u8
169 let tl: i64 = qlens[t]
170 // does term occur anywhere in [s,e)? (count distinct terms present, not total tf)
171 var found: i64 = 0
172 var p: i64 = s
173 while p + tl <= e {
174 var j: i64 = 0; var ok: i64 = 1
175 while j < tl { if body[p + j] != term[j] { ok = 0; j = tl } else { j = j + 1 } }
176 if ok == 1 { found = 1; p = e } else { p = p + 1 }
177 }
178 if found == 1 { hits = hits + 1 }
179 t = t + 1
180 }
181 if hits > best_hits { best_hits = hits; best_off = s }
182 s = s + 48 // stride < window so clusters aren't skipped
183 }
184 if best_hits <= 0 { return 0 }
185 return best_off
186}
187
188// ===== answer_ex: registry-prefix override for hermetic gate fixtures ===============================
189// reg_path = onsite registry; idx/manifest/base resolved from it by site=wiki. out = caller-allocated WaAnswer
190// (with snippet/slug/base_url buffers preallocated). Returns out.verdict.
191func answer_ex(reg_path: *u8, site: *u8, query: *u8, out: *WaAnswer) -> i64 {
192 out.score = 0
193 out.verdict = WA_BAD_INPUT
194 out.snippet[0] = 0 as u8
195 out.slug[0] = 0 as u8
196 out.base_url[0] = 0 as u8
197 if (query as i64) == 0 { return WA_BAD_INPUT }
198 if wa_strlen(query) < 1 { out.verdict = WA_NO_ANSWER; return WA_NO_ANSWER }
199
200 // 1. registry -> idx/manifest/base_url for this site
201 let regbox: *i64 = sys_mmap(16) as *i64
202 let reg: *u8 = sys_read_file(reg_path, regbox)
203 if (reg as i64) == 0 { out.verdict = WA_NO_REGISTRY; return WA_NO_REGISTRY }
204 let oidx: *i64 = sys_mmap(8) as *i64
205 let omf: *i64 = sys_mmap(8) as *i64
206 let obase: *i64 = sys_mmap(8) as *i64
207 if wa_lookup_site(reg, regbox[0], site, oidx, omf, obase) == 0 { out.verdict = WA_NO_REGISTRY; return WA_NO_REGISTRY }
208 let idx_path: *u8 = oidx[0] as *u8
209 let manifest_path: *u8 = omf[0] as *u8
210 let base_url: *u8 = obase[0] as *u8
211 // stash base_url into the output (copy; reg buffer is transient)
212 var bi: i64 = 0
213 while base_url[bi] != (0 as u8) { if bi < WA_SLUG_CAP - 1 { out.base_url[bi] = base_url[bi] } bi = bi + 1 }
214 out.base_url[bi] = 0 as u8
215
216 // 2. load index + parse manifest into url/title(slug)/body slices (docid = line)
217 let idx: *NxInvIndex = nx_inv_load(idx_path)
218 if idx == 0 as *NxInvIndex { out.verdict = WA_NO_INDEX; return WA_NO_INDEX }
219 let mbox: *i64 = sys_mmap(16) as *i64
220 let mbuf: *u8 = sys_read_file(manifest_path, mbox)
221 if (mbuf as i64) == 0 { out.verdict = WA_NO_INDEX; return WA_NO_INDEX }
222 let mn: i64 = mbox[0]
223 var nlines: i64 = 0; var ii: i64 = 0
224 while ii < mn { if mbuf[ii] == (10 as u8) { nlines = nlines + 1 } ii = ii + 1 }
225 if nlines <= 0 { out.verdict = WA_NO_INDEX; return WA_NO_INDEX }
226 let tp: *i64 = sys_mmap(8 * (nlines + 8)) as *i64 // title(slug) ptr
227 let tl: *i64 = sys_mmap(8 * (nlines + 8)) as *i64 // title len
228 let xp: *i64 = sys_mmap(8 * (nlines + 8)) as *i64 // body ptr
229 let xl: *i64 = sys_mmap(8 * (nlines + 8)) as *i64 // body len
230 var ndocs: i64 = 0; var ls: i64 = 0
231 ii = 0
232 while ii < mn {
233 if mbuf[ii] == (10 as u8) {
234 let le: i64 = ii
235 var f1: i64 = 0 - 1; var f2: i64 = 0 - 1; var k: i64 = ls
236 while k < le { if mbuf[k] == (9 as u8) { if f1 < 0 { f1 = k } else { if f2 < 0 { f2 = k } } } k = k + 1 }
237 if f1 >= ls { if f2 > f1 {
238 tp[ndocs] = (mbuf as i64) + f1 + 1; tl[ndocs] = f2 - f1 - 1
239 xp[ndocs] = (mbuf as i64) + f2 + 1; xl[ndocs] = le - f2 - 1
240 } else { tp[ndocs] = 0; tl[ndocs] = 0; xp[ndocs] = 0; xl[ndocs] = 0 } }
241 if f1 < ls { tp[ndocs] = 0; tl[ndocs] = 0; xp[ndocs] = 0; xl[ndocs] = 0 }
242 ndocs = ndocs + 1
243 ls = ii + 1
244 }
245 ii = ii + 1
246 }
247 if idx.n_rows != ndocs { out.verdict = WA_NO_INDEX; return WA_NO_INDEX }
248
249 // 3. parse the query into folded term ptr/len arrays (originals, lowercased; no thesaurus needed here)
250 let qbuf: *u8 = sys_mmap(WA_TERMS_CAP) // packed copy of folded query tokens (NUL-separated)
251 let qterms: *i64 = sys_mmap(8 * WA_MAX_TERMS) as *i64
252 let qlens: *i64 = sys_mmap(8 * WA_MAX_TERMS) as *i64
253 var nq: i64 = 0
254 var qo: i64 = 0
255 // tokenize the query on non-token-char boundaries (same convention as nx_search_inverted), fold to lower
256 let qn0: i64 = wa_strlen(query)
257 var st: i64 = 0 - 1 // start of the current token, or -1 when between tokens
258 var p: i64 = 0
259 while p <= qn0 {
260 var istok: i64 = 0
261 if p < qn0 { if nx_inv_is_token_char(query[p] as i64) == 1 { istok = 1 } }
262 if istok == 1 {
263 if st < 0 { st = p } // token opens
264 } else {
265 if st >= 0 { // token closes at p
266 let toklen: i64 = p - st
267 if toklen >= NX_INV_MIN_TOKEN_LEN { if nq < WA_MAX_TERMS { if qo + toklen + 1 <= WA_TERMS_CAP {
268 var b: i64 = 0
269 while b < toklen { var c: i64 = query[st + b] as i64; if c >= 65 { if c <= 90 { c = c + 32 } } qbuf[qo + b] = c as u8; b = b + 1 }
270 qbuf[qo + toklen] = 0 as u8
271 qterms[nq] = (qbuf as i64) + qo; qlens[nq] = toklen; nq = nq + 1
272 qo = qo + toklen + 1
273 } } }
274 st = 0 - 1
275 }
276 }
277 p = p + 1
278 }
279 if nq == 0 { out.verdict = WA_NO_ANSWER; return WA_NO_ANSWER }
280
281 // shortlist candidates = union of each term's postings
282 let seen: *u8 = sys_mmap(ndocs + 8)
283 let res: *NxInvQueryResult = sys_mmap(64) as *NxInvQueryResult
284 let rowids: *i64 = sys_mmap(8 * WA_ROWID_CAP) as *i64
285 var qi: i64 = 0
286 while qi < nq {
287 nx_inv_query_term(idx, qterms[qi] as *u8, qlens[qi], rowids, WA_ROWID_CAP, res)
288 var ri: i64 = 0
289 while ri < res.n_rowids_filled { let rid: i64 = rowids[ri]; if rid >= 0 { if rid < ndocs { seen[rid] = 1 as u8 } } ri = ri + 1 }
290 qi = qi + 1
291 }
292 let cand: *i64 = sys_mmap(8 * (ndocs + 8)) as *i64
293 var ncand: i64 = 0; var di: i64 = 0
294 while di < ndocs { if seen[di] == (1 as u8) { cand[ncand] = di; ncand = ncand + 1 } di = di + 1 }
295 if ncand == 0 { out.verdict = WA_NO_ANSWER; return WA_NO_ANSWER }
296
297 // 4. BM25F over candidates. Score on FOLDED copies of title+body (re_count is case-sensitive); keep the
298 // ORIGINAL manifest buffer intact for the displayed snippet.
299 let f_tt: *i64 = sys_mmap(8 * (ncand + 8)) as *i64
300 let f_tl: *i64 = sys_mmap(8 * (ncand + 8)) as *i64
301 let f_dt: *i64 = sys_mmap(8 * (ncand + 8)) as *i64
302 let f_bt: *i64 = sys_mmap(8 * (ncand + 8)) as *i64
303 let f_bl: *i64 = sys_mmap(8 * (ncand + 8)) as *i64
304 let f_db: *i64 = sys_mmap(8 * (ncand + 8)) as *i64
305 var sum_t: i64 = 0; var sum_b: i64 = 0; var ci: i64 = 0
306 while ci < ncand {
307 let d: i64 = cand[ci]
308 // folded title copy (bind the source ptr to a local FIRST -- indexing a cast-in-place expression
309 // `(tp[d] as *u8)[a]` miscompiles, the documented parser-desync gotcha; via a local it is correct)
310 let tlen: i64 = tl[d]
311 let tsrc: *u8 = tp[d] as *u8
312 let tcopy: *u8 = sys_mmap(tlen + 8)
313 var a: i64 = 0; while a < tlen { tcopy[a] = tsrc[a]; a = a + 1 }
314 wa_fold(tcopy, tlen)
315 // folded body copy
316 let blen: i64 = xl[d]
317 let bsrc: *u8 = xp[d] as *u8
318 let bcopy: *u8 = sys_mmap(blen + 8)
319 a = 0; while a < blen { bcopy[a] = bsrc[a]; a = a + 1 }
320 wa_fold(bcopy, blen)
321 f_tt[ci] = tcopy as i64; f_tl[ci] = tlen; f_dt[ci] = bm_token_count(tcopy, tlen)
322 f_bt[ci] = bcopy as i64; f_bl[ci] = blen; f_db[ci] = bm_token_count(bcopy, blen)
323 sum_t = sum_t + f_dt[ci]; sum_b = sum_b + f_db[ci]
324 ci = ci + 1
325 }
326 var avgt: i64 = sum_t / ncand; if avgt <= 0 { avgt = 1 }
327 var avgb: i64 = sum_b / ncand; if avgb <= 0 { avgb = 1 }
328 var best: i64 = 0 - 1; var bscore: i64 = 0 - 1
329 ci = 0
330 while ci < ncand {
331 let sc: i64 = bmf_score(f_tt, f_tl, f_dt, f_bt, f_bl, f_db, ncand, ci, avgt, avgb, qterms, nq)
332 if sc > bscore { bscore = sc; best = ci }
333 ci = ci + 1
334 }
335 if best < 0 { out.verdict = WA_NO_ANSWER; return WA_NO_ANSWER }
336 out.score = bscore
337
338 // 5. threshold gate (data-driven): refuse below the registered floor
339 let thr: i64 = wa_read_threshold()
340 if bscore <= thr { out.verdict = WA_NO_ANSWER; out.snippet[0] = 0 as u8; out.slug[0] = 0 as u8; return WA_NO_ANSWER }
341
342 // winner doc id + slug. Bind the manifest slug ptr to a local *u8 BEFORE indexing -- indexing a
343 // cast-in-place expression `(tp[wd] as *u8)[si]` hits the documented parser-desync miscompile (the
344 // body/title copy loops above already use this local-first workaround); doing it inline here was the
345 // root cause of garbled slugs (right length, garbage bytes e.g. "UUUUUUUUUUU").
346 let wd: i64 = cand[best]
347 let slug_src: *u8 = tp[wd] as *u8
348 let slug_len: i64 = tl[wd]
349 var si: i64 = 0
350 while si < slug_len { if si < WA_SLUG_CAP - 1 { out.slug[si] = slug_src[si] } si = si + 1 }
351 out.slug[si] = 0 as u8
352
353 // 6. answer passage: build an NxSearchQuery from the query, find the densest-cluster window in the ORIGINAL
354 // body, and run the snippet extractor centered there. snippet matching is case-insensitive (extractor
355 // folds), so we extract from the original (mixed-case) body for a readable, highlighted passage.
356 let q: *NxSearchQuery = sys_mmap(128) as *NxSearchQuery
357 let qtbuf: *u8 = sys_mmap(WA_TERMS_CAP)
358 nx_search_query_init(q, qtbuf, WA_TERMS_CAP)
359 nx_search_query_parse(q, query, wa_strlen(query))
360
361 let obody: *u8 = xp[wd] as *u8
362 let oblen: i64 = xl[wd]
363 let woff: i64 = wa_best_window(obody, oblen, qterms, qlens, nq)
364 // slice the body to begin at the densest window so the extractor's first-match window lands on the cluster
365 var slice_ptr: *u8 = obody
366 var slice_len: i64 = oblen
367 if woff > 0 { slice_ptr = ((obody as i64) + woff) as *u8; slice_len = oblen - woff }
368 let olp: *i64 = sys_mmap(16) as *i64
369 olp[0] = 0
370 let rc: i64 = nx_search_snippet_extract(out.snippet, WA_SNIP_CAP, slice_ptr, slice_len, q, olp)
371 if rc != NX_SSE_OK { out.snippet[0] = 0 as u8 }
372 out.snippet[olp[0]] = 0 as u8
373
374 out.verdict = WA_OK
375 return WA_OK
376}
377
378// production convenience: registry = the committed WA_REG_PATH, site = wiki.
379func answer(query: *u8, out: *WaAnswer) -> i64 {
380 return answer_ex(WA_REG_PATH, WA_SITE, query, out)
381}
382
383// allocate a ready WaAnswer (snippet/slug/base_url buffers preallocated).
384func wa_new() -> *WaAnswer {
385 let a: *WaAnswer = sys_mmap(64) as *WaAnswer
386 a.snippet = sys_mmap(WA_SNIP_CAP + 8)
387 a.slug = sys_mmap(WA_SLUG_CAP)
388 a.base_url = sys_mmap(WA_SLUG_CAP)
389 a.score = 0
390 a.verdict = WA_BAD_INPUT
391 return a
392}
393
394// human-readable verdict name (logs / self-test).
395func wa_verdict_name(v: i64) -> *u8 {
396 if v == WA_OK { return "WA_OK" as *u8 }
397 if v == WA_NO_ANSWER { return "WA_NO_ANSWER" as *u8 }
398 if v == WA_BAD_INPUT { return "WA_BAD_INPUT" as *u8 }
399 if v == WA_NO_REGISTRY { return "WA_NO_REGISTRY" as *u8 }
400 if v == WA_NO_INDEX { return "WA_NO_INDEX" as *u8 }
401 if v == WA_QUERY_FAIL { return "WA_QUERY_FAIL" as *u8 }
402 return "WA_UNKNOWN" as *u8
403}
404
405// ===== standalone self-test main (the gate imports this file and ignores main) =====================
406// Run: /tmp/nx_wiki_answer.sov.elf -> prints answer() for a couple sample queries over the LIVE wiki index.
407func wa_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
408func wa_pscore(v: i64) -> i64 {
409 var m: i64 = v; let bb: *u8 = sys_mmap(28); let t: *u8 = sys_mmap(28); var k: i64 = 0
410 let whole: i64 = m / WA_MAGIC_1000000; let frac: i64 = (m % WA_MAGIC_1000000) / WA_MAGIC_10000
411 var mm: i64 = whole; if mm == 0 { t[0] = 48 as u8; k = 1 } while mm > 0 { t[k] = (48 + (mm % 10)) as u8; mm = mm / 10; k = k + 1 }
412 var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 } sys_write(1, bb, k)
413 sys_write(1, "." as *u8, 1)
414 if frac < 10 { sys_write(1, "0" as *u8, 1) }
415 let bb2: *u8 = sys_mmap(28); let t2: *u8 = sys_mmap(28); var k2: i64 = 0; var fm: i64 = frac
416 if fm == 0 { t2[0] = 48 as u8; k2 = 1 } while fm > 0 { t2[k2] = (48 + (fm % 10)) as u8; fm = fm / 10; k2 = k2 + 1 }
417 var j: i64 = 0; while j < k2 { bb2[j] = t2[k2 - 1 - j]; j = j + 1 } sys_write(1, bb2, k2)
418 return 0
419}
420func wa_demo(query: *u8) -> i64 {
421 let a: *WaAnswer = wa_new()
422 answer(query, a)
423 wa_puts("Q: " as *u8); wa_puts(query); wa_puts("\n verdict=" as *u8); wa_puts(wa_verdict_name(a.verdict))
424 wa_puts(" score="); wa_pscore(a.score)
425 if a.verdict == WA_OK {
426 wa_puts("\n source=" as *u8); wa_puts(a.base_url); wa_puts(a.slug); wa_puts(".html"); wa_puts(" (slug=" as *u8); wa_puts(a.slug); wa_puts(")")
427 wa_puts("\n answer: " as *u8); wa_puts(a.snippet)
428 }
429 wa_puts("\n----\n" as *u8)
430 return 0
431}
432func main() -> i64 {
433 wa_puts("=== nx_wiki_answer self-test (instant-answer over LIVE wiki index) ===\n" as *u8)
434 wa_demo("zero trust access" as *u8)
435 wa_demo("capitalism lab game" as *u8)
436 wa_demo("rollback versions" as *u8)
437 wa_demo("xyzzy qwffz" as *u8)
438 sys_exit(0); return 0
439}