code wiki / _hdl_build / nx_ims_monitor.nx

nx_ims_monitor.nx source

↩ module page · 510 lines · 22409 B

1// nx_ims_monitor.nx -- IMS arc rung A1: READ-ONLY orphan + link-rot MONITOR. 2// 3// PURPOSE (Information Management System health, sovereign): 4// Given the published page set and each page's OUTBOUND internal links, find 5// ORPHANS = published pages with 0 INBOUND internal links, excluding the 6// designated front-door root(s) (start.html is the root -- it is 7// expected to be reached directly, never linked-to). 8// DEAD_LINKS = an outbound internal link whose target page is NOT in the 9// published set (internal link rot / a 404 waiting to happen). 10// 11// READ-ONLY BY CONSTRUCTION: 12// This organ NEVER opens a corpus file for write, NEVER deletes, NEVER renames. 13// It only READS (the live path uses sys_read_file) and computes in memory. The 14// gate proves the pure logic with in-memory fixtures (no I/O at all). 15// 16// COMPOSES (DRY -- zero new corpus/link substrate invented): 17// nx_wiki_index_builder NxWikiDocStore: the page corpus (rowid -> title/url/ 18// body) + lookup. Fixtures + live both populate this. 19// nx_wiki_backlinks nx_wiki_forward_links (the ONE shared [[wikilink]] 20// extractor) + nx_wiki_backlink_slug_matches (the ONE 21// shared slug resolver) -- so the monitor's notion of 22// "a link" agrees byte-for-byte with the backlinks 23// panel and the graph edge builder. No 4th scanner. 24// nx_syscalls sys_mmap scratch. 25// 26// LINK MODEL -- the monitor recognises BOTH internal-link conventions a Nishi 27// page carries, and folds them into ONE outbound-target list per page: 28// (a) [[wikilink]] -- markdown-body cross-refs (reused extractor). 29// (b) href="/wiki/<x>.html" -- served-HTML anchors (the live corpus is HTML). 30// Both are normalised to a bare slug (the "/wiki/" prefix and a trailing 31// ".html" are stripped) so a body link [[econsim]], an href "/wiki/econsim", 32// and a stored url "/wiki/econsim.html" all resolve to the same node. [[cite:]] 33// refs are skipped by the reused extractor (they are sources, not page links). 34// 35// Hygiene: M1 out-params not *T returns; M3 every while has a hard iter cap; 36// M5 every buffer index bounded; M6 no pretend stubs; M7 named constants; 37// M8 verdicts propagated. ("loop" is a reserved word -- never used as an ident.) 38// 39// Status: V1 (IMS arc A1). 2026-06-17. license_tier: ORIGINAL 40import "nx_syscalls.nx" 41import "nx_wiki_index_builder.nx" 42import "nx_wiki_backlinks.nx" 43 44// ===== Sealed verdict surface (codes 2720-2739) ============================== 45const NX_IMS_OK: i64 = 0 46const NX_IMS_BAD_INPUT: i64 = 2720 47const NX_IMS_OVERFLOW: i64 = 2721 48const NX_IMS_LOOP_BUDGET: i64 = 2722 49 50// ===== Named sizing constants (M7) =========================================== 51const NX_IMS_MAX_PAGES: i64 = 1000 // mirrors doc-store docs cap 52const NX_IMS_MAX_LINKS_PP: i64 = 512 // outbound links scanned per page 53const NX_IMS_MAX_ROOTS: i64 = 16 // designated root pages cap 54const NX_IMS_SCAN_BUDGET: i64 = 8000000 // per-body byte-scan cap (M3) 55const NX_IMS_MAX_SLUG_LEN: i64 = 256 // a single resolved slug name cap 56const NX_IMS_WIKI_PREFIX_LEN: i64 = 6 // len("/wiki/") 57const NX_IMS_HTML_SUFFIX_LEN: i64 = 5 // len(".html") 58 59// Common ASCII (M7) 60const NX_IMS_QUOTE: i64 = 0x22 // '"' 61const NX_IMS_DOT: i64 = 0x2E // '.' 62 63// Truncate (p,n) at the first '#' (fragment) or '?' (query) -- a link to 64// "start.html#tasks" or "edit?p=x" addresses the base resource, so the fragment/ 65// query is not part of the page identity. Returns the truncated length. 66func nx_ims_strip_frag_query(p: *u8, n: i64) -> i64 { 67 var i: i64 = 0 68 while i < n { 69 if p[i] == (0x23 as u8) { return i } // '#' 70 if p[i] == (0x3F as u8) { return i } // '?' 71 i = i + 1 72 } 73 return n 74} 75 76// ===== slug normalisation ===================================================== 77// 78// Normalise a raw link/url token (p,n) to its bare slug: strip any #fragment / 79// ?query, then an optional leading "/wiki/" and an optional trailing ".html". 80// Returns the bare length; the bare pointer is handed back via out_ptr. 81// Defensive: never reads OOB; an already-bare slug passes through unchanged. 82func nx_ims_norm_slug(p: *u8, n: i64, out_ptr: *i64) -> i64 { 83 out_ptr[0] = p as i64 84 var bp: i64 = p as i64 85 var bn: i64 = nx_ims_strip_frag_query(p, n) 86 if bn < 0 { bn = 0 } 87 // strip leading "/wiki/" 88 if bn >= NX_IMS_WIKI_PREFIX_LEN { 89 let q: *u8 = bp as *u8 90 if q[0] == (0x2F as u8) { // '/' 91 if q[1] == (0x77 as u8) { // 'w' 92 if q[2] == (0x69 as u8) { // 'i' 93 if q[3] == (0x6B as u8) { // 'k' 94 if q[4] == (0x69 as u8) { // 'i' 95 if q[5] == (0x2F as u8) { // '/' 96 bp = bp + NX_IMS_WIKI_PREFIX_LEN 97 bn = bn - NX_IMS_WIKI_PREFIX_LEN 98 } 99 } 100 } 101 } 102 } 103 } 104 } 105 // strip trailing ".html" 106 if bn >= NX_IMS_HTML_SUFFIX_LEN { 107 let r: *u8 = bp as *u8 108 let base: i64 = bn - NX_IMS_HTML_SUFFIX_LEN 109 if r[base] == (NX_IMS_DOT as u8) { // '.' 110 if r[base + 1] == (0x68 as u8) { // 'h' 111 if r[base + 2] == (0x74 as u8) { // 't' 112 if r[base + 3] == (0x6D as u8) { // 'm' 113 if r[base + 4] == (0x6C as u8) { // 'l' 114 bn = bn - NX_IMS_HTML_SUFFIX_LEN 115 } 116 } 117 } 118 } 119 } 120 } 121 out_ptr[0] = bp 122 return bn 123} 124 125// Do two raw link/url tokens name the SAME page after normalisation? 126// (1 = same, 0 = different). Case-sensitive on the bare name (slugs are 127// canonical lowercase by convention). 128func nx_ims_slug_eq(ap: *u8, an: i64, bp: *u8, bn: i64) -> i64 { 129 let aib: *i64 = sys_mmap(8) as *i64 130 let bib: *i64 = sys_mmap(8) as *i64 131 let ain: i64 = nx_ims_norm_slug(ap, an, aib) 132 let bin: i64 = nx_ims_norm_slug(bp, bn, bib) 133 if ain != bin { return 0 } 134 if ain < 1 { return 0 } 135 let aa: *u8 = aib[0] as *u8 136 let bb: *u8 = bib[0] as *u8 137 var i: i64 = 0 138 var eq: i64 = 1 139 while i < ain { 140 if aa[i] != bb[i] { eq = 0 } 141 i = i + 1 142 } 143 return eq 144} 145 146// ===== href="/wiki/<x>.html" extractor ======================================= 147// 148// The served corpus is HTML; its page-to-page links are anchors, not 149// [[wikilinks]]. Walk body (p,n); for each occurrence of href="/wiki/...." 150// record the inner target (offset,len up to the closing quote) into the caller's 151// parallel offs[]/lens[] (capacity cap). Targets are recorded RAW (still with 152// the "/wiki/" prefix + ".html"); the caller normalises via nx_ims_norm_slug. 153// Only /wiki/ hrefs are recorded -- external/absolute links are not internal 154// link-rot candidates. Returns count (>=0) or -verdict. 155func nx_ims_href_links(body: *u8, n: i64, 156 offs: *i64, lens: *i64, cap: i64, 157 out_count: *i64) -> i64 { 158 if (out_count as i64) == 0 { return 0 - NX_IMS_BAD_INPUT } 159 out_count[0] = 0 160 if (body as i64) == 0 { return NX_IMS_OK } 161 if n < 0 { return 0 - NX_IMS_BAD_INPUT } 162 // needle = href="/wiki/ (10 bytes) 163 let needle: *u8 = "href=\"/wiki/" as *u8 164 let needle_n: i64 = 12 165 var i: i64 = 0 166 var cnt: i64 = 0 167 var iter: i64 = 0 168 while i < n { 169 if iter >= NX_IMS_SCAN_BUDGET { return 0 - NX_IMS_LOOP_BUDGET } 170 iter = iter + 1 171 var advanced: i64 = 0 172 if i + needle_n <= n { 173 // match needle at i ? 174 var m: i64 = 0 175 var ok: i64 = 1 176 while m < needle_n { 177 if body[i + m] != needle[m] { ok = 0 } 178 m = m + 1 179 } 180 if ok == 1 { 181 // target starts right after href=" (at the '/wiki/...'), so we 182 // keep the /wiki/ prefix in the recorded token for symmetry with 183 // stored urls. start = i + 6 (len of href=" ) 184 let start: i64 = i + 6 185 // scan to the closing double-quote 186 var cur: i64 = start 187 var done: i64 = 0 188 var endq: i64 = start 189 var found: i64 = 0 190 while done == 0 { 191 if cur >= n { done = 1 } 192 if cur - start >= NX_IMS_MAX_SLUG_LEN { done = 1 } 193 if done == 0 { 194 if body[cur] == (NX_IMS_QUOTE as u8) { 195 endq = cur 196 found = 1 197 done = 1 198 } 199 if found == 0 { cur = cur + 1 } 200 } 201 } 202 if found == 1 { 203 let tlen: i64 = endq - start 204 if tlen > 0 { 205 if tlen <= NX_IMS_MAX_SLUG_LEN { 206 // Only record genuine internal-PAGE links: after 207 // stripping #fragment/?query the target must end in 208 // ".html". This excludes dynamic routes (/wiki/edit?.., 209 // /wiki/status, /wiki/components) which are NOT static 210 // pages and so are not internal-page link-rot. A 211 // fragment link (start.html#x) keeps its .html and 212 // resolves to the base page. 213 let tp: *u8 = (start + (body as i64)) as *u8 214 let bare_n: i64 = nx_ims_strip_frag_query(tp, tlen) 215 var is_html: i64 = 0 216 if bare_n >= NX_IMS_HTML_SUFFIX_LEN { 217 let base: i64 = bare_n - NX_IMS_HTML_SUFFIX_LEN 218 if tp[base] == (NX_IMS_DOT as u8) { 219 if tp[base + 1] == (0x68 as u8) { 220 if tp[base + 2] == (0x74 as u8) { 221 if tp[base + 3] == (0x6D as u8) { 222 if tp[base + 4] == (0x6C as u8) { is_html = 1 } 223 } 224 } 225 } 226 } 227 } 228 if is_html == 1 { 229 if cnt >= cap { return 0 - NX_IMS_OVERFLOW } 230 if cnt >= NX_IMS_MAX_LINKS_PP { return 0 - NX_IMS_OVERFLOW } 231 offs[cnt] = start 232 lens[cnt] = tlen 233 cnt = cnt + 1 234 } 235 } 236 } 237 i = endq + 1 238 advanced = 1 239 } 240 } 241 } 242 if advanced == 0 { i = i + 1 } 243 } 244 out_count[0] = cnt 245 return NX_IMS_OK 246} 247 248// ===== unified outbound-target collector ===================================== 249// 250// For one page body, collect ALL outbound internal targets -- BOTH [[wikilink]] 251// (reused nx_wiki_forward_links) AND href="/wiki/..." (nx_ims_href_links) -- as 252// (offset,len) pairs into offs[]/lens[] (capacity cap). Returns the total count 253// (>=0) or -verdict. Offsets are byte offsets INTO body. 254func nx_ims_collect_targets(body: *u8, n: i64, 255 offs: *i64, lens: *i64, cap: i64, 256 out_count: *i64) -> i64 { 257 if (out_count as i64) == 0 { return 0 - NX_IMS_BAD_INPUT } 258 out_count[0] = 0 259 // pass 1: [[wikilink]] (the shared extractor) 260 let wc: *i64 = sys_mmap(8) as *i64 261 let rc_w: i64 = nx_wiki_forward_links(body, n, offs, lens, cap, wc) 262 if rc_w != NX_WBL_OK { return 0 - NX_IMS_OVERFLOW } 263 var total: i64 = wc[0] 264 // pass 2: href="/wiki/..." appended after the wikilinks 265 if total < cap { 266 let hoffs: *i64 = (offs as i64 + total * 8) as *i64 267 let hlens: *i64 = (lens as i64 + total * 8) as *i64 268 let hc: *i64 = sys_mmap(8) as *i64 269 let rc_h: i64 = nx_ims_href_links(body, n, hoffs, hlens, cap - total, hc) 270 if rc_h != NX_IMS_OK { return rc_h } 271 total = total + hc[0] 272 } 273 out_count[0] = total 274 return NX_IMS_OK 275} 276 277// ===== root predicate ======================================================== 278// 279// Is page rowid `r` (by its stored url slug) one of the designated roots? 280// roots[] holds root rowids (already resolved by the caller). 1 = root. 281func nx_ims_is_root(r: i64, roots: *i64, nroots: i64) -> i64 { 282 var i: i64 = 0 283 while i < nroots { 284 if i >= NX_IMS_MAX_ROOTS { return 0 } 285 if roots[i] == r { return 1 } 286 i = i + 1 287 } 288 return 0 289} 290 291// Resolve a root slug (sp,sn) to a rowid in the store, or -1 if not present. 292func nx_ims_resolve_root(store: *NxWikiDocStore, sp: *u8, sn: i64) -> i64 { 293 let dc: i64 = nx_wiki_doc_store_count(store) 294 let tp: *i64 = sys_mmap(8) as *i64 295 let tn: *i64 = sys_mmap(8) as *i64 296 let up: *i64 = sys_mmap(8) as *i64 297 let un: *i64 = sys_mmap(8) as *i64 298 let bp: *i64 = sys_mmap(8) as *i64 299 let bn: *i64 = sys_mmap(8) as *i64 300 var r: i64 = 0 301 while r < dc { 302 if r >= NX_IMS_MAX_PAGES { return 0 - 1 } 303 let rc: i64 = nx_wiki_doc_store_lookup(store, r, tp, tn, up, un, bp, bn) 304 if rc == NX_WIB_OK { 305 if nx_ims_slug_eq(up[0] as *u8, un[0], sp, sn) == 1 { return r } 306 } 307 r = r + 1 308 } 309 return 0 - 1 310} 311 312// ===== CORE: inbound-link census ============================================= 313// 314// For the whole store, compute inbound[r] = number of DISTINCT published pages 315// that link TO page r (via any outbound convention). A page linking itself does 316// NOT count toward its own inbound (a self-link cannot rescue an orphan). Fills 317// the caller's inbound[] (length >= doc_count). Returns NX_IMS_OK or -verdict. 318func nx_ims_inbound_counts(store: *NxWikiDocStore, 319 inbound: *i64) -> i64 { 320 if (store as i64) == 0 { return 0 - NX_IMS_BAD_INPUT } 321 if store.valid != 1 { return 0 - NX_IMS_BAD_INPUT } 322 let dc: i64 = nx_wiki_doc_store_count(store) 323 var z: i64 = 0 324 while z < dc { 325 if z >= NX_IMS_MAX_PAGES { return 0 - NX_IMS_LOOP_BUDGET } 326 inbound[z] = 0 327 z = z + 1 328 } 329 // per-source scratch 330 let offs: *i64 = sys_mmap(NX_IMS_MAX_LINKS_PP * 8) as *i64 331 let lens: *i64 = sys_mmap(NX_IMS_MAX_LINKS_PP * 8) as *i64 332 let lc: *i64 = sys_mmap(8) as *i64 333 // source lookup out-params 334 let tp: *i64 = sys_mmap(8) as *i64 335 let tn: *i64 = sys_mmap(8) as *i64 336 let up: *i64 = sys_mmap(8) as *i64 337 let un: *i64 = sys_mmap(8) as *i64 338 let bp: *i64 = sys_mmap(8) as *i64 339 let bn: *i64 = sys_mmap(8) as *i64 340 // target lookup out-params 341 let u2p: *i64 = sys_mmap(8) as *i64 342 let u2n: *i64 = sys_mmap(8) as *i64 343 let t2p: *i64 = sys_mmap(8) as *i64 344 let t2n: *i64 = sys_mmap(8) as *i64 345 let b2p: *i64 = sys_mmap(8) as *i64 346 let b2n: *i64 = sys_mmap(8) as *i64 347 // per-source "already credited this target" marks (dedupe distinct sources) 348 let credited: *i64 = sys_mmap(NX_IMS_MAX_PAGES * 8) as *i64 349 350 var s: i64 = 0 351 while s < dc { 352 if s >= NX_IMS_MAX_PAGES { return 0 - NX_IMS_LOOP_BUDGET } 353 let rc_s: i64 = nx_wiki_doc_store_lookup(store, s, tp, tn, up, un, bp, bn) 354 if rc_s == NX_WIB_OK { 355 // reset credited marks for this source 356 var c0: i64 = 0 357 while c0 < dc { 358 if c0 >= NX_IMS_MAX_PAGES { c0 = dc } 359 if c0 < dc { credited[c0] = 0 } 360 c0 = c0 + 1 361 } 362 let body: *u8 = bp[0] as *u8 363 let body_n: i64 = bn[0] 364 let rc_c: i64 = nx_ims_collect_targets(body, body_n, offs, lens, NX_IMS_MAX_LINKS_PP, lc) 365 if rc_c == NX_IMS_OK { 366 var k: i64 = 0 367 while k < lc[0] { 368 if k >= NX_IMS_MAX_LINKS_PP { k = lc[0] } 369 if k < lc[0] { 370 let lp: *u8 = (body as i64 + offs[k]) as *u8 371 let ln: i64 = lens[k] 372 // resolve target -> rowid 373 var dst: i64 = 0 - 1 374 var t: i64 = 0 375 while t < dc { 376 if dst < 0 { 377 let rc_t: i64 = nx_wiki_doc_store_lookup(store, t, t2p, t2n, u2p, u2n, b2p, b2n) 378 if rc_t == NX_WIB_OK { 379 if nx_ims_slug_eq(lp, ln, u2p[0] as *u8, u2n[0]) == 1 { dst = t } 380 } 381 } 382 t = t + 1 383 } 384 if dst >= 0 { 385 if dst != s { // self-link does NOT count 386 if credited[dst] == 0 { // distinct source only once 387 inbound[dst] = inbound[dst] + 1 388 credited[dst] = 1 389 } 390 } 391 } 392 } 393 k = k + 1 394 } 395 } 396 } 397 s = s + 1 398 } 399 return NX_IMS_OK 400} 401 402// ===== CORE: orphan census =================================================== 403// 404// ORPHANS = pages with inbound==0, EXCLUDING any rowid in roots[]. Fills 405// out_rowids[] (capacity cap) with the orphan rowids, count via out_count. 406// Returns NX_IMS_OK or -verdict. Pure over the supplied inbound[] + roots[]. 407func nx_ims_orphans(store: *NxWikiDocStore, 408 roots: *i64, nroots: i64, 409 out_rowids: *i64, cap: i64, 410 out_count: *i64) -> i64 { 411 if (out_count as i64) == 0 { return 0 - NX_IMS_BAD_INPUT } 412 out_count[0] = 0 413 if (store as i64) == 0 { return 0 - NX_IMS_BAD_INPUT } 414 if store.valid != 1 { return 0 - NX_IMS_BAD_INPUT } 415 let dc: i64 = nx_wiki_doc_store_count(store) 416 let inbound: *i64 = sys_mmap(NX_IMS_MAX_PAGES * 8) as *i64 417 let rc_in: i64 = nx_ims_inbound_counts(store, inbound) 418 if rc_in != NX_IMS_OK { return rc_in } 419 var cnt: i64 = 0 420 var r: i64 = 0 421 while r < dc { 422 if r >= NX_IMS_MAX_PAGES { return 0 - NX_IMS_LOOP_BUDGET } 423 if inbound[r] == 0 { 424 if nx_ims_is_root(r, roots, nroots) == 0 { 425 if cnt >= cap { return 0 - NX_IMS_OVERFLOW } 426 out_rowids[cnt] = r 427 cnt = cnt + 1 428 } 429 } 430 r = r + 1 431 } 432 out_count[0] = cnt 433 return NX_IMS_OK 434} 435 436// ===== CORE: dead-link census ================================================ 437// 438// DEAD_LINKS = each outbound internal target that resolves to NO published page. 439// Reported as parallel arrays: out_src[] = source rowid, out_off[]/out_len[] = 440// the offending target token's (offset,len) INTO that source's body (so the 441// caller can print the exact dead href/wikilink). count via out_count. 442// Returns NX_IMS_OK or -verdict. 443func nx_ims_dead_links(store: *NxWikiDocStore, 444 out_src: *i64, out_off: *i64, out_len: *i64, cap: i64, 445 out_count: *i64) -> i64 { 446 if (out_count as i64) == 0 { return 0 - NX_IMS_BAD_INPUT } 447 out_count[0] = 0 448 if (store as i64) == 0 { return 0 - NX_IMS_BAD_INPUT } 449 if store.valid != 1 { return 0 - NX_IMS_BAD_INPUT } 450 let dc: i64 = nx_wiki_doc_store_count(store) 451 let offs: *i64 = sys_mmap(NX_IMS_MAX_LINKS_PP * 8) as *i64 452 let lens: *i64 = sys_mmap(NX_IMS_MAX_LINKS_PP * 8) as *i64 453 let lc: *i64 = sys_mmap(8) as *i64 454 let tp: *i64 = sys_mmap(8) as *i64 455 let tn: *i64 = sys_mmap(8) as *i64 456 let up: *i64 = sys_mmap(8) as *i64 457 let un: *i64 = sys_mmap(8) as *i64 458 let bp: *i64 = sys_mmap(8) as *i64 459 let bn: *i64 = sys_mmap(8) as *i64 460 let u2p: *i64 = sys_mmap(8) as *i64 461 let u2n: *i64 = sys_mmap(8) as *i64 462 let t2p: *i64 = sys_mmap(8) as *i64 463 let t2n: *i64 = sys_mmap(8) as *i64 464 let b2p: *i64 = sys_mmap(8) as *i64 465 let b2n: *i64 = sys_mmap(8) as *i64 466 467 var cnt: i64 = 0 468 var s: i64 = 0 469 while s < dc { 470 if s >= NX_IMS_MAX_PAGES { return 0 - NX_IMS_LOOP_BUDGET } 471 let rc_s: i64 = nx_wiki_doc_store_lookup(store, s, tp, tn, up, un, bp, bn) 472 if rc_s == NX_WIB_OK { 473 let body: *u8 = bp[0] as *u8 474 let body_n: i64 = bn[0] 475 let rc_c: i64 = nx_ims_collect_targets(body, body_n, offs, lens, NX_IMS_MAX_LINKS_PP, lc) 476 if rc_c == NX_IMS_OK { 477 var k: i64 = 0 478 while k < lc[0] { 479 if k >= NX_IMS_MAX_LINKS_PP { k = lc[0] } 480 if k < lc[0] { 481 let lp: *u8 = (body as i64 + offs[k]) as *u8 482 let ln: i64 = lens[k] 483 var dst: i64 = 0 - 1 484 var t: i64 = 0 485 while t < dc { 486 if dst < 0 { 487 let rc_t: i64 = nx_wiki_doc_store_lookup(store, t, t2p, t2n, u2p, u2n, b2p, b2n) 488 if rc_t == NX_WIB_OK { 489 if nx_ims_slug_eq(lp, ln, u2p[0] as *u8, u2n[0]) == 1 { dst = t } 490 } 491 } 492 t = t + 1 493 } 494 if dst < 0 { 495 if cnt >= cap { return 0 - NX_IMS_OVERFLOW } 496 out_src[cnt] = s 497 out_off[cnt] = offs[k] 498 out_len[cnt] = lens[k] 499 cnt = cnt + 1 500 } 501 } 502 k = k + 1 503 } 504 } 505 } 506 s = s + 1 507 } 508 out_count[0] = cnt 509 return NX_IMS_OK 510}