code wiki / _hdl_build / nx_gatereg_derive.nx

nx_gatereg_derive.nx source

↩ module page · 227 lines · 9076 B

1// nx_gatereg_derive.nx -- DERIVE the gate roster from the evidence itself. 2// 3// WHY THIS EXISTS (measured 2026-07-30): knowledge/gatereg.conf listed FOUR gates while the same NAS 4// held 181 knowledge/status/*.log files. nx_gate_rollup can only roll up what the roster names, so 5// "verdict=GREEN pass=4/4" was a true statement about a roster that covered ~2% of the evidence -- a 6// green light with almost nothing behind it. Hand-maintaining that roster is why it never grew. 7// 8// So: stop typing the roster, DERIVE it. A log whose newest line carries a verdict anchor IS a gate 9// log, by the gate plane's own contract; anything else (token_spend.log, resources.log, ...) is not 10// and is deliberately left out rather than registered as a permanent UNKNOWN. 11// 12// READ-ONLY BY DEFAULT. It prints the split (green / red / no-anchor) and the candidate rows, and 13// writes a roster ONLY when given an explicit emit path. Registering a RED gate is correct -- that is 14// the point of surfacing -- but it flips the rollup RED and nx_gate_rollup AUTO-FILES a debt, so the 15// operator sees the count before that happens instead of after. 16// 17// nx_gatereg_derive [statusdir] [emitpath] 18// statusdir default knowledge/status 19// emitpath if given, write "<BANNER>\t<path>" rows for every anchored log 20// 21// The banner is a DISPLAY LABEL only -- nx_gate_rollup reads the log's last line and never compares 22// the banner -- so it is derived from the filename (stem uppercased, '_' -> '-') and cannot be wrong 23// in a way that changes a verdict. 24// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 25import "nx_syscalls.nx" 26import "nx_gate_verdict_lib.nx" 27 28const GD_STDOUT: i64 = 1 29const GD_MODE: i64 = 420 30const GD_DIRBUF: i64 = 262144 31const GD_ROWCAP: i64 = 262144 32const GD_PATHCAP: i64 = 1024 33const GD_DIR_TYPE: i64 = 4 34 35// A gate log is a claim about NOW. Past this age it is treated as ABANDONED, not GREEN: excluded from 36// the roster and counted apart. 30 days is the documented default rather than a buried literal -- 37// override it per-run with argv[3] when a lane deliberately reports on a slower cadence. 38const GD_DEFAULT_MAX_AGE_DAYS: i64 = 30 39 40func gd_puts(s: *u8) -> i64 { let n: i64 = gv_len(s); sys_write(GD_STDOUT, s, n); return 0 } 41// write a NUL-terminated string to fd with a COMPUTED length -- never a hand-counted literal, which 42// is how a header line silently loses its trailing newline. 43func gd_fputs(fd: i64, s: *u8) -> i64 { let n: i64 = gv_len(s); sys_write(fd, s, n); return 0 } 44const GD_NUMBUF: i64 = 28 45const GD_ASCII_0: i64 = 48 46const GD_DEC: i64 = 10 47 48func gd_num(fd: i64, v: i64) -> i64 { 49 var m: i64 = v 50 if m < 0 { m = 0 - m; sys_write(fd, "-" as *u8, 1) } 51 let t: *u8 = sys_mmap(GD_NUMBUF) 52 var k: i64 = 0 53 if m == 0 { t[0] = GD_ASCII_0 as u8; k = 1 } 54 while m > 0 { t[k] = (GD_ASCII_0 + (m % GD_DEC)) as u8; m = m / GD_DEC; k = k + 1 } 55 let o: *u8 = sys_mmap(GD_NUMBUF) 56 var i: i64 = 0 57 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 58 sys_write(fd, o, k) 59 sys_munmap(t, GD_NUMBUF) 60 sys_munmap(o, GD_NUMBUF) 61 return 0 62} 63 64// name ends with ".log"? 65func gd_is_log(nm: *u8, n: i64) -> i64 { 66 if n < 5 { return 0 } 67 if nm[n-4] != (46 as u8) { return 0 } 68 if nm[n-3] != (108 as u8) { return 0 } 69 if nm[n-2] != (111 as u8) { return 0 } 70 if nm[n-1] != (103 as u8) { return 0 } 71 return 1 72} 73 74// "<dir>/<name>" into out 75func gd_join(dir: *u8, nm: *u8, out: *u8) -> i64 { 76 var o: i64 = 0 77 var i: i64 = 0 78 while dir[i] != (0 as u8) { out[o] = dir[i]; o = o + 1; i = i + 1 } 79 out[o] = 47 as u8; o = o + 1 80 i = 0 81 while nm[i] != (0 as u8) { out[o] = nm[i]; o = o + 1; i = i + 1 } 82 out[o] = 0 as u8 83 return o 84} 85 86// filename stem, uppercased, '_' -> '-' (display label only) 87func gd_banner(nm: *u8, n: i64, out: *u8) -> i64 { 88 var stop: i64 = n 89 if gd_is_log(nm, n) == 1 { stop = n - 4 } 90 var o: i64 = 0 91 var i: i64 = 0 92 while i < stop { 93 var c: i64 = nm[i] as i64 94 if c == 95 { c = 45 } 95 if c >= 97 { if c <= 122 { c = c - 32 } } 96 out[o] = c as u8 97 o = o + 1 98 i = i + 1 99 } 100 out[o] = 0 as u8 101 return o 102} 103 104func gd_bcat(buf: *u8, off: i64, s: *u8) -> i64 { 105 var o: i64 = off 106 var i: i64 = 0 107 while s[i] != (0 as u8) { buf[o] = s[i]; o = o + 1; i = i + 1 } 108 return o 109} 110 111func gd_atoi(s: *u8) -> i64 { 112 var v: i64 = 0 113 var i: i64 = 0 114 while s[i] != (0 as u8) { 115 let c: i64 = s[i] as i64 116 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 117 i = i + 1 118 } 119 return v 120} 121 122func main(argc: i64, argv: *i64) -> i64 { 123 var dir: *u8 = "knowledge/status" as *u8 124 var emit: *u8 = "" as *u8 125 var maxage: i64 = GD_DEFAULT_MAX_AGE_DAYS 126 if argc >= 2 { dir = argv[1] as *u8 } 127 if argc >= 3 { emit = argv[2] as *u8 } 128 if argc >= 4 { maxage = gd_atoi(argv[3] as *u8) } 129 let now: i64 = sys_now_realtime_sec() 130 131 let fd: i64 = sys_openat_rd(dir) 132 if fd < 0 { 133 gd_puts("NX-GATEREG-DERIVE\nverdict=RED rule=statusdir-absent dir=" as *u8) 134 gd_puts(dir) 135 gd_puts("\n" as *u8) 136 sys_exit(1) 137 return 1 138 } 139 140 let dbuf: *u8 = sys_mmap(GD_DIRBUF) 141 let lbuf: *u8 = sys_mmap(GV_LOGCAP) 142 let line: *u8 = sys_mmap(GV_LINECAP) 143 let path: *u8 = sys_mmap(GD_PATHCAP) 144 let ban: *u8 = sys_mmap(256) 145 let rows: *u8 = sys_mmap(GD_ROWCAP) 146 var ro: i64 = 0 147 148 var nlog: i64 = 0 149 var ngreen: i64 = 0 150 var nred: i64 = 0 151 var nnone: i64 = 0 152 var nstale: i64 = 0 153 154 gd_puts("=== NX-GATEREG-DERIVE -- roster derived from the evidence, not typed ===\n" as *u8) 155 156 var go: i64 = 1 157 while go == 1 { 158 let nr: i64 = sys_getdents64(fd, dbuf, GD_DIRBUF) 159 if nr <= 0 { go = 0 } else { 160 var off: i64 = 0 161 while off < nr { 162 let rec: *u8 = ((dbuf as i64) + off) as *u8 163 let ty: i64 = dirent_type(rec) 164 let nm: *u8 = dirent_name(rec) 165 let nmn: i64 = gv_len(nm) 166 if ty != GD_DIR_TYPE { 167 if gd_is_log(nm, nmn) == 1 { 168 nlog = nlog + 1 169 gd_join(dir, nm, path) 170 let st: i64 = gv_classify(path, lbuf, line) 171 if st == GV_NONE { nnone = nnone + 1 } else { 172 let age: i64 = gv_age_days(path, now) 173 if age > maxage { 174 // ABANDONED, not GREEN. Excluded from the roster and counted apart: 175 // a months-old last line is not evidence about today, and this is also 176 // what keeps one-off scratch runs out without a hand-kept denylist. 177 nstale = nstale + 1 178 gd_puts(" STALE " as *u8); gd_puts(path) 179 gd_puts(" age_days=" as *u8); gd_num(GD_STDOUT, age) 180 gd_puts("\n" as *u8) 181 } else { 182 if st == GV_GREEN { ngreen = ngreen + 1 } else { nred = nred + 1 } 183 gd_banner(nm, nmn, ban) 184 ro = gd_bcat(rows, ro, ban) 185 rows[ro] = 9 as u8; ro = ro + 1 186 ro = gd_bcat(rows, ro, path) 187 rows[ro] = 10 as u8; ro = ro + 1 188 if st == GV_RED { gd_puts(" RED " as *u8) } else { gd_puts(" GREEN " as *u8) } 189 gd_puts(path) 190 gd_puts("\n" as *u8) 191 } 192 } 193 } 194 } 195 off = off + dirent_reclen(rec) 196 } 197 } 198 } 199 sys_close(fd) 200 201 gd_puts("NX-GATEREG-DERIVE logs=" as *u8); gd_num(GD_STDOUT, nlog) 202 gd_puts(" anchored=" as *u8); gd_num(GD_STDOUT, ngreen + nred + nstale) 203 gd_puts(" registrable=" as *u8); gd_num(GD_STDOUT, ngreen + nred) 204 gd_puts(" green=" as *u8); gd_num(GD_STDOUT, ngreen) 205 gd_puts(" red=" as *u8); gd_num(GD_STDOUT, nred) 206 gd_puts(" stale=" as *u8); gd_num(GD_STDOUT, nstale) 207 gd_puts(" max_age_days=" as *u8); gd_num(GD_STDOUT, maxage) 208 gd_puts(" no-anchor=" as *u8); gd_num(GD_STDOUT, nnone) 209 210 if gv_len(emit) > 0 { 211 let efd: i64 = sys_openat_wr(emit, GD_MODE) 212 if efd < 0 { 213 gd_puts(" verdict=RED rule=emit-unwritable\n" as *u8) 214 sys_exit(1) 215 return 1 216 } 217 gd_fputs(efd, "# DERIVED by nx_gatereg_derive -- every knowledge/status log whose newest line\n" as *u8) 218 gd_fputs(efd, "# carries a verdict anchor. Re-run to refresh; do not hand-edit.\n" as *u8) 219 gd_fputs(efd, "# <gate-banner>TAB<verdict-log-path>\n" as *u8) 220 sys_write(efd, rows, ro) 221 sys_close(efd) 222 gd_puts(" emitted=" as *u8); gd_puts(emit) 223 } 224 gd_puts(" verdict=GREEN\n" as *u8) 225 sys_exit(0) 226 return 0 227}