code wiki / _hdl_build / nx_treecanon_gate.nx
nx_treecanon_gate.nx source
↩ module page · 612 lines · 31742 B
1// nx_treecanon_gate.nx -- THE DIVERGENCE GATE THAT REFUSES (debt 1785622769, operator directive
2// 2026-08-01: "ONE canonical tree per generation with a lineage manifest ... a divergence gate that
3// REFUSES a build when the deploying tree's copy diverges from canonical").
4//
5// MEASURED 2026-08-03, and it is why this exists: NAS buildroot vs laptop nxc2 diverge on
6// 8,258 files -- 3,705 NAS-newer, 1,838 laptop-newer, 739 NAS-only, 1,976 laptop-only. NEITHER
7// TREE IS A SUPERSET IN EITHER DIRECTION. A name-level diff saw only 969 of those, because the
8// dangerous class is files present in BOTH with DIFFERENT BYTES (5,543 of them).
9// ⇒ ★A NAME-LEVEL TREE DIFF IS NOT A DIVERGENCE CHECK. It cannot see the failure that actually
10// bites -- editing a file in one tree and compiling the other copy, which is precisely how the
11// unfreed-mmap fix had to be applied twice (the incident that opened this debt).
12//
13// SCOPE, DELIBERATE AND STATED: this gate does NOT try to reconcile the fork. Blocking on all
14// 8,258 would block every build, and mass-merging on a size screen would be DESTRUCTIVE -- byte
15// size is a SCREEN, not proof of newer (banked law: newer mtime + FEWER bytes = REVERT). It
16// enforces a CANON LIST from knowledge/tree_canon.conf (rule 11: the policy is DATA), starting at
17// the crown jewels and growing by measurement. Stopping the bleeding beats an unfinished cleanup.
18//
19// T0 LOADED both manifests parse and index (non-vacuity floor on the inputs)
20// T1 CANON READ the canon list loads and is non-empty (an empty list would pass vacuously)
21// T2 CANON CLEAN every canon path is present in BOTH trees and byte-identical -- REFUSES otherwise
22// T3 BITE the detector fires on a REAL divergent path and stays silent on a REAL identical
23// one, both discovered from the manifests at runtime (no hardcoded fixture)
24// T4 SETTLED OPTIONAL, needs argv[4]: resamples every listed canon file on the LIVE tree
25// TC_SETTLE_MS later and REFUSES if any moved -- T1..T3 read manifests, which are
26// claims about a moment already past and so cannot see a snapshot taken mid-write
27//
28// T5 CONFIRMED OPTIONAL, needs argv[4]: for every canon row T2 called divergent, hashes the LIVE
29// file and asks whether the manifests or the bytes are right -- separating a REAL fork
30// from a STALE MANIFEST. The two manifests are refreshed on DIFFERENT CLOCKS (NAS side
31// hourly by nx_treediverge_beat, laptop side per SessionStart), so manifest lag alone
32// can make identical trees look forked and block every seat's build.
33//
34// usage: nx_treecanon_gate <manifestCANON> <manifestOTHER> <canon.conf> [liveTreeRoot]
35// liveTreeRoot must be the tree manifestCANON describes (e.g. buildroot/runtime on the NAS).
36// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
37import "nx_syscalls.nx"
38import "nx_gate_verdict.nx"
39import "nx_sha256.nx" // T5 needs the SAME digest nx_treehash wrote into the manifests
40
41const TC_HBUCKETS: i64 = 65536
42const TC_MAXENT: i64 = 200000
43const TC_BUFCAP: i64 = 16777216
44const TC_CONFCAP: i64 = 262144
45const TC_HASH_SEED: i64 = 5381
46const TC_SPACE: i64 = 32
47const TC_NL: i64 = 10
48const TC_CR: i64 = 13
49const TC_HASHCH: i64 = 35
50const TC_BANG: i64 = 33 // '!' row prefix: the BUILD LANE freezes every build while that row
51 // diverges (nx_sov_build_run tree-canon admission); for THIS gate the
52 // identity check is the same either way, so the marker is stripped
53const TC_PATHCAP: i64 = 1024
54const TC_FILECAP: i64 = 4194304 // per-file read cap for T4; largest canon source is ~290KB
55const TC_MAXCANON: i64 = 256
56const TC_DIGEST_BYTES: i64 = 32
57const TC_EMPTY_RESERVE: i64 = 4294967296 // sys_read_file's reserve for a 0-byte file (see nx_treehash)
58const TC_ID_FULLBYTES: i64 = 7 // 15 hex chars of identity = 7 whole bytes + 1 high nibble
59const TC_SETTLE_MS: i64 = 4000 // sample window for T4; long enough to catch an active writer
60const TC_SLASH: i64 = 47
61const TC_D0: i64 = 48
62const TC_D9: i64 = 57
63// CONTENT-IDENTITY UPGRADE (2026-08-06). A row may now be "<64hex> <bytes> <relpath>" (nx_treehash)
64// as well as the legacy "<bytes> <relpath>" (nx_treediff). When the digest is present the identity
65// token is its leading 60 bits, so tc_diverges compares CONTENT rather than LENGTH.
66// WHY THIS WAS URGENT: this gate's own comment claimed "byte-identical", but tc_lookup returned the
67// SIZE and tc_diverges compared sizes -- so a change that preserves length passed it GREEN. A
68// MUTATION-CLASS DEFECT PRESERVES LENGTH BY CONSTRUCTION, and one had already escaped into buildroot
69// (_hdl_build/nx_media_extract.nx, `if hit != 0` vs `if hit == 0`, 4389 bytes on BOTH sides). The
70// crown jewels this gate guards -- nx_syscalls.nx, nx_gate_verdict.nx -- are imported by every organ
71// in the estate, so a size-only canon check was the widest blind spot in the build lane.
72// 60 bits (not 64) keeps the token NON-NEGATIVE, which is load-bearing: tc_lookup reserves -1 for
73// ABSENT, and a token with the sign bit set would be silently read as "file missing".
74const TC_HEXID_CHARS: i64 = 15
75const TC_SHA_CHARS: i64 = 64
76const TC_LC_A: i64 = 97
77const TC_LC_F: i64 = 102
78
79// two independent indexes: A (canonical/NAS) and B (the tree being compared)
80static tc_ha: *i64 = 0 as *i64
81static tc_hb: *i64 = 0 as *i64
82static tc_ka: *i64 = 0 as *i64
83static tc_kb: *i64 = 0 as *i64
84static tc_sa: *i64 = 0 as *i64
85static tc_sb: *i64 = 0 as *i64
86static tc_na: i64 = 0
87static tc_nb: i64 = 0
88
89func tc_readall(path: *u8, outn: *i64, cap: i64) -> *u8 {
90 let fd: i64 = sys_openat_rd(path)
91 if fd < 0 { outn[0] = 0 - 1; return 0 as *u8 }
92 let buf: *u8 = sys_mmap(cap)
93 var tot: i64 = 0
94 var go: i64 = 1
95 while go == 1 {
96 let r: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot - 1)
97 if r <= 0 { go = 0 } else { tot = tot + r }
98 if tot >= cap - 1 { go = 0 }
99 }
100 sys_close(fd)
101 buf[tot] = 0 as u8
102 outn[0] = tot
103 return buf
104}
105
106// Content hash of the LIVE file at <root>/<rel>; -1 when unreadable. Not cryptographic on purpose --
107// the only question is "did these bytes change in the last few seconds", and a rolling hash answers it.
108// Returns a STATUS (0 = read and hashed, -1 = could not be read) and writes the hash to outh[0].
109// >>THE STATUS AND THE VALUE TRAVEL ON SEPARATE CHANNELS, DELIBERATELY.<< The first cut returned the
110// hash itself and used -1 for "unreadable". But this is a 64-bit rolling multiply: it overflows and
111// lands on a NEGATIVE value for about half of all inputs, which is indistinguishable from the error
112// sentinel. Measured: the same file read as "unreadable" alone and as fine inside a 6-file run, and
113// 5 then 6 of 12 canon files were reported unreadable when all 12 were provably present and readable.
114// The comparison below then SKIPPED every negative-hashing file, so a file that moved could go
115// undetected while the gate printed a confident number.
116// ★★★★★★AN ERROR SENTINEL DRAWN FROM THE VALUE'S OWN RANGE IS NOT AN ERROR CHANNEL -- IT IS A
117// COLLISION WAITING FOR THE RIGHT INPUT, AND IT FAILS SILENTLY BECAUSE BOTH SIDES LOOK LEGITIMATE.
118func tc_filehash(root: *u8, rel: *u8, nbox: *i64, outh: *i64) -> i64 {
119 let path: *u8 = sys_mmap(TC_PATHCAP)
120 var o: i64 = 0
121 var t: i64 = 0
122 while root[t] != (0 as u8) { path[o] = root[t]; o = o + 1; t = t + 1 }
123 path[o] = TC_SLASH as u8
124 o = o + 1
125 t = 0
126 while rel[t] != (0 as u8) { path[o] = rel[t]; o = o + 1; t = t + 1 }
127 path[o] = 0 as u8
128 let b: *u8 = tc_readall(path, nbox, TC_FILECAP)
129 let n: i64 = nbox[0]
130 if n <= 0 { sys_munmap(path, TC_PATHCAP); return 0 - 1 }
131 // (status -1 below is now ONLY ever a real read failure; the hash cannot reach this channel)
132 // A read that exactly fills the buffer is indistinguishable from a TRUNCATED one, and a hash over
133 // truncated bytes is a confident wrong answer -- it would read as MOVING forever. Refuse instead.
134 if n >= TC_FILECAP - 1 { sys_munmap(b, TC_FILECAP); sys_munmap(path, TC_PATHCAP); return 0 - 1 }
135 var h: i64 = TC_HASH_SEED
136 var i: i64 = 0
137 while i < n { h = h * 131 + (b[i] as i64); i = i + 1 }
138 // >>FREE WHAT YOU ALLOCATED.<< tc_readall mmaps `cap` on EVERY call and never frees, and T4 calls
139 // it 12 canon files x 2 passes = 24 times. The first cut of this tooth passed TC_BUFCAP (16MB) and
140 // leaked ~384MB; sys_mmap then began failing and FIVE OF TWELVE canon files silently reported
141 // "unreadable" -- WHILE T4 STILL SAID PASS. That is the same unfreed-mmap class this gate's own
142 // header names as the incident that opened its debt.
143 sys_munmap(b, TC_FILECAP)
144 sys_munmap(path, TC_PATHCAP)
145 outh[0] = h
146 return 0
147}
148
149// Walk the canon conf and hash every listed LIVE file into out[]. Returns how many were sampled.
150// ⚠The caller must pass a FRESHLY READ conf buffer each time: the row parser below NUL-terminates
151// each line in place (exactly as the T1/T2 walk does), so a second pass over the same buffer would
152// find no line ends at all.
153func tc_sample_all(cb: *u8, lc: i64, root: *u8, out: *i64, st: *i64, cap: i64, nb2: *i64) -> i64 {
154 var cnt: i64 = 0
155 var p: i64 = 0
156 var ls: i64 = 0
157 while p <= lc {
158 var eol: i64 = 0
159 if p == lc { eol = 1 }
160 if p < lc { if cb[p] == (TC_NL as u8) { eol = 1 } }
161 if eol == 1 {
162 var e: i64 = p
163 if e > ls { if cb[e-1] == (TC_CR as u8) { e = e - 1 } }
164 if e > ls {
165 if cb[ls] != (TC_HASHCH as u8) {
166 cb[e] = 0 as u8
167 var rs: i64 = ls
168 if cb[rs] == (TC_BANG as u8) { rs = rs + 1 }
169 if rs < e {
170 let rel: *u8 = ((cb as i64) + rs) as *u8
171 if cnt < cap { st[cnt] = tc_filehash(root, rel, nb2, ((out as i64) + cnt * 8) as *i64); cnt = cnt + 1 }
172 }
173 }
174 }
175 ls = p + 1
176 }
177 p = p + 1
178 }
179 return cnt
180}
181
182// ---- T5 SUPPORT: the identity token of the LIVE file, comparable to a manifest row ----
183// WHY THIS EXISTS. T2 compares two MANIFESTS, and the estate refreshes them on DIFFERENT CLOCKS:
184// treecanon_nas_hash.mf is written by the hourly nx_treediverge_beat, treecanon_laptop_hash.mf by a
185// laptop SessionStart hook. So a file edited between those two moments reports DIVERGENT while both
186// trees hold byte-identical content -- MEASURED 2026-08-07 on this very file: the manifests disagreed
187// (22082 vs 23619) while both trees held 23619, and regenerating the NAS manifest cleared it.
188// A gate that REFUSES on that is a build outage manufactured by a clock, not by a fork.
189// ⇒ T5 asks the only question that settles it: WHAT DO THE LIVE BYTES SAY?
190// ★★★★★★A DISAGREEMENT BETWEEN TWO RECORDS OF THE PAST IS NOT EVIDENCE ABOUT THE PRESENT -- GO AND
191// LOOK AT THE THING ITSELF.
192// Returns 0 and writes the token to outid[0]; returns -1 when the file cannot be read.
193// The token is the leading 60 bits of the sha256, derived from the DIGEST BYTES directly -- exactly
194// the value tc_parse builds from the manifest's 15 leading hex chars, so the two are comparable
195// without a hex round-trip. Matches nx_treehash's known-good read/free idiom byte for byte.
196func tc_live_id(root: *u8, rel: *u8, outid: *i64) -> i64 {
197 let path: *u8 = sys_mmap(TC_PATHCAP)
198 var o: i64 = 0
199 var t: i64 = 0
200 while root[t] != (0 as u8) { path[o] = root[t]; o = o + 1; t = t + 1 }
201 path[o] = TC_SLASH as u8
202 o = o + 1
203 t = 0
204 while rel[t] != (0 as u8) { path[o] = rel[t]; o = o + 1; t = t + 1 }
205 path[o] = 0 as u8
206 let dg: *u8 = sys_mmap(TC_DIGEST_BYTES)
207 let ln: *i64 = sys_mmap(16) as *i64
208 ln[0] = 0
209 let buf: *u8 = sys_read_file(path, ln)
210 if (buf as i64) == 0 {
211 sys_munmap(ln as *u8, 16)
212 sys_munmap(dg, TC_DIGEST_BYTES)
213 sys_munmap(path, TC_PATHCAP)
214 return 0 - 1
215 }
216 let n: i64 = ln[0]
217 sha256_digest(buf, n, dg)
218 // Free EXACTLY what sys_read_file reserved -- the 0-byte case reserves TC_EMPTY_RESERVE, and
219 // freeing the wrong length is how the first cut of T4 leaked ~384MB and started reporting real
220 // files as unreadable while still printing PASS.
221 if n > 0 { sys_munmap(buf, n + 16) } else { sys_munmap(buf, TC_EMPTY_RESERVE + 16) }
222 var v: i64 = 0
223 var k: i64 = 0
224 while k < TC_ID_FULLBYTES { v = (v * 256) + (dg[k] as i64); k = k + 1 }
225 v = (v * 16) + ((dg[TC_ID_FULLBYTES] as i64) >> 4)
226 outid[0] = v
227 sys_munmap(ln as *u8, 16)
228 sys_munmap(dg, TC_DIGEST_BYTES)
229 sys_munmap(path, TC_PATHCAP)
230 return 0
231}
232
233func tc_hash(s: *u8) -> i64 {
234 var h: i64 = TC_HASH_SEED
235 var i: i64 = 0
236 while s[i] != (0 as u8) { h = ((h * 33) + (s[i] as i64)) & 0x7fffffff; i = i + 1 }
237 return h & (TC_HBUCKETS - 1)
238}
239func tc_streq(a: *u8, b: *u8) -> i64 {
240 var i: i64 = 0
241 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
242 if b[i] != (0 as u8) { return 0 }
243 return 1
244}
245
246func tc_hexval(c: i64) -> i64 {
247 if c >= TC_D0 { if c <= TC_D9 { return c - TC_D0 } }
248 if c >= TC_LC_A { if c <= TC_LC_F { return c - (TC_LC_A - 10) } }
249 return 0 - 1
250}
251// 1 iff the row at `start` opens with 64 lowercase-hex chars followed by a space.
252// nx_treehash writes 64 '-' for an UNREADABLE file, which is NOT hex, so an unreadable row falls
253// through to the legacy branch and fails to parse rather than matching anything.
254func tc_is_hashrow(buf: *u8, start: i64, end: i64) -> i64 {
255 if start + TC_SHA_CHARS + 1 > end { return 0 }
256 var i: i64 = 0
257 while i < TC_SHA_CHARS {
258 if tc_hexval(buf[start+i] as i64) < 0 { return 0 }
259 i = i + 1
260 }
261 if buf[start + TC_SHA_CHARS] != (TC_SPACE as u8) { return 0 }
262 return 1
263}
264// parse a manifest row in place; NUL-terminates the path at `end` and writes the IDENTITY TOKEN:
265// "<64hex> <bytes> <relpath>" -> leading 60 bits of the sha256 (CONTENT identity)
266// "<bytes> <relpath>" -> the byte size (the legacy SCREEN, back-compat)
267// STRICTLY STRONGER, NEVER WEAKER: pointed at size manifests this behaves exactly as before.
268func tc_parse(buf: *u8, start: i64, end: i64, szout: *i64) -> *u8 {
269 if tc_is_hashrow(buf, start, end) == 1 {
270 var t: i64 = 0
271 var k: i64 = 0
272 while k < TC_HEXID_CHARS { t = (t * 16) + tc_hexval(buf[start+k] as i64); k = k + 1 }
273 var j: i64 = start + TC_SHA_CHARS + 1
274 var g2: i64 = 1
275 while g2 == 1 {
276 if j >= end { g2 = 0 } else {
277 if buf[j] == (TC_SPACE as u8) { g2 = 0 } else { j = j + 1 }
278 }
279 }
280 if j >= end { return 0 as *u8 }
281 let p2: i64 = j + 1
282 if p2 >= end { return 0 as *u8 }
283 buf[end] = 0 as u8
284 szout[0] = t
285 return ((buf as i64) + p2) as *u8
286 }
287 var i: i64 = start
288 var v: i64 = 0
289 var any: i64 = 0
290 var go: i64 = 1
291 while go == 1 {
292 if i >= end { go = 0 } else {
293 let c: i64 = buf[i] as i64
294 if c >= TC_D0 { if c <= TC_D9 { v = v * 10 + (c - TC_D0); any = 1; i = i + 1 } else { go = 0 } } else { go = 0 }
295 }
296 }
297 if any == 0 { return 0 as *u8 }
298 if i >= end { return 0 as *u8 }
299 if buf[i] != (TC_SPACE as u8) { return 0 as *u8 }
300 let p: i64 = i + 1
301 if p >= end { return 0 as *u8 }
302 buf[end] = 0 as u8
303 szout[0] = v
304 return ((buf as i64) + p) as *u8
305}
306
307// index one manifest into (htab, keys, sizes); returns row count
308func tc_index(buf: *u8, n: i64, htab: *i64, keys: *i64, sizes: *i64) -> i64 {
309 var i: i64 = 0
310 var ls: i64 = 0
311 var cnt: i64 = 0
312 let szbox: *i64 = sys_mmap(8) as *i64
313 while i <= n {
314 var eol: i64 = 0
315 if i == n { eol = 1 }
316 if i < n { if buf[i] == (TC_NL as u8) { eol = 1 } }
317 if eol == 1 {
318 var e: i64 = i
319 if e > ls { if buf[e-1] == (TC_CR as u8) { e = e - 1 } }
320 if e > ls {
321 let rel: *u8 = tc_parse(buf, ls, e, szbox)
322 if rel != (0 as *u8) {
323 if cnt < TC_MAXENT {
324 keys[cnt] = rel as i64
325 sizes[cnt] = szbox[0]
326 var h: i64 = tc_hash(rel)
327 var guard: i64 = 0
328 while guard < TC_HBUCKETS {
329 if htab[h] == 0 { htab[h] = cnt + 1; guard = TC_HBUCKETS } else {
330 h = (h + 1) & (TC_HBUCKETS - 1)
331 guard = guard + 1
332 }
333 }
334 cnt = cnt + 1
335 }
336 }
337 }
338 ls = i + 1
339 }
340 i = i + 1
341 }
342 return cnt
343}
344
345// look up a path; returns size, or -1 if absent
346func tc_lookup(rel: *u8, htab: *i64, keys: *i64, sizes: *i64) -> i64 {
347 var h: i64 = tc_hash(rel)
348 var guard: i64 = 0
349 while guard < TC_HBUCKETS {
350 let slot: i64 = htab[h]
351 if slot == 0 { return 0 - 1 }
352 let idx: i64 = slot - 1
353 if tc_streq(rel, keys[idx] as *u8) == 1 { return sizes[idx] }
354 h = (h + 1) & (TC_HBUCKETS - 1)
355 guard = guard + 1
356 }
357 return 0 - 1
358}
359
360// THE DETECTOR: 1 = this path DIVERGES (absent from either tree, or different bytes)
361func tc_diverges(rel: *u8) -> i64 {
362 let a: i64 = tc_lookup(rel, tc_ha, tc_ka, tc_sa)
363 let b: i64 = tc_lookup(rel, tc_hb, tc_kb, tc_sb)
364 if a < 0 { return 1 }
365 if b < 0 { return 1 }
366 if a != b { return 1 }
367 return 0
368}
369
370func main(argc: i64, argv: *i64) -> i64 {
371 let ctr: *i64 = gv_ctr()
372 gv_head("nx_treecanon gate -- a build must never compile a copy that diverges from canon" as *u8)
373 if argc < 4 {
374 gv_puts("usage: nx_treecanon_gate <manifestCANON> <manifestOTHER> <canon.conf>\n" as *u8)
375 gv_puts(" manifests come from: nx_treediff <dir> <outfile>\n" as *u8)
376 return 2
377 }
378 let pa: *u8 = argv[1] as *u8
379 let pb: *u8 = argv[2] as *u8
380 let pc: *u8 = argv[3] as *u8
381
382 tc_ha = sys_mmap(TC_HBUCKETS*8) as *i64
383 tc_hb = sys_mmap(TC_HBUCKETS*8) as *i64
384 tc_ka = sys_mmap(TC_MAXENT*8) as *i64
385 tc_kb = sys_mmap(TC_MAXENT*8) as *i64
386 tc_sa = sys_mmap(TC_MAXENT*8) as *i64
387 tc_sb = sys_mmap(TC_MAXENT*8) as *i64
388
389 let nbox: *i64 = sys_mmap(8) as *i64
390 let ba: *u8 = tc_readall(pa, nbox, TC_BUFCAP)
391 let la: i64 = nbox[0]
392 let bb: *u8 = tc_readall(pb, nbox, TC_BUFCAP)
393 let lb: i64 = nbox[0]
394 if la > 0 { tc_na = tc_index(ba, la, tc_ha, tc_ka, tc_sa) }
395 if lb > 0 { tc_nb = tc_index(bb, lb, tc_hb, tc_kb, tc_sb) }
396 gv_puts(" measured: canon tree rows=" as *u8); gv_num(tc_na)
397 gv_puts(" other tree rows=" as *u8); gv_num(tc_nb); gv_puts("\n" as *u8)
398 var t0: i64 = 0
399 if tc_na > 0 { if tc_nb > 0 { t0 = 1 } }
400 gv_check("T0 LOADED both manifests parsed and indexed" as *u8, t0, ctr)
401
402 // ---- global divergence tally: context, NOT the blocking check ----
403 var div: i64 = 0
404 var same: i64 = 0
405 var i: i64 = 0
406 while i < tc_na {
407 let rel: *u8 = tc_ka[i] as *u8
408 let b2: i64 = tc_lookup(rel, tc_hb, tc_kb, tc_sb)
409 if b2 < 0 { div = div + 1 } else {
410 if b2 != tc_sa[i] { div = div + 1 } else { same = same + 1 }
411 }
412 i = i + 1
413 }
414 gv_puts(" measured: of the canon tree's rows, " as *u8); gv_num(div)
415 gv_puts(" diverge and " as *u8); gv_num(same)
416 gv_puts(" match in the other tree (CONTEXT ONLY -- not blocking)\n" as *u8)
417
418 // ---- the canon list ----
419 let cb: *u8 = tc_readall(pc, nbox, TC_CONFCAP)
420 let lc: i64 = nbox[0]
421 var listed: i64 = 0
422 var bad: i64 = 0
423 var stale: i64 = 0 // manifest-divergent, but the LIVE bytes match the other tree
424 var confirmed: i64 = 0 // manifest-divergent AND the live bytes confirm it
425 var unresolved: i64 = 0 // could not adjudicate (unreadable, or both manifests match live)
426 if lc > 0 {
427 var p: i64 = 0
428 var ls: i64 = 0
429 while p <= lc {
430 var eol: i64 = 0
431 if p == lc { eol = 1 }
432 if p < lc { if cb[p] == (TC_NL as u8) { eol = 1 } }
433 if eol == 1 {
434 var e: i64 = p
435 if e > ls { if cb[e-1] == (TC_CR as u8) { e = e - 1 } }
436 if e > ls {
437 if cb[ls] != (TC_HASHCH as u8) {
438 cb[e] = 0 as u8
439 var rs: i64 = ls
440 if cb[rs] == (TC_BANG as u8) { rs = rs + 1 }
441 if rs < e {
442 let rel: *u8 = ((cb as i64) + rs) as *u8
443 listed = listed + 1
444 if tc_diverges(rel) == 1 {
445 bad = bad + 1
446 let cid: i64 = tc_lookup(rel, tc_ha, tc_ka, tc_sa)
447 let oid: i64 = tc_lookup(rel, tc_hb, tc_kb, tc_sb)
448 gv_puts(" DIVERGENT CANON: " as *u8); gv_puts(rel)
449 gv_puts(" canonid=" as *u8); gv_num(cid)
450 gv_puts(" otherid=" as *u8); gv_num(oid)
451 gv_puts("\n" as *u8)
452 // T5: ask the live bytes which record is telling the truth.
453 if argc >= 5 {
454 let lidbox: *i64 = sys_mmap(16) as *i64
455 let lst: i64 = tc_live_id(argv[4] as *u8, rel, lidbox)
456 if lst != 0 {
457 unresolved = unresolved + 1
458 gv_puts(" LIVE UNREADABLE -- cannot adjudicate this row\n" as *u8)
459 }
460 if lst == 0 {
461 let lid: i64 = lidbox[0]
462 gv_puts(" liveid=" as *u8); gv_num(lid)
463 // The live tree IS the canon tree this gate runs on. If the live
464 // bytes match the OTHER manifest but not the canon one, the trees
465 // agree and it is the CANON MANIFEST that lagged.
466 if lid == oid {
467 if lid != cid {
468 stale = stale + 1
469 gv_puts(" >>STALE CANON MANIFEST: live bytes MATCH the other tree; this is manifest lag, NOT a fork.<<\n" as *u8)
470 }
471 }
472 if lid != oid {
473 confirmed = confirmed + 1
474 gv_puts(" >>FORK CONFIRMED AGAINST LIVE BYTES: the trees really do differ here.<<\n" as *u8)
475 }
476 if lid == oid { if lid == cid { unresolved = unresolved + 1; gv_puts(" >>UNEXPLAINED: live matches BOTH manifests yet they disagree -- read the rows by hand.<<\n" as *u8) } }
477 }
478 sys_munmap(lidbox as *u8, 16)
479 }
480 }
481
482 }
483 }
484 }
485 ls = p + 1
486 }
487 p = p + 1
488 }
489 }
490 gv_puts(" measured: canon paths listed=" as *u8); gv_num(listed)
491 gv_puts(" divergent=" as *u8); gv_num(bad); gv_puts("\n" as *u8)
492 var t1: i64 = 0
493 if listed > 0 { t1 = 1 }
494 gv_check("T1 CANON READ the canon list loaded and is non-empty (an empty list passes vacuously)" as *u8, t1, ctr)
495 var t2: i64 = 0
496 if bad == 0 { t2 = 1 }
497 gv_check("T2 CANON CLEAN every canon path present in BOTH trees and byte-identical" as *u8, t2, ctr)
498
499 // ---- T5 CONFIRMED AGAINST LIVE BYTES (needs argv[4]) ----
500 // T2 above is DELIBERATELY LEFT ALONE: it is the manifest-agreement contract other consumers read,
501 // and rule 19 says add a field, never redefine one. T5 is the sharper instrument beside it.
502 // The distinction it draws is the difference between a real build outage and a manufactured one:
503 // STALE -- the manifests disagree but the live bytes agree with the other tree. Refusing here
504 // blocks every seat for a reason that does not exist. Regenerate the manifest.
505 // CONFIRMED -- the live bytes really do differ from the other tree. This is the fork the gate was
506 // built to refuse, and it is the only class that should ever stop a build.
507 // ★★★★★A GATE THAT CANNOT SAY WHY IT REFUSED WILL EVENTUALLY BE SWITCHED OFF FOR REFUSING WRONGLY.
508 if argc >= 5 {
509 gv_puts(" adjudicated against live bytes: stale_manifest=" as *u8); gv_num(stale)
510 gv_puts(" confirmed_fork=" as *u8); gv_num(confirmed)
511 gv_puts(" unresolved=" as *u8); gv_num(unresolved); gv_puts("\n" as *u8)
512 if stale > 0 {
513 gv_puts(" >>" as *u8); gv_num(stale)
514 gv_puts(" of the " as *u8); gv_num(bad)
515 gv_puts(" divergent canon row(s) are MANIFEST LAG, not fork. The canon manifest is behind the\n" as *u8)
516 gv_puts(" live tree -- regenerate it (nx_treehash <tree> <manifest>) before reading T2 as a fork.<<\n" as *u8)
517 }
518 var t5: i64 = 0
519 if confirmed == 0 { if unresolved == 0 { t5 = 1 } }
520 gv_check("T5 NO CONFIRMED FORK every divergent canon row was adjudicated against the live bytes and none is a real fork" as *u8, t5, ctr)
521 }
522 if argc < 5 {
523 gv_puts(" T5 NOT ADJUDICATED -- without a live tree root, a manifest disagreement cannot be told\n" as *u8)
524 gv_puts(" >>apart from a stale manifest. T2 alone cannot distinguish a fork from a clock.<<\n" as *u8)
525 }
526
527 // ---- T3 BITE: calibrate the detector on REAL rows found at runtime, no hardcoded fixture ----
528 // A gate whose detector cannot fire is a voter with information content zero (council rule).
529 var badpath: *u8 = 0 as *u8
530 var goodpath: *u8 = 0 as *u8
531 i = 0
532 while i < tc_na {
533 let rel: *u8 = tc_ka[i] as *u8
534 let b3: i64 = tc_lookup(rel, tc_hb, tc_kb, tc_sb)
535 if b3 >= 0 {
536 if b3 != tc_sa[i] { if badpath == (0 as *u8) { badpath = rel } }
537 if b3 == tc_sa[i] { if goodpath == (0 as *u8) { goodpath = rel } }
538 }
539 i = i + 1
540 }
541 var fires_bad: i64 = 0
542 var fires_good: i64 = 1
543 if badpath != (0 as *u8) { fires_bad = tc_diverges(badpath) }
544 if goodpath != (0 as *u8) { fires_good = tc_diverges(goodpath) }
545 if badpath != (0 as *u8) { gv_puts(" bite fixture DIVERGENT: " as *u8); gv_puts(badpath); gv_puts("\n" as *u8) }
546 if goodpath != (0 as *u8) { gv_puts(" bite fixture IDENTICAL: " as *u8); gv_puts(goodpath); gv_puts("\n" as *u8) }
547 gv_bite("T3 BITE divergence detector fires on a real divergent path, silent on a real identical one" as *u8, fires_bad, fires_good, ctr)
548
549 // ---- T4 STABILITY: WERE THE CANON FILES HOLDING STILL WHILE WE MEASURED THEM? ----
550 // T1-T3 above compare two MANIFESTS. A manifest is a claim about a moment that has already
551 // passed, so this gate can return a confident GREEN over a snapshot taken WHILE a writer was
552 // mid-file -- and the estate has already paid for exactly that: a torn copy of
553 // nx_gate_verdict.nx was published this session with a hash matching NEITHER tree, on a `!`
554 // row, which freezes every build. The manifest legs structurally cannot see it: both sides
555 // agreed, because both sides described the same torn bytes.
556 // ⇒ THIS TOOTH SAMPLES THE LIVE FILES TWICE, TC_SETTLE_MS APART, AND ASKS WHETHER THEY MOVED.
557 // ★★★★★★A MANIFEST CANNOT TELL YOU WHETHER IT WAS TAKEN AT A SAFE MOMENT -- ONLY A SECOND
558 // SAMPLE CAN. Additive by construction: with no argv[4] the check is not added to the ballot
559 // and every existing caller keeps its exact previous verdict.
560 if argc >= 5 {
561 let liveroot: *u8 = argv[4] as *u8
562 let nb2: *i64 = sys_mmap(64) as *i64
563 let h1: *i64 = sys_mmap(8 * TC_MAXCANON) as *i64
564 let h2: *i64 = sys_mmap(8 * TC_MAXCANON) as *i64
565 let st1: *i64 = sys_mmap(8 * TC_MAXCANON) as *i64
566 let st2: *i64 = sys_mmap(8 * TC_MAXCANON) as *i64
567 // ⚠A FRESH CONF READ PER PASS: tc_sample_all NUL-terminates each row in place, so a second
568 // walk over the first buffer would find no line ends and silently sample nothing -- a
569 // vacuous PASS, the exact failure class this tooth exists to catch.
570 let cf1: *u8 = tc_readall(pc, nbox, TC_CONFCAP)
571 let s1: i64 = tc_sample_all(cf1, nbox[0], liveroot, h1, st1, TC_MAXCANON, nb2)
572 sys_sleep_ms(TC_SETTLE_MS)
573 let cf2: *u8 = tc_readall(pc, nbox, TC_CONFCAP)
574 let s2: i64 = tc_sample_all(cf2, nbox[0], liveroot, h2, st2, TC_MAXCANON, nb2)
575 var moved: i64 = 0
576 var unread: i64 = 0
577 var cmp: i64 = s1
578 if s2 < cmp { cmp = s2 }
579 i = 0
580 while i < cmp {
581 // Flat on purpose: status decides membership, THEN the hashes are compared. A file that
582 // could not be read on EITHER pass is unchecked, never quietly counted as unmoved.
583 var ok: i64 = 0
584 if st1[i] == 0 { if st2[i] == 0 { ok = 1 } }
585 if ok == 0 { unread = unread + 1 }
586 if ok == 1 { if h1[i] != h2[i] { moved = moved + 1 } }
587 i = i + 1
588 }
589 gv_puts(" live root: " as *u8); gv_puts(liveroot); gv_puts("\n" as *u8)
590 gv_puts(" sampled " as *u8); gv_num(cmp)
591 gv_puts(" canon files twice, " as *u8); gv_num(TC_SETTLE_MS)
592 gv_puts("ms apart -- moved=" as *u8); gv_num(moved)
593 gv_puts(" unreadable=" as *u8); gv_num(unread); gv_puts("\n" as *u8)
594 // A row count that changed between passes means the conf ITSELF is being edited; that is a
595 // moving target too, and it must not pass quietly just because no file hash differed.
596 if s1 != s2 { gv_puts(" >>THE CANON CONF CHANGED BETWEEN PASSES (" as *u8); gv_num(s1); gv_puts(" -> " as *u8); gv_num(s2); gv_puts(" rows)<<\n" as *u8) }
597 var t4: i64 = 0
598 // ★AN UNREADABLE CANON FILE IS AN UNCHECKED CANON FILE. Counting it and passing anyway is how
599 // the first cut of this tooth reported PASS over 5 of 12 files it never actually hashed.
600 if unread > 0 { gv_puts(" >>" as *u8); gv_num(unread); gv_puts(" CANON FILE(S) COULD NOT BE READ -- they were NOT checked for movement.<<\n" as *u8) }
601 if moved == 0 { if unread == 0 { if s1 == s2 { if cmp > 0 { t4 = 1 } } } }
602 gv_check("T4 CANON SETTLED every listed canon file is byte-stable across a resample; no snapshot was taken mid-write" as *u8, t4, ctr)
603 }
604 if argc < 5 {
605 gv_puts(" T4 STABILITY NOT SAMPLED -- pass a live tree root as argv[4] to check for mid-write snapshots.\n" as *u8)
606 gv_puts(" >>Without it this gate can only tell you the two MANIFESTS agree, never that they were taken at a safe moment.<<\n" as *u8)
607 }
608
609 let rc: i64 = gv_verdict("TREECANON-GATE" as *u8, ctr,
610 "canon paths are byte-identical across both trees; a build compiling a forked copy is refused" as *u8)
611 return rc
612}