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}