code wiki / _hdl_build / nx_store_put.nx
nx_store_put.nx source
↩ module page · 513 lines · 25486 B
1// nx_store_put.nx -- THE UNIFIED INFORMATION-PLANE WRITE VERB (operator 2026-07-18: "it shouldnt
2// be on the tsv we are supposed to use a unified nishi information management plane system thats
3// better than git ... not just debt it should be new feature capable").
4// Row-level, provenanced, additive writes DIRECTLY against any 7-col seg-store plane
5// (id title sev status owner scope note) -- debt-, work- (features), or any sibling prefix.
6// Flat staging files demote to bootstrap/recovery; the LIVE write path is this verb.
7// BETTER-THAN-GIT properties, in-plane: every mutation appends a revision to <prefix minus '-'>hist-
8// as epoch<TAB>actor<TAB>verb<TAB>id<TAB>old-row<TAB>new-row (inner tabs -> '|'), so history +
9// provenance live in the SOVEREIGN plane itself -- no working tree, no index, no merge dance;
10// row-independent writes cannot conflict, and the full before/after of every row is queryable.
11// nx_store_put <prefix> put <actor> <id> <field>... (N-col: id + 1..15 fields, ANY plane schema
12// incl the 9-col frontier -- v2 uplevel 07-18,
13// operator: "stop engaging with the tsv")
14// nx_store_put <prefix> setcol <actor> <id> <colidx> <value> (flip ONE cell, e.g. frontier status col5 T->D)
15// nx_store_put <prefix> close <actor> <id> <closing-note> (7-col convention: col3 open->closed, note appended)
16// nx_store_put <prefix> load (dump the plane)
17// nx_store_put <prefix> hist (dump the revision plane)
18// put on an unseeded prefix BOOTSTRAPS the plane (first row creates it -- no staging file needed).
19// close/setcol are FAIL-CLOSED: unknown id (or colidx past the row) -> exit 4, store untouched.
20// Whole-store last-writer-wins per write; writers serialized socially by session claims (flock = next rung).
21// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
22import "nx_store_seed_lib.nx"
23import "nx_seg_store.nx"
24import "nx_syscalls.nx"
25const NSP_MAGIC_4096: i64 = 4096
26
27const NSP_CAP: i64 = 1048576
28const NSP_NL: i64 = 10
29const NSP_TAB: i64 = 9
30const NSP_PIPE: i64 = 124
31const NSP_DASH: i64 = 45
32const NSP_NCOL: i64 = 7
33const NSP_PAIR: i64 = 2
34const NSP_SP_BYTES: i64 = 128
35const NSP_STDERR: i64 = 2
36const NSP_PFXCAP: i64 = 256
37const NSP_MSGCAP: i64 = 256
38const NSP_ARGC_PUT_MIN: i64 = 6 // prog prefix put actor id + at least 1 more field
39const NSP_ARGC_PUT_MAX: i64 = 20 // id + up to 15 fields (16-col plane cap)
40const NSP_ARGC_CLOSE: i64 = 6 // prog prefix close actor id note
41const NSP_ARGC_SETCOL: i64 = 7 // prog prefix setcol actor id colidx value
42const NSP_ARGC_MIN: i64 = 3
43const NSP_MAXCOL: i64 = 16 // max columns a plane row may carry
44const NSP_SP2_BYTES: i64 = 256 // 16 cols * 2 slots * 8B
45const NSP_EXIT_USAGE: i64 = 2
46const NSP_EXIT_IO: i64 = 1
47const NSP_EXIT_REFUSED: i64 = 4
48// put field argv indices:
49const NSP_F_ACTOR: i64 = 3
50const NSP_F_ID: i64 = 4
51const NSP_F_LAST: i64 = 10
52// close argv indices:
53const NSP_C_ID: i64 = 4
54const NSP_C_NOTE: i64 = 5
55// status column index in the 7-col row:
56const NSP_COL_STATUS: i64 = 3
57const NSP_COL_NOTE: i64 = 6
58
59func nsp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
60func nsp_werr(s: *u8) -> i64 { sys_write(NSP_STDERR, s, nsp_slen(s)); return 0 }
61func nsp_eqs(a: *u8, b: *u8) -> i64 {
62 var i: i64 = 0
63 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
64 if b[i] != (0 as u8) { return 0 }
65 return 1
66}
67// slice [a,b) of q equals C-string s
68func nsp_slice_eqs(q: *u8, a: i64, b: i64, s: *u8) -> i64 {
69 let sn: i64 = nsp_slen(s)
70 if b - a != sn { return 0 }
71 var i: i64 = 0
72 while i < sn { if q[a+i] != s[i] { return 0 } i = i + 1 }
73 return 1
74}
75// split line [ls,le) into up to NSP_MAXCOL (start,end) pairs (callers pass NSP_SP2_BYTES scratch)
76func nsp_cols(q: *u8, ls: i64, le: i64, sp: *i64) -> i64 {
77 var c: i64 = 0
78 var p: i64 = ls
79 while c < NSP_MAXCOL {
80 var e: i64 = p
81 var s: i64 = 1
82 while s == 1 { if e >= le { s = 0 } else { if q[e] == (NSP_TAB as u8) { s = 0 } else { e = e + 1 } } }
83 sp[c*NSP_PAIR] = p
84 sp[c*NSP_PAIR+1] = e
85 c = c + 1
86 if e >= le { return c }
87 p = e + 1
88 }
89 return c
90}
91func nsp_cat_slice(d: *u8, o: i64, q: *u8, a: i64, b: i64) -> i64 {
92 var oo: i64 = o
93 var i: i64 = a
94 while i < b { d[oo] = q[i]; oo = oo + 1; i = i + 1 }
95 return oo
96}
97// copy slice with inner tabs -> '|' (hist rows stay 6-col clean)
98func nsp_cat_slice_pipe(d: *u8, o: i64, q: *u8, a: i64, b: i64) -> i64 {
99 var oo: i64 = o
100 var i: i64 = a
101 while i < b {
102 var c: i64 = q[i]
103 if c == NSP_TAB { c = NSP_PIPE }
104 d[oo] = c as u8
105 oo = oo + 1
106 i = i + 1
107 }
108 return oo
109}
110// derive the hist prefix: "knowledge/store/debt-" -> "knowledge/store/debthist-"
111func nsp_hist_prefix(prefix: *u8, out: *u8) -> i64 {
112 var n: i64 = nsp_slen(prefix)
113 if n > 0 { if prefix[n-1] == (NSP_DASH as u8) { n = n - 1 } }
114 var i: i64 = 0
115 while i < n { out[i] = prefix[i]; i = i + 1 }
116 var o: i64 = ss_cat(out, n, "hist-" as *u8)
117 out[o] = 0 as u8
118 return o
119}
120// append one hist revision row: epoch actor verb id old new
121func nsp_hist_append(prefix: *u8, actor: *u8, verb: *u8, id: *u8, q: *u8, olda: i64, oldb: i64, neu: *u8, na: i64, nb: i64) -> i64 {
122 let hp: *u8 = sys_mmap(NSP_PFXCAP)
123 nsp_hist_prefix(prefix, hp)
124 let hb: *u8 = sys_mmap(NSP_CAP)
125 // O(1) APPEND 2026-07-30: this used to sts_load the ENTIRE hist plane just to append ONE revision
126 // row, then sts_seed the WHOLE plane back -- O(rows x segments) write amplification on EVERY put
127 // through this chokepoint. The row is a PURE APPEND (no existing row is modified), so we now build
128 // ONLY the new row at offset 0 and hand it to sts_append_fast.
129 var o: i64 = 0
130 o = ss_catn(hb, o, sys_now_realtime_sec())
131 hb[o] = NSP_TAB as u8
132 o = o + 1
133 o = ss_cat(hb, o, actor)
134 hb[o] = NSP_TAB as u8
135 o = o + 1
136 o = ss_cat(hb, o, verb)
137 hb[o] = NSP_TAB as u8
138 o = o + 1
139 o = ss_cat(hb, o, id)
140 hb[o] = NSP_TAB as u8
141 o = o + 1
142 if oldb > olda { o = nsp_cat_slice_pipe(hb, o, q, olda, oldb) } else { o = ss_cat(hb, o, "-" as *u8) }
143 hb[o] = NSP_TAB as u8
144 o = o + 1
145 if nb > na { o = nsp_cat_slice_pipe(hb, o, neu, na, nb) } else { o = ss_cat(hb, o, "-" as *u8) }
146 hb[o] = NSP_NL as u8
147 o = o + 1
148 // FAST PATH: O(1) append -- writes ONLY this row, not the whole plane. o-1 strips the trailing
149 // newline because sts_append_fast takes the bare row. BOOTSTRAP: sts_append_fast requires an
150 // existing q:n, so a hist plane that does not exist YET legitimately returns <0; in that case
151 // (and only that case) fall back to a full seed to create it. Every later append is O(1).
152 // LOCKED form: the hist plane is NOT the plane `main` locked, so this append is otherwise
153 // unprotected -- and sts_append_fast is itself a read-modify-write on q:n (two appenders read the
154 // same n, write the same q:<n>, and newest-wins silently OVERWRITES one row). Lock order is always
155 // main-then-hist, so no ABBA deadlock is possible.
156 var rc: i64 = sts_append_fast_locked(hp, hb, o - 1)
157 if rc < 0 { rc = sts_seed(hp, hb, o) }
158 if rc < 0 { return 0 - 1 }
159 return 0
160}
161func nsp_atoi(s: *u8) -> i64 {
162 var v: i64 = 0
163 var i: i64 = 0
164 while s[i] != (0 as u8) {
165 let c: i64 = s[i]
166 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } }
167 i = i + 1
168 }
169 return v
170}
171
172func nsp_report(verb: *u8, id: *u8, replaced: i64, rows: i64) -> i64 {
173 let m: *u8 = sys_mmap(NSP_MSGCAP)
174 var o: i64 = ss_cat(m, 0, verb)
175 o = ss_cat(m, o, " " as *u8)
176 o = ss_cat(m, o, id)
177 o = ss_cat(m, o, " replaced=" as *u8)
178 o = ss_catn(m, o, replaced)
179 o = ss_cat(m, o, " rows=" as *u8)
180 o = ss_catn(m, o, rows)
181 m[o] = NSP_NL as u8
182 o = o + 1
183 sys_write(1, m, o)
184 return 0
185}
186
187// SHARED ANTI-CLOBBER TOOTH (2026-08-02, debts 1785713047/1785713373) -- ONE guard, FOUR callers.
188// EVERY write verb here rebuilds the WHOLE plane from what sts_load could reach, then re-seeds it. If that
189// load came back PARTIAL, every row it could not see is silently DELETED. put/putn ADD rows; close/setcol
190// MODIFY one row in place -- so NONE of them can ever legitimately REDUCE the row count. A count below what
191// the plane already declares therefore means the load was short, and committing it would destroy data
192// (commontask- 37->10, dedupq- 21->3, both recovered 2026-08-02). Refuse instead of committing the loss.
193// FAIL-OPEN when q:n is absent -- that call is BOOTSTRAPPING the plane, which is legitimate.
194// ★Deliberately NOT in the shared sts_seed primitive: there it would refuse nx_plane_append's ROLLBACK and
195// nx_plane_repair, both of which shrink BY DEFINITION (1785713047). The guard belongs to the WRITER.
196// Proven both directions by nx_store_put_shrink_gate 4/4: it BITES a manufactured over-declared plane
197// (rc=4, store untouched) and SPARES a healthy one (rc=0, q:n 3->4).
198func nsp_would_shrink(prefix: *u8, rows: i64) -> i64 {
199 let vq: *i64 = sys_mmap(NSP_MAGIC_4096) as *i64
200 let vl: *i64 = sys_mmap(NSP_MAGIC_4096) as *i64
201 if ss_get(prefix, "q:n" as *u8, vq, vl) == 1 {
202 let declared: i64 = sts_atoi(vq[0] as *u8, vl[0])
203 if rows < declared { return 1 }
204 }
205 return 0
206}
207func nsp_refuse_shrink(vname: *u8) -> i64 {
208 nsp_werr("REFUSED: this " as *u8)
209 nsp_werr(vname)
210 nsp_werr(" would SHRINK the plane -- the load came back partial, so committing it would delete the rows it could not see (the commontask-/dedupq- clobber class). Store untouched; re-run, and if the plane is genuinely damaged use nx_plane_repair deliberately.\n" as *u8)
211 return 0
212}
213
214func main(argc: i64, argv: *i64) -> i64 {
215 if argc < NSP_ARGC_MIN { nsp_werr("usage: nx_store_put <prefix> {put <actor> <id> <title> <sev> <status> <owner> <scope> <note> | close <actor> <id> <note> | load | hist}\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
216 let prefix: *u8 = argv[1] as *u8
217 let verb: *u8 = argv[2] as *u8
218
219 // seq1559 MIGRATION 2026-07-30 (ws=sev-eater). Every mutating verb here is a READ-MODIFY-WRITE
220 // over a shared plane: sts_load -> rewrite the whole buffer -> sts_seed. Unlocked, two concurrent
221 // invocations interleave as A-loads / B-loads / A-commits / B-commits-without-A and one caller's
222 // row is gone silently. That is not theoretical -- nx_sts_lock_gate measures 5 of 6 rows destroyed
223 // under 6-way concurrency, and 0 lost once the lock is held.
224 // Taken HERE, before the first load, so the critical section covers the entire operation rather
225 // than just the commit -- locking only the write would still let two readers race the same
226 // snapshot. Uses sts_lock, i.e. the SAME <prefix>plock nx_debt / nx_plane_append / sts_append_row
227 // use; a private lock file would have been easier and would have excluded nobody.
228 // NO UNLOCK IS NEEDED ON ANY PATH: this is a one-shot CLI organ and every exit -- success or a
229 // fail-closed sys_exit() -- terminates the process, which releases the flock. There is therefore
230 // no path that can leak the lock, which is why the lock is safe to take this early.
231 sts_lock(prefix)
232
233 if nsp_eqs("load" as *u8, verb) == 1 {
234 let b: *u8 = sys_mmap(NSP_CAP)
235 let n: i64 = sts_load(prefix, b, NSP_CAP)
236 if n <= 0 { nsp_werr("plane EMPTY / unseeded\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
237 sys_write(1, b, n)
238 sys_exit(0)
239 return 0
240 }
241 if nsp_eqs("hist" as *u8, verb) == 1 {
242 let hp: *u8 = sys_mmap(NSP_PFXCAP)
243 nsp_hist_prefix(prefix, hp)
244 let b2: *u8 = sys_mmap(NSP_CAP)
245 let n2: i64 = sts_load(hp, b2, NSP_CAP)
246 if n2 <= 0 { nsp_werr("hist EMPTY (no revisions yet)\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
247 sys_write(1, b2, n2)
248 sys_exit(0)
249 return 0
250 }
251
252 if nsp_eqs("put" as *u8, verb) == 1 {
253 if argc < NSP_ARGC_PUT_MIN { nsp_werr("put needs <actor> <id> <field>... (1..15 fields after id)\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
254 if argc > NSP_ARGC_PUT_MAX { nsp_werr("put: too many fields (16-col plane cap)\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
255 let actor: *u8 = argv[NSP_F_ACTOR] as *u8
256 let id: *u8 = argv[NSP_F_ID] as *u8
257 let cur: *u8 = sys_mmap(NSP_CAP)
258 var cn: i64 = sts_load(prefix, cur, NSP_CAP - NSP_MAGIC_4096)
259 if cn < 0 { cn = 0 }
260 let neu: *u8 = sys_mmap(NSP_CAP)
261 let sp: *i64 = sys_mmap(NSP_SP2_BYTES) as *i64
262 // literal counters first
263 var no: i64 = 0
264 var replaced: i64 = 0
265 var rows: i64 = 0
266 var olda: i64 = 0
267 var oldb: i64 = 0
268 var i: i64 = 0
269 while i < cn {
270 var le: i64 = i
271 var s: i64 = 1
272 while s == 1 { if le >= cn { s = 0 } else { if cur[le] == (NSP_NL as u8) { s = 0 } else { le = le + 1 } } }
273 if le > i {
274 nsp_cols(cur, i, le, sp)
275 if nsp_slice_eqs(cur, sp[0], sp[1], id) == 1 {
276 replaced = 1
277 olda = i
278 oldb = le
279 } else {
280 no = nsp_cat_slice(neu, no, cur, i, le)
281 neu[no] = NSP_NL as u8
282 no = no + 1
283 rows = rows + 1
284 }
285 }
286 i = le + 1
287 }
288 // append the new/updated row (ALL fields argv[4..argc-1] tab-joined -- N-col, schema-agnostic)
289 let na: i64 = no
290 let flast: i64 = argc - 1
291 var f: i64 = NSP_F_ID
292 while f <= flast {
293 no = ss_cat(neu, no, argv[f] as *u8)
294 if f < flast { neu[no] = NSP_TAB as u8; no = no + 1 }
295 f = f + 1
296 }
297 let nb: i64 = no
298 neu[no] = NSP_NL as u8
299 no = no + 1
300 rows = rows + 1
301 if nsp_would_shrink(prefix, rows) == 1 { nsp_refuse_shrink("put" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
302 let rc: i64 = sts_seed(prefix, neu, no)
303 if rc < 0 { nsp_werr("plane commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
304 if nsp_hist_append(prefix, actor, "put" as *u8, id, cur, olda, oldb, neu, na, nb) < 0 { nsp_werr("hist commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
305 nsp_report("PUT" as *u8, id, replaced, rows)
306 sys_exit(0)
307 return 0
308 }
309
310 if nsp_eqs("close" as *u8, verb) == 1 {
311 if argc < NSP_ARGC_CLOSE { nsp_werr("close needs <actor> <id> <note>\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
312 let actor2: *u8 = argv[NSP_F_ACTOR] as *u8
313 let id2: *u8 = argv[NSP_C_ID] as *u8
314 let cnote: *u8 = argv[NSP_C_NOTE] as *u8
315 let cur2: *u8 = sys_mmap(NSP_CAP)
316 let cn2: i64 = sts_load(prefix, cur2, NSP_CAP - NSP_MAGIC_4096)
317 if cn2 <= 0 { nsp_werr("plane EMPTY: nothing to close\n" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
318 let neu2: *u8 = sys_mmap(NSP_CAP)
319 let sp2: *i64 = sys_mmap(NSP_SP2_BYTES) as *i64
320 var no2: i64 = 0
321 var found: i64 = 0
322 var rows2: i64 = 0
323 var olda2: i64 = 0
324 var oldb2: i64 = 0
325 var na2: i64 = 0
326 var nb2: i64 = 0
327 var j: i64 = 0
328 while j < cn2 {
329 var le2: i64 = j
330 var s2: i64 = 1
331 while s2 == 1 { if le2 >= cn2 { s2 = 0 } else { if cur2[le2] == (NSP_NL as u8) { s2 = 0 } else { le2 = le2 + 1 } } }
332 if le2 > j {
333 let nc: i64 = nsp_cols(cur2, j, le2, sp2)
334 if nsp_slice_eqs(cur2, sp2[0], sp2[1], id2) == 1 {
335 found = 1
336 olda2 = j
337 oldb2 = le2
338 na2 = no2
339 // rebuild: cols 0..2 as-is, col3 = closed, cols 4..5 as-is, col6 = old-note | close-note
340 var c: i64 = 0
341 while c < nc {
342 if c == NSP_COL_STATUS {
343 no2 = ss_cat(neu2, no2, "closed" as *u8)
344 } else {
345 no2 = nsp_cat_slice(neu2, no2, cur2, sp2[c*NSP_PAIR], sp2[c*NSP_PAIR+1])
346 }
347 if c == NSP_COL_NOTE {
348 no2 = ss_cat(neu2, no2, " | " as *u8)
349 no2 = ss_cat(neu2, no2, cnote)
350 }
351 if c < nc - 1 { neu2[no2] = NSP_TAB as u8; no2 = no2 + 1 }
352 c = c + 1
353 }
354 nb2 = no2
355 neu2[no2] = NSP_NL as u8
356 no2 = no2 + 1
357 rows2 = rows2 + 1
358 } else {
359 no2 = nsp_cat_slice(neu2, no2, cur2, j, le2)
360 neu2[no2] = NSP_NL as u8
361 no2 = no2 + 1
362 rows2 = rows2 + 1
363 }
364 }
365 j = le2 + 1
366 }
367 if found == 0 { nsp_werr("REFUSED: id not in plane (close is fail-closed; store untouched)\n" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
368 if nsp_would_shrink(prefix, rows2) == 1 { nsp_refuse_shrink("close" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
369 let rc2: i64 = sts_seed(prefix, neu2, no2)
370 if rc2 < 0 { nsp_werr("plane commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
371 if nsp_hist_append(prefix, actor2, "close" as *u8, id2, cur2, olda2, oldb2, neu2, na2, nb2) < 0 { nsp_werr("hist commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
372 nsp_report("CLOSED" as *u8, id2, found, rows2)
373 sys_exit(0)
374 return 0
375 }
376
377 if nsp_eqs("putn" as *u8, verb) == 1 {
378 // BATCH put (W008 residual, 2026-07-18): N rows in ONE call -- one store commit, per-row hist.
379 // putn <actor> <ncols> <field>... (groups of ncols fields, first field of each group = id)
380 if argc < 7 { nsp_werr("putn needs <actor> <ncols> <field>... (groups of ncols, id first)\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
381 let actorn: *u8 = argv[NSP_F_ACTOR] as *u8
382 let ncols: i64 = nsp_atoi(argv[4] as *u8)
383 if ncols < 2 { nsp_werr("putn: ncols must be 2..15\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
384 if ncols > 15 { nsp_werr("putn: ncols must be 2..15\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
385 let nfields: i64 = argc - 5
386 let ngroups: i64 = nfields / ncols
387 if ngroups * ncols != nfields { nsp_werr("putn REFUSED: field count not a multiple of ncols (fail-closed, store untouched)\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
388 if ngroups < 1 { nsp_werr("putn: no groups\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
389 if ngroups > 32 { nsp_werr("putn: max 32 groups per call\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
390 var abuf: *u8 = sys_mmap(NSP_CAP)
391 var bbuf: *u8 = sys_mmap(NSP_CAP)
392 var an: i64 = sts_load(prefix, abuf, NSP_CAP - NSP_MAGIC_4096)
393 if an < 0 { an = 0 }
394 let spn: *i64 = sys_mmap(NSP_SP2_BYTES) as *i64
395 var totrows: i64 = 0
396 var g: i64 = 0
397 while g < ngroups {
398 let base: i64 = 5 + g * ncols
399 let idn: *u8 = argv[base] as *u8
400 var no: i64 = 0
401 var olda: i64 = 0
402 var oldb: i64 = 0
403 var rows: i64 = 0
404 var i2: i64 = 0
405 while i2 < an {
406 var le: i64 = i2
407 var s: i64 = 1
408 while s == 1 { if le >= an { s = 0 } else { if abuf[le] == (NSP_NL as u8) { s = 0 } else { le = le + 1 } } }
409 if le > i2 {
410 nsp_cols(abuf, i2, le, spn)
411 if nsp_slice_eqs(abuf, spn[0], spn[1], idn) == 1 { olda = i2; oldb = le } else {
412 no = nsp_cat_slice(bbuf, no, abuf, i2, le)
413 bbuf[no] = NSP_NL as u8
414 no = no + 1
415 rows = rows + 1
416 }
417 }
418 i2 = le + 1
419 }
420 let na: i64 = no
421 var f2: i64 = base
422 let flast2: i64 = base + ncols - 1
423 while f2 <= flast2 {
424 no = ss_cat(bbuf, no, argv[f2] as *u8)
425 if f2 < flast2 { bbuf[no] = NSP_TAB as u8; no = no + 1 }
426 f2 = f2 + 1
427 }
428 let nb: i64 = no
429 bbuf[no] = NSP_NL as u8
430 no = no + 1
431 rows = rows + 1
432 if nsp_hist_append(prefix, actorn, "putn" as *u8, idn, abuf, olda, oldb, bbuf, na, nb) < 0 { nsp_werr("hist commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
433 let tswap: *u8 = abuf
434 abuf = bbuf
435 bbuf = tswap
436 an = no
437 totrows = rows
438 g = g + 1
439 }
440 // putn accumulates across groups, so totrows is the final count -- same invariant as put: a BATCH of
441 // adds can only grow or hold steady. ⚠This one commits ONCE after N groups whose hist frames are
442 // ALREADY written, so an unguarded partial load here loses rows the history says were just put.
443 if nsp_would_shrink(prefix, totrows) == 1 { nsp_refuse_shrink("putn" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
444 if sts_seed(prefix, abuf, an) < 0 { nsp_werr("plane commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
445 nsp_report("PUTN-GROUPS" as *u8, actorn, ngroups, totrows)
446 sys_exit(0)
447 return 0
448 }
449
450 if nsp_eqs("setcol" as *u8, verb) == 1 {
451 if argc < NSP_ARGC_SETCOL { nsp_werr("setcol needs <actor> <id> <colidx> <value>\n" as *u8); sys_exit(NSP_EXIT_USAGE); return NSP_EXIT_USAGE }
452 let actor3: *u8 = argv[NSP_F_ACTOR] as *u8
453 let id3: *u8 = argv[NSP_C_ID] as *u8
454 let colidx: i64 = nsp_atoi(argv[5] as *u8)
455 let val: *u8 = argv[6] as *u8
456 let cur3: *u8 = sys_mmap(NSP_CAP)
457 let cn3: i64 = sts_load(prefix, cur3, NSP_CAP - NSP_MAGIC_4096)
458 if cn3 <= 0 { nsp_werr("plane EMPTY: nothing to setcol\n" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
459 let neu3: *u8 = sys_mmap(NSP_CAP)
460 let sp3: *i64 = sys_mmap(NSP_SP2_BYTES) as *i64
461 var no3: i64 = 0
462 var found3: i64 = 0
463 var rows3: i64 = 0
464 var olda3: i64 = 0
465 var oldb3: i64 = 0
466 var na3: i64 = 0
467 var nb3: i64 = 0
468 var j3: i64 = 0
469 while j3 < cn3 {
470 var le3: i64 = j3
471 var s3: i64 = 1
472 while s3 == 1 { if le3 >= cn3 { s3 = 0 } else { if cur3[le3] == (NSP_NL as u8) { s3 = 0 } else { le3 = le3 + 1 } } }
473 if le3 > j3 {
474 let nc3: i64 = nsp_cols(cur3, j3, le3, sp3)
475 if nsp_slice_eqs(cur3, sp3[0], sp3[1], id3) == 1 {
476 if colidx >= nc3 { nsp_werr("REFUSED: colidx past the row (setcol is fail-closed; store untouched)\n" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
477 found3 = 1
478 olda3 = j3
479 oldb3 = le3
480 na3 = no3
481 var c3: i64 = 0
482 while c3 < nc3 {
483 if c3 == colidx { no3 = ss_cat(neu3, no3, val) } else { no3 = nsp_cat_slice(neu3, no3, cur3, sp3[c3*NSP_PAIR], sp3[c3*NSP_PAIR+1]) }
484 if c3 < nc3 - 1 { neu3[no3] = NSP_TAB as u8; no3 = no3 + 1 }
485 c3 = c3 + 1
486 }
487 nb3 = no3
488 neu3[no3] = NSP_NL as u8
489 no3 = no3 + 1
490 rows3 = rows3 + 1
491 } else {
492 no3 = nsp_cat_slice(neu3, no3, cur3, j3, le3)
493 neu3[no3] = NSP_NL as u8
494 no3 = no3 + 1
495 rows3 = rows3 + 1
496 }
497 }
498 j3 = le3 + 1
499 }
500 if found3 == 0 { nsp_werr("REFUSED: id not in plane (setcol is fail-closed; store untouched)\n" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
501 if nsp_would_shrink(prefix, rows3) == 1 { nsp_refuse_shrink("setcol" as *u8); sys_exit(NSP_EXIT_REFUSED); return NSP_EXIT_REFUSED }
502 let rc3: i64 = sts_seed(prefix, neu3, no3)
503 if rc3 < 0 { nsp_werr("plane commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
504 if nsp_hist_append(prefix, actor3, "setcol" as *u8, id3, cur3, olda3, oldb3, neu3, na3, nb3) < 0 { nsp_werr("hist commit error\n" as *u8); sys_exit(NSP_EXIT_IO); return NSP_EXIT_IO }
505 nsp_report("SETCOL" as *u8, id3, found3, rows3)
506 sys_exit(0)
507 return 0
508 }
509
510 nsp_werr("usage: nx_store_put <prefix> {put <actor> <id> <field>... | setcol <actor> <id> <colidx> <value> | close <actor> <id> <note> | load | hist}\n" as *u8)
511 sys_exit(NSP_EXIT_USAGE)
512 return NSP_EXIT_USAGE
513}