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}