code wiki / (root) / nx_sitesweep_lib.nx

nx_sitesweep_lib.nx source

↩ module page · 324 lines · 17176 B

1// nx_sitesweep_lib.nx -- the shared half of the site-wide broken-asset sweep: the classes, the 2// prefilter, the path->URL map, and THE RULER-OUTPUT PARSER. One owner, imported by BOTH the sweep 3// (nx_sitesweep) and its gate (nx_sitesweep_gate). 4// 5// WHY A LIB AND NOT A PRIVATE HELPER. The sweep's own judgement is exactly two things: which pages can 6// be proven ref-free without a fetch, and how nx_page_verify's verdict text is read. Everything else is 7// nx_page_verify's judgement. If the gate re-implemented either of those to test them, the gate would be 8// testing a SECOND ruler and could agree with itself while disagreeing with what ships. There is one 9// copy, so the tested thing and the shipped thing cannot differ. 10// 11// It also makes the gate FAST: these functions need no fork and no network, so the roster can run real 12// teeth in milliseconds instead of a gate that must always be skipped for cost. 13// license_tier: ORIGINAL 14import "nx_syscalls.nx" 15import "nx_pageref_lib.nx" // the trigger alphabet is the VOCABULARY's, never a copy of it 16 17// ---- classes. These ARE the partition: every enumerated page lands in exactly one. ----------------- 18// BROKEN and PAGE-RED are DELIBERATELY SEPARATE (2026-08-25, on the first live run). The ruler answers 19// RED both when a page's assets are broken and when the PAGE ITSELF did not serve -- measured the same 20// hour on /world/games.v1-bak-20260810, which reported RED with broken=0 because the page 404s and the 21// ruler never reached asset checking at all. Folding them would put a publish/routing fault in the 22// bucket labelled "broken images" and send the next reader to fix the wrong thing. A COMPOUND 23// ASSERTION THAT WILL NOT NAME ITS FAILING CONJUNCT IS A FALSE-ALARM GENERATOR. Both are RED; only the 24// worklists differ, and the remedy differs completely. 25const SS_C_UNCHECKED: i64 = 0 26const SS_C_NOREFS: i64 = 1 27const SS_C_GREEN: i64 = 2 28const SS_C_PARTIAL: i64 = 3 29const SS_C_BROKEN: i64 = 4 30const SS_C_PAGERED: i64 = 5 31const SS_C_UNOBSERVABLE: i64 = 6 32const SS_C_UNMEASURED: i64 = 7 33const SS_C_UNREADABLE: i64 = 8 34const SS_C_N: i64 = 9 35 36// ---- byte constants (named so the scans read as intent, never as character codes) ------------------- 37const SS_CH_NL: i64 = 10 38const SS_CH_CR: i64 = 13 39const SS_CH_SP: i64 = 32 40const SS_CH_MINUS: i64 = 45 41const SS_CH_DOT: i64 = 46 42const SS_CH_SLASH: i64 = 47 43const SS_CH_ZERO: i64 = 48 44const SS_CH_NINE: i64 = 57 45const SS_DEC: i64 = 10 46const SS_WORD: i64 = 8 47const SS_SCRATCH: i64 = 32 48const SS_MODE_644: i64 = 420 49const SS_HTML_SUF: i64 = 5 50const SS_INDEX_SUF: i64 = 6 51 52// token lengths, bound to the literal they follow so the two cannot drift apart. A HAND-COUNTED LENGTH 53// BESIDE A STRING LITERAL IS A SECOND COPY OF THAT LITERAL'S SHAPE. 54const SS_T_VERDICT: i64 = 8 // "VERDICT=" 55const SS_T_BROKEN: i64 = 8 // " broken=" 56const SS_T_CHECKED: i64 = 8 // "checked=" 57const SS_T_SKIPPED: i64 = 9 // " skipped=" 58const SS_T_LAW: i64 = 15 // "law-violations=" 59 60// outv slots for the ruler parse, named so a caller cannot mis-index them 61const SS_V_CLASS: i64 = 0 62const SS_V_BROKEN: i64 = 1 63const SS_V_CHECKED: i64 = 2 64const SS_V_LAW: i64 = 3 65const SS_V_MS: i64 = 4 66const SS_V_SKIPPED: i64 = 5 67// THE REASON TRAVELS WITH THE COUNT (added 2026-08-26). The ruler already audits accessibility, and it 68// already announces when its OWN extractor could not read every img src -- both on the same capture 69// this parser is already holding. The sweep kept the broken-asset verdict and DROPPED both. So the 70// estate had a population instrument for broken images and NONE for a page nobody can read, which is 71// how our own headline surface could fail its own accessibility check with nobody counting. 72// BOTH ABSTAIN AT -1 RATHER THAN AT 0: a missing line means the ruler never got that far, and an axis 73// that cannot see must abstain, never acquit. 74const SS_V_A11Y: i64 = 6 75const SS_V_COVGAP: i64 = 7 76const SS_V_N: i64 = 8 77 78// ===================================================================================================== 79// text primitives 80// ===================================================================================================== 81func ss_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 82func ss_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p } 83func ss_catn(d: *u8, o: i64, v: i64) -> i64 { 84 var p: i64 = o 85 var m: i64 = v 86 if m < 0 { d[p] = SS_CH_MINUS as u8; p = p + 1; m = 0 - m } 87 if m == 0 { d[p] = SS_CH_ZERO as u8; return p + 1 } 88 let t: *u8 = sys_mmap(SS_SCRATCH) 89 var k: i64 = 0 90 while m > 0 { t[k] = (SS_CH_ZERO + (m % SS_DEC)) as u8; m = m / SS_DEC; k = k + 1 } 91 while k > 0 { k = k - 1; d[p] = t[k]; p = p + 1 } 92 sys_munmap(t, SS_SCRATCH) 93 return p 94} 95// 1 iff buf[off..] starts with lit; bounded by n so a scan cannot run off the buffer 96func ss_at(buf: *u8, n: i64, off: i64, lit: *u8) -> i64 { 97 var i: i64 = 0 98 while lit[i] != (0 as u8) { 99 if off + i >= n { return 0 } 100 if buf[off + i] != lit[i] { return 0 } 101 i = i + 1 102 } 103 return 1 104} 105func ss_contains(buf: *u8, n: i64, lit: *u8) -> i64 { 106 let m: i64 = ss_len(lit) 107 if m == 0 { return 0 } 108 var i: i64 = 0 109 while i + m <= n { if ss_at(buf, n, i, lit) == 1 { return 1 } i = i + 1 } 110 return 0 111} 112// offset of the LAST occurrence, or -1. POSITIONAL ANCHORING: the ruler's verdict is its final line and 113// its own diagnostic prose quotes earlier ones, so only the last occurrence is the answer. This is the 114// same discipline gv_last_line uses, and it is why an unanchored match over untrusted output is banned. 115func ss_last(buf: *u8, n: i64, lit: *u8) -> i64 { 116 let m: i64 = ss_len(lit) 117 if m == 0 { return 0 - 1 } 118 var i: i64 = n - m 119 while i >= 0 { if ss_at(buf, n, i, lit) == 1 { return i } i = i - 1 } 120 return 0 - 1 121} 122// decimal integer starting at off; -1 if there is no digit there 123func ss_int_at(buf: *u8, n: i64, off: i64) -> i64 { 124 var o: i64 = off 125 var v: i64 = 0 126 var d: i64 = 0 127 while o < n { 128 let c: i64 = buf[o] as i64 129 if c < SS_CH_ZERO { break } 130 if c > SS_CH_NINE { break } 131 v = v * SS_DEC + (c - SS_CH_ZERO) 132 d = d + 1 133 o = o + 1 134 } 135 if d == 0 { return 0 - 1 } 136 return v 137} 138// offset just past pfx inside a, or -1 139func ss_pfx(a: *u8, pfx: *u8) -> i64 { 140 var i: i64 = 0 141 while pfx[i] != (0 as u8) { if a[i] != pfx[i] { return 0 - 1 } i = i + 1 } 142 return i 143} 144func ss_ends_html(s: *u8) -> i64 { 145 let n: i64 = ss_len(s) 146 if n < SS_HTML_SUF { return 0 } 147 if s[n-5] != (SS_CH_DOT as u8) { return 0 } 148 if s[n-4] != (104 as u8) { return 0 } 149 if s[n-3] != (116 as u8) { return 0 } 150 if s[n-2] != (109 as u8) { return 0 } 151 if s[n-1] != (108 as u8) { return 0 } 152 return 1 153} 154// field lookup INSIDE one line only, so a value from a neighbouring row can never be read as this 155// row's answer. Returns the offset just past key, or -1. 156func ss_field(buf: *u8, ls: i64, le: i64, key: *u8) -> i64 { 157 let m: i64 = ss_len(key) 158 var i: i64 = ls 159 while i + m <= le { if ss_at(buf, le, i, key) == 1 { return i + m } i = i + 1 } 160 return 0 - 1 161} 162// 1 iff NUL-terminated want equals buf[off..] up to the next space / newline / end 163func ss_tok_eq(buf: *u8, le: i64, off: i64, want: *u8) -> i64 { 164 var i: i64 = 0 165 while want[i] != (0 as u8) { 166 if off + i >= le { return 0 } 167 if buf[off + i] != want[i] { return 0 } 168 i = i + 1 169 } 170 if off + i >= le { return 1 } 171 let c: i64 = buf[off + i] as i64 172 if c == SS_CH_SP { return 1 } 173 if c == SS_CH_NL { return 1 } 174 if c == SS_CH_CR { return 1 } 175 return 0 176} 177// TRUNCATE-WRITE. Never append: a pre-deploy check that greps an append-only journal is vacuously green 178// forever, so an artifact meant to hold the CURRENT answer must hold only the current answer. 179func ss_write_all(path: *u8, buf: *u8, n: i64) -> i64 { 180 let fd: i64 = sys_openat_wr(path, SS_MODE_644) 181 if fd < 0 { return 0 - 1 } 182 var w: i64 = 0 183 while w < n { 184 let r: i64 = sys_write(fd, ((buf as i64) + w) as *u8, n - w) 185 if r <= 0 { sys_close(fd); return 0 - 1 } 186 w = w + r 187 } 188 sys_close(fd) 189 return w 190} 191 192// ===================================================================================================== 193// class names, both directions 194// ===================================================================================================== 195func ss_class_name(c: i64) -> *u8 { 196 if c == SS_C_NOREFS { return "NO-REFS" as *u8 } 197 if c == SS_C_GREEN { return "GREEN" as *u8 } 198 if c == SS_C_PARTIAL { return "GREEN-PARTIAL" as *u8 } 199 if c == SS_C_BROKEN { return "BROKEN" as *u8 } 200 if c == SS_C_PAGERED { return "PAGE-RED" as *u8 } 201 if c == SS_C_UNOBSERVABLE { return "UNOBSERVABLE" as *u8 } 202 if c == SS_C_UNMEASURED { return "UNMEASURED" as *u8 } 203 if c == SS_C_UNREADABLE { return "UNREADABLE" as *u8 } 204 return "UNCHECKED" as *u8 205} 206// GREEN-PARTIAL is tested BEFORE GREEN and PAGE-RED before any RED prefix: a prefix match on the 207// shorter word would swallow the longer one and silently promote an UNPROVEN coverage claim into a 208// clean pass. Order here is load-bearing, not cosmetic. 209func ss_class_of_name(s: *u8) -> i64 { 210 if ss_pfx(s, "GREEN-PARTIAL" as *u8) >= 0 { return SS_C_PARTIAL } 211 if ss_pfx(s, "GREEN" as *u8) >= 0 { return SS_C_GREEN } 212 if ss_pfx(s, "NO-REFS" as *u8) >= 0 { return SS_C_NOREFS } 213 if ss_pfx(s, "PAGE-RED" as *u8) >= 0 { return SS_C_PAGERED } 214 if ss_pfx(s, "BROKEN" as *u8) >= 0 { return SS_C_BROKEN } 215 if ss_pfx(s, "UNOBSERVABLE" as *u8) >= 0 { return SS_C_UNOBSERVABLE } 216 if ss_pfx(s, "UNMEASURED" as *u8) >= 0 { return SS_C_UNMEASURED } 217 if ss_pfx(s, "UNREADABLE" as *u8) >= 0 { return SS_C_UNREADABLE } 218 if ss_pfx(s, "RED" as *u8) >= 0 { return SS_C_BROKEN } 219 return SS_C_UNCHECKED 220} 221 222// ===================================================================================================== 223// THE PREFILTER -- a NECESSARY CONDITION ON THE COMPOSED RULER'S OWN TRIGGER ALPHABET, not a second 224// extractor. nx_page_verify's scanner only ever begins a ref at one of two tokens: src= (in either 225// quote style or bare) or a link tag, whose href it gates on an 80-byte lookback for the tag opener. 226// A page containing neither byte sequence therefore yields ZERO refs from that scanner, by 227// construction. So a 0 here is a PROOF that the page cannot carry a broken asset ref, not a guess -- 228// and the error direction is one-sided: src= appearing in prose costs an unnecessary fetch and can 229// never suppress a page the ruler would have flagged. Bite-proven in nx_sitesweep_gate against the 230// published unquoted-attribute fixture, which every earlier version of the ruler could not see. 231// ===================================================================================================== 232// THE TRIGGER ALPHABET IS READ FROM THE VOCABULARY, NEVER COPIED (nx_pageref_lib). Spelling "src=" here 233// made the by-construction proof above silently FALSE the moment the composed ruler learned a third 234// attribute -- which it did on 2026-08-26, when data-glb= was added so /exceed mesh panels stopped being 235// acquitted unfetched. A page carrying only mesh canvases would then have been prefiltered out as 236// NO-REFS and never verified: the prefilter would have been claiming a proof it no longer had. 237// Iterating pr_attr makes that class of drift impossible -- a new attribute is admitted here the same 238// instant the extractor learns it, because there is only one table. 239func ss_may_have_refs(buf: *u8, n: i64) -> i64 { 240 var k: i64 = 0 241 while k < pr_nattr() { 242 if ss_contains(buf, n, pr_attr(k)) == 1 { return 1 } 243 k = k + 1 244 } 245 if ss_contains(buf, n, "<link" as *u8) == 1 { return 1 } 246 return 0 247} 248 249// ===================================================================================================== 250// PATH -> URL. <docroot>/world/foundation.html -> <origin>/world/foundation. index.html collapses to 251// its own directory because the edge serves extensionless paths and 301-strips the trailing slash -- 252// the same edge behaviour that makes a relative ref RED by law in the first place. Returns url length. 253// ===================================================================================================== 254func ss_url_of(origin: *u8, docroot_len: i64, path: *u8, out: *u8) -> i64 { 255 var o: i64 = ss_cat(out, 0, origin) 256 let pl: i64 = ss_len(path) 257 let s: i64 = docroot_len 258 var e: i64 = pl - SS_HTML_SUF 259 if e - s >= SS_INDEX_SUF { 260 if ss_at(path, pl, e - SS_INDEX_SUF, "/index" as *u8) == 1 { e = e - SS_INDEX_SUF } 261 } 262 if e <= s { out[o] = SS_CH_SLASH as u8; o = o + 1; out[o] = 0 as u8; return o } 263 var i: i64 = s 264 while i < e { out[o] = path[i]; o = o + 1; i = i + 1 } 265 out[o] = 0 as u8 266 return o 267} 268 269// ===================================================================================================== 270// THE RULER-OUTPUT PARSER. Pure: give it the bytes nx_page_verify wrote and whether the capture was 271// cut, and it returns the class plus the numbers. Pure ON PURPOSE -- this is the one piece of judgement 272// the sweep adds, so it is the piece the gate must be able to bite without a fork or a network. 273// 274// cut != 0 -> UNREADABLE. A CUT CAPTURE DESTROYS THE TAIL, AND THE TAIL IS THE VERDICT: never guess 275// past a declared truncation, and never let a truncation read as a page fault. 276// ===================================================================================================== 277func ss_parse_ruler(buf: *u8, n: i64, cut: i64, outv: *i64) -> i64 { 278 outv[SS_V_BROKEN] = 0 279 outv[SS_V_CHECKED] = 0 - 1 280 outv[SS_V_LAW] = 0 281 outv[SS_V_SKIPPED] = 0 282 outv[SS_V_A11Y] = 0 - 1 283 outv[SS_V_COVGAP] = 0 - 1 284 if cut != 0 { outv[SS_V_CLASS] = SS_C_UNREADABLE; return SS_C_UNREADABLE } 285 if n <= 0 { outv[SS_V_CLASS] = SS_C_UNREADABLE; return SS_C_UNREADABLE } 286 let vo: i64 = ss_last(buf, n, "VERDICT=" as *u8) 287 if vo < 0 { outv[SS_V_CLASS] = SS_C_UNREADABLE; return SS_C_UNREADABLE } 288 var cl: i64 = ss_class_of_name(((buf as i64) + vo + SS_T_VERDICT) as *u8) 289 // An unrecognised verdict word must land in its OWN bucket and never in a known one -- the bucket 290 // it lands in becomes the number somebody plans against. NO-REFS, PAGE-RED and UNCHECKED are OUR 291 // words, never the ruler's; seeing one back from ruler output means the line was not readable. 292 if cl == SS_C_UNCHECKED { cl = SS_C_UNREADABLE } 293 if cl == SS_C_NOREFS { cl = SS_C_UNREADABLE } 294 if cl == SS_C_PAGERED { cl = SS_C_UNREADABLE } 295 let bo: i64 = ss_last(buf, n, " broken=" as *u8) 296 if bo >= 0 { let bv: i64 = ss_int_at(buf, n, bo + SS_T_BROKEN); if bv >= 0 { outv[SS_V_BROKEN] = bv } } 297 let co: i64 = ss_last(buf, n, "checked=" as *u8) 298 if co >= 0 { let cv: i64 = ss_int_at(buf, n, co + SS_T_CHECKED); if cv >= 0 { outv[SS_V_CHECKED] = cv } } 299 let so: i64 = ss_last(buf, n, " skipped=" as *u8) 300 if so >= 0 { let sv: i64 = ss_int_at(buf, n, so + SS_T_SKIPPED); if sv >= 0 { outv[SS_V_SKIPPED] = sv } } 301 let lo: i64 = ss_last(buf, n, "law-violations=" as *u8) 302 if lo >= 0 { let lv: i64 = ss_int_at(buf, n, lo + SS_T_LAW); if lv >= 0 { outv[SS_V_LAW] = lv } } 303 // The offset below is DERIVED from the literal with ss_len rather than hand-counted beside it: a 304 // hand-counted length is a second copy of that literal's shape and the two drift silently. 305 let ao: i64 = ss_last(buf, n, "a11y-issues=" as *u8) 306 if ao >= 0 { let av: i64 = ss_int_at(buf, n, ao + ss_len("a11y-issues=" as *u8)); if av >= 0 { outv[SS_V_A11Y] = av } } 307 // COVERAGE-GAP is decidable only once the ruler printed its asset summary. Before that the ref pass 308 // was never observed, and a 0 would be a claim we did not earn. 309 // DELIBERATELY NOT FOLDED TOGETHER WITH COVERAGE-CAP: they are different conditions with different 310 // remedies -- one says the extractor could not read a src, the other says the ref ceiling was hit -- 311 // and one counter for two causes is how a worklist stops pointing at the repair. 312 if outv[SS_V_CHECKED] >= 0 { 313 outv[SS_V_COVGAP] = 0 314 if ss_contains(buf, n, "COVERAGE-GAP:" as *u8) == 1 { outv[SS_V_COVGAP] = 1 } 315 } 316 // THE DISCRIMINATOR, read off the ruler's own output STRUCTURE rather than guessed: the ruler emits 317 // its asset summary line (checked=/skipped=/law-violations=/broken=) only once it has actually 318 // examined refs. A RED with no summary line means it exited BEFORE asset checking -- the page 319 // itself did not serve. That is a publish or routing fault, not a broken image, and it gets its own 320 // class so the worklist points at the right repair. 321 if cl == SS_C_BROKEN { if outv[SS_V_CHECKED] < 0 { cl = SS_C_PAGERED } } 322 outv[SS_V_CLASS] = cl 323 return cl 324}