code wiki / _hdl_build / nx_wiki_versioned_publish.nx

nx_wiki_versioned_publish.nx source

↩ module page · 267 lines · 13012 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. 50 51// ===== tiny local helpers (vp_ namespace; no clash with ss_/war_/pub_) ============================ 52func vp_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 53 54// concat NUL-terminated s into dst at off; returns new off (no terminator written). 55func vp_cat(dst: *u8, off: i64, s: *u8) -> i64 { 56 var o: i64 = off; var k: i64 = 0 57 while s[k] != (0 as u8) { dst[o] = s[k]; o = o + 1; k = k + 1 } 58 return o 59} 60 61// append decimal of v (>=0) into dst at off; returns new off. 62func vp_catn(dst: *u8, off: i64, v: i64) -> i64 { 63 var o: i64 = off; let t: *u8 = sys_mmap(28); var m: i64 = v 64 var k: i64 = 0 65 if m == 0 { t[0] = 48 as u8; k = 1 } 66 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 67 var i: i64 = 0 68 while i < k { dst[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 69 return o 70} 71 72// parse a non-negative decimal from a NUL-terminated buffer; -1 if any non-digit. (reads cnt values back.) 73func vp_parse_dec(b: *u8, n: i64) -> i64 { 74 if n < 1 { return 0 - 1 } 75 var v: i64 = 0; var i: i64 = 0 76 while i < n { 77 let c: i64 = b[i] 78 if c < 48 { return 0 - 1 } 79 if c > 57 { return 0 - 1 } 80 v = v * 10 + (c - 48) 81 i = i + 1 82 } 83 return v 84} 85 86// ---- version-index key builders (all NUL-terminated) ---- 87// wpv:cnt:<slug> 88func vp_cnt_key(slug: *u8, out: *u8) -> i64 { 89 var o: i64 = 0 90 o = vp_cat(out, o, "wpv:cnt:" as *u8) 91 o = vp_cat(out, o, slug) 92 out[o] = 0 as u8 93 return o 94} 95// wpv:ver:<slug>:<v#> 96func vp_ver_key(slug: *u8, v: i64, out: *u8) -> i64 { 97 var o: i64 = 0 98 o = vp_cat(out, o, "wpv:ver:" as *u8) 99 o = vp_cat(out, o, slug) 100 out[o] = 58 as u8; o = o + 1 // ':' 101 o = vp_catn(out, o, v) 102 out[o] = 0 as u8 103 return o 104} 105// wpv:ep:<slug>:<v#> 106func vp_ep_key(slug: *u8, v: i64, out: *u8) -> i64 { 107 var o: i64 = 0 108 o = vp_cat(out, o, "wpv:ep:" as *u8) 109 o = vp_cat(out, o, slug) 110 out[o] = 58 as u8; o = o + 1 // ':' 111 o = vp_catn(out, o, v) 112 out[o] = 0 as u8 113 return o 114} 115 116// ---- read the current retained version COUNT for a slug from the committed store ---- 117// Returns the count (>=0; 0 if the slug has never been versioned). prefix = archive store prefix. 118func vp_get_count(prefix: *u8, slug: *u8) -> i64 { 119 let ck: *u8 = sys_mmap(512) 120 vp_cnt_key(slug, ck) 121 let pp: *i64 = sys_mmap(16) as *i64 122 let ll: *i64 = sys_mmap(16) as *i64 123 let r: i64 = ss_get_cap(prefix, ck, pp, ll, VP_SCAN_CAP) 124 if r != 1 { return 0 } 125 let src: *u8 = pp[0] as *u8 126 let n: i64 = ll[0] 127 let tmp: *u8 = sys_mmap(n + 1) 128 var i: i64 = 0 129 while i < n { tmp[i] = src[i]; i = i + 1 } 130 tmp[n] = 0 as u8 131 let v: i64 = vp_parse_dec(tmp, n) 132 if v < 0 { return 0 } 133 return v 134} 135 136// ---- look up the CID retained for (slug, v#) ---- 137// Copies the NUL-terminated CID into cidout (>=72) and returns its length, or -1 if v# is not retained. 138func vp_cid_of_version(prefix: *u8, slug: *u8, v: i64, cidout: *u8) -> i64 { 139 let vk: *u8 = sys_mmap(512) 140 vp_ver_key(slug, v, vk) 141 let pp: *i64 = sys_mmap(16) as *i64 142 let ll: *i64 = sys_mmap(16) as *i64 143 let r: i64 = ss_get_cap(prefix, vk, pp, ll, VP_SCAN_CAP) 144 if r != 1 { return 0 - 1 } 145 let src: *u8 = pp[0] as *u8 146 let n: i64 = ll[0] 147 var i: i64 = 0 148 while i < n { cidout[i] = src[i]; i = i + 1 } 149 cidout[n] = 0 as u8 150 return n 151} 152 153// ===== vp_list: enumerate the FULL retained rollback-point list (the multi-point proof) ============ 154// Fills vers[i] (version#) and copies each version's CID (NUL-terminated, 70 bytes incl NUL) into the 155// flat cidbuf at offset i*72 (so cidbuf must be >= count*72). Returns the count of retained points (>=0). 156// This is what proves "MULTIPLE rollback points" exist -- not merely the latest. 157func vp_list_ex(prefix: *u8, slug: *u8, vers: *i64, cidbuf: *u8, max: i64) -> i64 { 158 let cnt: i64 = vp_get_count(prefix, slug) 159 var out: i64 = 0 160 var v: i64 = 1 161 while v <= cnt { 162 if out < max { 163 let cidp: *u8 = (cidbuf as i64 + out * 72) as *u8 164 let cl: i64 = vp_cid_of_version(prefix, slug, v, cidp) 165 if cl > 0 { 166 vers[out] = v 167 out = out + 1 168 } 169 } 170 v = v + 1 171 } 172 return out 173} 174 175// production convenience: prefix defaults to WAR_PREFIX (the live durable archive store). 176func vp_list(slug: *u8, vers: *i64, cidbuf: *u8, max: i64) -> i64 { 177 return vp_list_ex(WAR_PREFIX, slug, vers, cidbuf, max) 178} 179 180// ===== vpub: capture-then-publish (the versioned guarded publish) ================================== 181// 1. read the bytes that are about to go live (new_local_html_path). 182// 2. ARCHIVE those bytes as a content-addressed blob (war_archive_page -> wikiblob:<cid>, the no-rot 183// anchor) AND append the version-index records for the NEXT monotonic v# (wpv:ver / wpv:ep) plus the 184// bumped wpv:cnt -- all in ONE seg_store commit (atomic: readers see the new version only after the 185// manifest rename). This is captured BEFORE the live overwrite, so the version is retained even if the 186// push later fails. A new v# never rewrites an older version record (append-only multi-point). 187// 3. PUBLISH the bytes through the A3 GUARD (pub_publish_ex). If do_push_flag==0 the guard verdict is still 188// enforced (ALLOW/REJECT) but nothing is pushed -- lets the gate exercise the full archive+index path 189// hermetically. A guard REJECT returns VP_PUSH_REJECT and the live page is untouched (fail-closed); the 190// version we just retained stays in the store (additive -- a rejected publish still left a recoverable 191// snapshot, which is exactly the safety net we want). 192// 193// prefix = archive/index store prefix (production: WAR_PREFIX). epoch = a FIXED stamp (NO wall-clock here). 194// out (PubResult) receives the A3 push verdict. vout[0] receives the version# assigned. Returns VP_OK, or a 195// negative VP_* code on failure. 196func vpub_ex(prefix: *u8, slug: *u8, new_local_html_path: *u8, 197 cs_ptr: *i64, cs_len: *i64, ncorpus: i64, 198 epoch: i64, do_push_flag: i64, 199 out: *PubResult, vout: *i64) -> i64 { 200 vout[0] = 0 201 if (slug as i64) == 0 { return VP_BAD_INPUT } 202 if (new_local_html_path as i64) == 0 { return VP_BAD_INPUT } 203 if vp_len(slug) < 1 { return VP_BAD_INPUT } 204 205 // 1. read the page bytes that are about to be published. 206 let nbox: *i64 = sys_mmap(16) as *i64 207 let body: *u8 = sys_read_file(new_local_html_path, nbox) 208 if (body as i64) == 0 { return VP_READ_FAILED } 209 let n: i64 = nbox[0] 210 if n <= 0 { return VP_READ_FAILED } 211 212 // 2. capture as a RETAINED version: blob + index records in ONE commit. 213 let next_v: i64 = vp_get_count(prefix, slug) + 1 214 let w: *i64 = ss_begin() 215 let cid: *u8 = sys_mmap(80) 216 // content-addressed blob (REUSED: also stamps wikicid:<slug> = current pointer; the immutable blob is 217 // the durable anchor a CID resolves to forever). 218 let ar: i64 = war_archive_page(w, slug, body, n, cid) 219 if ar != 0 { return VP_ARCHIVE_FAIL } 220 let cidn: i64 = vp_len(cid) 221 // version-index records (each key unique => append-only; never rewrites an older v#). 222 let vk: *u8 = sys_mmap(512) 223 vp_ver_key(slug, next_v, vk) 224 if ss_add(w, 1, vk, cid, cidn) < 0 { return VP_ARCHIVE_FAIL } 225 let ek: *u8 = sys_mmap(512) 226 vp_ep_key(slug, next_v, ek) 227 let epbuf: *u8 = sys_mmap(28) 228 let epn: i64 = vp_catn(epbuf, 0, epoch) 229 if ss_add(w, 1, ek, epbuf, epn) < 0 { return VP_ARCHIVE_FAIL } 230 // bump the count pointer (latest-wins; prior count values stay in history = additive). 231 let ck: *u8 = sys_mmap(512) 232 vp_cnt_key(slug, ck) 233 let cntbuf: *u8 = sys_mmap(28) 234 let cntn: i64 = vp_catn(cntbuf, 0, next_v) 235 if ss_add(w, 1, ck, cntbuf, cntn) < 0 { return VP_ARCHIVE_FAIL } 236 // ONE atomic commit (segment temp -> rename, manifest temp -> rename; the manifest rename is the 237 // commit point -- a crash before it leaves the prior versions intact and this one invisible). 238 if ss_commit(prefix, w, next_v) != 0 { return VP_ARCHIVE_FAIL } 239 240 vout[0] = next_v 241 242 // 3. publish through the A3 GUARD (the sole enforcement point; fail-closed on REJECT). 243 let pubrc: i64 = pub_publish_ex(prefix, new_local_html_path, slug, 244 cs_ptr, cs_len, ncorpus, do_push_flag, out) 245 if pubrc == PUB_REJECTED { return VP_PUSH_REJECT } 246 return VP_OK 247} 248 249// production convenience: prefix = WAR_PREFIX (live durable store). 250func vpub(slug: *u8, new_local_html_path: *u8, 251 cs_ptr: *i64, cs_len: *i64, ncorpus: i64, 252 epoch: i64, do_push_flag: i64, 253 out: *PubResult, vout: *i64) -> i64 { 254 return vpub_ex(WAR_PREFIX, slug, new_local_html_path, cs_ptr, cs_len, ncorpus, 255 epoch, do_push_flag, out, vout) 256} 257 258// human-readable verdict name (for the live-proof runner / logs). 259func vp_status_name(s: i64) -> *u8 { 260 if s == VP_OK { return "VP_OK" as *u8 } 261 if s == VP_BAD_INPUT { return "VP_BAD_INPUT" as *u8 } 262 if s == VP_READ_FAILED { return "VP_READ_FAILED" as *u8 } 263 if s == VP_ARCHIVE_FAIL { return "VP_ARCHIVE_FAIL" as *u8 } 264 if s == VP_NOT_FOUND { return "VP_NOT_FOUND" as *u8 } 265 if s == VP_PUSH_REJECT { return "VP_PUSH_REJECT" as *u8 } 266 return "VP_UNKNOWN" as *u8 267}