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}