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}