code wiki / _hdl_build / nx_outlink_harvest.nx
nx_outlink_harvest.nx source
↩ module page · 374 lines · 19339 B
1// nx_outlink_harvest.nx — shared outlink -> link-graph-edge harvester (P1 PageRank feed). LIB (no main).
2// Extracted (by copy) from nx_web_crawl_step.wc_harvest so BOTH the live crawler AND nx_cc_ingest emit the same
3// row shape: out:<ci_hash(page_url)> = packed i64 ci_hash(target_url). Node identity = ci_hash(url) ==
4// dss_urlcid(url) at serve time, so an outlink resolves to a graph node with zero extra index; serve-time
5// authority = content_cid -> url:<cid> -> ci_hash(url) -> pr:<cid>. ABSOLUTE cross-site links (the authority-
6// bearing ones) produce byte-identical cids to the crawler.
7//
8// Three intentional improvements over the crawler's inline copy (all to backport when the crawler adopts this
9// lib): (1) root-relative "/path" resolves HOST-based (scheme://host + path) not base+path — the crawler's
10// concatenation makes ".../Trust_law" + "/about" a phantom node; (2) edge capture is DECOUPLED from the frontier
11// cap (the crawler stops recording edges once discbox hits WC_MAXDISC — a graph must not depend on frontier
12// state); (3) one REUSED url buffer instead of an mmap per outlink (bounds address-space growth for CC-scale
13// ingests of thousands of pages). license_tier: ORIGINAL
14import "nx_corpus_ingest.nx"
15import "nx_store_seed_lib.nx" // sts_load -> knowledge/store/urlpolicy- (R10: the denylist is DATA, not code)
16import "nx_url_canon.nx" // nx_url_canon -- ONE definition, shared with the HTML extractors // ci_hash
17const OLH_MAGIC_8192: i64 = 8192
18
19const OLH_MAXURL: i64 = 1500 // == the crawler's WC_MAXURL (measured junk/tracking-monster threshold) so
20 // edge nodes and frontier urls follow ONE canonical length policy
21const OLH_MAXEDGE: i64 = 1024
22
23func olh_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
24
25// literal substring present in u[0..ul)? (verbatim wc_hassub)
26func olh_hassub(u: *u8, ul: i64, lit: *u8) -> i64 {
27 let ll: i64 = olh_len(lit)
28 if ll == 0 { return 0 }
29 var i: i64 = 0
30 while i + ll <= ul {
31 var m: i64 = 1
32 var k: i64 = 0
33 while k < ll { if u[i + k] != lit[k] { m = 0; k = ll } else { k = k + 1 } }
34 if m == 1 { return 1 }
35 i = i + 1
36 }
37 return 0
38}
39// u ends with lit? (verbatim wc_endswith)
40func olh_endswith(u: *u8, ul: i64, lit: *u8) -> i64 {
41 let ll: i64 = olh_len(lit)
42 if ul < ll { return 0 }
43 var k: i64 = 0
44 while k < ll { if u[ul - ll + k] != lit[k] { return 0 } k = k + 1 }
45 return 1
46}
47// CONTENT-URL FILTER: 1 = a real page worth an authority edge. (verbatim wc_urlok; WC_MAXURL -> OLH_MAXURL)
48// R8 (2026-08-05): the body moved into olh_urlok_q with an allowq flag; olh_urlok keeps the exact
49// old contract (allowq=0) so CC ingest + the link graph are byte-identical. WHY: the blanket '?'
50// reject is correct for UNCURATED hosts (tracking junk, pagination traps) and WRONG for a curated
51// seed host whose CONTENT rides query strings -- measured: graphis.ne.jp galleries are
52// model.php?ID=..., so the whole entity vertical was structurally unreachable. Same law as the
53// R4b depth cap: re-derive every limit when the trust of its subject changes. ONE body, ONE flag
54// -- never a twin (twin-drift is what the 08-04 shared-policy fix killed).
55// ---- R11 PATH-CLASS POLICY AS DATA (2026-08-14) -------------------------------------------------
56// THE PATH AXIS OF R8, and the same defect on the same class of site. R8 moved the blanket '?' reject
57// onto a data-driven judge after measuring that graphis.ne.jp's whole gravure vertical was structurally
58// unreachable because its CONTENT rides query strings. The PATH axis kept five hardcoded literals and
59// is unreachable in exactly the same way: a tag-ORGANISED host's navigation IS its /tag/ and /category/
60// pages, so refusing the class refuses the host. MEASURED 2026-08-14: beeg.com and asianude4u.net are
61// absent from the index while their SEO doorway clones (bestbeeg.top, raj.red) are present -- doorways
62// publish flat article-shaped urls, real tag-organised sites do not. The rule selected AGAINST the
63// sites that serve content and FOR the ones that do not.
64// R10 already declared "the denylist is DATA, not code" and shipped knowledge/store/urlpolicy- to hold
65// it; the path rules never adopted it. This is an ADOPTION GAP, not a missing primitive.
66// * A DENYLIST IN CODE IS A POLICY NOBODY CAN CHANGE WITHOUT A COMPILER.
67// SELF-DEFAULTING, NOT LOADER-DEPENDENT (the law wc_query_ok learned the hard way): an absent or
68// path-free plane reproduces the EXACT 2026-08-04 list, so a missing data file can neither silently
69// open the gate nor silently close it. To make the plane authoritative -- INCLUDING "deny nothing" --
70// set __pathpolicy, which is the only way to express an intentionally empty policy.
71// Plane rows (ONE plane, TWO namespaces, no shared bucket -- the query axis owns the bare names):
72// path:<substr> deny -- substring anywhere in the url
73// pathend:<suffix> deny -- url ends with this
74// __pathpolicy plane -- the plane is authoritative even if it lists no path rules
75const OLH_PATHPOL_PLANE: *u8 = "knowledge/store/urlpolicy-"
76const OLH_PSLOT: i64 = 64 // bytes per rule: [kind byte][NUL-terminated pattern]
77const OLH_PMAXRULE: i64 = 128
78const OLH_PPLANECAP: i64 = 65536 // ONE-TIME read buffer (per process, not per call); bounded and named
79const OLH_PKIND_SUB: i64 = 1
80const OLH_PKIND_END: i64 = 2
81static olh_prule_g: *u8
82static olh_nprule_g: i64
83static olh_ploaded_g: i64
84static olh_ppath_g: *u8 // NULL = production plane; set = isolated fixture prefix (TEST ISOLATION)
85func olh_pathpol_set(p: *u8) -> i64 { olh_ppath_g = p; olh_ploaded_g = 0; return 0 }
86
87func olh_prule_add(kind: i64, pat: *u8, pl: i64) -> i64 {
88 if olh_nprule_g >= OLH_PMAXRULE { return 1 }
89 if pl >= OLH_PSLOT - 2 { return 1 }
90 let dst: *u8 = (olh_prule_g as i64 + olh_nprule_g * OLH_PSLOT) as *u8
91 dst[0] = kind as u8
92 var k: i64 = 0
93 while k < pl { dst[1 + k] = pat[k]; k = k + 1 }
94 dst[1 + pl] = 0 as u8
95 olh_nprule_g = olh_nprule_g + 1
96 return 0
97}
98func olh_prule_addc(kind: i64, pat: *u8) -> i64 { return olh_prule_add(kind, pat, olh_len(pat)) }
99// THE 2026-08-04 LIST, VERBATIM. This is the behavioural contract when the plane says nothing.
100func olh_prule_defaults() -> i64 {
101 olh_prule_addc(OLH_PKIND_SUB, "/tag/" as *u8)
102 olh_prule_addc(OLH_PKIND_SUB, "/tags/" as *u8)
103 olh_prule_addc(OLH_PKIND_SUB, "/category/" as *u8)
104 olh_prule_addc(OLH_PKIND_END, "/tag" as *u8)
105 olh_prule_addc(OLH_PKIND_END, "/category" as *u8)
106 return olh_nprule_g
107}
108func olh_pname_is(b: *u8, s: i64, nl: i64, lit: *u8) -> i64 {
109 let ll: i64 = olh_len(lit)
110 if ll != nl { return 0 }
111 var k: i64 = 0
112 while k < ll { if b[s + k] != lit[k] { return 0 } k = k + 1 }
113 return 1
114}
115// returns the prefix LENGTH when b[s..s+nl) starts with lit and is strictly longer, else 0
116func olh_pname_pref(b: *u8, s: i64, nl: i64, lit: *u8) -> i64 {
117 let ll: i64 = olh_len(lit)
118 if nl <= ll { return 0 }
119 var k: i64 = 0
120 while k < ll { if b[s + k] != lit[k] { return 0 } k = k + 1 }
121 return ll
122}
123func olh_load_pathpolicy() -> i64 {
124 if olh_ploaded_g == 1 { return olh_nprule_g }
125 olh_ploaded_g = 1
126 olh_prule_g = sys_mmap(OLH_PSLOT * OLH_PMAXRULE)
127 olh_nprule_g = 0
128 let b: *u8 = sys_mmap(OLH_PPLANECAP)
129 var pp: *u8 = olh_ppath_g
130 if (pp as i64) == 0 { pp = OLH_PATHPOL_PLANE }
131 let n: i64 = sts_load(pp, b, OLH_PPLANECAP - 16)
132 var authoritative: i64 = 0
133 if n > 0 {
134 var i: i64 = 0
135 while i < n {
136 let ls: i64 = i
137 var le: i64 = ls
138 var s: i64 = 1
139 while s == 1 { if le >= n { s = 0 } else { if b[le] == (10 as u8) { s = 0 } else { le = le + 1 } } }
140 i = le + 1
141 if le - ls > 2 { if b[ls] != (35 as u8) {
142 var t: i64 = 0 - 1
143 var j: i64 = ls
144 while j < le { if b[j] == (9 as u8) { t = j; j = le } else { j = j + 1 } }
145 if t > ls {
146 let nl: i64 = t - ls
147 if olh_pname_is(b, ls, nl, "__pathpolicy" as *u8) == 1 { authoritative = 1 }
148 else {
149 let po: i64 = olh_pname_pref(b, ls, nl, "path:" as *u8)
150 if po > 0 {
151 authoritative = 1
152 olh_prule_add(OLH_PKIND_SUB, (b as i64 + ls + po) as *u8, nl - po)
153 }
154 else {
155 let qo: i64 = olh_pname_pref(b, ls, nl, "pathend:" as *u8)
156 if qo > 0 {
157 authoritative = 1
158 olh_prule_add(OLH_PKIND_END, (b as i64 + ls + qo) as *u8, nl - qo)
159 }
160 }
161 }
162 }
163 } }
164 }
165 }
166 if authoritative == 0 { olh_prule_defaults() }
167 return olh_nprule_g
168}
169// 1 = this url is on the path denylist. The plane is loaded ONCE per process, so this is hot-loop safe:
170// the mmap and the read happen on first call only, never per outlink.
171func olh_path_denied(u: *u8, ul: i64) -> i64 {
172 olh_load_pathpolicy()
173 var i: i64 = 0
174 while i < olh_nprule_g {
175 let sp: *u8 = (olh_prule_g as i64 + i * OLH_PSLOT) as *u8
176 let kind: i64 = sp[0] as i64
177 let pat: *u8 = (sp as i64 + 1) as *u8
178 if kind == OLH_PKIND_SUB { if olh_hassub(u, ul, pat) == 1 { return 1 } }
179 if kind == OLH_PKIND_END { if olh_endswith(u, ul, pat) == 1 { return 1 } }
180 i = i + 1
181 }
182 return 0
183}
184func olh_urlok_q(u: *u8, ul: i64, allowq: i64) -> i64 {
185 if ul < 12 { return 0 }
186 if ul > OLH_MAXURL { return 0 }
187 var scheme: i64 = 0
188 if olh_hassub(u, 9, "https://" as *u8) == 1 { scheme = 1 }
189 if olh_hassub(u, 8, "http://" as *u8) == 1 { scheme = 1 }
190 if scheme == 0 { return 0 }
191 var i: i64 = 0
192 while i < ul {
193 if u[i] == (35 as u8) { return 0 } // '#' fragment
194 if allowq == 0 { if u[i] == (63 as u8) { return 0 } } // '?' query -- curated hosts may override (R8)
195 i = i + 1
196 }
197 if olh_hassub(u, ul, "mailto:" as *u8) == 1 { return 0 }
198 if olh_hassub(u, ul, "javascript:" as *u8) == 1 { return 0 }
199 if olh_hassub(u, ul, ".wikipedia.org/" as *u8) == 1 {
200 if olh_hassub(u, ul, "://en.wikipedia.org/" as *u8) == 0 { return 0 }
201 }
202 if olh_hassub(u, ul, ".wikimedia.org/" as *u8) == 1 { return 0 }
203 if olh_hassub(u, ul, ".wiktionary.org/" as *u8) == 1 { return 0 }
204 if olh_hassub(u, ul, ".wikidata.org/" as *u8) == 1 { return 0 }
205 if olh_hassub(u, ul, "/Special:" as *u8) == 1 { return 0 }
206 if olh_hassub(u, ul, "/File:" as *u8) == 1 { return 0 }
207 if olh_hassub(u, ul, "/Talk:" as *u8) == 1 { return 0 }
208 if olh_hassub(u, ul, "/Help:" as *u8) == 1 { return 0 }
209 if olh_hassub(u, ul, "/Template:" as *u8) == 1 { return 0 }
210 if olh_hassub(u, ul, "/Category:" as *u8) == 1 { return 0 }
211 if olh_hassub(u, ul, "/Portal:" as *u8) == 1 { return 0 }
212 if olh_hassub(u, ul, "/Wikipedia:" as *u8) == 1 { return 0 }
213 if olh_endswith(u, ul, ".png" as *u8) == 1 { return 0 }
214 if olh_endswith(u, ul, ".jpg" as *u8) == 1 { return 0 }
215 if olh_endswith(u, ul, ".jpeg" as *u8) == 1 { return 0 }
216 if olh_endswith(u, ul, ".gif" as *u8) == 1 { return 0 }
217 if olh_endswith(u, ul, ".svg" as *u8) == 1 { return 0 }
218 if olh_endswith(u, ul, ".css" as *u8) == 1 { return 0 }
219 if olh_endswith(u, ul, ".js" as *u8) == 1 { return 0 }
220 if olh_endswith(u, ul, ".ico" as *u8) == 1 { return 0 }
221 if olh_endswith(u, ul, ".pdf" as *u8) == 1 { return 0 }
222 if olh_endswith(u, ul, ".zip" as *u8) == 1 { return 0 }
223 if olh_endswith(u, ul, ".xml" as *u8) == 1 { return 0 }
224 if olh_hassub(u, ul, "/wp-json/" as *u8) == 1 { return 0 }
225 if olh_endswith(u, ul, ".json" as *u8) == 1 { return 0 }
226 if olh_endswith(u, ul, ".rss" as *u8) == 1 { return 0 }
227 if olh_endswith(u, ul, "/feed" as *u8) == 1 { return 0 }
228 if olh_endswith(u, ul, "/feed/" as *u8) == 1 { return 0 }
229 // ANTI-SLOP URL CLASSES (2026-08-04, debt 1785895889): tag/category LISTING pages are
230 // pagination-trap link farms, not content -- tag-farm pages (healthquerys.com/tag/*)
231 // reached page 1 of natural-language SERPs while the ruler read 367 vs its 721 ratchet.
232 // This is the ONE policy file, so crawler frontier, CC ingest and the link graph all
233 // refuse them together. Article pages link each other directly; losing tag-page link
234 // harvest costs little and stops the trap class at admission.
235 // R11 (2026-08-14): the five literals that were here are now DATA (olh_path_denied above).
236 // SAME rules by default and the SAME one-policy-file property -- crawler frontier, CC ingest,
237 // WARC ingest, page ingest and the link graph all reach this one predicate -- but narrowing or
238 // lifting the class is now a PLANE EDIT and a re-run, not a recompile and a promote across five
239 // consumers. That difference is the whole point: the rule was measured wrong on 2026-08-14 and
240 // nobody could change it without a build.
241 if olh_path_denied(u, ul) == 1 { return 0 }
242 return 1
243}
244func olh_urlok(u: *u8, ul: i64) -> i64 { return olh_urlok_q(u, ul, 0) }
245// out:<decimal cid> key. (verbatim wc_outkey)
246func olh_outkey(cid: i64, out: *u8) -> i64 {
247 out[0] = 111 as u8 // o
248 out[1] = 117 as u8 // u
249 out[2] = 116 as u8 // t
250 out[3] = 58 as u8 // :
251 var m: i64 = cid
252 let t: *u8 = sys_mmap(28)
253 var k: i64 = 0
254 if m == 0 { t[0] = 48 as u8; k = 1 }
255 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
256 var i: i64 = 0
257 while i < k { out[4 + i] = t[k - 1 - i]; i = i + 1 }
258 out[4 + k] = 0 as u8
259 return 4 + k
260}
261// resolve a root-relative href ("/path", NOT protocol-relative "//") against a NUL-terminated base url:
262// scheme://host + path (NOT base+path concat — that makes ".../Trust_law"+"/about" a phantom node). hp[0..clen)
263// = the raw href bytes. Writes the resolved NUL-terminated url into scratch (>= host + clen + 1 bytes; callers
264// pass an 8192 arena). Returns resolved length, or 0 if base has no "://" or the result would overflow.
265func olh_resolve_root(base: *u8, hp: *u8, clen: i64, scratch: *u8) -> i64 {
266 var sp: i64 = 0 - 1
267 var bi: i64 = 0
268 var bf: i64 = 0
269 while bf == 0 {
270 if base[bi] == (0 as u8) { bf = 1 }
271 else {
272 if base[bi] == (58 as u8) { if base[bi+1] == (47 as u8) { if base[bi+2] == (47 as u8) { sp = bi; bf = 1 } } }
273 if bf == 0 { bi = bi + 1 }
274 }
275 }
276 if sp < 0 { return 0 }
277 var he: i64 = sp + 3
278 var hf: i64 = 0
279 while hf == 0 { if base[he] == (0 as u8) { hf = 1 } else { if base[he] == (47 as u8) { hf = 1 } else { he = he + 1 } } }
280 if he + clen + 1 >= OLH_MAGIC_8192 { return 0 }
281 var b: i64 = 0
282 while b < he { scratch[b] = base[b]; b = b + 1 }
283 var cc: i64 = 0
284 while cc < clen { scratch[he + cc] = hp[cc]; cc = cc + 1 }
285 scratch[he + clen] = 0 as u8
286 return he + clen
287}
288// scan hh[0..hlen) for <a href="..."> outlinks -> edgebuf[0..nedge) = ci_hash(resolved_url); returns nedge.
289// base = NUL-terminated page url (for root-relative resolution). Absolute http(s) kept as-is; "/path" ->
290// scheme://host + path; protocol-relative "//..." and other relative forms skipped (no authoritative base).
291// PER-CALL SCRATCH -> LAZY STATICS (2026-08-19 lane F, measured by nx_crawl_callee_probe: 12 kB/call
292// here after the nx_feed_extract fix -- the last named per-page allocator, ~2.5 pages/call page-rounded).
293// Rewritten per call; crawler fetch children fork their own copies. One-time cost ~10 KiB per process.
294static olh_scr_g: *u8
295static olh_ubuf_g: *u8
296func olh_scratch() -> i64 {
297 if (olh_scr_g as i64) == 0 {
298 olh_scr_g = sys_mmap(OLH_MAGIC_8192)
299 olh_ubuf_g = sys_mmap(OLH_MAXURL + 2)
300 }
301 return 0
302}
303func olh_scan(hh: *u8, hlen: i64, base: *u8, blen: i64, edgebuf: *i64, maxedge: i64) -> i64 {
304 var nedge: i64 = 0
305 olh_scratch()
306 let scratch: *u8 = olh_scr_g
307 let ubuf: *u8 = olh_ubuf_g // reused per outlink (no per-link mmap)
308 var i: i64 = 0
309 while i < hlen {
310 var step: i64 = 1
311 var ish: i64 = 0
312 if i + 5 < hlen {
313 var c0: i64 = hh[i] as i64
314 if c0 >= 0x41 { if c0 <= 0x5a { c0 = c0 + 0x20 } }
315 if c0 == 0x68 {
316 var c1: i64 = hh[i+1] as i64
317 if c1 >= 0x41 { if c1 <= 0x5a { c1 = c1 + 0x20 } }
318 var c2: i64 = hh[i+2] as i64
319 if c2 >= 0x41 { if c2 <= 0x5a { c2 = c2 + 0x20 } }
320 var c3: i64 = hh[i+3] as i64
321 if c3 >= 0x41 { if c3 <= 0x5a { c3 = c3 + 0x20 } }
322 if c1 == 0x72 { if c2 == 0x65 { if c3 == 0x66 { if (hh[i+4] as i64) == 0x3d { ish = 1 } } } }
323 }
324 }
325 if ish == 1 {
326 let q: i64 = hh[i+5] as i64
327 var quoted: i64 = 0
328 if q == 0x22 { quoted = 1 }
329 if q == 0x27 { quoted = 1 }
330 if quoted == 1 {
331 let cs: i64 = i + 6
332 var e: i64 = cs
333 var run: i64 = 1
334 while run == 1 { run = 0; if e < hlen { if (hh[e] as i64) != q { e = e + 1; run = 1 } } }
335 let clen: i64 = e - cs
336 if clen > 0 { if clen <= OLH_MAXURL {
337 let hp: *u8 = ((hh as i64) + cs) as *u8
338 var cand: *u8 = 0 as *u8
339 var candlen: i64 = 0
340 var abs: i64 = 0
341 if clen >= 8 { if olh_hassub(hp, 8, "https://" as *u8) == 1 { abs = 1 } }
342 if abs == 0 { if clen >= 7 { if olh_hassub(hp, 7, "http://" as *u8) == 1 { abs = 1 } } }
343 if abs == 1 {
344 cand = hp
345 candlen = clen
346 } else {
347 if (hp[0] as i64) == 0x2f {
348 var protorel: i64 = 0
349 if clen >= 2 { if (hp[1] as i64) == 0x2f { protorel = 1 } }
350 if protorel == 0 {
351 let rl: i64 = olh_resolve_root(base, hp, clen, scratch)
352 if rl > 0 {
353 cand = scratch
354 candlen = rl
355 }
356 }
357 }
358 }
359 if (cand as i64) != 0 { if candlen <= OLH_MAXURL {
360 // CANONICALISE BEFORE JUDGING OR HASHING: node identity must be the identity of
361 // the url we would actually FETCH, or the link graph and the frontier disagree.
362 let cn: i64 = nx_url_canon(cand, candlen, ubuf, OLH_MAXURL)
363 if cn > 0 { if olh_urlok(ubuf, cn) == 1 {
364 if nedge < maxedge { edgebuf[nedge] = ci_hash(ubuf, cn); nedge = nedge + 1 }
365 } }
366 } }
367 } }
368 step = (e - i) + 1
369 }
370 }
371 i = i + step
372 }
373 return nedge
374}