code wiki / _hdl_build / nx_outlink_harvest.nx

nx_outlink_harvest.nx source

↩ module page · 228 lines · 11769 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" // ci_hash 15const OLH_MAGIC_8192: i64 = 8192 16 17const OLH_MAXURL: i64 = 1500 // == the crawler's WC_MAXURL (measured junk/tracking-monster threshold) so 18 // edge nodes and frontier urls follow ONE canonical length policy 19const OLH_MAXEDGE: i64 = 1024 20 21func olh_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 22 23// literal substring present in u[0..ul)? (verbatim wc_hassub) 24func olh_hassub(u: *u8, ul: i64, lit: *u8) -> i64 { 25 let ll: i64 = olh_len(lit) 26 if ll == 0 { return 0 } 27 var i: i64 = 0 28 while i + ll <= ul { 29 var m: i64 = 1 30 var k: i64 = 0 31 while k < ll { if u[i + k] != lit[k] { m = 0; k = ll } else { k = k + 1 } } 32 if m == 1 { return 1 } 33 i = i + 1 34 } 35 return 0 36} 37// u ends with lit? (verbatim wc_endswith) 38func olh_endswith(u: *u8, ul: i64, lit: *u8) -> i64 { 39 let ll: i64 = olh_len(lit) 40 if ul < ll { return 0 } 41 var k: i64 = 0 42 while k < ll { if u[ul - ll + k] != lit[k] { return 0 } k = k + 1 } 43 return 1 44} 45// CONTENT-URL FILTER: 1 = a real page worth an authority edge. (verbatim wc_urlok; WC_MAXURL -> OLH_MAXURL) 46// R8 (2026-08-05): the body moved into olh_urlok_q with an allowq flag; olh_urlok keeps the exact 47// old contract (allowq=0) so CC ingest + the link graph are byte-identical. WHY: the blanket '?' 48// reject is correct for UNCURATED hosts (tracking junk, pagination traps) and WRONG for a curated 49// seed host whose CONTENT rides query strings -- measured: graphis.ne.jp galleries are 50// model.php?ID=..., so the whole entity vertical was structurally unreachable. Same law as the 51// R4b depth cap: re-derive every limit when the trust of its subject changes. ONE body, ONE flag 52// -- never a twin (twin-drift is what the 08-04 shared-policy fix killed). 53func olh_urlok_q(u: *u8, ul: i64, allowq: i64) -> i64 { 54 if ul < 12 { return 0 } 55 if ul > OLH_MAXURL { return 0 } 56 var scheme: i64 = 0 57 if olh_hassub(u, 9, "https://" as *u8) == 1 { scheme = 1 } 58 if olh_hassub(u, 8, "http://" as *u8) == 1 { scheme = 1 } 59 if scheme == 0 { return 0 } 60 var i: i64 = 0 61 while i < ul { 62 if u[i] == (35 as u8) { return 0 } // '#' fragment 63 if allowq == 0 { if u[i] == (63 as u8) { return 0 } } // '?' query -- curated hosts may override (R8) 64 i = i + 1 65 } 66 if olh_hassub(u, ul, "mailto:" as *u8) == 1 { return 0 } 67 if olh_hassub(u, ul, "javascript:" as *u8) == 1 { return 0 } 68 if olh_hassub(u, ul, ".wikipedia.org/" as *u8) == 1 { 69 if olh_hassub(u, ul, "://en.wikipedia.org/" as *u8) == 0 { return 0 } 70 } 71 if olh_hassub(u, ul, ".wikimedia.org/" as *u8) == 1 { return 0 } 72 if olh_hassub(u, ul, ".wiktionary.org/" as *u8) == 1 { return 0 } 73 if olh_hassub(u, ul, ".wikidata.org/" as *u8) == 1 { return 0 } 74 if olh_hassub(u, ul, "/Special:" as *u8) == 1 { return 0 } 75 if olh_hassub(u, ul, "/File:" as *u8) == 1 { return 0 } 76 if olh_hassub(u, ul, "/Talk:" as *u8) == 1 { return 0 } 77 if olh_hassub(u, ul, "/Help:" as *u8) == 1 { return 0 } 78 if olh_hassub(u, ul, "/Template:" as *u8) == 1 { return 0 } 79 if olh_hassub(u, ul, "/Category:" as *u8) == 1 { return 0 } 80 if olh_hassub(u, ul, "/Portal:" as *u8) == 1 { return 0 } 81 if olh_hassub(u, ul, "/Wikipedia:" as *u8) == 1 { return 0 } 82 if olh_endswith(u, ul, ".png" as *u8) == 1 { return 0 } 83 if olh_endswith(u, ul, ".jpg" as *u8) == 1 { return 0 } 84 if olh_endswith(u, ul, ".jpeg" as *u8) == 1 { return 0 } 85 if olh_endswith(u, ul, ".gif" as *u8) == 1 { return 0 } 86 if olh_endswith(u, ul, ".svg" as *u8) == 1 { return 0 } 87 if olh_endswith(u, ul, ".css" as *u8) == 1 { return 0 } 88 if olh_endswith(u, ul, ".js" as *u8) == 1 { return 0 } 89 if olh_endswith(u, ul, ".ico" as *u8) == 1 { return 0 } 90 if olh_endswith(u, ul, ".pdf" as *u8) == 1 { return 0 } 91 if olh_endswith(u, ul, ".zip" as *u8) == 1 { return 0 } 92 if olh_endswith(u, ul, ".xml" as *u8) == 1 { return 0 } 93 if olh_hassub(u, ul, "/wp-json/" as *u8) == 1 { return 0 } 94 if olh_endswith(u, ul, ".json" as *u8) == 1 { return 0 } 95 if olh_endswith(u, ul, ".rss" as *u8) == 1 { return 0 } 96 if olh_endswith(u, ul, "/feed" as *u8) == 1 { return 0 } 97 if olh_endswith(u, ul, "/feed/" as *u8) == 1 { return 0 } 98 // ANTI-SLOP URL CLASSES (2026-08-04, debt 1785895889): tag/category LISTING pages are 99 // pagination-trap link farms, not content -- tag-farm pages (healthquerys.com/tag/*) 100 // reached page 1 of natural-language SERPs while the ruler read 367 vs its 721 ratchet. 101 // This is the ONE policy file, so crawler frontier, CC ingest and the link graph all 102 // refuse them together. Article pages link each other directly; losing tag-page link 103 // harvest costs little and stops the trap class at admission. 104 if olh_hassub(u, ul, "/tag/" as *u8) == 1 { return 0 } 105 if olh_hassub(u, ul, "/tags/" as *u8) == 1 { return 0 } 106 if olh_hassub(u, ul, "/category/" as *u8) == 1 { return 0 } 107 if olh_endswith(u, ul, "/tag" as *u8) == 1 { return 0 } 108 if olh_endswith(u, ul, "/category" as *u8) == 1 { return 0 } 109 return 1 110} 111func olh_urlok(u: *u8, ul: i64) -> i64 { return olh_urlok_q(u, ul, 0) } 112// out:<decimal cid> key. (verbatim wc_outkey) 113func olh_outkey(cid: i64, out: *u8) -> i64 { 114 out[0] = 111 as u8 // o 115 out[1] = 117 as u8 // u 116 out[2] = 116 as u8 // t 117 out[3] = 58 as u8 // : 118 var m: i64 = cid 119 let t: *u8 = sys_mmap(28) 120 var k: i64 = 0 121 if m == 0 { t[0] = 48 as u8; k = 1 } 122 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 123 var i: i64 = 0 124 while i < k { out[4 + i] = t[k - 1 - i]; i = i + 1 } 125 out[4 + k] = 0 as u8 126 return 4 + k 127} 128// resolve a root-relative href ("/path", NOT protocol-relative "//") against a NUL-terminated base url: 129// scheme://host + path (NOT base+path concat — that makes ".../Trust_law"+"/about" a phantom node). hp[0..clen) 130// = the raw href bytes. Writes the resolved NUL-terminated url into scratch (>= host + clen + 1 bytes; callers 131// pass an 8192 arena). Returns resolved length, or 0 if base has no "://" or the result would overflow. 132func olh_resolve_root(base: *u8, hp: *u8, clen: i64, scratch: *u8) -> i64 { 133 var sp: i64 = 0 - 1 134 var bi: i64 = 0 135 var bf: i64 = 0 136 while bf == 0 { 137 if base[bi] == (0 as u8) { bf = 1 } 138 else { 139 if base[bi] == (58 as u8) { if base[bi+1] == (47 as u8) { if base[bi+2] == (47 as u8) { sp = bi; bf = 1 } } } 140 if bf == 0 { bi = bi + 1 } 141 } 142 } 143 if sp < 0 { return 0 } 144 var he: i64 = sp + 3 145 var hf: i64 = 0 146 while hf == 0 { if base[he] == (0 as u8) { hf = 1 } else { if base[he] == (47 as u8) { hf = 1 } else { he = he + 1 } } } 147 if he + clen + 1 >= OLH_MAGIC_8192 { return 0 } 148 var b: i64 = 0 149 while b < he { scratch[b] = base[b]; b = b + 1 } 150 var cc: i64 = 0 151 while cc < clen { scratch[he + cc] = hp[cc]; cc = cc + 1 } 152 scratch[he + clen] = 0 as u8 153 return he + clen 154} 155// scan hh[0..hlen) for <a href="..."> outlinks -> edgebuf[0..nedge) = ci_hash(resolved_url); returns nedge. 156// base = NUL-terminated page url (for root-relative resolution). Absolute http(s) kept as-is; "/path" -> 157// scheme://host + path; protocol-relative "//..." and other relative forms skipped (no authoritative base). 158func olh_scan(hh: *u8, hlen: i64, base: *u8, blen: i64, edgebuf: *i64, maxedge: i64) -> i64 { 159 var nedge: i64 = 0 160 let scratch: *u8 = sys_mmap(OLH_MAGIC_8192) 161 let ubuf: *u8 = sys_mmap(OLH_MAXURL + 2) // reused per outlink (no per-link mmap) 162 var i: i64 = 0 163 while i < hlen { 164 var step: i64 = 1 165 var ish: i64 = 0 166 if i + 5 < hlen { 167 var c0: i64 = hh[i] as i64 168 if c0 >= 0x41 { if c0 <= 0x5a { c0 = c0 + 0x20 } } 169 if c0 == 0x68 { 170 var c1: i64 = hh[i+1] as i64 171 if c1 >= 0x41 { if c1 <= 0x5a { c1 = c1 + 0x20 } } 172 var c2: i64 = hh[i+2] as i64 173 if c2 >= 0x41 { if c2 <= 0x5a { c2 = c2 + 0x20 } } 174 var c3: i64 = hh[i+3] as i64 175 if c3 >= 0x41 { if c3 <= 0x5a { c3 = c3 + 0x20 } } 176 if c1 == 0x72 { if c2 == 0x65 { if c3 == 0x66 { if (hh[i+4] as i64) == 0x3d { ish = 1 } } } } 177 } 178 } 179 if ish == 1 { 180 let q: i64 = hh[i+5] as i64 181 var quoted: i64 = 0 182 if q == 0x22 { quoted = 1 } 183 if q == 0x27 { quoted = 1 } 184 if quoted == 1 { 185 let cs: i64 = i + 6 186 var e: i64 = cs 187 var run: i64 = 1 188 while run == 1 { run = 0; if e < hlen { if (hh[e] as i64) != q { e = e + 1; run = 1 } } } 189 let clen: i64 = e - cs 190 if clen > 0 { if clen <= OLH_MAXURL { 191 let hp: *u8 = ((hh as i64) + cs) as *u8 192 var cand: *u8 = 0 as *u8 193 var candlen: i64 = 0 194 var abs: i64 = 0 195 if clen >= 8 { if olh_hassub(hp, 8, "https://" as *u8) == 1 { abs = 1 } } 196 if abs == 0 { if clen >= 7 { if olh_hassub(hp, 7, "http://" as *u8) == 1 { abs = 1 } } } 197 if abs == 1 { 198 cand = hp 199 candlen = clen 200 } else { 201 if (hp[0] as i64) == 0x2f { 202 var protorel: i64 = 0 203 if clen >= 2 { if (hp[1] as i64) == 0x2f { protorel = 1 } } 204 if protorel == 0 { 205 let rl: i64 = olh_resolve_root(base, hp, clen, scratch) 206 if rl > 0 { 207 cand = scratch 208 candlen = rl 209 } 210 } 211 } 212 } 213 if (cand as i64) != 0 { if candlen <= OLH_MAXURL { 214 var z: i64 = 0 215 while z < candlen { ubuf[z] = cand[z]; z = z + 1 } 216 ubuf[candlen] = 0 as u8 217 if olh_urlok(ubuf, candlen) == 1 { 218 if nedge < maxedge { edgebuf[nedge] = ci_hash(ubuf, candlen); nedge = nedge + 1 } 219 } 220 } } 221 } } 222 step = (e - i) + 1 223 } 224 } 225 i = i + step 226 } 227 return nedge 228}