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}