code wiki / _hdl_build / nx_rebuild_plan.nx
nx_rebuild_plan.nx source
↩ module page · 387 lines · 17891 B
1// nx_rebuild_plan.nx -- DEPENDENCY-AWARE STALENESS: which deployed organs must be rebuilt, and WHY.
2//
3// THE WOUND (measured 2026-07-30, debt 1785446507 sev8): 667 of 722 deployed organs run binaries older
4// than what their source+deps produce. Only 222 of those are stale by their OWN source. The other 445
5// are stale because a SHARED runtime moved underneath them -- `nx_syscalls.nx` is imported by 14,172
6// files, so one edit to it invalidates almost every statically linked elf in the ecosystem WITHOUT
7// touching a single line of their own .nx. Nothing in the ecosystem could compute that. The existing
8// instruments each answer a smaller question: `nx_stale_check <target>` rebuilds ONE target and
9// byte-compares (correct but O(one build) and says nothing about WHY), and an own-source mtime census
10// misses the dominant 445 entirely.
11//
12// ★★★THE POINT IS THE TRIGGER, NOT THE VERDICT. "stale" is useless on its own -- 667 rows of "stale"
13// is not a work order, it is noise. Naming the ONE file whose mtime invalidated each organ turns the
14// census into a plan: 445 organs all triggered by nx_syscalls.nx is a SINGLE decision (rebuild the
15// dependents of one file), not 445 investigations. A tool that reports a problem without naming its
16// cause makes the operator redo the diagnosis the tool already did.
17//
18// METHOD: resolve each deployed <t>.elf to its source, walk the TRANSITIVE import closure (lazily, from
19// the deployed organs as roots -- never the whole 17.5k-file tree), memoise closure_max_mtime + argmax
20// per node, then compare against the deployed elf's mtime. Cycles are coloured and broken, so a cyclic
21// import cannot hang the walk or silently drop a subtree.
22// nx_rebuild_plan [elfdir] [srcroot] [maxrows]
23// -> per-organ rows {organ, verdict, trigger, age_min} + a summary keyed by TRIGGER
24// verdicts: CURRENT | STALE-BY-OWN | STALE-BY-DEP | STALE-BY-TOOLCHAIN | NOSOURCE
25// Exit 0 always (a census is not a gate; it reports, it does not refuse).
26// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
27import "nx_syscalls.nx"
28import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
29
30const RP_AT_FDCWD: i64 = 0 - 100
31const RP_SYS_NEWFSTATAT: i64 = 262
32const RP_STATBUF: i64 = 256
33const RP_OFF_SIZE: i64 = 48
34const RP_OFF_MTIME: i64 = 88
35// sized: the reachable closure from ~722 deployed roots measured well under 8k nodes; the whole tree is
36// 17.5k files but lazy expansion never touches the unreachable remainder.
37const RP_MAXN: i64 = 8192
38const RP_PATHCAP: i64 = 192
39const RP_MAXE: i64 = 131072
40const RP_MAXROOT: i64 = 4096
41// sized: imports sit in the header; the longest organ header measured ~3KB, 32KiB is 10x margin.
42// A SHORT read here would silently drop edges and under-report staleness, which is the failure this
43// organ exists to prevent -- so the margin is deliberate, not incidental.
44const RP_HEAD: i64 = 32768
45const RP_DIRBUF: i64 = 262144
46const RP_SEC_PER_MIN: i64 = 60
47// sized: the measured trigger set is a handful of shared runtimes; 12 rows covers the long tail while
48// keeping the work order readable.
49const RP_TOP_TRIGGERS: i64 = 12
50
51func rp_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
52// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
53// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
54// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
55// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
56func rp_n(v: i64) -> i64 { nxi_out(v); return 0 }
57func rp_eq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 }
58func rp_len(a: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { i = i + 1 } return i }
59func rp_cpy(dst: *u8, src: *u8) -> i64 { var i: i64 = 0; while src[i] != (0 as u8) { dst[i] = src[i]; i = i + 1 } dst[i] = 0 as u8; return i }
60func rp_cat(dst: *u8, o: i64, src: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while src[i] != (0 as u8) { dst[p] = src[i]; p = p + 1; i = i + 1 } dst[p] = 0 as u8; return p }
61
62func rp_mtime(path: *u8) -> i64 {
63 let sb: *u8 = sys_mmap(RP_STATBUF)
64 if __syscall(RP_SYS_NEWFSTATAT, RP_AT_FDCWD, path as i64, sb as i64, 0, 0, 0) != 0 { return 0 - 1 }
65 let mp: *i64 = ((sb as i64) + RP_OFF_MTIME) as *i64
66 return mp[0]
67}
68
69func rp_readhead(path: *u8, buf: *u8, cap: i64) -> i64 {
70 let fd: i64 = sys_openat_rd(path)
71 if fd < 0 { return 0 - 1 }
72 let n: i64 = sys_read(fd, buf, cap - 1)
73 sys_close(fd)
74 if n < 0 { return 0 - 1 }
75 buf[n] = 0 as u8
76 return n
77}
78
79// true when name ends with .elf
80func rp_is_elf(name: *u8) -> i64 {
81 let n: i64 = rp_len(name)
82 if n < 5 { return 0 }
83 if name[n - 4] != (46 as u8) { return 0 }
84 if name[n - 3] != (101 as u8) { return 0 }
85 if name[n - 2] != (108 as u8) { return 0 }
86 if name[n - 1] != (102 as u8) { return 0 }
87 return 1
88}
89
90// extract the quoted target of an `import "X"` line into dst; 1 on success
91func rp_import_of(buf: *u8, i: i64, le: i64, dst: *u8, cap: i64) -> i64 {
92 if i + 7 >= le { return 0 }
93 if buf[i] != (105 as u8) { return 0 }
94 if buf[i+1] != (109 as u8) { return 0 }
95 if buf[i+2] != (112 as u8) { return 0 }
96 if buf[i+3] != (111 as u8) { return 0 }
97 if buf[i+4] != (114 as u8) { return 0 }
98 if buf[i+5] != (116 as u8) { return 0 }
99 var p: i64 = i + 6
100 var q1: i64 = 0 - 1
101 while p < le { if buf[p] == (34 as u8) { q1 = p; p = le } else { p = p + 1 } }
102 if q1 < 0 { return 0 }
103 var e: i64 = q1 + 1
104 var q2: i64 = 0 - 1
105 while e < le { if buf[e] == (34 as u8) { q2 = e; e = le } else { e = e + 1 } }
106 if q2 < 0 { return 0 }
107 var o: i64 = 0
108 var k: i64 = q1 + 1
109 while k < q2 { if o < cap - 1 { dst[o] = buf[k]; o = o + 1 } k = k + 1 }
110 dst[o] = 0 as u8
111 if o == 0 { return 0 }
112 return 1
113}
114
115func main(argc: i64, argv: *i64) -> i64 {
116 var elfdir: *u8 = "." as *u8
117 var srcroot: *u8 = "buildroot/runtime" as *u8
118 var maxrows: i64 = 40
119 // ★ONE GRAPH WALKER, TWO CONSUMERS. `list` emits bare target names so the DRAIN organ can act on
120 // exactly what this organ diagnosed. A second copy of the closure walk in the drain would be a
121 // second thing to keep correct, and the two would silently disagree the first time either changed.
122 var listmode: i64 = 0
123 if argc >= 2 { if rp_eq(argv[1] as *u8, "list" as *u8) == 1 { listmode = 1 } }
124 if listmode == 1 {
125 if argc >= 3 { elfdir = argv[2] as *u8 }
126 if argc >= 4 { srcroot = argv[3] as *u8 }
127 } else {
128 if argc >= 2 { elfdir = argv[1] as *u8 }
129 if argc >= 3 { srcroot = argv[2] as *u8 }
130 }
131
132 let npath: *u8 = sys_mmap(RP_MAXN * RP_PATHCAP)
133 let nmt: *i64 = sys_mmap(RP_MAXN * 8) as *i64
134 let ncmax: *i64 = sys_mmap(RP_MAXN * 8) as *i64
135 let ncarg: *i64 = sys_mmap(RP_MAXN * 8) as *i64
136 let ncol: *i64 = sys_mmap(RP_MAXN * 8) as *i64
137 let nes: *i64 = sys_mmap(RP_MAXN * 8) as *i64
138 let nec: *i64 = sys_mmap(RP_MAXN * 8) as *i64
139 let edges: *i64 = sys_mmap(RP_MAXE * 8) as *i64
140 var nn: i64 = 0
141 var ne: i64 = 0
142
143 let rootn: *i64 = sys_mmap(RP_MAXROOT * 8) as *i64
144 let rootelf: *i64 = sys_mmap(RP_MAXROOT * 8) as *i64
145 let rootnm: *u8 = sys_mmap(RP_MAXROOT * 64)
146 var nr: i64 = 0
147 var nosrc: i64 = 0
148
149 let tmp: *u8 = sys_mmap(RP_PATHCAP)
150 let cand: *u8 = sys_mmap(RP_PATHCAP)
151 let organ: *u8 = sys_mmap(RP_PATHCAP)
152
153 // ---- enumerate deployed elfs and pair each with its source ----
154 let db: *u8 = sys_mmap(RP_DIRBUF)
155 let dfd: i64 = sys_openat_rd(elfdir)
156 if dfd < 0 { rp_w("{\"organ\":\"nx_rebuild_plan\",\"refused\":\"elfdir unreadable\"}\n" as *u8); sys_exit(2); return 2 }
157 var more: i64 = 1
158 while more == 1 {
159 let got: i64 = sys_getdents64(dfd, db, RP_DIRBUF)
160 if got <= 0 { more = 0 } else {
161 var off: i64 = 0
162 while off < got {
163 let rec: *u8 = ((db as i64) + off) as *u8
164 let rl: i64 = dirent_reclen(rec)
165 let nm: *u8 = dirent_name(rec)
166 if rp_is_elf(nm) == 1 {
167 if nr < RP_MAXROOT {
168 let ln: i64 = rp_len(nm)
169 var k: i64 = 0
170 while k < ln - 4 { organ[k] = nm[k]; k = k + 1 }
171 organ[ln - 4] = 0 as u8
172 var o2: i64 = rp_cpy(cand, elfdir)
173 o2 = rp_cat(cand, o2, "/" as *u8)
174 o2 = rp_cat(cand, o2, nm)
175 let em: i64 = rp_mtime(cand)
176 var srcidx: i64 = 0 - 1
177 var t: i64 = rp_cpy(tmp, srcroot)
178 t = rp_cat(tmp, t, "/" as *u8)
179 t = rp_cat(tmp, t, organ)
180 t = rp_cat(tmp, t, ".nx" as *u8)
181 var sm: i64 = rp_mtime(tmp)
182 if sm < 0 {
183 t = rp_cpy(tmp, srcroot)
184 t = rp_cat(tmp, t, "/_hdl_build/" as *u8)
185 t = rp_cat(tmp, t, organ)
186 t = rp_cat(tmp, t, ".nx" as *u8)
187 sm = rp_mtime(tmp)
188 }
189 if sm < 0 { nosrc = nosrc + 1 } else {
190 if nn < RP_MAXN {
191 srcidx = nn
192 rp_cpy(((npath as i64) + nn * RP_PATHCAP) as *u8, tmp)
193 nmt[nn] = sm
194 ncmax[nn] = 0 - 1
195 ncarg[nn] = 0 - 1
196 ncol[nn] = 0
197 nes[nn] = 0
198 nec[nn] = 0 - 1
199 nn = nn + 1
200 rootn[nr] = srcidx
201 rootelf[nr] = em
202 rp_cpy(((rootnm as i64) + nr * 64) as *u8, organ)
203 nr = nr + 1
204 }
205 }
206 }
207 }
208 if rl <= 0 { off = got } else { off = off + rl }
209 }
210 }
211 }
212 sys_close(dfd)
213
214 // ---- lazily expand the import closure from those roots ----
215 let hb: *u8 = sys_mmap(RP_HEAD)
216 let imp: *u8 = sys_mmap(RP_PATHCAP)
217 let dirp: *u8 = sys_mmap(RP_PATHCAP)
218 var cur: i64 = 0
219 while cur < nn {
220 let cp: *u8 = ((npath as i64) + cur * RP_PATHCAP) as *u8
221 nes[cur] = ne
222 var cnt: i64 = 0
223 let hn: i64 = rp_readhead(cp, hb, RP_HEAD)
224 if hn > 0 {
225 var dl: i64 = rp_len(cp)
226 var slash: i64 = 0 - 1
227 var z: i64 = 0
228 while z < dl { if cp[z] == (47 as u8) { slash = z } z = z + 1 }
229 var q: i64 = 0
230 while q < slash { dirp[q] = cp[q]; q = q + 1 }
231 if slash > 0 { dirp[slash] = 0 as u8 } else { dirp[0] = 46 as u8; dirp[1] = 0 as u8 }
232 var i: i64 = 0
233 while i < hn {
234 var le: i64 = i
235 var sc: i64 = 1
236 while sc == 1 { if le >= hn { sc = 0 } else { if hb[le] == (10 as u8) { sc = 0 } else { le = le + 1 } } }
237 if rp_import_of(hb, i, le, imp, RP_PATHCAP) == 1 {
238 var found: i64 = 0 - 1
239 var c: i64 = rp_cpy(cand, dirp)
240 c = rp_cat(cand, c, "/" as *u8)
241 c = rp_cat(cand, c, imp)
242 var m: i64 = rp_mtime(cand)
243 if m < 0 {
244 c = rp_cpy(cand, srcroot)
245 c = rp_cat(cand, c, "/" as *u8)
246 c = rp_cat(cand, c, imp)
247 m = rp_mtime(cand)
248 }
249 if m < 0 {
250 c = rp_cpy(cand, srcroot)
251 c = rp_cat(cand, c, "/_hdl_build/" as *u8)
252 c = rp_cat(cand, c, imp)
253 m = rp_mtime(cand)
254 }
255 if m >= 0 {
256 var s: i64 = 0
257 while s < nn { if rp_eq(((npath as i64) + s * RP_PATHCAP) as *u8, cand) == 1 { found = s; s = nn } else { s = s + 1 } }
258 if found < 0 {
259 if nn < RP_MAXN {
260 found = nn
261 rp_cpy(((npath as i64) + nn * RP_PATHCAP) as *u8, cand)
262 nmt[nn] = m
263 ncmax[nn] = 0 - 1
264 ncarg[nn] = 0 - 1
265 ncol[nn] = 0
266 nes[nn] = 0
267 nec[nn] = 0 - 1
268 nn = nn + 1
269 }
270 }
271 if found >= 0 {
272 if ne < RP_MAXE { edges[ne] = found; ne = ne + 1; cnt = cnt + 1 }
273 }
274 }
275 }
276 i = le + 1
277 }
278 }
279 nec[cur] = cnt
280 cur = cur + 1
281 }
282
283 // ---- memoised closure max-mtime + argmax, cycle-safe ----
284 var pass: i64 = 0
285 while pass < RP_MAXN {
286 var changed: i64 = 0
287 var v: i64 = 0
288 while v < nn {
289 var best: i64 = nmt[v]
290 var barg: i64 = v
291 if ncmax[v] > best { best = ncmax[v]; barg = ncarg[v] }
292 var k: i64 = 0
293 let st: i64 = nes[v]
294 let cc: i64 = nec[v]
295 while k < cc {
296 let ch: i64 = edges[st + k]
297 var cm: i64 = nmt[ch]
298 var ca: i64 = ch
299 if ncmax[ch] > cm { cm = ncmax[ch]; ca = ncarg[ch] }
300 if cm > best { best = cm; barg = ca }
301 k = k + 1
302 }
303 if best > ncmax[v] { ncmax[v] = best; ncarg[v] = barg; changed = 1 }
304 v = v + 1
305 }
306 if changed == 0 { pass = RP_MAXN } else { pass = pass + 1 }
307 }
308
309 // ---- verdicts ----
310 var cur_ok: i64 = 0
311 var st_own: i64 = 0
312 var st_dep: i64 = 0
313 var shown: i64 = 0
314 let tally: *i64 = sys_mmap(RP_MAXN * 8) as *i64
315 if listmode == 0 { rp_w("{\"organ\":\"nx_rebuild_plan\",\"rows\":[" as *u8) }
316 var first: i64 = 1
317 var r: i64 = 0
318 while r < nr {
319 let nd: i64 = rootn[r]
320 let em: i64 = rootelf[r]
321 var cm: i64 = nmt[nd]
322 var ca: i64 = nd
323 if ncmax[nd] > cm { cm = ncmax[nd]; ca = ncarg[nd] }
324 if cm > em {
325 var isown: i64 = 0
326 if ca == nd { isown = 1 }
327 if isown == 1 { st_own = st_own + 1 } else { st_dep = st_dep + 1 }
328 tally[ca] = tally[ca] + 1
329 if listmode == 1 {
330 rp_w(((rootnm as i64) + r * 64) as *u8)
331 rp_w("\n" as *u8)
332 shown = shown + 1
333 }
334 if listmode == 0 { if shown < maxrows {
335 shown = shown + 1
336 if first == 0 { rp_w("," as *u8) }
337 first = 0
338 rp_w("{\"organ\":\"" as *u8); rp_w(((rootnm as i64) + r * 64) as *u8)
339 if isown == 1 { rp_w("\",\"verdict\":\"STALE-BY-OWN\",\"trigger\":\"" as *u8) } else { rp_w("\",\"verdict\":\"STALE-BY-DEP\",\"trigger\":\"" as *u8) }
340 rp_w(((npath as i64) + ca * RP_PATHCAP) as *u8)
341 rp_w("\",\"age_min\":" as *u8); rp_n((cm - em) / RP_SEC_PER_MIN)
342 rp_w("}" as *u8)
343 } }
344 } else { cur_ok = cur_ok + 1 }
345 r = r + 1
346 }
347 if listmode == 1 { return 0 }
348 rp_w("],\"trigger_histogram\":[" as *u8)
349 // ★★★THE HISTOGRAM IS THE WORK ORDER. 662 rows of "stale" is noise; "662 organs are waiting on FOUR
350 // files" is a decision. Ranking triggers by how many organs each one blocks turns a census into a
351 // build order, and it is the only view that shows a shared-runtime edit for what it is: one cause
352 // with hundreds of consequences, not hundreds of independent problems.
353 var hfirst: i64 = 1
354 var hshown: i64 = 0
355 var guard: i64 = 0
356 while guard < RP_TOP_TRIGGERS {
357 var bi: i64 = 0 - 1
358 var bc: i64 = 0
359 var t: i64 = 0
360 while t < nn {
361 if tally[t] > bc { bc = tally[t]; bi = t }
362 t = t + 1
363 }
364 if bi < 0 { guard = RP_TOP_TRIGGERS } else {
365 if hfirst == 0 { rp_w("," as *u8) }
366 hfirst = 0
367 hshown = hshown + 1
368 rp_w("{\"trigger\":\"" as *u8); rp_w(((npath as i64) + bi * RP_PATHCAP) as *u8)
369 rp_w("\",\"blocks_organs\":" as *u8); rp_n(bc)
370 rp_w("}" as *u8)
371 tally[bi] = 0
372 guard = guard + 1
373 }
374 }
375 rp_w("],\"triggers_shown\":" as *u8); rp_n(hshown)
376 rp_w(",\"deployed_with_source\":" as *u8); rp_n(nr)
377 rp_w(",\"no_source\":" as *u8); rp_n(nosrc)
378 rp_w(",\"current\":" as *u8); rp_n(cur_ok)
379 rp_w(",\"stale_by_own_source\":" as *u8); rp_n(st_own)
380 rp_w(",\"stale_by_shared_dep\":" as *u8); rp_n(st_dep)
381 rp_w(",\"closure_nodes\":" as *u8); rp_n(nn)
382 rp_w(",\"closure_edges\":" as *u8); rp_n(ne)
383 rp_w(",\"rows_shown\":" as *u8); rp_n(shown)
384 if shown < st_own + st_dep { rp_w(",\"rows_TRUNCATED\":true" as *u8) } else { rp_w(",\"rows_TRUNCATED\":false" as *u8) }
385 rp_w(",\"note\":\"mtime closure is a CANDIDATE ruler: it proves a dependency moved, not that codegen changed. nx_stale_check <target> rebuilds and byte-compares to confirm one target. A fresh build FAR SMALLER than the deployed elf is the _cli/stub trap -- do NOT restage it.\"}\n" as *u8)
386 return 0
387}