nx_confdup.nx source
↩ module page · 329 lines · 12503 B
1// nx_confdup.nx -- find CONFIG FILES THAT DECLARE THE SAME CONCERN TWICE.
2//
3// WHY THIS EXISTS (measured 2026-07-30, media-vault lane): /api/promote reads
4// `knowledge/organ_kinds.conf` (PLURAL, values hyphenated `one-shot`) while
5// nx_organkind reads `knowledge/status/organ_kind.conf` (SINGULAR, values
6// unhyphenated `oneshot`). I added a row to the singular file, promote kept
7// working, and I concluded the row was the fix -- it was INERT. Undeclared
8// names fall through to a name-substring heuristic, so the wrong file "worked"
9// by luck until a name that failed the heuristic (nx_mvault_walk) became
10// unshippable by any sanctioned route.
11//
12// ★★★LAW: A SILENTLY-INERT CONFIG ROW IS WORSE THAN A MISSING ONE -- it
13// manufactures false confidence. The failure is invisible precisely because
14// the other source of truth keeps answering.
15//
16// This is a RECURRING class in this ecosystem, not a one-off:
17// * organ_kind.conf vs organ_kinds.conf (this session)
18// * nx_fs root=nishihost vs /api/build buildroot/runtime -- TWO ROOTS ONE NAME
19// * the ruler reading the wrong evidence root (rollup saw 181 of 1164 logs)
20// So it gets a DETECTOR rather than another banked warning.
21//
22// WHAT IT FLAGS -- two independent signals, reported separately because they
23// have different remedies:
24// COLLIDE-STEM two config files whose names normalize to the SAME stem
25// (plural/singular, hyphen/underscore, trailing digits).
26// Remedy: one file, or make one provably derived.
27// COLLIDE-BASE the SAME basename living under two different directories.
28// Remedy: name the roots apart, or collapse them.
29//
30// Normalization is deliberately AGGRESSIVE (drop ext, lowercase, '-'->'_',
31// strip trailing digits/underscores, strip ONE trailing plural 's') because a
32// false positive costs a glance while a false negative costs a session.
33//
34// nx_confdup scan [root] (default root: knowledge)
35//
36// Read-only. license_tier: ORIGINAL No hw writes (Rule 26).
37
38import "nx_syscalls.nx"
39
40const CD_QCAP: i64 = 1048576 // dir-queue arena
41const CD_DIRBUF: i64 = 262144 // getdents buffer
42const CD_ARENA: i64 = 1048576 // path + stem text arena
43const CD_MAXFILES: i64 = 8192 // bounded, and the cap is REPORTED not silent
44const CD_PATH: i64 = 4096
45const CD_STEM: i64 = 256
46const CD_OUT: i64 = 262144
47
48func cd_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
49
50func cd_cat(dst: *u8, off: i64, s: *u8) -> i64 {
51 var o: i64 = off
52 var i: i64 = 0
53 while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 }
54 return o
55}
56
57func cd_ch(dst: *u8, off: i64, c: i64) -> i64 { dst[off] = c as u8; return off + 1 }
58
59func cd_udec(dst: *u8, off: i64, v: i64) -> i64 {
60 if v == 0 { dst[off] = 48 as u8; return off + 1 }
61 let tmp: *u8 = sys_mmap(32)
62 var m: i64 = v
63 var k: i64 = 0
64 while m > 0 { tmp[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
65 var o: i64 = off
66 var i: i64 = k - 1
67 while i >= 0 { dst[o] = tmp[i]; o = o + 1; i = i - 1 }
68 sys_munmap(tmp, 32)
69 return o
70}
71
72// "<dir>/<name>" into dst; returns length
73func cd_join(dst: *u8, dir: *u8, name: *u8) -> i64 {
74 var o: i64 = cd_cat(dst, 0, dir)
75 o = cd_ch(dst, o, 47)
76 o = cd_cat(dst, o, name)
77 dst[o] = 0 as u8
78 return o
79}
80
81// last path component of a NUL-terminated path
82func cd_basename(p: *u8) -> *u8 {
83 let n: i64 = cd_len(p)
84 var last: i64 = 0 - 1
85 var i: i64 = 0
86 while i < n {
87 if p[i] == (47 as u8) { last = i }
88 i = i + 1
89 }
90 return ((p as i64) + last + 1) as *u8
91}
92
93// does the name end in ".conf"?
94func cd_is_conf(name: *u8) -> i64 {
95 let n: i64 = cd_len(name)
96 if n < 6 { return 0 }
97 let s: *u8 = ".conf" as *u8
98 var i: i64 = 0
99 while i < 5 {
100 if name[n - 5 + i] != s[i] { return 0 }
101 i = i + 1
102 }
103 return 1
104}
105
106// Normalize a basename to a concern STEM. See the header for why this is
107// aggressive. Returns the stem length.
108func cd_norm(dst: *u8, name: *u8) -> i64 {
109 let n: i64 = cd_len(name)
110 // drop the extension (last '.')
111 var end: i64 = n
112 var i: i64 = n - 1
113 var scanning: i64 = 1
114 while scanning == 1 {
115 if i <= 0 { scanning = 0 } else {
116 if name[i] == (46 as u8) { end = i; scanning = 0 } else { i = i - 1 }
117 }
118 }
119 var o: i64 = 0
120 i = 0
121 while i < end {
122 var c: i64 = name[i]
123 if c >= 65 { if c <= 90 { c = c + 32 } } // lowercase
124 if c == 45 { c = 95 } // '-' -> '_'
125 if o < CD_STEM - 2 { dst[o] = c as u8; o = o + 1 }
126 i = i + 1
127 }
128 // strip trailing digits (foo2 == foo)
129 var go: i64 = 1
130 while go == 1 {
131 go = 0
132 if o > 1 {
133 let c2: i64 = dst[o - 1]
134 if c2 >= 48 { if c2 <= 57 { o = o - 1; go = 1 } }
135 }
136 }
137 // strip trailing underscores
138 go = 1
139 while go == 1 {
140 go = 0
141 if o > 1 { if dst[o - 1] == (95 as u8) { o = o - 1; go = 1 } }
142 }
143 // strip ONE trailing plural 's' (kind/kinds)
144 if o > 2 { if dst[o - 1] == (115 as u8) { o = o - 1 } }
145 dst[o] = 0 as u8
146 return o
147}
148
149func cd_streq(a: *u8, b: *u8) -> i64 {
150 var i: i64 = 0
151 while 1 == 1 {
152 if a[i] != b[i] { return 0 }
153 if a[i] == (0 as u8) { return 1 }
154 i = i + 1
155 }
156 return 0
157}
158
159func main(argc: i64, argv: *i64) -> i64 {
160 var root: *u8 = "knowledge" as *u8
161 if argc > 2 { root = (argv[2]) as *u8 }
162
163 let q: *u8 = sys_mmap(CD_QCAP)
164 let dbuf: *u8 = sys_mmap(CD_DIRBUF)
165 let arena: *u8 = sys_mmap(CD_ARENA)
166 let pathoff: *i64 = sys_mmap(8 * CD_MAXFILES) as *i64
167 let stemoff: *i64 = sys_mmap(8 * CD_MAXFILES) as *i64
168 let taken: *i64 = sys_mmap(8 * CD_MAXFILES) as *i64
169 let path: *u8 = sys_mmap(CD_PATH)
170 let stem: *u8 = sys_mmap(CD_STEM)
171 let out: *u8 = sys_mmap(CD_OUT)
172
173 var ao: i64 = 0 // arena offset
174 var nf: i64 = 0 // file count
175 var truncated: i64 = 0
176 var dirs: i64 = 0
177 var qh: i64 = 0
178 var qt: i64 = 0
179
180 var rl: i64 = 0
181 while root[rl] != (0 as u8) { q[qt] = root[rl]; qt = qt + 1; rl = rl + 1 }
182 q[qt] = 0 as u8
183 qt = qt + 1
184
185 while qh < qt {
186 let dir: *u8 = ((q as i64) + qh) as *u8
187 var dl: i64 = 0
188 while dir[dl] != (0 as u8) { dl = dl + 1 }
189 qh = qh + dl + 1
190 dirs = dirs + 1
191 let dfd: i64 = sys_openat_rd(dir)
192 if dfd >= 0 {
193 var done: i64 = 0
194 while done == 0 {
195 let nread: i64 = sys_getdents64(dfd, dbuf, CD_DIRBUF)
196 if nread <= 0 { done = 1 } else {
197 var off: i64 = 0
198 while off < nread {
199 let drec: *u8 = ((dbuf as i64) + off) as *u8
200 let reclen: i64 = dirent_reclen(drec)
201 let dtype: i64 = dirent_type(drec)
202 let name: *u8 = dirent_name(drec)
203 var skip: i64 = 0
204 if name[0] == (46 as u8) { skip = 1 } // . .. dotdirs
205 if skip == 0 {
206 if dtype == DT_DIR {
207 let need: i64 = cd_join(path, dir, name)
208 if qt + need + 2 < CD_QCAP {
209 var c: i64 = 0
210 while c <= need { q[qt + c] = path[c]; c = c + 1 }
211 qt = qt + need + 1
212 }
213 }
214 if dtype == DT_REG {
215 if cd_is_conf(name) == 1 {
216 let pl: i64 = cd_join(path, dir, name)
217 let sl: i64 = cd_norm(stem, name)
218 if nf >= CD_MAXFILES { truncated = 1 } else {
219 if ao + pl + sl + 4 >= CD_ARENA { truncated = 1 } else {
220 pathoff[nf] = ao
221 var c2: i64 = 0
222 while c2 <= pl { arena[ao + c2] = path[c2]; c2 = c2 + 1 }
223 ao = ao + pl + 1
224 stemoff[nf] = ao
225 c2 = 0
226 while c2 <= sl { arena[ao + c2] = stem[c2]; c2 = c2 + 1 }
227 ao = ao + sl + 1
228 taken[nf] = 0
229 nf = nf + 1
230 }
231 }
232 }
233 }
234 }
235 if reclen <= 0 { off = nread } else { off = off + reclen }
236 }
237 }
238 }
239 sys_close(dfd)
240 }
241 }
242
243 var o: i64 = 0
244 o = cd_cat(out, o, "=== nx_confdup -- config files declaring the SAME CONCERN twice ===\n" as *u8)
245 o = cd_cat(out, o, "root=" as *u8)
246 o = cd_cat(out, o, root)
247 o = cd_cat(out, o, " dirs=" as *u8)
248 o = cd_udec(out, o, dirs)
249 o = cd_cat(out, o, " conf_files=" as *u8)
250 o = cd_udec(out, o, nf)
251 if truncated == 1 { o = cd_cat(out, o, " TRUNCATED=1 (raise CD_MAXFILES/CD_ARENA; NOT a silent cap)" as *u8) }
252 o = cd_ch(out, o, 10)
253
254 var groups: i64 = 0
255 var basegroups: i64 = 0
256 var i2: i64 = 0
257 while i2 < nf {
258 if taken[i2] == 0 {
259 let si: *u8 = ((arena as i64) + stemoff[i2]) as *u8
260 var members: i64 = 0
261 var j: i64 = i2 + 1
262 while j < nf {
263 if taken[j] == 0 {
264 let sj: *u8 = ((arena as i64) + stemoff[j]) as *u8
265 if cd_streq(si, sj) == 1 { members = members + 1 }
266 }
267 j = j + 1
268 }
269 if members > 0 {
270 groups = groups + 1
271 // are the BASENAMES identical too? then it is the two-roots-one-name shape
272 let pi: *u8 = ((arena as i64) + pathoff[i2]) as *u8
273 var samebase: i64 = 0
274 j = i2 + 1
275 while j < nf {
276 if taken[j] == 0 {
277 let sj2: *u8 = ((arena as i64) + stemoff[j]) as *u8
278 if cd_streq(si, sj2) == 1 {
279 let pj: *u8 = ((arena as i64) + pathoff[j]) as *u8
280 if cd_streq(cd_basename(pi), cd_basename(pj)) == 1 { samebase = 1 }
281 }
282 }
283 j = j + 1
284 }
285 if samebase == 1 {
286 basegroups = basegroups + 1
287 o = cd_cat(out, o, "COLLIDE-BASE stem=" as *u8)
288 } else {
289 o = cd_cat(out, o, "COLLIDE-STEM stem=" as *u8)
290 }
291 o = cd_cat(out, o, si)
292 o = cd_ch(out, o, 10)
293 o = cd_cat(out, o, " " as *u8)
294 o = cd_cat(out, o, pi)
295 o = cd_ch(out, o, 10)
296 taken[i2] = 1
297 j = i2 + 1
298 while j < nf {
299 if taken[j] == 0 {
300 let sj3: *u8 = ((arena as i64) + stemoff[j]) as *u8
301 if cd_streq(si, sj3) == 1 {
302 o = cd_cat(out, o, " " as *u8)
303 o = cd_cat(out, o, ((arena as i64) + pathoff[j]) as *u8)
304 o = cd_ch(out, o, 10)
305 taken[j] = 1
306 }
307 }
308 j = j + 1
309 }
310 }
311 }
312 i2 = i2 + 1
313 }
314
315 o = cd_cat(out, o, "\nNX-CONFDUP collisions=" as *u8)
316 o = cd_udec(out, o, groups)
317 o = cd_cat(out, o, " same_basename=" as *u8)
318 o = cd_udec(out, o, basegroups)
319 o = cd_cat(out, o, " scanned=" as *u8)
320 o = cd_udec(out, o, nf)
321 if groups == 0 {
322 o = cd_cat(out, o, " verdict=GREEN (no config concern is declared twice)\n" as *u8)
323 } else {
324 o = cd_cat(out, o, " verdict=RED (each group is a place a config row can be SILENTLY INERT)\n" as *u8)
325 }
326 sys_write(1, out, o)
327 if groups == 0 { return 0 }
328 return 1
329}