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}