code wiki / _hdl_build / nx_site_ingest.nx
nx_site_ingest.nx source
↩ module page · 137 lines · 6720 B
1// nx_site_ingest.nx -- FLAG ingestion-friendly sites + AUTO-INGEST the full site to the local Nishi
2// library (operator: "flag good sites of research that allow ingestion and ... auto ingest the full
3// site for local use via the nishi library", so "bot-blocking cant starve us"). The discipline:
4// 1. PERMISSION FIRST -- never bulk-ingest a site that doesn't allow it. si_robots_allows parses
5// robots.txt Disallow rules; si_is_open_access reads license/repository signals (CC / arXiv /
6// PMC / DOI / MIT / public-domain). Only INGEST_OK sites get flagged + mirrored.
7// 2. WHOLE-SITE MIRROR -- enumerate the site's URLs from its sitemap, fetch each POLITELY (via
8// nx_polite_crawl's per-host pacing), and write each page into the library on disk.
9// 3. LOCAL-FIRST / STARVE-PROOF -- once mirrored, the team reads the LOCAL copy; if the live site
10// goes dark or bot-blocks us, the corpus is already ours.
11// Composes nx_polite_crawl (the fetch governor that keeps it polite) + nx_research_synth/claim_extract
12// (which then corroborate/extract the mirrored text). LAWS: struct-free, integer-only. license_tier: ORIGINAL
13import "nx_polite_crawl.nx"
14import "nx_syscalls.nx"
15
16const SI_INGEST_OK: i64 = 2 // allowed + clearly licensed/repository -> flag + auto-ingest
17const SI_INGEST_PARTIAL: i64 = 1 // allowed by robots but unclear license -> ingest cautiously, flag-for-review
18const SI_INGEST_FORBIDDEN: i64 = 0 // robots disallows -> DO NOT bulk-ingest (respect the site)
19
20func si_strlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
21
22func si_match_at(buf: *u8, pos: i64, n: i64, needle: *u8) -> i64 {
23 var k: i64 = 0
24 while needle[k] != (0 as u8) {
25 if pos + k >= n { return 0 }
26 if (buf[pos+k] & 0xff) != (needle[k] & 0xff) { return 0 }
27 k = k + 1
28 }
29 return 1
30}
31
32func si_count_sub(buf: *u8, n: i64, needle: *u8) -> i64 {
33 let nl: i64 = si_strlen(needle)
34 if nl == 0 { return 0 }
35 var c: i64 = 0; var i: i64 = 0
36 while i + nl <= n { if si_match_at(buf, i, n, needle) == 1 { c = c + 1; i = i + nl } else { i = i + 1 } }
37 return c
38}
39
40// is the disallow VALUE buf[vs,vs+vl) a prefix of `path`[0,pathlen)?
41func si_is_prefix(buf: *u8, vs: i64, vl: i64, path: *u8, pathlen: i64) -> i64 {
42 if vl <= 0 { return 0 }
43 if vl > pathlen { return 0 }
44 var k: i64 = 0
45 while k < vl { if (buf[vs+k] & 0xff) != (path[k] & 0xff) { return 0 } k = k + 1 }
46 return 1
47}
48
49// does robots.txt DISALLOW `path`? scans every "Disallow:" rule; a non-empty value that prefixes the
50// path disallows it. ("Disallow: /" disallows everything; "Disallow:" with empty value allows.)
51// CONSERVATIVE by design: honors all Disallow lines (safe -- never ingest where told not to).
52func si_robots_disallows(robots: *u8, n: i64, path: *u8, pathlen: i64) -> i64 {
53 let needle: *u8 = "Disallow:" as *u8
54 let dl: i64 = si_strlen(needle)
55 var i: i64 = 0
56 while i + dl <= n {
57 if si_match_at(robots, i, n, needle) == 1 {
58 var p: i64 = i + dl
59 while p < n { if (robots[p] & 0xff) == 32 { p = p + 1 } else { break } } // skip spaces
60 let vs: i64 = p
61 while p < n { let c: i64 = robots[p] & 0xff; if c == 10 { break } if c == 13 { break } if c == 32 { break } p = p + 1 }
62 let vl: i64 = p - vs
63 if si_is_prefix(robots, vs, vl, path, pathlen) == 1 { return 1 }
64 i = p
65 } else { i = i + 1 }
66 }
67 return 0
68}
69
70func si_robots_allows(robots: *u8, n: i64, path: *u8, pathlen: i64) -> i64 {
71 if si_robots_disallows(robots, n, path, pathlen) == 1 { return 0 }
72 return 1
73}
74
75// count open-access / permissive-license / repository signals in a page or about-text
76func si_open_signals(buf: *u8, n: i64) -> i64 {
77 var c: i64 = 0
78 c = c + si_count_sub(buf, n, "creativecommons" as *u8)
79 c = c + si_count_sub(buf, n, "CC BY" as *u8)
80 c = c + si_count_sub(buf, n, "open access" as *u8)
81 c = c + si_count_sub(buf, n, "public domain" as *u8)
82 c = c + si_count_sub(buf, n, "MIT License" as *u8)
83 c = c + si_count_sub(buf, n, "arxiv" as *u8)
84 c = c + si_count_sub(buf, n, "arXiv" as *u8)
85 c = c + si_count_sub(buf, n, "PMC" as *u8)
86 return c
87}
88
89// the INGEST VERDICT: FORBIDDEN if robots disallows; OK if allowed AND (licensed OR repository);
90// PARTIAL if allowed but no clear license (flag for human review before bulk pull).
91func si_ingest_verdict(robots_allows: i64, open_signals: i64, is_repository: i64) -> i64 {
92 if robots_allows == 0 { return SI_INGEST_FORBIDDEN }
93 if open_signals >= 1 { return SI_INGEST_OK }
94 if is_repository == 1 { return SI_INGEST_OK }
95 return SI_INGEST_PARTIAL
96}
97
98// flag a GOOD ingestion target (the researcher surfaces these to the operator/library)
99func si_flag_good_site(verdict: i64) -> i64 { if verdict == SI_INGEST_OK { return 1 } return 0 }
100
101// ---- whole-site enumeration from a sitemap.xml ----
102func si_sitemap_url_count(sitemap: *u8, n: i64) -> i64 { return si_count_sub(sitemap, n, "<loc>" as *u8) }
103
104// ---- ingest progress + the STARVE-PROOF property ----
105func si_ingest_progress_permil(ingested: i64, total: i64) -> i64 {
106 if total <= 0 { return 0 }
107 return ingested * 1000 / total
108}
109func si_fully_mirrored(ingested: i64, total: i64) -> i64 {
110 if total <= 0 { return 0 }
111 if ingested >= total { return 1 }
112 return 0
113}
114// once the site is local, bot-blocking / the site going dark CANNOT starve the team's research.
115func si_starve_proof(fully_mirrored: i64) -> i64 { return fully_mirrored }
116
117// write one mirrored page to the local Nishi library; returns bytes written (0 on fail).
118// path e.g. "knowledge/library/mirror/<id>.txt". The local-first corpus the team reads from.
119func si_library_write(path: *u8, data: *u8, len: i64) -> i64 {
120 let fd: i64 = sys_openat_wr(path, 0x1a4)
121 if fd < 0 { return 0 }
122 let w: i64 = sys_write(fd, data, len)
123 sys_close(fd)
124 return w
125}
126
127// the END-TO-END gate for one site. NOTE the two politeness MODES are different:
128// * BROAD RESEARCH crawl = many hosts, FEW requests each (pc_research_gate floors per-host count).
129// * FULL-SITE INGEST = ONE host, MANY requests -- legitimately, BECAUSE it is permitted AND PACED.
130// So bulk ingest is admissible iff (a) verdict OK (robots+license permit it) AND (b) it is PACED
131// through the governor (per-host min-interval + robots crawl-delay honored), NOT gated on a low total
132// count. Pacing is the politeness for a permitted bulk pull, not scarcity.
133func si_ingest_admissible(verdict: i64, paced_through_governor: i64) -> i64 {
134 if verdict != SI_INGEST_OK { return 0 }
135 if paced_through_governor != 1 { return 0 }
136 return 1
137}