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}