code wiki / wiki / nx_wiki_graph.nx

nx_wiki_graph.nx source

↩ module page · 393 lines · 16190 B

1// nx_wiki_graph.nx -- wiki R4: static connections / link-graph view. 2// 3// COMPOSES: nx_wiki_index_builder (page corpus: count + per-rowid title/url/ 4// body) and nx_wiki_backlinks (the shared forward-[[wikilink]] extractor + 5// slug matcher -- so graph edges and the backlinks panel agree on what a link 6// is). nx_syscalls for scratch. 7// 8// MODEL: 9// node = a page in the doc store (one per rowid). 10// edge = a forward [[wikilink]] from page A's body to another page B that 11// EXISTS as a node. src -> dst. 12// broken = a forward [[wikilink]] whose target resolves to NO node (link 13// rot). Broken links are COUNTED but are NOT nodes and NOT edges -- 14// a dangling reference is rot, not a connection. 15// 16// OUTPUT: a self-contained, deterministic <svg> with a CIRCULAR layout -- nodes 17// evenly spaced on a circle (positions from a fixed integer unit-circle table, 18// no floating point, no trig dependency), <line> for each edge, <circle> + 19// <text> for each node label. NO <script>, NO event handlers -> static SVG is 20// sovereign and renders identically everywhere. (An interactive/force-directed 21// graph is a documented JS follow-on, deliberately out of scope.) 22// 23// Hygiene: M1 out-params; M3 capped loops; M5 bounded indexing; M6 real 24// semantics; M7 named constants; M8 propagated verdicts. 25// 26// Status: V1 (wiki R4). 2026-06-15. license_tier: ORIGINAL 27import "nx_syscalls.nx" 28import "nx_wiki_index_builder.nx" 29import "nx_wiki_backlinks.nx" 30 31// ===== Sealed verdict surface (codes 2680-2699) ============================== 32const NX_WGRAPH_OK: i64 = 0 33const NX_WGRAPH_BAD_INPUT: i64 = 2680 34const NX_WGRAPH_OVERFLOW: i64 = 2681 35const NX_WGRAPH_LOOP_BUDGET: i64 = 2682 36 37// ===== Named sizing constants (M7) =========================================== 38const NX_WGRAPH_MAX_NODES: i64 = 1000 // mirrors doc-store docs cap 39const NX_WGRAPH_MAX_EDGES: i64 = 8192 // total edges across corpus cap 40const NX_WGRAPH_MAX_LINKS_PP: i64 = 512 // forward links per page cap 41const NX_WGRAPH_LOOP_BUDGET_C: i64 = 8000000 42const NX_WGRAPH_LABEL_GUARD: i64 = 512 // per-label byte cap (M3/M5) 43 44// SVG layout geometry (M7) -- a fixed canvas; circular ring of radius R about 45// the centre (CX,CY). 46const NX_WGRAPH_W: i64 = 800 47const NX_WGRAPH_H: i64 = 800 48const NX_WGRAPH_CX: i64 = 400 49const NX_WGRAPH_CY: i64 = 400 50const NX_WGRAPH_R: i64 = 320 // ring radius 51const NX_WGRAPH_NODE_R: i64 = 7 // node dot radius 52const NX_WGRAPH_TBL: i64 = 24 // unit-circle table entries 53 54// ===== NxWikiGraph: parallel node + edge arrays ============================== 55struct NxWikiGraph { 56 node_count: i64 57 edge_count: i64 58 broken_count: i64 59 // edges as parallel src/dst rowid arrays 60 edge_src: *i64 61 edge_dst: *i64 62 edges_cap: i64 63 valid: i64 64} 65 66// Fixed 24-point unit circle, scaled by 1000 (cos,sin), starting at angle 0 67// going counter-clockwise. Deterministic integer layout -> no float/trig dep. 68// Returns scaled cos (*1000) for table slot k (0..23) via the function result 69// and scaled sin (*1000) via out_sin. k is taken mod 24 by the caller. 70func nx_wgraph_unit(k: i64, out_sin: *i64) -> i64 { 71 var idx: i64 = k 72 if idx < 0 { idx = 0 } 73 // cos table *1000 for 24 evenly spaced angles (15 deg steps) 74 var co: i64 = 1000 75 var si: i64 = 0 76 if idx == 0 { co = 1000; si = 0 } 77 if idx == 1 { co = 966; si = 259 } 78 if idx == 2 { co = 866; si = 500 } 79 if idx == 3 { co = 707; si = 707 } 80 if idx == 4 { co = 500; si = 866 } 81 if idx == 5 { co = 259; si = 966 } 82 if idx == 6 { co = 0; si = 1000 } 83 if idx == 7 { co = 0 - 259; si = 966 } 84 if idx == 8 { co = 0 - 500; si = 866 } 85 if idx == 9 { co = 0 - 707; si = 707 } 86 if idx == 10 { co = 0 - 866; si = 500 } 87 if idx == 11 { co = 0 - 966; si = 259 } 88 if idx == 12 { co = 0 - 1000; si = 0 } 89 if idx == 13 { co = 0 - 966; si = 0 - 259 } 90 if idx == 14 { co = 0 - 866; si = 0 - 500 } 91 if idx == 15 { co = 0 - 707; si = 0 - 707 } 92 if idx == 16 { co = 0 - 500; si = 0 - 866 } 93 if idx == 17 { co = 0 - 259; si = 0 - 966 } 94 if idx == 18 { co = 0; si = 0 - 1000 } 95 if idx == 19 { co = 259; si = 0 - 966 } 96 if idx == 20 { co = 500; si = 0 - 866 } 97 if idx == 21 { co = 707; si = 0 - 707 } 98 if idx == 22 { co = 866; si = 0 - 500 } 99 if idx == 23 { co = 966; si = 0 - 259 } 100 out_sin[0] = si 101 return co 102} 103 104// Node centre for node index i of nc total -> (x,y) via out_x/out_y. Position = 105// ring point at table slot (i * 24 / nc), so nodes spread evenly regardless of 106// count. Deterministic. 107func nx_wgraph_node_xy(i: i64, nc: i64, out_x: *i64, out_y: *i64) -> i64 { 108 var slot: i64 = 0 109 if nc > 0 { slot = (i * NX_WGRAPH_TBL) / nc } 110 let slot_mod: i64 = slot - ((slot / NX_WGRAPH_TBL) * NX_WGRAPH_TBL) 111 let sib: *i64 = sys_mmap(8) as *i64 112 let co: i64 = nx_wgraph_unit(slot_mod, sib) 113 let si: i64 = sib[0] 114 out_x[0] = NX_WGRAPH_CX + ((co * NX_WGRAPH_R) / 1000) 115 out_y[0] = NX_WGRAPH_CY + ((si * NX_WGRAPH_R) / 1000) 116 return NX_WGRAPH_OK 117} 118 119// ===== build: nodes = pages; edges = resolvable forward links ================ 120// 121// Fills g from store: node_count = doc_count; for each page, extract forward 122// [[wikilinks]], resolve each target to a node rowid (linear slug match); a 123// resolved target -> an edge (src_rowid -> dst_rowid); an unresolved target -> 124// broken_count++. Self-links (a page linking itself) are kept as edges (they 125// are real references; the renderer draws them as a tiny loop-less marker). 126// Returns NX_WGRAPH_OK or -verdict. 127 128func nx_wiki_graph_build(store: *NxWikiDocStore, g: *NxWikiGraph) -> i64 { 129 if (g as i64) == 0 { return 0 - NX_WGRAPH_BAD_INPUT } 130 if (store as i64) == 0 { return 0 - NX_WGRAPH_BAD_INPUT } 131 if store.valid != 1 { return 0 - NX_WGRAPH_BAD_INPUT } 132 133 g.edge_src = sys_mmap(NX_WGRAPH_MAX_EDGES * 8) as *i64 134 g.edge_dst = sys_mmap(NX_WGRAPH_MAX_EDGES * 8) as *i64 135 g.edges_cap = NX_WGRAPH_MAX_EDGES 136 g.edge_count = 0 137 g.broken_count = 0 138 g.valid = 1 139 140 let dc: i64 = nx_wiki_doc_store_count(store) 141 g.node_count = dc 142 143 // per-page forward-link scratch 144 let foffs: *i64 = sys_mmap(NX_WGRAPH_MAX_LINKS_PP * 8) as *i64 145 let flens: *i64 = sys_mmap(NX_WGRAPH_MAX_LINKS_PP * 8) as *i64 146 let lc: *i64 = sys_mmap(8) as *i64 147 // lookup out-params (source page) 148 let tp: *i64 = sys_mmap(8) as *i64 149 let tn: *i64 = sys_mmap(8) as *i64 150 let up: *i64 = sys_mmap(8) as *i64 151 let un: *i64 = sys_mmap(8) as *i64 152 let bp: *i64 = sys_mmap(8) as *i64 153 let bn: *i64 = sys_mmap(8) as *i64 154 // lookup out-params (candidate target page) 155 let t2p: *i64 = sys_mmap(8) as *i64 156 let t2n: *i64 = sys_mmap(8) as *i64 157 let u2p: *i64 = sys_mmap(8) as *i64 158 let u2n: *i64 = sys_mmap(8) as *i64 159 let b2p: *i64 = sys_mmap(8) as *i64 160 let b2n: *i64 = sys_mmap(8) as *i64 161 162 var r: i64 = 0 163 while r < dc { 164 if r >= NX_WGRAPH_MAX_NODES { return 0 - NX_WGRAPH_LOOP_BUDGET } 165 let rc_lk: i64 = nx_wiki_doc_store_lookup(store, r, tp, tn, up, un, bp, bn) 166 if rc_lk == NX_WIB_OK { 167 let body: *u8 = bp[0] as *u8 168 let body_n: i64 = bn[0] 169 let rc_fl: i64 = nx_wiki_forward_links(body, body_n, foffs, flens, NX_WGRAPH_MAX_LINKS_PP, lc) 170 if rc_fl == NX_WBL_OK { 171 var k: i64 = 0 172 while k < lc[0] { 173 if k >= NX_WGRAPH_MAX_LINKS_PP { k = lc[0] } 174 if k < lc[0] { 175 let lp: *u8 = (body as i64 + foffs[k]) as *u8 176 let ln: i64 = flens[k] 177 // resolve target -> node rowid via linear slug match 178 var dst: i64 = 0 - 1 179 var c: i64 = 0 180 while c < dc { 181 if dst < 0 { 182 let rc_c: i64 = nx_wiki_doc_store_lookup(store, c, t2p, t2n, u2p, u2n, b2p, b2n) 183 if rc_c == NX_WIB_OK { 184 if nx_wiki_backlink_slug_matches(lp, ln, u2p[0] as *u8, u2n[0]) == 1 { dst = c } 185 } 186 } 187 c = c + 1 188 } 189 if dst >= 0 { 190 if g.edge_count >= g.edges_cap { return 0 - NX_WGRAPH_OVERFLOW } 191 g.edge_src[g.edge_count] = r 192 g.edge_dst[g.edge_count] = dst 193 g.edge_count = g.edge_count + 1 194 } 195 if dst < 0 { 196 g.broken_count = g.broken_count + 1 197 } 198 } 199 k = k + 1 200 } 201 } 202 } 203 r = r + 1 204 } 205 return NX_WGRAPH_OK 206} 207 208func nx_wiki_graph_node_count(g: *NxWikiGraph) -> i64 { 209 if g.valid != 1 { return 0 } 210 return g.node_count 211} 212func nx_wiki_graph_edge_count(g: *NxWikiGraph) -> i64 { 213 if g.valid != 1 { return 0 } 214 return g.edge_count 215} 216func nx_wiki_graph_broken_count(g: *NxWikiGraph) -> i64 { 217 if g.valid != 1 { return 0 } 218 return g.broken_count 219} 220 221// ===== static SVG render ===================================================== 222// 223// Emits a complete <svg>...</svg> (NUL-terminated) into out (cap): edges as 224// <line> first (so nodes draw on top), then per node a <circle> + <text> label 225// (the page title). No script, no JS. Returns bytes written (>=0) or -verdict. 226 227func nx_wgraph_emit(out: *u8, off: i64, cap: i64, s: *u8) -> i64 { 228 var i: i64 = 0 229 while s[i] != (0 as u8) { 230 if off + i >= cap { return 0 - NX_WGRAPH_OVERFLOW } 231 out[off + i] = s[i] 232 i = i + 1 233 } 234 return off + i 235} 236func nx_wgraph_emit_bytes(out: *u8, off: i64, cap: i64, src: *u8, n: i64) -> i64 { 237 var i: i64 = 0 238 while i < n { 239 if i >= NX_WGRAPH_LABEL_GUARD { return 0 - NX_WGRAPH_OVERFLOW } 240 if off + i >= cap { return 0 - NX_WGRAPH_OVERFLOW } 241 out[off + i] = src[i] 242 i = i + 1 243 } 244 return off + i 245} 246// append decimal of v into out at off; returns new off (or -overflow) 247func nx_wgraph_emit_num(out: *u8, off: i64, cap: i64, v: i64) -> i64 { 248 var m: i64 = v 249 var o: i64 = off 250 if m < 0 { 251 if o >= cap { return 0 - NX_WGRAPH_OVERFLOW } 252 out[o] = 0x2D as u8 // '-' 253 o = o + 1 254 m = 0 - m 255 } 256 let t: *u8 = sys_mmap(28) 257 var k: i64 = 0 258 if m == 0 { t[0] = 48 as u8; k = 1 } 259 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 260 var i: i64 = 0 261 while i < k { 262 if o >= cap { return 0 - NX_WGRAPH_OVERFLOW } 263 out[o] = t[k - 1 - i] 264 o = o + 1 265 i = i + 1 266 } 267 return o 268} 269 270func nx_wiki_graph_render_svg(store: *NxWikiGraph, doc_store: *NxWikiDocStore, 271 out: *u8, cap: i64, out_used: *i64) -> i64 { 272 if (out_used as i64) == 0 { return 0 - NX_WGRAPH_BAD_INPUT } 273 out_used[0] = 0 274 if (out as i64) == 0 { return 0 - NX_WGRAPH_BAD_INPUT } 275 if cap < 256 { return 0 - NX_WGRAPH_BAD_INPUT } 276 if (store as i64) == 0 { return 0 - NX_WGRAPH_BAD_INPUT } 277 if store.valid != 1 { return 0 - NX_WGRAPH_BAD_INPUT } 278 279 let g: *NxWikiGraph = store 280 let nc: i64 = g.node_count 281 282 var o: i64 = 0 283 // svg open + viewBox + a small stylesheet (CSS only, sovereign, no JS) 284 o = nx_wgraph_emit(out, o, cap, "<svg class=\"nx-graph\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 " as *u8) 285 if o < 0 { return o } 286 o = nx_wgraph_emit_num(out, o, cap, NX_WGRAPH_W) 287 if o < 0 { return o } 288 o = nx_wgraph_emit(out, o, cap, " " as *u8) 289 if o < 0 { return o } 290 o = nx_wgraph_emit_num(out, o, cap, NX_WGRAPH_H) 291 if o < 0 { return o } 292 o = nx_wgraph_emit(out, o, cap, "\" role=\"img\" aria-label=\"Wiki link graph\">" as *u8) 293 if o < 0 { return o } 294 o = nx_wgraph_emit(out, o, cap, "<style>.nx-graph .e{stroke:#94a3b8;stroke-width:1.4}.nx-graph .n{fill:#2c7a7b}.nx-graph .l{fill:#1a202c;font:12px system-ui,sans-serif}</style>" as *u8) 295 if o < 0 { return o } 296 297 // scratch for node positions 298 let xb: *i64 = sys_mmap(8) as *i64 299 let yb: *i64 = sys_mmap(8) as *i64 300 let x2b: *i64 = sys_mmap(8) as *i64 301 let y2b: *i64 = sys_mmap(8) as *i64 302 // doc-store lookup out-params 303 let tp: *i64 = sys_mmap(8) as *i64 304 let tn: *i64 = sys_mmap(8) as *i64 305 let up: *i64 = sys_mmap(8) as *i64 306 let un: *i64 = sys_mmap(8) as *i64 307 let bp: *i64 = sys_mmap(8) as *i64 308 let bn: *i64 = sys_mmap(8) as *i64 309 310 // ---- edges (drawn first, under the nodes) ---- 311 o = nx_wgraph_emit(out, o, cap, "<g class=\"edges\">" as *u8) 312 if o < 0 { return o } 313 var e: i64 = 0 314 while e < g.edge_count { 315 if e >= NX_WGRAPH_MAX_EDGES { e = g.edge_count } 316 if e < g.edge_count { 317 nx_wgraph_node_xy(g.edge_src[e], nc, xb, yb) 318 nx_wgraph_node_xy(g.edge_dst[e], nc, x2b, y2b) 319 o = nx_wgraph_emit(out, o, cap, "<line class=\"e\" x1=\"" as *u8) 320 if o < 0 { return o } 321 o = nx_wgraph_emit_num(out, o, cap, xb[0]) 322 if o < 0 { return o } 323 o = nx_wgraph_emit(out, o, cap, "\" y1=\"" as *u8) 324 if o < 0 { return o } 325 o = nx_wgraph_emit_num(out, o, cap, yb[0]) 326 if o < 0 { return o } 327 o = nx_wgraph_emit(out, o, cap, "\" x2=\"" as *u8) 328 if o < 0 { return o } 329 o = nx_wgraph_emit_num(out, o, cap, x2b[0]) 330 if o < 0 { return o } 331 o = nx_wgraph_emit(out, o, cap, "\" y2=\"" as *u8) 332 if o < 0 { return o } 333 o = nx_wgraph_emit_num(out, o, cap, y2b[0]) 334 if o < 0 { return o } 335 o = nx_wgraph_emit(out, o, cap, "\"/>" as *u8) 336 if o < 0 { return o } 337 } 338 e = e + 1 339 } 340 o = nx_wgraph_emit(out, o, cap, "</g>" as *u8) 341 if o < 0 { return o } 342 343 // ---- nodes + labels ---- 344 o = nx_wgraph_emit(out, o, cap, "<g class=\"nodes\">" as *u8) 345 if o < 0 { return o } 346 var i: i64 = 0 347 while i < nc { 348 if i >= NX_WGRAPH_MAX_NODES { i = nc } 349 if i < nc { 350 nx_wgraph_node_xy(i, nc, xb, yb) 351 // circle 352 o = nx_wgraph_emit(out, o, cap, "<circle class=\"n\" cx=\"" as *u8) 353 if o < 0 { return o } 354 o = nx_wgraph_emit_num(out, o, cap, xb[0]) 355 if o < 0 { return o } 356 o = nx_wgraph_emit(out, o, cap, "\" cy=\"" as *u8) 357 if o < 0 { return o } 358 o = nx_wgraph_emit_num(out, o, cap, yb[0]) 359 if o < 0 { return o } 360 o = nx_wgraph_emit(out, o, cap, "\" r=\"" as *u8) 361 if o < 0 { return o } 362 o = nx_wgraph_emit_num(out, o, cap, NX_WGRAPH_NODE_R) 363 if o < 0 { return o } 364 o = nx_wgraph_emit(out, o, cap, "\"/>" as *u8) 365 if o < 0 { return o } 366 // label = page title (offset a few px right of the dot) 367 let rc_lk: i64 = nx_wiki_doc_store_lookup(doc_store, i, tp, tn, up, un, bp, bn) 368 if rc_lk == NX_WIB_OK { 369 o = nx_wgraph_emit(out, o, cap, "<text class=\"l\" x=\"" as *u8) 370 if o < 0 { return o } 371 o = nx_wgraph_emit_num(out, o, cap, xb[0] + 10) 372 if o < 0 { return o } 373 o = nx_wgraph_emit(out, o, cap, "\" y=\"" as *u8) 374 if o < 0 { return o } 375 o = nx_wgraph_emit_num(out, o, cap, yb[0] + 4) 376 if o < 0 { return o } 377 o = nx_wgraph_emit(out, o, cap, "\">" as *u8) 378 if o < 0 { return o } 379 o = nx_wgraph_emit_bytes(out, o, cap, tp[0] as *u8, tn[0]) 380 if o < 0 { return o } 381 o = nx_wgraph_emit(out, o, cap, "</text>" as *u8) 382 if o < 0 { return o } 383 } 384 } 385 i = i + 1 386 } 387 o = nx_wgraph_emit(out, o, cap, "</g></svg>" as *u8) 388 if o < 0 { return o } 389 390 out[o] = 0 as u8 391 out_used[0] = o 392 return NX_WGRAPH_OK 393}