code wiki / _hdl_build / nx_treediff.nx
nx_treediff.nx source
↩ module page · 218 lines · 10051 B
1// nx_treediff.nx -- CROSS-TREE SOURCE DIVERGENCE CENSUS (laptop SSOT vs NAS buildroot).
2//
3// THE GAP IT CLOSES (filed seq1429, hit FOUR times in one session 2026-07-30):
4// seq768 the build-admission enforcement was in NEITHER tree
5// seq1393 five compiler defects existed ONLY in the buildroot copy
6// seq1419 the deployed compiler was fine; the buildroot copy was the divergent one
7// seq1429 nx_office_daemon/nx_survey_daemon were stale in buildroot while the SSOT was migrated
8// nx_dup_source_check finds duplicate basenames WITHIN buildroot. NOTHING compared the buildroot
9// against the laptop SSOT, so every one of those was found by hand, by accident, after the damage.
10//
11// WHY IT MATTERS BEYOND TIDINESS: /api/build compiles from BUILDROOT. A fix that lives only in the
12// SSOT never reaches a running binary; a fix that lives only in buildroot is destroyed by the next
13// sync from the SSOT. BOTH DIRECTIONS SILENTLY LOSE WORK, which is why divergence must be a REPORT,
14// not an archaeology exercise.
15//
16// DESIGN: this half runs ON THE NAS and emits a manifest of `<bytes> <relpath>` for one tree, in a
17// stable sorted-by-path order. The caller diffs two manifests (one per tree). Byte SIZE is a
18// deliberately cheap discriminator -- it caught all four of today's instances -- and it needs no hash
19// primitive on either side. A size match is NOT proof of identity; it is a screen that turns an
20// unbounded search into a short candidate list, and it says so in its own output rather than implying
21// certainty it does not have.
22//
23// FAIL-CLOSED + HONEST ENVELOPE: every cap is declared in-band (files scanned, cap hit, depth). A
24// scanner that silently truncates its own coverage is the self-ceiling defect (law L011) -- so if a
25// cap is reached the verdict SAYS SO and reports PARTIAL rather than presenting a floor as a total.
26// DIALECT: plain-if, no empty literals, <=6 params, consts above use.
27import "nx_syscalls.nx"
28
29const TD_MAX_FILES: i64 = 40000 // upper bound on emitted rows; declared, never silent
30const TD_MAX_DEPTH: i64 = 8 // recursion depth cap
31const TD_PATH_CAP: i64 = 1024
32const TD_NAME_CAP: i64 = 256
33const TD_DIRENT_BUF: i64 = 65536
34
35const TD_OUTBUF: i64 = 4194304 // 4 MiB manifest buffer (~19.8k rows x ~60B, with headroom)
36
37static td_count: *i64 // [0]=rows emitted [1]=dirs walked [2]=1 if a cap was hit [3]=1 if outbuf full
38// MANIFEST SINK (2026-07-30). The first cut wrote rows to STDOUT, which capped the census at whatever
39// the MCP tools/call response allows: the first real run returned only 4447 of ~19800 NAS rows
40// (168600 B), so the measured "655 differing files" was a FLOOR bounded by a TRANSPORT limit rather
41// than by the corpus. A census whose coverage is decided by its caller's payload cap is the same
42// self-ceiling defect it is supposed to detect. With an output path the rows go to a file (read back
43// with nx_fs) and stdout carries only the envelope, so the response size is constant regardless of
44// tree size -- the same shape as the codewiki _async=1 fix.
45static td_out: *u8
46static td_out_n: *i64
47
48func td_puts(s: *u8) -> i64 {
49 var n: i64 = 0
50 while s[n] != (0 as u8) { n = n + 1 }
51 sys_write(1, s, n)
52 return 0
53}
54func td_putn(v: i64) -> i64 {
55 let b: *u8 = sys_mmap(32)
56 var m: i64 = v
57 if m == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 }
58 if m < 0 { td_puts("-" as *u8); m = 0 - m }
59 let t: *u8 = sys_mmap(32)
60 var k: i64 = 0
61 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
62 var i: i64 = 0
63 while i < k { b[i] = t[k - 1 - i]; i = i + 1 }
64 sys_write(1, b, k)
65 return 0
66}
67func td_cat(d: *u8, o: i64, s: *u8) -> i64 {
68 var a: i64 = o
69 var i: i64 = 0
70 while s[i] != (0 as u8) { d[a] = s[i]; a = a + 1; i = i + 1 }
71 d[a] = 0 as u8
72 return a
73}
74func td_ends_nx(nm: *u8) -> i64 {
75 var n: i64 = 0
76 while nm[n] != (0 as u8) { n = n + 1 }
77 if n < 3 { return 0 }
78 if nm[n-3] != (46 as u8) { return 0 }
79 if nm[n-2] != (110 as u8) { return 0 }
80 if nm[n-1] != (120 as u8) { return 0 }
81 return 1
82}
83// file size via fstatat; -1 when unreadable (reported, never silently 0)
84func td_size(path: *u8) -> i64 {
85 let stb: *u8 = sys_mmap(160)
86 if sys_fstatat(path, stb) != 0 { return 0 - 1 }
87 let szp: *i64 = ((stb as i64) + 48) as *i64 // st_size @ +48 (x86_64 struct stat)
88 return szp[0]
89}
90
91// append "<bytes> <relpath>\n" -- to the manifest buffer when an output path was given, else stdout.
92// FAIL-CLOSED ON OVERFLOW: a full buffer sets td_count[3] so the envelope reports PARTIAL. It must
93// never wrap or silently drop rows; a census that under-reports without saying so is the defect.
94func td_row(sz: i64, rel: *u8) -> i64 {
95 if (td_out as i64) == 0 {
96 td_putn(sz); td_puts(" " as *u8); td_puts(rel); td_puts("\n" as *u8)
97 return 0
98 }
99 let nb: *u8 = sys_mmap(64)
100 var k: i64 = 0
101 var m: i64 = sz
102 if m < 0 { nb[0] = 45 as u8; k = 1; m = 0 - m }
103 if m == 0 { nb[k] = 48 as u8; k = k + 1 }
104 let t: *u8 = sys_mmap(32)
105 var j: i64 = 0
106 while m > 0 { t[j] = (48 + (m % 10)) as u8; m = m / 10; j = j + 1 }
107 while j > 0 { nb[k] = t[j - 1]; k = k + 1; j = j - 1 }
108 var rl: i64 = 0
109 while rel[rl] != (0 as u8) { rl = rl + 1 }
110 let need: i64 = k + 1 + rl + 1
111 var o: i64 = td_out_n[0]
112 if o + need >= TD_OUTBUF { td_count[3] = 1; return 0 }
113 var i: i64 = 0
114 while i < k { td_out[o] = nb[i]; o = o + 1; i = i + 1 }
115 td_out[o] = 32 as u8; o = o + 1
116 i = 0
117 while i < rl { td_out[o] = rel[i]; o = o + 1; i = i + 1 }
118 td_out[o] = 10 as u8; o = o + 1
119 td_out_n[0] = o
120 return 0
121}
122
123func td_walk(dir: *u8, rel: *u8, depth: i64) -> i64 {
124 if depth > TD_MAX_DEPTH { td_count[2] = 1; return 0 }
125 let fd: i64 = sys_openat_rd(dir)
126 if fd < 0 { return 0 }
127 td_count[1] = td_count[1] + 1
128 let buf: *u8 = sys_mmap(TD_DIRENT_BUF)
129 var go: i64 = 1
130 while go == 1 {
131 let n: i64 = sys_getdents64(fd, buf, TD_DIRENT_BUF)
132 if n <= 0 { go = 0 }
133 if n > 0 {
134 var off: i64 = 0
135 while off < n {
136 // the dirent_* helpers take the RECORD POINTER, not (buf, offset) -- caught by the
137 // compiler's arity check, which is exactly the seal that makes this class fail closed.
138 let rec: *u8 = ((buf as i64) + off) as *u8
139 let rl: i64 = dirent_reclen(rec)
140 let ty: i64 = dirent_type(rec)
141 let nm: *u8 = dirent_name(rec)
142 var skip: i64 = 0
143 if nm[0] == (46 as u8) { skip = 1 } // . .. and dotfiles
144 if skip == 0 {
145 let sub: *u8 = sys_mmap(TD_PATH_CAP)
146 var o: i64 = td_cat(sub, 0, dir)
147 o = td_cat(sub, o, "/" as *u8)
148 o = td_cat(sub, o, nm)
149 let srel: *u8 = sys_mmap(TD_PATH_CAP)
150 var r: i64 = td_cat(srel, 0, rel)
151 if rel[0] != (0 as u8) { r = td_cat(srel, r, "/" as *u8) }
152 r = td_cat(srel, r, nm)
153 if ty == 4 { td_walk(sub, srel, depth + 1) }
154 if ty != 4 {
155 if td_ends_nx(nm) == 1 {
156 if td_count[0] >= TD_MAX_FILES { td_count[2] = 1 } else {
157 td_row(td_size(sub), srel)
158 td_count[0] = td_count[0] + 1
159 }
160 }
161 }
162 }
163 off = off + rl
164 }
165 }
166 }
167 sys_close(fd)
168 return 0
169}
170
171func main(argc: i64, argv: *i64) -> i64 {
172 if argc < 2 {
173 td_puts("usage: nx_treediff <dir> [outfile] -- '<bytes> <relpath>' per .nx file, for cross-tree\n" as *u8)
174 td_puts(" comparison. With [outfile] the manifest is WRITTEN THERE (read it back with nx_fs)\n" as *u8)
175 td_puts(" and stdout carries only the envelope, so a transport payload cap can never bound\n" as *u8)
176 td_puts(" coverage. Without it, rows go to stdout (fine for small trees).\n" as *u8)
177 sys_exit(2)
178 return 2
179 }
180 td_count = sys_mmap(32) as *i64
181 td_count[0] = 0
182 td_count[1] = 0
183 td_count[2] = 0
184 td_count[3] = 0
185 td_out_n = sys_mmap(16) as *i64
186 td_out_n[0] = 0
187 var outp: *u8 = 0 as *u8
188 if argc > 2 { outp = argv[2] as *u8; td_out = sys_mmap(TD_OUTBUF) }
189 let root: *u8 = argv[1] as *u8
190 td_walk(root, "" as *u8, 0)
191 if (td_out as i64) != 0 {
192 let fd: i64 = sys_openat_wr(outp, 0x1a4)
193 if fd < 0 {
194 td_puts("# TREEDIFF RED -- cannot open outfile: " as *u8); td_puts(outp); td_puts("\n" as *u8)
195 sys_exit(3); return 3
196 }
197 sys_write(fd, td_out, td_out_n[0])
198 sys_close(fd)
199 td_puts("# manifest written: " as *u8); td_puts(outp)
200 td_puts(" bytes=" as *u8); td_putn(td_out_n[0]); td_puts("\n" as *u8)
201 }
202 // ENVELOPE IN-BAND: a scanner that hides its coverage presents a floor as a total (law L011).
203 td_puts("# TREEDIFF-MANIFEST files=" as *u8); td_putn(td_count[0])
204 td_puts(" dirs=" as *u8); td_putn(td_count[1])
205 td_puts(" file_cap=" as *u8); td_putn(TD_MAX_FILES)
206 td_puts(" depth_cap=" as *u8); td_putn(TD_MAX_DEPTH)
207 td_puts(" cap_hit=" as *u8); td_putn(td_count[2])
208 td_puts(" outbuf_full=" as *u8); td_putn(td_count[3])
209 var partial: i64 = 0
210 if td_count[2] == 1 { partial = 1 }
211 if td_count[3] == 1 { partial = 1 }
212 if partial == 1 { td_puts(" verdict=PARTIAL (coverage truncated -- raise a cap deliberately, never read this as a total)" as *u8) }
213 if partial == 0 { td_puts(" verdict=COMPLETE" as *u8) }
214 td_puts("\n# NOTE: size is a SCREEN, not proof of identity -- equal sizes can still differ. It is cheap,\n" as *u8)
215 td_puts("# needs no hash primitive on either side, and caught all four 2026-07-30 divergences.\n" as *u8)
216 sys_exit(0)
217 return 0
218}