code wiki / (root) / nx_confdup.nx

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}