code wiki / _hdl_build / nx_ws_index_lib.nx

nx_ws_index_lib.nx source

↩ module page · 282 lines · 11878 B

1// nx_ws_index_lib.nx -- WMS durability LIB: derive the LIVE workstream auto-index from the 2// memory dir (the actual live source of workstream state) so the resume manifest + a publish 3// gate can NEVER silently go stale. This is the missing "regenerate-from-source" half the 4// operator named: every project-*.md becomes a manifest entry by construction, so nothing can 5// be dropped -- and the count-on-disk vs count-in-manifest comparison turns "we lost a 6// workstream" into a mechanical RED (the loss-detector), never a silent gap. 7// 8// REUSE / lineage: the getdents64 + newfstatat directory walk is the proven nx_sov_tree_audit 9// idiom (ta_getdents/ta_isdir); atomic publish = sys_openat_wr(tmp) + sys_renameat(tmp,live) 10// (the ss_commit / cst_write_atomic hot-swap discipline). Sovereign: imports only nx_syscalls. 11// license_tier: ORIGINAL 12import "nx_syscalls.nx" 13const WME_MAGIC_65536: i64 = 65536 14 15// the delimited block this lib owns inside the manifest. Everything ABOVE WME_BEGIN is curated 16// and preserved verbatim; the block from WME_BEGIN to EOF is regenerated each run. 17const WME_BEGIN: *u8 = "<!-- WS-AUTO-INDEX:BEGIN (nx_ws_manifest_emit -- generated, do not hand-edit below) -->" 18const WME_END: *u8 = "<!-- WS-AUTO-INDEX:END -->" 19const WME_MAXF: i64 = 8192 // max workstream records (382 today; headroom) 20const WME_SLOT: i64 = 320 // per-name / per-path byte slot 21const WME_FBUF: i64 = 1048576 // 1MB: read the existing manifest (curated prefix; ~25KB today) 22const WME_OUTBUF: i64 = 1048576 // 1MB: assembled output (bump-mmap is a finite arena -- stay lean) 23 24func wme_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 25 26// raw getdents64 (x86_64 nr 217) -- the proven nx_sov_tree_audit shape. 27func wme_getdents(fd: i64, buf: *u8, count: i64) -> i64 { return __syscall(217, fd, buf, count, 0, 0, 0) } 28 29// name starts with "project-" AND ends with ".md" -> a workstream record. 30func wme_is_project(name: *u8) -> i64 { 31 let pre: *u8 = "project-" as *u8 32 var i: i64 = 0 33 while pre[i] != (0 as u8) { if name[i] != pre[i] { return 0 } i = i + 1 } 34 let n: i64 = wme_len(name) 35 if n < 11 { return 0 } 36 if name[n - 3] != (46 as u8) { return 0 } // '.' 37 if name[n - 2] != (109 as u8) { return 0 } // 'm' 38 if name[n - 1] != (100 as u8) { return 0 } // 'd' 39 return 1 40} 41 42// dir + "/" + name -> dst (NUL-terminated). returns length. 43func wme_join(dst: *u8, dir: *u8, name: *u8) -> i64 { 44 var o: i64 = 0 45 var i: i64 = 0 46 while dir[i] != (0 as u8) { dst[o] = dir[i]; o = o + 1; i = i + 1 } 47 dst[o] = 47 as u8; o = o + 1 48 i = 0 49 while name[i] != (0 as u8) { dst[o] = name[i]; o = o + 1; i = i + 1 } 50 dst[o] = 0 as u8 51 return o 52} 53 54// bounded whole-file read into buf -> length, or -1 if open fails. (reused buffer; not sys_read_file.) 55func wme_read(path: *u8, buf: *u8, cap: i64) -> i64 { 56 let fd: i64 = sys_openat_rd(path) 57 if fd < 0 { return 0 - 1 } 58 var total: i64 = 0 59 var nrd: i64 = sys_read(fd, buf, cap) 60 while nrd > 0 { 61 total = total + nrd 62 if total >= cap { nrd = 0 } else { nrd = sys_read(fd, ((buf as i64) + total) as *u8, cap - total) } 63 } 64 sys_close(fd) 65 return total 66} 67 68// first index of NUL-term `pat` in buf[0..n), or -1. 69func wme_find(buf: *u8, n: i64, pat: *u8) -> i64 { 70 let pl: i64 = wme_len(pat) 71 if pl == 0 { return 0 - 1 } 72 var i: i64 = 0 73 while i + pl <= n { 74 var j: i64 = 0 75 var ok: i64 = 1 76 while j < pl { if buf[i + j] != pat[j] { ok = 0; j = pl } else { j = j + 1 } } 77 if ok == 1 { return i } 78 i = i + 1 79 } 80 return 0 - 1 81} 82 83func wme_cat(buf: *u8, off: i64, s: *u8) -> i64 { 84 var o: i64 = off 85 var i: i64 = 0 86 while s[i] != (0 as u8) { buf[o] = s[i]; o = o + 1; i = i + 1 } 87 return o 88} 89 90func wme_catn(buf: *u8, off: i64, v: i64) -> i64 { 91 var o: i64 = off 92 var m: i64 = v 93 if m == 0 { buf[o] = 48 as u8; return o + 1 } 94 if m < 0 { buf[o] = 45 as u8; o = o + 1; m = 0 - m } 95 let t: *u8 = sys_mmap(28) 96 var k: i64 = 0 97 while m > 0 { t[k] = ((48 + (m % 10)) as u8); m = m / 10; k = k + 1 } 98 var i: i64 = 0 99 while i < k { buf[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 100 return o 101} 102 103// SCAN memdir for every project-*.md -> names[] (NUL-term ptrs) + mtimes[] (st_mtim.tv_sec). 104// returns count, or -1 if memdir cannot be opened. ss-style snapshot: one getdents stream. 105func wme_scan(memdir: *u8, names: *i64, mtimes: *i64, cap: i64) -> i64 { 106 let fd: i64 = sys_openat_rd(memdir) 107 if fd < 0 { return 0 - 1 } 108 let gbuf: *u8 = sys_mmap(WME_MAGIC_65536) 109 let stbuf: *u8 = sys_mmap(256) 110 let child: *u8 = sys_mmap(WME_SLOT * 2) 111 var cnt: i64 = 0 112 var nread: i64 = wme_getdents(fd, gbuf, WME_MAGIC_65536) 113 while nread > 0 { 114 var off: i64 = 0 115 while off < nread { 116 let reclen: i64 = (gbuf[off + 16] as i64) | ((gbuf[off + 17] as i64) << 8) 117 if reclen <= 0 { off = nread } else { 118 let name: *u8 = ((gbuf as i64) + off + 19) as *u8 119 if wme_is_project(name) == 1 { 120 if cnt < cap { 121 let nb: *u8 = sys_mmap(WME_SLOT) 122 var c: i64 = 0 123 while name[c] != (0 as u8) { nb[c] = name[c]; c = c + 1 } 124 nb[c] = 0 as u8 125 wme_join(child, memdir, name) 126 var mt: i64 = 0 127 if sys_fstatat(child, stbuf) == 0 { 128 // st_mtim.tv_sec @ offset 88 (low 6 bytes ample for any real epoch) 129 mt = (stbuf[88] as i64) | ((stbuf[89] as i64) << 8) | ((stbuf[90] as i64) << 16) | ((stbuf[91] as i64) << 24) | ((stbuf[92] as i64) << 32) | ((stbuf[93] as i64) << 40) 130 } 131 names[cnt] = nb as i64 132 mtimes[cnt] = mt 133 cnt = cnt + 1 134 } 135 } 136 off = off + reclen 137 } 138 } 139 nread = wme_getdents(fd, gbuf, WME_MAGIC_65536) 140 } 141 sys_close(fd) 142 return cnt 143} 144 145// count project-*.md on disk (the ground truth for the loss-detector). 146func wme_count_disk(memdir: *u8) -> i64 { 147 let names: *i64 = sys_mmap(8 * WME_MAXF) as *i64 148 let mtimes: *i64 = sys_mmap(8 * WME_MAXF) as *i64 149 return wme_scan(memdir, names, mtimes, WME_MAXF) 150} 151 152// count "- [[" entries inside the BEGIN..END auto-block of `manifest`. -1 if no block. 153func wme_count_manifest(manifest: *u8) -> i64 { 154 let fbuf: *u8 = sys_mmap(WME_FBUF) 155 let flen: i64 = wme_read(manifest, fbuf, WME_FBUF) 156 if flen <= 0 { return 0 - 1 } 157 let b: i64 = wme_find(fbuf, flen, WME_BEGIN) 158 if b < 0 { return 0 - 1 } 159 var endpos: i64 = flen 160 let e: i64 = wme_find(fbuf, flen, WME_END) 161 if e >= 0 { endpos = e } 162 let pat: *u8 = "- [[" as *u8 163 let pl: i64 = wme_len(pat) 164 var cnt: i64 = 0 165 var i: i64 = b 166 while i + pl <= endpos { 167 var j: i64 = 0 168 var ok: i64 = 1 169 while j < pl { if fbuf[i + j] != pat[j] { ok = 0; j = pl } else { j = j + 1 } } 170 if ok == 1 { cnt = cnt + 1; i = i + pl } else { i = i + 1 } 171 } 172 return cnt 173} 174 175// the loss-detector: 0 = manifest auto-block matches disk (FRESH); 1 = STALE/lossy (counts differ 176// or block absent). This is what the publish gate refuses on. 177func wme_is_stale(memdir: *u8, manifest: *u8) -> i64 { 178 let d: i64 = wme_count_disk(memdir) 179 let m: i64 = wme_count_manifest(manifest) 180 if d < 0 { return 1 } 181 if m < 0 { return 1 } 182 if d != m { return 1 } 183 return 0 184} 185 186func wme_write_all(fd: i64, buf: *u8, n: i64) -> i64 { 187 var done: i64 = 0 188 while done < n { 189 let w: i64 = sys_write(fd, ((buf as i64) + done) as *u8, n - done) 190 if w <= 0 { return done } 191 done = done + w 192 } 193 return done 194} 195 196// THE EMITTER: regenerate the WS-AUTO-INDEX block in `manifest` from `memdir`, newest-first. 197// Curated content above WME_BEGIN is preserved verbatim. Atomic: write <manifest>.tmp then 198// renameat over the live file (a concurrent reader sees old-whole or new-whole, never torn). 199// returns the number of workstream records indexed, or a negative error code. 200func wme_emit(memdir: *u8, manifest: *u8) -> i64 { 201 let names: *i64 = sys_mmap(8 * WME_MAXF) as *i64 202 let mtimes: *i64 = sys_mmap(8 * WME_MAXF) as *i64 203 let cnt: i64 = wme_scan(memdir, names, mtimes, WME_MAXF) 204 if cnt < 0 { return 0 - 1 } 205 206 // insertion sort by mtime DESC (newest workstream first). ~382 items -> trivial. 207 var a: i64 = 1 208 while a < cnt { 209 let mv: i64 = mtimes[a] 210 let nv: i64 = names[a] 211 var b: i64 = a - 1 212 var go: i64 = 1 213 while go == 1 { 214 if b < 0 { go = 0 } else { 215 if mtimes[b] < mv { mtimes[b + 1] = mtimes[b]; names[b + 1] = names[b]; b = b - 1 } else { go = 0 } 216 } 217 } 218 mtimes[b + 1] = mv 219 names[b + 1] = nv 220 a = a + 1 221 } 222 223 // read existing manifest -> curated prefix is everything before WME_BEGIN (or the whole file). 224 let fbuf: *u8 = sys_mmap(WME_FBUF) 225 let flen: i64 = wme_read(manifest, fbuf, WME_FBUF) 226 // ROOT FIX 2026-07-31 DATA-LOSS INCIDENT: wme_read returns -1 when the OPEN fails, and the old 227 // code left prefixlen=0 in that case -- so a FAILED READ silently meant -no curated prefix- and 228 // the emitter wrote a GENERATED-ONLY file, destroying ~955 lines of hand-authored crash-recovery 229 // content in workstream-resume-manifest.md. No backup, no git. 230 // LAW: A GENERATOR THAT CANNOT READ THE FILE IT SPLICES INTO MUST REFUSE TO WRITE IT. 231 // A failed read is NOT evidence of an empty prefix -- it is evidence of NOTHING. 232 // The correct idiom already existed ~70 lines above in wme_count; the COUNTING path was 233 // fail-closed while the DESTRUCTIVE path was fail-open. 234 // REFUSE ONLY WHEN THE FILE EXISTS BUT IS UNREADABLE -- an ABSENT manifest is the legitimate 235 // FIRST-EMIT case and must still create the file. ABSENT and UNREADABLE are OPPOSITE situations 236 // with OPPOSITE correct responses; stat separates them, open-for-read cannot. 237 if flen <= 0 { 238 let stb: *u8 = sys_mmap(160) 239 if sys_fstatat(manifest, stb) == 0 { return 0 - 9 } 240 } 241 var prefixlen: i64 = 0 242 if flen > 0 { 243 let at: i64 = wme_find(fbuf, flen, WME_BEGIN) 244 if at >= 0 { prefixlen = at } else { prefixlen = flen } 245 } 246 247 let out: *u8 = sys_mmap(WME_OUTBUF) 248 var o: i64 = 0 249 var i: i64 = 0 250 while i < prefixlen { out[o] = fbuf[i]; o = o + 1; i = i + 1 } 251 if o > 0 { if out[o - 1] != (10 as u8) { out[o] = 10 as u8; o = o + 1 } } 252 o = wme_cat(out, o, WME_BEGIN) 253 o = wme_cat(out, o, "\n## LIVE WORKSTREAM AUTO-INDEX -- every project-*.md, newest-first. Regenerated by nx_ws_manifest_emit (no hand-edit).\n\n" as *u8) 254 var k: i64 = 0 255 while k < cnt { 256 o = wme_cat(out, o, "- [[" as *u8) 257 let nm: *u8 = names[k] as *u8 258 let nl: i64 = wme_len(nm) 259 var j: i64 = 0 260 while j < nl - 3 { out[o] = nm[j]; o = o + 1; j = j + 1 } // strip ".md" 261 o = wme_cat(out, o, "]]\n" as *u8) 262 k = k + 1 263 } 264 o = wme_cat(out, o, "\nTOTAL workstream records: " as *u8) 265 o = wme_catn(out, o, cnt) 266 o = wme_cat(out, o, "\n" as *u8) 267 o = wme_cat(out, o, WME_END) 268 o = wme_cat(out, o, "\n" as *u8) 269 out[o] = 0 as u8 270 271 // write tmp then atomic rename 272 let tmp: *u8 = sys_mmap(WME_SLOT * 2) 273 var t: i64 = wme_cat(tmp, 0, manifest) 274 t = wme_cat(tmp, t, ".tmp" as *u8) 275 tmp[t] = 0 as u8 276 let wfd: i64 = sys_openat_wr(tmp, 420) // 0644 277 if wfd < 0 { return 0 - 2 } 278 wme_write_all(wfd, out, o) 279 sys_close(wfd) 280 if sys_renameat(tmp, manifest) != 0 { return 0 - 3 } 281 return cnt 282}