code wiki / _hdl_build / nx_corpus_ingest.nx
nx_corpus_ingest.nx source
↩ module page · 681 lines · 36150 B
1// nx_corpus_ingest.nx -- SOVEREIGN corpus ingestion: a DIRECTORY of documents -> a domain's PUBLIC seg_store
2// shard, searchable at once (ss_write_seg builds .terms per segment; the store IS the index -- no tsv, no
3// derived artifact). This is the front door for ALL THREE nishi-search fronts (operator 2026-07-03):
4// library: nx_corpus_ingest knowledge/library nishifamily.com (.txt/.md -> /doc text views)
5// site pages: nx_corpus_ingest <pagesdir> andelinwest.com / (.html -> text + url:<cid> row,
6// so the SERP result links to the REAL page instead of /doc)
7// web: the crawler drops fetched pages in a dir -> same ingest with absolute-URL prefix rows
8// estate: nx_corpus_ingest tree <root> estate estate://<root>/ (E1, 2026-09-14: every source file
9// under a tree, recursively, one document per file, its relative path as the url, its identifiers
10// anchored, and a changed file REPLACING its previous document through a path row + tombstones)
11// Mechanics: getdents64 walk (flat dir, or the recursive tree walk) -> .txt/.md raw, .html/.htm via
12// nx_html_to_text (script/style suppressed, entities decoded) -> cid = the same polynomial hash
13// nx_dp_tsv_migrate uses (content-addressed, idempotent) -> skip-if-already-present (re-runs are clean) ->
14// RAW ss_add doc:<cid> (+ url:<cid>) batched into 1MB writer segments, each committed under a FRESH segid
15// (the seg store's ss_next_segid -- never clobbers existing segments). ONE ingest body (ci_ingest_file) and ONE
16// writer put (ci_put) serve both walks: two copies would agree the day they were written and drift on the next
17// edit. license_tier: ORIGINAL
18import "nx_docportal_search_seg.nx"
19import "nx_html_to_text.nx"
20import "nx_funchead_lib.nx"
21const CI_MAGIC_1125899906842597: i64 = 1125899906842597
22const CI_MAGIC_131072: i64 = 131072
23const CI_MAGIC_1024: i64 = 1024
24const CI_MAGIC_65536: i64 = 65536
25
26const CI_DOCCAP: i64 = 900000 // per-doc byte cap (writer segments are 1MB; a doc must fit with headroom)
27const CI_MINDOC: i64 = 20 // skip empty-ish extractions
28const CI_RAWCAP: i64 = 8388608 // largest raw file we read
29const CI_DT_DIR: i64 = 4 // dirent d_type of a directory
30const CI_WORD: i64 = 8
31const CI_BOX: i64 = 16
32const CI_KEYBUF: i64 = 64
33const CI_PREFIXBUF: i64 = 512
34const CI_DIGIT0: i64 = 48
35const CI_DIGIT9: i64 = 57
36const CI_DECBUF: i64 = 24
37const CI_LF: i64 = 10
38const CI_KIND_LIVE: i64 = 1 // seg-store entry kinds: a live row, and a tombstone that retires a key
39const CI_KIND_TOMB: i64 = 2
40const CI_DOC_JOURNAL: i64 = 3 // ci_kind_source: an append-only journal (.jrnl .log), ingested TAIL-anchored (E3)
41const CI_DOC_BOARD: i64 = 4 // ci_kind_source: a compare board (.plan .matrix), ingested whole AND row by row (E3b)
42const CI_ROW_SEP: *u8 = "#" // a row document's url is the board url, this separator, the row index
43const CI_ROW_RUNG: *u8 = "rung|" // a rung row carries the definition token of its id, the way a function head does
44const CI_CONF_PIPE: i64 = 124 // estate_corpora.conf field separator
45const CI_CONF_HASH: i64 = 35 // estate_corpora.conf comment lead byte
46const CI_CR: i64 = 13 // a CRLF conf leaves this as the last byte of a row; stripped, never parsed as data
47const CI_STATE_WORDS: i64 = 12
48const CI_ANCHOR_PAD: i64 = 64 // the anchor tail is never longer than the text it came from; this is the newline and slack
49const CI_TREE_MAXDEPTH: i64 = 12 // a source tree deeper than this is not a source tree; announced, never silent
50
51func ci_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
52func ci_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
53func ci_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i }
54// decimal digits of a non-negative value into out (no terminator); returns the digit count. THE one digit emitter
55// here: the keys, the path row value and the report all spell numbers through it.
56func ci_dec(v: i64, out: *u8) -> i64 {
57 if v == 0 { out[0] = CI_DIGIT0 as u8; return 1 }
58 let t: *u8 = sys_mmap(CI_DECBUF)
59 var k: i64 = 0
60 var m: i64 = v
61 while m > 0 { t[k] = (CI_DIGIT0 + (m % 10)) as u8; m = m / 10; k = k + 1 }
62 var j: i64 = 0
63 while j < k { out[j] = t[k - 1 - j]; j = j + 1 }
64 return k
65}
66func ci_num(v: i64) -> i64 {
67 let bb: *u8 = sys_mmap(CI_DECBUF + 4)
68 var m: i64 = v
69 var o: i64 = 0
70 if m < 0 { bb[0] = 45 as u8; o = 1; m = 0 - m }
71 o = o + ci_dec(m, (bb as i64 + o) as *u8)
72 sys_write(1, bb, o); return 0
73}
74// digits in p[0..n) as a value; a non-digit byte is skipped (the path row value is digits only)
75func ci_atoi(p: *u8, n: i64) -> i64 {
76 var v: i64 = 0
77 var i: i64 = 0
78 while i < n { let c: i64 = p[i] as i64; if c >= CI_DIGIT0 { if c <= CI_DIGIT9 { v = v * 10 + (c - CI_DIGIT0) } } i = i + 1 }
79 return v
80}
81
82// stable positive content id -- SAME polynomial as nx_dp_tsv_migrate.mg_hash (idempotent across organs)
83func ci_hash(s: *u8, n: i64) -> i64 {
84 var h: i64 = CI_MAGIC_1125899906842597
85 var i: i64 = 0
86 while i < n { h = (h * 131) + (s[i] as i64); i = i + 1 }
87 if h < 0 { h = 0 - h }
88 return h & 0x7fffffffffffffff
89}
90// build "<pfx><id>" NUL-terminated into out; returns its length. ONE key builder for url:/fp:/pth: (a literal each,
91// never a string spelled as character codes -- the earlier url:/fp: builders were, and no grep could find them).
92func ci_mkkey_pfx(pfx: *u8, id: i64, out: *u8) -> i64 {
93 var o: i64 = ci_cat(out, 0, pfx)
94 o = o + ci_dec(id, (out as i64 + o) as *u8)
95 out[o] = 0 as u8
96 return o
97}
98// build "url:<cid>" (MUST match the serve side's dsv_mkurlkey)
99func ci_mkurlkey(cid: i64, out: *u8) -> i64 { return ci_mkkey_pfx("url:" as *u8, cid, out) }
100// build "fp:<cid>" -- the near-duplicate FINGERPRINT row, sibling of doc:/url: (2026-08-16).
101// WHY IT EXISTS: every ingest organ ALREADY computes nx_simhash_fingerprint and then THROWS IT AWAY --
102// it dedupes within one run against keptfp[] and vanishes at exit. So intra-host near-duplication, which
103// is the real doorway signature (many urls, one template, one swapped word), could only be asked of the
104// corpus by re-fetching and re-computing ALL of it. Persisting the value that was already computed turns
105// that into a query. Capture once, ask forever.
106// MEASURED 2026-08-16, the reason this is the right signal and redirect depth is not: bestbeeg.top/key/dudh
107// returns 200 with ZERO redirects and a 9,399-byte body. A hop-count detector fires on nothing there; the
108// duplication is visible, the redirection is not.
109// ADDITIVE BY CONSTRUCTION: a new key prefix. Readers scanning doc:/url:/fr:/hd:/pol:/pr:/out: are
110// untouched, and an absent fp: row simply means "ingested before this shipped" -- never a wrong answer.
111func ci_mkfpkey(cid: i64, out: *u8) -> i64 { return ci_mkkey_pfx("fp:" as *u8, cid, out) }
112// build "fe:<cid>" -- the FETCH EPOCH row, sibling of doc:/url:/fp: (S12, 2026-09-17). Every crawler knows the moment it
113// fetched a kept page and then threw it away; the only ledger was crawl_outcomes.log, which no result can be joined to at
114// query time. Stored beside the doc, a result can say how old its bytes are. ADDITIVE BY CONSTRUCTION: readers of the
115// other prefixes are untouched, and an absent fe: row means "ingested before this shipped" -- unobserved, never an age of zero.
116func ci_mkfekey(cid: i64, out: *u8) -> i64 { return ci_mkkey_pfx("fe:" as *u8, cid, out) }
117// build "pth:<hash of the url tail>" -- the PATH row (E1, 2026-09-14): value = the decimal cid of the document that
118// currently stands for that path, so a re-ingest of a CHANGED file finds its predecessor and tombstones it instead
119// of leaving two live documents for one file. Additive: readers of the older prefixes never see it.
120func ci_mkpthkey(pathhash: i64, out: *u8) -> i64 { return ci_mkkey_pfx("pth:" as *u8, pathhash, out) }
121// does name end with the null-terminated suffix? (case-sensitive; corpus files are ours)
122func ci_ends(name: *u8, suf: *u8) -> i64 {
123 let nl: i64 = ci_len(name)
124 let sl: i64 = ci_len(suf)
125 if nl < sl { return 0 }
126 var i: i64 = 0
127 while i < sl { if name[nl - sl + i] != suf[i] { return 0 } i = i + 1 }
128 return 1
129}
130func ci_contains(name: *u8, ndl: *u8) -> i64 {
131 let nl: i64 = ci_len(name)
132 let dl: i64 = ci_len(ndl)
133 if dl == 0 { return 0 }
134 var i: i64 = 0
135 while i + dl <= nl {
136 var j: i64 = 0
137 var hit: i64 = 1
138 while j < dl { if name[i + j] != ndl[j] { hit = 0; j = dl } else { j = j + 1 } }
139 if hit == 1 { return 1 }
140 i = i + 1
141 }
142 return 0
143}
144// classify: 1 = raw text (.txt/.md), 2 = html (.html/.htm), 0 = skip (.bak anywhere, everything else)
145func ci_kind(name: *u8) -> i64 {
146 if ci_contains(name, ".bak" as *u8) == 1 { return 0 }
147 if ci_ends(name, ".txt" as *u8) == 1 { return 1 }
148 if ci_ends(name, ".md" as *u8) == 1 { return 1 }
149 if ci_ends(name, ".rst" as *u8) == 1 { return 1 }
150 if ci_ends(name, ".html" as *u8) == 1 { return 2 }
151 if ci_ends(name, ".htm" as *u8) == 1 { return 2 }
152 return 0
153}
154// E1 source kinds: the estate's own text -- NishiLang, markdown, plain text, confs and the compare boards.
155// Everything else (binaries, fossils, media) is skipped and COUNTED as skipped_other.
156func ci_kind_source(name: *u8) -> i64 {
157 if ci_contains(name, ".bak" as *u8) == 1 { return 0 }
158 if ci_ends(name, ".nx" as *u8) == 1 { return 1 }
159 if ci_ends(name, ".md" as *u8) == 1 { return 1 }
160 if ci_ends(name, ".txt" as *u8) == 1 { return 1 }
161 if ci_ends(name, ".conf" as *u8) == 1 { return 1 }
162 if ci_ends(name, ".plan" as *u8) == 1 { return CI_DOC_BOARD }
163 if ci_ends(name, ".matrix" as *u8) == 1 { return CI_DOC_BOARD }
164 if ci_ends(name, ".sota" as *u8) == 1 { return 1 }
165 if ci_ends(name, ".gates" as *u8) == 1 { return 1 }
166 if ci_ends(name, ".axes" as *u8) == 1 { return 1 }
167 if ci_ends(name, ".bench" as *u8) == 1 { return 1 }
168 if ci_ends(name, ".list" as *u8) == 1 { return 1 }
169 if ci_ends(name, ".status" as *u8) == 1 { return 1 }
170 if ci_ends(name, ".stamp" as *u8) == 1 { return 1 }
171 if ci_ends(name, ".jrnl" as *u8) == 1 { return CI_DOC_JOURNAL }
172 if ci_ends(name, ".log" as *u8) == 1 { return CI_DOC_JOURNAL }
173 if ci_ends(name, ".refs" as *u8) == 1 { return 1 }
174 return 0
175}
176// directories the tree walk never enters: hidden entries (which also covers . and ..), build fossils, job
177// artifacts, retired copies, the stores and the fetched mirrors -- none of them is the estate's own text.
178// exact name equality. A SUFFIX test here skipped _hdl_build -- the tree that holds most current organs -- on the
179// first estate ingest, because _hdl_build ends with _build; MEASURED 2026-09-14 by a query for a lib that was not
180// there. A skip list matches whole names, never tails.
181func ci_name_eq(name: *u8, lit: *u8) -> i64 {
182 if ci_len(name) != ci_len(lit) { return 0 }
183 return ci_contains(name, lit)
184}
185func ci_tree_skipdir(name: *u8) -> i64 {
186 let dot: *u8 = "." as *u8
187 if name[0] == dot[0] { return 1 }
188 if ci_name_eq(name, "_build" as *u8) == 1 { return 1 }
189 if ci_name_eq(name, "_jobs" as *u8) == 1 { return 1 }
190 if ci_name_eq(name, "retired" as *u8) == 1 { return 1 }
191 if ci_name_eq(name, "store" as *u8) == 1 { return 1 }
192 if ci_name_eq(name, "fetched" as *u8) == 1 { return 1 }
193 return 0
194}
195
196// Delegates to the seg store's ONE derivation (ss_next_segid, dynamic manifest read): a shard past the old
197// 256-name buffer -- the estate source tree commits hundreds of segments -- can no longer overrun anything here.
198func ci_next_segid(prefix: *u8) -> i64 {
199 return ss_next_segid(prefix)
200}
201// THE INGEST STATE, one per run, shared by both walks: [0]=writer [1]=segid [2]=prefix [3]=store handle
202// (0 = fresh shard, fine) [4]=pbox [5]=lbox [6]=counts [7]=key [8]=ukey [9]=urlval [10]=hout [11]=anchors
203// (1 = the tree walk: identifier anchors appended to every text document and a path row kept per file).
204// counts[0]=ingested counts[1]=skipped-present counts[2]=skipped-other counts[3]=segments-committed
205// counts[4]=replaced (a changed file whose previous document was tombstoned)
206func ci_state_new(prefix: *u8, counts: *i64) -> *i64 {
207 counts[0] = 0; counts[1] = 0; counts[2] = 0; counts[3] = 0; counts[4] = 0
208 let st: *i64 = sys_mmap(CI_STATE_WORDS * CI_WORD) as *i64
209 st[0] = ss_begin() as i64
210 st[1] = ci_next_segid(prefix)
211 st[2] = prefix as i64
212 st[3] = ss_open(prefix) as i64 // open the CURRENT store once for skip-if-present and the path rows
213 st[4] = sys_mmap(CI_BOX) as i64
214 st[5] = sys_mmap(CI_BOX) as i64
215 st[6] = counts as i64
216 st[7] = sys_mmap(CI_KEYBUF) as i64
217 st[8] = sys_mmap(CI_KEYBUF) as i64
218 st[9] = sys_mmap(CI_MAGIC_1024) as i64
219 st[10] = sys_mmap(CI_RAWCAP + CI_MAGIC_65536) as i64
220 st[11] = 0
221 return st
222}
223// commit whatever the writer still holds
224func ci_state_finish(st: *i64) -> i64 {
225 let w: *i64 = st[0] as *i64
226 let counts: *i64 = st[6] as *i64
227 if w[1] > 0 {
228 if ss_commit(st[2] as *u8, w, st[1]) == 0 { counts[3] = counts[3] + 1 } else { ci_puts(" FINAL-COMMIT-FAIL\n" as *u8) }
229 }
230 return 0
231}
232// ONE writer put: a row into the current writer, and when the writer is full, commit the segment, open the next
233// and put again. -1 only when a row cannot fit an EMPTY writer (a document past the segment size).
234func ci_put(st: *i64, kind: i64, key: *u8, val: *u8, vlen: i64) -> i64 {
235 var w: *i64 = st[0] as *i64
236 if ss_add(w, kind, key, val, vlen) >= 0 { return 0 }
237 let counts: *i64 = st[6] as *i64
238 if ss_commit(st[2] as *u8, w, st[1]) != 0 { ci_puts(" COMMIT-FAIL seg=" as *u8); ci_num(st[1]); ci_puts("\n" as *u8) } else { counts[3] = counts[3] + 1 }
239 st[1] = st[1] + 1
240 w = ss_begin()
241 st[0] = w as i64
242 if ss_add(w, kind, key, val, vlen) < 0 { return 0 - 1 }
243 return 0
244}
245// E1b DEFINITION ANCHORS: one token per function head, the def prefix glued to the collapsed name (ss_def_token, the
246// same derivation the query side uses), appended after the identifier anchors so the file that DECLARES a symbol
247// carries a term no caller carries. The heads come from nx_funchead_lib (the scanner nx_funcmine reads), never a
248// second scanner. The head table is sized from the text itself: a head is at least CI_DEF_MINHEAD bytes, so
249// n / CI_DEF_MINHEAD + 1 entries can never truncate -- a derived bound, not a guessed cap.
250const CI_DEF_MINHEAD: i64 = 7 // the shortest head: func, a space, a one-byte name, the paren
251func ci_def_anchors(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
252 let maxh: i64 = n / CI_DEF_MINHEAD + 1
253 let starts: *i64 = sys_mmap(maxh * CI_WORD) as *i64
254 let noffs: *i64 = sys_mmap(maxh * CI_WORD) as *i64
255 let nlens: *i64 = sys_mmap(maxh * CI_WORD) as *i64
256 let nf: i64 = fh_scan(src, n, starts, noffs, nlens, maxh)
257 // the token buffer is sized from the longest head this file declares plus the prefix: derived, never a guessed cap
258 var maxnl: i64 = 0
259 var m: i64 = 0
260 while m < nf { if nlens[m] > maxnl { maxnl = nlens[m] } m = m + 1 }
261 let tokcap: i64 = maxnl + ci_len(SS_DEF_PREFIX) + 1
262 let tok: *u8 = sys_mmap(tokcap)
263 var o: i64 = 0
264 var k: i64 = 0
265 while k < nf {
266 let tl: i64 = ss_def_token((src as i64 + noffs[k]) as *u8, nlens[k], tok, tokcap)
267 if o + tl + 1 < cap {
268 var j: i64 = 0
269 while j < tl { out[o] = tok[j]; o = o + 1; j = j + 1 }
270 out[o] = SS_SP as u8; o = o + 1
271 }
272 k = k + 1
273 }
274 sys_munmap(starts, maxh * CI_WORD)
275 sys_munmap(noffs, maxh * CI_WORD)
276 sys_munmap(nlens, maxh * CI_WORD)
277 sys_munmap(tok, tokcap)
278 return o
279}
280// ONE FILE INTO THE WRITER -- the single ingest body. kind 1 = raw text, 2 = html. urlname is the url tail
281// appended to urlprefix (the bare file name for the flat walk, the relative path for the tree walk).
282func ci_ingest_file(st: *i64, path: *u8, kind: i64, urlprefix: *u8, urlname: *u8) -> i64 {
283 let counts: *i64 = st[6] as *i64
284 let h: *i64 = st[3] as *i64
285 let pbox: *i64 = st[4] as *i64
286 let lbox: *i64 = st[5] as *i64
287 let key: *u8 = st[7] as *u8
288 let ukey: *u8 = st[8] as *u8
289 let urlval: *u8 = st[9] as *u8
290 let hout: *u8 = st[10] as *u8
291 let szp: *i64 = sys_mmap(CI_BOX) as *i64
292 let raw: *u8 = ss_readall(path, szp)
293 var rn: i64 = szp[0]
294 if rn > CI_RAWCAP { rn = 0 } // absurd file -> skip, never OOM
295 if rn <= 0 { counts[2] = counts[2] + 1; return 0 }
296 var txt: *u8 = raw
297 var tn: i64 = rn
298 if kind == 2 {
299 let hr: i64 = nx_html_to_text(raw, rn, hout, CI_RAWCAP + CI_MAGIC_65536)
300 if hr <= 0 { tn = 0 } else { txt = hout; tn = hr }
301 }
302 // E3: an append-only journal is anchored at its TAIL -- the newest CI_DOCCAP bytes -- because the head of an
303 // append-only file is its past; the tail is the document that answers a question about now.
304 if kind == CI_DOC_JOURNAL { if tn > CI_DOCCAP { txt = (raw as i64 + (tn - CI_DOCCAP)) as *u8; tn = CI_DOCCAP } }
305 if tn > CI_DOCCAP { tn = CI_DOCCAP }
306 if tn < CI_MINDOC { counts[2] = counts[2] + 1; ss_freeall(raw, szp[0]); return 0 }
307 // IDENTIFIER ANCHORS (E1): the tree walk appends every underscored identifier's collapsed form after a newline,
308 // so the tokenizer indexes the whole identifier and the file that defines it ranks first for it.
309 var doc: *u8 = txt
310 var dn: i64 = tn
311 var ab: *u8 = 0 as *u8
312 var abn: i64 = 0
313 if st[11] == 1 {
314 // three lengths: the text, its identifier anchors (never longer than the text) and its definition anchors
315 // (one token per head, each shorter than the head it came from), plus the newline and slack
316 abn = tn + tn + tn + CI_ANCHOR_PAD
317 ab = sys_mmap(abn)
318 var ao: i64 = 0
319 var ci: i64 = 0
320 while ci < tn { ab[ao] = txt[ci]; ao = ao + 1; ci = ci + 1 }
321 ab[ao] = CI_LF as u8; ao = ao + 1
322 ao = ao + ss_ident_anchors(txt, tn, (ab as i64 + ao) as *u8, abn - ao)
323 ao = ao + ci_def_anchors(txt, tn, (ab as i64 + ao) as *u8, abn - ao)
324 doc = ab
325 dn = ao
326 if dn > CI_DOCCAP { dn = CI_DOCCAP }
327 }
328 let r: i64 = ci_ingest_doc(st, doc, dn, urlprefix, urlname)
329 // E3b: a board is also ingested ROW BY ROW; the row pass runs whether or not the whole-file document was present,
330 // because the rows are content-addressed themselves (a present row costs one key lookup, never a second copy)
331 if r >= 0 { if kind == CI_DOC_BOARD { ci_ingest_board_rows(st, txt, tn, urlprefix, urlname) } }
332 // the writer copied the bytes it needs; the raw file and the anchored copy are released per document so a
333 // tree of thousands of sources never becomes the load it indexes
334 ss_freeall(raw, szp[0])
335 if abn > 0 { sys_munmap(ab, abn) }
336 return 0
337}
338// ONE DOCUMENT INTO THE STORE: the content-addressed skip, the path row's tombstones for a replaced document, the doc
339// row, the url row and the path row. Shared by the file ingest and the board-row ingest (E3b): two copies would agree
340// the day they were written and drift on the next edit. Returns 1 ingested, 0 present, -1 when the put failed.
341func ci_ingest_doc(st: *i64, doc: *u8, dn: i64, urlprefix: *u8, urlname: *u8) -> i64 {
342 let counts: *i64 = st[6] as *i64
343 let h: *i64 = st[3] as *i64
344 let pbox: *i64 = st[4] as *i64
345 let lbox: *i64 = st[5] as *i64
346 let key: *u8 = st[7] as *u8
347 let ukey: *u8 = st[8] as *u8
348 let urlval: *u8 = st[9] as *u8
349 let cid: i64 = ci_hash(doc, dn)
350 dss_mkkey(cid, key)
351 var present: i64 = 0
352 if (h as i64) != 0 { if ss_hget(h, key, pbox, lbox) == 1 { present = 1 } }
353 if present == 1 { counts[1] = counts[1] + 1; return 0 }
354 // THE PATH ROW (E1): when this path already stands for an older document, that document and its url row are
355 // tombstoned before the replacement lands -- one live document per file, and a changed file is a replacement,
356 // never a second copy beside the first.
357 let pkey: *u8 = sys_mmap(CI_KEYBUF)
358 var pathhash: i64 = 0
359 if st[11] == 1 {
360 pathhash = ci_hash(urlname, ci_len(urlname))
361 ci_mkpthkey(pathhash, pkey)
362 if (h as i64) != 0 { if ss_hget(h, pkey, pbox, lbox) == 1 {
363 let oldcid: i64 = ci_atoi(pbox[0] as *u8, lbox[0])
364 if oldcid != 0 { if oldcid != cid {
365 let okey: *u8 = sys_mmap(CI_KEYBUF)
366 dss_mkkey(oldcid, okey)
367 ci_put(st, CI_KIND_TOMB, okey, 0 as *u8, 0)
368 ci_mkurlkey(oldcid, okey)
369 ci_put(st, CI_KIND_TOMB, okey, 0 as *u8, 0)
370 counts[4] = counts[4] + 1
371 sys_munmap(okey, CI_KEYBUF)
372 } }
373 } }
374 }
375 if ci_put(st, CI_KIND_LIVE, key, doc, dn) < 0 { counts[2] = counts[2] + 1; sys_munmap(pkey, CI_KEYBUF); return 0 - 1 }
376 counts[0] = counts[0] + 1
377 // NULL POINTER = no url rows. NEVER an empty-string literal here: `"" as *u8`
378 // miscompiles to an unterminated pool pointer (the 2026-07-03 library-url bug --
379 // 596 docs got "corpus-ingest: dir=..." hrefs; same codegen-gotcha family as the
380 // inline cast-index one). Gate T8 pins the no-urlprefix path clean.
381 if (urlprefix as i64) != 0 { if ci_len(urlprefix) > 0 {
382 var uo: i64 = ci_cat(urlval, 0, urlprefix)
383 uo = ci_cat(urlval, uo, urlname)
384 ci_mkurlkey(cid, ukey)
385 ci_put(st, CI_KIND_LIVE, ukey, urlval, uo)
386 } }
387 if st[11] == 1 {
388 let pv: *u8 = sys_mmap(CI_DECBUF)
389 let pvn: i64 = ci_dec(cid, pv)
390 ci_put(st, CI_KIND_LIVE, pkey, pv, pvn)
391 sys_munmap(pv, CI_DECBUF)
392 }
393 sys_munmap(pkey, CI_KEYBUF)
394 return 1
395}
396// E3b BOARD ROWS AS DOCUMENTS: every non-empty, non-comment row of a .plan or .matrix is its own document, url =
397// the board url + CI_ROW_SEP + the row index, with its own path row (a changed row replaces itself), its identifier
398// anchors, and for a `rung|<id>|` row the definition token of the id -- so a rung id ranks its rung row first the way
399// a function name ranks its head. The whole-file document beside it still answers prose. Returns rows ingested.
400func ci_ingest_board_rows(st: *i64, txt: *u8, tn: i64, urlprefix: *u8, urlname: *u8) -> i64 {
401 let counts: *i64 = st[6] as *i64
402 let un: i64 = ci_len(urlname)
403 let rncap: i64 = un + ci_len(CI_ROW_SEP) + CI_DECBUF + 1
404 let rowname: *u8 = sys_mmap(rncap)
405 let rungw: *u8 = CI_ROW_RUNG
406 let rungl: i64 = ci_len(rungw)
407 var rows: i64 = 0
408 var idx: i64 = 0
409 var i: i64 = 0
410 while i < tn {
411 var e: i64 = i
412 var found: i64 = 0
413 while found == 0 { if e >= tn { found = 1 } else { if (txt[e] as i64) == CI_LF { found = 1 } else { e = e + 1 } } }
414 var le: i64 = e
415 if le > i { if (txt[le - 1] as i64) == CI_CR { le = le - 1 } }
416 let rl: i64 = le - i
417 if rl >= CI_MINDOC { if (txt[i] as i64) != CI_CONF_HASH {
418 let row: *u8 = (txt as i64 + i) as *u8
419 // the row, a newline, its identifier anchors and (rung rows) the definition token of the id: three row
420 // lengths plus slack, the same derived bound the file ingest uses
421 let cap: i64 = rl + rl + rl + CI_ANCHOR_PAD
422 let rb: *u8 = sys_mmap(cap)
423 var o: i64 = 0
424 var c: i64 = 0
425 while c < rl { rb[o] = row[c]; o = o + 1; c = c + 1 }
426 rb[o] = CI_LF as u8; o = o + 1
427 o = o + ss_ident_anchors(row, rl, (rb as i64 + o) as *u8, cap - o)
428 var isrung: i64 = 1
429 if rl <= rungl { isrung = 0 }
430 var q: i64 = 0
431 while q < rungl { if isrung == 1 { if row[q] != rungw[q] { isrung = 0 } } q = q + 1 }
432 if isrung == 1 {
433 var idn: i64 = 0
434 var stop: i64 = 0
435 while stop == 0 { if rungl + idn >= rl { stop = 1 } else { if (row[rungl + idn] as i64) == CI_CONF_PIPE { stop = 1 } else { idn = idn + 1 } } }
436 if idn > 0 { if o + idn + rungl + 1 < cap {
437 let dl: i64 = ss_def_token((row as i64 + rungl) as *u8, idn, (rb as i64 + o) as *u8, cap - o)
438 o = o + dl
439 rb[o] = SS_SP as u8; o = o + 1
440 } }
441 }
442 var ro: i64 = ci_cat(rowname, 0, urlname)
443 ro = ci_cat(rowname, ro, CI_ROW_SEP)
444 ro = ro + ci_dec(idx, (rowname as i64 + ro) as *u8)
445 rowname[ro] = 0 as u8
446 if ci_ingest_doc(st, rb, o, urlprefix, rowname) == 1 { rows = rows + 1 }
447 sys_munmap(rb, cap)
448 } }
449 idx = idx + 1
450 i = e + 1
451 }
452 counts[5] = counts[5] + rows
453 sys_munmap(rowname, rncap)
454 return rows
455}
456
457// THE FLAT INGEST. Returns docs ingested (>=0), or -1 open-fail. counts as in ci_state_new.
458func ci_run(dir: *u8, domain: *u8, urlprefix: *u8, counts: *i64) -> i64 {
459 let prefix: *u8 = sys_mmap(CI_PREFIXBUF)
460 dss_prefix(domain, prefix)
461 let st: *i64 = ci_state_new(prefix, counts)
462 let fd: i64 = sys_openat_rd(dir)
463 if fd < 0 { ci_puts("corpus-ingest: cannot open dir " as *u8); ci_puts(dir); ci_puts("\n" as *u8); return 0 - 1 }
464 let dbuf: *u8 = sys_mmap(CI_MAGIC_131072)
465 let path: *u8 = sys_mmap(CI_MAGIC_1024)
466 var go: i64 = 1
467 while go == 1 {
468 let nr: i64 = sys_getdents64(fd, dbuf, CI_MAGIC_131072)
469 if nr <= 0 { go = 0 } else {
470 var off: i64 = 0
471 while off < nr {
472 let rec: *u8 = (dbuf as i64 + off) as *u8
473 let ty: i64 = dirent_type(rec)
474 let nm: *u8 = dirent_name(rec)
475 off = off + dirent_reclen(rec)
476 if ty != CI_DT_DIR {
477 let kind: i64 = ci_kind(nm)
478 if kind == 0 { counts[2] = counts[2] + 1 } else {
479 var po: i64 = ci_cat(path, 0, dir)
480 po = ci_cat(path, po, "/" as *u8)
481 po = ci_cat(path, po, nm)
482 path[po] = 0 as u8
483 ci_ingest_file(st, path, kind, urlprefix, nm)
484 }
485 }
486 }
487 }
488 }
489 sys_close(fd)
490 ci_state_finish(st)
491 return counts[0]
492}
493
494// E1 -- THE TREE WALK: every source file under dir, recursively, urlname = the path relative to the root.
495// Returns files ingested or skipped-present (walked), so a caller can tell an empty tree from a refusal.
496func ci_walk_tree(st: *i64, dir: *u8, rel: *u8, depth: i64, urlprefix: *u8) -> i64 {
497 if depth > CI_TREE_MAXDEPTH { ci_puts("corpus-ingest: tree deeper than CI_TREE_MAXDEPTH at " as *u8); ci_puts(dir); ci_puts(" -- not entered\n" as *u8); return 0 }
498 let fd: i64 = sys_openat_rd(dir)
499 if fd < 0 { return 0 }
500 let counts: *i64 = st[6] as *i64
501 let dbuf: *u8 = sys_mmap(CI_MAGIC_131072)
502 let path: *u8 = sys_mmap(CI_MAGIC_1024)
503 let sub: *u8 = sys_mmap(CI_MAGIC_1024)
504 var walked: i64 = 0
505 var go: i64 = 1
506 while go == 1 {
507 let nr: i64 = sys_getdents64(fd, dbuf, CI_MAGIC_131072)
508 if nr <= 0 { go = 0 } else {
509 var off: i64 = 0
510 while off < nr {
511 let rec: *u8 = (dbuf as i64 + off) as *u8
512 let ty: i64 = dirent_type(rec)
513 let nm: *u8 = dirent_name(rec)
514 off = off + dirent_reclen(rec)
515 var po: i64 = ci_cat(path, 0, dir)
516 po = ci_cat(path, po, "/" as *u8)
517 po = ci_cat(path, po, nm)
518 path[po] = 0 as u8
519 var so: i64 = ci_cat(sub, 0, rel)
520 if so > 0 { so = ci_cat(sub, so, "/" as *u8) }
521 so = ci_cat(sub, so, nm)
522 sub[so] = 0 as u8
523 if ty == CI_DT_DIR {
524 if ci_tree_skipdir(nm) == 0 { walked = walked + ci_walk_tree(st, path, sub, depth + 1, urlprefix) }
525 } else {
526 let kind: i64 = ci_kind_source(nm)
527 if kind == 0 { counts[2] = counts[2] + 1 } else { ci_ingest_file(st, path, kind, urlprefix, sub); walked = walked + 1 }
528 }
529 }
530 }
531 }
532 sys_close(fd)
533 sys_munmap(dbuf, CI_MAGIC_131072)
534 sys_munmap(path, CI_MAGIC_1024)
535 sys_munmap(sub, CI_MAGIC_1024)
536 return walked
537}
538// E1 -- THE CODE BASE AS A CORPUS (search rung E1, 2026-09-14): every text file under a source tree becomes one
539// document in the domain's shard, its path relative to the root as the url under <urlprefix>, its underscored
540// identifiers anchored so a query for one finds the file that defines it, and a path row so a changed file
541// REPLACES its previous document. The shard is dp-<domain>-pub- like every other ingest; no public scope resolves
542// to the estate domain, so the corpus is reachable only through nx_estate_search until an auth-gated scope lands
543// (E4). Returns files walked (>= 0) or -1 when the root cannot be opened.
544func ci_ingest_source_tree(root: *u8, domain: *u8, urlprefix: *u8, counts: *i64) -> i64 {
545 let prefix: *u8 = sys_mmap(CI_PREFIXBUF)
546 dss_prefix(domain, prefix)
547 let st: *i64 = ci_state_new(prefix, counts)
548 st[11] = 1
549 let fd0: i64 = sys_openat_rd(root)
550 if fd0 < 0 { ci_puts("corpus-ingest: cannot open root " as *u8); ci_puts(root); ci_puts("\n" as *u8); return 0 - 1 }
551 sys_close(fd0)
552 let walked: i64 = ci_walk_tree(st, root, "" as *u8, 0, urlprefix)
553 ci_state_finish(st)
554 return walked
555}
556
557// E3 THE ESTATE'S OWN RECORD AS CORPORA: one conf row per tree (root|domain|urlprefix, knowledge/estate_corpora.conf;
558// a row whose first byte is the hash is a comment; a trailing CR is stripped so a CRLF conf parses clean), each row
559// walked through the ONE tree ingest above with the counts summed across rows. Returns the row count (0 = nothing
560// walked), -1 when the conf cannot be read. Adding a corpus is a data edit here, never a second organ or clock row.
561func ci_ingest_boards(confpath: *u8, counts: *i64) -> i64 {
562 let szp: *i64 = sys_mmap(CI_BOX) as *i64
563 let buf: *u8 = ss_readall(confpath, szp)
564 let n: i64 = szp[0]
565 if n <= 0 { return 0 - 1 }
566 let root: *u8 = sys_mmap(CI_MAGIC_1024)
567 let dom: *u8 = sys_mmap(CI_KEYBUF)
568 let pfx: *u8 = sys_mmap(CI_MAGIC_1024)
569 let rc: *i64 = sys_mmap(CI_KEYBUF) as *i64
570 let words: i64 = CI_KEYBUF / CI_WORD
571 var rows: i64 = 0
572 var i: i64 = 0
573 while i < n {
574 var e: i64 = i
575 var found: i64 = 0
576 while found == 0 { if e >= n { found = 1 } else { if (buf[e] as i64) == CI_LF { found = 1 } else { e = e + 1 } } }
577 var le: i64 = e
578 if le > i { if (buf[le - 1] as i64) == CI_CR { le = le - 1 } }
579 if le > i { if (buf[i] as i64) != CI_CONF_HASH {
580 var f: i64 = 0
581 var o: i64 = 0
582 var k: i64 = i
583 pfx[0] = 0 as u8
584 while k < le {
585 let c: i64 = buf[k] as i64
586 if c == CI_CONF_PIPE {
587 if f == 0 { root[o] = 0 as u8 }
588 if f == 1 { dom[o] = 0 as u8 }
589 f = f + 1
590 o = 0
591 } else {
592 if f == 0 { if o < CI_MAGIC_1024 - 1 { root[o] = c as u8; o = o + 1 } }
593 if f == 1 { if o < CI_KEYBUF - 1 { dom[o] = c as u8; o = o + 1 } }
594 if f == 2 { if o < CI_MAGIC_1024 - 1 { pfx[o] = c as u8; o = o + 1 } }
595 }
596 k = k + 1
597 }
598 if f == 0 { root[o] = 0 as u8 }
599 if f == 1 { dom[o] = 0 as u8 }
600 if f == 2 { pfx[o] = 0 as u8 }
601 if f >= 1 {
602 var z: i64 = 0
603 while z < words { rc[z] = 0; z = z + 1 }
604 var up: *u8 = pfx
605 if pfx[0] == (0 as u8) { up = 0 as *u8 }
606 ci_puts(" boards row root=" as *u8); ci_puts(root); ci_puts(" domain=" as *u8); ci_puts(dom)
607 let walked: i64 = ci_ingest_source_tree(root, dom, up, rc)
608 ci_puts(" walked=" as *u8); ci_num(walked); ci_puts(" ingested=" as *u8); ci_num(rc[0]); ci_puts(" replaced=" as *u8); ci_num(rc[4]); ci_puts(" rows=" as *u8); ci_num(rc[5]); ci_puts(" skipped_other=" as *u8); ci_num(rc[2]); ci_puts("\n" as *u8)
609 var q: i64 = 0
610 while q < words { counts[q] = counts[q] + rc[q]; q = q + 1 }
611 rows = rows + 1
612 }
613 } }
614 i = e + 1
615 }
616 ss_freeall(buf, szp[0])
617 return rows
618}
619
620func main(argc: i64, argv: *i64) -> i64 {
621 if argc < 3 { ci_puts("usage: nx_corpus_ingest <dir> <domain> [urlprefix] | nx_corpus_ingest tree <root> <domain> [urlprefix]\n" as *u8); return 1 }
622 let a1: *u8 = argv[1] as *u8
623 let treew: *u8 = "tree" as *u8
624 var is_tree: i64 = 0
625 if ci_len(a1) == ci_len(treew) { if ci_contains(a1, treew) == 1 { is_tree = 1 } }
626 if is_tree == 1 {
627 if argc < 4 { ci_puts("usage: nx_corpus_ingest tree <root> <domain> [urlprefix]\n" as *u8); return 1 }
628 let root: *u8 = argv[2] as *u8
629 let tdomain: *u8 = argv[3] as *u8
630 var tprefix: *u8 = 0 as *u8
631 if argc >= 5 { tprefix = argv[4] as *u8 }
632 let tcounts: *i64 = sys_mmap(CI_KEYBUF) as *i64
633 let walked: i64 = ci_ingest_source_tree(root, tdomain, tprefix, tcounts)
634 if walked < 0 { return 2 }
635 ci_puts("corpus-ingest: tree=" as *u8); ci_puts(root)
636 ci_puts(" -> shard dp-" as *u8); ci_puts(tdomain); ci_puts("-pub- walked=" as *u8); ci_num(walked)
637 ci_puts(" ingested=" as *u8); ci_num(tcounts[0])
638 ci_puts(" replaced=" as *u8); ci_num(tcounts[4])
639 ci_puts(" skipped_present=" as *u8); ci_num(tcounts[1])
640 ci_puts(" skipped_other=" as *u8); ci_num(tcounts[2])
641 ci_puts(" segments=" as *u8); ci_num(tcounts[3])
642 ci_puts(" rows=" as *u8); ci_num(tcounts[5])
643 ci_puts("\n" as *u8)
644 if tcounts[0] + tcounts[1] == 0 { ci_puts("corpus-ingest: NOTHING ingested (empty tree or all skipped)\n" as *u8); return 3 }
645 return 0
646 }
647 let boardsw: *u8 = "boards" as *u8
648 var is_boards: i64 = 0
649 if ci_len(a1) == ci_len(boardsw) { if ci_contains(a1, boardsw) == 1 { is_boards = 1 } }
650 if is_boards == 1 {
651 let bcounts: *i64 = sys_mmap(CI_KEYBUF) as *i64
652 let rows: i64 = ci_ingest_boards(argv[2] as *u8, bcounts)
653 if rows < 0 { ci_puts("corpus-ingest: boards conf unreadable\n" as *u8); return 2 }
654 ci_puts("corpus-ingest: boards conf=" as *u8); ci_puts(argv[2] as *u8)
655 ci_puts(" rows=" as *u8); ci_num(rows)
656 ci_puts(" ingested=" as *u8); ci_num(bcounts[0])
657 ci_puts(" replaced=" as *u8); ci_num(bcounts[4])
658 ci_puts(" skipped_present=" as *u8); ci_num(bcounts[1])
659 ci_puts(" skipped_other=" as *u8); ci_num(bcounts[2])
660 ci_puts(" segments=" as *u8); ci_num(bcounts[3])
661 ci_puts(" rows=" as *u8); ci_num(bcounts[5])
662 ci_puts("\n" as *u8)
663 if rows == 0 { ci_puts("corpus-ingest: NOTHING ingested (no rows in the conf)\n" as *u8); return 3 }
664 return 0
665 }
666 let dir: *u8 = a1
667 let domain: *u8 = argv[2] as *u8
668 var urlprefix: *u8 = 0 as *u8 // null = no url rows ("" literal miscompiles -- see ci_ingest_file)
669 if argc >= 4 { urlprefix = argv[3] as *u8 }
670 let counts: *i64 = sys_mmap(CI_KEYBUF) as *i64
671 let rc: i64 = ci_run(dir, domain, urlprefix, counts)
672 if rc < 0 { return 2 }
673 ci_puts("corpus-ingest: dir=" as *u8); ci_puts(dir)
674 ci_puts(" -> shard dp-" as *u8); ci_puts(domain); ci_puts("-pub- ingested=" as *u8); ci_num(counts[0])
675 ci_puts(" skipped_present=" as *u8); ci_num(counts[1])
676 ci_puts(" skipped_other=" as *u8); ci_num(counts[2])
677 ci_puts(" segments=" as *u8); ci_num(counts[3])
678 ci_puts("\n" as *u8)
679 if counts[0] + counts[1] == 0 { ci_puts("corpus-ingest: NOTHING ingested (empty dir or all skipped)\n" as *u8); return 3 }
680 return 0
681}