code wiki / _hdl_build / nx_treecanon_gate.nx
nx_treecanon_gate.nx
buildroot/runtime/_hdl_build/nx_treecanon_gate.nx
about
nx_treecanon_gate.nx -- THE DIVERGENCE GATE THAT REFUSES (debt 1785622769, operator directive
2026-08-01: "ONE canonical tree per generation with a lineage manifest ... a divergence gate that
REFUSES a build when the deploying tree's copy diverges from canonical").
MEASURED 2026-08-03, and it is why this exists: NAS buildroot vs laptop nxc2 diverge on
8,258 files -- 3,705 NAS-newer, 1,838 laptop-newer, 739 NAS-only, 1,976 laptop-only. NEITHER
TREE IS A SUPERSET IN EITHER DIRECTION. A name-level diff saw only 969 of those, because the
dangerous class is files present in BOTH with DIFFERENT BYTES (5,543 of them).
⇒ ★A NAME-LEVEL TREE DIFF IS NOT A DIVERGENCE CHECK. It cannot see the failure that actually
bites -- editing a file in one tree and compiling the other copy, which is precisely how the
unfreed-mmap fix had to be applied twice (the incident that opened this debt).
SCOPE, DELIBERATE AND STATED: this gate does NOT try to reconcile the fork. Blocking on all
8,258 would block every build, and mass-merging on a size screen would be DESTRUCTIVE -- byte
size is a SCREEN, not proof of newer (banked law: newer mtime + FEWER bytes = REVERT). It
enforces a CANON LIST from knowledge/tree_canon.conf (rule 11: the policy is DATA), starting at
the crown jewels and growing by measurement. Stopping the bleeding beats an unfinished cleanup.
T0 LOADED both manifests parse and index (non-vacuity floor on the inputs)
T1 CANON READ the canon list loads and is non-empty (an empty list would pass vacuously)
T2 CANON CLEAN every canon path is present in BOTH trees and byte-identical -- REFUSES otherwise
T3 BITE the detector fires on a REAL divergent path and stays silent on a REAL identical
one, both discovered from the manifests at runtime (no hardcoded fixture)
T4 SETTLED OPTIONAL, needs argv[4]: resamples every listed canon file on the LIVE tree
TC_SETTLE_MS later and REFUSES if any moved -- T1..T3 read manifests, which are
claims about a moment already past and so cannot see a snapshot taken mid-write
T5 CONFIRMED OPTIONAL, needs argv[4]: for every canon row T2 called divergent, hashes the LIVE
file and asks whether the manifests or the bytes are right -- separating a REAL fork
from a STALE MANIFEST. The two manifests are refreshed on DIFFERENT CLOCKS (NAS side
hourly by nx_treediverge_beat, laptop side per SessionStart), so manifest lag alone
can make identical trees look forked and block every seat's build.
usage: nx_treecanon_gate <manifestCANON> <manifestOTHER> <canon.conf> [liveTreeRoot]
liveTreeRoot must be the tree manifestCANON describes (e.g. buildroot/runtime on the NAS).
license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
dependencies 3 imports · 0 importers
imports: nx_syscalls.nxnx_gate_verdict.nxnx_sha256.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 41 | const TC_HBUCKETS: i64 = 65536 |
| 42 | const TC_MAXENT: i64 = 200000 |
| 43 | const TC_BUFCAP: i64 = 16777216 |
| 44 | const TC_CONFCAP: i64 = 262144 |
| 45 | const TC_HASH_SEED: i64 = 5381 |
| 46 | const TC_SPACE: i64 = 32 |
| 47 | const TC_NL: i64 = 10 |
| 48 | const TC_CR: i64 = 13 |
| 49 | const TC_HASHCH: i64 = 35 |
| 50 | const TC_BANG: i64 = 33 // '!' row prefix: the BUILD LANE freezes every build while that row |
| 53 | const TC_PATHCAP: i64 = 1024 |
| 54 | const TC_FILECAP: i64 = 4194304 // per-file read cap for T4; largest canon source is ~290KB |
| 55 | const TC_MAXCANON: i64 = 256 |
| 56 | const TC_DIGEST_BYTES: i64 = 32 |
| 57 | const TC_EMPTY_RESERVE: i64 = 4294967296 // sys_read_file's reserve for a 0-byte file (see nx_treehash) |
| 58 | const TC_ID_FULLBYTES: i64 = 7 // 15 hex chars of identity = 7 whole bytes + 1 high nibble |
| 59 | const TC_SETTLE_MS: i64 = 4000 // sample window for T4; long enough to catch an active writer |
| 60 | const TC_SLASH: i64 = 47 |
| 61 | const TC_D0: i64 = 48 |
| 62 | const TC_D9: i64 = 57 |
| 74 | const TC_HEXID_CHARS: i64 = 15 |
| 75 | const TC_SHA_CHARS: i64 = 64 |
| 76 | const TC_LC_A: i64 = 97 |
| 77 | const TC_LC_F: i64 = 102 |
functions
| 89 | func tc_readall(path: *u8, outn: *i64, cap: i64) -> *u8 |
| 118 | func tc_filehash(root: *u8, rel: *u8, nbox: *i64, outh: *i64) -> i64 |
| 153 | func tc_sample_all(cb: *u8, lc: i64, root: *u8, out: *i64, st: *i64, cap: i64, nb2: *i64) -> i64 |
| 196 | func tc_live_id(root: *u8, rel: *u8, outid: *i64) -> i64 |
| 233 | func tc_hash(s: *u8) -> i64 |
| 239 | func tc_streq(a: *u8, b: *u8) -> i64 called by 1: tc_lookup |
| 246 | func tc_hexval(c: i64) -> i64 |
| 254 | func tc_is_hashrow(buf: *u8, start: i64, end: i64) -> i64 |
| 268 | func tc_parse(buf: *u8, start: i64, end: i64, szout: *i64) -> *u8 |
| 308 | func tc_index(buf: *u8, n: i64, htab: *i64, keys: *i64, sizes: *i64) -> i64 |
| 346 | func tc_lookup(rel: *u8, htab: *i64, keys: *i64, sizes: *i64) -> i64 |
| 361 | func tc_diverges(rel: *u8) -> i64 |
| 370 | func main(argc: i64, argv: *i64) -> i64 |