code wiki / _hdl_build / nx_reader_xray.nx

nx_reader_xray.nx source

↩ module page · 436 lines · 19868 B

1// nx_reader_xray.nx -- SOVEREIGN, ZERO-DEPENDENCY, book-INTRINSIC X-Ray. Operator law: attack the BEHIND "aids 2// ecosystem" axis of nx_reader_exceed_gate with a REAL capability, not a sovereignty restatement. Kindle's X-Ray 3// is PROVISIONED-ONLY (exists only where Amazon pre-built the data, so most books show "X-Ray: Not available"). 4// This organ DERIVES an X-Ray from the book's OWN text -- works on ANY book, offline, deterministic, auditable. 5// 6// Algorithm (deterministic, no random, no external data): 7// - per chapter, skip the leading duplicate-title lines exactly as nx_reader_render does (index PROSE not headings) 8// - tokenize into maximal A-Za-z runs; track for each token: capitalized? sentence-initial? space-joined-to-prev? 9// - a NAME = a maximal run of consecutive capitalized tokens joined ONLY by spaces (so "White Rabbit" is one 10// entity, but "Alice. Rabbit" is two). A multi-token run always qualifies (proper-noun phrase). A single 11// capitalized token qualifies only if it is MID-sentence (not sentence-initial) AND not a stopword -- this 12// is what separates "thought Alice" (entity) from "But when" / "There was" (sentence openers, not entities). 13// - count occurrences per distinct name + remember the first chapter it appears in (for jump-to-first-mention) 14// - keep names with count >= MINOCC; sort by count desc, then name asc (fully deterministic) 15// Emits knowledge/staging/media/reader/<slug>/xray.json + prints a raw scorecard. Reads OUR own book.json + 16// chap<N>.txt (emitted by nx_epub_book). expect_exit: 0 license_tier: ORIGINAL 17import "nx_syscalls.nx" 18const K_MAGIC_1000000: i64 = 1000000 19const K_MAGIC_1024: i64 = 1024 20 21const MAXTERMS: i64 = 512 22const NAMELEN: i64 = 64 23const MAXTOK: i64 = 8192 24const MINOCC: i64 = 2 // a term must recur >=2x to be "notable" (filters one-off caps). TODO: promote to reader svc-config. 25const MAXCHAP: i64 = 256 // per-chapter occurrence-distribution cap (concordance); books rarely exceed, distribution truncates beyond 26 27func slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 28func w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, slen(s)); return 0 } 29func wn(fd: i64, v0: i64) -> i64 { var v: i64=v0; if v<0 { sys_write(fd,"-" as *u8,1); v=0-v } let b: *u8=sys_mmap(24); var k: i64=0; if v==0 {b[0]=48 as u8;k=1} while v>0 {b[k]=(48+(v%10)) as u8; v=v/10; k=k+1} let o: *u8=sys_mmap(24); var j: i64=0; while j<k {o[j]=b[k-1-j];j=j+1} sys_write(fd,o,k); return 0 } 30func p(s: *u8) -> i64 { w(1, s); return 0 } 31 32// ---- JSON helpers (same proven shapes as nx_reader_render) ---- 33func jfrom(hay: *u8, hl: i64, start: i64, needle: *u8) -> i64 { 34 let nl: i64 = slen(needle); if nl == 0 { return 0-1 } 35 var i: i64 = start; if i < 0 { i = 0 } 36 while i + nl <= hl { var k: i64=0; var hit: i64=1; while k<nl { if hay[i+k]!=needle[k]{hit=0;k=nl}else{k=k+1} } if hit==1 {return i} i=i+1 } 37 return 0-1 38} 39func jint(hay: *u8, hl: i64, pos: i64) -> i64 { 40 var i: i64 = pos 41 while i < hl { let c: i64 = hay[i] as i64; if c==0x2d { break } if c>=48 { if c<=57 { break } } i=i+1 } 42 var neg: i64 = 0 43 if i < hl { if hay[i]==(0x2d as u8) { neg=1; i=i+1 } } 44 var v: i64 = 0 45 while i < hl { let c: i64 = hay[i] as i64; if c<48 { break } if c>57 { break } v = v*10 + (c-48); i=i+1 } 46 if neg==1 { return 0-v } 47 return v 48} 49func jstr(hay: *u8, hl: i64, pos: i64, out: *u8, cap: i64) -> i64 { 50 var i: i64 = pos 51 if i < hl { if hay[i]==(0x22 as u8) { i=i+1 } } 52 var o: i64 = 0 53 while i < hl { 54 let c: i64 = hay[i] as i64 55 if c == 0x22 { i = hl } 56 else { if c == 0x5c { if i+1 < hl { if o<cap-1 { out[o]=hay[i+1]; o=o+1 } i=i+2 } else { i=i+1 } } 57 else { if o<cap-1 { out[o]=hay[i] as u8; o=o+1 } i=i+1 } } 58 } 59 out[o] = 0 as u8 60 return o 61} 62func jval_str(hay: *u8, hl: i64, key: *u8, out: *u8, cap: i64) -> i64 { 63 let at: i64 = jfrom(hay, hl, 0, key) 64 if at < 0 { return 0-1 } 65 return jstr(hay, hl, at + slen(key) - 1, out, cap) 66} 67 68// ---- character + string helpers ---- 69func is_upper(c: i64) -> i64 { if c>=65 { if c<=90 { return 1 } } return 0 } 70func is_alpha(c: i64) -> i64 { if c>=65 { if c<=90 { return 1 } } if c>=97 { if c<=122 { return 1 } } return 0 } 71func streq(a: *u8, b: *u8) -> i64 { 72 var i: i64 = 0 73 while i < K_MAGIC_1000000 { 74 let ca: i64 = a[i] as i64; let cb: i64 = b[i] as i64 75 if ca != cb { return 0 } 76 if ca == 0 { return 1 } 77 i = i + 1 78 } 79 return 0 80} 81// lexicographic a<b on NUL-terminated bytes -> 1, else 0 82func strlt(a: *u8, b: *u8) -> i64 { 83 var i: i64 = 0 84 while i < K_MAGIC_1000000 { 85 let ca: i64 = a[i] as i64; let cb: i64 = b[i] as i64 86 if ca != cb { if ca < cb { return 1 } return 0 } 87 if ca == 0 { return 0 } 88 i = i + 1 89 } 90 return 0 91} 92// HTML/JSON-escape a NUL-terminated name into a fd (book text -> JSON string value): escape \ and " and control. 93func wesc_json(fd: i64, s: *u8) -> i64 { 94 var i: i64 = 0 95 while s[i] != (0 as u8) { 96 let c: i64 = s[i] as i64 97 if c == 0x5c { w(fd, "\\\\" as *u8) } 98 else { if c == 0x22 { w(fd, "\\\"" as *u8) } 99 else { if c < 32 { w(fd, " " as *u8) } 100 else { sys_write(fd, (s as i64 + i) as *u8, 1) } } } 101 i = i + 1 102 } 103 return 0 104} 105 106// stopwords: capitalized words that are NOT named entities. The sentence-initial test already drops sentence 107// openers; this list mainly catches MID-sentence pronouns/conjunctions ("...miles I have fallen...") and a few 108// commonly-mid-capitalized function words. Conservative on purpose (false-negative entity beats false-positive noise). 109func is_stopword(s: *u8) -> i64 { 110 if streq(s,"I" as *u8)==1 { return 1 } 111 if streq(s,"A" as *u8)==1 { return 1 } 112 if streq(s,"An" as *u8)==1 { return 1 } 113 if streq(s,"The" as *u8)==1 { return 1 } 114 if streq(s,"And" as *u8)==1 { return 1 } 115 if streq(s,"But" as *u8)==1 { return 1 } 116 if streq(s,"So" as *u8)==1 { return 1 } 117 if streq(s,"Or" as *u8)==1 { return 1 } 118 if streq(s,"Nor" as *u8)==1 { return 1 } 119 if streq(s,"If" as *u8)==1 { return 1 } 120 if streq(s,"As" as *u8)==1 { return 1 } 121 if streq(s,"At" as *u8)==1 { return 1 } 122 if streq(s,"In" as *u8)==1 { return 1 } 123 if streq(s,"On" as *u8)==1 { return 1 } 124 if streq(s,"Of" as *u8)==1 { return 1 } 125 if streq(s,"To" as *u8)==1 { return 1 } 126 if streq(s,"For" as *u8)==1 { return 1 } 127 if streq(s,"With" as *u8)==1 { return 1 } 128 if streq(s,"He" as *u8)==1 { return 1 } 129 if streq(s,"She" as *u8)==1 { return 1 } 130 if streq(s,"It" as *u8)==1 { return 1 } 131 if streq(s,"Its" as *u8)==1 { return 1 } 132 if streq(s,"His" as *u8)==1 { return 1 } 133 if streq(s,"Her" as *u8)==1 { return 1 } 134 if streq(s,"They" as *u8)==1 { return 1 } 135 if streq(s,"We" as *u8)==1 { return 1 } 136 if streq(s,"You" as *u8)==1 { return 1 } 137 if streq(s,"Your" as *u8)==1 { return 1 } 138 if streq(s,"My" as *u8)==1 { return 1 } 139 if streq(s,"Our" as *u8)==1 { return 1 } 140 if streq(s,"This" as *u8)==1 { return 1 } 141 if streq(s,"That" as *u8)==1 { return 1 } 142 if streq(s,"These" as *u8)==1 { return 1 } 143 if streq(s,"Those" as *u8)==1 { return 1 } 144 if streq(s,"There" as *u8)==1 { return 1 } 145 if streq(s,"Then" as *u8)==1 { return 1 } 146 if streq(s,"Here" as *u8)==1 { return 1 } 147 if streq(s,"When" as *u8)==1 { return 1 } 148 if streq(s,"Where" as *u8)==1 { return 1 } 149 if streq(s,"What" as *u8)==1 { return 1 } 150 if streq(s,"Who" as *u8)==1 { return 1 } 151 if streq(s,"Which" as *u8)==1 { return 1 } 152 if streq(s,"Why" as *u8)==1 { return 1 } 153 if streq(s,"How" as *u8)==1 { return 1 } 154 if streq(s,"Would" as *u8)==1 { return 1 } 155 if streq(s,"Could" as *u8)==1 { return 1 } 156 if streq(s,"Should" as *u8)==1 { return 1 } 157 if streq(s,"Will" as *u8)==1 { return 1 } 158 if streq(s,"Shall" as *u8)==1 { return 1 } 159 if streq(s,"No" as *u8)==1 { return 1 } 160 if streq(s,"Yes" as *u8)==1 { return 1 } 161 if streq(s,"Oh" as *u8)==1 { return 1 } 162 if streq(s,"Down" as *u8)==1 { return 1 } 163 if streq(s,"Up" as *u8)==1 { return 1 } 164 if streq(s,"Now" as *u8)==1 { return 1 } 165 if streq(s,"Once" as *u8)==1 { return 1 } 166 if streq(s,"Either" as *u8)==1 { return 1 } 167 if streq(s,"Neither" as *u8)==1 { return 1 } 168 if streq(s,"Both" as *u8)==1 { return 1 } 169 if streq(s,"Each" as *u8)==1 { return 1 } 170 if streq(s,"All" as *u8)==1 { return 1 } 171 if streq(s,"Not" as *u8)==1 { return 1 } 172 return 0 173} 174 175// exact line equals title? (trims handled by caller) -- same shape as render's line_eq 176func line_eq(txt: *u8, s: i64, e: i64, title: *u8) -> i64 { 177 var tl: i64 = 0; while title[tl] != (0 as u8) { tl = tl + 1 } 178 if (e - s) != tl { return 0 } 179 var k: i64 = 0 180 while k < tl { if txt[s+k] != title[k] { return 0 } k = k + 1 } 181 return 1 182} 183// return the offset where PROSE begins: skip leading blank lines + lines duplicating the chapter title 184// (nx_html_to_text emits <title> AND <h1> -> 2-3 title repeats). Mirrors nx_reader_render.render_text's preamble. 185func skip_title(txt: *u8, n: i64, title: *u8) -> i64 { 186 var i: i64 = 0 187 var sk: i64 = 1 188 while sk == 1 { 189 var j: i64 = i 190 var wj: i64 = 1 191 while wj == 1 { if j >= n { wj = 0 } else { let c: i64 = txt[j] as i64; if c==10 { j=j+1 } else { if c==32 { j=j+1 } else { if c==13 { j=j+1 } else { if c==9 { j=j+1 } else { wj=0 } } } } } } 192 if j >= n { i = j; sk = 0 } else { 193 var le: i64 = j; while le < n { if txt[le]==(10 as u8) { break } le=le+1 } 194 var te: i64 = le; while te > j { let c2: i64 = txt[te-1] as i64; if c2==32 { te=te-1 } else { if c2==13 { te=te-1 } else { if c2==9 { te=te-1 } else { break } } } } 195 if line_eq(txt, j, te, title) == 1 { i = le + 1 } else { i = j; sk = 0 } 196 } 197 } 198 return i 199} 200 201// register one name occurrence into the term table held in ctx (ctx[0]=nterms, ctx[1]=names, ctx[2]=count, ctx[3]=first) 202func tt_register(ctx: *i64, nm: *u8, cidx: i64) -> i64 { 203 let tnames: *u8 = ctx[1] as *u8 204 let tcount: *i64 = ctx[2] as *i64 205 let tfirst: *i64 = ctx[3] as *i64 206 let tchap: *i64 = ctx[9] as *i64 207 var nt: i64 = ctx[0] 208 var idx: i64 = 0-1 209 var j: i64 = 0 210 while j < nt { 211 if streq((tnames as i64 + j*NAMELEN) as *u8, nm) == 1 { idx = j; j = nt } else { j = j + 1 } 212 } 213 if idx < 0 { 214 if nt < MAXTERMS { 215 let dst: *u8 = (tnames as i64 + nt*NAMELEN) as *u8 216 var k: i64 = 0 217 while nm[k] != (0 as u8) { if k < NAMELEN-1 { dst[k] = nm[k] } k = k + 1 } 218 if k > NAMELEN-1 { k = NAMELEN-1 } 219 dst[k] = 0 as u8 220 tcount[nt] = 0 221 tfirst[nt] = cidx 222 idx = nt 223 ctx[0] = nt + 1 224 } 225 } 226 if idx >= 0 { 227 tcount[idx] = tcount[idx] + 1 228 if cidx >= 0 { if cidx < MAXCHAP { tchap[idx*MAXCHAP + cidx] = tchap[idx*MAXCHAP + cidx] + 1 } } 229 } 230 return idx 231} 232 233// tokenize one chapter's prose + register named-entity runs. ctx[4..8] = scratch token arrays (start/cap/sent/sep) 234func process_chapter(txt: *u8, n: i64, title: *u8, cidx: i64, ctx: *i64) -> i64 { 235 let start: i64 = skip_title(txt, n, title) 236 let tok_s: *i64 = ctx[4] as *i64 237 let tok_e: *i64 = ctx[5] as *i64 238 let tok_cap: *i64 = ctx[6] as *i64 239 let tok_snt: *i64 = ctx[7] as *i64 240 let tok_sep: *i64 = ctx[8] as *i64 241 242 // ---- pass 1: tokenize, recording gap analysis (sentence-initial? joined-by-spaces-only?) ---- 243 var ntok: i64 = 0 244 var i: i64 = start 245 while i < n { 246 // scan the gap before the next token 247 var saw_term: i64 = 0 248 var saw_nonspace: i64 = 0 249 var first: i64 = 1 250 if ntok > 0 { first = 0 } 251 while i < n { let c: i64 = txt[i] as i64; if is_alpha(c)==1 { break } if c==46 { saw_term=1 } if c==33 { saw_term=1 } if c==63 { saw_term=1 } if c==10 { saw_term=1 } if c!=32 { saw_nonspace=1 } i=i+1 } 252 if i >= n { break } 253 let ts: i64 = i 254 while i < n { if is_alpha(txt[i] as i64)==0 { break } i=i+1 } 255 let te: i64 = i 256 if ntok < MAXTOK { 257 tok_s[ntok] = ts 258 tok_e[ntok] = te 259 tok_cap[ntok] = is_upper(txt[ts] as i64) 260 var si: i64 = 0 261 if first == 1 { si = 1 } else { if saw_term == 1 { si = 1 } } 262 tok_snt[ntok] = si 263 var sp: i64 = 0 264 if first == 0 { if saw_nonspace == 0 { sp = 1 } } 265 tok_sep[ntok] = sp 266 ntok = ntok + 1 267 } 268 } 269 270 // ---- pass 2: maximal capitalized runs -> names ---- 271 let nm: *u8 = sys_mmap(256) 272 var jj: i64 = 0 273 while jj < ntok { 274 if tok_cap[jj] == 0 { jj = jj + 1 } else { 275 var kk: i64 = jj 276 var ext: i64 = 1 277 while ext == 1 { 278 if kk+1 < ntok { if tok_cap[kk+1]==1 { if tok_sep[kk+1]==1 { kk = kk+1 } else { ext=0 } } else { ext=0 } } else { ext=0 } 279 } 280 let run_len: i64 = kk - jj + 1 281 // build the name (original case, single-space joined) 282 var o: i64 = 0 283 var t: i64 = jj 284 while t <= kk { 285 var c: i64 = tok_s[t] 286 while c < tok_e[t] { if o < 254 { nm[o] = txt[c]; o = o + 1 } c = c + 1 } 287 if t < kk { if o < 254 { nm[o] = 32 as u8; o = o + 1 } } 288 t = t + 1 289 } 290 nm[o] = 0 as u8 291 // qualify 292 var ok: i64 = 0 293 if run_len >= 2 { ok = 1 } else { if tok_snt[jj] == 0 { if is_stopword(nm) == 0 { ok = 1 } } } 294 if ok == 1 { tt_register(ctx, nm, cidx) } 295 jj = kk + 1 296 } 297 } 298 return 0 299} 300 301func main(argc: i64, argv: *i64) -> i64 { 302 var slug: *u8 = "nishi_fixture" as *u8 303 if argc >= 2 { slug = argv[1] as *u8 } 304 305 // dir = knowledge/staging/media/reader/<slug> 306 let dir: *u8 = sys_mmap(K_MAGIC_1024) 307 var dl: i64 = 0 308 let pfx: *u8 = "knowledge/staging/media/reader/" as *u8 309 var z: i64 = 0; while pfx[z]!=(0 as u8){ dir[dl]=pfx[z]; dl=dl+1; z=z+1 } 310 z = 0; while slug[z]!=(0 as u8){ dir[dl]=slug[z]; dl=dl+1; z=z+1 } 311 dir[dl] = 0 as u8 312 313 let bjp: *u8 = sys_mmap(K_MAGIC_1024) 314 var bl: i64 = 0; while dir[bl]!=(0 as u8){ bjp[bl]=dir[bl]; bl=bl+1 } 315 let sj: *u8 = "/book.json" as *u8; z=0; while sj[z]!=(0 as u8){ bjp[bl]=sj[z]; bl=bl+1; z=z+1 } bjp[bl]=0 as u8 316 317 let lp: *i64 = sys_mmap(8) as *i64; lp[0]=0 318 let bj: *u8 = sys_read_file(bjp, lp) 319 if (bj as i64)==0 { p("READER-XRAY FAIL read book.json: " as *u8); p(bjp); p("\n" as *u8); sys_exit(1); return 1 } 320 let bn: i64 = lp[0] 321 322 // term table + scratch token arrays packed into ctx 323 let ctx: *i64 = sys_mmap(8*16) as *i64 324 ctx[0] = 0 325 ctx[1] = sys_mmap(MAXTERMS*NAMELEN) as i64 326 ctx[2] = sys_mmap(MAXTERMS*8) as i64 327 ctx[3] = sys_mmap(MAXTERMS*8) as i64 328 ctx[4] = sys_mmap(MAXTOK*8) as i64 329 ctx[5] = sys_mmap(MAXTOK*8) as i64 330 ctx[6] = sys_mmap(MAXTOK*8) as i64 331 ctx[7] = sys_mmap(MAXTOK*8) as i64 332 ctx[8] = sys_mmap(MAXTOK*8) as i64 333 ctx[9] = sys_mmap(MAXTERMS*MAXCHAP*8) as i64 334 335 // ---- iterate chapters[] in order; read each chap file; extract ---- 336 let cpath: *u8 = sys_mmap(K_MAGIC_1024) 337 let lab: *u8 = sys_mmap(K_MAGIC_1024) 338 let file: *u8 = sys_mmap(256) 339 var cstart: i64 = jfrom(bj, bn, 0, "\"chapters\":[" as *u8) 340 var nchap: i64 = 0 341 if cstart >= 0 { 342 var cend: i64 = jfrom(bj, bn, cstart, "],\"nchapters\":" as *u8) // end of chapters[] (NOT first "]", which now matches a nested fn:[]) 343 if cend < 0 { cend = bn } 344 var cur: i64 = cstart + 11 345 var go: i64 = 1 346 while go == 1 { 347 let i_at: i64 = jfrom(bj, bn, cur, "\"idx\":" as *u8) 348 if i_at < 0 { go = 0 } else { if i_at > cend { go = 0 } else { 349 let idx: i64 = jint(bj, bn, i_at + 6) 350 let t_at: i64 = jfrom(bj, bn, i_at, "\"title\":\"" as *u8) 351 jstr(bj, bn, t_at + 8, lab, K_MAGIC_1024) 352 let f_at: i64 = jfrom(bj, bn, t_at, "\"file\":\"" as *u8) 353 jstr(bj, bn, f_at + 7, file, 256) 354 var pl: i64 = 0; while dir[pl]!=(0 as u8){ cpath[pl]=dir[pl]; pl=pl+1 } cpath[pl]=0x2f as u8; pl=pl+1 355 var fz: i64 = 0; while file[fz]!=(0 as u8){ cpath[pl]=file[fz]; pl=pl+1; fz=fz+1 } cpath[pl]=0 as u8 356 let clp: *i64 = sys_mmap(8) as *i64; clp[0]=0 357 let ct: *u8 = sys_read_file(cpath, clp) 358 if (ct as i64) != 0 { process_chapter(ct, clp[0], lab, idx, ctx) } 359 nchap = nchap + 1 360 cur = f_at + 7 361 } } 362 } 363 } 364 365 // ---- filter (count>=MINOCC) + sort by count desc, name asc ---- 366 let nterms: i64 = ctx[0] 367 let tnames: *u8 = ctx[1] as *u8 368 let tcount: *i64 = ctx[2] as *i64 369 let tfirst: *i64 = ctx[3] as *i64 370 let tchap: *i64 = ctx[9] as *i64 371 let order: *i64 = sys_mmap(MAXTERMS*8) as *i64 372 var K: i64 = 0 373 var ii: i64 = 0 374 while ii < nterms { if tcount[ii] >= MINOCC { order[K] = ii; K = K + 1 } ii = ii + 1 } 375 var a: i64 = 0 376 while a < K { 377 var best: i64 = a 378 var b: i64 = a + 1 379 while b < K { 380 let x: i64 = order[b]; let y: i64 = order[best] 381 var bef: i64 = 0 382 if tcount[x] > tcount[y] { bef = 1 } else { if tcount[x] == tcount[y] { bef = strlt((tnames as i64 + x*NAMELEN) as *u8, (tnames as i64 + y*NAMELEN) as *u8) } } 383 if bef == 1 { best = b } 384 b = b + 1 385 } 386 let tmp: i64 = order[a]; order[a] = order[best]; order[best] = tmp 387 a = a + 1 388 } 389 390 // ---- emit xray.json ---- 391 let outp: *u8 = sys_mmap(K_MAGIC_1024) 392 var ol: i64 = 0; while dir[ol]!=(0 as u8){ outp[ol]=dir[ol]; ol=ol+1 } 393 let xj: *u8 = "/xray.json" as *u8; z=0; while xj[z]!=(0 as u8){ outp[ol]=xj[z]; ol=ol+1; z=z+1 } outp[ol]=0 as u8 394 let fd: i64 = sys_openat_wr(outp, 0x1a4) 395 if fd < 0 { p("READER-XRAY FAIL open out\n" as *u8); sys_exit(1); return 1 } 396 w(fd, "{\"slug\":\"" as *u8); wesc_json(fd, slug); w(fd, "\",\"source\":\"book-intrinsic\",\"minocc\":" as *u8); wn(fd, MINOCC) 397 w(fd, ",\"nterms\":" as *u8); wn(fd, K); w(fd, ",\"terms\":[" as *u8) 398 var e: i64 = 0 399 while e < K { 400 let ti: i64 = order[e] 401 if e > 0 { w(fd, "," as *u8) } 402 w(fd, "{\"name\":\"" as *u8); wesc_json(fd, (tnames as i64 + ti*NAMELEN) as *u8) 403 w(fd, "\",\"count\":" as *u8); wn(fd, tcount[ti]) 404 w(fd, ",\"first\":" as *u8); wn(fd, tfirst[ti]) 405 w(fd, ",\"by\":[" as *u8) // flat per-chapter distribution: [chap,count,chap,count,...] 406 var fb: i64 = 1 407 var ci: i64 = 0 408 while ci < MAXCHAP { 409 let cc: i64 = tchap[ti*MAXCHAP + ci] 410 if cc > 0 { 411 if fb == 0 { w(fd, "," as *u8) } 412 wn(fd, ci); w(fd, "," as *u8); wn(fd, cc) 413 fb = 0 414 } 415 ci = ci + 1 416 } 417 w(fd, "]}" as *u8) 418 e = e + 1 419 } 420 w(fd, "]}\n" as *u8) 421 sys_close(fd) 422 423 // ---- raw scorecard to stdout (show-raw-output, no overclaim) ---- 424 p("READER-XRAY ok slug=" as *u8); p(slug); p(" chapters=" as *u8); wn(1, nchap) 425 p(" terms_total=" as *u8); wn(1, nterms); p(" kept(>=" as *u8); wn(1, MINOCC); p(")=" as *u8); wn(1, K); p("\n" as *u8) 426 var d: i64 = 0 427 while d < K { 428 let ti: i64 = order[d] 429 p(" term " as *u8); p((tnames as i64 + ti*NAMELEN) as *u8) 430 p(" count=" as *u8); wn(1, tcount[ti]); p(" first=c" as *u8); wn(1, tfirst[ti]); p("\n" as *u8) 431 d = d + 1 432 } 433 p(" -> " as *u8); p(outp); p("\n" as *u8) 434 if K <= 0 { p("READER-XRAY note: 0 terms kept (book may have few recurring proper nouns)\n" as *u8) } 435 sys_exit(0); return 0 436}