code wiki / (root) / nx_compare_cite.nx

nx_compare_cite.nx source

↩ module page · 345 lines · 18419 B

1// nx_compare_cite.nx -- CITE A /compare ROW IN ONE PUBLISH: the ref row AND its inline mark, or neither. 2// 3// WHY (operator, 2026-09-02: "this should be capabilities that the nishi team and estate and ecosystem 4// have"). A citation on a /compare board is TWO writes in TWO files -- a `ref|key|...` row in <dom>.refs 5// and a `[@key]` mark inside one matrix row's note -- and nx_compare_refs_gate refuses the board when 6// either half is missing (UNCITED when the row lands without its mark, UNRESOLVED-MARK when the mark 7// lands without its row). Measured the day this was written: fifteen refs landed as one edit and five 8// marks then had to be placed by five sequential compare-and-swap edits, each a separate transport 9// round-trip, each a chance to half-land. The estate had a GATE for citations (nx_compare_refs_gate), 10// a FORMAT PRECHECK (nx_atlas_cite) and a VERIFIER (nx_cite_lib) -- and no WRITER. This is the writer. 11// 12// WHAT IT REFUSES BY CONSTRUCTION (the fabrication class the refs law names): 13// * the pin is COMPUTED from the mirror file, never typed -- a reference whose mirror is absent cannot 14// be cited at all (REFUSED-MIRROR-ABSENT), so "a reference you did not fetch" cannot enter a register. 15// * the target row is named by its LABEL PREFIX and must match EXACTLY ONE matrix row (0 -> ROW-NOT-FOUND, 16// 2+ -> ROW-AMBIGUOUS with the count), so a mark can never land on the wrong row. 17// * a pipe inside any prose field is refused (the row grammar has exactly 9 fields). 18// * both files are re-written whole under nx_atomic_publish's compare-and-swap with the sha256 THIS run 19// read (composed: forked, never re-implemented), so a concurrent editor is refused, never clobbered. 20// * ORDER: refs first, then the mark. A ref without its mark is the gate's UNCITED class (named, and 21// this organ is idempotent so a re-run adds the missing half); a mark without its ref would DANGLE. 22// * IDEMPOTENT: key present AND mark present -> ALREADY-CITED, exit 0, nothing written. 23// 24// TREE RESOLUTION is composed from nx_comparetree_lib in PUBLISHED order (buildroot first, the tree the 25// page renders from; the authored tree second), the same order the referee reads .matrix and .refs. 26// 27// usage: nx_compare_cite <domain> <key> <row-label-prefix> <cite> <url> <mirror> <class> <grounds> 28// nx_compare_cite @<specfile> (8 lines in the same order -- the argv-mangling-safe form the 29// memory rail already uses; CRLF tolerated) 30// receipt (last line, positional): CITED | ALREADY-CITED | REFUSED-<reason> | PARTIAL-refs-published-mark-refused 31// exit: 0 cited or already | 2 usage | 3 field refused (pipe, empty, non-http url) | 4 mirror absent 32// 5 refs or matrix file unresolvable | 6 row not found or ambiguous | 7 refs publish refused 33// 8 mark publish refused after the ref landed (re-run: idempotent, it adds only the mark) 34// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 35import "nx_syscalls.nx" 36import "nx_sha256.nx" 37import "nx_comparetree_lib.nx" 38import "nx_tool_run.nx" 39 40const CC_PUBLISH_ELF: *u8 = "/volume1/homes/elderwesto/nishihost/nx_atomic_publish.elf" 41const CC_TMP_SUFFIX: *u8 = ".cite.tmp" 42const CC_SUF_REFS: *u8 = ".refs" 43const CC_SUF_MATRIX: *u8 = ".matrix" 44const CC_MODE: i64 = 420 // 0644, the mode every compare data file already carries 45const CC_PATHCAP: i64 = 1024 46const CC_ROWCAP: i64 = 16384 // one ref row: 9 fields of prose; a longer row is refused, not cut 47const CC_CAPCAP: i64 = 65536 // captured stdout of one nx_atomic_publish fork (announced if it fills) 48const CC_HEX: i64 = 64 49const CC_SPEC_FIELDS: i64 = 8 50const CC_PIPE: i64 = 124 51const CC_NL: i64 = 10 52const CC_CR: i64 = 13 53const CC_AT: i64 = 64 54const CC_HASH: i64 = 35 55const CC_ATSIGN: i64 = 64 56const CC_DAYSECS: i64 = 86400 57const CC_EX_USAGE: i64 = 2 58const CC_EX_FIELD: i64 = 3 59const CC_EX_MIRROR: i64 = 4 60const CC_EX_TREE: i64 = 5 61const CC_EX_ROW: i64 = 6 62const CC_EX_PUB_REFS: i64 = 7 63const CC_EX_PUB_MARK: i64 = 8 64 65func cc_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 66func cc_puts(s: *u8) -> i64 { sys_write(1, s, cc_len(s)); return 0 } 67func cc_putn(v: i64) -> i64 { 68 let t: *u8 = sys_mmap(32) 69 var m: i64 = v 70 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 71 var k: i64 = 0 72 if m == 0 { t[0] = 48 as u8; k = 1 } 73 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 74 let b: *u8 = sys_mmap(32) 75 var i: i64 = 0 76 while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 77 sys_write(1, b, k) 78 sys_munmap(t, 32) 79 sys_munmap(b, 32) 80 return 0 81} 82func cc_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } d[p] = 0 as u8; return p } 83func cc_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; var p: i64 = o; while i < n { d[p] = s[i]; p = p + 1; i = i + 1 } d[p] = 0 as u8; return p } 84func cc_has_pipe(s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { if s[i] == (CC_PIPE as u8) { return 1 } i = i + 1 } return 0 } 85func cc_starts(s: *u8, pfx: *u8) -> i64 { var i: i64 = 0; while pfx[i] != (0 as u8) { if s[i] != pfx[i] { return 0 } i = i + 1 } return 1 } 86// first offset of needle inside buf[0..n) or -1 87func cc_find(buf: *u8, n: i64, needle: *u8) -> i64 { 88 let m: i64 = cc_len(needle) 89 if m == 0 { return 0 - 1 } 90 var i: i64 = 0 91 while i + m <= n { 92 var k: i64 = 0 93 var same: i64 = 1 94 while k < m { if buf[i + k] != needle[k] { same = 0; k = m } else { k = k + 1 } } 95 if same == 1 { return i } 96 i = i + 1 97 } 98 return 0 - 1 99} 100// 2-digit zero-padded decimal into d at o 101func cc_pad2(d: *u8, o: i64, v: i64) -> i64 { d[o] = (48 + (v / 10) % 10) as u8; d[o + 1] = (48 + v % 10) as u8; return o + 2 } 102// epoch seconds -> "YYYY-MM-DD" (proleptic Gregorian, days-from-civil inverted; integer-only) 103func cc_civil_date(epoch: i64, out: *u8) -> i64 { 104 let z: i64 = epoch / CC_DAYSECS + 719468 105 let era: i64 = z / 146097 106 let doe: i64 = z - era * 146097 107 let yoe: i64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365 108 var y: i64 = yoe + era * 400 109 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100) 110 let mp: i64 = (5 * doy + 2) / 153 111 let dd: i64 = doy - (153 * mp + 2) / 5 + 1 112 var mm: i64 = mp + 3 113 if mp >= 10 { mm = mp - 9 } 114 if mm <= 2 { y = y + 1 } 115 var o: i64 = 0 116 o = cc_pad2(out, o, y / 100) 117 o = cc_pad2(out, o, y % 100) 118 out[o] = 45 as u8 119 o = cc_pad2(out, o + 1, mm) 120 out[o] = 45 as u8 121 o = cc_pad2(out, o + 1, dd) 122 out[o] = 0 as u8 123 return o 124} 125func cc_hex(dig: *u8, out: *u8) -> i64 { 126 let d: *u8 = "0123456789abcdef" as *u8 127 var i: i64 = 0 128 while i < 32 { let v: i64 = dig[i] as i64; out[i * 2] = d[(v / 16) % 16]; out[i * 2 + 1] = d[v % 16]; i = i + 1 } 129 out[CC_HEX] = 0 as u8 130 return CC_HEX 131} 132// resolve <dom><suf> in PUBLISHED order; returns bytes read (whole file, sys_read_file sizes it), fills path + buf pointer 133func cc_resolve(dom: *u8, suf: *u8, path: *u8, bufp: *i64) -> i64 { 134 let szp: *i64 = sys_mmap(16) as *i64 135 ct_build_path(ct_first_published(), dom, suf, path) 136 var b: *u8 = sys_read_file(path, szp) 137 if (b as i64) != 0 { if szp[0] > 0 { bufp[0] = b as i64; return szp[0] } } 138 ct_build_path(ct_second_published(), dom, suf, path) 139 b = sys_read_file(path, szp) 140 if (b as i64) != 0 { if szp[0] > 0 { bufp[0] = b as i64; return szp[0] } } 141 bufp[0] = 0 142 return 0 - 1 143} 144// write buf[0..n) to <target>.cite.tmp then fork nx_atomic_publish <tmp> <target> <expect-hex>; returns the child's exit code 145func cc_publish(target: *u8, buf: *u8, n: i64, expect_hex: *u8, cap: *u8, capn: *i64) -> i64 { 146 let tmp: *u8 = sys_mmap(CC_PATHCAP) 147 var o: i64 = cc_cat(tmp, 0, target) 148 o = cc_cat(tmp, o, CC_TMP_SUFFIX) 149 let fd: i64 = sys_openat_wr(tmp, CC_MODE) 150 if fd < 0 { return 0 - 1 } 151 let w: i64 = sys_write(fd, buf, n) 152 sys_close(fd) 153 if w != n { return 0 - 2 } 154 let argv: *i64 = sys_mmap(8 * 5) as *i64 155 argv[0] = CC_PUBLISH_ELF as i64 156 argv[1] = tmp as i64 157 argv[2] = target as i64 158 argv[3] = expect_hex as i64 159 argv[4] = 0 160 return tr_run_capture(CC_PUBLISH_ELF, argv, cap, CC_CAPCAP - 1, capn) 161} 162// load 8 fields from a spec file (one per line, CRLF tolerated) into slots[0..8) 163func cc_load_spec(path: *u8, slots: *i64) -> i64 { 164 let szp: *i64 = sys_mmap(16) as *i64 165 let b: *u8 = sys_read_file(path, szp) 166 if (b as i64) == 0 { return 0 } 167 let n: i64 = szp[0] 168 var f: i64 = 0 169 var i: i64 = 0 170 var start: i64 = 0 171 while i <= n { if f < CC_SPEC_FIELDS { 172 var eol: i64 = 0 173 if i == n { eol = 1 } else { if b[i] == (CC_NL as u8) { eol = 1 } } 174 if eol == 1 { 175 var e: i64 = i 176 if e > start { if b[e - 1] == (CC_CR as u8) { e = e - 1 } } 177 b[e] = 0 as u8 178 slots[f] = ((b as i64) + start) 179 f = f + 1 180 start = i + 1 181 } 182 } i = i + 1 } 183 return f 184} 185 186func main(argc: i64, argv: *i64) -> i64 { 187 let slots: *i64 = sys_mmap(8 * CC_SPEC_FIELDS) as *i64 188 var nf: i64 = 0 189 if argc == 2 { 190 let a1: *u8 = argv[1] as *u8 191 if a1[0] == (CC_AT as u8) { nf = cc_load_spec(((a1 as i64) + 1) as *u8, slots) } 192 } 193 if argc >= 9 { var k: i64 = 0; while k < CC_SPEC_FIELDS { slots[k] = argv[k + 1]; k = k + 1 } nf = CC_SPEC_FIELDS } 194 if nf != CC_SPEC_FIELDS { 195 cc_puts("usage: nx_compare_cite <domain> <key> <row-label-prefix> <cite> <url> <mirror> <class> <grounds> | nx_compare_cite @<specfile> (8 lines)\nREFUSED-USAGE fields=" as *u8); cc_putn(nf); cc_puts("\n" as *u8) 196 return CC_EX_USAGE 197 } 198 let dom: *u8 = slots[0] as *u8 199 let key: *u8 = slots[1] as *u8 200 let label: *u8 = slots[2] as *u8 201 let cite: *u8 = slots[3] as *u8 202 let url: *u8 = slots[4] as *u8 203 let mirror: *u8 = slots[5] as *u8 204 let cls: *u8 = slots[6] as *u8 205 let grounds: *u8 = slots[7] as *u8 206 // ---- field refusals, each named ------------------------------------------------------------ 207 var k2: i64 = 0 208 while k2 < CC_SPEC_FIELDS { if cc_len(slots[k2] as *u8) == 0 { cc_puts("REFUSED-EMPTY-FIELD index=" as *u8); cc_putn(k2); cc_puts("\n" as *u8); return CC_EX_FIELD } k2 = k2 + 1 } 209 k2 = 0 210 while k2 < CC_SPEC_FIELDS { if cc_has_pipe(slots[k2] as *u8) == 1 { cc_puts("REFUSED-PIPE-IN-FIELD index=" as *u8); cc_putn(k2); cc_puts(" (the ref row has exactly 9 fields; a pipe in prose corrupts every reader)\n" as *u8); return CC_EX_FIELD } k2 = k2 + 1 } 211 if cc_starts(url, "http" as *u8) == 0 { cc_puts("REFUSED-URL-NOT-HTTP\n" as *u8); return CC_EX_FIELD } 212 // ---- the pin is COMPUTED from the mirror: absent mirror -> no citation --------------------- 213 let mszp: *i64 = sys_mmap(16) as *i64 214 let mb: *u8 = sys_read_file(mirror, mszp) 215 if (mb as i64) == 0 { cc_puts("REFUSED-MIRROR-ABSENT mirror=" as *u8); cc_puts(mirror); cc_puts(" (fetch it with nx_research_fetch first -- a reference you did not fetch is a fabrication)\n" as *u8); return CC_EX_MIRROR } 216 if mszp[0] <= 0 { cc_puts("REFUSED-MIRROR-EMPTY mirror=" as *u8); cc_puts(mirror); cc_puts("\n" as *u8); return CC_EX_MIRROR } 217 let dig: *u8 = sys_mmap(32) 218 sha256_digest(mb, mszp[0], dig) 219 let pin: *u8 = sys_mmap(CC_HEX + 2) 220 pin[0] = 104 as u8 221 cc_hex(dig, ((pin as i64) + 1) as *u8) 222 // ---- resolve both files in PUBLISHED order -------------------------------------------------- 223 let rpath: *u8 = sys_mmap(CC_PATHCAP) 224 let rbp: *i64 = sys_mmap(16) as *i64 225 let rn: i64 = cc_resolve(dom, CC_SUF_REFS, rpath, rbp) 226 if rn < 0 { cc_puts("REFUSED-NO-REFS-FILE domain=" as *u8); cc_puts(dom); cc_puts(" (neither tree holds <dom>.refs)\n" as *u8); return CC_EX_TREE } 227 let mpath: *u8 = sys_mmap(CC_PATHCAP) 228 let mbp: *i64 = sys_mmap(16) as *i64 229 let mn: i64 = cc_resolve(dom, CC_SUF_MATRIX, mpath, mbp) 230 if mn < 0 { cc_puts("REFUSED-NO-MATRIX-FILE domain=" as *u8); cc_puts(dom); cc_puts("\n" as *u8); return CC_EX_TREE } 231 let rbuf: *u8 = rbp[0] as *u8 232 let mbuf: *u8 = mbp[0] as *u8 233 // ---- is the key already declared? ---------------------------------------------------------- 234 let keypat: *u8 = sys_mmap(CC_ROWCAP) 235 var ko: i64 = cc_cat(keypat, 0, "\nref|" as *u8) 236 ko = cc_cat(keypat, ko, key) 237 ko = cc_cat(keypat, ko, "|" as *u8) 238 var key_present: i64 = 0 239 if cc_find(rbuf, rn, keypat) >= 0 { key_present = 1 } 240 if cc_starts(rbuf, ((keypat as i64) + 1) as *u8) == 1 { key_present = 1 } 241 // ---- find EXACTLY ONE matrix row by label prefix -------------------------------------------- 242 let markpat: *u8 = sys_mmap(CC_ROWCAP) 243 var mo: i64 = cc_cat(markpat, 0, "[@" as *u8) 244 mo = cc_cat(markpat, mo, key) 245 mo = cc_cat(markpat, mo, "]" as *u8) 246 var matches: i64 = 0 247 var row_start: i64 = 0 - 1 248 var row_end: i64 = 0 - 1 249 var mark_present: i64 = 0 250 var i: i64 = 0 251 while i < mn { 252 var e: i64 = i 253 var eol: i64 = 0 254 while eol == 0 { if e >= mn { eol = 1 } else { if mbuf[e] == (CC_NL as u8) { eol = 1 } else { e = e + 1 } } } 255 var isrow: i64 = 1 256 if mbuf[i] == (CC_HASH as u8) { isrow = 0 } 257 if mbuf[i] == (CC_ATSIGN as u8) { isrow = 0 } 258 if e == i { isrow = 0 } 259 if isrow == 1 { 260 let saved: u8 = mbuf[e] 261 mbuf[e] = 0 as u8 262 if cc_starts(((mbuf as i64) + i) as *u8, label) == 1 { 263 matches = matches + 1 264 row_start = i 265 row_end = e 266 if cc_find(((mbuf as i64) + i) as *u8, e - i, markpat) >= 0 { mark_present = 1 } 267 } 268 mbuf[e] = saved 269 } 270 i = e + 1 271 } 272 if matches == 0 { cc_puts("REFUSED-ROW-NOT-FOUND label-prefix=" as *u8); cc_puts(label); cc_puts(" matrix=" as *u8); cc_puts(mpath); cc_puts("\n" as *u8); return CC_EX_ROW } 273 if matches > 1 { cc_puts("REFUSED-ROW-AMBIGUOUS label-prefix=" as *u8); cc_puts(label); cc_puts(" matches=" as *u8); cc_putn(matches); cc_puts(" (lengthen the prefix until exactly one row answers)\n" as *u8); return CC_EX_ROW } 274 if key_present == 1 { if mark_present == 1 { 275 cc_puts("ALREADY-CITED domain=" as *u8); cc_puts(dom); cc_puts(" key=" as *u8); cc_puts(key); cc_puts(" (ref row present, mark present, nothing written)\n" as *u8) 276 return 0 277 } } 278 // ---- hashes of what THIS run read = the compare-and-swap tokens ---------------------------- 279 let rhex: *u8 = sys_mmap(CC_HEX + 2) 280 sha256_digest(rbuf, rn, dig) 281 cc_hex(dig, rhex) 282 let mhex: *u8 = sys_mmap(CC_HEX + 2) 283 sha256_digest(mbuf, mn, dig) 284 cc_hex(dig, mhex) 285 let cap: *u8 = sys_mmap(CC_CAPCAP) 286 let capn: *i64 = sys_mmap(16) as *i64 287 let today: *u8 = sys_mmap(16) 288 cc_civil_date(sys_now_realtime_sec(), today) 289 var rc_refs: i64 = 0 290 var newrn: i64 = rn 291 if key_present == 0 { 292 let row: *u8 = sys_mmap(CC_ROWCAP) 293 var ro: i64 = cc_cat(row, 0, "ref|" as *u8) 294 ro = cc_cat(row, ro, key); ro = cc_cat(row, ro, "|" as *u8) 295 ro = cc_cat(row, ro, cite); ro = cc_cat(row, ro, "|" as *u8) 296 ro = cc_cat(row, ro, url); ro = cc_cat(row, ro, "|" as *u8) 297 ro = cc_cat(row, ro, mirror); ro = cc_cat(row, ro, "|" as *u8) 298 ro = cc_cat(row, ro, pin); ro = cc_cat(row, ro, "|" as *u8) 299 ro = cc_cat(row, ro, today); ro = cc_cat(row, ro, "|" as *u8) 300 ro = cc_cat(row, ro, cls); ro = cc_cat(row, ro, "|" as *u8) 301 ro = cc_cat(row, ro, grounds) 302 row[ro] = CC_NL as u8 303 ro = ro + 1 304 if ro >= CC_ROWCAP - 2 { cc_puts("REFUSED-ROW-TOO-LONG bytes=" as *u8); cc_putn(ro); cc_puts("\n" as *u8); return CC_EX_FIELD } 305 let nr: *u8 = sys_mmap(rn + ro + 2) 306 var no: i64 = cc_catb(nr, 0, rbuf, rn) 307 if rbuf[rn - 1] != (CC_NL as u8) { nr[no] = CC_NL as u8; no = no + 1 } 308 no = cc_catb(nr, no, row, ro) 309 newrn = no 310 rc_refs = cc_publish(rpath, nr, no, rhex, cap, capn) 311 if rc_refs != 0 { 312 cc_puts("publish-refs rc=" as *u8); cc_putn(rc_refs); cc_puts(" capture=" as *u8); sys_write(1, cap, capn[0]); cc_puts("\nREFUSED-PUBLISH-REFS (nothing written: the compare-and-swap on " as *u8); cc_puts(rpath); cc_puts(" refused -- re-read and re-run)\n" as *u8) 313 return CC_EX_PUB_REFS 314 } 315 } 316 var rc_mark: i64 = 0 317 var newmn: i64 = mn 318 if mark_present == 0 { 319 let markins: *u8 = sys_mmap(CC_ROWCAP) 320 var mi: i64 = cc_cat(markins, 0, " " as *u8) 321 mi = cc_cat(markins, mi, markpat) 322 var cut: i64 = row_end 323 if cut > row_start { if mbuf[cut - 1] == (CC_CR as u8) { cut = cut - 1 } } 324 let nm: *u8 = sys_mmap(mn + mi + 2) 325 var mo2: i64 = cc_catb(nm, 0, mbuf, cut) 326 mo2 = cc_catb(nm, mo2, markins, mi) 327 mo2 = cc_catb(nm, mo2, ((mbuf as i64) + cut) as *u8, mn - cut) 328 newmn = mo2 329 rc_mark = cc_publish(mpath, nm, mo2, mhex, cap, capn) 330 if rc_mark != 0 { 331 cc_puts("publish-mark rc=" as *u8); cc_putn(rc_mark); cc_puts(" capture=" as *u8); sys_write(1, cap, capn[0]); cc_puts("\n" as *u8) 332 if key_present == 0 { cc_puts("PARTIAL-refs-published-mark-refused domain=" as *u8); cc_puts(dom); cc_puts(" key=" as *u8); cc_puts(key); cc_puts(" (the ref row landed; the gate will name it UNCITED until a re-run adds the mark -- re-run with the same arguments, this organ is idempotent)\n" as *u8) } 333 else { cc_puts("REFUSED-PUBLISH-MARK domain=" as *u8); cc_puts(dom); cc_puts(" key=" as *u8); cc_puts(key); cc_puts("\n" as *u8) } 334 return CC_EX_PUB_MARK 335 } 336 } 337 cc_puts("CITED domain=" as *u8); cc_puts(dom); cc_puts(" key=" as *u8); cc_puts(key) 338 cc_puts(" row=" as *u8); cc_puts(label) 339 cc_puts(" pin=" as *u8); cc_puts(pin) 340 cc_puts(" date=" as *u8); cc_puts(today) 341 cc_puts(" refs=" as *u8); cc_puts(rpath); cc_puts(" refs_bytes=" as *u8); cc_putn(rn); cc_puts("->" as *u8); cc_putn(newrn); cc_puts(" ref_added=" as *u8); cc_putn(1 - key_present) 342 cc_puts(" matrix=" as *u8); cc_puts(mpath); cc_puts(" matrix_bytes=" as *u8); cc_putn(mn); cc_puts("->" as *u8); cc_putn(newmn); cc_puts(" mark_added=" as *u8); cc_putn(1 - mark_present) 343 cc_puts(" publish=nx_atomic_publish-cas\n" as *u8) 344 return 0 345}