code wiki / _hdl_build / nx_symdrop.nx
nx_symdrop.nx source
↩ module page · 776 lines · 37029 B
1// nx_symdrop.nx -- catch SILENT SYMBOL LOSS in shared sources (seq1407 root fix).
2//
3// THE DEFECT CLASS, witnessed three times: a session lands a source change, verifies it LIVE, and a
4// sibling later writes back a stale WHOLE-FILE copy that erases it. The running binary keeps serving
5// the fix, so every health check stays green -- and the revert only detonates at the NEXT rebuild,
6// silently restoring the old behaviour with NO failing test. My own seq1318 supervisor fix was erased
7// this way 25 minutes after I proved it live (diff against my pre-edit original: ZERO lines = a pure
8// stale-copy write-back, not a competing edit).
9//
10// WHY THIS SHAPE: a fix that must be REMEMBERED at N write sites is not a fix -- and the writes arrive
11// through many doors (nx_fs_write, ssh redirects, treepack unpack, cron rewrites). So bind the check to
12// the one property they all share: after any of them, the FILE'S SYMBOL SET either kept everything it
13// had, or something was removed. Removal is the signal. Additions are normal and ignored.
14//
15// FAIL-CLOSED, and deliberately NOT auto-healing: an unreadable/vanished watched file is itself the
16// defect, so it is reported RED rather than skipped; and nothing here ever rewrites a source. A real
17// removal is legitimised by an explicit `bless`, which makes an intentional deletion a deliberate,
18// recorded act instead of an indistinguishable accident.
19//
20// nx_symdrop check [watchconf] [snapfile] -- compare live symbols vs the snapshot; exit 1 on ANY drop
21// nx_symdrop bless [watchconf] [snapfile] -- accept current symbols as the new baseline
22// nx_symdrop list [watchconf] -- show what is watched and each file's symbol count
23// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
24import "nx_syscalls.nx"
25import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
26
27const SD_SRCCAP: i64 = 2097152 // largest guarded source (nx_hostctl.nx measured ~281KB) with headroom
28const SD_SYMCAP: i64 = 1048576 // symbol csv per file
29const SD_SNAPCAP: i64 = 8388608 // whole snapshot file
30const SD_CONFCAP: i64 = 65536
31const SD_PATHCAP: i64 = 1024
32const SD_NAMECAP: i64 = 256
33const SD_NL: i64 = 10
34const SD_TAB: i64 = 9
35const SD_COMMA: i64 = 44
36const SD_HASH: i64 = 35
37const SD_SPACE: i64 = 32
38const SD_STDOUT: i64 = 1
39const SD_STDERR: i64 = 2
40const SD_MODE: i64 = 420
41const SD_EXIT_DROP: i64 = 1
42const SD_EXIT_USAGE: i64 = 2
43const SD_EXIT_REFUSED: i64 = 3
44const SD_EXIT_IO: i64 = 4
45const SD_DEF_CONF: *u8 = "knowledge/symdrop_watch.conf" as *u8
46const SD_DEF_SNAP: *u8 = "knowledge/symdrop_snapshot.tsv" as *u8
47const SD_DEF_HIST: *u8 = "knowledge/symdrop_history.tsv" as *u8
48const SD_FNV_OFF: i64 = 1469598103934665603
49const SD_FNV_PRIME: i64 = 1099511628211
50
51// ---- THE PRIMARY DETECTOR: DID THIS FILE GO BACKWARDS? --------------------------------------------
52// v1 of this organ watched the top-level SYMBOL SET and it was HOLLOW for the very event that motivated
53// it: the seq1407 revert erased an `import`, a function BODY, and a CALL -- the declaration set was
54// byte-for-byte identical before and after, so a symbol guard is structurally blind to it. Replaying the
55// real event proved that, after the fixture-based gate had already gone green. (★the tooth that matters
56// is the one aimed at the ACTUAL event, not the one the implementation makes easy.)
57//
58// The exact invariant that separates a revert from ordinary editing needs no parsing at all:
59// a normal edit moves the file to a state it has NEVER been in;
60// a stale write-back returns it to a state it ALREADY HAD.
61// So keep an append-only history of content hashes per file. Current hash == newest ⇒ unchanged. Hash
62// never seen ⇒ ordinary edit, record it, stay silent. Hash seen EARLIER but not newest ⇒ THE FILE WENT
63// BACKWARDS -- alarm, and name the generation it fell back to. Zero false positives on normal work, and
64// it catches reverts in bodies, calls, imports and comments alike. A deliberate revert is legitimised by
65// `bless`, which is the point: undoing someone's landed work should be an explicit, recorded act.
66// Hash = FNV-1a 64-bit: not cryptographic, and not claimed to be -- an adversary is not the threat model
67// here, an accidental stale copy is, and for that a 64-bit content fingerprint is decisive.
68func sd_fnv1a(b: *u8, n: i64) -> i64 {
69 var h: i64 = SD_FNV_OFF
70 var i: i64 = 0
71 while i < n {
72 h = h ^ (b[i] as i64)
73 h = h * SD_FNV_PRIME
74 i = i + 1
75 }
76 return h
77}
78
79func sd_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
80func sd_puts(s: *u8) -> i64 { sys_write(SD_STDOUT, s, sd_slen(s)); return 0 }
81func sd_werr(s: *u8) -> i64 { sys_write(SD_STDERR, s, sd_slen(s)); return 0 }
82// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
83// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
84// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
85// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
86func sd_putn(v: i64) -> i64 { nxi_out(v); return 0 }
87func sd_eqs(a: *u8, b: *u8) -> i64 {
88 var i: i64 = 0
89 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
90 if b[i] != (0 as u8) { return 0 }
91 return 1
92}
93func sd_read(path: *u8, b: *u8, cap: i64) -> i64 {
94 let fd: i64 = sys_openat_rd(path)
95 if fd < 0 { return 0 - 1 }
96 var n: i64 = 0
97 var go: i64 = 1
98 while go == 1 {
99 let r: i64 = sys_read(fd, (b as i64 + n) as *u8, cap - n)
100 if r > 0 { n = n + r } else { go = 0 }
101 if n >= cap { go = 0 }
102 }
103 sys_close(fd)
104 return n
105}
106func sd_write(path: *u8, b: *u8, n: i64) -> i64 {
107 let fd: i64 = sys_openat_wr(path, SD_MODE)
108 if fd < 0 { return 0 - 1 }
109 sys_write(fd, b, n)
110 sys_fsync(fd)
111 sys_close(fd)
112 return 0
113}
114// does buf[at..] begin with lit?
115func sd_starts(buf: *u8, at: i64, n: i64, lit: *u8) -> i64 {
116 let l: i64 = sd_slen(lit)
117 if at + l > n { return 0 }
118 var i: i64 = 0
119 while i < l { if buf[at+i] != lit[i] { return 0 } i = i + 1 }
120 return 1
121}
122// identifier char? [A-Za-z0-9_]
123func sd_identch(c: i64) -> i64 {
124 if c >= 48 { if c <= 57 { return 1 } }
125 if c >= 65 { if c <= 90 { return 1 } }
126 if c >= 97 { if c <= 122 { return 1 } }
127 if c == 95 { return 1 }
128 return 0
129}
130
131// Extract TOP-LEVEL declared symbols from src into out as a comma-separated list; returns length.
132// Column-0 only: an indented `func` is a nested/forward form and a commented one starts with '/'. We
133// take func/const/static/struct -- the four declaration forms whose disappearance is a real capability
134// loss. Order follows the file, so the comparison below is order-insensitive by construction (it is a
135// SET test: for each old symbol, is it still present anywhere).
136func sd_symbols(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
137 var o: i64 = 0
138 var ls: i64 = 0
139 var i: i64 = 0
140 while i <= n {
141 var eol: i64 = 0
142 if i == n { eol = 1 } else { if src[i] == (SD_NL as u8) { eol = 1 } }
143 if eol == 1 {
144 if i > ls {
145 var kw: i64 = 0
146 if sd_starts(src, ls, n, "func " as *u8) == 1 { kw = 5 }
147 if sd_starts(src, ls, n, "const " as *u8) == 1 { kw = 6 }
148 if sd_starts(src, ls, n, "static " as *u8) == 1 { kw = 7 }
149 if sd_starts(src, ls, n, "struct " as *u8) == 1 { kw = 7 }
150 if kw > 0 {
151 // skip spaces between keyword and name (`const NAME` is legal), then take the
152 // identifier run. Both scans are explicit while-loops with a single exit flag --
153 // no clever index arithmetic, because this function decides whether a symbol is
154 // considered LOST, and a subtle off-by-one here would either cry wolf forever or
155 // stay silent on a real revert.
156 var st: i64 = ls + kw
157 var sgo: i64 = 1
158 while sgo == 1 {
159 if st < i { if src[st] == (SD_SPACE as u8) { st = st + 1 } else { sgo = 0 } } else { sgo = 0 }
160 }
161 var e: i64 = st
162 var go: i64 = 1
163 while go == 1 {
164 if e < i { if sd_identch(src[e] as i64) == 1 { e = e + 1 } else { go = 0 } } else { go = 0 }
165 }
166 if e > st {
167 if o + (e - st) + 2 < cap {
168 if o > 0 { out[o] = SD_COMMA as u8; o = o + 1 }
169 var c: i64 = st
170 while c < e { out[o] = src[c]; o = o + 1; c = c + 1 }
171 }
172 }
173 }
174 }
175 ls = i + 1
176 }
177 i = i + 1
178 }
179 out[o] = 0 as u8
180 return o
181}
182// is `name` (len nl) present as a whole element of the comma list csv[0..cn)?
183func sd_has(csv: *u8, cn: i64, name: *u8, nl: i64) -> i64 {
184 var s: i64 = 0
185 var i: i64 = 0
186 while i <= cn {
187 var sep: i64 = 0
188 if i == cn { sep = 1 } else { if csv[i] == (SD_COMMA as u8) { sep = 1 } }
189 if sep == 1 {
190 if i - s == nl {
191 var m: i64 = 1
192 var c: i64 = 0
193 while c < nl { if csv[s+c] != name[c] { m = 0; c = nl } else { c = c + 1 } }
194 if m == 1 { return 1 }
195 }
196 s = i + 1
197 }
198 i = i + 1
199 }
200 return 0
201}
202// Walk the append-only history for `path`. out3[0]=generation count seen, out3[1]=1 if `h` equals the
203// NEWEST recorded hash, out3[2]=the 0-based generation index where `h` was seen EARLIER (-1 if never).
204// History line format: <path>\t<decimal hash>
205func sd_hist_probe(hist: *u8, hn: i64, path: *u8, h: i64, out3: *i64) -> i64 {
206 let pl: i64 = sd_slen(path)
207 out3[0] = 0
208 out3[1] = 0
209 out3[2] = 0 - 1
210 var gen: i64 = 0
211 var ls: i64 = 0
212 var i: i64 = 0
213 while i <= hn {
214 var eol: i64 = 0
215 if i == hn { eol = 1 } else { if hist[i] == (SD_NL as u8) { eol = 1 } }
216 if eol == 1 {
217 if i > ls {
218 var t: i64 = ls
219 var tab: i64 = 0 - 1
220 while t < i { if hist[t] == (SD_TAB as u8) { tab = t; t = i } else { t = t + 1 } }
221 if tab > 0 { if tab - ls == pl {
222 var m: i64 = 1
223 var c: i64 = 0
224 while c < pl { if hist[ls+c] != path[c] { m = 0; c = pl } else { c = c + 1 } }
225 if m == 1 {
226 var v: i64 = 0
227 var neg: i64 = 0
228 var k: i64 = tab + 1
229 if k < i { if hist[k] == (45 as u8) { neg = 1; k = k + 1 } }
230 while k < i { v = v * 10 + ((hist[k] as i64) - 48); k = k + 1 }
231 if neg == 1 { v = 0 - v }
232 if v == h { out3[2] = gen; out3[1] = 1 } else { out3[1] = 0 }
233 gen = gen + 1
234 }
235 } }
236 }
237 ls = i + 1
238 }
239 i = i + 1
240 }
241 out3[0] = gen
242 return gen
243}
244// format "<path>\t<hash>\n" into dst at off; returns the new offset. Split out from the appender so a
245// bless can STAGE its generations in memory and commit them only AFTER the refusal check passes --
246// otherwise a refused bless would still have polluted the history it was refused for touching.
247func sd_hist_line(dst: *u8, off: i64, path: *u8, h: i64) -> i64 { return sd_hist_line_b(dst, off, path, h, 0 as *u8, 0) }
248func sd_hist_line_b(dst: *u8, off: i64, path: *u8, h: i64, bodies: *u8, bn: i64) -> i64 {
249 var o: i64 = off
250 var i: i64 = 0
251 while path[i] != (0 as u8) { dst[o] = path[i]; o = o + 1; i = i + 1 }
252 dst[o] = SD_TAB as u8; o = o + 1
253 var m: i64 = h
254 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m }
255 let t: *u8 = sys_mmap(32)
256 var k: i64 = 0
257 if m == 0 { t[0] = 48 as u8; k = 1 }
258 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
259 var z: i64 = k - 1
260 while z >= 0 { dst[o] = t[z]; o = o + 1; z = z - 1 }
261 if bn > 0 { dst[o] = SD_TAB as u8; o = o + 1; var q: i64 = 0; while q < bn { dst[o] = bodies[q]; o = o + 1; q = q + 1 } }
262 dst[o] = SD_NL as u8; o = o + 1
263 return o
264}
265// append bytes to the history file (append-only; history is never rewritten)
266func sd_hist_write(histp: *u8, buf: *u8, n: i64) -> i64 {
267 if n <= 0 { return 0 }
268 let fd: i64 = sys_openat_append(histp, SD_MODE)
269 if fd < 0 { return 0 - 1 }
270 sys_write(fd, buf, n)
271 sys_fsync(fd)
272 sys_close(fd)
273 return 0
274}
275func sd_hist_append(histp: *u8, path: *u8, h: i64) -> i64 {
276 let line: *u8 = sys_mmap(SD_PATHCAP + 64)
277 let o: i64 = sd_hist_line(line, 0, path, h)
278 return sd_hist_write(histp, line, o)
279}
280
281// ---- seq1546: PER-DECLARATION BODY HASHES -- catching capability SUBSTITUTION ---------------------
282// The symbol-set rule catches capability DELETION. It is blind to SUBSTITUTION, and that blindness let
283// my own seq1318 supervisor fix be erased a FOURTH time while this organ reported reverted=0
284// symbols_dropped=0 -- and I blessed it, baking the reverted state into the baseline. The revert swapped
285// a function BODY (a one-line delegation back to the original 38-line inline scan) and restored the
286// import, so the DECLARED set was identical on both sides and nothing looked dropped. The whole-file
287// hash could not save it either: the incoming file matched no recorded generation (a sibling edit on a
288// stale base, not an exact replay), so it read as an ordinary forward edit.
289// ★A DECLARATION SET IS A COARSE FINGERPRINT OF CAPABILITY: two files can declare identically and
290// behave oppositely. The finer fingerprint that stays content-decided is a hash PER DECLARATION.
291//
292// THE RULE, and why it cannot cry wolf: a body is REVERTED when its current hash differs from the
293// NEWEST recorded hash for that same name AND equals some EARLIER recorded hash for it. An ordinary
294// edit produces a hash never recorded before, so it is silent; only a body that has genuinely been
295// here before, and is not the latest, fires.
296const SD_BODYCAP: i64 = 1048576
297const SD_EQ: i64 = 61
298const SD_MAXGEN: i64 = 256 // generations compared per declaration; bounded, and a longer history simply compares its most recent 256
299
300// hash the bytes [s,e) of src
301func sd_bhash(src: *u8, s: i64, e: i64) -> i64 {
302 var h: i64 = SD_FNV_OFF
303 var i: i64 = s
304 while i < e { h = h ^ (src[i] as i64); h = h * SD_FNV_PRIME; i = i + 1 }
305 return h
306}
307// is line [ls,le) the start of a top-level declaration? returns keyword length, else 0
308func sd_declkw(src: *u8, ls: i64, n: i64) -> i64 {
309 if sd_starts(src, ls, n, "func " as *u8) == 1 { return 5 }
310 if sd_starts(src, ls, n, "const " as *u8) == 1 { return 6 }
311 if sd_starts(src, ls, n, "static " as *u8) == 1 { return 7 }
312 if sd_starts(src, ls, n, "struct " as *u8) == 1 { return 7 }
313 return 0
314}
315// Emit "name=hash,name=hash,..." -- one entry per TOP-LEVEL declaration, the hash covering that
316// declaration's bytes up to the next top-level declaration (or EOF). Same acceptance rule as
317// sd_symbols, so the two views can never disagree about what a declaration is.
318func sd_bodies(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
319 var o: i64 = 0
320 var cur_s: i64 = 0 - 1
321 var cur_ns: i64 = 0
322 var cur_ne: i64 = 0
323 var ls: i64 = 0
324 var i: i64 = 0
325 while i <= n {
326 var eol: i64 = 0
327 if i == n { eol = 1 } else { if src[i] == (SD_NL as u8) { eol = 1 } }
328 if eol == 1 {
329 var kw: i64 = 0
330 if i > ls { kw = sd_declkw(src, ls, n) }
331 if kw > 0 {
332 if cur_s >= 0 {
333 let h: i64 = sd_bhash(src, cur_s, ls)
334 o = sd_emit_pair(out, o, cap, src, cur_ns, cur_ne, h)
335 }
336 var st: i64 = ls + kw
337 var sgo: i64 = 1
338 while sgo == 1 {
339 if st < i { if src[st] == (SD_SPACE as u8) { st = st + 1 } else { sgo = 0 } } else { sgo = 0 }
340 }
341 var e2: i64 = st
342 var go2: i64 = 1
343 while go2 == 1 {
344 if e2 < i { if sd_identch(src[e2] as i64) == 1 { e2 = e2 + 1 } else { go2 = 0 } } else { go2 = 0 }
345 }
346 cur_s = ls
347 cur_ns = st
348 cur_ne = e2
349 }
350 ls = i + 1
351 }
352 i = i + 1
353 }
354 if cur_s >= 0 {
355 let h2: i64 = sd_bhash(src, cur_s, n)
356 o = sd_emit_pair(out, o, cap, src, cur_ns, cur_ne, h2)
357 }
358 out[o] = 0 as u8
359 return o
360}
361func sd_emit_pair(out: *u8, off: i64, cap: i64, src: *u8, ns: i64, ne: i64, h: i64) -> i64 {
362 var o: i64 = off
363 if ne <= ns { return o }
364 if o + (ne - ns) + 32 >= cap { return o }
365 if o > 0 { out[o] = SD_COMMA as u8; o = o + 1 }
366 var c: i64 = ns
367 while c < ne { out[o] = src[c]; o = o + 1; c = c + 1 }
368 out[o] = SD_EQ as u8
369 o = o + 1
370 var m: i64 = h
371 if m < 0 { out[o] = 45 as u8; o = o + 1; m = 0 - m }
372 let t: *u8 = sys_mmap(32)
373 var k: i64 = 0
374 if m == 0 { t[0] = 48 as u8; k = 1 }
375 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
376 var z: i64 = k - 1
377 while z >= 0 { out[o] = t[z]; o = o + 1; z = z - 1 }
378 return o
379}
380// look up `name` in a "name=hash,..." csv -> hash, or 0 if absent (0 is never a real FNV value here)
381func sd_body_of(csv: *u8, cn: i64, name: *u8, nl: i64) -> i64 {
382 var s: i64 = 0
383 var i: i64 = 0
384 while i <= cn {
385 var sep: i64 = 0
386 if i == cn { sep = 1 } else { if csv[i] == (SD_COMMA as u8) { sep = 1 } }
387 if sep == 1 {
388 var eq: i64 = 0 - 1
389 var k: i64 = s
390 while k < i { if csv[k] == (SD_EQ as u8) { eq = k; k = i } else { k = k + 1 } }
391 if eq > s { if eq - s == nl {
392 var m: i64 = 1
393 var c: i64 = 0
394 while c < nl { if csv[s+c] != name[c] { m = 0; c = nl } else { c = c + 1 } }
395 if m == 1 {
396 var v: i64 = 0
397 var neg: i64 = 0
398 var p: i64 = eq + 1
399 if p < i { if csv[p] == (45 as u8) { neg = 1; p = p + 1 } }
400 while p < i { v = v * 10 + ((csv[p] as i64) - 48); p = p + 1 }
401 if neg == 1 { v = 0 - v }
402 return v
403 }
404 } }
405 s = i + 1
406 }
407 i = i + 1
408 }
409 return 0
410}
411
412// History line is "<path>\t<filehash>\t<name=hash,...>". Field 3 is optional so pre-seq1546 lines
413// (two fields) still parse -- an older history simply has no bodies to compare, which degrades to the
414// previous behaviour rather than erroring.
415func sd_hist_field3(hist: *u8, ls: i64, le: i64, out2: *i64) -> i64 {
416 var t1: i64 = 0 - 1
417 var t2: i64 = 0 - 1
418 var k: i64 = ls
419 while k < le {
420 if hist[k] == (SD_TAB as u8) {
421 if t1 < 0 { t1 = k } else { if t2 < 0 { t2 = k } }
422 }
423 k = k + 1
424 }
425 if t2 < 0 { return 0 }
426 out2[0] = t2 + 1
427 out2[1] = le - t2 - 1
428 return 1
429}
430// Count declarations whose CURRENT body hash differs from the NEWEST recorded hash for that name yet
431// EQUALS an EARLIER recorded one -- i.e. bodies that have been here before and are not the latest.
432// Names the first offender into `firstnm`. An ordinary edit yields a hash never recorded, so it is silent.
433func sd_bodies_reverted(hist: *u8, hn: i64, path: *u8, cur: *u8, cn: i64, firstnm: *u8) -> i64 {
434 let pl: i64 = sd_slen(path)
435 let f2: *i64 = sys_mmap(16) as *i64
436 let nm: *u8 = sys_mmap(SD_NAMECAP)
437 var reverted: i64 = 0
438 var named: i64 = 0
439 var s: i64 = 0
440 var i: i64 = 0
441 while i <= cn {
442 var sep: i64 = 0
443 if i == cn { sep = 1 } else { if cur[i] == (SD_COMMA as u8) { sep = 1 } }
444 if sep == 1 {
445 var eq: i64 = 0 - 1
446 var k: i64 = s
447 while k < i { if cur[k] == (SD_EQ as u8) { eq = k; k = i } else { k = k + 1 } }
448 if eq > s {
449 let nl: i64 = eq - s
450 if nl > 0 { if nl < SD_NAMECAP {
451 var z: i64 = 0
452 while z < nl { nm[z] = cur[s+z]; z = z + 1 }
453 nm[nl] = 0 as u8
454 let curh: i64 = sd_body_of(cur, cn, nm, nl)
455 // Collect this name's recorded hashes oldest -> newest, then decide. Written as a
456 // list rather than running flags because the condition (differs from the LAST,
457 // equals an EARLIER) is the whole point of the tooth and must be obviously correct.
458 let gens: *i64 = sys_mmap(8 * SD_MAXGEN) as *i64
459 var ngen: i64 = 0
460 var ls: i64 = 0
461 var j: i64 = 0
462 while j <= hn {
463 var eol: i64 = 0
464 if j == hn { eol = 1 } else { if hist[j] == (SD_NL as u8) { eol = 1 } }
465 if eol == 1 {
466 if j > ls {
467 var tab: i64 = 0 - 1
468 var t: i64 = ls
469 while t < j { if hist[t] == (SD_TAB as u8) { tab = t; t = j } else { t = t + 1 } }
470 if tab > 0 { if tab - ls == pl {
471 var m: i64 = 1
472 var c: i64 = 0
473 while c < pl { if hist[ls+c] != path[c] { m = 0; c = pl } else { c = c + 1 } }
474 if m == 1 { if sd_hist_field3(hist, ls, j, f2) == 1 {
475 let bh: i64 = sd_body_of(((hist as i64) + f2[0]) as *u8, f2[1], nm, nl)
476 if bh != 0 { if ngen < SD_MAXGEN { gens[ngen] = bh; ngen = ngen + 1 } }
477 } }
478 } }
479 }
480 ls = j + 1
481 }
482 j = j + 1
483 }
484 // REVERTED iff: differs from the NEWEST recorded body, and equals an EARLIER one.
485 if ngen > 1 { if gens[ngen-1] != curh {
486 var hit: i64 = 0
487 var g: i64 = 0
488 while g < ngen - 1 { if gens[g] == curh { hit = 1; g = ngen } else { g = g + 1 } }
489 if hit == 1 {
490 reverted = reverted + 1
491 if named == 0 { var q: i64 = 0; while q <= nl { firstnm[q] = nm[q]; q = q + 1 } named = 1 }
492 }
493 } }
494 } }
495 }
496 s = i + 1
497 }
498 i = i + 1
499 }
500 return reverted
501}
502
503// fetch the snapshot line for `path` -> out2[0]=csv offset, out2[1]=csv len; 1 found / 0 absent
504func sd_snap_find(snap: *u8, sn: i64, path: *u8, out2: *i64) -> i64 {
505 let pl: i64 = sd_slen(path)
506 var ls: i64 = 0
507 var i: i64 = 0
508 while i <= sn {
509 var eol: i64 = 0
510 if i == sn { eol = 1 } else { if snap[i] == (SD_NL as u8) { eol = 1 } }
511 if eol == 1 {
512 if i > ls {
513 var t: i64 = ls
514 var found: i64 = 0 - 1
515 while t < i { if snap[t] == (SD_TAB as u8) { found = t; t = i } else { t = t + 1 } }
516 if found > 0 {
517 if found - ls == pl {
518 var m: i64 = 1
519 var c: i64 = 0
520 while c < pl { if snap[ls+c] != path[c] { m = 0; c = pl } else { c = c + 1 } }
521 if m == 1 { out2[0] = found + 1; out2[1] = i - found - 1; return 1 }
522 }
523 }
524 }
525 ls = i + 1
526 }
527 i = i + 1
528 }
529 return 0
530}
531
532func main(argc: i64, argv: *i64) -> i64 {
533 if argc < 2 { sd_werr("usage: nx_symdrop check|bless|list [watchconf] [snapfile]\n" as *u8); sys_exit(SD_EXIT_USAGE); return SD_EXIT_USAGE }
534 let verb: *u8 = argv[1] as *u8
535 // Positional args, FLAG-AWARE. Naive positional binding made `bless --force` treat "--force" as the
536 // watch-list PATH and refuse with a confusing "watch list absent/empty: --force" -- the flag silently
537 // consumed a positional slot. Flags may appear anywhere; only non-flag args take positions.
538 var confp: *u8 = SD_DEF_CONF
539 var snapp: *u8 = SD_DEF_SNAP
540 var histpos: *u8 = SD_DEF_HIST
541 var pidx: i64 = 0
542 var pa: i64 = 2
543 while pa < argc {
544 let av: *u8 = argv[pa] as *u8
545 if av[0] != (45 as u8) {
546 if pidx == 0 { confp = av }
547 if pidx == 1 { snapp = av }
548 if pidx == 2 { histpos = av }
549 pidx = pidx + 1
550 }
551 pa = pa + 1
552 }
553
554 let conf: *u8 = sys_mmap(SD_CONFCAP)
555 let cn: i64 = sd_read(confp, conf, SD_CONFCAP - 1)
556 if cn <= 0 {
557 sd_werr("REFUSED: watch list absent/empty: " as *u8); sd_werr(confp); sd_werr("\n" as *u8)
558 sys_exit(SD_EXIT_REFUSED); return SD_EXIT_REFUSED
559 }
560 let snap: *u8 = sys_mmap(SD_SNAPCAP)
561 var sn: i64 = sd_read(snapp, snap, SD_SNAPCAP - 1)
562 if sn < 0 { sn = 0 }
563 let histp: *u8 = histpos
564 let hist: *u8 = sys_mmap(SD_SNAPCAP)
565 var hn: i64 = sd_read(histp, hist, SD_SNAPCAP - 1)
566 if hn < 0 { hn = 0 }
567 let h3: *i64 = sys_mmap(32) as *i64
568 var reverts: i64 = 0
569 var newgen: i64 = 0
570 // BLESS MUST NOT BE ABLE TO MUTE AN UNREAD ALARM. I proved this failure mode on myself: a check
571 // reported symbols_dropped=1 (a sibling had reverted a landed change), I ran `bless` before reading
572 // the line, and the evidence was gone -- the guard's own baseline swallowed the finding it existed
573 // to surface. So bless now RUNS THE FULL CHECK FIRST and REFUSES while anything is firing; accepting
574 // a real revert requires --force, which makes it a deliberate, visible act instead of a reflex.
575 var isbless: i64 = 0
576 if sd_eqs(verb, "bless" as *u8) == 1 { isbless = 1 }
577 var analyze: i64 = 0
578 if isbless == 1 { analyze = 1 }
579 if sd_eqs(verb, "check" as *u8) == 1 { analyze = 1 }
580 var force: i64 = 0
581 var ai: i64 = 2
582 while ai < argc {
583 if sd_eqs(argv[ai] as *u8, "--force" as *u8) == 1 { force = 1 }
584 ai = ai + 1
585 }
586 let pend: *u8 = sys_mmap(SD_SNAPCAP)
587 let bods: *u8 = sys_mmap(SD_BODYCAP)
588 let hline: *u8 = sys_mmap(SD_BODYCAP + SD_PATHCAP)
589 let revnm: *u8 = sys_mmap(SD_NAMECAP)
590 var pb: i64 = 0
591
592 let src: *u8 = sys_mmap(SD_SRCCAP)
593 let syms: *u8 = sys_mmap(SD_SYMCAP)
594 let outbuf: *u8 = sys_mmap(SD_SNAPCAP)
595 let path: *u8 = sys_mmap(SD_PATHCAP)
596 let nm: *u8 = sys_mmap(SD_NAMECAP)
597 let f2: *i64 = sys_mmap(16) as *i64
598 var ob: i64 = 0
599 var watched: i64 = 0
600 var drops: i64 = 0
601 var errs: i64 = 0
602 var newfiles: i64 = 0
603
604 var ls: i64 = 0
605 var i: i64 = 0
606 while i <= cn {
607 var eol: i64 = 0
608 if i == cn { eol = 1 } else { if conf[i] == (SD_NL as u8) { eol = 1 } }
609 if eol == 1 {
610 if i > ls { if conf[ls] != (SD_HASH as u8) {
611 var p: i64 = 0
612 var c: i64 = ls
613 while c < i { if conf[c] != (13 as u8) { path[p] = conf[c]; p = p + 1 } c = c + 1 }
614 path[p] = 0 as u8
615 if p > 0 {
616 watched = watched + 1
617 let srcn: i64 = sd_read(path, src, SD_SRCCAP - 1)
618 if srcn <= 0 {
619 // a watched file that cannot be read IS the defect -- never a silent skip
620 sd_puts("ERROR unreadable/missing: " as *u8); sd_puts(path); sd_puts("\n" as *u8)
621 errs = errs + 1
622 } else {
623 let syn: i64 = sd_symbols(src, srcn, syms, SD_SYMCAP)
624 let bdn: i64 = sd_bodies(src, srcn, bods, SD_BODYCAP)
625 if sd_eqs(verb, "list" as *u8) == 1 {
626 var count: i64 = 0
627 if syn > 0 { count = 1 }
628 var q: i64 = 0
629 while q < syn { if syms[q] == (SD_COMMA as u8) { count = count + 1 } q = q + 1 }
630 sd_puts(" " as *u8); sd_puts(path); sd_puts(" symbols=" as *u8); sd_putn(count); sd_puts("\n" as *u8)
631 }
632 if analyze == 1 {
633 // PRIMARY: did this file go BACKWARDS to a state it already had?
634 let ch: i64 = sd_fnv1a(src, srcn)
635 sd_hist_probe(hist, hn, path, ch, h3)
636 if h3[0] == 0 {
637 if isbless == 0 { let hl: i64 = sd_hist_line_b(hline, 0, path, ch, bods, bdn); sd_hist_write(histp, hline, hl); newgen = newgen + 1 }
638 } else {
639 if h3[1] == 1 {
640 // current == newest recorded: unchanged, silent
641 } else {
642 if h3[2] >= 0 {
643 sd_puts("REVERTED " as *u8); sd_puts(path)
644 sd_puts(" -- content matches an EARLIER generation (#" as *u8); sd_putn(h3[2])
645 sd_puts(" of " as *u8); sd_putn(h3[0])
646 sd_puts("): this file went BACKWARDS, someone's landed change is gone\n" as *u8)
647 reverts = reverts + 1
648 } else {
649 if isbless == 0 { let hl: i64 = sd_hist_line_b(hline, 0, path, ch, bods, bdn); sd_hist_write(histp, hline, hl); newgen = newgen + 1 }
650 }
651 }
652 }
653 // seq1546: BODY-level revert -- same declarations, an older body.
654 let brv: i64 = sd_bodies_reverted(hist, hn, path, bods, bdn, revnm)
655 if brv > 0 {
656 sd_puts("BODY-REVERTED " as *u8); sd_puts(path)
657 sd_puts(" declaration=" as *u8); sd_puts(revnm)
658 sd_puts(" -- its body matches an EARLIER generation while the declaration set is unchanged
659" as *u8)
660 reverts = reverts + brv
661 }
662 if sd_snap_find(snap, sn, path, f2) == 1 {
663 // for every symbol the baseline had, is it STILL present?
664 let os: i64 = f2[0]
665 let ol: i64 = f2[1]
666 var s2: i64 = os
667 var k: i64 = os
668 while k <= os + ol {
669 var sep: i64 = 0
670 if k == os + ol { sep = 1 } else { if snap[k] == (SD_COMMA as u8) { sep = 1 } }
671 if sep == 1 {
672 let nl2: i64 = k - s2
673 if nl2 > 0 { if nl2 < SD_NAMECAP {
674 var z: i64 = 0
675 while z < nl2 { nm[z] = snap[s2+z]; z = z + 1 }
676 nm[nl2] = 0 as u8
677 if sd_has(syms, syn, nm, nl2) == 0 {
678 sd_puts("DROPPED " as *u8); sd_puts(path); sd_puts(" symbol=" as *u8); sd_puts(nm); sd_puts("\n" as *u8)
679 drops = drops + 1
680 }
681 } }
682 s2 = k + 1
683 }
684 k = k + 1
685 }
686 } else {
687 sd_puts("NEW (no baseline yet, run bless): " as *u8); sd_puts(path); sd_puts("\n" as *u8)
688 newfiles = newfiles + 1
689 }
690 }
691 if isbless == 1 {
692 // accepting the CURRENT state = a new generation, even if it is a revert.
693 // STAGED only: committed after the refusal check below, never before.
694 let bh: i64 = sd_fnv1a(src, srcn)
695 sd_hist_probe(hist, hn, path, bh, h3)
696 if h3[1] != 1 { pb = sd_hist_line_b(pend, pb, path, bh, bods, bdn); newgen = newgen + 1 }
697 var z2: i64 = 0
698 while z2 < p { outbuf[ob] = path[z2]; ob = ob + 1; z2 = z2 + 1 }
699 outbuf[ob] = SD_TAB as u8; ob = ob + 1
700 var z3: i64 = 0
701 while z3 < syn { outbuf[ob] = syms[z3]; ob = ob + 1; z3 = z3 + 1 }
702 outbuf[ob] = SD_NL as u8; ob = ob + 1
703 }
704 }
705 }
706 } }
707 ls = i + 1
708 }
709 i = i + 1
710 }
711
712 if isbless == 1 {
713 if force == 0 { if reverts + drops > 0 {
714 sd_werr("REFUSED: bless would MUTE a live finding -- reverted=" as *u8)
715 let m: *u8 = sys_mmap(256)
716 var mo: i64 = 0
717 var rv: i64 = reverts
718 if rv == 0 { m[mo] = 48 as u8; mo = mo + 1 } else {
719 let t2: *u8 = sys_mmap(32); var k2: i64 = 0
720 while rv > 0 { t2[k2] = (48 + (rv % 10)) as u8; rv = rv / 10; k2 = k2 + 1 }
721 var z4: i64 = k2 - 1
722 while z4 >= 0 { m[mo] = t2[z4]; mo = mo + 1; z4 = z4 - 1 }
723 }
724 sys_write(SD_STDERR, m, mo)
725 sd_werr(" symbols_dropped=" as *u8)
726 let m2: *u8 = sys_mmap(256)
727 var mo2: i64 = 0
728 var dv: i64 = drops
729 if dv == 0 { m2[mo2] = 48 as u8; mo2 = mo2 + 1 } else {
730 let t3: *u8 = sys_mmap(32); var k3: i64 = 0
731 while dv > 0 { t3[k3] = (48 + (dv % 10)) as u8; dv = dv / 10; k3 = k3 + 1 }
732 var z5: i64 = k3 - 1
733 while z5 >= 0 { m2[mo2] = t3[z5]; mo2 = mo2 + 1; z5 = z5 - 1 }
734 }
735 sys_write(SD_STDERR, m2, mo2)
736 sd_werr(" (see the lines above). Baseline UNCHANGED. Investigate first; re-run with --force only if the current state is genuinely the one you want to keep.\n" as *u8)
737 sys_exit(SD_EXIT_REFUSED)
738 return SD_EXIT_REFUSED
739 } }
740 // nothing was firing (or --force was given): NOW commit the staged generations, then the snapshot
741 if sd_hist_write(histp, pend, pb) < 0 {
742 sd_werr("REFUSED: cannot append history " as *u8); sd_werr(histp); sd_werr("\n" as *u8)
743 sys_exit(SD_EXIT_IO); return SD_EXIT_IO
744 }
745 if sd_write(snapp, outbuf, ob) < 0 {
746 sd_werr("REFUSED: cannot write snapshot " as *u8); sd_werr(snapp); sd_werr("\n" as *u8)
747 sys_exit(SD_EXIT_REFUSED); return SD_EXIT_REFUSED
748 }
749 sd_puts("SYMDROP-BLESSED files=" as *u8); sd_putn(watched)
750 sd_puts(" generations_recorded=" as *u8); sd_putn(newgen)
751 sd_puts(" errors=" as *u8); sd_putn(errs)
752 sd_puts(" snapshot=" as *u8); sd_puts(snapp); sd_puts("\n" as *u8)
753 if errs > 0 { sys_exit(SD_EXIT_DROP); return SD_EXIT_DROP }
754 sys_exit(0); return 0
755 }
756 if sd_eqs(verb, "list" as *u8) == 1 {
757 sd_puts("SYMDROP-LIST watched=" as *u8); sd_putn(watched); sd_puts(" errors=" as *u8); sd_putn(errs); sd_puts("\n" as *u8)
758 sys_exit(0); return 0
759 }
760 if sd_eqs(verb, "check" as *u8) == 1 {
761 sd_puts("SYMDROP-CHECK watched=" as *u8); sd_putn(watched)
762 sd_puts(" reverted=" as *u8); sd_putn(reverts)
763 sd_puts(" symbols_dropped=" as *u8); sd_putn(drops)
764 sd_puts(" new_generations=" as *u8); sd_putn(newgen)
765 sd_puts(" unbaselined=" as *u8); sd_putn(newfiles)
766 sd_puts(" errors=" as *u8); sd_putn(errs)
767 sd_puts("\n" as *u8)
768 if reverts > 0 { sys_exit(SD_EXIT_DROP); return SD_EXIT_DROP }
769 if drops > 0 { sys_exit(SD_EXIT_DROP); return SD_EXIT_DROP }
770 if errs > 0 { sys_exit(SD_EXIT_DROP); return SD_EXIT_DROP }
771 sys_exit(0); return 0
772 }
773 sd_werr("usage: nx_symdrop check|bless|list [watchconf] [snapfile]\n" as *u8)
774 sys_exit(SD_EXIT_USAGE)
775 return SD_EXIT_USAGE
776}