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}