code wiki / _hdl_build / nx_beir_eval.nx
nx_beir_eval.nx source
↩ module page · 505 lines · 22111 B
1// nx_beir_eval.nx -- SOVEREIGN BEIR harness: real external ground truth (BEIR/nfcorpus) scored by our own
2// integer BM25 -> mean nDCG@10 (the standard BEIR metric). The HONEST external row the maturity ladder is
3// gated on (F236): real 3633-doc corpus + 323 test queries + 12335 graded qrels, vs the published BEIR BM25
4// baseline (nfcorpus nDCG@10 ~0.32, Anserini). BM25 k1=0.9 b=0.4 (BEIR defaults). IDF=log2 (ranking-invariant
5// vs ln -> identical nDCG). Integer/fixed-point throughout (no float) = bit-reproducible. Forward index
6// (per-doc sorted term,tf). Tokenizer = lowercase [a-z0-9] runs, NO stem / NO stopwords (v1 -- the gap to the
7// stemmed 0.32 baseline IS the stemming rung, honestly, not a defect).
8// expect_exit: 0 license_tier: ORIGINAL
9import "nx_qabench_engine.nx"
10import "nx_ppmi_lib.nx"
11// seq1494 RERANK ARM: BM25 retrieves a shortlist, the PPMI late-interaction model re-orders it, and BOTH
12// nDCG@10 values are printed FROM THE SAME RUN so the delta cannot be confounded by corpus/qrel/tokeniser
13// drift. The BM25 arm is untouched: top[0..9] keeps its exact BM25 order, we merely widen the candidate
14// selection past 10 so the reranker has something to re-order.
15// derived: 50 candidates is the standard first-stage depth for late-interaction reranking -- deep enough
16// that recall@50 >> recall@10 (so the reranker CAN win) and shallow enough to stay O(50) maxsim per query.
17const BE_KCAND: i64 = 50
18// sized: query/doc PPMI id buffers; nfcorpus docs are abstracts, far under this
19const BE_IDCAP: i64 = 4096
20const K_MAGIC_1024: i64 = 1024
21const K_MAGIC_8192: i64 = 8192
22const K_MAGIC_4096: i64 = 4096
23const K_MAGIC_1900: i64 = 1900
24
25const MAXDOC: i64 = 4096
26const CAPFILE: i64 = 67108864
27const NTCAP: i64 = 2000000
28const DTCAP: i64 = 2000000
29const NJCAP: i64 = 20000
30
31func be_read(path: *u8, slot: *i64) -> i64 {
32 let fd: i64 = sys_openat_rd(path)
33 if fd < 0 { return 0 - 1 }
34 let buf: *u8 = sys_mmap(CAPFILE)
35 var total: i64 = 0
36 var r: i64 = 1
37 while r > 0 {
38 if total >= CAPFILE { r = 0 } else {
39 r = sys_read(fd, (buf as i64 + total) as *u8, CAPFILE - total)
40 if r > 0 { total = total + r }
41 }
42 }
43 sys_close(fd)
44 slot[0] = buf as i64
45 return total
46}
47
48// read pathA, else fall back to pathB (NAS stable path first, local /tmp second)
49func be_read_fb(pa: *u8, pb: *u8, slot: *i64) -> i64 {
50 let n: i64 = be_read(pa, slot)
51 if n > 0 { return n }
52 return be_read(pb, slot)
53}
54
55// index of byte ch in buf[from,limit), else limit
56func be_find(buf: *u8, from: i64, limit: i64, ch: i64) -> i64 {
57 var i: i64 = from
58 var res: i64 = limit
59 var go: i64 = 1
60 while go == 1 {
61 if i >= limit { go = 0 } else {
62 if buf[i] == (ch as u8) { res = i; go = 0 } else { i = i + 1 }
63 }
64 }
65 return res
66}
67
68// lowercase+hash [a-z0-9] tokens in buf[start,end); append hashes to out at noff[0]; return token count
69func be_tok(buf: *u8, start: i64, end: i64, out: *i64, noff: *i64, scr: *u8) -> i64 {
70 var i: i64 = start
71 var cnt: i64 = 0
72 var sl: i64 = 0
73 while i <= end {
74 var c: i64 = 0
75 if i < end { c = buf[i] as i64 }
76 var isc: i64 = 0
77 var lc: i64 = c
78 if c >= 65 { if c <= 90 { lc = c + 32; isc = 1 } }
79 if c >= 97 { if c <= 122 { isc = 1 } }
80 if c >= 48 { if c <= 57 { isc = 1 } }
81 if isc == 1 {
82 if sl < 500 { scr[sl] = lc as u8; sl = sl + 1 }
83 } else {
84 if sl > 0 {
85 out[noff[0]] = db_semhash(scr, 0, sl)
86 noff[0] = noff[0] + 1
87 cnt = cnt + 1
88 sl = 0
89 }
90 }
91 i = i + 1
92 }
93 return cnt
94}
95
96func be_qs1(a: *i64, lo: i64, hi: i64) -> i64 {
97 if lo >= hi { return 0 }
98 var i: i64 = lo
99 var j: i64 = hi
100 let p: i64 = a[(lo + hi) / 2]
101 while i <= j {
102 while a[i] < p { i = i + 1 }
103 while a[j] > p { j = j - 1 }
104 if i <= j {
105 let t: i64 = a[i]; a[i] = a[j]; a[j] = t
106 i = i + 1; j = j - 1
107 }
108 }
109 be_qs1(a, lo, j)
110 be_qs1(a, i, hi)
111 return 0
112}
113
114func be_qs2(key: *i64, pay: *i64, lo: i64, hi: i64) -> i64 {
115 if lo >= hi { return 0 }
116 var i: i64 = lo
117 var j: i64 = hi
118 let p: i64 = key[(lo + hi) / 2]
119 while i <= j {
120 while key[i] < p { i = i + 1 }
121 while key[j] > p { j = j - 1 }
122 if i <= j {
123 let t: i64 = key[i]; key[i] = key[j]; key[j] = t
124 let u: i64 = pay[i]; pay[i] = pay[j]; pay[j] = u
125 i = i + 1; j = j - 1
126 }
127 }
128 be_qs2(key, pay, lo, j)
129 be_qs2(key, pay, i, hi)
130 return 0
131}
132
133// log2(q) in 1/1024ths for q>=1
134func be_log2_1024(q: i64) -> i64 {
135 if q <= 1 { return 0 }
136 var bl: i64 = 0
137 var t: i64 = q
138 while t > 1 { t = t / 2; bl = bl + 1 }
139 var frac: i64 = 0
140 if bl >= 6 { frac = (q / (1 << (bl - 6))) - 64 }
141 if bl < 6 { frac = (q << (6 - bl)) - 64 }
142 if frac < 0 { frac = 0 }
143 return bl * K_MAGIC_1024 + frac * 16
144}
145
146func be_disc(r: i64) -> i64 {
147 if r == 1 { return 1000 }
148 if r == 2 { return 631 }
149 if r == 3 { return 500 }
150 if r == 4 { return 431 }
151 if r == 5 { return 387 }
152 if r == 6 { return 356 }
153 if r == 7 { return 333 }
154 if r == 8 { return 315 }
155 if r == 9 { return 301 }
156 if r == 10 { return 289 }
157 return 0
158}
159
160func main() -> i64 {
161 let G: *i64 = sys_mmap(64) as *i64
162 let scr: *u8 = sys_mmap(K_MAGIC_1024)
163
164 // ---------- load corpus ----------
165 let cn: i64 = be_read_fb("knowledge/beir/nfcorpus/corpus.tsv" as *u8, "/tmp/beir/nfcorpus/corpus.tsv" as *u8, G)
166 if cn <= 0 { db_w("{\"error\":\"corpus.tsv missing\"}\n" as *u8); return 1 }
167 let cbuf: *u8 = G[0] as *u8
168
169 let thash: *i64 = sys_mmap(NTCAP * 8) as *i64
170 let noff: *i64 = sys_mmap(8) as *i64
171 noff[0] = 0
172 let doc_start: *i64 = sys_mmap((MAXDOC + 1) * 8) as *i64
173 let doc_len: *i64 = sys_mmap(MAXDOC * 8) as *i64
174 let doc_idh: *i64 = sys_mmap(MAXDOC * 8) as *i64
175 // seq1494 RERANK ARM: keep each doc's RAW TEXT SPAN in cbuf. BM25 works on this harness's own hashed
176 // vocab, but the PPMI late-interaction model has its OWN word ids, so the reranker needs the original
177 // characters. Two i64 arrays is the whole cost of making rung 2 measurable instead of asserted.
178 let doc_ts: *i64 = sys_mmap(MAXDOC * 8) as *i64
179 let doc_te: *i64 = sys_mmap(MAXDOC * 8) as *i64
180 var nd: i64 = 0
181
182 var p: i64 = 0
183 while p < cn {
184 let eol: i64 = be_find(cbuf, p, cn, 10)
185 let tab: i64 = be_find(cbuf, p, eol, 9)
186 if tab < eol { if nd < MAXDOC {
187 doc_idh[nd] = db_semhash(cbuf, p, tab - p)
188 doc_start[nd] = noff[0]
189 doc_ts[nd] = tab + 1
190 doc_te[nd] = eol
191 doc_len[nd] = be_tok(cbuf, tab + 1, eol, thash, noff, scr)
192 nd = nd + 1
193 } }
194 p = eol + 1
195 }
196 doc_start[nd] = noff[0]
197 let ntok: i64 = noff[0]
198
199 // ---------- vocab = sort+unique ----------
200 let vocab: *i64 = sys_mmap(ntok * 8) as *i64
201 var vi: i64 = 0
202 while vi < ntok { vocab[vi] = thash[vi]; vi = vi + 1 }
203 be_qs1(vocab, 0, ntok - 1)
204 var nv: i64 = 0
205 var k: i64 = 0
206 while k < ntok {
207 if nv == 0 { vocab[nv] = vocab[k]; nv = nv + 1 }
208 else { if vocab[k] != vocab[nv - 1] { vocab[nv] = vocab[k]; nv = nv + 1 } }
209 k = k + 1
210 }
211
212 // ---------- forward index + df ----------
213 let dterm: *i64 = sys_mmap(DTCAP * 8) as *i64
214 let dtf: *i64 = sys_mmap(DTCAP * 8) as *i64
215 let doc_off: *i64 = sys_mmap((MAXDOC + 1) * 8) as *i64
216 let df: *i64 = sys_mmap(nv * 8) as *i64
217 var z: i64 = 0
218 while z < nv { df[z] = 0; z = z + 1 }
219 let tmp: *i64 = sys_mmap(K_MAGIC_8192 * 8) as *i64
220 var dpos: i64 = 0
221 var d: i64 = 0
222 while d < nd {
223 let s0: i64 = doc_start[d]
224 let s1: i64 = doc_start[d + 1]
225 var m: i64 = 0
226 var q: i64 = s0
227 while q < s1 {
228 if m < K_MAGIC_8192 { tmp[m] = db_bsearch_i64(vocab, nv, thash[q]); m = m + 1 }
229 q = q + 1
230 }
231 be_qs1(tmp, 0, m - 1)
232 doc_off[d] = dpos
233 var a: i64 = 0
234 while a < m {
235 var b: i64 = a + 1
236 var go3: i64 = 1
237 while go3 == 1 { if b < m { if tmp[b] == tmp[a] { b = b + 1 } else { go3 = 0 } } else { go3 = 0 } }
238 if dpos < DTCAP { dterm[dpos] = tmp[a]; dtf[dpos] = b - a; dpos = dpos + 1 }
239 df[tmp[a]] = df[tmp[a]] + 1
240 a = b
241 }
242 d = d + 1
243 }
244 doc_off[nd] = dpos
245
246 var totlen: i64 = 0
247 z = 0
248 while z < nd { totlen = totlen + doc_len[z]; z = z + 1 }
249 var avgdl: i64 = 1
250 if nd > 0 { avgdl = totlen / nd }
251 if avgdl < 1 { avgdl = 1 }
252
253 // ---------- doc-id -> docidx (sorted idhash) ----------
254 let sidh: *i64 = sys_mmap(nd * 8) as *i64
255 let sidx: *i64 = sys_mmap(nd * 8) as *i64
256 z = 0
257 while z < nd { sidh[z] = doc_idh[z]; sidx[z] = z; z = z + 1 }
258 be_qs2(sidh, sidx, 0, nd - 1)
259
260 // ---------- qrels ----------
261 let qn: i64 = be_read_fb("knowledge/beir/nfcorpus/qrels/test.tsv" as *u8, "/tmp/beir/nfcorpus/qrels/test.tsv" as *u8, (G as i64 + 8) as *i64)
262 if qn <= 0 { db_w("{\"error\":\"qrels test.tsv missing\"}\n" as *u8); return 1 }
263 let qbuf: *u8 = G[1] as *u8
264 let j_qid: *i64 = sys_mmap(NJCAP * 8) as *i64
265 let j_doc: *i64 = sys_mmap(NJCAP * 8) as *i64
266 let j_scr: *i64 = sys_mmap(NJCAP * 8) as *i64
267 var nj: i64 = 0
268 var qp: i64 = 0
269 var hdr: i64 = 1
270 while qp < qn {
271 let e2: i64 = be_find(qbuf, qp, qn, 10)
272 if hdr == 1 { hdr = 0 } else {
273 let t1: i64 = be_find(qbuf, qp, e2, 9)
274 if t1 < e2 {
275 let t2: i64 = be_find(qbuf, t1 + 1, e2, 9)
276 if t2 < e2 {
277 var sc: i64 = 0
278 var sp: i64 = t2 + 1
279 while sp < e2 { let dch: i64 = qbuf[sp] as i64; if dch >= 48 { if dch <= 57 { sc = sc * 10 + (dch - 48) } } sp = sp + 1 }
280 let dh: i64 = db_semhash(qbuf, t1 + 1, t2 - t1 - 1)
281 let dpos2: i64 = db_bsearch_i64(sidh, nd, dh)
282 if dpos2 >= 0 { if nj < NJCAP {
283 j_qid[nj] = db_semhash(qbuf, qp, t1 - qp)
284 j_doc[nj] = sidx[dpos2]
285 j_scr[nj] = sc
286 nj = nj + 1
287 } }
288 }
289 }
290 }
291 qp = e2 + 1
292 }
293 // sort judgments by qid, carrying an index permutation (so j_doc/j_scr stay addressable via jidx)
294 let jidx: *i64 = sys_mmap(nj * 8) as *i64
295 z = 0
296 while z < nj { jidx[z] = z; z = z + 1 }
297 be_qs2(j_qid, jidx, 0, nj - 1)
298
299 // ---------- per query ----------
300 let dqn: i64 = be_read_fb("knowledge/beir/nfcorpus/queries.tsv" as *u8, "/tmp/beir/nfcorpus/queries.tsv" as *u8, (G as i64 + 16) as *i64)
301 if dqn <= 0 { db_w("{\"error\":\"queries.tsv missing\"}\n" as *u8); return 1 }
302 let dbuf: *u8 = G[2] as *u8
303 let score: *i64 = sys_mmap(MAXDOC * 8) as *i64
304 let qtid: *i64 = sys_mmap(K_MAGIC_1024 * 8) as *i64
305 let top: *i64 = sys_mmap(16 * 8) as *i64
306 let goldsc: *i64 = sys_mmap(K_MAGIC_4096 * 8) as *i64
307 let used: *u8 = sys_mmap(MAXDOC)
308 let qhbuf: *i64 = sys_mmap(K_MAGIC_1024 * 8) as *i64
309 let qnoff: *i64 = sys_mmap(8) as *i64
310
311 // seq1494: load the PPMI model ONCE. HONEST-ABSENT: if it is missing the rerank arm reports
312 // UNAVAILABLE rather than silently scoring 0 -- an absent model must never read as "reranking is bad".
313 let PG: *i64 = sys_mmap(64) as *i64
314 let rr_ok: i64 = ppl_load(PG, "knowledge/index/semppmi_v1.bin" as *u8)
315 let qids: *i64 = sys_mmap(BE_IDCAP * 8) as *i64
316 let dids: *i64 = sys_mmap(BE_IDCAP * 8) as *i64
317 let cscore: *i64 = sys_mmap(BE_KCAND * 8) as *i64
318 let cused: *u8 = sys_mmap(BE_KCAND)
319 var sum_ndcg_rr: i64 = 0
320 var q_improved: i64 = 0
321 var q_worsened: i64 = 0
322 var q_same: i64 = 0
323 var sum_ndcg: i64 = 0
324 var nq: i64 = 0
325 var n_noqrel: i64 = 0
326 var n_noterm: i64 = 0
327 var n_noidcg: i64 = 0
328 var qpp: i64 = 0
329 while qpp < dqn {
330 let e3: i64 = be_find(dbuf, qpp, dqn, 10)
331 let tb: i64 = be_find(dbuf, qpp, e3, 9)
332 if tb < e3 {
333 let qh2: i64 = db_semhash(dbuf, qpp, tb - qpp)
334 qnoff[0] = 0
335 be_tok(dbuf, tb + 1, e3, qhbuf, qnoff, scr)
336 var nqt: i64 = 0
337 var qi: i64 = 0
338 while qi < qnoff[0] {
339 let tid2: i64 = db_bsearch_i64(vocab, nv, qhbuf[qi])
340 if tid2 >= 0 { if nqt < K_MAGIC_1024 { qtid[nqt] = tid2; nqt = nqt + 1 } }
341 qi = qi + 1
342 }
343 let lo0: i64 = db_bsearch_i64(j_qid, nj, qh2)
344 if lo0 < 0 { n_noqrel = n_noqrel + 1 } else {
345 var jlo: i64 = lo0
346 var g1: i64 = 1
347 while g1 == 1 { if jlo > 0 { if j_qid[jlo - 1] == qh2 { jlo = jlo - 1 } else { g1 = 0 } } else { g1 = 0 } }
348 var jhi: i64 = lo0
349 var g2: i64 = 1
350 while g2 == 1 { if jhi < nj - 1 { if j_qid[jhi + 1] == qh2 { jhi = jhi + 1 } else { g2 = 0 } } else { g2 = 0 } }
351
352 // gold + IDCG@10 FIRST (methodology: every query with relevant judgments is counted)
353 var ng: i64 = 0
354 var jjg: i64 = jlo
355 while jjg <= jhi { if ng < K_MAGIC_4096 { goldsc[ng] = j_scr[jidx[jjg]]; ng = ng + 1 } jjg = jjg + 1 }
356 be_qs1(goldsc, 0, ng - 1)
357 var idcg: i64 = 0
358 var ri: i64 = 0
359 while ri < 10 { if ri < ng { idcg = idcg + goldsc[ng - 1 - ri] * be_disc(ri + 1) } ri = ri + 1 }
360
361 if idcg <= 0 { n_noidcg = n_noidcg + 1 } else {
362 var dcg: i64 = 0
363 if nqt <= 0 { n_noterm = n_noterm + 1 } else {
364 z = 0
365 while z < nd { score[z] = 0; z = z + 1 }
366 var qt: i64 = 0
367 while qt < nqt {
368 let tid3: i64 = qtid[qt]
369 let dfi: i64 = df[tid3]
370 if dfi > 0 {
371 let idf: i64 = be_log2_1024((2 * nd + 2) / (2 * dfi + 1))
372 var dd: i64 = 0
373 while dd < nd {
374 let o0: i64 = doc_off[dd]
375 let o1: i64 = doc_off[dd + 1]
376 let pp: i64 = db_bsearch_i64((dterm as i64 + o0 * 8) as *i64, o1 - o0, tid3)
377 if pp >= 0 {
378 let tf: i64 = dtf[o0 + pp]
379 let norm1000: i64 = 600 + (400 * doc_len[dd]) / avgdl
380 let denom1000: i64 = tf * 1000 + (900 * norm1000) / 1000
381 if denom1000 > 0 { score[dd] = score[dd] + (idf * tf * K_MAGIC_1900) / denom1000 }
382 }
383 dd = dd + 1
384 }
385 }
386 qt = qt + 1
387 }
388 z = 0
389 while z < nd { used[z] = 0 as u8; z = z + 1 }
390 var rr: i64 = 0
391 while rr < BE_KCAND {
392 var best: i64 = 0 - 1
393 var dd2: i64 = 0
394 while dd2 < nd {
395 if used[dd2] == (0 as u8) {
396 if best < 0 { best = dd2 } else { if score[dd2] > score[best] { best = dd2 } }
397 }
398 dd2 = dd2 + 1
399 }
400 top[rr] = best
401 if best >= 0 { used[best] = 1 as u8 }
402 rr = rr + 1
403 }
404 rr = 0
405 while rr < 10 {
406 let dpk: i64 = top[rr]
407 var gv: i64 = 0
408 if dpk >= 0 {
409 var jj2: i64 = jlo
410 while jj2 <= jhi {
411 if j_doc[jidx[jj2]] == dpk { gv = j_scr[jidx[jj2]]; jj2 = jhi + 1 } else { jj2 = jj2 + 1 }
412 }
413 }
414 dcg = dcg + gv * be_disc(rr + 1)
415 rr = rr + 1
416 }
417 }
418 // ---- seq1494 RERANK ARM: re-order the SAME BM25 shortlist by PPMI late-interaction ----
419 // Identical qrels, identical idcg, identical candidate pool -> the ONLY difference is
420 // the ordering function, which is what makes this a controlled comparison.
421 var dcg_rr: i64 = dcg
422 if rr_ok == 1 { if nqt > 0 {
423 // same (buf,len) fix on the query side: the query text is dbuf[tb+1 .. e3)
424 let nqi: i64 = ppl_tokenize_ids(PG, ((dbuf as i64) + tb + 1) as *u8, e3 - tb - 1, qids, BE_IDCAP)
425 var ci: i64 = 0
426 while ci < BE_KCAND {
427 cscore[ci] = 0 - 1
428 cused[ci] = 0 as u8
429 let dpc: i64 = top[ci]
430 if dpc >= 0 {
431 let dsx: i64 = doc_ts[dpc]
432 let dex: i64 = doc_te[dpc]
433 // BUGFIX (caught by an EMPTY result file, not by a passing test): this took
434 // (buf, len) but was handed (cbuf, dex) -- the corpus BASE and an ABSOLUTE
435 // END offset -- so every candidate re-tokenised the WHOLE corpus from byte 0.
436 // 323 queries x 50 candidates x 3633 docs never finished. Pass the doc's own
437 // span. ★a silent non-finish is a LOUDER bug than a wrong number.
438 let ndi: i64 = ppl_tokenize_ids(PG, ((cbuf as i64) + dsx) as *u8, dex - dsx, dids, BE_IDCAP)
439 cscore[ci] = ppl_maxsim_idf(PG, qids, nqi, dids, ndi)
440 }
441 ci = ci + 1
442 }
443 var dcg2: i64 = 0
444 var rk: i64 = 0
445 while rk < 10 {
446 var bi: i64 = 0 - 1
447 var cj: i64 = 0
448 while cj < BE_KCAND {
449 if cused[cj] == (0 as u8) { if top[cj] >= 0 {
450 if bi < 0 { bi = cj } else { if cscore[cj] > cscore[bi] { bi = cj } }
451 } }
452 cj = cj + 1
453 }
454 if bi >= 0 {
455 cused[bi] = 1 as u8
456 let dpk2: i64 = top[bi]
457 var gv2: i64 = 0
458 var jj3: i64 = jlo
459 while jj3 <= jhi {
460 if j_doc[jidx[jj3]] == dpk2 { gv2 = j_scr[jidx[jj3]]; jj3 = jhi + 1 } else { jj3 = jj3 + 1 }
461 }
462 dcg2 = dcg2 + gv2 * be_disc(rk + 1)
463 }
464 rk = rk + 1
465 }
466 dcg_rr = dcg2
467 } }
468 if dcg_rr > dcg { q_improved = q_improved + 1 } else { if dcg_rr < dcg { q_worsened = q_worsened + 1 } else { q_same = q_same + 1 } }
469 sum_ndcg_rr = sum_ndcg_rr + (dcg_rr * 1000) / idcg
470 sum_ndcg = sum_ndcg + (dcg * 1000) / idcg
471 nq = nq + 1
472 }
473 }
474 }
475 qpp = e3 + 1
476 }
477
478 var mean: i64 = 0
479 if nq > 0 { mean = sum_ndcg / nq }
480 db_w("{\"tool\":\"nx_beir_eval\",\"dataset\":\"BEIR/nfcorpus\",\"docs\":" as *u8); db_n(nd)
481 db_w(",\"vocab\":" as *u8); db_n(nv)
482 db_w(",\"test_queries_scored\":" as *u8); db_n(nq)
483 db_w(",\"dropped_no_qrels\":" as *u8); db_n(n_noqrel)
484 db_w(",\"dropped_no_relevant\":" as *u8); db_n(n_noidcg)
485 db_w(",\"counted_zero_no_query_term_match\":" as *u8); db_n(n_noterm)
486 db_w(",\"avgdl\":" as *u8); db_n(avgdl)
487 db_w(",\"bm25\":{\"k1x1000\":900,\"bx1000\":400,\"idf\":\"log2\",\"stemming\":false,\"stopwords\":false}" as *u8)
488 db_w(",\"ndcg_at_10_permil\":" as *u8); db_n(mean)
489 db_w(",\"published_bm25_baseline_permil\":320" as *u8)
490 // seq1494: the rerank arm, printed from the SAME run. UNAVAILABLE (not 0) when the model is absent.
491 var mean_rr: i64 = 0
492 if nq > 0 { mean_rr = sum_ndcg_rr / nq }
493 db_w(",\"rerank\":{\"model\":" as *u8)
494 if rr_ok == 1 { db_w("\"semppmi_v1 late-interaction maxsim(idf)\"" as *u8) } else { db_w("\"UNAVAILABLE -- semppmi_v1.bin did not load; rerank arm NOT measured (this is not a score of 0)\"" as *u8) }
495 db_w(",\"candidates\":" as *u8); db_n(BE_KCAND)
496 db_w(",\"ndcg_at_10_permil\":" as *u8); db_n(mean_rr)
497 db_w(",\"delta_permil\":" as *u8); db_n(mean_rr - mean)
498 db_w(",\"queries_improved\":" as *u8); db_n(q_improved)
499 db_w(",\"queries_worsened\":" as *u8); db_n(q_worsened)
500 db_w(",\"queries_unchanged\":" as *u8); db_n(q_same)
501 db_w(",\"accept_rule\":\"declared BEFORE the run: rerank must EXCEED the BM25 arm on these same queries or it does NOT get wired into search (seq1494)\"}" as *u8)
502 db_w(",\"note\":\"real external ground truth; integer/bit-reproducible; no-stem/no-stopword v1 (gap to the 0.32 stemmed baseline = the stemming rung)\"}" as *u8)
503 db_w("\n" as *u8)
504 return 0
505}