code wiki / (root) / nx_genealogist_gapscan.nx

nx_genealogist_gapscan.nx source

↩ module page · 188 lines · 8335 B

1// nx_genealogist_gapscan.nx -- the Nishi GENEALOGIST's gap-scanner (FamilySearch-for-code: 2// "find the missing gaps"), sovereign NishiLang (raw syscalls, no shell/sed/awk). 3// (renamed from nx_genealogist to un-shadow the DISTINCT _hdl_build/nx_genealogist anti-DUPLICATION library 4// -- that one prevents rebuilding what exists; this one finds what is MISSING. Two facets, one old name.) 5// 6// Mechanizes the Genealogist leg of the "what comes next" plan: it scans a 7// registry of modules for GAP MARKERS (TODO_SILICON = an unbuilt/stub silicon 8// path; SEED = a not-yet-matured module) and reports which modules carry which. 9// Each gap it finds is fused (in NISHI_WHAT_COMES_NEXT.md) with the RESEARCHER's 10// target for that capability (NISHI_RESEARCH_BASIS.md) to produce a ranked 11// next-step -- so the roadmap is DERIVED from code-state x research, not 12// hand-curated, and re-derives itself as we build (gaps shrink, plan updates). 13// 14// DATA-DRIVEN (Cardinal 11, fixed 2026-06-16): the scan targets and the DRY 15// threshold are NO LONGER hardcoded. The old organ baked `let N=4` / `let M=6` 16// + two arrays of absolute paths straight into code -- the exact magic-number / 17// data-in-code smell. Now the target list lives in DATA 18// (knowledge/registry/genealogist_targets.tsv: kind<TAB>name<TAB>path), the 19// counts are DERIVED from the row count, and the DRY threshold is one named 20// const. Add/remove a module = edit the data file, no code change (Cardinal 25); 21// the scan re-derives from current data every run (fresh, not a baked snapshot). 22// 23// Bounded length-safe substring scan over the raw file bytes (sys_read_file), 24// so it never over-reads. license_tier: ORIGINAL 25 26import "nx_syscalls.nx" 27 28const G_TGT_FILE: *u8 = "knowledge/registry/genealogist_targets.tsv" 29// DRY policy threshold: a snippet copy-pasted across >= this many modules is a 30// DRY violation worth a next-step. Named + documented (Cardinal 11) -- the value 31// lives here, not buried in a comparison; override by editing this one line. 32const G_DUP_DRY_THRESHOLD: i64 = 3 33 34func g_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != 0 as u8 { n = n + 1 } sys_write(1, s, n); return 0 } 35func g_putn(n: i64) -> i64 { 36 if n == 0 { sys_write(1, "0" as *u8, 1); return 0 } 37 var m: i64 = n 38 let d: *u8 = sys_mmap(24); var k: i64 = 0 39 while m > 0 { d[k] = (0x30 + (m % 10)) as u8; m = m / 10; k = k + 1 } 40 var j: i64 = k - 1 41 while j >= 0 { sys_write(1, ((d as i64)+j) as *u8, 1); j = j - 1 } 42 return 0 43} 44 45func g_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != 0 as u8 { n = n + 1 } return n } 46 47func g_streq(a: *u8, b: *u8) -> i64 { 48 var i: i64 = 0 49 while a[i] != 0 as u8 { if a[i] != b[i] { return 0 } i = i + 1 } 50 if b[i] != 0 as u8 { return 0 } 51 return 1 52} 53 54// length-bounded substring search over raw bytes (no null-terminator assumption). 55func g_find(hay: *u8, hlen: i64, needle: *u8) -> i64 { 56 let nn: i64 = g_strlen(needle) 57 if nn == 0 { return 1 } 58 if nn > hlen { return 0 } 59 var i: i64 = 0 60 while i <= hlen - nn { 61 var j: i64 = 0 62 var ok: i64 = 1 63 while j < nn { 64 if hay[i + j] != needle[j] { ok = 0; j = nn } else { j = j + 1 } 65 } 66 if ok == 1 { return 1 } 67 i = i + 1 68 } 69 return 0 70} 71 72// does `path` contain `marker`? 1 yes, 0 no, -1 unreadable. 73func g_file_has(path: *u8, marker: *u8) -> i64 { 74 let lenbox: *i64 = sys_mmap(16) as *i64 75 lenbox[0] = 0 76 let data: *u8 = sys_read_file(path, lenbox) 77 if (data as i64) == 0 { return 0 - 1 } 78 let n: i64 = lenbox[0] 79 if n <= 0 { return 0 } 80 return g_find(data, n, marker) 81} 82 83// extract TAB field f (0-based) of line[ls,le) into out (NUL-terminated); returns length. 84func g_field(b: *u8, ls: i64, le: i64, f: i64, out: *u8) -> i64 { 85 var cur: i64 = 0 86 var i: i64 = ls 87 while cur < f { 88 if i >= le { out[0] = 0 as u8; return 0 } 89 if b[i] == 9 as u8 { cur = cur + 1 } 90 i = i + 1 91 } 92 var o: i64 = 0 93 while i < le { 94 if b[i] == 9 as u8 { i = le } else { out[o] = b[i]; o = o + 1; i = i + 1 } 95 } 96 out[o] = 0 as u8 97 return o 98} 99 100// THE SCAN: read the DATA target list, dispatch each row by kind, tally. 101// outs[0]=total_gaps outs[1]=n_gap_targets outs[2]=n_dup_targets outs[3]=dup_count 102// Returns 0 ok, -1 if the target file is unreadable (fail LOUD, no silent empty scan). 103func g_scan(tgtfile: *u8, outs: *i64) -> i64 { 104 let lenbox: *i64 = sys_mmap(16) as *i64 105 lenbox[0] = 0 106 let data: *u8 = sys_read_file(tgtfile, lenbox) 107 if (data as i64) == 0 { return 0 - 1 } 108 let n: i64 = lenbox[0] 109 let kindbuf: *u8 = sys_mmap(32) 110 let namebuf: *u8 = sys_mmap(160) 111 let pathbuf: *u8 = sys_mmap(512) 112 var total_gaps: i64 = 0 113 var n_gap: i64 = 0 114 var n_dup: i64 = 0 115 var dup: i64 = 0 116 var ls: i64 = 0 117 var i: i64 = 0 118 while i <= n { 119 var atend: i64 = 0 120 if i == n { atend = 1 } 121 if i < n { if data[i] == 10 as u8 { atend = 1 } } 122 if atend == 1 { 123 let le: i64 = i 124 if le > ls { 125 if data[ls] != 35 as u8 { // skip '#' comment lines 126 g_field(data, ls, le, 0, kindbuf) 127 g_field(data, ls, le, 1, namebuf) 128 g_field(data, ls, le, 2, pathbuf) 129 if pathbuf[0] != 0 as u8 { 130 if g_streq(kindbuf, "gap" as *u8) == 1 { 131 n_gap = n_gap + 1 132 let todo: i64 = g_file_has(pathbuf, "TODO_SILICON" as *u8) 133 let seed: i64 = g_file_has(pathbuf, "Status: SEED" as *u8) 134 g_puts(" "); g_puts(namebuf); g_puts(" TODO_SILICON=") 135 if todo == 1 { g_puts("YES"); total_gaps = total_gaps + 1 } else { g_puts("no ") } 136 g_puts(" SEED=") 137 if seed == 1 { g_puts("YES"); total_gaps = total_gaps + 1 } else { g_puts("no ") } 138 g_puts("\n") 139 } 140 if g_streq(kindbuf, "dup" as *u8) == 1 { 141 n_dup = n_dup + 1 142 if g_file_has(pathbuf, "% 10" as *u8) == 1 { dup = dup + 1 } 143 } 144 } 145 } 146 } 147 ls = i + 1 148 } 149 i = i + 1 150 } 151 outs[0] = total_gaps 152 outs[1] = n_gap 153 outs[2] = n_dup 154 outs[3] = dup 155 return 0 156} 157 158func main() -> i64 { 159 g_puts("NISHI GENEALOGIST -- gap scan (find the missing gaps; feeds the what-comes-next plan)\n") 160 g_puts("===================================================================================\n") 161 162 let outs: *i64 = sys_mmap(64) as *i64 163 if g_scan(G_TGT_FILE, outs) != 0 { 164 g_puts(" FATAL: "); g_puts(G_TGT_FILE); g_puts(" unreadable -- no data-driven targets, refusing a silent empty scan\n") 165 sys_exit(1); return 1 166 } 167 var total_gaps: i64 = outs[0] 168 let dup: i64 = outs[3] 169 170 g_puts(" scanned gap-targets="); g_putn(outs[1]) 171 g_puts(" dup-targets="); g_putn(outs[2]); g_puts(" (DATA-DRIVEN from genealogist_targets.tsv)\n") 172 g_puts(" DUPLICATION: decimal int-printer (% 10 loop) copy-pasted across "); g_putn(dup); g_puts(" modules -> DRY violation\n") 173 g_puts(" Researcher: a superior approach exists -- ONE shared fmt lib (Cardinal #15 DRY; cf. C printf / Rust + Go std fmt). Next-step: extract nx_fmt, all import it.\n") 174 if dup >= G_DUP_DRY_THRESHOLD { total_gaps = total_gaps + 1 } 175 176 g_puts("total gaps = "); g_putn(total_gaps) 177 g_puts(" -> each gap x its Researcher target (NISHI_RESEARCH_BASIS.md) = a ranked next-step\n") 178 179 // NEG-CONTROL (the detector must SEE absence, not just presence): an absent marker 180 // returns 0, and an unreadable path returns -1 -- proves "no" is real, not a default. 181 var negok: i64 = 1 182 if g_file_has(G_TGT_FILE, "ZZ_MARKER_THAT_DOES_NOT_EXIST_ZZ" as *u8) != 0 { negok = 0 } 183 if g_file_has("/nonexistent/path/zzz.nx" as *u8, "x" as *u8) != 0 - 1 { negok = 0 } 184 if negok == 0 { g_puts(" NEG-CONTROL FAILED: scanner reports phantom markers\n"); sys_exit(2); return 2 } 185 186 if total_gaps < 1 { sys_exit(1); return 1 } // the scan must actually find the known gaps 187 sys_exit(0); return 0 188}