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}