code wiki / (root) / nx_closurehash.nx

nx_closurehash.nx source

↩ module page · 241 lines · 12675 B

1// nx_closurehash.nx -- PROVENANCE AT PRODUCTION, GENERALISED FROM ONE FILE TO THE WHOLE IMPORT CLOSURE. 2// 3// THE DEFECT THIS RETIRES (measured all day 2026-08-07): the build lane decides "has this tree forked?" 4// by comparing the LIVE NAS tree against `treecanon_laptop_hash.mf` -- a manifest generated from the 5// LAPTOP mirror and pushed across. But edits are made with nx_fs_write, which writes DIRECTLY to the NAS, 6// and builds compile FROM the NAS. So the newest truth is on the NAS and the judge is the stale mirror: 7// *THE CANON GUARD RUNS BACKWARDS RELATIVE TO HOW EDITS ACTUALLY FLOW, AND CALLS THE TRUTH A FORK.* 8// Consequences seen today: builds blocked by my own newer edit · a manifest push losing a file-lock race · 9// four different hashes for one header inside ten minutes · ~3,800 NAS-ahead files nobody can reconcile. 10// Every guard involved was working correctly on a bad premise. 11// 12// nx_mgmt_api already states the right principle, for ONE file: 13// "PROVENANCE, CAPTURED AT THE MOMENT OF PRODUCTION ... A CLOCK CANNOT ESTABLISH PROVENANCE. 14// RECORD IT WHERE IT IS CHEAP: HERE. LATER IT IS UNRECOVERABLE." 15// It emits src_sha256/src_stable for the target's own source. That is the whole idea, one file wide. 16// This widens it to every source the compiler will actually read. 17// 18// *A MANIFEST IS A CLAIM ABOUT A TREE AT A PAST INSTANT; A CLOSURE HASH IS A FACT ABOUT THE BINARY IN 19// YOUR HAND.* With the closure recorded at build time you can answer "was this artifact built from these 20// exact bytes?" with no manifest, no push, no watermark, and nothing to go stale -- and the question a 21// census can never answer, "which of the two trees was I compiled from", becomes trivial. 22// 23// SOURCE RESOLUTION ORDER IS LOAD-BEARING and copied from nx_stalesweep's header rather than guessed: 24// _hdl_build/<t>.nx FIRST, then runtime/<t>.nx -- because buildrun compiles the _hdl_build twin when BOTH 25// exist, so hashing the runtime twin would describe a file the compiler never opened. 26// 27// UNRESOLVED IMPORTS ARE THEIR OWN BUCKET AND ARE COUNTED. An import that resolves nowhere is exactly the 28// kind of thing this must not silently omit -- omitting it would make a BROKEN closure hash the same 29// shape as a healthy one. *A PROVENANCE RECORD THAT DROPS WHAT IT COULD NOT FIND IS A FORGERY.* 30// 31// nx_closurehash <target> [root] root default "." (run from the buildroot) 32// -> one line per source, then: closure_sha=<64hex> files=N unresolved=M bytes=B 33// exit 0 complete | 1 closure has unresolved imports | 2 target source not found | 3 usage 34// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 35import "nx_syscalls.nx" 36import "nx_sha256.nx" 37// ONE RESOLVER, ONE OWNER (2026-08-25, rung LN10). chresolve's body moved VERBATIM into 38// nx_incclosure_lib.icl_resolve and this organ now DELEGATES to it, because nx_inc_compile -- the 39// per-module incremental build cache -- must resolve module names to exactly the files THIS organ read, 40// and a second resolver that disagrees by one path hashes the wrong file, reports a cache hit and serves 41// a stale artifact. Disagreement is now impossible by construction rather than by discipline. Neutrality 42// was proven, not asserted: nx_behaveprobe live-vs-staged over real closures. 43import "nx_incclosure_lib.nx" 44const CH_MAGIC_262144: i64 = 262144 45const CH_MAGIC_1024: i64 = 1024 46 47const CH_MAXF: i64 = 1024 // closure entries; refuses past this rather than silently truncating 48const CH_NAMEW: i64 = 192 // bytes per stored name 49const CH_FCAP: i64 = 4194304 // 4MB per source file 50const CH_ACC: i64 = 262144 // accumulator for the combined digest input 51 52func chp(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 53func che(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(2,s,n); return 0 } 54func chn(v: i64) -> i64 { 55 var m: i64 = v 56 if m < 0 { chp("-" as *u8); m = 0 - m } 57 let t: *u8 = sys_mmap(32) 58 var k: i64 = 0 59 if m == 0 { t[0] = 48 as u8; k = 1 } 60 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 61 let b: *u8 = sys_mmap(32) 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 chcat(d: *u8, o: i64, s: *u8) -> i64 { var x: i64=o; var i: i64=0; while s[i]!=(0 as u8){d[x]=s[i];x=x+1;i=i+1} return x } 68func chlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 69func chslot(b: *u8, i: i64) -> *u8 { return ((b as i64) + i*CH_NAMEW) as *u8 } 70func chseq(a: *u8, b: *u8) -> i64 { 71 var i: i64 = 0 72 var go: i64 = 1 73 while go == 1 { 74 let x: i64 = a[i] as i64 75 let y: i64 = b[i] as i64 76 if x != y { return 0 } 77 if x == 0 { go = 0 } 78 i = i + 1 79 } 80 return 1 81} 82func chhex(dig: *u8, out: *u8) -> i64 { 83 let h: *u8 = "0123456789abcdef" as *u8 84 var i: i64 = 0 85 while i < 32 { 86 let v: i64 = dig[i] as i64 87 out[i*2] = h[(v>>4)&15] 88 out[i*2+1] = h[v&15] 89 i = i + 1 90 } 91 out[64] = 0 as u8 92 return 64 93} 94func chexists(p: *u8) -> i64 { let fd: i64 = sys_openat_rd(p); if fd < 0 { return 0 } sys_close(fd); return 1 } 95func chread(path: *u8, buf: *u8, cap: i64) -> i64 { 96 let fd: i64 = sys_openat_rd(path) 97 if fd < 0 { return 0 - 1 } 98 var n: i64 = 0 99 var go: i64 = 1 100 while go == 1 { 101 let r: i64 = sys_read(fd, ((buf as i64) + n) as *u8, cap - n) 102 if r <= 0 { go = 0 } else { n = n + r } 103 if n >= cap { go = 0 } 104 } 105 sys_close(fd) 106 return n 107} 108 109// Resolve a bare source name (e.g. "nx_syscalls.nx") to a path under root. _hdl_build FIRST -- see header. 110// Returns 1 and fills `out`, or 0 when it resolves nowhere. 111// MEASURED 2026-08-07: a two-path resolver (_hdl_build, runtime) reported unresolved=2 on nx_mgmt_api -- 112// nx_h2c_p256.nx and nx_voprf.nx -- while that exact target COMPILES CLEAN. They live in `runtime/hub/`, 113// a THIRD search path I did not know about. *AN INCOMPLETE RESOLVER REPORTS A HEALTHY TREE AS BROKEN*, 114// and a provenance tool that does that is worse than none: it manufactures forks. 115// Hardcoding the directory list would drift from nx_cc the first time a lane adds a folder, so after the 116// two ORDERED paths this ENUMERATES the subdirectories of runtime/ and tries each. The order still 117// matters and is preserved: _hdl_build first (buildrun compiles that twin when both exist), then runtime, 118// then any subdir -- so a name present in two places still resolves to the one the compiler would read. 119func chresolve(root: *u8, name: *u8, out: *u8) -> i64 { return icl_resolve(root, name, out) } 120 121func main(argc: i64, argv: *i64) -> i64 { 122 if argc < 2 { che("usage: nx_closurehash <target> [root]\n" as *u8); sys_exit(3); return 3 } 123 let tgt: *u8 = argv[1] as *u8 124 var root: *u8 = "." as *u8 125 if argc >= 3 { let r: *u8 = argv[2] as *u8; if r[0] != (0 as u8) { root = r } } 126 127 // worklist of bare source names; `seen` doubles as the visited set and the emit order 128 let seen: *u8 = sys_mmap(CH_MAXF * CH_NAMEW) 129 var nseen: i64 = 0 130 var head: i64 = 0 131 var unresolved: i64 = 0 132 var total_bytes: i64 = 0 133 134 // seed with "<target>.nx" 135 let s0: *u8 = chslot(seen, 0) 136 var so: i64 = chcat(s0, 0, tgt) 137 so = chcat(s0, so, ".nx" as *u8) 138 s0[so] = 0 as u8 139 nseen = 1 140 141 let path: *u8 = sys_mmap(CH_MAGIC_1024) 142 let fbuf: *u8 = sys_mmap(CH_FCAP) 143 let acc: *u8 = sys_mmap(CH_ACC) 144 var acco: i64 = 0 145 let dig: *u8 = sys_mmap(64) 146 let hex: *u8 = sys_mmap(80) 147 let imp: *u8 = sys_mmap(CH_NAMEW) 148 149 if chresolve(root, s0, path) == 0 { che("nx_closurehash: target source not found for " as *u8); che(tgt); che("\n" as *u8); sys_exit(2); return 2 } 150 151 chp("# sha256 bytes source\n" as *u8) 152 while head < nseen { 153 let nm: *u8 = chslot(seen, head) 154 if chresolve(root, nm, path) == 0 { 155 unresolved = unresolved + 1 156 chp("UNRESOLVED - " as *u8) 157 chp(nm) 158 chp("\n" as *u8) 159 } else { 160 let fn: i64 = chread(path, fbuf, CH_FCAP) 161 if fn < 0 { 162 unresolved = unresolved + 1 163 chp("UNREADABLE - " as *u8) 164 chp(nm); chp("\n" as *u8) 165 } else { 166 total_bytes = total_bytes + fn 167 sha256_digest(fbuf, fn, dig) 168 chhex(dig, hex) 169 chp(hex); chp(" " as *u8); chn(fn); chp(" " as *u8); chp(nm); chp("\n" as *u8) 170 // the combined digest is taken over "<name> <sha>\n" rows in DISCOVERY order. Order is 171 // deterministic because the walk is: seed, then imports in file order, breadth-first. 172 acco = chcat(acc, acco, nm) 173 acc[acco] = 32 as u8; acco = acco + 1 174 acco = chcat(acc, acco, hex) 175 acc[acco] = 10 as u8; acco = acco + 1 176 177 // scan for `import "` and enqueue each unseen name 178 // LINE-ANCHORED. MEASURED on the FIRST run of this organ against its own source: an 179 // unanchored scan matched the marker inside this very string constant and enqueued a 180 // fragment of source code as an import, which then reported UNRESOLVED. 181 // *A SCANNER THAT MATCHES ITS OWN SOURCE TEXT WILL FIND ITSELF* -- and it was caught only 182 // because unresolved entries are their own bucket with a non-zero exit; had they been 183 // silently dropped, this would have published a confident closure hash for a set it had 184 // mis-parsed. In NishiLang an import is line-initial, so require offset 0 or a preceding 185 // newline: the language's own grammar is the discriminator, not a heuristic. 186 var i: i64 = 0 187 while i + 8 < fn { 188 var anchored: i64 = 0 189 if i == 0 { anchored = 1 } 190 if i > 0 { if (fbuf[i-1] as i64) == 10 { anchored = 1 } } 191 var hit: i64 = anchored 192 let k: *u8 = "import \"" as *u8 193 var m: i64 = 0 194 while m < 8 { if fbuf[i+m] != k[m] { hit = 0; m = 8 } else { m = m + 1 } } 195 if hit == 1 { 196 var s: i64 = i + 8 197 var o: i64 = 0 198 while s < fn { 199 let c: i64 = fbuf[s] as i64 200 if c == 34 { s = fn } else { 201 if o < CH_NAMEW - 2 { imp[o] = fbuf[s]; o = o + 1 } 202 s = s + 1 203 } 204 } 205 imp[o] = 0 as u8 206 if o > 0 { 207 var dup: i64 = 0 208 var q: i64 = 0 209 while q < nseen { if chseq(chslot(seen, q), imp) == 1 { dup = 1; q = nseen } else { q = q + 1 } } 210 if dup == 0 { 211 // REFUSE rather than truncate: a closure silently cut at the cap would 212 // publish a confident hash for a PARTIAL set, which is the same defect 213 // class as a directory read that stops at one getdents batch. 214 if nseen >= CH_MAXF { che("nx_closurehash: REFUSED -- closure exceeds CH_MAXF; raise the cap deliberately rather than publish a partial hash\n" as *u8); sys_exit(2); return 2 } 215 let dstn: *u8 = chslot(seen, nseen) 216 var z: i64 = 0 217 while imp[z] != (0 as u8) { dstn[z] = imp[z]; z = z + 1 } 218 dstn[z] = 0 as u8 219 nseen = nseen + 1 220 } 221 } 222 } 223 i = i + 1 224 } 225 } 226 } 227 head = head + 1 228 } 229 230 sha256_digest(acc, acco, dig) 231 chhex(dig, hex) 232 chp("closure_sha=" as *u8); chp(hex) 233 chp(" files=" as *u8); chn(nseen) 234 chp(" unresolved=" as *u8); chn(unresolved) 235 chp(" bytes=" as *u8); chn(total_bytes) 236 chp("\n" as *u8) 237 // An incomplete closure must NOT exit 0: a caller gating a promote on this needs the failure in the 238 // exit code, not buried in a field it may not read. 239 if unresolved > 0 { sys_exit(1); return 1 } 240 return 0 241}