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}