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}