code wiki / _hdl_build / nx_treediverge.nx
nx_treediverge.nx source
↩ module page · 252 lines · 11410 B
1// nx_treediverge.nx -- THE MISSING CROSS-TREE DIVERGENCE DETECTOR (debt 1785438829, sev-8).
2//
3// THE GAP IT CLOSES, in that debt's own words: "nx_dup_source_check finds dups WITHIN buildroot but
4// there is NO laptop-vs-NAS divergence detector -- I did it BY HAND four times today. BUILD THAT: a
5// cross-tree census emitting per-file laptop_bytes / nas_bytes / delta / direction, so divergence is a
6// REPORT rather than an archaeology exercise; size alone would have caught all four of today's instances."
7//
8// WHY IT MATTERS (not tidiness): /api/build compiles from buildroot, so a fix living only in the laptop
9// SSOT NEVER REACHES A RUNNING BINARY, and a fix living only in buildroot is DESTROYED by the next sync
10// from the SSOT. The divergence is BIDIRECTIONAL -- neither tree is a superset -- so a one-way sync in
11// EITHER direction silently loses work. You cannot merge safely without first knowing the direction
12// per file. That is exactly what this emits.
13//
14// COMPOSES, DOES NOT DUPLICATE: nx_treediff already emits one tree's manifest as "<bytes> <relpath>"
15// per .nx file and says in its own usage that it is "for cross-tree comparison" -- it just never
16// performed the comparison. This is the missing half. Run nx_treediff on each tree, then:
17// nx_treediverge <manifestA> <manifestB> [labelA] [labelB]
18//
19// VERDICTS, per file: A-ONLY / B-ONLY / A-AHEAD / B-AHEAD / SAME. "AHEAD" is BY BYTES, which is the
20// signal that caught all four historical instances; it is a FLOOR, not proof of content (equal size can
21// still differ -- see the same-size-rewrite caveat on expect=<size> CAS). Declared, never implied.
22//
23// NON-VACUITY: an empty or unreadable manifest REFUSES (exit 3) instead of reporting "0 divergent",
24// because a comparator that reads nothing and prints GREEN is the self-ceiling defect this ecosystem
25// has banned repeatedly. A zero-divergence verdict is only emitted when BOTH sides actually had rows.
26import "nx_syscalls.nx"
27const TV_MAGIC_5381: i64 = 5381
28
29const TV_HBUCKETS: i64 = 65536 // hash buckets; power of two so the mask is an AND
30const TV_MAXENT: i64 = 200000 // per-tree entry ceiling (corpus is ~17.7k .nx today, 11x headroom)
31const TV_BUFCAP: i64 = 16777216 // 16 MiB per manifest (17.7k rows x ~80B ~= 1.4 MiB, 11x headroom)
32
33static tv_keyp: *i64 // B-side: pointer to each relpath (NUL-terminated in place)
34static tv_size: *i64 // B-side: bytes for that relpath
35static tv_next: *i64 // B-side: hash chain
36static tv_head: *i64 // B-side: bucket heads (-1 = empty)
37static tv_seen: *i64 // B-side: 1 once matched by an A row (to find B-ONLY)
38static tv_nent: *i64
39
40func tv_puts(s: *u8) -> i64 {
41 var n: i64 = 0
42 while s[n] != (0 as u8) { n = n + 1 }
43 sys_write(1, s, n)
44 return 0
45}
46func tv_putn(v: i64) -> i64 {
47 let b: *u8 = sys_mmap(32)
48 var m: i64 = v
49 if m == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 }
50 if m < 0 { tv_puts("-" as *u8); m = 0 - m }
51 let t: *u8 = sys_mmap(32)
52 var k: i64 = 0
53 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
54 var i: i64 = 0
55 while i < k { b[i] = t[k - 1 - i]; i = i + 1 }
56 sys_write(1, b, k)
57 return 0
58}
59
60// whole-file read; outn[0] = bytes, or -1 when the path is unreadable (REPORTED, never silently 0)
61func tv_readall(path: *u8, outn: *i64) -> *u8 {
62 let fd: i64 = sys_openat_rd(path)
63 if fd < 0 { outn[0] = 0 - 1; return 0 as *u8 }
64 let buf: *u8 = sys_mmap(TV_BUFCAP)
65 var tot: i64 = 0
66 var go: i64 = 1
67 while go == 1 {
68 let r: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, TV_BUFCAP - tot - 1)
69 if r <= 0 { go = 0 } else { tot = tot + r }
70 if tot >= TV_BUFCAP - 1 { go = 0 }
71 }
72 sys_close(fd)
73 buf[tot] = 0 as u8
74 outn[0] = tot
75 return buf
76}
77
78func tv_hash(s: *u8) -> i64 {
79 var h: i64 = TV_MAGIC_5381
80 var i: i64 = 0
81 while s[i] != (0 as u8) { h = ((h * 33) + (s[i] as i64)) & 0x7fffffff; i = i + 1 }
82 return h & (TV_HBUCKETS - 1)
83}
84func tv_streq(a: *u8, b: *u8) -> i64 {
85 var i: i64 = 0
86 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
87 if b[i] != (0 as u8) { return 0 }
88 return 1
89}
90
91// Parse one manifest line "<bytes> <relpath>" IN PLACE: NUL-terminate the relpath, return its pointer;
92// size lands in szout[0]. Returns 0 for a blank/garbled line so the caller can skip without aborting.
93// NOTE: written WITHOUT `break` on purpose -- this dialect's loop-exit idiom is a go-flag, and a
94// hand-rolled pseudo-break is exactly what span forever in the seq95ad autofix intake gate.
95func tv_parse(buf: *u8, start: i64, end: i64, szout: *i64) -> *u8 {
96 var i: i64 = start
97 var v: i64 = 0
98 var any: i64 = 0
99 var go: i64 = 1
100 while go == 1 {
101 if i >= end { go = 0 } else {
102 let c: i64 = buf[i] as i64
103 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; i = i + 1 } else { go = 0 } } else { go = 0 }
104 }
105 }
106 if any == 0 { return 0 as *u8 }
107 if i >= end { return 0 as *u8 }
108 if buf[i] != (32 as u8) { return 0 as *u8 }
109 let p: i64 = i + 1
110 if p >= end { return 0 as *u8 }
111 buf[end] = 0 as u8
112 szout[0] = v
113 return ((buf as i64) + p) as *u8
114}
115
116// index the B manifest into the hash table; returns row count
117func tv_index(buf: *u8, n: i64) -> i64 {
118 var i: i64 = 0
119 var ls: i64 = 0
120 var cnt: i64 = 0
121 let szp: *i64 = sys_mmap(16) as *i64
122 while i < n {
123 if buf[i] == (10 as u8) {
124 if cnt < TV_MAXENT {
125 let rel: *u8 = tv_parse(buf, ls, i, szp)
126 if (rel as i64) != 0 {
127 let h: i64 = tv_hash(rel)
128 tv_keyp[cnt] = rel as i64
129 tv_size[cnt] = szp[0]
130 tv_next[cnt] = tv_head[h]
131 tv_seen[cnt] = 0
132 tv_head[h] = cnt
133 cnt = cnt + 1
134 }
135 }
136 ls = i + 1
137 }
138 i = i + 1
139 }
140 return cnt
141}
142
143// find a relpath in the B index; returns entry index or -1
144func tv_find(rel: *u8) -> i64 {
145 let h: i64 = tv_hash(rel)
146 var e: i64 = tv_head[h]
147 while e >= 0 {
148 if tv_streq(rel, tv_keyp[e] as *u8) == 1 { return e }
149 e = tv_next[e]
150 }
151 return 0 - 1
152}
153
154func tv_emit(tag: *u8, rel: *u8, a: i64, b: i64) -> i64 {
155 tv_puts(tag); tv_puts(" " as *u8); tv_puts(rel)
156 tv_puts(" a=" as *u8); tv_putn(a)
157 tv_puts(" b=" as *u8); tv_putn(b)
158 tv_puts(" delta=" as *u8); tv_putn(a - b)
159 tv_puts("\n" as *u8)
160 return 0
161}
162
163func main(argc: i64, argv: *i64) -> i64 {
164 if argc < 3 {
165 tv_puts("usage: nx_treediverge <manifestA> <manifestB> [labelA] [labelB]\n" as *u8)
166 tv_puts(" manifests come from: nx_treediff <dir> <outfile> (one per tree)\n" as *u8)
167 tv_puts(" verdicts per file: A-ONLY / B-ONLY / A-AHEAD / B-AHEAD (by BYTES, a FLOOR)\n" as *u8)
168 sys_exit(2)
169 return 2
170 }
171 let pa: *u8 = argv[1] as *u8
172 let pb: *u8 = argv[2] as *u8
173 var la: *u8 = "A" as *u8
174 var lb: *u8 = "B" as *u8
175 if argc >= 4 { la = argv[3] as *u8 }
176 if argc >= 5 { lb = argv[4] as *u8 }
177
178 let na: *i64 = sys_mmap(16) as *i64
179 let nb: *i64 = sys_mmap(16) as *i64
180 let ba: *u8 = tv_readall(pa, na)
181 let bb: *u8 = tv_readall(pb, nb)
182 // NON-VACUITY: refuse rather than report a comforting zero off an empty read.
183 if (ba as i64) == 0 { tv_puts("TREEDIVERGE REFUSED: cannot read manifest A\nverdict=REFUSED\n" as *u8); sys_exit(3); return 3 }
184 if (bb as i64) == 0 { tv_puts("TREEDIVERGE REFUSED: cannot read manifest B\nverdict=REFUSED\n" as *u8); sys_exit(3); return 3 }
185 if na[0] <= 0 { tv_puts("TREEDIVERGE REFUSED: manifest A is EMPTY -- a comparator that read nothing must not print zero divergence\nverdict=REFUSED\n" as *u8); sys_exit(3); return 3 }
186 if nb[0] <= 0 { tv_puts("TREEDIVERGE REFUSED: manifest B is EMPTY -- a comparator that read nothing must not print zero divergence\nverdict=REFUSED\n" as *u8); sys_exit(3); return 3 }
187
188 tv_head = sys_mmap(8 * TV_HBUCKETS) as *i64
189 tv_keyp = sys_mmap(8 * TV_MAXENT) as *i64
190 tv_size = sys_mmap(8 * TV_MAXENT) as *i64
191 tv_next = sys_mmap(8 * TV_MAXENT) as *i64
192 tv_seen = sys_mmap(8 * TV_MAXENT) as *i64
193 var h: i64 = 0
194 while h < TV_HBUCKETS { tv_head[h] = 0 - 1; h = h + 1 }
195
196 let bn: i64 = tv_index(bb, nb[0])
197
198 tv_puts("=== nx_treediverge " as *u8); tv_puts(la); tv_puts(" vs " as *u8); tv_puts(lb); tv_puts(" ===\n" as *u8)
199
200 var same: i64 = 0
201 var aahead: i64 = 0
202 var bahead: i64 = 0
203 var aonly: i64 = 0
204 var an: i64 = 0
205 let szp: *i64 = sys_mmap(16) as *i64
206 var i: i64 = 0
207 var ls: i64 = 0
208 let nn: i64 = na[0]
209 while i < nn {
210 if ba[i] == (10 as u8) {
211 let rel: *u8 = tv_parse(ba, ls, i, szp)
212 if (rel as i64) != 0 {
213 an = an + 1
214 let e: i64 = tv_find(rel)
215 if e < 0 { aonly = aonly + 1; tv_emit("A-ONLY " as *u8, rel, szp[0], 0 - 1) }
216 else {
217 tv_seen[e] = 1
218 let bs: i64 = tv_size[e]
219 if szp[0] == bs { same = same + 1 }
220 else { if szp[0] > bs { aahead = aahead + 1; tv_emit("A-AHEAD" as *u8, rel, szp[0], bs) } else { bahead = bahead + 1; tv_emit("B-AHEAD" as *u8, rel, szp[0], bs) } }
221 }
222 }
223 ls = i + 1
224 }
225 i = i + 1
226 }
227 var bonly: i64 = 0
228 var e2: i64 = 0
229 while e2 < bn { if tv_seen[e2] == 0 { bonly = bonly + 1; tv_emit("B-ONLY " as *u8, tv_keyp[e2] as *u8, 0 - 1, tv_size[e2]) } e2 = e2 + 1 }
230
231 tv_puts("\n--- ENVELOPE ---\n" as *u8)
232 tv_puts("rows_" as *u8); tv_puts(la); tv_puts("=" as *u8); tv_putn(an); tv_puts(" rows_" as *u8); tv_puts(lb); tv_puts("=" as *u8); tv_putn(bn); tv_puts("\n" as *u8)
233 tv_puts("same=" as *u8); tv_putn(same)
234 tv_puts(" " as *u8); tv_puts(la); tv_puts("_ahead=" as *u8); tv_putn(aahead)
235 tv_puts(" " as *u8); tv_puts(lb); tv_puts("_ahead=" as *u8); tv_putn(bahead)
236 tv_puts(" " as *u8); tv_puts(la); tv_puts("_only=" as *u8); tv_putn(aonly)
237 tv_puts(" " as *u8); tv_puts(lb); tv_puts("_only=" as *u8); tv_putn(bonly)
238 tv_puts("\n" as *u8)
239 let div: i64 = aahead + bahead + aonly + bonly
240 tv_puts("divergent=" as *u8); tv_putn(div); tv_puts("\n" as *u8)
241 // ★THE VERDICT THAT MATTERS: BIDIRECTIONAL means a one-way sync in EITHER direction destroys work.
242 if aahead > 0 { if bahead > 0 {
243 tv_puts("verdict=BIDIRECTIONAL -- NEITHER TREE IS A SUPERSET. A one-way sync in EITHER direction DESTROYS WORK; merge per-file, never push wholesale.\n" as *u8)
244 sys_exit(1); return 1
245 } }
246 if div == 0 { tv_puts("verdict=CONVERGED -- byte-identical on every shared path (a FLOOR: equal size is not proof of equal content)\n" as *u8); sys_exit(0); return 0 }
247 if bahead == 0 { if bonly == 0 { tv_puts("verdict=A-SUPERSET -- safe to sync A->B\n" as *u8); sys_exit(0); return 0 } }
248 if aahead == 0 { if aonly == 0 { tv_puts("verdict=B-SUPERSET -- safe to sync B->A\n" as *u8); sys_exit(0); return 0 } }
249 tv_puts("verdict=DIVERGENT-ONE-WAY -- one side leads on content but the other has unique paths; reconcile the *_only lists before syncing.\n" as *u8)
250 sys_exit(1)
251 return 1
252}