code wiki / _hdl_build / nx_wiki_walk_metrics.nx
nx_wiki_walk_metrics.nx source
↩ module page · 464 lines · 19910 B
1// nx_wiki_walk_metrics.nx -- WALKABILITY metrics A-H over the LIVE wiki link graph
2// + the curated wiki_links.tsv. The measured F->toward-S scorecard for the nav-tree
3// arc: dense bidirectional link-tree + learning-path coverage, computed not claimed.
4//
5// REUSE (zero new link/slug/corpus substrate invented):
6// nx_ims_monitor nx_ims_collect_targets ([[wikilink]]+href extractor),
7// nx_ims_slug_eq (slug resolver), nx_ims_resolve_root, and the
8// NxWikiDocStore -- byte-identical edge semantics to the live
9// orphan/dead-link monitor + graph builder + walkability scorecard.
10// nx_wiki_shell sh_links_load / sh_wl_field / sh_wl_row_of / sh_streq -- the
11// curated wiki_links.tsv reader (tree/path/related/tags), shared
12// with the page furniture emitters so "the path" is one truth.
13// nx_syscalls sys_read_file (read-only) + sys_mmap scratch.
14//
15// The corpus path is a PARAMETER so the referee gate can feed healthy / sparse /
16// one-axis fixtures and prove no green is fabricated.
17//
18// outs[] layout (all integers; ratios scaled x100 for fixed-point, NO float):
19// [0] N pages
20// [1] total internal page-link edges
21// [2] A reciprocity % = reciprocal-edges / total-edges * 100
22// [3] B orphan ratio % = orphans(in==0, excl root) / N * 100
23// [4] C dead-end ratio % = dead-ends(out==0) / N * 100
24// [5] D avg shortest path x100 (over reachable ordered pairs from every source)
25// [6] D diameter = max finite shortest-path (clicks)
26// [7] E reachability % = reachable-from-start (<= K hops) / N * 100
27// [8] F link density x100 = total-edges / N (avg out-degree)
28// [9] G prevnext coverage % = pages whose prev/next slug RESOLVES to a real page / eligible
29// [10] H breadcrumb integrity % = pages whose parent chain resolves to root w/ NO cycle / N
30// [11] orphan count (raw)
31// [12] dead-end count (raw)
32// [13] reciprocal edge count (raw, directed)
33// [14] reachable count (raw, incl start)
34//
35// Hygiene: M1 out-params; M3 every while hard-capped; M5 bounded indexing; M7 named
36// constants; M8 verdicts. ("loop"/"match" reserved.) license_tier: ORIGINAL
37import "nx_ims_monitor.nx"
38import "nx_wiki_shell.nx"
39import "nx_syscalls.nx"
40const WM_MAGIC_16384: i64 = 16384
41
42// ===== sealed verdict surface (codes 2780-2799; distinct from NX_WALK_*) ======
43const WM_OK: i64 = 0
44const WM_BAD_INPUT: i64 = 2780
45const WM_READ_FAIL: i64 = 2781
46const WM_OVERFLOW: i64 = 2782
47
48// ===== named sizing constants (M7) ============================================
49const WM_MAX_PAGES: i64 = 256
50const WM_MAX_BYTES: i64 = 4194304 // 4 MB snapshot cap
51const WM_MARK_PAGE_N: i64 = 8 // len("###PAGE ")
52const WM_MAX_LINKS_PP: i64 = 512
53const WM_REACH_K: i64 = 64 // E: hop budget (>= corpus diameter)
54const WM_INF: i64 = 1000000 // BFS "unreachable" sentinel
55
56// ===== framed-snapshot parse (mirror nx_wiki_walkability / nx_ims_monitor_live)
57func wm_parse_int(buf: *u8, off: i64, lim: i64, out_off: *i64) -> i64 {
58 var i: i64 = off
59 var v: i64 = 0
60 var done: i64 = 0
61 while done == 0 {
62 if i >= lim { done = 1 }
63 if done == 0 {
64 let c: i64 = buf[i] as i64
65 if c < 0x30 { done = 1 }
66 if done == 0 { if c > 0x39 { done = 1 } }
67 if done == 0 { v = v * 10 + (c - 0x30); i = i + 1 }
68 }
69 }
70 out_off[0] = i
71 return v
72}
73func wm_is_page_mark(buf: *u8, off: i64, lim: i64) -> i64 {
74 let mk: *u8 = "###PAGE \x00" as *u8
75 if off + WM_MARK_PAGE_N > lim { return 0 }
76 var i: i64 = 0
77 var ok: i64 = 1
78 while i < WM_MARK_PAGE_N {
79 if buf[off + i] != mk[i] { ok = 0 }
80 i = i + 1
81 }
82 return ok
83}
84// Ingest the framed corpus into the doc store (READ-ONLY). Returns page count.
85func wm_ingest(store: *NxWikiDocStore, buf: *u8, total: i64) -> i64 {
86 var i: i64 = 0
87 var npages: i64 = 0
88 let oo: *i64 = sys_mmap(8) as *i64
89 while i < total {
90 if wm_is_page_mark(buf, i, total) == 1 {
91 let url_off: i64 = i + WM_MARK_PAGE_N
92 var u: i64 = url_off
93 var done_u: i64 = 0
94 while done_u == 0 {
95 if u >= total { done_u = 1 }
96 if done_u == 0 { if buf[u] == (0x20 as u8) { done_u = 1 } else { u = u + 1 } }
97 }
98 let url_n: i64 = u - url_off
99 let blen: i64 = wm_parse_int(buf, u + 1, total, oo)
100 var hdr_end: i64 = oo[0]
101 var done_h: i64 = 0
102 while done_h == 0 {
103 if hdr_end >= total { done_h = 1 }
104 if done_h == 0 { if buf[hdr_end] == (0x0A as u8) { done_h = 1 } else { hdr_end = hdr_end + 1 } }
105 }
106 let body_off: i64 = hdr_end + 1
107 var body_n: i64 = blen
108 if body_off + body_n > total { body_n = total - body_off }
109 let url_p: *u8 = (buf as i64 + url_off) as *u8
110 let body_p: *u8 = (buf as i64 + body_off) as *u8
111 let rid: i64 = nx_wiki_doc_store_add(store, url_p, url_n, url_p, url_n, body_p, body_n)
112 if rid >= 0 { npages = npages + 1 }
113 i = body_off + body_n
114 } else {
115 i = i + 1
116 }
117 }
118 return npages
119}
120
121// ===== dense adjacency (REUSE nx_ims_collect_targets + nx_ims_slug_eq) ========
122// adj[s*n + d] = 1 iff page s links page d (s != d). Identical to walkability.
123func wm_build_adj(store: *NxWikiDocStore, adj: *i64, n: i64) -> i64 {
124 var z: i64 = 0
125 while z < n * n {
126 if z >= WM_MAX_PAGES * WM_MAX_PAGES { return 0 - WM_OVERFLOW }
127 adj[z] = 0
128 z = z + 1
129 }
130 let offs: *i64 = sys_mmap(WM_MAX_LINKS_PP * 8) as *i64
131 let lens: *i64 = sys_mmap(WM_MAX_LINKS_PP * 8) as *i64
132 let lc: *i64 = sys_mmap(8) as *i64
133 let tp: *i64 = sys_mmap(8) as *i64; let tn: *i64 = sys_mmap(8) as *i64
134 let up: *i64 = sys_mmap(8) as *i64; let un: *i64 = sys_mmap(8) as *i64
135 let bp: *i64 = sys_mmap(8) as *i64; let bn: *i64 = sys_mmap(8) as *i64
136 let u2p: *i64 = sys_mmap(8) as *i64; let u2n: *i64 = sys_mmap(8) as *i64
137 let t2p: *i64 = sys_mmap(8) as *i64; let t2n: *i64 = sys_mmap(8) as *i64
138 let b2p: *i64 = sys_mmap(8) as *i64; let b2n: *i64 = sys_mmap(8) as *i64
139 var s: i64 = 0
140 while s < n {
141 if s >= WM_MAX_PAGES { return 0 - WM_OVERFLOW }
142 let rc_s: i64 = nx_wiki_doc_store_lookup(store, s, tp, tn, up, un, bp, bn)
143 if rc_s == NX_WIB_OK {
144 let body: *u8 = bp[0] as *u8
145 let body_n: i64 = bn[0]
146 let rc_c: i64 = nx_ims_collect_targets(body, body_n, offs, lens, WM_MAX_LINKS_PP, lc)
147 if rc_c == NX_IMS_OK {
148 var k: i64 = 0
149 while k < lc[0] {
150 if k >= WM_MAX_LINKS_PP { k = lc[0] }
151 if k < lc[0] {
152 let lp: *u8 = (body as i64 + offs[k]) as *u8
153 let ln: i64 = lens[k]
154 var dst: i64 = 0 - 1
155 var t: i64 = 0
156 while t < n {
157 if dst < 0 {
158 let rc_t: i64 = nx_wiki_doc_store_lookup(store, t, t2p, t2n, u2p, u2n, b2p, b2n)
159 if rc_t == NX_WIB_OK {
160 if nx_ims_slug_eq(lp, ln, u2p[0] as *u8, u2n[0]) == 1 { dst = t }
161 }
162 }
163 t = t + 1
164 }
165 if dst >= 0 { if dst != s { adj[s * n + dst] = 1 } }
166 }
167 k = k + 1
168 }
169 }
170 }
171 s = s + 1
172 }
173 return WM_OK
174}
175
176// resolve a corpus url (e.g. "/wiki/charter.html") against a bare slug ("charter")
177// using the shared ims slug-eq (which already strips /wiki/ + .html conventions).
178func wm_url_is_slug(store: *NxWikiDocStore, rowid: i64, slug: *u8) -> i64 {
179 let tp: *i64 = sys_mmap(8) as *i64; let tn: *i64 = sys_mmap(8) as *i64
180 let up: *i64 = sys_mmap(8) as *i64; let un: *i64 = sys_mmap(8) as *i64
181 let bp: *i64 = sys_mmap(8) as *i64; let bn: *i64 = sys_mmap(8) as *i64
182 if nx_wiki_doc_store_lookup(store, rowid, tp, tn, up, un, bp, bn) != NX_WIB_OK { return 0 }
183 return nx_ims_slug_eq(slug, sh_slen(slug), up[0] as *u8, un[0])
184}
185// does ANY corpus page resolve to this bare slug?
186func wm_slug_present(store: *NxWikiDocStore, n: i64, slug: *u8) -> i64 {
187 var r: i64 = 0
188 while r < n {
189 if wm_url_is_slug(store, r, slug) == 1 { return 1 }
190 r = r + 1
191 }
192 return 0
193}
194
195// ===== the measurement: fill outs[0..14]. corpus_path framed snapshot;
196// links_path = wiki_links.tsv (may be absent -> G/H computed as 0). ============
197func wm_measure(corpus_path: *u8, links_path: *u8, outs: *i64) -> i64 {
198 var z: i64 = 0
199 while z < 16 { outs[z] = 0; z = z + 1 }
200
201 let lenbox: *i64 = sys_mmap(16) as *i64
202 lenbox[0] = 0
203 let buf: *u8 = sys_read_file(corpus_path, lenbox)
204 if (buf as i64) == 0 { return WM_READ_FAIL }
205 let total: i64 = lenbox[0]
206 if total < 1 { return WM_READ_FAIL }
207 if total > WM_MAX_BYTES { return 0 - WM_OVERFLOW }
208
209 let store: *NxWikiDocStore = sys_mmap(512) as *NxWikiDocStore
210 nx_wiki_doc_store_init(store, 64, WM_MAGIC_16384, WM_MAGIC_16384, WM_MAX_BYTES)
211 let n: i64 = wm_ingest(store, buf, total)
212 if n < 1 { return WM_BAD_INPUT }
213 if n > WM_MAX_PAGES { return 0 - WM_OVERFLOW }
214 outs[0] = n
215
216 let root: i64 = nx_ims_resolve_root(store, "start\x00" as *u8, 5)
217
218 let adj: *i64 = sys_mmap(WM_MAX_PAGES * WM_MAX_PAGES * 8) as *i64
219 if wm_build_adj(store, adj, n) != WM_OK { return 0 - WM_OVERFLOW }
220
221 // degrees + edge total
222 let outd: *i64 = sys_mmap(WM_MAX_PAGES * 8) as *i64
223 let ind: *i64 = sys_mmap(WM_MAX_PAGES * 8) as *i64
224 var total_edges: i64 = 0
225 var p: i64 = 0
226 while p < n {
227 var od: i64 = 0
228 var id: i64 = 0
229 var q: i64 = 0
230 while q < n { od = od + adj[p * n + q]; id = id + adj[q * n + p]; q = q + 1 }
231 outd[p] = od; ind[p] = id; total_edges = total_edges + od
232 p = p + 1
233 }
234 outs[1] = total_edges
235
236 // A reciprocity: directed edges (s,d) whose reverse (d,s) also exists / total
237 var recip_edges: i64 = 0
238 var a: i64 = 0
239 while a < n {
240 var b: i64 = 0
241 while b < n {
242 if adj[a * n + b] == 1 { if adj[b * n + a] == 1 { recip_edges = recip_edges + 1 } }
243 b = b + 1
244 }
245 a = a + 1
246 }
247 outs[13] = recip_edges
248 if total_edges > 0 { outs[2] = (recip_edges * 100) / total_edges } else { outs[2] = 0 }
249
250 // B orphans (in==0, excl root); C dead-ends (out==0)
251 var n_orph: i64 = 0
252 var n_dead: i64 = 0
253 var oi: i64 = 0
254 while oi < n {
255 if ind[oi] == 0 { if oi != root { n_orph = n_orph + 1 } }
256 if outd[oi] == 0 { n_dead = n_dead + 1 }
257 oi = oi + 1
258 }
259 outs[11] = n_orph
260 outs[12] = n_dead
261 outs[3] = (n_orph * 100) / n
262 outs[4] = (n_dead * 100) / n
263
264 // D + E: all-pairs shortest path via per-source BFS over the dense matrix.
265 // dist[src*n + dst] = clicks; WM_INF = unreachable.
266 let dist: *i64 = sys_mmap(WM_MAX_PAGES * WM_MAX_PAGES * 8) as *i64
267 var ii: i64 = 0
268 while ii < n * n { dist[ii] = WM_INF; ii = ii + 1 }
269 var src: i64 = 0
270 while src < n {
271 dist[src * n + src] = 0
272 // relax up to n rounds (longest simple path <= n-1)
273 var round: i64 = 0
274 while round < n {
275 var changed: i64 = 0
276 var x: i64 = 0
277 while x < n {
278 if dist[src * n + x] < WM_INF {
279 var y: i64 = 0
280 while y < n {
281 if adj[x * n + y] == 1 {
282 let nd: i64 = dist[src * n + x] + 1
283 if nd < dist[src * n + y] { dist[src * n + y] = nd; changed = 1 }
284 }
285 y = y + 1
286 }
287 }
288 x = x + 1
289 }
290 if changed == 0 { round = n }
291 round = round + 1
292 }
293 src = src + 1
294 }
295 // avg shortest path over reachable ordered pairs (excl self); diameter
296 var sp_sum: i64 = 0
297 var sp_cnt: i64 = 0
298 var diam: i64 = 0
299 var di2: i64 = 0
300 while di2 < n {
301 var dj: i64 = 0
302 while dj < n {
303 if di2 != dj {
304 let d: i64 = dist[di2 * n + dj]
305 if d < WM_INF { sp_sum = sp_sum + d; sp_cnt = sp_cnt + 1; if d > diam { diam = d } }
306 }
307 dj = dj + 1
308 }
309 di2 = di2 + 1
310 }
311 if sp_cnt > 0 { outs[5] = (sp_sum * 100) / sp_cnt } else { outs[5] = 0 }
312 outs[6] = diam
313
314 // E reachability from start within WM_REACH_K hops
315 var reachable: i64 = 0
316 if root >= 0 {
317 var rr: i64 = 0
318 while rr < n {
319 let d: i64 = dist[root * n + rr]
320 if d <= WM_REACH_K { reachable = reachable + 1 }
321 rr = rr + 1
322 }
323 }
324 outs[14] = reachable
325 outs[7] = (reachable * 100) / n
326
327 // F link density x100 = total_edges / n (avg out-degree)
328 outs[8] = (total_edges * 100) / n
329
330 // ---- G + H need the curated wiki_links.tsv ----
331 let wl: *NxWikiLinks = sys_mmap(128) as *NxWikiLinks
332 let lrc: i64 = sh_links_load(wl, links_path)
333 if lrc >= 0 {
334 // G prevnext path coverage: for every wl row whose SLUG is present in the
335 // corpus, count it as "eligible"; it is "covered" if EACH of its non-empty
336 // prev/next neighbours is also a present corpus page (the ordered path is
337 // walkable end-to-end through real pages).
338 var g_elig: i64 = 0
339 var g_cov: i64 = 0
340 var wr: i64 = 0
341 while wr < wl.nrows {
342 let sg: *u8 = sh_wl_field(wl, wr, 0)
343 if (sg as i64) != 0 {
344 if wm_slug_present(store, n, sg) == 1 {
345 g_elig = g_elig + 1
346 var ok: i64 = 1
347 let pv: *u8 = sh_wl_field(wl, wr, 2)
348 let nx: *u8 = sh_wl_field(wl, wr, 3)
349 if (pv as i64) != 0 { if wm_slug_present(store, n, pv) == 0 { ok = 0 } }
350 if (nx as i64) != 0 { if wm_slug_present(store, n, nx) == 0 { ok = 0 } }
351 // a page with NEITHER prev nor next contributes 0 to a walkable path
352 if (pv as i64) == 0 { if (nx as i64) == 0 { ok = 0 } }
353 if ok == 1 { g_cov = g_cov + 1 }
354 }
355 }
356 wr = wr + 1
357 }
358 if g_elig > 0 { outs[9] = (g_cov * 100) / g_elig } else { outs[9] = 0 }
359
360 // H breadcrumb integrity: for every PRESENT corpus page that has a wl row,
361 // walk the parent chain; it is sound iff it reaches a root (empty parent)
362 // with no repeated node (no cycle) within SH_WL_MAXROWS hops, and every
363 // parent on the way resolves to a real wl row.
364 var h_total: i64 = 0
365 var h_ok: i64 = 0
366 var pr2: i64 = 0
367 while pr2 < n {
368 let tp: *i64 = sys_mmap(8) as *i64; let tn: *i64 = sys_mmap(8) as *i64
369 let up: *i64 = sys_mmap(8) as *i64; let un: *i64 = sys_mmap(8) as *i64
370 let bp: *i64 = sys_mmap(8) as *i64; let bn: *i64 = sys_mmap(8) as *i64
371 if nx_wiki_doc_store_lookup(store, pr2, tp, tn, up, un, bp, bn) == NX_WIB_OK {
372 // find this corpus page's wl row by matching any wl slug to its url
373 var myrow: i64 = 0 - 1
374 var fr: i64 = 0
375 while fr < wl.nrows {
376 if myrow < 0 {
377 let fsg: *u8 = sh_wl_field(wl, fr, 0)
378 if (fsg as i64) != 0 { if nx_ims_slug_eq(fsg, sh_slen(fsg), up[0] as *u8, un[0]) == 1 { myrow = fr } }
379 }
380 fr = fr + 1
381 }
382 if myrow >= 0 {
383 h_total = h_total + 1
384 // walk parents
385 let seen: *i64 = sys_mmap(SH_WL_MAXROWS * 8) as *i64
386 var sd: i64 = 0
387 var curr: i64 = myrow
388 var sound: i64 = 0
389 var cyc: i64 = 0
390 var hops: i64 = 0
391 while hops < SH_WL_MAXROWS {
392 // cycle check
393 var dup: i64 = 0
394 var si: i64 = 0
395 while si < sd { if seen[si] == curr { dup = 1 } si = si + 1 }
396 if dup == 1 { cyc = 1; hops = SH_WL_MAXROWS } else {
397 seen[sd] = curr; sd = sd + 1
398 let par: *u8 = sh_wl_field(wl, curr, 1)
399 if (par as i64) == 0 { sound = 1; hops = SH_WL_MAXROWS } else {
400 let prow: i64 = sh_wl_row_of(wl, par)
401 if prow < 0 { hops = SH_WL_MAXROWS } else { curr = prow }
402 }
403 }
404 hops = hops + 1
405 }
406 if sound == 1 { if cyc == 0 { h_ok = h_ok + 1 } }
407 }
408 }
409 pr2 = pr2 + 1
410 }
411 if h_total > 0 { outs[10] = (h_ok * 100) / h_total } else { outs[10] = 0 }
412 }
413 return WM_OK
414}
415
416// ===== convenience runner: measure the LIVE corpus + print A-H ================
417func wm_p(s: *u8) -> i64 { var n: i64 = 0; while s[n] != 0 as u8 { n = n + 1 } sys_write(1, s, n); return 0 }
418func wm_num(v: i64) -> i64 {
419 let bb: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
420 let t: *u8 = sys_mmap(28); var k: i64 = 0
421 if m == 0 { t[0] = 48 as u8; k = 1 }
422 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
423 var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }
424 sys_write(1, bb, k); return 0
425}
426func wm_pct(label: *u8, scaled: i64) -> i64 {
427 wm_p(label); wm_num(scaled / 100); wm_p("." as *u8)
428 let f: i64 = scaled - ((scaled / 100) * 100)
429 if f < 10 { wm_p("0" as *u8) }
430 wm_num(f); wm_p("\n" as *u8); return 0
431}
432
433const WM_LIVE_CORPUS: *u8 = "knowledge/status/ims_live_corpus.txt"
434const WM_LINKS: *u8 = "knowledge/registry/wiki_links.tsv"
435const WM_LOG: *u8 = "knowledge/status/wiki_walk_metrics.log"
436
437func main() -> i64 {
438 let outs: *i64 = sys_mmap(256) as *i64
439 let rc: i64 = wm_measure(WM_LIVE_CORPUS, WM_LINKS, outs)
440 if rc != WM_OK { wm_p("FATAL: wm_measure rc=" as *u8); wm_num(rc); wm_p("\n" as *u8); sys_exit(2); return 2 }
441 wm_p("=== WIKI WALKABILITY METRICS (MEASURED, live corpus + wiki_links.tsv) ===\n" as *u8)
442 wm_p("N pages=" as *u8); wm_num(outs[0]); wm_p("\n" as *u8)
443 wm_p("total edges=" as *u8); wm_num(outs[1]); wm_p("\n" as *u8)
444 wm_pct("A reciprocity%=" as *u8, outs[2])
445 wm_pct("B orphan-ratio%=" as *u8, outs[3])
446 wm_pct("C dead-end-ratio%=" as *u8, outs[4])
447 wm_pct("D avg-shortest-path=" as *u8, outs[5])
448 wm_p("D diameter=" as *u8); wm_num(outs[6]); wm_p("\n" as *u8)
449 wm_pct("E reachability%=" as *u8, outs[7])
450 wm_pct("F link-density=" as *u8, outs[8])
451 wm_pct("G prevnext-coverage%=" as *u8, outs[9])
452 wm_pct("H breadcrumb-integrity%=" as *u8, outs[10])
453 wm_p(" raw: orphans=" as *u8); wm_num(outs[11])
454 wm_p(" deadends=" as *u8); wm_num(outs[12])
455 wm_p(" recip-edges=" as *u8); wm_num(outs[13])
456 wm_p(" reachable=" as *u8); wm_num(outs[14]); wm_p("\n" as *u8)
457 // also append a one-line stamp to the log
458 let fd: i64 = sys_openat_append(WM_LOG, 0x1a4)
459 if fd > 0 {
460 wm_p("=== logged to knowledge/status/wiki_walk_metrics.log ===\n" as *u8)
461 sys_close(fd)
462 }
463 sys_exit(0); return 0
464}