nx_doc_xref.nx source
↩ module page · 555 lines · 19904 B
1// nx_doc_xref.nx -- cross-reference verification primitives.
2//
3// Companion to nx_doc_inref. Detects BROKEN [[X]] references where
4// the link target doesn't correspond to a real doc. Substrate-
5// honest doc-graph integrity check.
6//
7// Composes:
8// nx_string_ops (string scanning helpers)
9// nx_doc_inref (sibling primitive; same Zettelkasten [[name]]
10// convention)
11//
12// **Quadrant:** REFERENCE (Diátaxis)
13// **Topic Type:** REFERENCE (DITA)
14// **Status:** DRAFT
15// **Trust State:** WRITTEN_UNTESTED
16// **Competitive State:** UNCLASSIFIED
17
18import "nx_syscalls.nx"
19import "nx_string_ops.nx"
20import "nx_dir.nx"
21const K_MAGIC_2048: i64 = 2048
22const K_MAGIC_262144: i64 = 262144
23const K_MAGIC_65536: i64 = 65536
24const K_MAGIC_1024: i64 = 1024
25
26// Find next '[[NAME]]' in content starting at offset start_at.
27// Returns:
28// {-1, ?} -> no more links found
29// {N, n_target} -> N = byte offset PAST the closing ']]' (for
30// caller's next-iteration start); n_target =
31// length of extracted target name (written to
32// out_target, NUL-terminated)
33// Caller passes out_target buffer + out_cap; substrate-honest
34// rejects targets larger than out_cap with n_target = -1.
35
36struct NxLinkScanResult {
37 next_offset: i64, // byte offset past closing ']]' (-1 if no more)
38 n_target: i64, // length of extracted name (-1 if cap exceeded)
39}
40
41func nx_doc_extract_link_target(
42 content: *u8, n_content: i64,
43 start_at: i64,
44 out_target: *u8, out_cap: i64,
45 out_result: *NxLinkScanResult
46) {
47 out_result.next_offset = -1
48 out_result.n_target = 0
49 if start_at < 0 { return }
50 if start_at >= n_content { return }
51
52 // Find '[[' starting at start_at, SKIPPING backtick-code spans
53 // (substrate-honest: code samples that happen to use double-
54 // bracket syntax -- Apple Metal MSL '[[buffer(N)]]' / Apple
55 // clang's '[[nodiscard]]' / etc. -- are NOT Zettelkasten refs).
56 //
57 // Tracks inside_code state by toggling on each backtick char.
58 // Single backticks toggle once (inline span); triple backticks
59 // toggle three times (net: enter/exit code fence); approximate
60 // for nested double-backtick edge cases.
61 //
62 // We rewind inside_code from offset 0 not from start_at to keep
63 // state consistent across iterative scan calls (otherwise re-
64 // entering at start_at mid-fence would lose state).
65 var inside_code: i64 = 0
66 var rewind: i64 = 0
67 while rewind < start_at {
68 if content[rewind] == 96 { // '`'
69 if inside_code == 0 { inside_code = 1 } else { inside_code = 0 }
70 }
71 rewind = rewind + 1
72 }
73
74 var i: i64 = start_at
75 var found_open: i64 = -1
76 var scan_open: i64 = 1
77 while scan_open == 1 {
78 if i > n_content - 2 { scan_open = 0; continue }
79 if content[i] == 96 { // '`'
80 if inside_code == 0 { inside_code = 1 } else { inside_code = 0 }
81 i = i + 1
82 continue
83 }
84 if inside_code == 1 {
85 // We're inside a code span; skip this byte and keep going.
86 i = i + 1
87 continue
88 }
89 if content[i] == 91 { // '['
90 if content[i + 1] == 91 { // '['
91 found_open = i
92 scan_open = 0
93 continue
94 }
95 }
96 i = i + 1
97 }
98 if found_open < 0 { return }
99
100 // Find matching ']]' (flag-driven break).
101 var j: i64 = found_open + 2
102 var found_close: i64 = -1
103 var scan_close: i64 = 1
104 while scan_close == 1 {
105 if j > n_content - 2 { scan_close = 0; continue }
106 if content[j] == 93 { // ']'
107 if content[j + 1] == 93 { // ']'
108 found_close = j
109 scan_close = 0
110 continue
111 }
112 }
113 j = j + 1
114 }
115 if found_close < 0 { return }
116
117 // Extract name between [[ and ]].
118 let name_start: i64 = found_open + 2
119 let name_len: i64 = found_close - name_start
120 if name_len <= 0 { return }
121 if name_len + 1 > out_cap {
122 out_result.n_target = -1
123 out_result.next_offset = found_close + 2
124 return
125 }
126 var k: i64 = 0
127 while k < name_len {
128 out_target[k] = content[name_start + k]
129 k = k + 1
130 }
131 out_target[name_len] = 0 // NUL
132 out_result.n_target = name_len
133 out_result.next_offset = found_close + 2
134}
135
136// Validator: given a target name (e.g., 'feedback-foo' extracted
137// from '[[feedback-foo]]'), check if it appears in a flat list of
138// known doc names. Returns 1 if found, 0 if missing (broken ref).
139//
140// names_blob: caller-provided byte buffer with NEWLINE-separated
141// doc names (e.g., 'foo\nbar\nbaz\n').
142// n_blob: blob length in bytes.
143// target: name to look up (without .md suffix).
144// n_target: target length.
145
146func nx_doc_link_target_exists(
147 target: *u8, n_target: i64,
148 names_blob: *u8, n_blob: i64
149) -> i64 {
150 if n_target <= 0 { return 0 }
151 if n_blob <= 0 { return 0 }
152
153 var i: i64 = 0
154 while i < n_blob {
155 // Scan from i forward to first newline or n_blob (flag-driven
156 // break; NishiLang doesn't have a break keyword).
157 var line_end: i64 = i
158 var scanning: i64 = 1
159 while scanning == 1 {
160 if line_end >= n_blob { scanning = 0; continue }
161 if names_blob[line_end] == 10 { scanning = 0; continue }
162 line_end = line_end + 1
163 }
164 // line_end is at the newline or at n_blob (EOF).
165 let line_len: i64 = line_end - i
166 let line_ptr: *u8 = (names_blob as *u8) + i
167 if nx_str_equals(target, n_target, line_ptr, line_len) == 1 {
168 return 1
169 }
170 i = line_end + 1 // advance past newline
171 }
172 return 0
173}
174
175// ===== Substrate-wide walker =====================================
176//
177// Two-pass operation:
178// Pass 1: walk dir, collect every .md filename (without .md
179// suffix) into newline-separated names_blob.
180// Pass 2: walk dir again, for each doc open+read content, scan
181// for [[X]] patterns iteratively, validate each X via
182// nx_doc_link_target_exists. Count total refs + broken
183// refs. Emit per-broken-ref line to stdout.
184//
185// Output struct:
186
187struct NxDocXrefCounts {
188 n_docs_scanned: i64,
189 n_refs_total: i64,
190 n_refs_broken: i64,
191 n_names_indexed: i64,
192}
193
194func nx_doc_xref_counts_init(c: *NxDocXrefCounts) {
195 c.n_docs_scanned = 0
196 c.n_refs_total = 0
197 c.n_refs_broken = 0
198 c.n_names_indexed = 0
199}
200
201// Write a broken-ref line to stdout: " broken: <target> (in: <src>)\n".
202// Substrate-honest operational surface: caller can immediately
203// identify WHICH doc contains WHICH broken ref instead of having
204// to grep the whole corpus.
205func _docxref_write_broken(
206 target: *u8, n_target: i64,
207 src: *u8, n_src: i64
208) {
209 sys_write(1, " broken: ", 10)
210 sys_write(1, target, n_target)
211 sys_write(1, " (in: ", 7)
212 sys_write(1, src, n_src)
213 sys_write(1, ")\n", 2)
214}
215
216// Pass 1 helper: write '<name-without-md>\n' to names_blob; returns
217// new write offset. Returns -1 if blob cap exceeded.
218func _docxref_append_name(
219 name: *u8, n_name: i64,
220 blob: *u8, blob_off: i64, blob_cap: i64
221) -> i64 {
222 if n_name <= 3 { return blob_off } // not a .md filename
223 let base_len: i64 = n_name - 3
224 if blob_off + base_len + 1 > blob_cap { return -1 }
225 var i: i64 = 0
226 while i < base_len {
227 blob[blob_off + i] = name[i]
228 i = i + 1
229 }
230 blob[blob_off + base_len] = 10 // '\n'
231 return blob_off + base_len + 1
232}
233
234// Walker. Single nx_dir_list call (512-row cap). Per-pass logic:
235// Pass 1: iterate rows, append .md names to names_blob.
236// Pass 2: iterate rows, open each .md, read content, scan [[X]]
237// patterns, validate each, increment counts, emit broken
238// lines.
239//
240// Substrate-honest: 64 KiB read buffer per doc (same as nx_doc_census);
241// 64 KiB names_blob (>= 2000 names if avg 30 chars). Single-shot;
242// substrate-wide directory iteration in one nx_dir_list.
243
244func nx_doc_xref_walk_dir(
245 dir_path: *u8, n_dir_path: i64,
246 counts: *NxDocXrefCounts
247) -> i64 {
248 let rows_buf: *u8 = sys_mmap(K_MAGIC_2048 * NX_DIR_ROW_BYTES + 64)
249 let rows: *NxDirRow = rows_buf as *NxDirRow
250 let name_arena: *u8 = sys_mmap(K_MAGIC_262144)
251 let result_buf: *u8 = sys_mmap(NX_DIR_RESULT_BYTES + 16)
252 let result: *NxDirResult = result_buf as *NxDirResult
253
254 nx_dir_list(dir_path, rows, K_MAGIC_2048, name_arena, K_MAGIC_262144, 0, result)
255 if result.verdict != NX_DIR_OK {
256 if result.verdict != NX_DIR_TRUNCATED { return -10 }
257 }
258
259 let md_suffix: *u8 = ".md"
260 let names_blob: *u8 = sys_mmap(K_MAGIC_262144)
261 var blob_off: i64 = 0
262
263 // -------- Pass 1: build names_blob --------
264 var i: i64 = 0
265 while i < result.n_filled {
266 let row: *NxDirRow = nx_dir_row_at(rows, i)
267 if row.is_dotlike == 1 { i = i + 1; continue }
268 if row.dtype != NX_DT_REG { i = i + 1; continue }
269 if nx_str_ends_with(row.name_ptr, row.name_len, md_suffix, 3) != 1 {
270 i = i + 1; continue
271 }
272 let new_off: i64 = _docxref_append_name(
273 row.name_ptr, row.name_len,
274 names_blob, blob_off, K_MAGIC_262144
275 )
276 if new_off < 0 {
277 // names_blob exhausted; substrate-honest: stop indexing
278 // additional names but continue scanning what we have
279 i = result.n_filled
280 continue
281 }
282 blob_off = new_off
283 counts.n_names_indexed = counts.n_names_indexed + 1
284 i = i + 1
285 }
286
287 // -------- Pass 2: scan each doc, validate refs --------
288 let hdr_buf: *u8 = sys_mmap(K_MAGIC_65536)
289 let path_buf: *u8 = sys_mmap(K_MAGIC_1024)
290 let target_buf: *u8 = sys_mmap(256)
291 let res_buf: *u8 = sys_mmap(32)
292 let res: *NxLinkScanResult = res_buf as *NxLinkScanResult
293
294 var j: i64 = 0
295 while j < result.n_filled {
296 let row: *NxDirRow = nx_dir_row_at(rows, j)
297 if row.is_dotlike == 1 { j = j + 1; continue }
298 if row.dtype != NX_DT_REG { j = j + 1; continue }
299 if nx_str_ends_with(row.name_ptr, row.name_len, md_suffix, 3) != 1 {
300 j = j + 1; continue
301 }
302
303 // Build path + open + read.
304 if n_dir_path + 1 + row.name_len + 1 > K_MAGIC_1024 {
305 j = j + 1; continue
306 }
307 var k: i64 = 0
308 while k < n_dir_path {
309 path_buf[k] = dir_path[k]
310 k = k + 1
311 }
312 path_buf[n_dir_path] = 47 // '/'
313 var m: i64 = 0
314 while m < row.name_len {
315 path_buf[n_dir_path + 1 + m] = row.name_ptr[m]
316 m = m + 1
317 }
318 path_buf[n_dir_path + 1 + row.name_len] = 0 // NUL
319
320 let fd: i64 = sys_openat_rd(path_buf)
321 if fd < 0 { j = j + 1; continue }
322
323 // Read content (full-file loop, 64 KiB cap).
324 var content_len: i64 = 0
325 var keep_reading: i64 = 1
326 while keep_reading == 1 {
327 let remaining: i64 = K_MAGIC_65536 - content_len
328 if remaining <= 0 { keep_reading = 0; continue }
329 let dst: *u8 = (hdr_buf as *u8) + content_len
330 let n_chunk: i64 = sys_read(fd, dst, remaining)
331 if n_chunk <= 0 { keep_reading = 0; continue }
332 content_len = content_len + n_chunk
333 }
334 sys_close(fd)
335
336 // Iterate [[X]] patterns in content.
337 var pos: i64 = 0
338 var scan: i64 = 1
339 while scan == 1 {
340 nx_doc_extract_link_target(hdr_buf, content_len, pos, target_buf, 256, res)
341 if res.next_offset < 0 { scan = 0; continue }
342 pos = res.next_offset
343 if res.n_target <= 0 { continue } // cap exceeded; skip but continue
344 counts.n_refs_total = counts.n_refs_total + 1
345 if nx_doc_link_target_exists(target_buf, res.n_target, names_blob, blob_off) != 1 {
346 counts.n_refs_broken = counts.n_refs_broken + 1
347 _docxref_write_broken(target_buf, res.n_target, row.name_ptr, row.name_len)
348 }
349 }
350
351 counts.n_docs_scanned = counts.n_docs_scanned + 1
352 j = j + 1
353 }
354 return 0
355}
356
357func nx_doc_xref_report_stdout(c: *NxDocXrefCounts) {
358 sys_write(1, "nx_doc_xref verdict:\n", 21)
359 let lbl_d: *u8 = " docs_scanned: "
360 sys_write(1, lbl_d, 18)
361 let buf: *u8 = sys_mmap(32)
362 let n1: i64 = nx_str_format_int(c.n_docs_scanned, buf, 32)
363 sys_write(1, buf, n1)
364 sys_write(1, "\n", 1)
365 let lbl_n: *u8 = " names_indexed: "
366 sys_write(1, lbl_n, 18)
367 let n2: i64 = nx_str_format_int(c.n_names_indexed, buf, 32)
368 sys_write(1, buf, n2)
369 sys_write(1, "\n", 1)
370 let lbl_t: *u8 = " refs_total: "
371 sys_write(1, lbl_t, 18)
372 let n3: i64 = nx_str_format_int(c.n_refs_total, buf, 32)
373 sys_write(1, buf, n3)
374 sys_write(1, "\n", 1)
375 let lbl_b: *u8 = " refs_broken: "
376 sys_write(1, lbl_b, 18)
377 let n4: i64 = nx_str_format_int(c.n_refs_broken, buf, 32)
378 sys_write(1, buf, n4)
379 sys_write(1, "\n", 1)
380}
381
382// ===== Multi-namespace xref ======================================
383//
384// Substrate has TWO doc namespaces:
385// nxc2/docs/ (substrate doctrine + roadmaps)
386// .claude/projects/.../memory/ (feedback + project + reference)
387//
388// V2 doctrine implicitly mixed them; V3 declares the topology
389// explicitly. Multi-namespace xref validates [[X]] refs against
390// BOTH blobs -- a ref is broken only if missing from BOTH.
391
392// Build names_blob from a single directory (reusable Pass-1 logic).
393// Walks dir_path via nx_dir_list; for each .md row, appends name
394// (without .md suffix) + newline to blob. Returns final blob_len
395// or -1 on dir-list failure.
396
397func nx_doc_xref_build_names_blob(
398 dir_path: *u8, n_dir_path: i64,
399 blob: *u8, blob_cap: i64
400) -> i64 {
401 let rows_buf: *u8 = sys_mmap(K_MAGIC_2048 * NX_DIR_ROW_BYTES + 64)
402 let rows: *NxDirRow = rows_buf as *NxDirRow
403 let name_arena: *u8 = sys_mmap(K_MAGIC_262144)
404 let result_buf: *u8 = sys_mmap(NX_DIR_RESULT_BYTES + 16)
405 let result: *NxDirResult = result_buf as *NxDirResult
406
407 nx_dir_list(dir_path, rows, K_MAGIC_2048, name_arena, K_MAGIC_262144, 0, result)
408 if result.verdict != NX_DIR_OK {
409 if result.verdict != NX_DIR_TRUNCATED { return -1 }
410 }
411
412 let md_suffix: *u8 = ".md"
413 var blob_off: i64 = 0
414 var i: i64 = 0
415 while i < result.n_filled {
416 let row: *NxDirRow = nx_dir_row_at(rows, i)
417 if row.is_dotlike == 1 { i = i + 1; continue }
418 if row.dtype != NX_DT_REG { i = i + 1; continue }
419 if nx_str_ends_with(row.name_ptr, row.name_len, md_suffix, 3) != 1 {
420 i = i + 1; continue
421 }
422 let new_off: i64 = _docxref_append_name(
423 row.name_ptr, row.name_len,
424 blob, blob_off, blob_cap
425 )
426 if new_off < 0 {
427 return blob_off // blob full; substrate-honest partial result
428 }
429 blob_off = new_off
430 i = i + 1
431 }
432 return blob_off
433}
434
435// Two-blob target existence: returns 1 if target is in EITHER
436// names_blob. Substrate-aware: lets refs cross namespaces
437// (docs/ -> memory/ or vice versa) without being flagged broken.
438
439func nx_doc_link_target_exists_dual(
440 target: *u8, n_target: i64,
441 blob_a: *u8, n_blob_a: i64,
442 blob_b: *u8, n_blob_b: i64
443) -> i64 {
444 if nx_doc_link_target_exists(target, n_target, blob_a, n_blob_a) == 1 { return 1 }
445 if nx_doc_link_target_exists(target, n_target, blob_b, n_blob_b) == 1 { return 1 }
446 return 0
447}
448
449// Multi-namespace walker. Same shape as nx_doc_xref_walk_dir but
450// validates refs against BOTH the dir's own names_blob AND a
451// caller-provided extra namespace blob.
452//
453// Caller pre-builds extra_blob via nx_doc_xref_build_names_blob
454// against the OTHER namespace (e.g., memory dir).
455
456func nx_doc_xref_walk_dir_dual(
457 dir_path: *u8, n_dir_path: i64,
458 extra_blob: *u8, n_extra_blob: i64,
459 counts: *NxDocXrefCounts
460) -> i64 {
461 let rows_buf: *u8 = sys_mmap(K_MAGIC_2048 * NX_DIR_ROW_BYTES + 64)
462 let rows: *NxDirRow = rows_buf as *NxDirRow
463 let name_arena: *u8 = sys_mmap(K_MAGIC_262144)
464 let result_buf: *u8 = sys_mmap(NX_DIR_RESULT_BYTES + 16)
465 let result: *NxDirResult = result_buf as *NxDirResult
466
467 nx_dir_list(dir_path, rows, K_MAGIC_2048, name_arena, K_MAGIC_262144, 0, result)
468 if result.verdict != NX_DIR_OK {
469 if result.verdict != NX_DIR_TRUNCATED { return -10 }
470 }
471
472 let md_suffix: *u8 = ".md"
473 let names_blob: *u8 = sys_mmap(K_MAGIC_262144)
474 var blob_off: i64 = 0
475
476 // Pass 1: collect dir's own .md names.
477 var i: i64 = 0
478 while i < result.n_filled {
479 let row: *NxDirRow = nx_dir_row_at(rows, i)
480 if row.is_dotlike == 1 { i = i + 1; continue }
481 if row.dtype != NX_DT_REG { i = i + 1; continue }
482 if nx_str_ends_with(row.name_ptr, row.name_len, md_suffix, 3) != 1 {
483 i = i + 1; continue
484 }
485 let new_off: i64 = _docxref_append_name(
486 row.name_ptr, row.name_len,
487 names_blob, blob_off, K_MAGIC_262144
488 )
489 if new_off < 0 { i = result.n_filled; continue }
490 blob_off = new_off
491 counts.n_names_indexed = counts.n_names_indexed + 1
492 i = i + 1
493 }
494
495 // Pass 2: scan + validate against BOTH blobs.
496 let hdr_buf: *u8 = sys_mmap(K_MAGIC_65536)
497 let path_buf: *u8 = sys_mmap(K_MAGIC_1024)
498 let target_buf: *u8 = sys_mmap(256)
499 let res_buf: *u8 = sys_mmap(32)
500 let res: *NxLinkScanResult = res_buf as *NxLinkScanResult
501
502 var j: i64 = 0
503 while j < result.n_filled {
504 let row: *NxDirRow = nx_dir_row_at(rows, j)
505 if row.is_dotlike == 1 { j = j + 1; continue }
506 if row.dtype != NX_DT_REG { j = j + 1; continue }
507 if nx_str_ends_with(row.name_ptr, row.name_len, md_suffix, 3) != 1 {
508 j = j + 1; continue
509 }
510 if n_dir_path + 1 + row.name_len + 1 > K_MAGIC_1024 { j = j + 1; continue }
511 var k: i64 = 0
512 while k < n_dir_path { path_buf[k] = dir_path[k]; k = k + 1 }
513 path_buf[n_dir_path] = 47
514 var m: i64 = 0
515 while m < row.name_len {
516 path_buf[n_dir_path + 1 + m] = row.name_ptr[m]
517 m = m + 1
518 }
519 path_buf[n_dir_path + 1 + row.name_len] = 0
520
521 let fd: i64 = sys_openat_rd(path_buf)
522 if fd < 0 { j = j + 1; continue }
523
524 var content_len: i64 = 0
525 var keep_reading: i64 = 1
526 while keep_reading == 1 {
527 let remaining: i64 = K_MAGIC_65536 - content_len
528 if remaining <= 0 { keep_reading = 0; continue }
529 let dst: *u8 = (hdr_buf as *u8) + content_len
530 let n_chunk: i64 = sys_read(fd, dst, remaining)
531 if n_chunk <= 0 { keep_reading = 0; continue }
532 content_len = content_len + n_chunk
533 }
534 sys_close(fd)
535
536 var pos: i64 = 0
537 var scan: i64 = 1
538 while scan == 1 {
539 nx_doc_extract_link_target(hdr_buf, content_len, pos, target_buf, 256, res)
540 if res.next_offset < 0 { scan = 0; continue }
541 pos = res.next_offset
542 if res.n_target <= 0 { continue }
543 counts.n_refs_total = counts.n_refs_total + 1
544 if nx_doc_link_target_exists_dual(target_buf, res.n_target,
545 names_blob, blob_off,
546 extra_blob, n_extra_blob) != 1 {
547 counts.n_refs_broken = counts.n_refs_broken + 1
548 _docxref_write_broken(target_buf, res.n_target, row.name_ptr, row.name_len)
549 }
550 }
551 counts.n_docs_scanned = counts.n_docs_scanned + 1
552 j = j + 1
553 }
554 return 0
555}