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}