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}