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}