code wiki / _hdl_build / nx_wiki_versioned_publish.nx

nx_wiki_versioned_publish.nx source

↩ module page · 268 lines · 13048 B

1// nx_wiki_versioned_publish.nx -- A4a: VERSIONED-PUBLISH + the multi-point rollback INDEX. 2// 3// THE OPERATOR PRECONDITION (verbatim intent): "ability to revert via versions held so we can roll back 4// not just to another broken one but have FULL RESTORE via MULTIPLE rollback points." This is the safety 5// net that must exist BEFORE any full-auto re-publishing: every time a page goes live we FIRST capture the 6// exact bytes as a RETAINED, content-addressed version record, so a later restore can land the LIVE page 7// byte-exact on ANY prior version -- not just the last one, not just a different broken one. 8// 9// COMPOSITION (no storage/crypto reinvented -- every line below routes through a proven organ): 10// nx_wiki_archive war_archive_page / war_get_by_cid / war_cid_of_body (content-addressed blob = the 11// no-rot anchor; the SAME WAR_PREFIX store A2's guard resolves citations from, so a 12// version blob survives across runs / is durable by construction). 13// nx_seg_store ss_begin / ss_add / ss_commit / ss_get_cap (append-only durable KV: a new 14// record is a NEW segment, never an in-place overwrite -- the additive-data law #13). 15// nx_canon_cid cid_of (via war_cid_of_body) (CID = nxc1-+sha256(bytes)). 16// nx_wiki_publish pub_publish_ex (A3 GUARDED publish -- the SOLE 17// enforcement point; EVERY live write here flows through the A2 guard, fail-closed). 18// 19// THE VERSION INDEX (a SEPARATE key family in the SAME durable store; never collides with wikiblob:/wikicid: 20// which the archive owns): 21// wpv:cnt:<slug> -> decimal version COUNT for the slug (a latest-wins pointer; seg_store keeps 22// every prior value in history too, so even this is additive, never destroyed) 23// wpv:ver:<slug>:<v#> -> the content CID of version v# (written EXACTLY ONCE per (slug,v#); a new 24// publish appends a NEW v#, it NEVER rewrites an existing wpv:ver key -- this 25// is the append-only, multi-rollback-point guarantee, mechanical not promised) 26// wpv:ep:<slug>:<v#> -> the fixed epoch stamped for v# (written once; NO wall-clock call -- the 27// caller passes a fixed epoch so the record is deterministic/reproducible) 28// Because v# is monotonic and each wpv:ver:<slug>:<v#> key is unique, publishing v2/v3 can NEVER mutate or 29// delete the v1 record. The blob itself is content-addressed, so even dropping a pointer cannot lose bytes. 30// 31// Pure NishiLang, NO sql/.sh/.py/.js, no new .tsv/.conf. license_tier: ORIGINAL 32import "nx_wiki_publish.nx" 33import "nx_wiki_archive.nx" 34import "nx_seg_store.nx" 35import "nx_syscalls.nx" 36 37// ===== sealed verdict surface (codes 4940-4949; distinct from PUB_*/PG_*/NX_WV_*) ================= 38const VP_OK: i64 = 0 39const VP_BAD_INPUT: i64 = 0 - 4940 // null/empty slug or path 40const VP_READ_FAILED: i64 = 0 - 4941 // could not read the new local page bytes 41const VP_ARCHIVE_FAIL: i64 = 0 - 4942 // archive add/commit returned an error 42const VP_NOT_FOUND: i64 = 0 - 4943 // requested version# has no retained record 43const VP_PUSH_REJECT: i64 = 0 - 4944 // A3 guard REJECTED the page (fail-closed; nothing pushed) 44 45// scan cap handed to ss_get_cap (>= live segment count for the archive store). 46const VP_SCAN_CAP: i64 = 256 47// a generous bound on retained versions enumerated by vp_list per call. 48const VP_MAX_VERS: i64 = 4096 49// file mode 0644 for the restore staging file. 50const VP_MODE_0644: i64 = 0x1a4 51 52// ===== tiny local helpers (vp_ namespace; no clash with ss_/war_/pub_) ============================ 53func vp_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 54 55// concat NUL-terminated s into dst at off; returns new off (no terminator written). 56func vp_cat(dst: *u8, off: i64, s: *u8) -> i64 { 57 var o: i64 = off; var k: i64 = 0 58 while s[k] != (0 as u8) { dst[o] = s[k]; o = o + 1; k = k + 1 } 59 return o 60} 61 62// append decimal of v (>=0) into dst at off; returns new off. 63func vp_catn(dst: *u8, off: i64, v: i64) -> i64 { 64 var o: i64 = off; let t: *u8 = sys_mmap(28); var m: i64 = v 65 var k: i64 = 0 66 if m == 0 { t[0] = 48 as u8; k = 1 } 67 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 68 var i: i64 = 0 69 while i < k { dst[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 70 return o 71} 72 73// parse a non-negative decimal from a NUL-terminated buffer; -1 if any non-digit. (reads cnt values back.) 74func vp_parse_dec(b: *u8, n: i64) -> i64 { 75 if n < 1 { return 0 - 1 } 76 var v: i64 = 0; var i: i64 = 0 77 while i < n { 78 let c: i64 = b[i] 79 if c < 48 { return 0 - 1 } 80 if c > 57 { return 0 - 1 } 81 v = v * 10 + (c - 48) 82 i = i + 1 83 } 84 return v 85} 86 87// ---- version-index key builders (all NUL-terminated) ---- 88// wpv:cnt:<slug> 89func vp_cnt_key(slug: *u8, out: *u8) -> i64 { 90 var o: i64 = 0 91 o = vp_cat(out, o, "wpv:cnt:" as *u8) 92 o = vp_cat(out, o, slug) 93 out[o] = 0 as u8 94 return o 95} 96// wpv:ver:<slug>:<v#> 97func vp_ver_key(slug: *u8, v: i64, out: *u8) -> i64 { 98 var o: i64 = 0 99 o = vp_cat(out, o, "wpv:ver:" as *u8) 100 o = vp_cat(out, o, slug) 101 out[o] = 58 as u8; o = o + 1 // ':' 102 o = vp_catn(out, o, v) 103 out[o] = 0 as u8 104 return o 105} 106// wpv:ep:<slug>:<v#> 107func vp_ep_key(slug: *u8, v: i64, out: *u8) -> i64 { 108 var o: i64 = 0 109 o = vp_cat(out, o, "wpv:ep:" as *u8) 110 o = vp_cat(out, o, slug) 111 out[o] = 58 as u8; o = o + 1 // ':' 112 o = vp_catn(out, o, v) 113 out[o] = 0 as u8 114 return o 115} 116 117// ---- read the current retained version COUNT for a slug from the committed store ---- 118// Returns the count (>=0; 0 if the slug has never been versioned). prefix = archive store prefix. 119func vp_get_count(prefix: *u8, slug: *u8) -> i64 { 120 let ck: *u8 = sys_mmap(512) 121 vp_cnt_key(slug, ck) 122 let pp: *i64 = sys_mmap(16) as *i64 123 let ll: *i64 = sys_mmap(16) as *i64 124 let r: i64 = ss_get_cap(prefix, ck, pp, ll, VP_SCAN_CAP) 125 if r != 1 { return 0 } 126 let src: *u8 = pp[0] as *u8 127 let n: i64 = ll[0] 128 let tmp: *u8 = sys_mmap(n + 1) 129 var i: i64 = 0 130 while i < n { tmp[i] = src[i]; i = i + 1 } 131 tmp[n] = 0 as u8 132 let v: i64 = vp_parse_dec(tmp, n) 133 if v < 0 { return 0 } 134 return v 135} 136 137// ---- look up the CID retained for (slug, v#) ---- 138// Copies the NUL-terminated CID into cidout (>=72) and returns its length, or -1 if v# is not retained. 139func vp_cid_of_version(prefix: *u8, slug: *u8, v: i64, cidout: *u8) -> i64 { 140 let vk: *u8 = sys_mmap(512) 141 vp_ver_key(slug, v, vk) 142 let pp: *i64 = sys_mmap(16) as *i64 143 let ll: *i64 = sys_mmap(16) as *i64 144 let r: i64 = ss_get_cap(prefix, vk, pp, ll, VP_SCAN_CAP) 145 if r != 1 { return 0 - 1 } 146 let src: *u8 = pp[0] as *u8 147 let n: i64 = ll[0] 148 var i: i64 = 0 149 while i < n { cidout[i] = src[i]; i = i + 1 } 150 cidout[n] = 0 as u8 151 return n 152} 153 154// ===== vp_list: enumerate the FULL retained rollback-point list (the multi-point proof) ============ 155// Fills vers[i] (version#) and copies each version's CID (NUL-terminated, 70 bytes incl NUL) into the 156// flat cidbuf at offset i*72 (so cidbuf must be >= count*72). Returns the count of retained points (>=0). 157// This is what proves "MULTIPLE rollback points" exist -- not merely the latest. 158func vp_list_ex(prefix: *u8, slug: *u8, vers: *i64, cidbuf: *u8, max: i64) -> i64 { 159 let cnt: i64 = vp_get_count(prefix, slug) 160 var out: i64 = 0 161 var v: i64 = 1 162 while v <= cnt { 163 if out < max { 164 let cidp: *u8 = (cidbuf as i64 + out * 72) as *u8 165 let cl: i64 = vp_cid_of_version(prefix, slug, v, cidp) 166 if cl > 0 { 167 vers[out] = v 168 out = out + 1 169 } 170 } 171 v = v + 1 172 } 173 return out 174} 175 176// production convenience: prefix defaults to WAR_PREFIX (the live durable archive store). 177func vp_list(slug: *u8, vers: *i64, cidbuf: *u8, max: i64) -> i64 { 178 return vp_list_ex(WAR_PREFIX, slug, vers, cidbuf, max) 179} 180 181// ===== vpub: capture-then-publish (the versioned guarded publish) ================================== 182// 1. read the bytes that are about to go live (new_local_html_path). 183// 2. ARCHIVE those bytes as a content-addressed blob (war_archive_page -> wikiblob:<cid>, the no-rot 184// anchor) AND append the version-index records for the NEXT monotonic v# (wpv:ver / wpv:ep) plus the 185// bumped wpv:cnt -- all in ONE seg_store commit (atomic: readers see the new version only after the 186// manifest rename). This is captured BEFORE the live overwrite, so the version is retained even if the 187// push later fails. A new v# never rewrites an older version record (append-only multi-point). 188// 3. PUBLISH the bytes through the A3 GUARD (pub_publish_ex). If do_push_flag==0 the guard verdict is still 189// enforced (ALLOW/REJECT) but nothing is pushed -- lets the gate exercise the full archive+index path 190// hermetically. A guard REJECT returns VP_PUSH_REJECT and the live page is untouched (fail-closed); the 191// version we just retained stays in the store (additive -- a rejected publish still left a recoverable 192// snapshot, which is exactly the safety net we want). 193// 194// prefix = archive/index store prefix (production: WAR_PREFIX). epoch = a FIXED stamp (NO wall-clock here). 195// out (PubResult) receives the A3 push verdict. vout[0] receives the version# assigned. Returns VP_OK, or a 196// negative VP_* code on failure. 197func vpub_ex(prefix: *u8, slug: *u8, new_local_html_path: *u8, 198 cs_ptr: *i64, cs_len: *i64, ncorpus: i64, 199 epoch: i64, do_push_flag: i64, 200 out: *PubResult, vout: *i64) -> i64 { 201 vout[0] = 0 202 if (slug as i64) == 0 { return VP_BAD_INPUT } 203 if (new_local_html_path as i64) == 0 { return VP_BAD_INPUT } 204 if vp_len(slug) < 1 { return VP_BAD_INPUT } 205 206 // 1. read the page bytes that are about to be published. 207 let nbox: *i64 = sys_mmap(16) as *i64 208 let body: *u8 = sys_read_file(new_local_html_path, nbox) 209 if (body as i64) == 0 { return VP_READ_FAILED } 210 let n: i64 = nbox[0] 211 if n <= 0 { return VP_READ_FAILED } 212 213 // 2. capture as a RETAINED version: blob + index records in ONE commit. 214 let next_v: i64 = vp_get_count(prefix, slug) + 1 215 let w: *i64 = ss_begin() 216 let cid: *u8 = sys_mmap(80) 217 // content-addressed blob (REUSED: also stamps wikicid:<slug> = current pointer; the immutable blob is 218 // the durable anchor a CID resolves to forever). 219 let ar: i64 = war_archive_page(w, slug, body, n, cid) 220 if ar != 0 { return VP_ARCHIVE_FAIL } 221 let cidn: i64 = vp_len(cid) 222 // version-index records (each key unique => append-only; never rewrites an older v#). 223 let vk: *u8 = sys_mmap(512) 224 vp_ver_key(slug, next_v, vk) 225 if ss_add(w, 1, vk, cid, cidn) < 0 { return VP_ARCHIVE_FAIL } 226 let ek: *u8 = sys_mmap(512) 227 vp_ep_key(slug, next_v, ek) 228 let epbuf: *u8 = sys_mmap(28) 229 let epn: i64 = vp_catn(epbuf, 0, epoch) 230 if ss_add(w, 1, ek, epbuf, epn) < 0 { return VP_ARCHIVE_FAIL } 231 // bump the count pointer (latest-wins; prior count values stay in history = additive). 232 let ck: *u8 = sys_mmap(512) 233 vp_cnt_key(slug, ck) 234 let cntbuf: *u8 = sys_mmap(28) 235 let cntn: i64 = vp_catn(cntbuf, 0, next_v) 236 if ss_add(w, 1, ck, cntbuf, cntn) < 0 { return VP_ARCHIVE_FAIL } 237 // ONE atomic commit (segment temp -> rename, manifest temp -> rename; the manifest rename is the 238 // commit point -- a crash before it leaves the prior versions intact and this one invisible). 239 if ss_commit(prefix, w, next_v) != 0 { return VP_ARCHIVE_FAIL } 240 241 vout[0] = next_v 242 243 // 3. publish through the A3 GUARD (the sole enforcement point; fail-closed on REJECT). 244 let pubrc: i64 = pub_publish_ex(prefix, new_local_html_path, slug, 245 cs_ptr, cs_len, ncorpus, do_push_flag, out) 246 if pubrc == PUB_REJECTED { return VP_PUSH_REJECT } 247 return VP_OK 248} 249 250// production convenience: prefix = WAR_PREFIX (live durable store). 251func vpub(slug: *u8, new_local_html_path: *u8, 252 cs_ptr: *i64, cs_len: *i64, ncorpus: i64, 253 epoch: i64, do_push_flag: i64, 254 out: *PubResult, vout: *i64) -> i64 { 255 return vpub_ex(WAR_PREFIX, slug, new_local_html_path, cs_ptr, cs_len, ncorpus, 256 epoch, do_push_flag, out, vout) 257} 258 259// human-readable verdict name (for the live-proof runner / logs). 260func vp_status_name(s: i64) -> *u8 { 261 if s == VP_OK { return "VP_OK" as *u8 } 262 if s == VP_BAD_INPUT { return "VP_BAD_INPUT" as *u8 } 263 if s == VP_READ_FAILED { return "VP_READ_FAILED" as *u8 } 264 if s == VP_ARCHIVE_FAIL { return "VP_ARCHIVE_FAIL" as *u8 } 265 if s == VP_NOT_FOUND { return "VP_NOT_FOUND" as *u8 } 266 if s == VP_PUSH_REJECT { return "VP_PUSH_REJECT" as *u8 } 267 return "VP_UNKNOWN" as *u8 268}