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}