nx_wiki_backlinks.nx source
↩ module page · 350 lines · 14942 B
1// nx_wiki_backlinks.nx -- wiki R4: "what links here" reverse-link index.
2//
3// COMPOSES (zero new storage/parse substrate introduced):
4// nx_wiki_index_builder NxWikiDocStore: doc_count + per-rowid
5// (title, url, body) lookup -- the page corpus.
6// nx_syscalls sys_mmap for the reverse-map scratch.
7//
8// WHAT IT DOES:
9// Every wiki page body carries forward [[wikilink]] cross-references
10// (resolved/rendered by nx_wiki_doc_render). This organ INVERTS them:
11// given a target slug, it enumerates the source pages whose bodies link
12// TO it. That is the classic wiki "What links here" affordance, computed
13// from ground truth (the actual bodies in the doc store), never a
14// hand-maintained list.
15//
16// IDENTITY MODEL:
17// A page's identity ("slug") is its store URL field as-stored. The wiki
18// convention is a bare name (e.g. "alpha") or "/wiki/alpha"; a [[alpha]]
19// wikilink targets that same slug. nx_wiki_backlink_slug_matches compares
20// a link target against a stored slug allowing the optional "/wiki/"
21// prefix on either side, so both conventions resolve.
22//
23// [[cite:...]] links are SKIPPED -- those are supporting-source citations
24// handled by nx_wiki_cite_render, not page-to-page cross-references.
25//
26// Hygiene (NISHI_CODE_HYGIENE_STANDARD): M1 out-params not *T returns;
27// M3 every while has a hard iter cap; M5 every buffer index bounded;
28// M6 no pretend stubs; M7 named sizing constants; M8 verdicts propagated.
29//
30// Status: V1 (wiki R4). 2026-06-15. license_tier: ORIGINAL
31import "nx_syscalls.nx"
32import "nx_wiki_index_builder.nx"
33
34// ===== Sealed verdict surface (codes 2640-2659) ==============================
35const NX_WBL_OK: i64 = 0
36const NX_WBL_BAD_INPUT: i64 = 2640
37const NX_WBL_OVERFLOW: i64 = 2641
38const NX_WBL_LOOP_BUDGET: i64 = 2642
39
40// ===== Named sizing constants (M7) ===========================================
41const NX_WBL_MAX_LINKS_PER_PAGE: i64 = 512 // forward links scanned per body
42const NX_WBL_MAX_LINK_LEN: i64 = 256 // a single [[target]] name cap
43const NX_WBL_MAX_PAGES: i64 = 1000 // mirrors doc-store docs cap
44const NX_WBL_SCAN_BUDGET: i64 = 8000000 // per-body byte-scan cap (M3)
45const NX_WBL_WIKI_PREFIX_LEN: i64 = 6 // len("/wiki/")
46
47// Common ASCII (M7)
48const NX_WBL_LBRACKET: i64 = 0x5B // '['
49const NX_WBL_RBRACKET: i64 = 0x5D // ']'
50const NX_WBL_COLON: i64 = 0x3A // ':'
51
52// ===== small str helpers =====================================================
53func nx_wbl_slen(s: *u8) -> i64 {
54 var n: i64 = 0
55 while n < NX_WBL_SCAN_BUDGET {
56 if s[n] == (0 as u8) { return n }
57 n = n + 1
58 }
59 return n
60}
61
62// Strip a leading "/wiki/" from (p,n); returns the inner pointer via out_ptr
63// and inner length as the function result. Defensive: if shorter than the
64// prefix or no match, returns the input unchanged.
65func nx_wbl_strip_wiki(p: *u8, n: i64, out_ptr: *i64) -> i64 {
66 out_ptr[0] = p as i64
67 if n < NX_WBL_WIKI_PREFIX_LEN { return n }
68 if p[0] != (0x2F as u8) { return n } // '/'
69 if p[1] != (0x77 as u8) { return n } // 'w'
70 if p[2] != (0x69 as u8) { return n } // 'i'
71 if p[3] != (0x6B as u8) { return n } // 'k'
72 if p[4] != (0x69 as u8) { return n } // 'i'
73 if p[5] != (0x2F as u8) { return n } // '/'
74 out_ptr[0] = (p as i64) + NX_WBL_WIKI_PREFIX_LEN
75 return n - NX_WBL_WIKI_PREFIX_LEN
76}
77
78// Does a [[wikilink]] target (lp,ln) name the same page as stored slug (sp,sn)?
79// Compares with the optional "/wiki/" prefix stripped from BOTH sides, so
80// "alpha", "/wiki/alpha" all match each other. Case-sensitive on the bare name
81// (wiki slugs are canonical lowercase by convention).
82func nx_wiki_backlink_slug_matches(lp: *u8, ln: i64, sp: *u8, sn: i64) -> i64 {
83 if ln < 1 { return 0 }
84 if sn < 1 { return 0 }
85 let lib: *i64 = sys_mmap(8) as *i64
86 let sib: *i64 = sys_mmap(8) as *i64
87 let lin: i64 = nx_wbl_strip_wiki(lp, ln, lib)
88 let sin: i64 = nx_wbl_strip_wiki(sp, sn, sib)
89 if lin != sin { return 0 }
90 if lin < 1 { return 0 }
91 let la: *u8 = lib[0] as *u8
92 let sa: *u8 = sib[0] as *u8
93 var i: i64 = 0
94 var eq: i64 = 1
95 while i < lin {
96 if la[i] != sa[i] { eq = 0 }
97 i = i + 1
98 }
99 return eq
100}
101
102// ===== Forward-link extractor (the [[wikilink]] scanner, R4-reusable) ========
103//
104// Walks body (p,n); for each [[name]] (NOT [[cite:...]]) records the name's
105// (offset,len) into the caller's parallel arrays offs[]/lens[] (capacity cap).
106// Returns the count found (>=0) or -verdict. This is the single forward-link
107// truth source the backlinks inversion, the TOC's link audit and the graph
108// edge builder all share -- no three divergent scanners.
109//
110// A name is "cite" iff it begins with "cite:". Those are skipped (not counted).
111func nx_wiki_forward_links(body: *u8, n: i64,
112 offs: *i64, lens: *i64, cap: i64,
113 out_count: *i64) -> i64 {
114 if (out_count as i64) == 0 { return 0 - NX_WBL_BAD_INPUT }
115 out_count[0] = 0
116 if (body as i64) == 0 { return NX_WBL_OK }
117 if n < 0 { return 0 - NX_WBL_BAD_INPUT }
118 var i: i64 = 0
119 var cnt: i64 = 0
120 var iter: i64 = 0
121 while i < n {
122 if iter >= NX_WBL_SCAN_BUDGET { return 0 - NX_WBL_LOOP_BUDGET }
123 iter = iter + 1
124 var advanced: i64 = 0
125 // Look for "[[" at i.
126 if i + 1 < n {
127 if body[i] == (NX_WBL_LBRACKET as u8) {
128 if body[i + 1] == (NX_WBL_LBRACKET as u8) {
129 let start_name: i64 = i + 2
130 // scan for the FIRST "]]" at/after start_name, within the
131 // per-link length cap. cur walks; on a "]]" hit we record
132 // end_name and stop via done. A clean single-exit loop --
133 // no cap-guard clobbering the frozen end position.
134 var cur: i64 = start_name
135 var end_name: i64 = start_name
136 var found: i64 = 0
137 var done: i64 = 0
138 while done == 0 {
139 if cur >= n - 1 { done = 1 }
140 if cur - start_name >= NX_WBL_MAX_LINK_LEN { done = 1 }
141 if done == 0 {
142 if body[cur] == (NX_WBL_RBRACKET as u8) {
143 if body[cur + 1] == (NX_WBL_RBRACKET as u8) {
144 end_name = cur
145 found = 1
146 done = 1
147 }
148 }
149 if found == 0 { cur = cur + 1 }
150 }
151 }
152 if found == 1 {
153 let name_off: i64 = start_name
154 let name_n: i64 = end_name - start_name
155 // is it a [[cite:...]] ? (skip)
156 var is_cite: i64 = 0
157 if name_n >= 5 {
158 if body[name_off] == (0x63 as u8) { // 'c'
159 if body[name_off + 1] == (0x69 as u8) { // 'i'
160 if body[name_off + 2] == (0x74 as u8) { // 't'
161 if body[name_off + 3] == (0x65 as u8) { // 'e'
162 if body[name_off + 4] == (NX_WBL_COLON as u8) { is_cite = 1 }
163 }
164 }
165 }
166 }
167 }
168 if name_n > 0 {
169 if name_n <= NX_WBL_MAX_LINK_LEN {
170 if is_cite == 0 {
171 if cnt >= cap { return 0 - NX_WBL_OVERFLOW }
172 if cnt >= NX_WBL_MAX_LINKS_PER_PAGE { return 0 - NX_WBL_OVERFLOW }
173 offs[cnt] = name_off
174 lens[cnt] = name_n
175 cnt = cnt + 1
176 }
177 }
178 }
179 i = end_name + 2
180 advanced = 1
181 }
182 }
183 }
184 }
185 if advanced == 0 { i = i + 1 }
186 }
187 out_count[0] = cnt
188 return NX_WBL_OK
189}
190
191// ===== Reverse map: backlinks_of(store, slug) -> source rowids ===============
192//
193// For target slug (slug,slug_n): scan every page in the store; if a page's
194// body contains a forward [[wikilink]] resolving to this slug, record that
195// page's rowid into out_rowids[] (capacity cap). A page is recorded AT MOST
196// once even if it links the target several times. Returns the count of
197// distinct source pages (>=0) or -verdict.
198func nx_wiki_backlinks_of(store: *NxWikiDocStore,
199 slug: *u8, slug_n: i64,
200 out_rowids: *i64, cap: i64,
201 out_count: *i64) -> i64 {
202 if (out_count as i64) == 0 { return 0 - NX_WBL_BAD_INPUT }
203 out_count[0] = 0
204 if (store as i64) == 0 { return 0 - NX_WBL_BAD_INPUT }
205 if store.valid != 1 { return 0 - NX_WBL_BAD_INPUT }
206 if slug_n < 1 { return 0 - NX_WBL_BAD_INPUT }
207
208 let dc: i64 = nx_wiki_doc_store_count(store)
209 // per-page forward-link scratch (offsets+lens)
210 let foffs: *i64 = sys_mmap(NX_WBL_MAX_LINKS_PER_PAGE * 8) as *i64
211 let flens: *i64 = sys_mmap(NX_WBL_MAX_LINKS_PER_PAGE * 8) as *i64
212 let lc: *i64 = sys_mmap(8) as *i64
213 // lookup out-params
214 let tp: *i64 = sys_mmap(8) as *i64
215 let tn: *i64 = sys_mmap(8) as *i64
216 let up: *i64 = sys_mmap(8) as *i64
217 let un: *i64 = sys_mmap(8) as *i64
218 let bp: *i64 = sys_mmap(8) as *i64
219 let bn: *i64 = sys_mmap(8) as *i64
220
221 var cnt: i64 = 0
222 var r: i64 = 0
223 while r < dc {
224 if r >= NX_WBL_MAX_PAGES { return 0 - NX_WBL_LOOP_BUDGET }
225 let rc_lk: i64 = nx_wiki_doc_store_lookup(store, r, tp, tn, up, un, bp, bn)
226 if rc_lk == NX_WIB_OK {
227 let body: *u8 = bp[0] as *u8
228 let body_n: i64 = bn[0]
229 let rc_fl: i64 = nx_wiki_forward_links(body, body_n, foffs, flens, NX_WBL_MAX_LINKS_PER_PAGE, lc)
230 if rc_fl == NX_WBL_OK {
231 var k: i64 = 0
232 var hit: i64 = 0
233 while k < lc[0] {
234 if hit == 0 {
235 let lp: *u8 = (body as i64 + foffs[k]) as *u8
236 if nx_wiki_backlink_slug_matches(lp, flens[k], slug, slug_n) == 1 { hit = 1 }
237 }
238 k = k + 1
239 }
240 if hit == 1 {
241 if cnt >= cap { return 0 - NX_WBL_OVERFLOW }
242 out_rowids[cnt] = r
243 cnt = cnt + 1
244 }
245 }
246 }
247 r = r + 1
248 }
249 out_count[0] = cnt
250 return NX_WBL_OK
251}
252
253// ===== "What links here" panel render ========================================
254//
255// Emits an <aside class="nx-backlinks"> listing every source page that links
256// to target slug, each as <a href="/wiki/<src-slug>"><src-title></a>. Empty
257// state when none. Writes NUL-terminated HTML into out (cap); returns bytes
258// written (excluding the NUL, >=0) or -verdict.
259//
260// Title/slug text is copied verbatim from the store (already-trusted internal
261// content per Cardinal 12); the served pipeline html-escapes upstream.
262
263func nx_wbl_emit(out: *u8, off: i64, cap: i64, s: *u8) -> i64 {
264 var i: i64 = 0
265 while s[i] != (0 as u8) {
266 if off + i >= cap { return 0 - NX_WBL_OVERFLOW }
267 out[off + i] = s[i]
268 i = i + 1
269 }
270 return off + i
271}
272func nx_wbl_emit_bytes(out: *u8, off: i64, cap: i64, src: *u8, n: i64) -> i64 {
273 var i: i64 = 0
274 while i < n {
275 if i >= NX_WBL_MAX_LINK_LEN { return 0 - NX_WBL_OVERFLOW }
276 if off + i >= cap { return 0 - NX_WBL_OVERFLOW }
277 out[off + i] = src[i]
278 i = i + 1
279 }
280 return off + i
281}
282
283func nx_wiki_backlinks_panel(store: *NxWikiDocStore,
284 slug: *u8, slug_n: i64,
285 out: *u8, cap: i64,
286 out_used: *i64) -> i64 {
287 if (out_used as i64) == 0 { return 0 - NX_WBL_BAD_INPUT }
288 out_used[0] = 0
289 if (out as i64) == 0 { return 0 - NX_WBL_BAD_INPUT }
290 if cap < 64 { return 0 - NX_WBL_BAD_INPUT }
291 if (store as i64) == 0 { return 0 - NX_WBL_BAD_INPUT }
292 if store.valid != 1 { return 0 - NX_WBL_BAD_INPUT }
293
294 let rowids: *i64 = sys_mmap(NX_WBL_MAX_PAGES * 8) as *i64
295 let bc: *i64 = sys_mmap(8) as *i64
296 let rc_b: i64 = nx_wiki_backlinks_of(store, slug, slug_n, rowids, NX_WBL_MAX_PAGES, bc)
297 if rc_b != NX_WBL_OK { return rc_b }
298
299 var o: i64 = 0
300 o = nx_wbl_emit(out, o, cap, "<aside class=\"nx-backlinks\"><h2>What links here</h2>" as *u8)
301 if o < 0 { return o }
302
303 if bc[0] == 0 {
304 o = nx_wbl_emit(out, o, cap, "<p class=\"nx-backlinks-empty\">No other pages link here yet.</p></aside>" as *u8)
305 if o < 0 { return o }
306 out[o] = 0 as u8
307 out_used[0] = o
308 return NX_WBL_OK
309 }
310
311 o = nx_wbl_emit(out, o, cap, "<ul>" as *u8)
312 if o < 0 { return o }
313
314 // lookup out-params for each source page's title + url(slug)
315 let tp: *i64 = sys_mmap(8) as *i64
316 let tn: *i64 = sys_mmap(8) as *i64
317 let up: *i64 = sys_mmap(8) as *i64
318 let un: *i64 = sys_mmap(8) as *i64
319 let bp: *i64 = sys_mmap(8) as *i64
320 let bn: *i64 = sys_mmap(8) as *i64
321
322 var i: i64 = 0
323 while i < bc[0] {
324 if i >= NX_WBL_MAX_PAGES { return 0 - NX_WBL_LOOP_BUDGET }
325 let rid: i64 = rowids[i]
326 let rc_lk: i64 = nx_wiki_doc_store_lookup(store, rid, tp, tn, up, un, bp, bn)
327 if rc_lk == NX_WIB_OK {
328 o = nx_wbl_emit(out, o, cap, "<li><a href=\"/wiki/" as *u8)
329 if o < 0 { return o }
330 // href slug = stored url with any leading "/wiki/" stripped (so the
331 // emitted href is canonical "/wiki/<slug>" not "/wiki//wiki/<slug>").
332 let sib: *i64 = sys_mmap(8) as *i64
333 let sin: i64 = nx_wbl_strip_wiki(up[0] as *u8, un[0], sib)
334 o = nx_wbl_emit_bytes(out, o, cap, sib[0] as *u8, sin)
335 if o < 0 { return o }
336 o = nx_wbl_emit(out, o, cap, "\">" as *u8)
337 if o < 0 { return o }
338 o = nx_wbl_emit_bytes(out, o, cap, tp[0] as *u8, tn[0])
339 if o < 0 { return o }
340 o = nx_wbl_emit(out, o, cap, "</a></li>" as *u8)
341 if o < 0 { return o }
342 }
343 i = i + 1
344 }
345 o = nx_wbl_emit(out, o, cap, "</ul></aside>" as *u8)
346 if o < 0 { return o }
347 out[o] = 0 as u8
348 out_used[0] = o
349 return NX_WBL_OK
350}