code wiki / _hdl_build / nx_treediverge_beat.nx

nx_treediverge_beat.nx source

↩ module page · 243 lines · 13964 B

1// nx_treediverge_beat.nx -- no-arg BEAT for the ONE clock: the standing cross-tree divergence gauge 2// (debt 1785622769: divergence must be caught the day it appears, not by archaeology). Composes the 3// three PROVEN instruments through tr_run_capture -- zero detector logic duplicated: nx_treediff 4// regenerates the NAS manifest, nx_treediverge compares it against the authoring manifest (pushed 5// fresh at every session start by the treecanon SessionStart hook), nx_treecanon_gate re-runs the 6// canon verdict. APPENDS one evidence line per run to the trend log -- including BEAT-FAIL lines 7// (a beat with no evidence line is a claim; failure lines are evidence too). The BURN-DOWN is the 8// point: divergent= over time IS the 8,258-file fork shrinking, minable from the trend file. 9// Scheduler contract: the tickless clock (clock_jobs.tsv / the clockjobs- store plane) fork+execs 10// this elf with NO args from cwd nishihost. argv[1]=authoring-manifest argv[2]=trendfile override 11// the defaults for testing (argv[1] pointed at a missing file is the negative control: 12// nx_treediverge REFUSES by its own non-vacuity law and this beat must append BEAT-FAIL + exit 1, 13// never a comforting line). 14// trend line: <epoch>\tDIVERGE <envelope, newlines as ' | '>\tGATE rc=<rc> <verdict line>\n 15// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 16import "nx_sovjson_lib.nx" 17import "nx_syscalls.nx" 18import "nx_tool_run.nx" 19 20const TDB_DIVCAP: i64 = 4194304 // treediverge prints a row per divergent file (8,258 today, ~40B 21 // each = ~350KB); 4MB holds ~100k rows. AT the cap = truncated = 22 // the envelope (printed LAST) may be gone -> BEAT-FAIL, because a 23 // capped result is a FLOOR, never a count. 24const TDB_SMALLCAP: i64 = 16384 25const TDB_LINE: i64 = 4096 26const TDB_MODE: i64 = 420 27const TDB_TAB: i64 = 9 28const TDB_NL: i64 = 10 29const TDB_SPACE: i64 = 32 30const TDB_PIPE: i64 = 124 31 32func tdb_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 33func tdb_werr(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 } 34func tdb_append(path: *u8, buf: *u8, n: i64) -> i64 { 35 let fd: i64 = sys_openat_append(path, TDB_MODE) 36 if fd < 0 { return 0 - 1 } 37 sys_write(fd, buf, n) 38 sys_fsync(fd) 39 sys_close(fd) 40 return 0 41} 42// end index after stripping trailing newlines 43func tdb_tail_end(buf: *u8, n: i64) -> i64 { 44 var e: i64 = n 45 var go: i64 = 1 46 while go == 1 { if e > 0 { if buf[e-1] == (TDB_NL as u8) { e = e - 1 } else { go = 0 } } else { go = 0 } } 47 return e 48} 49// start index of the `want`-th line counting back from the (newline-stripped) end; 0-bounded 50func tdb_tail_start(buf: *u8, n: i64, want: i64) -> i64 { 51 let e: i64 = tdb_tail_end(buf, n) 52 var i: i64 = e 53 var cnt: i64 = 0 54 var go: i64 = 1 55 while go == 1 { 56 if i <= 0 { go = 0 } else { 57 if buf[i-1] == (TDB_NL as u8) { cnt = cnt + 1; if cnt >= want { go = 0 } } 58 if go == 1 { i = i - 1 } 59 } 60 } 61 return i 62} 63func tdb_contains(buf: *u8, n: i64, pat: *u8) -> i64 { 64 var pl: i64 = 0 65 while pat[pl] != (0 as u8) { pl = pl + 1 } 66 if pl == 0 { return 0 } 67 var i: i64 = 0 68 while i + pl <= n { 69 var j: i64 = 0 70 var hit: i64 = 1 71 while j < pl { if buf[i+j] != pat[j] { hit = 0; j = pl } else { j = j + 1 } } 72 if hit == 1 { return 1 } 73 i = i + 1 74 } 75 return 0 76} 77// copy [s,e) of buf into ln at o, rendering newlines as ' | '; bounded by TDB_LINE 78func tdb_cat_span(ln: *u8, o: i64, buf: *u8, s: i64, e: i64) -> i64 { 79 var p: i64 = o 80 var i: i64 = s 81 while i < e { 82 if p >= TDB_LINE - 4 { i = e } else { 83 if buf[i] == (TDB_NL as u8) { ln[p] = TDB_SPACE as u8; ln[p+1] = TDB_PIPE as u8; ln[p+2] = TDB_SPACE as u8; p = p + 3 } 84 else { ln[p] = buf[i]; p = p + 1 } 85 i = i + 1 86 } 87 } 88 return p 89} 90 91func main(argc: i64, argv: *i64) -> i64 { 92 var lapmf: *u8 = "knowledge/status/treecanon_laptop.mf" as *u8 93 var trend: *u8 = "knowledge/status/treediverge_trend.log" as *u8 94 if argc > 1 { lapmf = argv[1] as *u8 } 95 if argc > 2 { trend = argv[2] as *u8 } 96 let nasmf: *u8 = "knowledge/status/treecanon_nas.mf" as *u8 97 let conf: *u8 = "knowledge/tree_canon.conf" as *u8 98 let e_td: *u8 = "/volume1/homes/elderwesto/nishihost/nx_treediff.elf" as *u8 99 let e_dv: *u8 = "/volume1/homes/elderwesto/nishihost/nx_treediverge.elf" as *u8 100 let e_gt: *u8 = "/volume1/homes/elderwesto/nishihost/nx_treecanon_gate.elf" as *u8 101 102 // 1) fresh NAS manifest (also a durable artifact other instruments can read) 103 let av1: *i64 = sys_mmap(8*4) as *i64 104 av1[0] = e_td as i64; av1[1] = ("buildroot/runtime" as *u8) as i64; av1[2] = nasmf as i64; av1[3] = 0 105 let out1: *u8 = sys_mmap(TDB_SMALLCAP) 106 let ol1: *i64 = sys_mmap(16) as *i64 107 let rc1: i64 = tr_run_capture(e_td, av1, out1, TDB_SMALLCAP, ol1) 108 109 // 2) the divergence report (the burn-down number lives in its envelope, printed LAST) 110 let av2: *i64 = sys_mmap(8*6) as *i64 111 av2[0] = e_dv as i64; av2[1] = nasmf as i64; av2[2] = lapmf as i64 112 av2[3] = ("NAS" as *u8) as i64; av2[4] = ("LAPTOP" as *u8) as i64; av2[5] = 0 113 let out2: *u8 = sys_mmap(TDB_DIVCAP) 114 let ol2: *i64 = sys_mmap(16) as *i64 115 let rc2: i64 = tr_run_capture(e_dv, av2, out2, TDB_DIVCAP, ol2) 116 117 // 1b) CONTENT manifest of the NAS tree -- MOVED ABOVE THE GATE 2026-08-07. 118 // It used to run last (as leg 5), which meant the canon verdict below could only ever read the 119 // manifest this beat wrote AN HOUR AGO. Hashing 18k files is the expensive half and this beat 120 // already walks the tree, so it belongs on the clock -- but it has to happen BEFORE the consumer 121 // that reads it, not after. A missing/failed leg is reported in the envelope and never fails the beat. 122 let e_th: *u8 = "/volume1/homes/elderwesto/nishihost/nx_treehash.elf" as *u8 123 let nashash: *u8 = "knowledge/status/treecanon_nas_hash.mf" as *u8 124 let av5: *i64 = sys_mmap(8*4) as *i64 125 av5[0] = e_th as i64; av5[1] = ("buildroot/runtime" as *u8) as i64; av5[2] = nashash as i64; av5[3] = 0 126 let out5: *u8 = sys_mmap(TDB_SMALLCAP) 127 let ol5: *i64 = sys_mmap(16) as *i64 128 let rc5: i64 = tr_run_capture(e_th, av5, out5, TDB_SMALLCAP, ol5) 129 130 // 3) the canon verdict (RED here is EVIDENCE, not a beat failure -- the build lane enforces; 131 // this beat only measures, so its own exit stays 0 as long as the evidence was captured) 132 // 133 // ---- 2026-08-07: THIS LEG NOW RUNS ON THE CONTENT MANIFESTS, NOT THE SIZE ONES. ---- 134 // It was passing treecanon_nas.mf / treecanon_laptop.mf, which carry `<bytes> <relpath>`. A SIZE 135 // SCREEN CANNOT SEE A MUTATION-CLASS DEFECT, because such a defect preserves length by construction 136 // -- this estate has an escaped `==` -> `!=` mutant on record that was byte-for-byte the same size. 137 // The sha256 manifests were already being written (leg 1b above, and the laptop's by its hook) and 138 // simply were not being read by the one leg whose whole job is to refuse a forked build. 139 // ★★★★★THE STRONGER RULER ALREADY EXISTED AND THE GATE WAS STILL POINTED AT THE WEAKER ONE. 140 // 141 // argv[4] is the LIVE tree this manifest describes. It arms two teeth that cannot work without it: 142 // T4 resamples every canon file to catch a snapshot taken mid-write, and 143 // T5 adjudicates each divergent canon row against the live bytes, separating a REAL fork from a 144 // STALE MANIFEST -- the difference between a build outage that is earned and one that is not. 145 let laphash: *u8 = "knowledge/status/treecanon_laptop_hash.mf" as *u8 146 let liveroot: *u8 = "buildroot/runtime" as *u8 147 let av3: *i64 = sys_mmap(8*6) as *i64 148 av3[0] = e_gt as i64; av3[1] = nashash as i64; av3[2] = laphash as i64; av3[3] = conf as i64 149 av3[4] = liveroot as i64; av3[5] = 0 150 151 let out3: *u8 = sys_mmap(TDB_SMALLCAP) 152 let ol3: *i64 = sys_mmap(16) as *i64 153 let rc3: i64 = tr_run_capture(e_gt, av3, out3, TDB_SMALLCAP, ol3) 154 155 // 4) SYMBOL-LEVEL WORK-LOSS on the canonical sources (added 2026-08-06). nx_srcguard already 156 // existed, was GREEN, and NOTHING RAN IT ON A SCHEDULE -- and until today its watch set did not 157 // contain a single tree_canon.conf row, so the two registries of files-that-must-not-silently- 158 // change had ZERO overlap. It is argless-defaults-to-check, so it drops straight in here rather 159 // than needing its own organ and its own clock row: this beat is ALREADY on the clock, already 160 // composes instruments through tr_run_capture, and already leaves one evidence line per run. 161 // A WORK-LOST verdict is EVIDENCE, not a beat failure -- same stance as the canon gate leg above; 162 // the beat measures, the build lane enforces. A MISSING elf is reported LOUD but must not kill the 163 // divergence gauge that was already working: a new leg may never take down the old instrument. 164 let e_sg: *u8 = "/volume1/homes/elderwesto/nishihost/nx_srcguard.elf" as *u8 165 let av4: *i64 = sys_mmap(8*3) as *i64 166 av4[0] = e_sg as i64; av4[1] = ("check" as *u8) as i64; av4[2] = 0 167 let out4: *u8 = sys_mmap(TDB_SMALLCAP) 168 let ol4: *i64 = sys_mmap(16) as *i64 169 let rc4: i64 = tr_run_capture(e_sg, av4, out4, TDB_SMALLCAP, ol4) 170 171 // 5) CONTENT manifest of the NAS tree (added 2026-08-07). Leg 1 already writes the SIZE manifest 172 // (nx_treediff), which is what the build-lane canon check consumes. This leg additionally writes a 173 // sha256 manifest so a laptop-side flow-back can compute "which NAS files CHANGED since my last 174 // sync" against a stored baseline -- CONTENT-exact, so a same-size seat edit cannot hide in it. 175 // WHY IT LIVES HERE: hashing 18k files is the expensive half, and this beat is already on the clock 176 // and already walks the tree. Putting it in a session-start hook would pay the cost per session and 177 // still be later than hourly. PUT THE HEAVY HALF ON THE BEAT THAT ALREADY TICKS; leave the cheap 178 // delta to the consumer. A missing/failed leg is reported in the envelope and never fails the beat. 179 // ---- 2026-08-07: THIS LEG'S WORK NOW RUNS ABOVE, BEFORE THE CANON GATE. ---- 180 // It stayed here, after the gate, for a day -- and that ordering is why the gate could only ever 181 // read the PREVIOUS beat's content manifest. The call moved; only the reporting stayed, so the 182 // envelope below is unchanged for its readers. See leg 1b. 183 // ★★★★★★A PRODUCER PLACED AFTER ITS CONSUMER IN THE SAME LOOP MAKES THE CONSUMER EXACTLY ONE 184 // FULL CYCLE STALE, FOREVER -- AND BOTH LEGS STILL REPORT SUCCESS, SO NOTHING EVER SAYS SO. 185 186 var ok: i64 = 1 187 if rc1 != 0 { ok = 0 } 188 if ol2[0] <= 0 { ok = 0 } 189 if ol2[0] >= TDB_DIVCAP - 1 { ok = 0 } 190 if tdb_contains(out2, ol2[0], "verdict=" as *u8) == 0 { ok = 0 } 191 // MEASURED (first negative-control run): a REFUSED report also prints "verdict=REFUSED", so 192 // contains("verdict=") alone waved a blind gauge through as OK. treediverge's exit codes are 193 // rc=1 for a successful DIVERGENT compare and rc=3 for REFUSED, so rc can't be the ok-signal 194 // either without baking in its full code table -- anchor on the tool's own refusal vocabulary. 195 if tdb_contains(out2, ol2[0], "verdict=REFUSED" as *u8) == 1 { ok = 0 } 196 if ol3[0] <= 0 { ok = 0 } 197 198 let ln: *u8 = sys_mmap(TDB_LINE) 199 var o: i64 = sj_catn(ln, 0, sys_now_realtime_sec()) 200 ln[o] = TDB_TAB as u8; o = o + 1 201 if ok == 1 { 202 o = sj_cat(ln, o, "DIVERGE " as *u8) 203 o = tdb_cat_span(ln, o, out2, tdb_tail_start(out2, ol2[0], 3), tdb_tail_end(out2, ol2[0])) 204 ln[o] = TDB_TAB as u8; o = o + 1 205 o = sj_cat(ln, o, "GATE rc=" as *u8) 206 o = sj_catn(ln, o, rc3) 207 ln[o] = TDB_SPACE as u8; o = o + 1 208 o = tdb_cat_span(ln, o, out3, tdb_tail_start(out3, ol3[0], 1), tdb_tail_end(out3, ol3[0])) 209 ln[o] = TDB_TAB as u8; o = o + 1 210 o = sj_cat(ln, o, "SRCGUARD rc=" as *u8) 211 o = sj_catn(ln, o, rc4) 212 ln[o] = TDB_SPACE as u8; o = o + 1 213 // ABSENT is said OUT LOUD rather than left as a blank field: a leg that silently contributes 214 // nothing is indistinguishable from a leg that ran and found nothing wrong. 215 if ol4[0] <= 0 { o = sj_cat(ln, o, "SRCGUARD-ABSENT (no output captured -- is nx_srcguard.elf deployed?)" as *u8) } 216 if ol4[0] > 0 { o = tdb_cat_span(ln, o, out4, tdb_tail_start(out4, ol4[0], 1), tdb_tail_end(out4, ol4[0])) } 217 ln[o] = TDB_TAB as u8; o = o + 1 218 o = sj_cat(ln, o, "NASHASH rc=" as *u8) 219 o = sj_catn(ln, o, rc5) 220 ln[o] = TDB_SPACE as u8; o = o + 1 221 if ol5[0] <= 0 { o = sj_cat(ln, o, "NASHASH-ABSENT (no content manifest written -- flow-back consumers will see a stale baseline)" as *u8) } 222 if ol5[0] > 0 { o = tdb_cat_span(ln, o, out5, tdb_tail_start(out5, ol5[0], 1), tdb_tail_end(out5, ol5[0])) } 223 } else { 224 o = sj_cat(ln, o, "BEAT-FAIL treediff_rc=" as *u8) 225 o = sj_catn(ln, o, rc1) 226 o = sj_cat(ln, o, " diverge_rc=" as *u8) 227 o = sj_catn(ln, o, rc2) 228 o = sj_cat(ln, o, " diverge_outlen=" as *u8) 229 o = sj_catn(ln, o, ol2[0]) 230 o = sj_cat(ln, o, " gate_rc=" as *u8) 231 o = sj_catn(ln, o, rc3) 232 } 233 ln[o] = TDB_NL as u8; o = o + 1 234 if tdb_append(trend, ln, o) != 0 { tdb_werr("TREEDIVERGE-BEAT FAIL cannot append trend file\n" as *u8); sys_exit(1); return 1 } 235 if ok == 1 { 236 tdb_puts("TREEDIVERGE-BEAT OK appended evidence line\n" as *u8) 237 sys_exit(0) 238 return 0 239 } 240 tdb_werr("TREEDIVERGE-BEAT FAIL (BEAT-FAIL line appended as evidence)\n" as *u8) 241 sys_exit(1) 242 return 1 243}