nx_pub_lib.nx source
↩ module page · 1105 lines · 51664 B
1// nx_pub_lib.nx -- THE PUBLISHING PLANE CORE (multi-site, 2026-07-30).
2//
3// THE OPERATOR'S DIAGNOSIS: "how we publish in these workstreams to nishifamily.com is random and
4// arbitrary -- we want nishifamily and all our client sites to have SOTA capabilities so we and the
5// client can focus on work, not on where something was put."
6//
7// MEASURED FIRST (2026-07-30, nx_pub_desk census + live edge fetches), which is why this file exists:
8// nishifamily: FOUR different answers to "what pages does this site have" -- the nav/sitemap page
9// says 23 surfaces (hardcoded in nx_site_chrome), sitemap.xml says 96 urls, the registry says 157
10// rows, the docroot holds 135 pages (+61 .prev artifacts +11 debris). 54 pages are SERVED but in
11// NO registry.
12// andelinwest (a CLIENT): registry 0 rows, no sitemap.xml, no robots.txt (404), and
13// https://andelinwest.com/index.html.bak-v1 returns 200 with a whole previous homepage.
14//
15// THE ELITE PATTERN (news-org + docs-as-code + headless-CMS sweep, July 2026): ONE machine-readable
16// registry per site is the SSOT; every discovery artifact -- sitemap.xml, robots.txt, llms.txt, nav,
17// hubs, feeds -- is DERIVED from it and never hand-curated; and content reaches the docroot only
18// through a gate that maintains the registry. Astro content collections make it a build error, the
19// NYT Gateway makes it a publish refusal. Same law: THE DERIVED ARTIFACT MUST NOT BE AUTHORABLE.
20//
21// THIS FILE is the derivation core, site-agnostic. It reads TWO data sources and writes NO policy:
22// 1. the SITE TABLE (knowledge/pub_sites.conf, TSV):
23// site <TAB> docroot <TAB> regprefix <TAB> baseurl <TAB> title <TAB> disallow-csv
24// 2. the per-site REGISTRY plane at regprefix (the ecosystem row convention, status@col3):
25// id <TAB> title <TAB> owner <TAB> status <TAB> section <TAB> path <TAB> note
26// status vocabulary: live (indexable page) | asset (served, never in the sitemap) |
27// draft | withdrawn | redirect | debris
28//
29// WHAT IT FIXES vs the pd_sitemap it supersedes (both stay until nx_pub_desk migrates, F-debt filed):
30// * REAL XML ESCAPING. pd_esc maps & < > to '.' -- lossy by design for JSON/HTML attributes, but in
31// a <loc> it silently CORRUPTS any URL carrying them. Here & < > " ' are emitted.
32// * NO PER-ROW ALLOCATION. pd_sitemap calls sys_mmap INSIDE the row loop (a 512B path buffer per
33// registry row). That is the exact per-call-alloc-in-a-primitive class that leaked 8KB/call through
34// ss_hget across 205 sites. Every buffer here is hoisted above the loop.
35// * HONEST LOAD. sts_load_honest, not sts_load: a stale q:n silently truncates a registry, and a
36// truncated registry emits a SHORT SITEMAP that looks perfectly well-formed. Callers get the flags.
37// * MULTI-SITE BY CONSTRUCTION. No docroot, prefix, domain or disallow rule is compiled in (rule 11).
38// license_tier: ORIGINAL No hw writes (Rule 26).
39// DEPENDENCIES ARE runtime/-ONLY ON PURPOSE (measured 2026-07-30): import resolution reaches
40// _hdl_build/ -> runtime/ but NOT runtime/ -> _hdl_build/. This lib is imported by
41// runtime/nx_site_publish_lib.nx, so importing nx_sovjson_lib.nx (which lives only in _hdl_build/)
42// made every dependent fail with a bare "expand_imports failed". The five string helpers it supplied
43// are inlined below instead -- five lines, versus a shared lib this file structurally cannot reach.
44import "nx_store_seed_lib.nx"
45import "nx_seg_store.nx"
46import "nx_syscalls.nx"
47
48const PL_CAP: i64 = 1048576
49const PL_CONFCAP: i64 = 65536
50const PL_PATHCAP: i64 = 1024
51const PL_SPAN: i64 = 16
52const PL_STATB: i64 = 144
53const PL_MT_OFF: i64 = 88
54const PL_MODE: i64 = 420
55const PL_SECMAX: i64 = 128
56const PL_I64B: i64 = 8
57const PL_STDERR: i64 = 2
58const PL_NUMCAP: i64 = 32
59const PL_BASE10: i64 = 10
60const PL_TAB: i64 = 9
61const PL_NL: i64 = 10
62const PL_SLASH: i64 = 47
63const PL_HASH: i64 = 35
64const PL_COMMA: i64 = 44
65const PL_DASH: i64 = 45
66const PL_DOT: i64 = 46
67const PL_ZERO: i64 = 48
68const PL_SPACE: i64 = 32
69const PL_AMP: i64 = 38
70const PL_LT: i64 = 60
71const PL_GT: i64 = 62
72const PL_QUOTE: i64 = 34
73const PL_APOS: i64 = 39
74const PL_HTML_EXT: i64 = 5
75const PL_INDEX_LEN: i64 = 10
76// getdents64 record layout + scan envelope (DECLARED caps, never a silent truncation)
77const PL_DBUF: i64 = 65536
78const PL_DE_RLO: i64 = 16
79const PL_DE_RHI: i64 = 17
80const PL_DE_TYPE: i64 = 18
81const PL_DE_NAME: i64 = 19
82const PL_DT_DIR: i64 = 4
83const PL_BYTE: i64 = 256
84const PL_NAMES: i64 = 262144
85const PL_MAXN: i64 = 4096
86// tree-walk bounds (2026-08-01, debt 1785614931): the deepest REAL published surface today is
87// 3 dirs (compare/atlas/card); 8 is headroom. Beyond-depth REFUSES loudly, never truncates.
88const PL_MAXDEPTH: i64 = 8
89const PL_ENVB: i64 = 64
90const PL_NAME_PAD: i64 = 2
91const PL_I64B: i64 = 8
92const PL_DAY: i64 = 86400
93const PL_TWO_DIG: i64 = 10
94// civil-from-days (Howard Hinnant) -- integer calendar, no libc
95const PL_EPOCH_SHIFT: i64 = 719468
96const PL_ERA_DAYS: i64 = 146097
97const PL_CENT_DAYS: i64 = 36524
98const PL_QUAD_DAYS: i64 = 1460
99const PL_YEAR_DAYS: i64 = 365
100const PL_ERA_YEARS: i64 = 400
101const PL_LEAP4: i64 = 4
102const PL_LEAP100: i64 = 100
103const PL_MONTH_SLOPE: i64 = 153
104const PL_MP_BIAS: i64 = 2
105const PL_MP_SCALE: i64 = 5
106const PL_MP_CUT: i64 = 10
107const PL_MAR_OFF: i64 = 3
108const PL_DEC_WRAP: i64 = 9
109// registry columns (the ecosystem convention -- status@col3; ADD new columns at the end only, rule 19)
110const PL_C_ID: i64 = 0
111const PL_C_TITLE: i64 = 1
112const PL_C_OWNER: i64 = 2
113const PL_C_STATUS: i64 = 3
114const PL_C_SECTION: i64 = 4
115const PL_C_PATH: i64 = 5
116const PL_C_NOTE: i64 = 6
117// site-table columns
118const PL_S_SITE: i64 = 0
119const PL_S_DOCROOT: i64 = 1
120const PL_S_PREFIX: i64 = 2
121const PL_S_BASEURL: i64 = 3
122const PL_S_TITLE: i64 = 4
123const PL_S_DISALLOW: i64 = 5
124// gateway verdicts
125const PL_OK: i64 = 0
126const PL_REFUSED: i64 = 3
127
128func pl_vlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
129func pl_puts(s: *u8) -> i64 { sys_write(1, s, pl_vlen(s)); return 0 }
130func pl_werr(s: *u8) -> i64 { sys_write(PL_STDERR, s, pl_vlen(s)); return 0 }
131func pl_cat(d: *u8, o: i64, s: *u8) -> i64 {
132 var i: i64 = 0
133 var j: i64 = o
134 while s[i] != (0 as u8) { d[j] = s[i]; j = j + 1; i = i + 1 }
135 return j
136}
137func pl_catn(d: *u8, o: i64, v: i64) -> i64 {
138 var m: i64 = v
139 var j: i64 = o
140 if m < 0 { d[j] = PL_DASH as u8; j = j + 1; m = 0 - m }
141 let t: *u8 = sys_mmap(PL_NUMCAP)
142 var k: i64 = 0
143 if m == 0 { t[0] = PL_ZERO as u8; k = 1 }
144 while m > 0 { t[k] = (PL_ZERO + (m % PL_BASE10)) as u8; m = m / PL_BASE10; k = k + 1 }
145 var i: i64 = 0
146 while i < k { d[j] = t[k - 1 - i]; j = j + 1; i = i + 1 }
147 sys_munmap(t, PL_NUMCAP)
148 return j
149}
150
151// ---- row / column primitives over a newline-and-tab buffer ------------------------------------
152func pl_le(q: *u8, i: i64, n: i64) -> i64 {
153 var e: i64 = i
154 var s: i64 = 1
155 while s == 1 { if e >= n { s = 0 } else { if q[e] == (PL_NL as u8) { s = 0 } else { e = e + 1 } } }
156 return e
157}
158func pl_col(q: *u8, ls: i64, le: i64, c: i64, out: *i64) -> i64 {
159 var col: i64 = 0
160 var p: i64 = ls
161 while col < c {
162 var s: i64 = 1
163 while s == 1 { if p >= le { return 0 } if q[p] == (PL_TAB as u8) { s = 0 } else { p = p + 1 } }
164 p = p + 1
165 col = col + 1
166 }
167 var e: i64 = p
168 var s2: i64 = 1
169 while s2 == 1 { if e >= le { s2 = 0 } else { if q[e] == (PL_TAB as u8) { s2 = 0 } else { e = e + 1 } } }
170 out[0] = p
171 out[1] = e
172 return 1
173}
174func pl_lit_eq(q: *u8, s: i64, e: i64, lit: *u8) -> i64 {
175 var i: i64 = 0
176 while s + i < e { if lit[i] == (0 as u8) { return 0 } if q[s+i] != lit[i] { return 0 } i = i + 1 }
177 if lit[i] != (0 as u8) { return 0 }
178 return 1
179}
180func pl_span_eq(a: *u8, s1: i64, e1: i64, b: *u8, s2: i64, e2: i64) -> i64 {
181 if e1 - s1 != e2 - s2 { return 0 }
182 var i: i64 = 0
183 while s1 + i < e1 { if a[s1+i] != b[s2+i] { return 0 } i = i + 1 }
184 return 1
185}
186func pl_ends(q: *u8, s: i64, n: i64, suf: *u8) -> i64 {
187 let sl: i64 = pl_vlen(suf)
188 if n < sl { return 0 }
189 var i: i64 = 0
190 while i < sl { if q[s + n - sl + i] != suf[i] { return 0 } i = i + 1 }
191 return 1
192}
193func pl_find(buf: *u8, n: i64, needle: *u8) -> i64 {
194 let nl: i64 = pl_vlen(needle)
195 if nl == 0 { return 0 }
196 var i: i64 = 0
197 while i + nl <= n {
198 var k: i64 = 0
199 var hit: i64 = 1
200 while k < nl { if buf[i+k] != needle[k] { hit = 0; k = nl } else { k = k + 1 } }
201 if hit == 1 { return 1 }
202 i = i + 1
203 }
204 return 0
205}
206// copy a span out as a nul-terminated C string in caller scratch
207func pl_span_cstr(q: *u8, s: i64, e: i64, out: *u8) -> i64 {
208 var o: i64 = 0
209 var i: i64 = s
210 while i < e { out[o] = q[i]; o = o + 1; i = i + 1 }
211 out[o] = 0 as u8
212 return o
213}
214func pl_span_put(d: *u8, o: i64, q: *u8, s: i64, e: i64) -> i64 {
215 var i: i64 = s
216 while i < e { d[o] = q[i]; o = o + 1; i = i + 1 }
217 return o
218}
219
220// ---- XML text escaping. NOT pd_esc: in a <loc> a lossy escape is silent CORRUPTION ------------
221func pl_xesc(d: *u8, o: i64, q: *u8, s: i64, e: i64) -> i64 {
222 var i: i64 = s
223 while i < e {
224 let c: i64 = q[i] as i64
225 var done: i64 = 0
226 if c == PL_AMP { o = pl_cat(d, o, "&" as *u8); done = 1 }
227 if c == PL_LT { if done == 0 { o = pl_cat(d, o, "<" as *u8); done = 1 } }
228 if c == PL_GT { if done == 0 { o = pl_cat(d, o, ">" as *u8); done = 1 } }
229 if c == PL_QUOTE { if done == 0 { o = pl_cat(d, o, """ as *u8); done = 1 } }
230 if c == PL_APOS { if done == 0 { o = pl_cat(d, o, "'" as *u8); done = 1 } }
231 if done == 0 {
232 var cc: i64 = c
233 if cc < PL_SPACE { cc = PL_SPACE }
234 d[o] = cc as u8
235 o = o + 1
236 }
237 i = i + 1
238 }
239 return o
240}
241// markdown link-text escaping for llms.txt: ] and ) would break the link syntax
242func pl_mesc(d: *u8, o: i64, q: *u8, s: i64, e: i64) -> i64 {
243 var i: i64 = s
244 while i < e {
245 var c: i64 = q[i] as i64
246 if c == 93 { c = 41 }
247 if c == 91 { c = 40 }
248 if c < PL_SPACE { c = PL_SPACE }
249 d[o] = c as u8
250 o = o + 1
251 i = i + 1
252 }
253 return o
254}
255
256// ---- file io: read, and ATOMIC commit (tmp + rename; a browser never sees a torn artifact) ----
257func pl_wfile(path: *u8, buf: *u8, n: i64) -> i64 {
258 let fd: i64 = sys_openat_wr(path, PL_MODE)
259 if fd < 0 { return 0 - 1 }
260 var o: i64 = 0
261 while o < n {
262 let r: i64 = sys_write(fd, ((buf as i64) + o) as *u8, n - o)
263 if r <= 0 { sys_close(fd); return 0 - 1 }
264 o = o + r
265 }
266 sys_close(fd)
267 return 0
268}
269func pl_rfile(path: *u8, buf: *u8, cap: i64) -> i64 {
270 let fd: i64 = sys_openat_rd(path)
271 if fd < 0 { return 0 - 1 }
272 var n: i64 = 0
273 var go: i64 = 1
274 while go == 1 {
275 let r: i64 = sys_read(fd, ((buf as i64) + n) as *u8, cap - 1 - n)
276 if r <= 0 { go = 0 } else { n = n + r }
277 if n >= cap - 1 { go = 0 }
278 }
279 sys_close(fd)
280 return n
281}
282func pl_commit(path: *u8, buf: *u8, n: i64) -> i64 {
283 let tmp: *u8 = sys_mmap(PL_PATHCAP)
284 var t: i64 = pl_cat(tmp, 0, path)
285 t = pl_cat(tmp, t, ".nxpub" as *u8)
286 tmp[t] = 0 as u8
287 if pl_wfile(tmp, buf, n) != 0 { sys_munmap(tmp, PL_PATHCAP); return 0 - 1 }
288 if sys_renameat(tmp, path) != 0 { sys_munmap(tmp, PL_PATHCAP); return 0 - 1 }
289 sys_munmap(tmp, PL_PATHCAP)
290 return 0
291}
292func pl_join(out: *u8, dir: *u8, name: *u8) -> i64 {
293 var o: i64 = pl_cat(out, 0, dir)
294 out[o] = PL_SLASH as u8
295 o = o + 1
296 o = pl_cat(out, o, name)
297 out[o] = 0 as u8
298 return o
299}
300// mtime (epoch sec) of a path, -1 if unstatable. The freshness channel is a MACHINE fact, never
301// a self-reported date -- and it doubles as the EXISTS test the sitemap law requires.
302func pl_mtime_path(path: *u8) -> i64 {
303 let sb: *u8 = sys_mmap(PL_STATB)
304 if sys_fstatat(path, sb) != 0 { sys_munmap(sb, PL_STATB); return 0 - 1 }
305 let mp: *i64 = ((sb as i64) + PL_MT_OFF) as *i64
306 let v: i64 = mp[0]
307 sys_munmap(sb, PL_STATB)
308 return v
309}
310// append yyyy-mm-dd for an epoch (civil-from-days, pure integer)
311func pl_iso(d: *u8, o: i64, epoch: i64) -> i64 {
312 var z: i64 = epoch / PL_DAY
313 z = z + PL_EPOCH_SHIFT
314 let era: i64 = z / PL_ERA_DAYS
315 let doe: i64 = z - era * PL_ERA_DAYS
316 let yoe: i64 = (doe - doe/PL_QUAD_DAYS + doe/PL_CENT_DAYS - doe/(PL_ERA_DAYS - 1)) / PL_YEAR_DAYS
317 var y: i64 = yoe + era * PL_ERA_YEARS
318 let doy: i64 = doe - (PL_YEAR_DAYS*yoe + yoe/PL_LEAP4 - yoe/PL_LEAP100)
319 let mp: i64 = (PL_MP_SCALE*doy + PL_MP_BIAS) / PL_MONTH_SLOPE
320 let dd: i64 = doy - (PL_MONTH_SLOPE*mp + PL_MP_BIAS)/PL_MP_SCALE + 1
321 var mm: i64 = mp + PL_MAR_OFF
322 if mp >= PL_MP_CUT { mm = mp - PL_DEC_WRAP }
323 if mm <= PL_MP_BIAS { y = y + 1 }
324 var oo: i64 = pl_catn(d, o, y)
325 d[oo] = PL_DASH as u8
326 oo = oo + 1
327 if mm < PL_TWO_DIG { d[oo] = PL_ZERO as u8; oo = oo + 1 }
328 oo = pl_catn(d, oo, mm)
329 d[oo] = PL_DASH as u8
330 oo = oo + 1
331 if dd < PL_TWO_DIG { d[oo] = PL_ZERO as u8; oo = oo + 1 }
332 oo = pl_catn(d, oo, dd)
333 return oo
334}
335
336// ---- THE URL CONTRACT --------------------------------------------------------------------------
337// ONE canonical URL per resource, and it is the CLEAN one the edge already 301s to:
338// index.html -> / contact/index.html -> /contact/ X.html -> /X else verbatim
339// The DIRECTORY-INDEX case is not cosmetic: matching only the bare literal "index.html" emits
340// /contact/index for a nested index, which is a URL that does not exist -- a sitemap full of them
341// is worse than no sitemap. Matching the SUFFIX handles the root and every subdirectory with one rule.
342// Emitted into d at o, escaped for XML. Returns the new offset.
343func pl_clean_url(d: *u8, o: i64, q: *u8, s: i64, e: i64) -> i64 {
344 var end: i64 = e
345 if pl_ends(q, s, e - s, "index.html" as *u8) == 1 { end = e - PL_INDEX_LEN } else {
346 if pl_ends(q, s, e - s, ".html" as *u8) == 1 { end = e - PL_HTML_EXT }
347 }
348 return pl_xesc(d, o, q, s, end)
349}
350
351// ---- the SITE TABLE ----------------------------------------------------------------------------
352// resolve one site row; spans[0..1]=docroot [2..3]=prefix [4..5]=baseurl [6..7]=title [8..9]=disallow.
353// conf bytes are returned in `conf` (caller-owned) because the spans point INTO it.
354func pl_site_lookup(confpath: *u8, site: *u8, conf: *u8, spans: *i64) -> i64 {
355 let n: i64 = pl_rfile(confpath, conf, PL_CONFCAP)
356 if n <= 0 { return 0 }
357 let c: *i64 = sys_mmap(PL_SPAN) as *i64
358 var found: i64 = 0
359 var i: i64 = 0
360 while i < n {
361 let le: i64 = pl_le(conf, i, n)
362 var skip: i64 = 0
363 if le <= i { skip = 1 }
364 if skip == 0 { if conf[i] == (PL_HASH as u8) { skip = 1 } }
365 if skip == 0 {
366 if pl_col(conf, i, le, PL_S_SITE, c) == 1 {
367 if pl_lit_eq(conf, c[0], c[1], site) == 1 {
368 var ok: i64 = 1
369 if pl_col(conf, i, le, PL_S_DOCROOT, c) == 1 { spans[0] = c[0]; spans[1] = c[1] } else { ok = 0 }
370 if pl_col(conf, i, le, PL_S_PREFIX, c) == 1 { spans[2] = c[0]; spans[3] = c[1] } else { ok = 0 }
371 if pl_col(conf, i, le, PL_S_BASEURL, c) == 1 { spans[4] = c[0]; spans[5] = c[1] } else { ok = 0 }
372 if pl_col(conf, i, le, PL_S_TITLE, c) == 1 { spans[6] = c[0]; spans[7] = c[1] } else { ok = 0 }
373 spans[8] = 0
374 spans[9] = 0
375 if pl_col(conf, i, le, PL_S_DISALLOW, c) == 1 { spans[8] = c[0]; spans[9] = c[1] }
376 if ok == 1 { found = 1; i = n }
377 }
378 }
379 }
380 if found == 0 { i = le + 1 }
381 }
382 sys_munmap(c as *u8, PL_SPAN)
383 return found
384}
385
386// ---- sitemap.xml: REGISTRY-DERIVED, canonical-only ---------------------------------------------
387// A URL enters iff status=live AND the file EXISTS on disk. Everything the 2026 guidance calls a
388// conflicting discovery signal -- withdrawn, redirect, draft, debris, registered-but-absent ghosts,
389// unregistered orphans -- is excluded BY CONSTRUCTION, because the loop can only read the registry.
390// Returns url count, or -1 on emit failure. flags[] carries the honest-load report.
391func pl_sitemap(docroot: *u8, prefix: *u8, baseurl: *u8, outpath: *u8, flags: *i64) -> i64 {
392 let reg: *u8 = sys_mmap(PL_CAP)
393 let rn: i64 = sts_load_honest(prefix, reg, PL_CAP, flags)
394 let out: *u8 = sys_mmap(PL_CAP)
395 // ALL scratch hoisted above the row loop -- a per-row sys_mmap is the leak class this file exists to end
396 let cs: *i64 = sys_mmap(PL_SPAN) as *i64
397 let cp: *i64 = sys_mmap(PL_SPAN) as *i64
398 let pathb: *u8 = sys_mmap(PL_PATHCAP)
399 let relb: *u8 = sys_mmap(PL_PATHCAP)
400 var o: i64 = 0
401 o = pl_cat(out, o, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" as *u8)
402 o = pl_cat(out, o, "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n" as *u8)
403 var nurl: i64 = 0
404 var i: i64 = 0
405 while i < rn {
406 let le: i64 = pl_le(reg, i, rn)
407 var live: i64 = 0
408 if pl_col(reg, i, le, PL_C_STATUS, cs) == 1 { if pl_lit_eq(reg, cs[0], cs[1], "live" as *u8) == 1 { live = 1 } }
409 if live == 1 {
410 if pl_col(reg, i, le, PL_C_PATH, cp) == 1 {
411 pl_span_cstr(reg, cp[0], cp[1], relb)
412 pl_join(pathb, docroot, relb)
413 let mt: i64 = pl_mtime_path(pathb)
414 if mt > 0 {
415 o = pl_cat(out, o, " <url><loc>" as *u8)
416 o = pl_cat(out, o, baseurl)
417 o = pl_cat(out, o, "/" as *u8)
418 o = pl_clean_url(out, o, reg, cp[0], cp[1])
419 o = pl_cat(out, o, "</loc><lastmod>" as *u8)
420 o = pl_iso(out, o, mt)
421 o = pl_cat(out, o, "</lastmod></url>\n" as *u8)
422 nurl = nurl + 1
423 }
424 }
425 }
426 i = le + 1
427 }
428 o = pl_cat(out, o, "</urlset>\n" as *u8)
429 let rc: i64 = pl_commit(outpath, out, o)
430 sys_munmap(reg, PL_CAP)
431 sys_munmap(out, PL_CAP)
432 sys_munmap(cs as *u8, PL_SPAN)
433 sys_munmap(cp as *u8, PL_SPAN)
434 sys_munmap(pathb, PL_PATHCAP)
435 sys_munmap(relb, PL_PATHCAP)
436 if rc != 0 { return 0 - 1 }
437 return nurl
438}
439
440// ---- robots.txt: points at the sitemap, disallows are DATA (the site table), never compiled in --
441func pl_robots(baseurl: *u8, disallow: *u8, ds: i64, de: i64, outpath: *u8) -> i64 {
442 let out: *u8 = sys_mmap(PL_CONFCAP)
443 var o: i64 = 0
444 o = pl_cat(out, o, "User-agent: *\n" as *u8)
445 var n: i64 = 0
446 var i: i64 = ds
447 var seg: i64 = ds
448 while i <= de {
449 var atend: i64 = 0
450 if i == de { atend = 1 } else { if disallow[i] == (PL_COMMA as u8) { atend = 1 } }
451 if atend == 1 {
452 if i > seg {
453 o = pl_cat(out, o, "Disallow: /" as *u8)
454 o = pl_span_put(out, o, disallow, seg, i)
455 o = pl_cat(out, o, "\n" as *u8)
456 n = n + 1
457 }
458 seg = i + 1
459 }
460 i = i + 1
461 }
462 o = pl_cat(out, o, "Allow: /\n" as *u8)
463 o = pl_cat(out, o, "Sitemap: " as *u8)
464 o = pl_cat(out, o, baseurl)
465 o = pl_cat(out, o, "/sitemap.xml\n" as *u8)
466 let rc: i64 = pl_commit(outpath, out, o)
467 sys_munmap(out, PL_CONFCAP)
468 if rc != 0 { return 0 - 1 }
469 return n
470}
471
472// ---- llms.txt: the SAME registry, rendered for machine readers (llmstxt.org shape) -------------
473// HONEST NOTE: Google states llms.txt has zero effect on crawling, indexing or ranking. It is emitted
474// because it costs one derivation off a registry we already maintain and it serves non-Google agent
475// readers a curated map instead of a scrape. It is NOT claimed as an SEO control.
476// Sections come out in first-appearance order (no sort; n is site-sized).
477func pl_llms(docroot: *u8, prefix: *u8, baseurl: *u8, title: *u8, outpath: *u8) -> i64 {
478 let reg: *u8 = sys_mmap(PL_CAP)
479 let fl: *i64 = sys_mmap(PL_SPAN) as *i64
480 let rn: i64 = sts_load_honest(prefix, reg, PL_CAP, fl)
481 let out: *u8 = sys_mmap(PL_CAP)
482 let cs: *i64 = sys_mmap(PL_SPAN) as *i64
483 let cp: *i64 = sys_mmap(PL_SPAN) as *i64
484 let ct: *i64 = sys_mmap(PL_SPAN) as *i64
485 let cn: *i64 = sys_mmap(PL_SPAN) as *i64
486 let cg: *i64 = sys_mmap(PL_SPAN) as *i64
487 let ch: *i64 = sys_mmap(PL_SPAN) as *i64
488 let secs: *i64 = sys_mmap(PL_SECMAX * PL_I64B * 2) as *i64
489 let pathb: *u8 = sys_mmap(PL_PATHCAP)
490 let relb: *u8 = sys_mmap(PL_PATHCAP)
491 var o: i64 = 0
492 o = pl_cat(out, o, "# " as *u8)
493 o = pl_cat(out, o, title)
494 o = pl_cat(out, o, "\n\n> Every published surface of " as *u8)
495 o = pl_cat(out, o, baseurl)
496 o = pl_cat(out, o, ", derived from the sovereign publishing registry. Each link is a live, canonical page; nothing here is hand-curated.\n" as *u8)
497 // pass 1: distinct sections, first-appearance order
498 var nsec: i64 = 0
499 var i: i64 = 0
500 while i < rn {
501 let le: i64 = pl_le(reg, i, rn)
502 var live: i64 = 0
503 if pl_col(reg, i, le, PL_C_STATUS, cs) == 1 { if pl_lit_eq(reg, cs[0], cs[1], "live" as *u8) == 1 { live = 1 } }
504 if live == 1 {
505 if pl_col(reg, i, le, PL_C_SECTION, cg) == 1 {
506 var seen: i64 = 0
507 var k: i64 = 0
508 while k < nsec {
509 if pl_span_eq(reg, secs[k+k], secs[k+k+1], reg, cg[0], cg[1]) == 1 { seen = 1; k = nsec } else { k = k + 1 }
510 }
511 if seen == 0 {
512 if nsec < PL_SECMAX {
513 secs[nsec+nsec] = cg[0]
514 secs[nsec+nsec+1] = cg[1]
515 nsec = nsec + 1
516 }
517 }
518 }
519 }
520 i = le + 1
521 }
522 // pass 2: one block per section
523 var nlink: i64 = 0
524 var s: i64 = 0
525 while s < nsec {
526 o = pl_cat(out, o, "\n## " as *u8)
527 o = pl_span_put(out, o, reg, secs[s+s], secs[s+s+1])
528 o = pl_cat(out, o, "\n\n" as *u8)
529 var j: i64 = 0
530 while j < rn {
531 let le2: i64 = pl_le(reg, j, rn)
532 var live2: i64 = 0
533 if pl_col(reg, j, le2, PL_C_STATUS, cs) == 1 { if pl_lit_eq(reg, cs[0], cs[1], "live" as *u8) == 1 { live2 = 1 } }
534 if live2 == 1 {
535 var same: i64 = 0
536 if pl_col(reg, j, le2, PL_C_SECTION, ch) == 1 {
537 if pl_span_eq(reg, ch[0], ch[1], reg, secs[s+s], secs[s+s+1]) == 1 { same = 1 }
538 }
539 if same == 1 {
540 if pl_col(reg, j, le2, PL_C_PATH, cp) == 1 {
541 pl_span_cstr(reg, cp[0], cp[1], relb)
542 pl_join(pathb, docroot, relb)
543 if pl_mtime_path(pathb) > 0 {
544 o = pl_cat(out, o, "- [" as *u8)
545 if pl_col(reg, j, le2, PL_C_TITLE, ct) == 1 { o = pl_mesc(out, o, reg, ct[0], ct[1]) }
546 o = pl_cat(out, o, "](" as *u8)
547 o = pl_cat(out, o, baseurl)
548 o = pl_cat(out, o, "/" as *u8)
549 o = pl_clean_url(out, o, reg, cp[0], cp[1])
550 o = pl_cat(out, o, ")" as *u8)
551 if pl_col(reg, j, le2, PL_C_NOTE, cn) == 1 {
552 if cn[1] > cn[0] {
553 o = pl_cat(out, o, ": " as *u8)
554 o = pl_mesc(out, o, reg, cn[0], cn[1])
555 }
556 }
557 o = pl_cat(out, o, "\n" as *u8)
558 nlink = nlink + 1
559 }
560 }
561 }
562 }
563 j = le2 + 1
564 }
565 s = s + 1
566 }
567 let rc: i64 = pl_commit(outpath, out, o)
568 sys_munmap(reg, PL_CAP)
569 sys_munmap(out, PL_CAP)
570 sys_munmap(pathb, PL_PATHCAP)
571 sys_munmap(relb, PL_PATHCAP)
572 if rc != 0 { return 0 - 1 }
573 return nlink
574}
575
576// ---- docroot scan (ONE level) ------------------------------------------------------------------
577// Names land in `arena` NUL-terminated, dirs carrying a trailing '/', offsets in offs[].
578// ONE level on purpose: a publishing registry is the list of PUBLISHED SURFACES, not of every file
579// on disk. nishifamily's docroot holds 70k files, almost all generated leaf artifacts under
580// compare/atlas/... -- adopting those would turn the SSOT into a file listing and destroy its meaning.
581// envp[0]=dropped-beyond-cap envp[1]=.prev rollback artifacts seen.
582func pl_scan(docroot: *u8, arena: *u8, arena_cap: i64, offs: *i64, maxn: i64, envp: *i64) -> i64 {
583 envp[0] = 0
584 envp[1] = 0
585 let fd: i64 = sys_openat_rd(docroot)
586 if fd < 0 { return 0 - 1 }
587 let db: *u8 = sys_mmap(PL_DBUF)
588 var cnt: i64 = 0
589 var ao: i64 = 0
590 var nr: i64 = 1
591 while nr > 0 {
592 nr = sys_getdents64(fd, db, PL_DBUF)
593 if nr > 0 {
594 var o: i64 = 0
595 while o < nr {
596 let rl: i64 = (db[o+PL_DE_RLO] as i64) + ((db[o+PL_DE_RHI] as i64) * PL_BYTE)
597 if rl <= 0 { o = nr } else {
598 var nl: i64 = 0
599 while db[o+PL_DE_NAME+nl] != (0 as u8) { nl = nl + 1 }
600 var dot: i64 = 0
601 if nl >= 1 { if db[o+PL_DE_NAME] == (PL_DOT as u8) { dot = 1 } }
602 if dot == 0 {
603 if pl_ends(db, o+PL_DE_NAME, nl, ".prev" as *u8) == 1 { envp[1] = envp[1] + 1 }
604 if cnt >= maxn { envp[0] = envp[0] + 1 } else {
605 if ao + nl + PL_NAME_PAD >= arena_cap { envp[0] = envp[0] + 1 } else {
606 offs[cnt] = ao
607 var t: i64 = 0
608 while t < nl { arena[ao] = db[o+PL_DE_NAME+t]; ao = ao + 1; t = t + 1 }
609 if db[o+PL_DE_TYPE] == (PL_DT_DIR as u8) { arena[ao] = PL_SLASH as u8; ao = ao + 1 }
610 arena[ao] = 0 as u8
611 ao = ao + 1
612 cnt = cnt + 1
613 }
614 }
615 }
616 o = o + rl
617 }
618 }
619 }
620 }
621 sys_close(fd)
622 sys_munmap(db, PL_DBUF)
623 return cnt
624}
625
626// one refusal voice for every tree-scan cap: name the CAP, name the COUNT, commit NOTHING.
627// Returns -3 so the driver can tell a cap refusal from -1 (unreadable docroot / seed fail)
628// and -2 (lossy plane). Same discipline as the lossy-load guard in pl_adopt: refuse rather
629// than bake a loss, and never stamp the wrong cause over the right one.
630func pl_adopt_refuse(cap: *u8, n: i64) -> i64 {
631 pl_werr("PUB-ADOPT REFUSED: " as *u8)
632 pl_werr(cap)
633 pl_werr(" (affected=" as *u8)
634 let nb: *u8 = sys_mmap(PL_NUMCAP)
635 var no: i64 = pl_catn(nb, 0, n)
636 nb[no] = 0 as u8
637 pl_werr(nb)
638 sys_munmap(nb, PL_NUMCAP)
639 pl_werr("). A truncated adopt would register PART of the tree and the gateway would refuse the rest forever. NOTHING COMMITTED.\n" as *u8)
640 return 0 - 3
641}
642
643// ---- docroot TREE scan (2026-08-01) -- BOUNDED walk via an explicit worklist -------------------
644// pl_scan above stays ONE level for the desk's top-level disk counts. ADOPTION cannot use it:
645// the one-level walk detects subdirectories and SKIPS them, so a page under code/ or world/
646// could NEVER enter the registry, and the fail-closed gateway therefore refused every nested
647// page FOREVER while it was live and serving (debt 1785614931). This walk descends, emitting
648// each FILE under its full docroot-relative path ("code/research_map.html") -- the exact string
649// pl_reg_has and pl_check match on. Directories are walked, never emitted: a registry row is a
650// published surface, not a folder. BOUNDED EVERYWHERE; the envelope names each cap SEPARATELY
651// so the caller can refuse with the RIGHT cause (the two-causes lesson: refusing is right either
652// way, but naming the wrong cause sends the operator to the wrong fix):
653// envp[0] = file entries beyond maxn / the name arena (ENTRY cap)
654// envp[1] = .prev rollback artifacts seen (same meaning as pl_scan)
655// envp[2] = directories beyond PL_MAXDEPTH (DEPTH cap)
656// envp[3] = directories beyond the worklist (DIR cap)
657// envp[4] = relpaths too long to join under PL_PATHCAP (PATH cap)
658// envp[5] = subdirectories that could not be opened (UNREADABLE)
659// envp must be PL_ENVB bytes. Returns file count, or -1 iff the docroot itself will not open.
660func pl_scan_tree(docroot: *u8, arena: *u8, arena_cap: i64, offs: *i64, maxn: i64, envp: *i64) -> i64 {
661 var z: i64 = 0
662 while z < 6 { envp[z] = 0; z = z + 1 }
663 let dlen: i64 = pl_vlen(docroot)
664 let db: *u8 = sys_mmap(PL_DBUF)
665 let dira: *u8 = sys_mmap(PL_NAMES)
666 let diro: *i64 = sys_mmap(PL_MAXN * PL_I64B) as *i64
667 let dird: *i64 = sys_mmap(PL_MAXN * PL_I64B) as *i64
668 let pathb: *u8 = sys_mmap(PL_PATHCAP)
669 // worklist seed: the docroot itself -- empty relpath at arena offset 0, depth 0
670 diro[0] = 0
671 dird[0] = 0
672 dira[0] = 0 as u8
673 var dhead: i64 = 0
674 var dtail: i64 = 1
675 var dao: i64 = 1
676 var cnt: i64 = 0
677 var ao: i64 = 0
678 var rootfail: i64 = 0
679 while dhead < dtail {
680 let roff: i64 = diro[dhead]
681 let rdep: i64 = dird[dhead]
682 dhead = dhead + 1
683 let rlen: i64 = pl_vlen(((dira as i64) + roff) as *u8)
684 var fd: i64 = 0 - 1
685 if rlen == 0 { fd = sys_openat_rd(docroot) } else {
686 pl_join(pathb, docroot, ((dira as i64) + roff) as *u8)
687 fd = sys_openat_rd(pathb)
688 }
689 if fd < 0 {
690 if rlen == 0 { rootfail = 1 } else { envp[5] = envp[5] + 1 }
691 } else {
692 var nr: i64 = 1
693 while nr > 0 {
694 nr = sys_getdents64(fd, db, PL_DBUF)
695 if nr > 0 {
696 var o: i64 = 0
697 while o < nr {
698 let rl: i64 = (db[o+PL_DE_RLO] as i64) + ((db[o+PL_DE_RHI] as i64) * PL_BYTE)
699 if rl <= 0 { o = nr } else {
700 var nl: i64 = 0
701 while db[o+PL_DE_NAME+nl] != (0 as u8) { nl = nl + 1 }
702 var dot: i64 = 0
703 if nl >= 1 { if db[o+PL_DE_NAME] == (PL_DOT as u8) { dot = 1 } }
704 if dot == 0 {
705 var pre: i64 = 0
706 if rlen > 0 { pre = rlen + 1 }
707 let flen: i64 = pre + nl
708 if db[o+PL_DE_TYPE] == (PL_DT_DIR as u8) {
709 if rdep + 1 > PL_MAXDEPTH { envp[2] = envp[2] + 1 } else {
710 if dtail >= maxn { envp[3] = envp[3] + 1 } else {
711 if dao + flen + PL_NAME_PAD >= PL_NAMES { envp[3] = envp[3] + 1 } else {
712 if dlen + 1 + flen + PL_NAME_PAD >= PL_PATHCAP { envp[4] = envp[4] + 1 } else {
713 diro[dtail] = dao
714 dird[dtail] = rdep + 1
715 if rlen > 0 {
716 var q: i64 = 0
717 while q < rlen { dira[dao] = dira[roff + q]; dao = dao + 1; q = q + 1 }
718 dira[dao] = PL_SLASH as u8
719 dao = dao + 1
720 }
721 var t: i64 = 0
722 while t < nl { dira[dao] = db[o+PL_DE_NAME+t]; dao = dao + 1; t = t + 1 }
723 dira[dao] = 0 as u8
724 dao = dao + 1
725 dtail = dtail + 1
726 }
727 }
728 }
729 }
730 } else {
731 if pl_ends(db, o+PL_DE_NAME, nl, ".prev" as *u8) == 1 { envp[1] = envp[1] + 1 }
732 if cnt >= maxn { envp[0] = envp[0] + 1 } else {
733 if ao + flen + PL_NAME_PAD >= arena_cap { envp[0] = envp[0] + 1 } else {
734 if dlen + 1 + flen + PL_NAME_PAD >= PL_PATHCAP { envp[4] = envp[4] + 1 } else {
735 offs[cnt] = ao
736 if rlen > 0 {
737 var q2: i64 = 0
738 while q2 < rlen { arena[ao] = dira[roff + q2]; ao = ao + 1; q2 = q2 + 1 }
739 arena[ao] = PL_SLASH as u8
740 ao = ao + 1
741 }
742 var t2: i64 = 0
743 while t2 < nl { arena[ao] = db[o+PL_DE_NAME+t2]; ao = ao + 1; t2 = t2 + 1 }
744 arena[ao] = 0 as u8
745 ao = ao + 1
746 cnt = cnt + 1
747 }
748 }
749 }
750 }
751 }
752 o = o + rl
753 }
754 }
755 }
756 }
757 sys_close(fd)
758 }
759 }
760 sys_munmap(db, PL_DBUF)
761 sys_munmap(dira, PL_NAMES)
762 sys_munmap(diro as *u8, PL_MAXN * PL_I64B)
763 sys_munmap(dird as *u8, PL_MAXN * PL_I64B)
764 sys_munmap(pathb, PL_PATHCAP)
765 if rootfail == 1 { return 0 - 1 }
766 return cnt
767}
768// is this arena name already carried by some registry row's path column?
769func pl_reg_has(reg: *u8, rn: i64, arena: *u8, off: i64) -> i64 {
770 let c: *i64 = sys_mmap(PL_SPAN) as *i64
771 let nl: i64 = pl_vlen(((arena as i64) + off) as *u8)
772 var hit: i64 = 0
773 var i: i64 = 0
774 while i < rn {
775 let le: i64 = pl_le(reg, i, rn)
776 if pl_col(reg, i, le, PL_C_PATH, c) == 1 {
777 if pl_span_eq(reg, c[0], c[1], arena, off, off + nl) == 1 { hit = 1; i = rn } else { i = le + 1 }
778 } else { i = le + 1 }
779 }
780 sys_munmap(c as *u8, PL_SPAN)
781 return hit
782}
783// classify an unregistered docroot entry into the status vocabulary. Rollback/backup/temp artifacts
784// are DEBRIS (they must never be indexed and should never have been reachable); .html is a live page;
785// anything else is an asset (served, never in the sitemap).
786func pl_find_at(q: *u8, off: i64, n: i64, needle: *u8) -> i64 {
787 let nl: i64 = pl_vlen(needle)
788 if nl == 0 { return 0 }
789 var i: i64 = 0
790 while i + nl <= n {
791 var k: i64 = 0
792 var hit: i64 = 1
793 while k < nl { if q[off+i+k] != needle[k] { hit = 0; k = nl } else { k = k + 1 } }
794 if hit == 1 { return 1 }
795 i = i + 1
796 }
797 return 0
798}
799// classify an unregistered docroot entry into the status vocabulary. Rollback/backup/temp artifacts
800// are DEBRIS (they must never be indexed and should never have been reachable); .html is a live page;
801// anything else is an asset (served, never in the sitemap).
802func pl_classify(arena: *u8, off: i64) -> *u8 {
803 let n: i64 = pl_vlen(((arena as i64) + off) as *u8)
804 if pl_ends(arena, off, n, ".prev" as *u8) == 1 { return "debris" as *u8 }
805 if pl_ends(arena, off, n, ".nxtmp" as *u8) == 1 { return "debris" as *u8 }
806 if pl_find_at(arena, off, n, ".bak" as *u8) == 1 { return "debris" as *u8 }
807 if pl_find_at(arena, off, n, ".nxw" as *u8) == 1 { return "debris" as *u8 }
808 // DRAFT, NOT LIVE, and this is the whole safety of the migration. Adoption exists to make the
809 // registry describe reality; it must not also DECIDE that 54 never-reviewed pages should be
810 // handed to search engines. draft = registered (the gateway admits republishing it) but absent
811 // from sitemap.xml and llms.txt until an owner promotes the row to live. The 2026 guidance is
812 // explicit that thin or unreviewed pages in a sitemap are a conflicting discovery signal, and
813 // an adoption tool that silently indexed a docroot would be exactly that, at scale.
814 if pl_ends(arena, off, n, ".html" as *u8) == 1 { return "draft" as *u8 }
815 return "asset" as *u8
816}
817
818// ADOPT: bring every UNREGISTERED docroot entry -- the WHOLE TREE, depth-bounded -- into the
819// registry so the SSOT describes
820// reality before anything starts enforcing against it. Migration order matters: switching on the
821// gateway first would refuse pages that are already live and legitimate. IDEMPOTENT (rule 10) --
822// an entry already carried by a row is skipped, so running this twice adds nothing.
823// Returns rows ADDED, or -1. counts[0]=draft counts[1]=asset counts[2]=debris counts[3]=skipped.
824func pl_adopt(docroot: *u8, prefix: *u8, counts: *i64) -> i64 {
825 let reg: *u8 = sys_mmap(PL_CAP)
826 let fl: *i64 = sys_mmap(PL_SPAN) as *i64
827 var rn: i64 = sts_load_honest(prefix, reg, PL_CAP, fl)
828 // ⚠REFUSE TO BAKE A LOSS. sts_seed rewrites the WHOLE plane from the buffer we just loaded, so
829 // if the load came back SHORT of the declared q:n -- rows a previous writer dropped -- committing
830 // would make that loss the next generation's truth and it becomes unrecoverable. Learned the hard
831 // way 2026-07-30: nx_debt refused this exact commit on the debt plane WHILE THIS CODE WAS BEING
832 // WRITTEN, and this function had no such guard. Any read-modify-write over a plane needs it.
833 // ⚠TWO CAUSES, OPPOSITE REMEDIES (sibling law, same day): "loaded fewer rows than declared" means
834 // EITHER real row loss (never rewrite -- you would bake it) OR that the READER hit its own buffer cap
835 // (the plane simply outgrew PL_CAP -- the data is fine, raise the cap or archive). Refusing is right
836 // either way, but telling the operator the WRONG cause sends them to the wrong fix, so name which one:
837 // a load that filled the buffer is a reader-cap verdict, not a loss verdict.
838 if fl[0] > fl[1] {
839 if rn >= PL_CAP - 1 {
840 pl_werr("PUB-ADOPT REFUSED: READER CAP, not data loss -- the registry outgrew PL_CAP. Raise it or archive rows; do NOT compact. declared " as *u8)
841 }
842 pl_werr("PUB-ADOPT REFUSED: lossy load (declared " as *u8)
843 let nb: *u8 = sys_mmap(PL_NUMCAP)
844 var no: i64 = pl_catn(nb, 0, fl[0])
845 nb[no] = 0 as u8
846 pl_werr(nb)
847 no = pl_catn(nb, 0, fl[1])
848 nb[no] = 0 as u8
849 pl_werr(" rows, reached " as *u8)
850 pl_werr(nb)
851 pl_werr("). Rewriting the plane would BAKE the loss. NOTHING COMMITTED.\n" as *u8)
852 return 0 - 2
853 }
854 let arena: *u8 = sys_mmap(PL_NAMES)
855 let offs: *i64 = sys_mmap(PL_MAXN * PL_I64B) as *i64
856 let envp: *i64 = sys_mmap(PL_ENVB) as *i64
857 // THE TREE, not one level (2026-08-01, debt 1785614931): pl_scan detects subdirectories and
858 // SKIPS them, so no nested page could ever enter the registry and the fail-closed gateway
859 // refused code/, world/, compare/ pages FOREVER -- adopt reported success and fixed nothing,
860 // which is why publishing kept escaping through direct docroot writes. The tree walk emits
861 // FULL relpaths, which are exactly the strings pl_reg_has and pl_check match on.
862 let cnt: i64 = pl_scan_tree(docroot, arena, PL_NAMES, offs, PL_MAXN, envp)
863 if cnt < 0 { return 0 - 1 }
864 // ⚠REFUSE, NEVER TRUNCATE -- and NAME the cap, for the same reason the lossy-load guard
865 // below names its cause: the right refusal with the wrong cause sends the operator to the
866 // wrong fix. A partial walk would register PART of the tree and the gateway would refuse
867 // the rest forever behind a green adopt log -- the exact defect this walk ends.
868 if envp[0] > 0 { return pl_adopt_refuse("ENTRY CAP -- more files than PL_MAXN/PL_NAMES carries; raise them DELIBERATELY or archive docroot debris first" as *u8, envp[0]) }
869 if envp[2] > 0 { return pl_adopt_refuse("DEPTH CAP -- tree deeper than PL_MAXDEPTH; a deeper publishing tree is a decision, not a default" as *u8, envp[2]) }
870 if envp[3] > 0 { return pl_adopt_refuse("DIR CAP -- more directories than the worklist carries (PL_MAXN/PL_NAMES)" as *u8, envp[3]) }
871 if envp[4] > 0 { return pl_adopt_refuse("PATH CAP -- a relpath will not fit PL_PATHCAP once joined to the docroot" as *u8, envp[4]) }
872 if envp[5] > 0 { return pl_adopt_refuse("UNREADABLE SUBDIRECTORY -- adopting around it would silently drop its pages" as *u8, envp[5]) }
873 counts[0] = 0
874 counts[1] = 0
875 counts[2] = 0
876 counts[3] = 0
877 // next id = highest existing numeric id + 1 (ids stay stable; rule 19)
878 let c0: *i64 = sys_mmap(PL_SPAN) as *i64
879 var maxid: i64 = 0
880 var i: i64 = 0
881 while i < rn {
882 let le: i64 = pl_le(reg, i, rn)
883 if pl_col(reg, i, le, PL_C_ID, c0) == 1 {
884 var v: i64 = 0
885 var k: i64 = c0[0]
886 while k < c0[1] {
887 let ch: i64 = reg[k] as i64
888 if ch >= PL_ZERO { if ch <= PL_ZERO + 9 { v = v * 10 + (ch - PL_ZERO) } }
889 k = k + 1
890 }
891 if v > maxid { maxid = v }
892 }
893 i = le + 1
894 }
895 var o: i64 = rn
896 var added: i64 = 0
897 var j: i64 = 0
898 while j < cnt {
899 let off: i64 = offs[j]
900 let nl: i64 = pl_vlen(((arena as i64) + off) as *u8)
901 var isdir: i64 = 0
902 if nl > 0 { if arena[off + nl - 1] == (PL_SLASH as u8) { isdir = 1 } }
903 if isdir == 0 {
904 if pl_reg_has(reg, rn, arena, off) == 1 { counts[3] = counts[3] + 1 } else {
905 let st: *u8 = pl_classify(arena, off)
906 maxid = maxid + 1
907 o = pl_catn(reg, o, maxid)
908 reg[o] = PL_TAB as u8
909 o = o + 1
910 o = pl_span_put(reg, o, arena, off, off + nl)
911 o = pl_cat(reg, o, "\tunassigned\t" as *u8)
912 o = pl_cat(reg, o, st)
913 o = pl_cat(reg, o, "\tUnsorted\t" as *u8)
914 o = pl_span_put(reg, o, arena, off, off + nl)
915 o = pl_cat(reg, o, "\tadopted from the docroot; title/section/owner need a human pass\n" as *u8)
916 added = added + 1
917 if pl_lit_eq(st, 0, pl_vlen(st), "draft" as *u8) == 1 { counts[0] = counts[0] + 1 }
918 if pl_lit_eq(st, 0, pl_vlen(st), "asset" as *u8) == 1 { counts[1] = counts[1] + 1 }
919 if pl_lit_eq(st, 0, pl_vlen(st), "debris" as *u8) == 1 { counts[2] = counts[2] + 1 }
920 }
921 }
922 j = j + 1
923 }
924 if added == 0 { return 0 }
925 let seeded: i64 = sts_seed(prefix, reg, o)
926 if seeded < 0 { return 0 - 1 }
927 return added
928}
929
930// ---- SELF-REGISTRATION: the emitter must declare what it wrote ----------------------------------
931// THE DEFECT THIS CLOSES (found by the desk I built, on my own work): emit writes sitemap.xml,
932// robots.txt and llms.txt INTO the docroot and never registered them, so the publishing plane's own
933// output showed up as ORPHANS -- files the site serves that its registry does not describe. A plane
934// whose rule is 'nothing reaches the docroot unregistered' must not be the thing breaking that rule.
935// IDEMPOTENT (rule 10): a path already carried by any row is left completely alone, so re-emitting
936// never duplicates and never overwrites a human's curated title/owner/status.
937// Returns 1 if a row was added, 0 if already present, -1 refused, -2 lossy plane.
938func pl_register_asset(prefix: *u8, relpath: *u8, note: *u8) -> i64 {
939 let reg: *u8 = sys_mmap(PL_CAP)
940 let fl: *i64 = sys_mmap(PL_SPAN) as *i64
941 let rn: i64 = sts_load_honest(prefix, reg, PL_CAP, fl)
942 // same read-modify-write guard as pl_adopt: sts_seed rewrites the WHOLE plane, so a short load
943 // would bake the loss. Refuse rather than commit a truncated generation.
944 if fl[0] > fl[1] { sys_munmap(reg, PL_CAP); return 0 - 2 }
945 let cp: *i64 = sys_mmap(PL_SPAN) as *i64
946 let c0: *i64 = sys_mmap(PL_SPAN) as *i64
947 var maxid: i64 = 0
948 var found: i64 = 0
949 var i: i64 = 0
950 while i < rn {
951 let le: i64 = pl_le(reg, i, rn)
952 if pl_col(reg, i, le, PL_C_PATH, cp) == 1 {
953 if pl_lit_eq(reg, cp[0], cp[1], relpath) == 1 { found = 1 }
954 }
955 if pl_col(reg, i, le, PL_C_ID, c0) == 1 {
956 var v: i64 = 0
957 var k: i64 = c0[0]
958 while k < c0[1] {
959 let ch: i64 = reg[k] as i64
960 if ch >= PL_ZERO { if ch <= PL_ZERO + 9 { v = v * 10 + (ch - PL_ZERO) } }
961 k = k + 1
962 }
963 if v > maxid { maxid = v }
964 }
965 i = le + 1
966 }
967 if found == 1 { sys_munmap(reg, PL_CAP); return 0 }
968 var o: i64 = rn
969 o = pl_catn(reg, o, maxid + 1)
970 reg[o] = PL_TAB as u8
971 o = o + 1
972 o = pl_cat(reg, o, relpath)
973 o = pl_cat(reg, o, "\tpublishing-plane\tasset\tDiscovery\t" as *u8)
974 o = pl_cat(reg, o, relpath)
975 reg[o] = PL_TAB as u8
976 o = o + 1
977 o = pl_cat(reg, o, note)
978 reg[o] = PL_NL as u8
979 o = o + 1
980 let seeded: i64 = sts_seed(prefix, reg, o)
981 sys_munmap(reg, PL_CAP)
982 if seeded < 0 { return 0 - 1 }
983 return 1
984}
985
986// ---- MANAGEMENT COUNTS: the numbers the publishing desk renders ---------------------------------
987// Every figure on the board comes from HERE -- the registry plane and the docroot walk -- so the desk
988// cannot drift from reality the way a hand-maintained status page does. c[] = live asset draft
989// withdrawn redirect debris rows_total.
990func pl_status_counts(prefix: *u8, c: *i64) -> i64 {
991 var z: i64 = 0
992 while z < 7 { c[z] = 0; z = z + 1 }
993 let reg: *u8 = sys_mmap(PL_CAP)
994 let fl: *i64 = sys_mmap(PL_SPAN) as *i64
995 let rn: i64 = sts_load_honest(prefix, reg, PL_CAP, fl)
996 let cs: *i64 = sys_mmap(PL_SPAN) as *i64
997 var i: i64 = 0
998 while i < rn {
999 let le: i64 = pl_le(reg, i, rn)
1000 if le > i {
1001 c[6] = c[6] + 1
1002 if pl_col(reg, i, le, PL_C_STATUS, cs) == 1 {
1003 if pl_lit_eq(reg, cs[0], cs[1], "live" as *u8) == 1 { c[0] = c[0] + 1 }
1004 if pl_lit_eq(reg, cs[0], cs[1], "asset" as *u8) == 1 { c[1] = c[1] + 1 }
1005 if pl_lit_eq(reg, cs[0], cs[1], "draft" as *u8) == 1 { c[2] = c[2] + 1 }
1006 if pl_lit_eq(reg, cs[0], cs[1], "withdrawn" as *u8) == 1 { c[3] = c[3] + 1 }
1007 if pl_lit_eq(reg, cs[0], cs[1], "redirect" as *u8) == 1 { c[4] = c[4] + 1 }
1008 if pl_lit_eq(reg, cs[0], cs[1], "debris" as *u8) == 1 { c[5] = c[5] + 1 }
1009 }
1010 }
1011 i = le + 1
1012 }
1013 sys_munmap(reg, PL_CAP)
1014 sys_munmap(cs as *u8, PL_SPAN)
1015 return rn
1016}
1017// Is any registry path INSIDE this directory? A one-level docroot walk yields "contact/" while the
1018// registry holds "contact/index.html", so an exact-match orphan test calls a directory full of
1019// registered pages an ORPHAN -- and the same mismatch makes a census report that row as MISSING while
1020// the file plainly exists and is in the sitemap. Neither side is wrong; the COMPARISON was. A directory
1021// counts as registered when at least one row lives under it.
1022func pl_reg_has_prefix(reg: *u8, rn: i64, arena: *u8, off: i64) -> i64 {
1023 let c: *i64 = sys_mmap(PL_SPAN) as *i64
1024 let nl: i64 = pl_vlen(((arena as i64) + off) as *u8)
1025 var hit: i64 = 0
1026 var i: i64 = 0
1027 while i < rn {
1028 let le: i64 = pl_le(reg, i, rn)
1029 if pl_col(reg, i, le, PL_C_PATH, c) == 1 {
1030 if c[1] - c[0] > nl {
1031 var k: i64 = 0
1032 var same: i64 = 1
1033 while k < nl { if reg[c[0] + k] != arena[off + k] { same = 0; k = nl } else { k = k + 1 } }
1034 if same == 1 { hit = 1; i = rn }
1035 }
1036 }
1037 if hit == 0 { i = le + 1 }
1038 }
1039 sys_munmap(c as *u8, PL_SPAN)
1040 return hit
1041}
1042// d[] = pages dirs others prev_artifacts orphans. ORPHAN = on disk, in NO registry row -- the exact
1043// number that made the operator call publishing "random and arbitrary", so the desk states it plainly.
1044func pl_disk_counts(docroot: *u8, prefix: *u8, d: *i64) -> i64 {
1045 var z: i64 = 0
1046 while z < 5 { d[z] = 0; z = z + 1 }
1047 let arena: *u8 = sys_mmap(PL_NAMES)
1048 let offs: *i64 = sys_mmap(PL_MAXN * PL_I64B) as *i64
1049 let envp: *i64 = sys_mmap(PL_SPAN) as *i64
1050 let cnt: i64 = pl_scan(docroot, arena, PL_NAMES, offs, PL_MAXN, envp)
1051 if cnt < 0 { return 0 - 1 }
1052 d[3] = envp[1]
1053 let reg: *u8 = sys_mmap(PL_CAP)
1054 let fl: *i64 = sys_mmap(PL_SPAN) as *i64
1055 let rn: i64 = sts_load_honest(prefix, reg, PL_CAP, fl)
1056 var k: i64 = 0
1057 while k < cnt {
1058 let off: i64 = offs[k]
1059 let nl: i64 = pl_vlen(((arena as i64) + off) as *u8)
1060 var isdir: i64 = 0
1061 if nl > 0 { if arena[off + nl - 1] == (PL_SLASH as u8) { isdir = 1 } }
1062 if isdir == 1 { d[1] = d[1] + 1 } else {
1063 if pl_ends(arena, off, nl, ".html" as *u8) == 1 { d[0] = d[0] + 1 } else { d[2] = d[2] + 1 }
1064 }
1065 var regd: i64 = pl_reg_has(reg, rn, arena, off)
1066 if regd == 0 { if isdir == 1 { regd = pl_reg_has_prefix(reg, rn, arena, off) } }
1067 if regd == 0 { d[4] = d[4] + 1 }
1068 k = k + 1
1069 }
1070 sys_munmap(reg, PL_CAP)
1071 sys_munmap(arena, PL_NAMES)
1072 return cnt
1073}
1074
1075// ---- THE GATEWAY -------------------------------------------------------------------------------
1076// A relpath may be published ONLY if the registry carries it with a publishable status. This is the
1077// half that makes "random and arbitrary" impossible rather than merely visible: pd_check states the
1078// same rule but its own header records that enforcement inside the publish organ was never wired.
1079// Returns PL_OK (0) or PL_REFUSED (3). Fail-CLOSED: an unreadable/empty registry refuses everything.
1080func pl_check(prefix: *u8, relpath: *u8) -> i64 {
1081 let reg: *u8 = sys_mmap(PL_CAP)
1082 let fl: *i64 = sys_mmap(PL_SPAN) as *i64
1083 let rn: i64 = sts_load_honest(prefix, reg, PL_CAP, fl)
1084 let cs: *i64 = sys_mmap(PL_SPAN) as *i64
1085 let cp: *i64 = sys_mmap(PL_SPAN) as *i64
1086 var rc: i64 = PL_REFUSED
1087 var i: i64 = 0
1088 while i < rn {
1089 let le: i64 = pl_le(reg, i, rn)
1090 if pl_col(reg, i, le, PL_C_PATH, cp) == 1 {
1091 if pl_lit_eq(reg, cp[0], cp[1], relpath) == 1 {
1092 if pl_col(reg, i, le, PL_C_STATUS, cs) == 1 {
1093 if pl_lit_eq(reg, cs[0], cs[1], "live" as *u8) == 1 { rc = PL_OK }
1094 if pl_lit_eq(reg, cs[0], cs[1], "asset" as *u8) == 1 { rc = PL_OK }
1095 if pl_lit_eq(reg, cs[0], cs[1], "draft" as *u8) == 1 { rc = PL_OK }
1096 }
1097 i = rn
1098 } else { i = le + 1 }
1099 } else { i = le + 1 }
1100 }
1101 sys_munmap(reg, PL_CAP)
1102 sys_munmap(cs as *u8, PL_SPAN)
1103 sys_munmap(cp as *u8, PL_SPAN)
1104 return rc
1105}