code wiki / (root) / nx_deployjrnl.nx

nx_deployjrnl.nx source

↩ module page · 618 lines · 30239 B

1// nx_deployjrnl.nx -- THE DEPLOY AUDIT THE ESTATE DID NOT HAVE. 2// 3// verbs: 4// scan reconstruct deploy history since the last scan; append the journal, rewrite the 5// status artifact, ratchet the rollback-risk floor. 6// exit 0 GREEN | 1 AMBER | 2 RED | 3 UNPROVEN (the nx_sizeguard ladder) 7// check <target> WOULD DEPLOYING THIS RIGHT NOW DESTROY THE ONLY ROLLBACK? 8// exit 0 SAFE | 1 WOULD-DESTROY | 2 usage | 3 UNPROVEN 9// status print the current status artifact 10// 11// --------------------------------------------------------------------------------------------- 12// HONESTY, UP FRONT, BECAUSE IT CHANGES HOW EVERY NUMBER BELOW SHOULD BE READ. 13// 14// THIS JOURNAL IS RECONSTRUCTED FROM ARTIFACTS. IT IS NOT WRITTEN BY THE DEPLOY. 15// The deploy path is a daemon and is out of scope for this lane, so nothing here observes the call. 16// What it observes is the residue: the live binary and the .prev rollback slot, hashed, compared 17// against what the previous scan recorded. That buys a great deal and it costs four things, each 18// named rather than implied: 19// 20// 1. NO ACTOR. It can say a deploy happened and what it destroyed. It CANNOT say who called it, 21// through which surface, or why. "Was /api/deploy called, and by whom" stays unanswerable 22// until the deploy path itself writes a record. 23// 2. NO TIME OF CALL. mtime is the artifact's WRITE time. A deploy that installs by rename 24// carries the staged file's mtime, so mtime is a proxy that can predate the deploy by any 25// amount. The scan epoch is what orders the journal; mtime is carried as evidence, labelled. 26// 3. RESOLUTION IS BOUNDED BY THE SCAN CADENCE -- but NOT silently. Two deploys inside one window 27// would collapse into one row, so the reconstruction checks the invariant that a single deploy 28// must satisfy (the new prev equals the live seen last scan) and reports 29// DEPLOY-MULTI-UNWITNESSED when it does not. It proves its own blindness instead of reporting 30// a clean deploy it cannot justify. 31// 4. IT CANNOT SEE BACKWARDS. The first scan establishes a BASELINE and reports no deploys, 32// because there is nothing to compare against. Everything before the first scan is invisible 33// and is declared so rather than counted as zero. 34// 35// WHAT IT DOES DO THAT NOTHING ELSE DID: it records the sha of the artifact sitting in the rollback 36// slot BEFORE that slot is overwritten. That is the whole point. Once a second deploy evicts it the 37// bytes may be unrecoverable -- an older-generation binary cannot be rebuilt -- so the sha recorded 38// in advance is the only evidence that the artifact ever existed, and the only way to say whether 39// its loss mattered. 40// 41// TWO ARTIFACTS, DELIBERATELY, AND THE REASON IS A DEFECT THIS ESTATE ALREADY MEASURED: 42// knowledge/status/deployjrnl.jrnl APPEND-ONLY history. NEVER branch on this file. A check 43// that greps an append-only journal for a marker is 44// VACUOUSLY GREEN FOREVER, because every marker it could 45// look for is already in there somewhere from an older run. 46// knowledge/status/deployjrnl.status TRUNCATE-written current state, canonical LAST LINE. This 47// is the file a consumer branches on. It is also parsed back 48// by this organ on the next scan, so a format drift breaks 49// this organ's own next run rather than quietly misleading a 50// reader. 51// 52// license_tier: ORIGINAL 53import "nx_syscalls.nx" 54import "nx_deployjrnl_lib.nx" 55 56const DJ_MAX_TARGETS: i64 = 4096 57const DJ_MAX_BANK: i64 = 2048 58const DJ_ROW_MAX: i64 = 768 59const DJ_HDR_ROWS: i64 = 32 60const DJ_SLASH_C: i64 = 47 61const DJ_PREVSUF_LEN: i64 = 5 62const DJ_I64: i64 = 8 63 64const DJ_ROOT: *u8 = "." as *u8 65const DJ_BANKDIR: *u8 = "knowledge/bank" as *u8 66const DJ_PREV_SUF: *u8 = ".elf.prev" as *u8 67const DJ_ELF_SUF: *u8 = ".elf" as *u8 68const DJ_STATUS_PATH: *u8 = "knowledge/status/deployjrnl.status" as *u8 69const DJ_JRNL_PATH: *u8 = "knowledge/status/deployjrnl.jrnl" as *u8 70const DJ_CONF_PATH: *u8 = "knowledge/status/deployjrnl_ratchet.conf" as *u8 71 72// ---- bank table ------------------------------------------------------------------------------ 73// Returns the dj_walk verdict. On anything but DJ_W_OK the caller must ABSTAIN: a bank we could not 74// enumerate must never be reported as a bank that does not contain the artifact. 75// 76// SIZE PREFILTER, AND IT IS EXACT RATHER THAN A HEURISTIC. Hashing every bank file on every scan 77// reads hundreds of megabytes of stored assets -- the bank holds meshes, pages and index segments 78// as well as binaries -- to answer a question that cannot possibly involve most of them. Two files 79// with different sizes CANNOT share a sha256, so a bank file whose size matches no rollback 80// artifact is skipped without changing a single answer. The scanned and hashed counts are both 81// reported, so the skipping is visible rather than silent. 82func dj_bank_load(bankhex: *u8, outn: *i64, sizes: *i64, nsizes: i64, outscan: *i64) -> i64 { 83 let names: *u8 = sys_mmap(DJ_MAX_BANK * DJ_NAME_SLOT) 84 let cnt: *i64 = sys_mmap(DJ_LENSLOT) as *i64 85 let skp: *i64 = sys_mmap(DJ_LENSLOT) as *i64 86 let rc: i64 = dj_walk(DJ_BANKDIR, "" as *u8, names, DJ_NAME_SLOT, DJ_MAX_BANK, cnt, skp) 87 var good: i64 = 0 88 outscan[0] = 0 89 if rc == DJ_W_OK { 90 outscan[0] = cnt[0] 91 let p: *u8 = sys_mmap(DJ_PATH_SLOT) 92 let st: *i64 = sys_mmap(DJ_LENSLOT * 2) as *i64 93 var i: i64 = 0 94 while i < cnt[0] { 95 var o: i64 = dj_cat(p, 0, DJ_BANKDIR) 96 p[o] = DJ_SLASH_C as u8 97 o = o + 1 98 o = dj_cat(p, o, ((names as i64) + i * DJ_NAME_SLOT) as *u8) 99 p[o] = 0 as u8 100 var want: i64 = 0 101 if dj_stat(p, st) == 1 { 102 var z: i64 = 0 103 while z < nsizes { 104 if sizes[z] == st[1] { want = 1; z = nsizes } else { z = z + 1 } 105 } 106 } 107 if want == 1 { 108 let slot: *u8 = ((bankhex as i64) + good * DJ_HEXSLOT) as *u8 109 if dj_hash_file(p, slot) >= 0 { good = good + 1 } 110 } 111 i = i + 1 112 } 113 sys_munmap(st as *u8, DJ_LENSLOT * 2) 114 sys_munmap(p, DJ_PATH_SLOT) 115 } 116 outn[0] = good 117 sys_munmap(names, DJ_MAX_BANK * DJ_NAME_SLOT) 118 return rc 119} 120 121func dj_find(names: *u8, n: i64, want: *u8) -> i64 { 122 var i: i64 = 0 123 while i < n { 124 if dj_streq(((names as i64) + i * DJ_NAME_SLOT) as *u8, want) == 1 { return i } 125 i = i + 1 126 } 127 return 0 - 1 128} 129 130// ---- check <target> -------------------------------------------------------------------------- 131func dj_check(target: *u8) -> i64 { 132 let live: *u8 = sys_mmap(DJ_PATH_SLOT) 133 let prev: *u8 = sys_mmap(DJ_PATH_SLOT) 134 var o: i64 = dj_cat(live, 0, target) 135 if dj_ends_with(target, DJ_ELF_SUF) == 0 { o = dj_cat(live, o, DJ_ELF_SUF) } 136 live[o] = 0 as u8 137 var p: i64 = dj_cat(prev, 0, live) 138 p = dj_cat(prev, p, ".prev" as *u8) 139 prev[p] = 0 as u8 140 141 let phex: *u8 = sys_mmap(DJ_HEXSLOT) 142 let lhex: *u8 = sys_mmap(DJ_HEXSLOT) 143 let pn: i64 = dj_hash_file(prev, phex) 144 let ln: i64 = dj_hash_file(live, lhex) 145 146 dj_puts("NX-DEPLOYJRNL check target=" as *u8) 147 dj_puts(live) 148 dj_puts("\n" as *u8) 149 if ln >= 0 { 150 dj_puts(" live_sha=" as *u8) 151 dj_puts(lhex) 152 dj_puts(" bytes=" as *u8) 153 dj_num(ln) 154 dj_puts("\n" as *u8) 155 } 156 if ln < 0 { dj_puts(" live=ABSENT (nothing is serving under this name)\n" as *u8) } 157 158 // No rollback slot means a deploy would CREATE one from the live binary. Nothing is destroyed, 159 // so this is SAFE -- but it is a different situation from a banked rollback and is named as one. 160 if pn < 0 { 161 dj_puts(" rollback=NONE -- no .prev exists, so a deploy would create it and destroy nothing.\n" as *u8) 162 dj_puts("verdict=SAFE\n" as *u8) 163 return 0 164 } 165 166 dj_puts(" rollback_sha=" as *u8) 167 dj_puts(phex) 168 dj_puts(" bytes=" as *u8) 169 dj_num(pn) 170 dj_puts("\n" as *u8) 171 172 let bankhex: *u8 = sys_mmap(DJ_MAX_BANK * DJ_HEXSLOT) 173 let nb: *i64 = sys_mmap(DJ_LENSLOT) as *i64 174 let bscan: *i64 = sys_mmap(DJ_LENSLOT) as *i64 175 let want1: *i64 = sys_mmap(DJ_LENSLOT) as *i64 176 want1[0] = pn 177 let brc: i64 = dj_bank_load(bankhex, nb, want1, 1, bscan) 178 if brc != DJ_W_OK { 179 dj_puts(" bank=UNREADABLE -- cannot decide whether the rollback is banked.\n" as *u8) 180 dj_puts(" ABSTAINING. An unreadable bank must never be reported as an absent bank.\n" as *u8) 181 dj_puts("verdict=UNPROVEN\n" as *u8) 182 return 3 183 } 184 let banked: i64 = dj_bank_has(bankhex, nb[0], phex) 185 dj_puts(" bank_files_scanned=" as *u8) 186 dj_num(bscan[0]) 187 dj_puts(" size_matched_and_hashed=" as *u8) 188 dj_num(nb[0]) 189 dj_puts(" rollback_banked=" as *u8) 190 dj_num(banked) 191 dj_puts("\n" as *u8) 192 193 if dj_would_destroy_only_rollback(1, banked) == 0 { 194 dj_puts(" the rollback artifact has a durable copy under the bank, so a deploy is recoverable.\n" as *u8) 195 dj_puts("verdict=SAFE\n" as *u8) 196 return 0 197 } 198 dj_puts(" A DEPLOY NOW WOULD SHIFT live INTO prev AND DESTROY THE ARTIFACT ABOVE PERMANENTLY.\n" as *u8) 199 dj_puts(" It exists in no durable copy, and an older-generation binary cannot be rebuilt.\n" as *u8) 200 dj_puts(" REMEDY, one call, then re-run this check:\n" as *u8) 201 dj_puts(" nx_filecopy " as *u8) 202 dj_puts(prev) 203 dj_puts(" knowledge/bank/" as *u8) 204 dj_puts(live) 205 dj_puts(".pre-<tag>\n" as *u8) 206 dj_puts("verdict=WOULD-DESTROY-ONLY-ROLLBACK\n" as *u8) 207 return 1 208} 209 210// ---- scan ------------------------------------------------------------------------------------ 211func dj_scan() -> i64 { 212 let now: i64 = sys_now_realtime_sec() 213 214 // The bank is loaded LATER, once the rollback artifact sizes are known, so the size prefilter 215 // has something to filter against. 216 let bankhex: *u8 = sys_mmap(DJ_MAX_BANK * DJ_HEXSLOT) 217 let nbs: *i64 = sys_mmap(DJ_LENSLOT) as *i64 218 let bscan: *i64 = sys_mmap(DJ_LENSLOT) as *i64 219 220 let names: *u8 = sys_mmap(DJ_MAX_TARGETS * DJ_NAME_SLOT) 221 let ncs: *i64 = sys_mmap(DJ_LENSLOT) as *i64 222 let nsk: *i64 = sys_mmap(DJ_LENSLOT) as *i64 223 let walk_rc: i64 = dj_walk(DJ_ROOT, DJ_PREV_SUF, names, DJ_NAME_SLOT, DJ_MAX_TARGETS, ncs, nsk) 224 let n: i64 = ncs[0] 225 226 var observed: i64 = 1 227 if walk_rc != DJ_W_OK { observed = 0 } 228 229 let livenm: *u8 = sys_mmap(DJ_MAX_TARGETS * DJ_NAME_SLOT) 230 let lhex: *u8 = sys_mmap(DJ_MAX_TARGETS * DJ_HEXSLOT) 231 let phex: *u8 = sys_mmap(DJ_MAX_TARGETS * DJ_HEXSLOT) 232 let lby: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 233 let pby: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 234 let lmt: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 235 let pmt: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 236 let bnk: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 237 let rsk: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 238 let trn: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 239 let evb: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 240 let st: *i64 = sys_mmap(DJ_LENSLOT * 2) as *i64 241 242 var i: i64 = 0 243 while i < n { 244 let pnm: *u8 = ((names as i64) + i * DJ_NAME_SLOT) as *u8 245 let lnm: *u8 = ((livenm as i64) + i * DJ_NAME_SLOT) as *u8 246 let pl: i64 = dj_slen(pnm) 247 var k: i64 = 0 248 while k < pl - DJ_PREVSUF_LEN { lnm[k] = pnm[k]; k = k + 1 } 249 lnm[k] = 0 as u8 250 251 let lh: *u8 = ((lhex as i64) + i * DJ_HEXSLOT) as *u8 252 let ph: *u8 = ((phex as i64) + i * DJ_HEXSLOT) as *u8 253 lby[i] = dj_hash_file(lnm, lh) 254 pby[i] = dj_hash_file(pnm, ph) 255 if lby[i] < 0 { lh[0] = 0 as u8 } 256 if pby[i] < 0 { ph[0] = 0 as u8 } 257 258 if dj_stat(lnm, st) == 1 { lmt[i] = st[0] } else { lmt[i] = 0 } 259 if dj_stat(pnm, st) == 1 { pmt[i] = st[0] } else { pmt[i] = 0 } 260 261 bnk[i] = 0 262 rsk[i] = DJ_R_NO_ROLLBACK 263 trn[i] = DJ_T_NEW 264 evb[i] = 0 265 i = i + 1 266 } 267 268 // ---- PRIOR STATE, read back from the status artifact this organ wrote last scan ---- 269 // This is the whole state machine. Without it there is nothing to compare against and every 270 // target is a BASELINE, which is exactly what the first run reports and says so. 271 let pnames: *u8 = sys_mmap(DJ_MAX_TARGETS * DJ_NAME_SLOT) 272 let pplive: *u8 = sys_mmap(DJ_MAX_TARGETS * DJ_HEXSLOT) 273 let ppprev: *u8 = sys_mmap(DJ_MAX_TARGETS * DJ_HEXSLOT) 274 let ppprevb: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 275 let pseen: *i64 = sys_mmap(DJ_MAX_TARGETS * DJ_I64) as *i64 276 var np: i64 = 0 277 var prior_seq: i64 = 0 278 var had_prior: i64 = 0 279 280 let sln: *i64 = sys_mmap(DJ_LENSLOT) as *i64 281 let sbuf: *u8 = sys_read_file(DJ_STATUS_PATH, sln) 282 if (sbuf as i64) != 0 { 283 had_prior = 1 284 let slen: i64 = sln[0] 285 let fv: *u8 = sys_mmap(DJ_NAME_SLOT) 286 var ls: i64 = 0 287 while ls < slen { 288 // End of THIS line. A loop that breaks by clobbering its own cursor cannot also report 289 // where it stopped, so the exit is a flag and the cursor stays truthful. 290 var e2: i64 = ls 291 var stop: i64 = 0 292 while stop == 0 { 293 if e2 >= slen { stop = 1 } 294 if stop == 0 { if sbuf[e2] == (DJ_NL as u8) { stop = 1 } else { e2 = e2 + 1 } } 295 } 296 if dj_field(sbuf, ls, e2, "seq=" as *u8, fv, DJ_NAME_SLOT) == 1 { 297 let sv: i64 = dj_atoi(fv) 298 if sv >= 0 { prior_seq = sv } 299 } 300 if dj_field(sbuf, ls, e2, "target=" as *u8, fv, DJ_NAME_SLOT) == 1 { 301 if np < DJ_MAX_TARGETS { 302 dj_cat(pnames, np * DJ_NAME_SLOT, fv) 303 let dst: *u8 = ((pnames as i64) + np * DJ_NAME_SLOT) as *u8 304 dst[dj_slen(fv)] = 0 as u8 305 let lv: *u8 = ((pplive as i64) + np * DJ_HEXSLOT) as *u8 306 let pv: *u8 = ((ppprev as i64) + np * DJ_HEXSLOT) as *u8 307 lv[0] = 0 as u8 308 pv[0] = 0 as u8 309 dj_field(sbuf, ls, e2, "live=" as *u8, lv, DJ_HEXSLOT) 310 dj_field(sbuf, ls, e2, "prev=" as *u8, pv, DJ_HEXSLOT) 311 ppprevb[np] = 0 - 1 312 if dj_field(sbuf, ls, e2, "pbytes=" as *u8, fv, DJ_NAME_SLOT) == 1 { 313 ppprevb[np] = dj_atoi(fv) 314 } 315 pseen[np] = 0 316 np = np + 1 317 } 318 } 319 ls = e2 + 1 320 } 321 sys_munmap(fv, DJ_NAME_SLOT) 322 } 323 324 // ---- SIZE SET, THEN THE BANK ---- 325 // Candidate sizes are the rollback artifacts we hold NOW plus the ones the last scan recorded. 326 // The second half matters: an artifact that a deploy has already evicted can no longer be 327 // stat'd, and its recorded size is the only way to ask whether a copy of it survives. 328 let wsz: *i64 = sys_mmap((DJ_MAX_TARGETS * 2) * DJ_I64) as *i64 329 var nsz: i64 = 0 330 var wi: i64 = 0 331 while wi < n { 332 if pby[wi] >= 0 { wsz[nsz] = pby[wi]; nsz = nsz + 1 } 333 wi = wi + 1 334 } 335 wi = 0 336 while wi < np { 337 if ppprevb[wi] >= 0 { wsz[nsz] = ppprevb[wi]; nsz = nsz + 1 } 338 wi = wi + 1 339 } 340 let bank_rc: i64 = dj_bank_load(bankhex, nbs, wsz, nsz, bscan) 341 let nbank: i64 = nbs[0] 342 if bank_rc != DJ_W_OK { observed = 0 } 343 344 // ---- CLASSIFY ---- 345 var atrisk: i64 = 0 346 var norb: i64 = 0 347 var c_new: i64 = 0 348 var c_unch: i64 = 0 349 var c_wit: i64 = 0 350 var c_multi: i64 = 0 351 var c_prevonly: i64 = 0 352 var irrep: i64 = 0 353 354 i = 0 355 while i < n { 356 let lnm: *u8 = ((livenm as i64) + i * DJ_NAME_SLOT) as *u8 357 let lh: *u8 = ((lhex as i64) + i * DJ_HEXSLOT) as *u8 358 let ph: *u8 = ((phex as i64) + i * DJ_HEXSLOT) as *u8 359 var pres: i64 = 0 360 if pby[i] >= 0 { pres = 1 } 361 var bk: i64 = 0 362 if pres == 1 { bk = dj_bank_has(bankhex, nbank, ph) } 363 bnk[i] = bk 364 rsk[i] = dj_risk(pres, bk) 365 let pi: i64 = dj_find(pnames, np, lnm) 366 var seen: i64 = 0 367 var live_changed: i64 = 0 368 var prev_is_was: i64 = 0 369 var prev_changed: i64 = 0 370 if pi >= 0 { 371 seen = 1 372 pseen[pi] = 1 373 let olv: *u8 = ((pplive as i64) + pi * DJ_HEXSLOT) as *u8 374 let opv: *u8 = ((ppprev as i64) + pi * DJ_HEXSLOT) as *u8 375 if dj_streq(olv, lh) == 0 { live_changed = 1 } 376 if dj_streq(opv, ph) == 0 { prev_changed = 1 } 377 if dj_streq(olv, ph) == 1 { prev_is_was = 1 } 378 // The artifact EVICTED by this deploy is whatever the rollback slot held BEFORE it. 379 // Its bytes are already gone; the only reason we can name it at all is that the last 380 // scan recorded its sha before the slot was overwritten. That is the point of this organ. 381 if live_changed == 1 { 382 if opv[0] != (0 as u8) { 383 let eb: i64 = dj_bank_has(bankhex, nbank, opv) 384 evb[i] = eb 385 if eb == 0 { irrep = irrep + 1 } 386 } 387 } 388 } 389 trn[i] = dj_transition(seen, live_changed, prev_is_was, prev_changed) 390 if trn[i] == DJ_T_NEW { c_new = c_new + 1 } 391 if trn[i] == DJ_T_UNCHANGED { c_unch = c_unch + 1 } 392 if trn[i] == DJ_T_DEPLOY_WITNESSED { c_wit = c_wit + 1 } 393 if trn[i] == DJ_T_DEPLOY_MULTI { c_multi = c_multi + 1 } 394 if trn[i] == DJ_T_PREV_ONLY { c_prevonly = c_prevonly + 1 } 395 if rsk[i] == DJ_R_AT_RISK { atrisk = atrisk + 1 } 396 if rsk[i] == DJ_R_NO_ROLLBACK { norb = norb + 1 } 397 i = i + 1 398 } 399 var c_gone: i64 = 0 400 var pj: i64 = 0 401 while pj < np { 402 if pseen[pj] == 0 { c_gone = c_gone + 1 } 403 pj = pj + 1 404 } 405 406 // ---- ratchet floor ---- 407 var floor: i64 = atrisk 408 var floor_src: i64 = 0 409 let cln: *i64 = sys_mmap(DJ_LENSLOT) as *i64 410 let cbuf: *u8 = sys_read_file(DJ_CONF_PATH, cln) 411 if (cbuf as i64) != 0 { 412 let fv2: *u8 = sys_mmap(DJ_NAME_SLOT) 413 if dj_field(cbuf, 0, cln[0], "floor=" as *u8, fv2, DJ_NAME_SLOT) == 1 { 414 let pv2: i64 = dj_atoi(fv2) 415 if pv2 >= 0 { floor = pv2; floor_src = 1 } 416 } 417 sys_munmap(fv2, DJ_NAME_SLOT) 418 } 419 420 // A first scan has no prior state, so it CANNOT have witnessed a deploy. Reporting GREEN there 421 // would be true but misleading; the status carries baseline=1 so a reader knows why it is quiet. 422 let verdict: i64 = dj_verdict(irrep, c_multi, atrisk, floor, observed) 423 424 // ---- STATUS ARTIFACT: truncate-written, canonical LAST line ---- 425 // Built whole and written with ONE write. Buffer DERIVED from the row count, not a guessed cap. 426 let obytes: i64 = (n + np + DJ_HDR_ROWS) * DJ_ROW_MAX 427 let ob: *u8 = sys_mmap(obytes) 428 var o: i64 = 0 429 o = dj_cat(ob, o, "-- nx_deployjrnl status. RECONSTRUCTED from artifacts; the deploy path does not write this.\n" as *u8) 430 o = dj_cat(ob, o, "-- Rows below are the CURRENT rollback census. The final line is the canonical machine answer.\n" as *u8) 431 o = dj_cat(ob, o, "-- Resolution is bounded by the scan cadence; an unwitnessed multi-deploy is reported, not hidden.\n" as *u8) 432 o = dj_cat(ob, o, "ts=" as *u8); o = dj_catn(ob, o, now); o = dj_cat(ob, o, "\n" as *u8) 433 o = dj_cat(ob, o, "seq=" as *u8); o = dj_catn(ob, o, prior_seq + 1); o = dj_cat(ob, o, "\n" as *u8) 434 o = dj_cat(ob, o, "method=stat-plus-sha256-over-live-and-prev\n" as *u8) 435 o = dj_cat(ob, o, "baseline=" as *u8); o = dj_catn(ob, o, 1 - had_prior); o = dj_cat(ob, o, "\n" as *u8) 436 o = dj_cat(ob, o, "observed=" as *u8); o = dj_catn(ob, o, observed); o = dj_cat(ob, o, "\n" as *u8) 437 o = dj_cat(ob, o, "walk_rc=" as *u8); o = dj_catn(ob, o, walk_rc); o = dj_cat(ob, o, "\n" as *u8) 438 o = dj_cat(ob, o, "bank_rc=" as *u8); o = dj_catn(ob, o, bank_rc); o = dj_cat(ob, o, "\n" as *u8) 439 o = dj_cat(ob, o, "bank_files_scanned=" as *u8); o = dj_catn(ob, o, bscan[0]); o = dj_cat(ob, o, "\n" as *u8) 440 o = dj_cat(ob, o, "bank_size_matched=" as *u8); o = dj_catn(ob, o, nbank); o = dj_cat(ob, o, "\n" as *u8) 441 o = dj_cat(ob, o, "rollback_slots=" as *u8); o = dj_catn(ob, o, n); o = dj_cat(ob, o, "\n" as *u8) 442 o = dj_cat(ob, o, "at_risk=" as *u8); o = dj_catn(ob, o, atrisk); o = dj_cat(ob, o, "\n" as *u8) 443 o = dj_cat(ob, o, "no_rollback=" as *u8); o = dj_catn(ob, o, norb); o = dj_cat(ob, o, "\n" as *u8) 444 o = dj_cat(ob, o, "floor=" as *u8); o = dj_catn(ob, o, floor); o = dj_cat(ob, o, "\n" as *u8) 445 o = dj_cat(ob, o, "cls_baseline=" as *u8); o = dj_catn(ob, o, c_new); o = dj_cat(ob, o, "\n" as *u8) 446 o = dj_cat(ob, o, "cls_unchanged=" as *u8); o = dj_catn(ob, o, c_unch); o = dj_cat(ob, o, "\n" as *u8) 447 o = dj_cat(ob, o, "cls_deploy_witnessed=" as *u8); o = dj_catn(ob, o, c_wit); o = dj_cat(ob, o, "\n" as *u8) 448 o = dj_cat(ob, o, "cls_deploy_multi_unwitnessed=" as *u8); o = dj_catn(ob, o, c_multi); o = dj_cat(ob, o, "\n" as *u8) 449 o = dj_cat(ob, o, "cls_prev_only=" as *u8); o = dj_catn(ob, o, c_prevonly); o = dj_cat(ob, o, "\n" as *u8) 450 o = dj_cat(ob, o, "cls_gone=" as *u8); o = dj_catn(ob, o, c_gone); o = dj_cat(ob, o, "\n" as *u8) 451 // A partition is a claim: print the parts AND the sum, so an unreconciled leak cannot hide. 452 o = dj_cat(ob, o, "cls_sum=" as *u8) 453 o = dj_catn(ob, o, c_new + c_unch + c_wit + c_multi + c_prevonly) 454 o = dj_cat(ob, o, " of_current=" as *u8); o = dj_catn(ob, o, n); o = dj_cat(ob, o, "\n" as *u8) 455 o = dj_cat(ob, o, "irreplaceable_evictions=" as *u8); o = dj_catn(ob, o, irrep); o = dj_cat(ob, o, "\n" as *u8) 456 457 i = 0 458 while i < n { 459 let lnm: *u8 = ((livenm as i64) + i * DJ_NAME_SLOT) as *u8 460 o = dj_cat(ob, o, "ROW target=" as *u8) 461 o = dj_cat(ob, o, lnm) 462 o = dj_cat(ob, o, " live=" as *u8) 463 o = dj_cat(ob, o, ((lhex as i64) + i * DJ_HEXSLOT) as *u8) 464 o = dj_cat(ob, o, " lbytes=" as *u8); o = dj_catn(ob, o, lby[i]) 465 o = dj_cat(ob, o, " lmtime=" as *u8); o = dj_catn(ob, o, lmt[i]) 466 o = dj_cat(ob, o, " prev=" as *u8) 467 o = dj_cat(ob, o, ((phex as i64) + i * DJ_HEXSLOT) as *u8) 468 o = dj_cat(ob, o, " pbytes=" as *u8); o = dj_catn(ob, o, pby[i]) 469 o = dj_cat(ob, o, " pmtime=" as *u8); o = dj_catn(ob, o, pmt[i]) 470 o = dj_cat(ob, o, " banked=" as *u8); o = dj_catn(ob, o, bnk[i]) 471 o = dj_cat(ob, o, " risk=" as *u8); o = dj_catn(ob, o, rsk[i]) 472 o = dj_cat(ob, o, " cls=" as *u8); o = dj_cat(ob, o, dj_transition_name(trn[i])) 473 o = dj_cat(ob, o, "\n" as *u8) 474 i = i + 1 475 } 476 o = dj_cat(ob, o, "verdict=" as *u8) 477 o = dj_cat(ob, o, dj_verdict_name(verdict)) 478 o = dj_cat(ob, o, "\n" as *u8) 479 480 let sfd: i64 = sys_openat_wr(DJ_STATUS_PATH, DJ_MODE_644) 481 if sfd >= 0 { sys_write(sfd, ob, o); sys_close(sfd) } 482 483 // ---- APPEND-ONLY JOURNAL: history, never a branch surface ---- 484 var jo: i64 = 0 485 let jb: *u8 = sys_mmap(obytes) 486 jo = dj_cat(jb, jo, "SCAN ts=" as *u8); jo = dj_catn(jb, jo, now) 487 jo = dj_cat(jb, jo, " seq=" as *u8); jo = dj_catn(jb, jo, prior_seq + 1) 488 jo = dj_cat(jb, jo, " slots=" as *u8); jo = dj_catn(jb, jo, n) 489 jo = dj_cat(jb, jo, " at_risk=" as *u8); jo = dj_catn(jb, jo, atrisk) 490 jo = dj_cat(jb, jo, " deploys=" as *u8); jo = dj_catn(jb, jo, c_wit + c_multi) 491 jo = dj_cat(jb, jo, " unwitnessed=" as *u8); jo = dj_catn(jb, jo, c_multi) 492 jo = dj_cat(jb, jo, " irreplaceable=" as *u8); jo = dj_catn(jb, jo, irrep) 493 jo = dj_cat(jb, jo, " observed=" as *u8); jo = dj_catn(jb, jo, observed) 494 jo = dj_cat(jb, jo, " verdict=" as *u8); jo = dj_cat(jb, jo, dj_verdict_name(verdict)) 495 jo = dj_cat(jb, jo, "\n" as *u8) 496 497 i = 0 498 while i < n { 499 var emit: i64 = 0 500 if trn[i] == DJ_T_DEPLOY_WITNESSED { emit = 1 } 501 if trn[i] == DJ_T_DEPLOY_MULTI { emit = 1 } 502 if trn[i] == DJ_T_PREV_ONLY { emit = 1 } 503 if emit == 1 { 504 let lnm: *u8 = ((livenm as i64) + i * DJ_NAME_SLOT) as *u8 505 let pi: i64 = dj_find(pnames, np, lnm) 506 jo = dj_cat(jb, jo, "DEPLOY ts=" as *u8); jo = dj_catn(jb, jo, now) 507 jo = dj_cat(jb, jo, " target=" as *u8); jo = dj_cat(jb, jo, lnm) 508 jo = dj_cat(jb, jo, " cls=" as *u8); jo = dj_cat(jb, jo, dj_transition_name(trn[i])) 509 if pi >= 0 { 510 jo = dj_cat(jb, jo, " was_live=" as *u8) 511 jo = dj_cat(jb, jo, ((pplive as i64) + pi * DJ_HEXSLOT) as *u8) 512 jo = dj_cat(jb, jo, " prev_before=" as *u8) 513 jo = dj_cat(jb, jo, ((ppprev as i64) + pi * DJ_HEXSLOT) as *u8) 514 jo = dj_cat(jb, jo, " evicted=" as *u8) 515 jo = dj_cat(jb, jo, ((ppprev as i64) + pi * DJ_HEXSLOT) as *u8) 516 } 517 jo = dj_cat(jb, jo, " became_live=" as *u8) 518 jo = dj_cat(jb, jo, ((lhex as i64) + i * DJ_HEXSLOT) as *u8) 519 jo = dj_cat(jb, jo, " prev_after=" as *u8) 520 jo = dj_cat(jb, jo, ((phex as i64) + i * DJ_HEXSLOT) as *u8) 521 jo = dj_cat(jb, jo, " evicted_banked=" as *u8); jo = dj_catn(jb, jo, evb[i]) 522 jo = dj_cat(jb, jo, " lmtime=" as *u8); jo = dj_catn(jb, jo, lmt[i]) 523 jo = dj_cat(jb, jo, "\n" as *u8) 524 } 525 if trn[i] == DJ_T_NEW { 526 let lnm2: *u8 = ((livenm as i64) + i * DJ_NAME_SLOT) as *u8 527 jo = dj_cat(jb, jo, "BASELINE ts=" as *u8); jo = dj_catn(jb, jo, now) 528 jo = dj_cat(jb, jo, " target=" as *u8); jo = dj_cat(jb, jo, lnm2) 529 jo = dj_cat(jb, jo, " live=" as *u8) 530 jo = dj_cat(jb, jo, ((lhex as i64) + i * DJ_HEXSLOT) as *u8) 531 jo = dj_cat(jb, jo, " prev=" as *u8) 532 jo = dj_cat(jb, jo, ((phex as i64) + i * DJ_HEXSLOT) as *u8) 533 jo = dj_cat(jb, jo, " prev_banked=" as *u8); jo = dj_catn(jb, jo, bnk[i]) 534 jo = dj_cat(jb, jo, "\n" as *u8) 535 } 536 i = i + 1 537 } 538 pj = 0 539 while pj < np { 540 if pseen[pj] == 0 { 541 jo = dj_cat(jb, jo, "GONE ts=" as *u8); jo = dj_catn(jb, jo, now) 542 jo = dj_cat(jb, jo, " target=" as *u8) 543 jo = dj_cat(jb, jo, ((pnames as i64) + pj * DJ_NAME_SLOT) as *u8) 544 jo = dj_cat(jb, jo, " was_live=" as *u8) 545 jo = dj_cat(jb, jo, ((pplive as i64) + pj * DJ_HEXSLOT) as *u8) 546 jo = dj_cat(jb, jo, "\n" as *u8) 547 } 548 pj = pj + 1 549 } 550 let jfd: i64 = sys_openat_append(DJ_JRNL_PATH, DJ_MODE_644) 551 if jfd >= 0 { sys_write(jfd, jb, jo); sys_close(jfd) } 552 553 // ---- ratchet: tighten on a fall, NEVER rewrite the baseline on a rise ---- 554 let nf: i64 = dj_new_floor(atrisk, floor) 555 let cb: *u8 = sys_mmap(DJ_ROW_MAX) 556 var co: i64 = 0 557 co = dj_cat(cb, co, "-- nx_deployjrnl rollback-risk ratchet. Tightens on a fall, never loosens.\n" as *u8) 558 co = dj_cat(cb, co, "floor=" as *u8); co = dj_catn(cb, co, nf); co = dj_cat(cb, co, "\n" as *u8) 559 let cfd: i64 = sys_openat_wr(DJ_CONF_PATH, DJ_MODE_644) 560 if cfd >= 0 { sys_write(cfd, cb, co); sys_close(cfd) } 561 562 // ---- stdout ---- 563 dj_puts("NX-DEPLOYJRNL scan\n" as *u8) 564 dj_puts(" method=RECONSTRUCTED-FROM-ARTIFACTS (the deploy path does not write this)\n" as *u8) 565 dj_puts(" rollback_slots=" as *u8); dj_num(n) 566 dj_puts(" bank_scanned=" as *u8); dj_num(bscan[0]) 567 dj_puts(" bank_size_matched=" as *u8); dj_num(nbank) 568 dj_puts(" at_risk=" as *u8); dj_num(atrisk) 569 dj_puts(" floor=" as *u8); dj_num(floor) 570 dj_puts("\n" as *u8) 571 dj_puts(" baseline_run=" as *u8); dj_num(1 - had_prior) 572 dj_puts(" deploys_seen=" as *u8); dj_num(c_wit + c_multi) 573 dj_puts(" unwitnessed=" as *u8); dj_num(c_multi) 574 dj_puts(" irreplaceable_evictions=" as *u8); dj_num(irrep) 575 dj_puts("\n" as *u8) 576 dj_puts(" partition baseline=" as *u8); dj_num(c_new) 577 dj_puts(" unchanged=" as *u8); dj_num(c_unch) 578 dj_puts(" witnessed=" as *u8); dj_num(c_wit) 579 dj_puts(" multi=" as *u8); dj_num(c_multi) 580 dj_puts(" prev_only=" as *u8); dj_num(c_prevonly) 581 dj_puts(" sum=" as *u8); dj_num(c_new + c_unch + c_wit + c_multi + c_prevonly) 582 dj_puts(" of=" as *u8); dj_num(n) 583 dj_puts("\n" as *u8) 584 if had_prior == 0 { 585 dj_puts(" FIRST SCAN: no prior state existed, so no deploy CAN have been witnessed. This is a\n" as *u8) 586 dj_puts(" baseline, not a clean bill of health -- everything before now is invisible to it.\n" as *u8) 587 } 588 if observed == 0 { 589 dj_puts(" COVERAGE INCOMPLETE walk_rc=" as *u8); dj_num(walk_rc) 590 dj_puts(" bank_rc=" as *u8); dj_num(bank_rc) 591 dj_puts(" -- abstaining rather than publishing a partial census as a total.\n" as *u8) 592 } 593 dj_puts("verdict=" as *u8) 594 dj_puts(dj_verdict_name(verdict)) 595 dj_puts("\n" as *u8) 596 return verdict 597} 598 599func main(argc: i64, argv: *i64) -> i64 { 600 // A BARE FORK IS THE NORMAL CASE, NOT AN ERROR. The clock plane and the gate roster both fork an 601 // organ with no argv, so an organ whose bare invocation only prints usage can never be put on a 602 // beat. scan writes only under knowledge/status/ and is idempotent, so defaulting to it is safe. 603 // The default is ANNOUNCED, so a bare run is never mistaken for a verb that was silently ignored. 604 if argc < 2 { 605 dj_puts("nx_deployjrnl: no verb given, defaulting to scan (clock and roster forks are bare)\n" as *u8) 606 return dj_scan() 607 } 608 if dj_streq(argv[1] as *u8, "scan" as *u8) == 1 { return dj_scan() } 609 if dj_streq(argv[1] as *u8, "check" as *u8) == 1 { 610 if argc < 3 { 611 dj_puts("usage: nx_deployjrnl check <target>\n" as *u8) 612 return 2 613 } 614 return dj_check(argv[2] as *u8) 615 } 616 dj_puts("usage: nx_deployjrnl scan | check <target> | status\n" as *u8) 617 return 2 618}