code wiki / _hdl_build / nx_dup_source_check.nx

nx_dup_source_check.nx source

↩ module page · 1041 lines · 58476 B

1// nx_dup_source_check.nx v3 -- ANTI-CLOBBER cross-tree source-dup detector. 2// 3// 🚨v3 EXISTS BECAUSE v2 WAS AN OOM BOMB THAT TOOK THE NAS DOWN (2026-07-20). Root cause, stated plainly: 4// v2 widened the scan from 2 hardcoded dirs to ALL discovered source trees (78 pairs) -- a good change -- but 5// REUSED v1's `exists()` helper UNCHANGED, and that helper did `sys_mmap(4096)` PER CALL and never unmapped. 6// v1 called it ~15k times (ONE pair) = ~60MB = survivable. v2 called it ~250k times = ~1GB LEAKED PER RUN, on a 7// cron. Result: NAS userspace starved -- kernel still SYN-ACKs but sshd gives no banner, HTTPS returns status=0, 8// even DSM stops answering. 9// 10// ★THE LESSON, so it is never repeated: CHANGING A CALLER'S FAN-OUT RE-COSTS EVERY CALLEE. A 2->78 pair change 11// is a 17x amplifier on an O(files) helper. Reviewing only the code you WROTE is not enough -- you must re-cost 12// the code you INHERITED AND AMPLIFIED. And note what the v2 selftest could NOT do: it was GREEN 4/4 because 13// fixture dirs hold 2-3 files, so it tested the ALGORITHM at scale and never the RESOURCE at scale. A scale-law 14// tooth that asserts only correctness-on-big-input is HALF a tooth; the other half is a RESOURCE bound. 15// 16// v3 FIX: every path buffer is allocated ONCE PER PAIR (or once per walk) and passed down, never per file. 17// Allocation profile is now O(pairs + dirs), not O(files), and it is DECLARED in the output envelope so the 18// property is inspectable rather than assumed. Detection semantics and the emitted " DUP basename: " line are 19// UNCHANGED (rule 19) -- nx_favela_census counts that exact substring out of knowledge/status/dup_source.log. 20// expect_exit:0 when clean. 21import "nx_syscalls.nx" 22import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 23const K_MAGIC_131072: i64 = 131072 24const K_MAGIC_4096: i64 = 4096 25// ★v4 (seq207): 1MiB per side for the CONTENT compare. A source over the cap is reported UNKNOWN, 26// never silently folded into "identical" -- partial coverage presented as complete is the L011 27// self-ceiling defect. Buffers are allocated ONCE PER RUN and passed down (see main). 28const K_CMP_CAP: i64 = 1048576 29// FINGERPRINT. This detector can only see trees it can OPEN, which means NAS-side trees. A working copy 30// on a laptop is a divergent duplicate it is structurally blind to -- and that blind spot bit us: the 31// laptop copy of nx_page_verify.nx sat 6092 bytes BEHIND the buildroot canonical and was missing the 32// entire recovered auto-connect-override, so any rebuild from that tree would have silently reverted a 33// landed fix. Emitting size+sum lets an off-NAS copy detect divergence in ONE call instead of one fetch 34// per organ. 35const FP_MULT: i64 = 131 36const FP_MASK: i64 = 1099511627775 37const FP_FILECAP: i64 = 1048576 38// PAGINATION, added after the very first real run was SILENTLY TRUNCATED by the transport: 1650 rows 39// went out, the tail was cut mid-alphabet, and the summary line -- the one thing that would have 40// revealed the cut -- was itself lost because it printed LAST. The total now prints FIRST, so a clipped 41// response can never look complete. 42const FP_PAGE_DEF: i64 = 300 43const FP_PAGE_MAX: i64 = 900 44// FRESHNESS. Size and sum say two copies DIFFER; only mtime says which way to reconcile, and getting 45// that backwards destroys work. Uses the already-proven stat channel (sys_fstatat/262, see 46// _freshness_gate.nx) rather than a second mechanism. st_mtim.tv_sec is at byte offset 88 = sp[11]. 47const FP_STATBUF: i64 = 144 48const FP_MTIME_SLOT: i64 = 11 49const FP_TEST_MTIME: i64 = 1750000000 50 51func dp_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 52// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 53// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 54// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 55// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 56func dp_wn(v: i64) -> i64 { nxi_out(v); return 0 } 57 58func ends_nx(nm: *u8) -> i64 { 59 var n: i64=0; while nm[n]!=(0 as u8){n=n+1} 60 if n < 3 { return 0 } 61 if nm[n-3]==(46 as u8) { if nm[n-2]==(110 as u8) { if nm[n-1]==(120 as u8) { return 1 } } } 62 return 0 63} 64 65func join_path(buf: *u8, dir: *u8, name: *u8) -> i64 { 66 var o: i64=0; var i: i64=0 67 while dir[i]!=(0 as u8) { buf[o]=dir[i]; o=o+1; i=i+1 } 68 buf[o]=47 as u8; o=o+1 69 i=0; while name[i]!=(0 as u8) { buf[o]=name[i]; o=o+1; i=i+1 } 70 buf[o]=0 as u8 71 return 0 72} 73 74// ★v3: takes a CALLER-OWNED scratch buffer. No allocation here -- this is the hot path (once per .nx file). 75func exists(dir: *u8, name: *u8, scratch: *u8) -> i64 { 76 join_path(scratch, dir, name) 77 let fd: i64=sys_openat_rd(scratch) 78 if fd >= 0 { sys_close(fd); return 1 } 79 return 0 80} 81 82func is_dot(nm: *u8) -> i64 { 83 if nm[0]==(46 as u8) { 84 if nm[1]==(0 as u8) { return 1 } 85 if nm[1]==(46 as u8) { if nm[2]==(0 as u8) { return 1 } } 86 } 87 return 0 88} 89 90func str_copy(dst: *u8, src: *u8) -> i64 { var i: i64=0; while src[i]!=(0 as u8) { dst[i]=src[i]; i=i+1 } dst[i]=0 as u8; return i } 91 92func dp_streq(a: *u8, b: *u8) -> i64 { 93 var i: i64=0 94 while a[i]!=(0 as u8) { if a[i]!=b[i] { return 0 } i=i+1 } 95 if b[i]!=(0 as u8) { return 0 } 96 return 1 97} 98 99// ★F1127 -- DEFINE THE HAZARD BY THE MECHANISM, NOT THE RESEMBLANCE. 100// A duplicate basename can only cause the harm this organ warns about ("a rebuild resolves the stale 101// copy and silently regresses the service") if a BUILD CAN ACTUALLY RESOLVE IT. The build resolver 102// states its own search path when it fails to find a source: 103// "SOURCE-NOT-FOUND (probed runtime/_hdl_build/, runtime/, nxasm/, runtime/wiki/)" 104// So a twin living in bin/, _retired/ or a nested runtime/runtime/ is NEVER read by a build and cannot 105// clobber anything -- it is litter, not a hazard. Counting it as a hazard leaves the board permanently 106// RED with no action that could ever clear it, which trains readers to ignore the verdict. 107// Nothing is hidden: out-of-scope divergence is still PRINTED, just counted in its own class. 108// ⚠The probe-path concept only has meaning INSIDE the buildroot -- that is the only tree a build 109// resolves from. A scan of any other directory (notably the hermetic self-test fixtures) has no build 110// semantics at all, so everything there is in scope. Without this the detector's OWN self-test breaks, 111// which is precisely how this was caught: the gate refused the change before it could ship. 112func dp_in_buildroot(dir: *u8) -> i64 { 113 let p: *u8 = "buildroot/" as *u8 114 var i: i64 = 0 115 while p[i]!=(0 as u8) { if dir[i]!=p[i] { return 0 } i=i+1 } 116 return 1 117} 118 119// ★★★THE COMPILER'S PROBE ORDER -- what turns "divergent + newer" into a VERDICT instead of a riddle. 120// /api/build states it outright: "exists in BOTH buildroot/runtime/_hdl_build (RESOLVED FIRST) AND 121// buildroot/runtime". Lower rank wins. Without it this organ could only print "if the BUILD resolves the 122// OTHER directory, later work is being silently discarded" and leave the reader to finish the sentence -- 123// and on 2026-07-30 finishing it revealed FOUR organs compiling source up to 17.7 DAYS stale (nx_hls_parse, 124// nx_clock, nx_input_abstract, nx_site_lock_lib), buried among six pairs where the newer copy already wins 125// and nothing is wrong. Same two facts, opposite conclusions; only the probe order separates them. 126// ⚠keep this in step with dp_is_probed: a dir that is probed but unranked falls to 9 and reads as LAST. 127func dp_probe_rank(dir: *u8) -> i64 { 128 if dp_streq(dir, "buildroot/runtime/_hdl_build" as *u8)==1 { return 0 } 129 if dp_streq(dir, "buildroot/runtime" as *u8)==1 { return 1 } 130 if dp_streq(dir, "buildroot/runtime/wiki" as *u8)==1 { return 2 } 131 if dp_streq(dir, "buildroot/nxasm" as *u8)==1 { return 3 } 132 return 9 133} 134 135// ★1 = the copy that WINS the probe rank is the MAIN-LESS one, so the build is DEAD (nxasm rc=102). 136// 0 = either the rank-winner has main (the target builds; the OTHER copy is the unreachable one), or the 137// two dirs tie and no claim can be made. PURE ON PURPOSE: dp_probe_rank returns 9 for anything outside the 138// four ranked build dirs, so a /tmp fixture ALWAYS ties and can never reach the dead branch. Extracting the 139// decision lets the selftest bite it with synthetic ranks instead of leaving it dark -- a branch no fixture 140// can reach is a branch no gate is testing. 141func dp_deadwin(ra: i64, ha: i64, rb: i64, hb: i64) -> i64 { 142 if ra < rb { if ha == 0 { return 1 } return 0 } 143 if rb < ra { if hb == 0 { return 1 } return 0 } 144 return 0 145} 146 147func dp_is_probed(dir: *u8) -> i64 { 148 if dp_in_buildroot(dir)==0 { return 1 } 149 if dp_streq(dir, "buildroot/runtime" as *u8)==1 { return 1 } 150 if dp_streq(dir, "buildroot/runtime/_hdl_build" as *u8)==1 { return 1 } 151 if dp_streq(dir, "buildroot/runtime/wiki" as *u8)==1 { return 1 } 152 if dp_streq(dir, "buildroot/nxasm" as *u8)==1 { return 1 } 153 return 0 154} 155 156// read up to cap bytes. Returns byte count, or -1 unreadable, or -2 if the file EXCEEDS cap. 157// An over-cap file is NEVER folded into "identical" -- it is reported UNKNOWN (L011). 158func dp_atoi(s: *u8) -> i64 { 159 var v: i64 = 0 160 var i: i64 = 0 161 while s[i] != (0 as u8) { 162 let c: i64 = (s[i] as i64) & 255 163 if c >= 48 { if c <= 57 { v = v*10 + (c-48) } } 164 i = i + 1 165 } 166 return v 167} 168 169func dp_sum(buf: *u8, n: i64) -> i64 { 170 var h: i64 = 0 171 var i: i64 = 0 172 while i < n { 173 h = ((h * FP_MULT) + ((buf[i] as i64) & 255)) & FP_MASK 174 i = i + 1 175 } 176 return h 177} 178 179func dp_slurp(path: *u8, buf: *u8, cap: i64) -> i64 { 180 let fd: i64 = sys_openat_rd(path) 181 if fd < 0 { return 0 - 1 } 182 var total: i64 = 0 183 var go: i64 = 1 184 while go == 1 { 185 let n: i64 = sys_read(fd, ((buf as i64) + total) as *u8, cap - total) 186 if n <= 0 { go = 0 } else { 187 total = total + n 188 if total >= cap { sys_close(fd); return 0 - 2 } 189 } 190 } 191 sys_close(fd) 192 return total 193} 194 195// 1 identical / 0 divergent / -1 unknown. Caller owns both compare buffers. 196func dp_same(pa: *u8, pb: *u8, ba: *u8, bb: *u8, cap: i64) -> i64 { 197 let na: i64 = dp_slurp(pa, ba, cap) 198 if na < 0 { return 0 - 1 } 199 let nb: i64 = dp_slurp(pb, bb, cap) 200 if nb < 0 { return 0 - 1 } 201 if na != nb { return 0 } 202 var i: i64 = 0 203 while i < na { if ba[i] != bb[i] { return 0 } i = i + 1 } 204 return 1 205} 206 207// ★★★v7 (2026-07-31, lib-reconcile): TARGET RESOLUTION AND IMPORT RESOLUTION ARE DIFFERENT RULES, AND 208// APPLYING THE FIRST TO A LIBRARY MANUFACTURES A FALSE WORK-LOSS ALARM WHOSE REMEDY CAUSES AN OUTAGE. 209// /api/build resolves a BUILD TARGET by dp_probe_rank (_hdl_build first), so for a file that IS a target, 210// "newer copy sits in the later dir" really does mean every build compiles stale source -- seq1343 211// nx_skullgen is exactly that and stays a hazard. But a LIBRARY is never a target: it reaches the compiler 212// only through another file's import, and an import resolves to the IMPORTER'S OWN DIRECTORY FIRST. 213// PROOF (measured 2026-07-31, artifact not claim): nx_clock.nx exists in BOTH dirs exporting DISJOINT 214// symbols -- runtime/ defines nx_clock_monotonic_ns, _hdl_build/ is the clk_* tickless scheduler -- and 215// nx_f32_llm_serve.nx, which exists ONLY in runtime/, compiles with nx_clock_monotonic_ns .globl-defined 216// in buildroot/_build/nx_f32_llm_serve.s:2511. A runtime importer therefore resolved the runtime copy 217// while a rank-0 _hdl_build copy of that basename existed. Reporting that pair as WORK-LOSS told the 218// operator to reconcile to ONE dir; doing so deletes runtime/nx_clock.nx and re-darkens the 174 organs 219// the 2026-07-30 fix recovered. An alarm whose remedy is the outage is worse than no alarm. 220// DISCRIMINATOR: a build target defines main(); a library omits it ON PURPOSE so it can be imported -- 221// nx_gate_verdict_lib.nx's header states exactly that reason. This reads the ARTIFACT, not the filename, 222// because name-based rules rot. 223// FAIL-CLOSED: unreadable or over-cap returns -1 and the caller keeps the LOUD hazard classification. 224// "I could not tell" must never downgrade an alarm -- that is how a real clobber gets filed as litter. 225// COST: one extra slurp per DIVERGENT pair (11 today) into the caller's existing buffer, never per file. 226// v2's OOM lesson is that changing a caller's fan-out re-costs every callee; this adds none. 227const DP_NDLCAP: i64 = 512 228static dp_ndl: *u8 229 230func dp_find_in(hay: *u8, hn: i64, ndl: *u8, nn: i64) -> i64 { 231 if nn == 0 { return 0 } 232 if nn > hn { return 0 } 233 var i: i64 = 0 234 while i + nn <= hn { 235 // column 0 on THIS side too, or a comment in the OTHER file fakes the overlap and the caller 236 // downgrades a real name collision to a merge -- see the note in dp_exports_overlap. 237 var atcol0: i64 = 1 238 if i > 0 { if hay[i-1] != (10 as u8) { atcol0 = 0 } } 239 if atcol0 == 1 { 240 var k: i64 = 0 241 var hit: i64 = 1 242 while k < nn { if hay[i+k] != ndl[k] { hit = 0; k = nn } else { k = k + 1 } } 243 if hit == 1 { return 1 } 244 } 245 i = i + 1 246 } 247 return 0 248} 249 250// index of a column-0 match, or -1. Same anchoring rule as dp_find_in; returns WHERE so the caller can 251// read the signature that follows. 252func dp_find_at(hay: *u8, hn: i64, ndl: *u8, nn: i64) -> i64 { 253 if nn == 0 { return 0 - 1 } 254 if nn > hn { return 0 - 1 } 255 var i: i64 = 0 256 while i + nn <= hn { 257 var atcol0: i64 = 1 258 if i > 0 { if hay[i-1] != (10 as u8) { atcol0 = 0 } } 259 if atcol0 == 1 { 260 var k: i64 = 0 261 var hit: i64 = 1 262 while k < nn { if hay[i+k] != ndl[k] { hit = 0; k = nn } else { k = k + 1 } } 263 if hit == 1 { return i } 264 } 265 i = i + 1 266 } 267 return 0 - 1 268} 269 270// parameter count of the definition starting at `start`, or -1 if unparseable. Signatures in this corpus 271// carry no nested parens, so commas at one level are the whole story: 0 params = empty parens. 272func dp_arity(buf: *u8, n: i64, start: i64) -> i64 { 273 var i: i64 = start 274 var go: i64 = 1 275 while go == 1 { 276 if i >= n { return 0 - 1 } 277 if buf[i] == (40 as u8) { go = 0 } else { i = i + 1 } 278 } 279 i = i + 1 280 var commas: i64 = 0 281 var any: i64 = 0 282 var go2: i64 = 1 283 while go2 == 1 { 284 if i >= n { return 0 - 1 } 285 let c: i64 = buf[i] as i64 286 if c == 41 { go2 = 0 } else { 287 if c == 44 { commas = commas + 1 } 288 if c != 32 { any = 1 } 289 i = i + 1 290 } 291 } 292 if any == 0 { return 0 } 293 return commas + 1 294} 295 296// 1 = the two copies share >=1 exported func name | 0 = DISJOINT (name collision) | -1 = unreadable 297func dp_exports_overlap(pa: *u8, pb: *u8, ba: *u8, bb: *u8, cap: i64) -> i64 { 298 let na: i64 = dp_slurp(pa, ba, cap) 299 if na <= 0 { return 0 - 1 } 300 let nb: i64 = dp_slurp(pb, bb, cap) 301 if nb <= 0 { return 0 - 1 } 302 if (dp_ndl as i64) == 0 { dp_ndl = sys_mmap(DP_NDLCAP) } 303 let ndl: *u8 = dp_ndl 304 var i: i64 = 0 305 while i + 5 <= na { 306 var isf: i64 = 0 307 if ba[i] == (102 as u8) { if ba[i+1] == (117 as u8) { if ba[i+2] == (110 as u8) { if ba[i+3] == (99 as u8) { if ba[i+4] == (32 as u8) { isf = 1 } } } } } 308 // COLUMN 0 ONLY. `func ` also occurs inside COMMENTS, and a shared comment mention would fake an 309 // OVERLAP -- the UNSAFE direction: a false collision costs one renamed file, a false overlap tells 310 // the operator to MERGE TWO PROGRAMS. Top-level definitions in this corpus are unindented, comments 311 // are not, so requiring column 0 removes the fake without needing a tokenizer. 312 if isf == 1 { if i > 0 { if ba[i-1] != (10 as u8) { isf = 0 } } } 313 if isf == 1 { 314 var k: i64 = 0 315 while k < 5 { ndl[k] = ba[i+k]; k = k + 1 } 316 var j: i64 = i + 5 317 var go: i64 = 1 318 while go == 1 { 319 if j >= na { go = 0 } else { 320 let c: i64 = ba[j] as i64 321 if c == 40 { go = 0 } else { if c == 32 { go = 0 } else { if c == 10 { go = 0 } else { 322 if k < DP_NDLCAP - 2 { ndl[k] = ba[j]; k = k + 1 } 323 j = j + 1 324 } } } 325 } 326 } 327 if k > 5 { 328 ndl[k] = 40 as u8 329 k = k + 1 330 if dp_find_in(bb, nb, ndl, k) == 1 { return 1 } 331 } 332 i = j 333 } else { i = i + 1 } 334 } 335 return 0 336} 337 338// 1 = a name exported by BOTH copies has a DIFFERENT parameter count | 0 = none | -1 = unreadable. 339// THE SILENT CLASS. A full name collision is loud the moment anything resolves the wrong copy; a PARTIAL 340// overlap binds QUIETLY and only the mismatched call misbehaves -- and nx_cc is fails-open on exactly that 341// (seq1012, 1785447657), so there is no diagnostic. Live instance: nx_input_abstract.nx, where runtime:88 342// is ia_init(p: *i64) and _hdl_build:68 is ia_init(s: *i64, nact: i64) while the rest is disjoint. 343func dp_sig_mismatch(pa: *u8, pb: *u8, ba: *u8, bb: *u8, cap: i64) -> i64 { 344 let na: i64 = dp_slurp(pa, ba, cap) 345 if na <= 0 { return 0 - 1 } 346 let nb: i64 = dp_slurp(pb, bb, cap) 347 if nb <= 0 { return 0 - 1 } 348 if (dp_ndl as i64) == 0 { dp_ndl = sys_mmap(DP_NDLCAP) } 349 let ndl: *u8 = dp_ndl 350 var i: i64 = 0 351 while i + 5 <= na { 352 var isf: i64 = 0 353 if ba[i] == (102 as u8) { if ba[i+1] == (117 as u8) { if ba[i+2] == (110 as u8) { if ba[i+3] == (99 as u8) { if ba[i+4] == (32 as u8) { isf = 1 } } } } } 354 if isf == 1 { if i > 0 { if ba[i-1] != (10 as u8) { isf = 0 } } } 355 if isf == 1 { 356 var k: i64 = 0 357 while k < 5 { ndl[k] = ba[i+k]; k = k + 1 } 358 var j: i64 = i + 5 359 var go: i64 = 1 360 while go == 1 { 361 if j >= na { go = 0 } else { 362 let c: i64 = ba[j] as i64 363 if c == 40 { go = 0 } else { if c == 32 { go = 0 } else { if c == 10 { go = 0 } else { 364 if k < DP_NDLCAP - 2 { ndl[k] = ba[j]; k = k + 1 } 365 j = j + 1 366 } } } 367 } 368 } 369 if k > 5 { 370 ndl[k] = 40 as u8 371 k = k + 1 372 let pos: i64 = dp_find_at(bb, nb, ndl, k) 373 if pos >= 0 { 374 let aa: i64 = dp_arity(ba, na, i) 375 let ab: i64 = dp_arity(bb, nb, pos) 376 if aa >= 0 { if ab >= 0 { if aa != ab { return 1 } } } 377 } 378 } 379 i = j 380 } else { i = i + 1 } 381 } 382 return 0 383} 384 385func dp_has_main(path: *u8, buf: *u8, cap: i64) -> i64 { 386 let n: i64 = dp_slurp(path, buf, cap) 387 if n <= 0 { return 0 - 1 } 388 let pat: *u8 = "func main(" as *u8 389 var pn: i64 = 0 390 while pat[pn] != (0 as u8) { pn = pn + 1 } 391 if pn > n { return 0 } 392 var i: i64 = 0 393 while i + pn <= n { 394 var k: i64 = 0 395 var hit: i64 = 1 396 while k < pn { if buf[i+k] != pat[k] { hit = 0; k = pn } else { k = k + 1 } } 397 if hit == 1 { return 1 } 398 i = i + 1 399 } 400 return 0 401} 402 403// ★v4 (seq207): a dup basename is only a REAL clobber hazard when the two copies DIVERGE. Identical 404// copies are litter (still LATENT -- the next one-sided edit diverges them). v3 compared basenames ONLY, 405// which is why 31 undifferentiated alarms sat unreconciled: nobody could tell which ones could actually 406// regress a service. stats[0]=divergent-IN-PROBE-PATH (real hazard) stats[1]=identical stats[2]=unknown 407// Compare buffers are CALLER-OWNED, allocated ONCE PER RUN: re-costing a callee you amplified is the v2 408// OOM lesson (2MiB per pair x 78 pairs = 156MB leaked). 409// ★★seq1343: NAMING WHICH COPY WINS IS NOT ENOUGH -- THE CASE THAT SILENTLY EATS WORK IS THE RESOLVED 410// COPY BEING THE OLDER ONE. Measured live 2026-07-30: nx_skullgen.nx resolved the 15481B Jul-27-15:03 411// copy while a 17685B Jul-27-21:56 copy -- the F1110 skull-v2 anatomy fix, 6h53m newer -- sat in a 412// shadow the build never read, so the fix for the renders-as-an-egg defect had never once compiled. 413// ★dp_mtime ALREADY EXISTED and was gate-proven (T8/T8b), and line 43 of this very file already said 414// "only mtime says which way to reconcile" -- it was simply never wired to the hazard line. Freshness 415// IS the triage: it turns each divergent pair from an investigation into one readable row. 416func dp_fresh(pa: *u8, pb: *u8, dirA: *u8, dirB: *u8, sb: *u8) -> i64 { 417 let ma: i64 = dp_mtime(pa, sb) 418 let mb: i64 = dp_mtime(pb, sb) 419 if ma < 0 { return 0 } 420 if mb < 0 { return 0 } 421 var d: i64 = ma - mb 422 if d < 0 { d = 0 - d } 423 var nw: i64 = 0 424 dp_w(" FRESHNESS: newer copy is in " as *u8) 425 if ma > mb { dp_w(dirA); nw = 1 } 426 if mb > ma { dp_w(dirB); nw = 2 } 427 if ma == mb { dp_w("(both carry the same mtime)" as *u8) } 428 dp_w(" gap " as *u8) 429 dp_wn(d) 430 dp_w("s -- crossed with the probe order below (seq1343)\n" as *u8) 431 return nw 432} 433 434func scan2(dirA: *u8, dirB: *u8, verbose: i64, stats: *i64, ca: *u8, cb: *u8, ccap: i64) -> i64 { 435 let fd: i64=sys_openat_rd(dirA) 436 if fd < 0 { return 0 } 437 let dbuf: *u8=sys_mmap(K_MAGIC_131072) 438 let pbuf: *u8=sys_mmap(K_MAGIC_4096) 439 let pa: *u8=sys_mmap(K_MAGIC_4096) 440 // one stat buffer per dir-pair, allocated with its siblings -- NEVER inside the file loop (v2 OOM lesson) 441 let stb: *u8=sys_mmap(K_MAGIC_4096) 442 var dups: i64=0 443 var go: i64=1 444 while go == 1 { 445 let nr: i64=sys_getdents64(fd, dbuf, K_MAGIC_131072) 446 if nr <= 0 { go = 0 } else { 447 var off: i64=0 448 while off < nr { 449 let rec: *u8=(dbuf as i64 + off) as *u8 450 let ty: i64=dirent_type(rec) 451 let nm: *u8=dirent_name(rec) 452 if ty != 4 { 453 if ends_nx(nm) == 1 { 454 if exists(dirB, nm, pbuf) == 1 { 455 dups = dups + 1 456 if verbose == 1 { dp_w(" DUP basename: "); dp_w(nm); dp_w(" (present in both "); dp_w(dirA); dp_w(" and "); dp_w(dirB); dp_w(")\ 457" as *u8) } 458 join_path(pa, dirA, nm) 459 let sm: i64 = dp_same(pa, pbuf, ca, cb, ccap) 460 if sm == 1 { 461 stats[1] = stats[1] + 1 462 if verbose == 1 { dp_w(" ^ identical bytes -- litter (latent: diverges on the next one-sided edit)\ 463" as *u8) } 464 } else { 465 if sm == 0 { 466 if dp_is_probed(dirA)*dp_is_probed(dirB) == 0 { 467 stats[3] = stats[3] + 1 468 if verbose == 1 { dp_w(" ^ divergent but OUT OF THE BUILD PROBE PATH -- a build never reads this tree, so it cannot clobber; litter, not a hazard\ 469" as *u8) } 470 } else { 471 var nwin: i64 = 0 472 if verbose == 1 { nwin = dp_fresh(pa, pbuf, dirA, dirB, stb) } 473 // v7: WHICH resolution rule applies is decided by target-vs-library, read from the 474 // ARTIFACT. Fail-closed: dp_has_main returns -1 when it cannot read, and -1 counts 475 // as a target so an unreadable pair keeps the LOUD hazard line instead of being 476 // quietly downgraded to litter. 477 let hm_a: i64 = dp_has_main(pa, ca, ccap) 478 let hm_b: i64 = dp_has_main(pbuf, cb, ccap) 479 var istgt: i64 = 0 480 if hm_a != 0 { istgt = 1 } 481 if hm_b != 0 { istgt = 1 } 482 // the newer copy LOSES iff it sits in the dir the compiler probes LATER -- a 483 // question that only MEANS anything for a target, so it is asked only for one. 484 var lost: i64 = 0 485 if istgt == 1 { 486 if nwin == 1 { if dp_probe_rank(dirA) > dp_probe_rank(dirB) { lost = 1 } } 487 if nwin == 2 { if dp_probe_rank(dirB) > dp_probe_rank(dirA) { lost = 1 } } 488 } 489 // ★v10: EXACTLY ONE copy has main => LIBRARY IN ONE DIR, TARGET IN THE OTHER. The 490 // library wins the target probe rank, so building this basename ALWAYS resolves the 491 // main-less copy and ALWAYS fails nxasm rc=102 'UNDEFINED label: main'. Measured on 492 // nx_hls_parse (id 1785520111): permanently RED, and no edit to either FILE can clear 493 // it -- only a rename can. This is the one dup shape that announces itself; the other 494 // four fail silently, which is exactly why it is worth naming rather than describing. 495 var lvt: i64 = 0 496 if hm_a == 1 { if hm_b == 0 { lvt = 1 } } 497 if hm_b == 1 { if hm_a == 0 { lvt = 1 } } 498 if lvt == 1 { stats[7] = stats[7] + 1 } 499 // ⚠THE SHAPE IS SYMMETRIC BUT THE CONSEQUENCE IS NOT, and v10's first message got this 500 // WRONG by claiming the build ALWAYS fails. It fails only when the copy that WINS the 501 // probe rank is the MAIN-LESS one. Measured: nx_hls_parse and nx_tissue lose that way 502 // (rc=102 / NOELF), while nx_gen's rank-winning copy DOES have main so it builds fine -- 503 // and the damage there is the opposite, the main-less R5 generator can never be built 504 // under its own name. Same collision, two different failures; say which one. 505 var deadwin: i64 = 0 506 if lvt == 1 { deadwin = dp_deadwin(dp_probe_rank(dirA), hm_a, dp_probe_rank(dirB), hm_b) } 507 if verbose == 1 { if lvt == 1 { if deadwin == 1 { 508 dp_w(" ^ LIBRARY-VS-TARGET COLLISION (BUILD IS DEAD): the copy that WINS the probe rank has NO main(), so building this basename fails nxasm rc=102 UNDEFINED label: main -- permanently, and no edit to either file clears it. Only a RENAME does\n" as *u8) 509 } else { 510 dp_w(" ^ LIBRARY-VS-TARGET COLLISION (BUILD OK, OTHER COPY UNREACHABLE): the rank-winning copy HAS main() so this target builds, but the main-less copy is a LIBRARY that can never be built or resolved under this name. RENAME it so it becomes reachable\n" as *u8) 511 } } } 512 // v8: disjoint exports = two PROGRAMS, not two versions. Asked for TARGETS too, 513 // because nx_gen and nx_tissue -- the costliest collisions found -- are targets. 514 let ov: i64 = dp_exports_overlap(pa, pbuf, ca, cb, ccap) 515 if ov == 0 { stats[5] = stats[5] + 1 } 516 if verbose == 1 { if ov == 0 { 517 dp_w(" ^ NAME COLLISION: the two copies share NO exported func name -- these are DIFFERENT PROGRAMS wearing one filename, not two versions of one module. Do NOT reconcile them to one dir (that DELETES a program); RENAME one so the name carries the identity\n" as *u8) 518 } } 519 // v9: only meaningful when the names DO overlap -- that is the partial case. 520 var sig: i64 = 0 521 if ov == 1 { sig = dp_sig_mismatch(pa, pbuf, ca, cb, ccap) } 522 if sig == 1 { stats[6] = stats[6] + 1 } 523 if verbose == 1 { if sig == 1 { 524 dp_w(" ^ SIGNATURE MISMATCH: both copies export the SAME name with a DIFFERENT parameter count. Worse than a full collision because it binds SILENTLY -- only the mismatched call misbehaves and nx_cc emits no diagnostic. Fix FIRST, and fix it by RENAMING, not by merging\n" as *u8) 525 } } 526 if istgt == 1 { stats[0] = stats[0] + 1 } else { stats[4] = stats[4] + 1 } 527 if verbose == 1 { if istgt == 0 { 528 dp_w(" ^ DIVERGENT LIBRARY (no main() in either copy) -- an import resolves to the IMPORTER'S OWN dir FIRST, so there is NO single losing copy and this is NOT work-loss. Reconcile ONLY if both copies export the SAME symbols; disjoint exports are a deliberate per-layer split (nx_clock.nx: runtime=monotonic clock, _hdl_build=tickless scheduler) and collapsing them re-darkens every importer of whichever copy you delete\n" as *u8) 529 } else { if lost == 1 { 530 dp_w(" ^ DIVERGENT + WORK-LOSS NOW: the NEWER copy sits in the dir the compiler probes SECOND, so EVERY build silently compiles the OLDER source\n" as *u8) 531 } else { 532 dp_w(" ^ DIVERGENT but the newer copy is the one resolved FIRST -- builds are correct today; litter, still reconcile (one one-sided edit flips it)\n" as *u8) 533 } } } 534 } 535 } else { 536 stats[2] = stats[2] + 1 537 if verbose == 1 { dp_w(" ^ UNKNOWN (unreadable or over the compare cap) -- NOT assumed identical\ 538" as *u8) } 539 } 540 } 541 } 542 } 543 } 544 off = off + dirent_reclen(rec) 545 } 546 } 547 } 548 sys_close(fd) 549 return dups 550} 551 552// v1/v2/v3 SIGNATURE PRESERVED VERBATIM (rule 19) -- selftest and any other caller keep working. 553func scan(dirA: *u8, dirB: *u8, verbose: i64) -> i64 { 554 // ★v7: 32->64 for the same reason as main's -- scan2 now writes st[4], and selftest reaches 555 // scan2 THROUGH here, so leaving this at 4 slots would make the SELF-TEST the out-of-bounds writer. 556 let st: *i64 = sys_mmap(64) as *i64 557 st[0]=0; st[1]=0; st[2]=0; st[3]=0; st[4]=0 558 let ca: *u8 = sys_mmap(K_CMP_CAP) 559 let cb: *u8 = sys_mmap(K_CMP_CAP) 560 return scan2(dirA, dirB, verbose, st, ca, cb, K_CMP_CAP) 561} 562 563func wfile(path: *u8) -> i64 { let fd: i64=sys_openat_wr(path, 420); if fd>=0 { sys_write(fd, "x" as *u8, 1); sys_close(fd) } return 0 } 564// content-specific fixture writer for the v4 divergence tooth. Content is FIXED PER PATH so repeated 565// runs stay idempotent even though sys_openat_wr does not truncate. 566func wfile2(path: *u8, s: *u8) -> i64 { 567 let fd: i64=sys_openat_wr(path, 420) 568 if fd>=0 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd, s, n); sys_close(fd) } 569 return 0 570} 571 572// BFS-discover dirs holding >=1 .nx. Buffers hoisted; allocation is O(1) per walk, not per entry. 573func discover(root: *u8, out: *u8, maxd: i64, maxdepth: i64, cap: *i64) -> i64 { 574 let QCAP: i64 = 256 575 let qbuf: *u8 = sys_mmap(QCAP*256) 576 let qdep: *i64 = sys_mmap(QCAP*8) as *i64 577 let dbuf: *u8 = sys_mmap(K_MAGIC_131072) 578 let child: *u8 = sys_mmap(K_MAGIC_4096) 579 var qh: i64 = 0 580 var qt: i64 = 0 581 cap[0] = 0 582 str_copy((qbuf as i64) as *u8, root); qdep[0]=0; qt=1 583 var nd: i64 = 0 584 while qh < qt { 585 let cur: *u8 = (qbuf as i64 + qh*256) as *u8 586 let cdep: i64 = qdep[qh] 587 qh = qh + 1 588 let fd: i64 = sys_openat_rd(cur) 589 if fd >= 0 { 590 var hasnx: i64 = 0 591 var go: i64 = 1 592 while go == 1 { 593 let nr: i64 = sys_getdents64(fd, dbuf, K_MAGIC_131072) 594 if nr <= 0 { go = 0 } else { 595 var off: i64 = 0 596 while off < nr { 597 let rec: *u8 = (dbuf as i64 + off) as *u8 598 let ty: i64 = dirent_type(rec) 599 let nm: *u8 = dirent_name(rec) 600 if ty == 4 { 601 if is_dot(nm) == 0 { 602 if cdep < maxdepth { 603 if qt < QCAP { 604 join_path(child, cur, nm) 605 str_copy((qbuf as i64 + qt*256) as *u8, child) 606 qdep[qt] = cdep + 1 607 qt = qt + 1 608 } else { cap[0] = 1 } 609 } 610 } 611 } else { 612 if ends_nx(nm) == 1 { hasnx = 1 } 613 } 614 off = off + dirent_reclen(rec) 615 } 616 } 617 } 618 sys_close(fd) 619 if hasnx == 1 { 620 if nd < maxd { str_copy((out as i64 + nd*256) as *u8, cur); nd = nd + 1 } 621 else { cap[0] = 1 } 622 } 623 } 624 } 625 return nd 626} 627 628func selftest() -> i64 { 629 // T1 v1 REGRESSION: two-dir case still finds exactly one dup, ignores the non-dup 630 sys_mkdir("/tmp/nxdupA" as *u8, 0x1ed) 631 sys_mkdir("/tmp/nxdupB" as *u8, 0x1ed) 632 wfile("/tmp/nxdupA/foo.nx" as *u8) 633 wfile("/tmp/nxdupA/bar.nx" as *u8) 634 wfile("/tmp/nxdupB/foo.nx" as *u8) 635 let dups: i64=scan("/tmp/nxdupA" as *u8, "/tmp/nxdupB" as *u8, 0) 636 if dups != 1 { dp_w(" SELFTEST FAIL T1: expected 1 dup, got "); dp_wn(dups); dp_w("\ 637" as *u8); return 0 } 638 // T2 nested-tree tooth (the seq281 class v1 was blind to) 639 sys_mkdir("/tmp/nxdup3" as *u8, 0x1ed) 640 sys_mkdir("/tmp/nxdup3/_hdl_build" as *u8, 0x1ed) 641 sys_mkdir("/tmp/nxdup3/runtime" as *u8, 0x1ed) 642 wfile("/tmp/nxdup3/only_here.nx" as *u8) 643 wfile("/tmp/nxdup3/_hdl_build/organ.nx" as *u8) 644 wfile("/tmp/nxdup3/runtime/organ.nx" as *u8) 645 let tbl: *u8 = sys_mmap(16*256) 646 let cap: *i64 = sys_mmap(16) as *i64 647 let n: i64 = discover("/tmp/nxdup3" as *u8, tbl, 16, 5, cap) 648 if n != 3 { dp_w(" SELFTEST FAIL T2a: expected 3 trees, got "); dp_wn(n); dp_w("\ 649" as *u8); return 0 } 650 var found: i64 = 0 651 var i: i64 = 0 652 while i < n { 653 var j: i64 = i + 1 654 while j < n { found = found + scan((tbl as i64 + i*256) as *u8, (tbl as i64 + j*256) as *u8, 0); j = j + 1 } 655 i = i + 1 656 } 657 if found != 1 { dp_w(" SELFTEST FAIL T2b: nested dup not caught, got "); dp_wn(found); dp_w("\ 658" as *u8); return 0 } 659 // T3 NON-VACUOUS negative control: v1's hardcoded pair reports ZERO here 660 let v1blind: i64 = scan("/tmp/nxdup3" as *u8, "/tmp/nxdup3/_hdl_build" as *u8, 0) 661 if v1blind != 0 { dp_w(" SELFTEST FAIL T3: negative control expected 0, got "); dp_wn(v1blind); dp_w("\ 662" as *u8); return 0 } 663 // T4 truncation must be LOUD, never silent 664 let tbl2: *u8 = sys_mmap(4*256) 665 let cap2: *i64 = sys_mmap(16) as *i64 666 discover("/tmp/nxdup3" as *u8, tbl2, 1, 5, cap2) 667 if cap2[0] != 1 { dp_w(" SELFTEST FAIL T4: silent cap\ 668" as *u8); return 0 } 669 // ★T5 RESOURCE TOOTH -- the half of the scale law v2 was missing. 600 files in ONE dir: v2 would have 670 // allocated 600 x 4096 here (per-file mmap); v3 allocates TWO buffers for the whole pair. This asserts the 671 // hot loop is allocation-free by exercising it at a file count a fixture normally never reaches. 672 sys_mkdir("/tmp/nxdupR" as *u8, 0x1ed) 673 sys_mkdir("/tmp/nxdupS" as *u8, 0x1ed) 674 let nmb: *u8 = sys_mmap(256) 675 let dg: *u8 = sys_mmap(16) 676 // ⚠LM-030: a string literal may NOT be indexed in expression position (CONST[i] crashes nx_cc with 677 // "unexpected operator token kind=47"). Bind it to a local pointer FIRST, then index the local. 678 let pfx: *u8 = "/tmp/nxdupR/f" as *u8 679 var k: i64 = 0 680 while k < 600 { 681 // build /tmp/nxdupR/f<k>.nx -- ZERO allocations in this loop (nmb + dg are hoisted above), which is the 682 // whole point of the tooth: if the hot path allocated per iteration this fixture would show it. 683 var p: i64 = 0 684 while pfx[p]!=(0 as u8) { nmb[p]=pfx[p]; p=p+1 } 685 var t: i64 = k 686 var dn: i64 = 0 687 if t==0 { dg[0]=48 as u8; dn=1 } 688 while t>0 { dg[dn]=(48+(t%10)) as u8; t=t/10; dn=dn+1 } 689 var q: i64 = 0 690 while q<dn { nmb[p]=dg[dn-1-q]; p=p+1; q=q+1 } 691 nmb[p]=46 as u8; nmb[p+1]=110 as u8; nmb[p+2]=120 as u8; nmb[p+3]=0 as u8 692 wfile(nmb) 693 k = k + 1 694 } 695 let big: i64 = scan("/tmp/nxdupR" as *u8, "/tmp/nxdupS" as *u8, 0) 696 if big != 0 { dp_w(" SELFTEST FAIL T5: expected 0 dups vs empty dir, got "); dp_wn(big); dp_w("\ 697" as *u8); return 0 } 698 // ★T6 (v4/seq207) DIVERGENCE TOOTH, with its own negative control in the same fixture: the detector 699 // must tell a DIVERGENT dup (a real clobber hazard -- a rebuild can resolve the stale copy and 700 // silently regress a service) from an IDENTICAL one (litter). Both are the SAME basename in two 701 // trees, so a detector that only compares names cannot separate them and scores 2/0 here. 702 sys_mkdir("/tmp/nxdupD1" as *u8, 0x1ed) 703 sys_mkdir("/tmp/nxdupD2" as *u8, 0x1ed) 704 wfile2("/tmp/nxdupD1/same.nx" as *u8, "IDENTICAL-BYTES" as *u8) 705 wfile2("/tmp/nxdupD2/same.nx" as *u8, "IDENTICAL-BYTES" as *u8) 706 // ★v7: these two now carry a main(), because T6 is the TARGET divergence tooth and only a target 707 // resolves by probe rank. wfile2 does NOT truncate, so each replacement content is deliberately 708 // LONGER than the string it supersedes -- a shorter one would leave the old tail behind and the 709 // fixture would silently stop being what its name claims. 710 wfile2("/tmp/nxdupD1/diff.nx" as *u8, "func main() -> i64 { return 0 } VERSION-A" as *u8) 711 wfile2("/tmp/nxdupD2/diff.nx" as *u8, "func main() -> i64 { return 1 } VERSION-B-longer" as *u8) 712 // ★v7: 32->64. scan2 now writes stats[4]; at 4 slots THE SELF-TEST would be the OOB writer. 713 let st6: *i64 = sys_mmap(64) as *i64 714 st6[0]=0; st6[1]=0; st6[2]=0; st6[3]=0; st6[4]=0 715 let c6a: *u8 = sys_mmap(K_CMP_CAP) 716 let c6b: *u8 = sys_mmap(K_CMP_CAP) 717 let d6: i64 = scan2("/tmp/nxdupD1" as *u8, "/tmp/nxdupD2" as *u8, 0, st6, c6a, c6b, K_CMP_CAP) 718 if d6 != 2 { dp_w(" SELFTEST FAIL T6a: expected 2 dup basenames, got "); dp_wn(d6); dp_w("\ 719" as *u8); return 0 } 720 if st6[0] != 1 { dp_w(" SELFTEST FAIL T6b: expected exactly 1 DIVERGENT, got "); dp_wn(st6[0]); dp_w("\ 721" as *u8); return 0 } 722 if st6[1] != 1 { dp_w(" SELFTEST FAIL T6c: expected exactly 1 IDENTICAL, got "); dp_wn(st6[1]); dp_w("\ 723" as *u8); return 0 } 724 if st6[2] != 0 { dp_w(" SELFTEST FAIL T6d: expected 0 unknown, got "); dp_wn(st6[2]); dp_w("\ 725" as *u8); return 0 } 726 // ★T6f (v8) NEGATIVE CONTROL FOR THE COLLISION RULE, free from the T6 fixture: both diff.nx copies 727 // define main(), so their exports OVERLAP and they are two VERSIONS -- must NOT read as a collision. 728 if st6[5] != 0 { dp_w(" SELFTEST FAIL T6f: overlapping exports must NOT be a name collision, got "); dp_wn(st6[5]); dp_w("\n" as *u8); return 0 } 729 // ★T6e (v7) THE NEW RULE'S OWN TOOTH, negative control built in. T6 above proves a divergent TARGET 730 // still counts as a clobber hazard; this proves a divergent LIBRARY does not, and lands in the library 731 // bucket instead. BOTH halves are required -- a change that silenced both would sail through a 732 // one-sided test while quietly destroying the detector, which is the exact failure mode that let the 733 // false nx_clock.nx alarm stand. The fixtures carry NO main(), which is what makes them libraries, and 734 // they are the SAME LENGTH on purpose so the comparison must reach the byte loop rather than stopping 735 // at a size mismatch. 736 sys_mkdir("/tmp/nxdupL1" as *u8, 0x1ed) 737 sys_mkdir("/tmp/nxdupL2" as *u8, 0x1ed) 738 wfile2("/tmp/nxdupL1/lib.nx" as *u8, "func lib_alpha() -> i64 { return 1 }" as *u8) 739 wfile2("/tmp/nxdupL2/lib.nx" as *u8, "func lib_beta() -> i64 { return 22 }" as *u8) 740 let st7: *i64 = sys_mmap(64) as *i64 741 st7[0]=0; st7[1]=0; st7[2]=0; st7[3]=0; st7[4]=0 742 let c7a: *u8 = sys_mmap(K_CMP_CAP) 743 let c7b: *u8 = sys_mmap(K_CMP_CAP) 744 let d7: i64 = scan2("/tmp/nxdupL1" as *u8, "/tmp/nxdupL2" as *u8, 0, st7, c7a, c7b, K_CMP_CAP) 745 if d7 != 1 { dp_w(" SELFTEST FAIL T6e-a: expected 1 dup basename, got "); dp_wn(d7); dp_w("\n" as *u8); return 0 } 746 if st7[0] != 0 { dp_w(" SELFTEST FAIL T6e-b: a divergent LIBRARY must NOT be counted a clobber hazard, got "); dp_wn(st7[0]); dp_w("\n" as *u8); return 0 } 747 if st7[4] != 1 { dp_w(" SELFTEST FAIL T6e-c: expected exactly 1 divergent-library, got "); dp_wn(st7[4]); dp_w("\n" as *u8); return 0 } 748 // ★T6e-d (v8) THE COLLISION RULE ITSELF: lib_alpha vs lib_beta share NO exported name, so these are two 749 // PROGRAMS wearing one filename and must be flagged. With T6f above, both directions are now bitten. 750 if st7[5] != 1 { dp_w(" SELFTEST FAIL T6e-d: disjoint exports must be flagged a NAME COLLISION, got "); dp_wn(st7[5]); dp_w("\n" as *u8); return 0 } 751 // ★T6g (v9) SIGNATURE-MISMATCH TOOTH + negative control: T6 shares main() at the SAME arity and must 752 // NOT flag; this pair shares sig_go at DIFFERENT arity and MUST. Without both halves a constant-return 753 // dp_sig_mismatch would pass. 754 if st6[6] != 0 { dp_w(" SELFTEST FAIL T6g-a: equal arity must NOT be a signature mismatch, got "); dp_wn(st6[6]); dp_w("\n" as *u8); return 0 } 755 sys_mkdir("/tmp/nxdupS1" as *u8, 0x1ed) 756 sys_mkdir("/tmp/nxdupS2" as *u8, 0x1ed) 757 wfile2("/tmp/nxdupS1/sig.nx" as *u8, "func sig_go(a: *i64) -> i64 { return 1 }" as *u8) 758 wfile2("/tmp/nxdupS2/sig.nx" as *u8, "func sig_go(a: *i64, b: i64) -> i64 { return 2 }" as *u8) 759 let st8: *i64 = sys_mmap(64) as *i64 760 st8[0]=0; st8[1]=0; st8[2]=0; st8[3]=0; st8[4]=0; st8[5]=0; st8[6]=0 761 let c8a: *u8 = sys_mmap(K_CMP_CAP) 762 let c8b: *u8 = sys_mmap(K_CMP_CAP) 763 let d8: i64 = scan2("/tmp/nxdupS1" as *u8, "/tmp/nxdupS2" as *u8, 0, st8, c8a, c8b, K_CMP_CAP) 764 if d8 != 1 { dp_w(" SELFTEST FAIL T6g-b: expected 1 dup basename, got "); dp_wn(d8); dp_w("\n" as *u8); return 0 } 765 if st8[5] != 0 { dp_w(" SELFTEST FAIL T6g-c: a SHARED name must not read as a name collision, got "); dp_wn(st8[5]); dp_w("\n" as *u8); return 0 } 766 if st8[6] != 1 { dp_w(" SELFTEST FAIL T6g-d: differing arity must be flagged SIGNATURE MISMATCH, got "); dp_wn(st8[6]); dp_w("\n" as *u8); return 0 } 767 // ★T6h (v10) LIBRARY-VS-TARGET TOOTH + negative control: T6 has main() in BOTH copies and must NOT 768 // flag; this pair has main in exactly ONE and MUST. Both halves or a constant-return would pass. 769 if st6[7] != 0 { dp_w(" SELFTEST FAIL T6h-a: main() in BOTH copies must NOT flag library-vs-target, got "); dp_wn(st6[7]); dp_w("\n" as *u8); return 0 } 770 sys_mkdir("/tmp/nxdupT1" as *u8, 0x1ed) 771 sys_mkdir("/tmp/nxdupT2" as *u8, 0x1ed) 772 wfile2("/tmp/nxdupT1/mix.nx" as *u8, "func main() -> i64 { return 0 }" as *u8) 773 wfile2("/tmp/nxdupT2/mix.nx" as *u8, "func mix_helper(a: *i64) -> i64 { return 1 }" as *u8) 774 let st9: *i64 = sys_mmap(64) as *i64 775 st9[0]=0; st9[1]=0; st9[2]=0; st9[3]=0; st9[4]=0; st9[5]=0; st9[6]=0; st9[7]=0 776 let c9a: *u8 = sys_mmap(K_CMP_CAP) 777 let c9b: *u8 = sys_mmap(K_CMP_CAP) 778 let d9: i64 = scan2("/tmp/nxdupT1" as *u8, "/tmp/nxdupT2" as *u8, 0, st9, c9a, c9b, K_CMP_CAP) 779 if d9 != 1 { dp_w(" SELFTEST FAIL T6h-b: expected 1 dup basename, got "); dp_wn(d9); dp_w("\n" as *u8); return 0 } 780 if st9[7] != 1 { dp_w(" SELFTEST FAIL T6h-c: main in exactly ONE copy must flag LIBRARY-VS-TARGET, got "); dp_wn(st9[7]); dp_w("\n" as *u8); return 0 } 781 // ★T6i (v10.1) THE deadwin BRANCH, unit-tested with SYNTHETIC ranks because no fixture can reach it: 782 // dp_probe_rank returns 9 for anything outside the four ranked build dirs, so a /tmp pair always TIES 783 // and T6h above can only ever exercise the BUILD-OK side. Both directions plus the tie are asserted, so 784 // a dp_deadwin that returned a constant could not pass. A branch no fixture can reach is a branch no 785 // gate is testing -- this is the honest way to close that, and it is why dp_deadwin is a pure function. 786 if dp_deadwin(0, 0, 1, 1) != 1 { dp_w(" SELFTEST FAIL T6i-a: a rank-winner WITHOUT main must read DEAD\n" as *u8); return 0 } 787 if dp_deadwin(1, 1, 0, 0) != 1 { dp_w(" SELFTEST FAIL T6i-b: DEAD detection must not depend on argument order\n" as *u8); return 0 } 788 if dp_deadwin(0, 1, 1, 0) != 0 { dp_w(" SELFTEST FAIL T6i-c: a rank-winner WITH main builds fine (the nx_gen shape) and must NOT read DEAD\n" as *u8); return 0 } 789 if dp_deadwin(1, 0, 0, 1) != 0 { dp_w(" SELFTEST FAIL T6i-d: the mirror of T6i-c must also NOT read DEAD\n" as *u8); return 0 } 790 if dp_deadwin(9, 0, 9, 1) != 0 { dp_w(" SELFTEST FAIL T6i-e: TIED ranks support no claim and must NOT read DEAD\n" as *u8); return 0 } 791 // T7 the fingerprint must be ORDER-SENSITIVE. A plain byte total calls "ab" and "ba" equal, and a 792 // fingerprint blind to a transposition would bless a corrupted file as matching its canonical. 793 let fa: *u8 = sys_mmap(16) 794 let fb: *u8 = sys_mmap(16) 795 fa[0]=(97 as u8) 796 fa[1]=(98 as u8) 797 fb[0]=(98 as u8) 798 fb[1]=(97 as u8) 799 if dp_sum(fa,2) == dp_sum(fb,2) { dp_w(" SELFTEST FAIL T7: fingerprint is order-BLIND -- ab and ba produced the same value\ 800" as *u8); return 0 } 801 // T7b positive control. Without this, a fingerprint that returned a different value every call 802 // would sail through T7 while being useless for comparison -- the classic non-vacuity trap. 803 fb[0]=(97 as u8) 804 fb[1]=(98 as u8) 805 if dp_sum(fa,2) != dp_sum(fb,2) { dp_w(" SELFTEST FAIL T7b: identical content produced DIFFERENT fingerprints\ 806" as *u8); return 0 } 807 // T8 the freshness channel must round-trip a KNOWN value. Without this the column could return a 808 // constant, or garbage from the wrong struct offset, and every direction call built on it would be 809 // wrong in the one situation it exists for. 810 let tsb: *u8 = sys_mmap(FP_STATBUF) 811 let tms: *i64 = sys_mmap(64) as *i64 812 tms[0] = FP_TEST_MTIME 813 tms[1] = 0 814 tms[2] = FP_TEST_MTIME 815 tms[3] = 0 816 if sys_utimensat("/tmp/nxdupA/foo.nx" as *u8, tms) != 0 { dp_w(" SELFTEST FAIL T8: could not set a known mtime\ 817" as *u8); return 0 } 818 let gotm: i64 = dp_mtime("/tmp/nxdupA/foo.nx" as *u8, tsb) 819 if gotm != FP_TEST_MTIME { dp_w(" SELFTEST FAIL T8: mtime round-trip got "); dp_wn(gotm); dp_w(" want "); dp_wn(FP_TEST_MTIME); dp_w("\ 820" as *u8); return 0 } 821 // T8b an unstattable path must report -1, NOT 0. A zero would pass for a real epoch timestamp and 822 // silently rank a missing file as the oldest thing in the tree. 823 if dp_mtime("/tmp/nxdupA/does_not_exist.nx" as *u8, tsb) != (0 - 1) { dp_w(" SELFTEST FAIL T8b: a missing file did not report -1\ 824" as *u8); return 0 } 825 // T7c length must matter independently of content, or a truncation reads as a match. 826 fb[2]=(98 as u8) 827 if dp_sum(fb,2) == dp_sum(fb,3) { dp_w(" SELFTEST FAIL T7c: a truncated read produced the same fingerprint\ 828" as *u8); return 0 } 829 return 1 830} 831 832// Pass 1: count only. Cheap -- dirents only, no file is opened -- and it is what makes the total 833// honest before a single row is emitted. 834// -1 when the file cannot be stat''d. Never 0: a zero would read as a real epoch timestamp and make an 835// unstattable file look like the oldest thing in the tree. 836func dp_mtime(path: *u8, sb: *u8) -> i64 { 837 if sys_fstatat(path, sb) != 0 { return 0 - 1 } 838 let sp: *i64 = sb as *i64 839 return sp[FP_MTIME_SLOT] 840} 841 842func dp_fp_count(dir: *u8) -> i64 { 843 let fd: i64 = sys_openat_rd(dir) 844 if fd < 0 { return 0 - 1 } 845 let dbuf: *u8 = sys_mmap(K_MAGIC_131072) 846 var nf: i64 = 0 847 var go: i64 = 1 848 while go == 1 { 849 let nr: i64 = sys_getdents64(fd, dbuf, K_MAGIC_131072) 850 if nr <= 0 { go = 0 } else { 851 var off: i64 = 0 852 while off < nr { 853 let rec: *u8 = (dbuf as i64 + off) as *u8 854 if dirent_type(rec) != 4 { if ends_nx(dirent_name(rec)) == 1 { nf = nf + 1 } } 855 off = off + dirent_reclen(rec) 856 } 857 } 858 } 859 sys_close(fd) 860 return nf 861} 862 863func dp_fingerprint(dir: *u8, offset: i64, limit: i64) -> i64 { 864 let total: i64 = dp_fp_count(dir) 865 if total < 0 { dp_w("FINGERPRINT-ERROR cannot open dir: " as *u8); dp_w(dir); dp_w("\n" as *u8); return 1 } 866 var lim: i64 = limit 867 if lim <= 0 { lim = FP_PAGE_DEF } 868 if lim > FP_PAGE_MAX { lim = FP_PAGE_MAX } 869 // The total goes out FIRST and unconditionally. Everything after it may be clipped by the 870 // transport; this line cannot be, so shown-vs-total is always checkable by the caller. 871 dp_w("-- FINGERPRINT dir=" as *u8) 872 dp_w(dir) 873 dp_w(" total=" as *u8) 874 dp_wn(total) 875 dp_w(" offset=" as *u8) 876 dp_wn(offset) 877 dp_w(" limit=" as *u8) 878 dp_wn(lim) 879 dp_w("\n" as *u8) 880 let fd: i64 = sys_openat_rd(dir) 881 if fd < 0 { dp_w("FINGERPRINT-ERROR cannot reopen dir\n" as *u8); return 1 } 882 let dbuf: *u8 = sys_mmap(K_MAGIC_131072) 883 let pth: *u8 = sys_mmap(K_MAGIC_4096) 884 let cbuf: *u8 = sys_mmap(FP_FILECAP) 885 let stb: *u8 = sys_mmap(FP_STATBUF) 886 var idx: i64 = 0 887 var shown: i64 = 0 888 var nerr: i64 = 0 889 var go: i64 = 1 890 while go == 1 { 891 let nr: i64 = sys_getdents64(fd, dbuf, K_MAGIC_131072) 892 if nr <= 0 { go = 0 } else { 893 var off: i64 = 0 894 while off < nr { 895 let rec: *u8 = (dbuf as i64 + off) as *u8 896 let ty: i64 = dirent_type(rec) 897 let nm: *u8 = dirent_name(rec) 898 if ty != 4 { if ends_nx(nm) == 1 { 899 if idx >= offset { if shown < lim { 900 join_path(pth, dir, nm) 901 let n: i64 = dp_slurp(pth, cbuf, FP_FILECAP) 902 dp_w(nm) 903 dp_w("\x09" as *u8) 904 if n < 0 { 905 // Never print a sum we could not compute. A zero here would read as a real 906 // fingerprint and silently match another unreadable file. 907 dp_w("ERR\x09ERR" as *u8) 908 nerr = nerr + 1 909 } else { 910 dp_wn(n) 911 dp_w("\x09" as *u8) 912 dp_wn(dp_sum(cbuf, n)) 913 } 914 // Appended column (Rule 19: additive). Emitted for readable and unreadable 915 // rows alike, so column count never varies between rows. 916 dp_w("\x09" as *u8) 917 dp_wn(dp_mtime(pth, stb)) 918 dp_w("\n" as *u8) 919 shown = shown + 1 920 } } 921 idx = idx + 1 922 } } 923 off = off + dirent_reclen(rec) 924 } 925 } 926 } 927 sys_close(fd) 928 dp_w("-- END shown=" as *u8) 929 dp_wn(shown) 930 dp_w(" offset=" as *u8) 931 dp_wn(offset) 932 dp_w(" total=" as *u8) 933 dp_wn(total) 934 dp_w(" unreadable=" as *u8) 935 dp_wn(nerr) 936 dp_w(" more=" as *u8) 937 if offset + shown < total { dp_wn(1) } else { dp_wn(0) } 938 dp_w("\n" as *u8) 939 return 0 940} 941 942func main(argc: i64, argv: *i64) -> i64 { 943 // Additive verb. With no arguments this organ behaves exactly as before (Rule 19): the full 944 // cross-tree scan is untouched and remains the default. 945 if argc > 2 { if dp_streq(argv[1] as *u8, "fingerprint" as *u8) == 1 { 946 var fpoff: i64 = 0 947 var fplim: i64 = 0 948 if argc > 3 { fpoff = dp_atoi(argv[3] as *u8) } 949 if argc > 4 { fplim = dp_atoi(argv[4] as *u8) } 950 let frc: i64 = dp_fingerprint(argv[2] as *u8, fpoff, fplim) 951 sys_exit(frc) 952 return frc 953 } } 954 dp_w("nx_dup_source_check v3 (cross-tree source-dup detector -- scope DISCOVERED; allocation O(pairs) not O(files))\ 955" as *u8) 956 if selftest() != 1 { dp_w("verdict=RED (self-test of the detector failed)\ 957" as *u8); sys_exit(1); return 1 } 958 dp_w(" self-test OK (v1 regression + nested-tree tooth + non-vacuous negative control + loud-cap + 600-file RESOURCE tooth + v4 divergence-vs-identical tooth + v5 fingerprint order/identity/length teeth + v6 mtime round-trip and missing-file teeth)\ 959" as *u8) 960 let MAXD: i64 = 24 961 let MAXDEPTH: i64 = 6 962 let tbl: *u8 = sys_mmap(MAXD*256) 963 let cap: *i64 = sys_mmap(16) as *i64 964 dp_w(" discovering source trees under buildroot/ ...\ 965" as *u8) 966 let nd: i64 = discover("buildroot" as *u8, tbl, MAXD, MAXDEPTH, cap) 967 var i: i64 = 0 968 while i < nd { dp_w(" source tree: "); dp_w((tbl as i64 + i*256) as *u8); dp_w("\ 969" as *u8); i = i + 1 } 970 dp_w(" source trees discovered: "); dp_wn(nd); dp_w("\ 971" as *u8) 972 var realdups: i64 = 0 973 var pairs: i64 = 0 974 // ★ONCE FOR THE WHOLE RUN, never per pair: 2MiB total. Per-pair would be 2MiB x 78 pairs = 156MB, 975 // which is exactly the v2 OOM-bomb class documented in this file's header. 976 // ★v7: WIDENED 32->64 BEFORE adding a bucket. stats[0..3] filled the old 32-byte (4-slot) 977 // allocation EXACTLY, so writing the new library bucket at stats[4] without this would be a 978 // one-slot out-of-bounds write -- the reg_index OOB class, silent until it corrupts a neighbour. 979 let stats: *i64 = sys_mmap(64) as *i64 980 stats[0] = 0 981 stats[1] = 0 982 stats[2] = 0 983 stats[3] = 0 984 stats[4] = 0 985 stats[5] = 0 986 stats[6] = 0 987 // ⚠v10 USES stats[7], THE LAST SLOT of the 64-byte (8-slot) allocation. The NEXT bucket added here 988 // MUST widen this mmap and scan()'s and every selftest st* buffer FIRST -- see the v7 note above for 989 // why: stats[0..3] filled the original 32-byte alloc exactly and one more write would have been OOB. 990 stats[7] = 0 991 let cmpa: *u8 = sys_mmap(K_CMP_CAP) 992 let cmpb: *u8 = sys_mmap(K_CMP_CAP) 993 i = 0 994 while i < nd { 995 var j: i64 = i + 1 996 while j < nd { 997 realdups = realdups + scan2((tbl as i64 + i*256) as *u8, (tbl as i64 + j*256) as *u8, 1, stats, cmpa, cmpb, K_CMP_CAP) 998 pairs = pairs + 1 999 j = j + 1 1000 } 1001 i = i + 1 1002 } 1003 dp_w(" cross-tree source-dup basenames found: "); dp_wn(realdups); dp_w("\ 1004" as *u8) 1005 dp_w(" LIBRARY-VS-TARGET="); dp_wn(stats[7]) 1006 dp_w(" <- one copy has main(), the other does not. Where the RANK-WINNING copy LACKS main the target is permanently rc=102 (nx_hls_parse, id 1785520111). Where it HAS main the target builds FINE and the main-less copy is the unreachable one (nx_gen: the R5 generator; nx_tissue: the voxel-grid lib -- its NOELF means never-deployed, NOT un-buildable). Only a RENAME clears either\n" as *u8) 1007 dp_w(" TRIAGE (seq207/v10): divergent-target="); dp_wn(stats[0]) 1008 dp_w(" <- REAL clobber hazards (TARGETS only -- these resolve by probe rank), fix FIRST | divergent-library="); dp_wn(stats[4]) 1009 dp_w(" (resolved per-importer from its OWN dir, so NOT work-loss) | NAME-COLLISIONS="); dp_wn(stats[5]) 1010 dp_w(" <- DIFFERENT PROGRAMS sharing one filename (disjoint exports): RENAME, never merge -- the 2026-07-21 dedupe deleted live capabilities (nx_http_health, nx_teacher) by treating exactly this as copies | SIGNATURE-MISMATCH="); dp_wn(stats[6]) 1011 dp_w(" <- same exported name, DIFFERENT arity: the SILENT class, binds without a diagnostic (nx_input_abstract ia_init) | identical="); dp_wn(stats[1]) 1012 dp_w(" litter (latent) | out-of-probe-path="); dp_wn(stats[3]); dp_w(" (divergent but a build never reads that tree -- litter, F1127) | unknown="); dp_wn(stats[2]); dp_w(" (never assumed identical)\ 1013" as *u8) 1014 dp_w(" ENVELOPE: pairs_compared="); dp_wn(pairs) 1015 dp_w(" trees="); dp_wn(nd) 1016 dp_w(" tree_cap="); dp_wn(MAXD) 1017 dp_w(" depth_cap="); dp_wn(MAXDEPTH) 1018 dp_w(" cap_hit="); dp_wn(cap[0]) 1019 dp_w(" allocs=O(pairs+dirs) NOT O(files) [v2 regression guard: the per-file mmap in exists() leaked ~1GB/run and starved the host]") 1020 dp_w(" (counts are per-pair: one basename in three trees counts twice -- an upper bound on PAIRWISE hazards, not a distinct-basename count)\ 1021" as *u8) 1022 if cap[0] == 1 { 1023 dp_w("verdict=RED (TRUNCATED -- a bound was hit; this scan is INCOMPLETE, raise MAXD/MAXDEPTH before trusting it)\ 1024" as *u8) 1025 sys_exit(3); return 3 1026 } 1027 // ★F1127: the verdict keys on ACTIONABLE HAZARDS (divergent copies both sitting in trees a build 1028 // actually probes), not on the raw count of duplicate basenames. Identical litter and 1029 // out-of-probe-path divergence are still REPORTED above -- they are real housekeeping -- but they 1030 // are not clobber hazards, and a permanent RED that no action can clear teaches readers to ignore 1031 // the verdict. GREEN here means "nothing can silently regress a build", which is the claim the 1032 // organ actually supports; the litter counts remain visible for anyone who wants to tidy. 1033 if stats[0] == 0 { 1034 dp_w("verdict=GREEN (no clobber hazards: every remaining duplicate is identical litter or lives outside the build probe path, so no build can resolve a stale copy)\ 1035" as *u8) 1036 sys_exit(0); return 0 1037 } 1038 dp_w("verdict=RED (clobber hazard(s) present -- reconcile each basename to ONE canonical source dir; see debt seq207/seq281/seq284)\ 1039" as *u8) 1040 sys_exit(3); return 3 1041}