code wiki / (root) / nx_doc_xref.nx

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}