code wiki / _hdl_build / nx_fp_backfill.nx
nx_fp_backfill.nx source
↩ module page · 157 lines · 8601 B
1// nx_fp_backfill.nx -- BACKFILL THE fp: FINGERPRINT ROW OVER ALREADY-INGESTED DOCS.
2//
3// WHY: nx_web_crawl_step now persists fp:<cid> (the simhash it always computed and used to discard),
4// so intra-host near-duplication -- the real doorway signature -- is a property of the index. But that
5// is FORWARD-ONLY: every doc ingested before 2026-08-16 has no fp: row, so a duplication census would
6// silently measure only the newest slice and report it as the corpus. This closes that gap.
7//
8// NO NETWORK. The text is already stored; this reads doc:<cid>, hashes it, writes fp:<cid>. A source
9// that died years ago costs nothing here -- which is the whole argument for an entity corpus over a
10// page index: capture once, and every later question is local.
11//
12// MEMORY SHAPE, STATED because the sibling's is different: nx_web_shard_compact was OOM-killed twice
13// (bytes-in=1883885124, 2026-07-25) because it MATERIALISES a merged segment in RAM. This does not --
14// it holds one doc at a time and emits a ~20-byte row, so its bound is on RUNTIME, not memory.
15//
16// BOUNDED AND RESUMABLE, never a silent runaway:
17// argv[1] max REQUIRED and must be > 0 -- refuse rather than default to unbounded
18// argv[2] skip resume offset into the walk order (default 0)
19// argv[3] commit literal "commit" to write; DEFAULT IS A DRY RUN that changes nothing
20// PROGRESS IS PROVABLE: every run prints scanned/had_fp/written/capped. The sibling's range-fold note
21// is the warning -- a bounded sweep with the wrong selection rule runs forever WITHOUT ADVANCING, so
22// "did this converge" must be a number, not a feeling. capped=1 says the run stopped at max, i.e. the
23// corpus is NOT yet fully covered and the next run needs skip=<scanned+skip>.
24// license_tier: ORIGINAL
25import "nx_corpus_ingest.nx" // dss_prefix / dss_mkkey / ci_mkfpkey / ss_* primitives
26import "nx_simhash.nx" // nx_simhash_fingerprint -- the SAME hasher the crawler uses, composed
27import "nx_itoa_lib.nx" // ccz_cat_num -- THE canonical integer emitter
28
29const FB_KEYCAP: i64 = 64
30const FB_VALCAP: i64 = 32
31
32func fb_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
33func fb_num(v: i64) -> i64 {
34 if v == 0 { fb_puts("0" as *u8); return 0 }
35 var m: i64 = v
36 if m < 0 { fb_puts("-" as *u8); m = 0 - m }
37 let d: *u8 = sys_mmap(24); var k: i64 = 0
38 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
39 let o: *u8 = sys_mmap(24); var i: i64 = 0
40 while i < k { o[i] = d[k - 1 - i]; i = i + 1 }
41 sys_write(1, o, k)
42 return 0
43}
44func fb_atoi(s: *u8) -> i64 {
45 var v: i64 = 0
46 var i: i64 = 0
47 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 }
48 return v
49}
50
51func main(argc: i64, argv: *i64) -> i64 {
52 if argc < 2 { fb_puts("usage: nx_fp_backfill <max> [skip] [commit] (max REQUIRED > 0; default is a DRY RUN)\n" as *u8); return 2 }
53 let maxn: i64 = fb_atoi(argv[1] as *u8)
54 if maxn <= 0 { fb_puts("REFUSED: max must be > 0 -- an unbounded sweep over this shard is how the sibling got OOM-killed\n" as *u8); return 2 }
55 var skip: i64 = 0
56 if argc >= 3 { skip = fb_atoi(argv[2] as *u8) }
57 var docommit: i64 = 0
58 if argc >= 4 { let a3: *u8 = argv[3] as *u8; if a3[0] == (99 as u8) { docommit = 1 } }
59
60 let prefix: *u8 = sys_mmap(512)
61 dss_prefix("web" as *u8, prefix)
62 let h: *i64 = ss_open(prefix)
63 if (h as i64) == 0 { fb_puts("BACKFILL FAIL: web shard absent\n" as *u8); return 3 }
64 let w: *i64 = ss_begin()
65 let segsbox: *i64 = sys_mmap(16) as *i64
66 segsbox[0] = 0
67 let segidbox: *i64 = sys_mmap(16) as *i64
68 segidbox[0] = ss_next_segid(prefix)
69
70 let kbuf: *u8 = sys_mmap(FB_KEYCAP)
71 let dkey: *u8 = sys_mmap(FB_KEYCAP)
72 let fkey: *u8 = sys_mmap(FB_KEYCAP)
73 let fval: *u8 = sys_mmap(FB_VALCAP)
74 let pp: *i64 = sys_mmap(16) as *i64
75 let pl: *i64 = sys_mmap(16) as *i64
76 let dp: *i64 = sys_mmap(16) as *i64
77 let dl: *i64 = sys_mmap(16) as *i64
78
79 var seen: i64 = 0 // url: rows encountered (walk position, for the resume offset)
80 var scanned: i64 = 0 // rows actually examined this run
81 var had: i64 = 0 // already carried an fp: row
82 var wrote: i64 = 0 // fp: rows emitted
83 var nodoc: i64 = 0 // url: row with no readable doc: row -- reported, never silently skipped
84 var capped: i64 = 0
85
86 let ns: i64 = h[0]
87 var s: i64 = 0
88 while s < ns {
89 let kb: *u8 = h[1 + 8 * s] as *u8
90 if h[2 + 8 * s] >= 8 {
91 let m9: i64 = ss_r32(kb, 4)
92 var e9: i64 = 0
93 while e9 < m9 {
94 let eo: i64 = 8 + 4 * m9 + ss_r32(kb, 8 + 4 * e9)
95 if (kb[eo] as i64) == 1 {
96 let kl9: i64 = ss_r32(kb, eo + 1)
97 if kl9 >= 5 { if kl9 < 60 {
98 if kb[eo + 5] == (117 as u8) { if kb[eo + 6] == (114 as u8) { if kb[eo + 7] == (108 as u8) { if kb[eo + 8] == (58 as u8) {
99 seen = seen + 1
100 if seen > skip { if capped == 0 {
101 if scanned >= maxn { capped = 1 } else {
102 scanned = scanned + 1
103 var c: i64 = 0
104 while c < kl9 { kbuf[c] = kb[eo + 5 + c]; c = c + 1 }
105 kbuf[kl9] = 0 as u8
106 var cid: i64 = 0
107 var ki: i64 = 4
108 while ki < kl9 { let kc: i64 = kbuf[ki] as i64; if kc >= 48 { if kc <= 57 { cid = cid * 10 + (kc - 48) } } ki = ki + 1 }
109 ci_mkfpkey(cid, fkey)
110 var present: i64 = 0
111 if ss_hget(h, fkey, pp, pl) == 1 { if pl[0] > 0 { present = 1 } }
112 if present == 1 { had = had + 1 } else {
113 dss_mkkey(cid, dkey)
114 var got: i64 = 0
115 if ss_hget(h, dkey, dp, dl) == 1 { if dl[0] > 0 { got = 1 } }
116 if got == 0 { nodoc = nodoc + 1 } else {
117 let fp: i64 = nx_simhash_fingerprint(dp[0] as *u8, dl[0])
118 let fn2: i64 = ccz_cat_num(fval, 0, fp)
119 wrote = wrote + 1
120 if docommit == 1 {
121 if ss_add(w, 1, fkey, fval, fn2) < 0 {
122 if ss_commit(prefix, w, segidbox[0]) == 0 { segsbox[0] = segsbox[0] + 1 }
123 segidbox[0] = segidbox[0] + 1
124 w[1] = 0
125 ss_add(w, 1, fkey, fval, fn2)
126 }
127 }
128 }
129 }
130 }
131 } }
132 } } } }
133 } }
134 }
135 e9 = e9 + 1
136 }
137 }
138 s = s + 1
139 }
140 if docommit == 1 { if ss_commit(prefix, w, segidbox[0]) == 0 { segsbox[0] = segsbox[0] + 1 } }
141
142 fb_puts("{\"tool\":\"nx_fp_backfill\",\"url_rows_walked\":" as *u8); fb_num(seen)
143 fb_puts(",\"skip\":" as *u8); fb_num(skip)
144 fb_puts(",\"scanned\":" as *u8); fb_num(scanned)
145 fb_puts(",\"had_fp\":" as *u8); fb_num(had)
146 fb_puts(",\"wrote_fp\":" as *u8); fb_num(wrote)
147 fb_puts(",\"no_doc_row\":" as *u8); fb_num(nodoc)
148 fb_puts(",\"segments_committed\":" as *u8); fb_num(segsbox[0])
149 fb_puts(",\"committed\":" as *u8); fb_num(docommit)
150 fb_puts(",\"capped\":" as *u8); fb_num(capped)
151 fb_puts("}\n" as *u8)
152 // The partition MUST reconcile: every scanned row is exactly one of had/wrote/no_doc.
153 if had + wrote + nodoc == scanned { fb_puts("PARTITION-RECONCILES\n" as *u8) } else { fb_puts("PARTITION-LEAK (had+wrote+no_doc != scanned) -- do not trust these counts\n" as *u8) }
154 if capped == 1 { fb_puts("CAPPED at max -- corpus NOT fully covered; next run: skip=" as *u8); fb_num(skip + scanned); fb_puts("\n" as *u8) }
155 fb_puts("FP-BACKFILL-OK\n" as *u8)
156 return 0
157}