code wiki / _hdl_build / nx_jrnlguard.nx
nx_jrnlguard.nx source
↩ module page · 541 lines · 21717 B
1// nx_jrnlguard.nx -- DATA-LAYER CLOBBER/SHRINK DETECTOR (ws=ws-intel9, 2026-07-20).
2// The ecosystem guards its ROUTES (nx_route_diff), its TOOLS (nx_tooldiff), its BUILDS and
3// its CRONS -- but nothing guards the append-only JOURNALS AND PLANES that a dozen concurrent
4// sessions write all day (ws_sync, claims, pm_intake, actlog, every knowledge/store plane).
5// This is the data-layer twin, and it eats the filed anti-clobber CLASS (F744-F748).
6// CONTRACT: an append-only journal may only GROW. So:
7// bytes shrink -> RED (truncation / overwrite)
8// frames shrink -> RED (lines lost)
9// ANY per-key count DECREASES -> RED <-- THE SUBTLE ONE: a rewrite can ADD bytes while
10// silently DROPPING frames, so a totals-only check passes while data is already gone.
11// (Observed for real on the memory index 2026-07-20: it grew and lost entries at once.)
12// malformed lines rise -> WARN (corruption creeping in; a column-shifted row emits invalid
13// downstream JSON -- observed live in the commontask catalog this same session)
14// A shrink is REPORTED, never "repaired": if the shrink was an INTENTIONAL compaction, the
15// operator re-snapshots the baseline (the same discipline nx_route_diff uses for routes).
16// key column is an ARGUMENT (default 1) so one organ guards every journal shape.
17// SCALE LAW: windowed read + declared envelope (window_bytes/truncated/keys_capped).
18// license_tier: ORIGINAL No hw writes (Rule 26).
19// snapshot <journal> <baseline> [keycol] -> write baseline, JSON summary
20// check <journal> <baseline> [keycol] -> JSON verdict; exit 0 GREEN / 3 RED / 4 no-baseline
21// scan <journal> [keycol] -> structural audit JSON (READ-ONLY)
22// selftest <scratch-base> -> gate T1..T8 (caller pre-cleans)
23import "nx_syscalls.nx"
24import "nx_gate_verdict.nx"
25const JG_MAGIC_1024: i64 = 1024
26
27const JG_WIN: i64 = 4194304
28const JG_BASE: i64 = 262144
29const JG_OUT: i64 = 262144
30const JG_SOFT: i64 = 49152
31const JG_KEY_MAX: i64 = 120
32const JG_MIN_COLS: i64 = 3
33const JG_KEYS_CAP: i64 = 200
34
35func jg_cat(d: *u8, o: i64, s: *u8) -> i64 { var p: i64=o; var i: i64=0; while s[i]!=(0 as u8){ d[p]=s[i]; p=p+1; i=i+1 } return p }
36func jg_catn(d: *u8, o: i64, v: i64) -> i64 {
37 let t: *u8 = sys_mmap(28)
38 var m: i64 = v
39 var p: i64 = o
40 if m < 0 { d[p]=45 as u8; p=p+1; m=0-m }
41 var k: i64 = 0
42 if m == 0 { t[0]=48 as u8; k=1 }
43 while m > 0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
44 var i: i64 = 0
45 while i < k { d[p]=t[k-1-i]; p=p+1; i=i+1 }
46 return p
47}
48func jg_vlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
49func jg_atoi_z(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v }
50func jg_read(path: *u8, buf: *u8, cap: i64) -> i64 {
51 let fd: i64 = sys_openat_rd(path)
52 if fd < 0 { return -1 }
53 var n: i64 = 0
54 var go: i64 = 1
55 while go == 1 { let r: i64 = sys_read(fd, ((buf as i64)+n) as *u8, cap-n); if r <= 0 { go = 0 } else { n = n + r } if n >= cap { go = 0 } }
56 sys_close(fd)
57 return n
58}
59func jg_le(q: *u8, i: i64, n: i64) -> i64 {
60 var le: i64 = i
61 var s: i64 = 1
62 while s==1 { if le>=n { s=0 } else { if q[le]==(10 as u8){s=0} else {le=le+1} } }
63 return le
64}
65func jg_col(q: *u8, ls: i64, le: i64, c: i64, out: *i64) -> i64 {
66 var col: i64 = 0
67 var p: i64 = ls
68 while col < c {
69 var s: i64 = 1
70 while s == 1 { if p >= le { return 0 } if q[p]==(9 as u8) { s = 0 } else { p = p+1 } }
71 p = p + 1
72 col = col + 1
73 }
74 var e: i64 = p
75 var s2: i64 = 1
76 while s2 == 1 { if e >= le { s2 = 0 } else { if q[e]==(9 as u8) { s2 = 0 } else { e = e+1 } } }
77 out[0] = p
78 out[1] = e
79 return 1
80}
81func jg_ncols(q: *u8, ls: i64, le: i64) -> i64 {
82 if le <= ls { return 0 }
83 var c: i64 = 1
84 var i: i64 = ls
85 while i < le { if q[i]==(9 as u8) { c = c + 1 } i = i + 1 }
86 return c
87}
88func jg_span_eq(q: *u8, s1: i64, e1: i64, s2: i64, e2: i64) -> i64 {
89 if e1-s1 != e2-s2 { return 0 }
90 var i: i64 = 0
91 while s1+i < e1 { if q[s1+i]!=q[s2+i] { return 0 } i=i+1 }
92 return 1
93}
94func jg_lit_eq(q: *u8, s: i64, e: i64, lit: *u8) -> i64 {
95 var i: i64 = 0
96 while s+i < e { if lit[i]==(0 as u8) { return 0 } if q[s+i]!=lit[i] { return 0 } i=i+1 }
97 if lit[i]!=(0 as u8) { return 0 }
98 return 1
99}
100// cross-buffer span compare (baseline key text vs journal key text)
101func jg_cross_eq(a: *u8, as1: i64, ae: i64, b: *u8, bs: i64, be: i64) -> i64 {
102 if ae-as1 != be-bs { return 0 }
103 var i: i64 = 0
104 while as1+i < ae { if a[as1+i]!=b[bs+i] { return 0 } i=i+1 }
105 return 1
106}
107// a line counts as a FRAME if it is non-empty and not a comment
108func jg_is_frame(q: *u8, ls: i64, le: i64) -> i64 {
109 if le <= ls { return 0 }
110 if q[ls]==(35 as u8) { return 0 }
111 return 1
112}
113func jg_frames(q: *u8, n: i64) -> i64 {
114 var c: i64 = 0
115 var i: i64 = 0
116 while i < n { let le: i64 = jg_le(q,i,n); if jg_is_frame(q,i,le)==1 { c = c + 1 } i = le + 1 }
117 return c
118}
119func jg_malformed(q: *u8, n: i64) -> i64 {
120 var c: i64 = 0
121 var i: i64 = 0
122 while i < n {
123 let le: i64 = jg_le(q,i,n)
124 if jg_is_frame(q,i,le)==1 { if jg_ncols(q,i,le) < JG_MIN_COLS { c = c + 1 } }
125 i = le + 1
126 }
127 return c
128}
129// first occurrence of this key span among frames before `upto`?
130func jg_key_first(q: *u8, upto: i64, keycol: i64, ks: i64, ke: i64) -> i64 {
131 let ck: *i64 = sys_mmap(16) as *i64
132 var i: i64 = 0
133 while i < upto {
134 let le: i64 = jg_le(q,i,upto)
135 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_span_eq(q,ck[0],ck[1],ks,ke)==1 { return 0 } } }
136 i = le + 1
137 }
138 return 1
139}
140func jg_key_count(q: *u8, n: i64, keycol: i64, ks: i64, ke: i64) -> i64 {
141 let ck: *i64 = sys_mmap(16) as *i64
142 var c: i64 = 0
143 var i: i64 = 0
144 while i < n {
145 let le: i64 = jg_le(q,i,n)
146 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_span_eq(q,ck[0],ck[1],ks,ke)==1 { c = c + 1 } } }
147 i = le + 1
148 }
149 return c
150}
151// count of a key given as literal text in ANOTHER buffer (baseline -> journal lookup)
152func jg_key_count_x(q: *u8, n: i64, keycol: i64, b: *u8, bs: i64, be: i64) -> i64 {
153 let ck: *i64 = sys_mmap(16) as *i64
154 var c: i64 = 0
155 var i: i64 = 0
156 while i < n {
157 let le: i64 = jg_le(q,i,n)
158 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_cross_eq(q,ck[0],ck[1],b,bs,be)==1 { c = c + 1 } } }
159 i = le + 1
160 }
161 return c
162}
163// ---------- baseline write ----------
164func jg_write_baseline(path: *u8, q: *u8, n: i64, keycol: i64, cap_hit: *i64) -> i64 {
165 let d: *u8 = sys_mmap(JG_OUT)
166 var p: i64 = 0
167 p = jg_cat(d,p,"# nx_jrnlguard baseline v1 -- keys prefixed __ are reserved totals\n" as *u8)
168 p = jg_cat(d,p,"__bytes__\t" as *u8)
169 p = jg_catn(d,p,n)
170 d[p]=10 as u8; p=p+1
171 p = jg_cat(d,p,"__frames__\t" as *u8)
172 p = jg_catn(d,p,jg_frames(q,n))
173 d[p]=10 as u8; p=p+1
174 p = jg_cat(d,p,"__malformed__\t" as *u8)
175 p = jg_catn(d,p,jg_malformed(q,n))
176 d[p]=10 as u8; p=p+1
177 p = jg_cat(d,p,"__keycol__\t" as *u8)
178 p = jg_catn(d,p,keycol)
179 d[p]=10 as u8; p=p+1
180 let ck: *i64 = sys_mmap(16) as *i64
181 var keys: i64 = 0
182 cap_hit[0] = 0
183 var i: i64 = 0
184 while i < n {
185 let le: i64 = jg_le(q,i,n)
186 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_key_first(q,i,keycol,ck[0],ck[1])==1 {
187 if keys >= JG_KEYS_CAP { cap_hit[0] = cap_hit[0] + 1 } else {
188 var z: i64 = ck[0]
189 var used: i64 = 0
190 while z < ck[1] { if used < JG_KEY_MAX { d[p]=q[z]; p=p+1; used=used+1 } z=z+1 }
191 d[p]=9 as u8; p=p+1
192 p = jg_catn(d,p,jg_key_count(q,n,keycol,ck[0],ck[1]))
193 d[p]=10 as u8; p=p+1
194 keys = keys + 1
195 }
196 } } }
197 i = le + 1
198 }
199 let fd: i64 = sys_openat_wr(path, 0x1a4)
200 if fd < 0 { return -1 }
201 sys_write(fd,d,p)
202 sys_close(fd)
203 return keys
204}
205func jg_base_num(b: *u8, bn: i64, name: *u8) -> i64 {
206 let c0: *i64 = sys_mmap(16) as *i64
207 let c1: *i64 = sys_mmap(16) as *i64
208 var i: i64 = 0
209 while i < bn {
210 let le: i64 = jg_le(b,i,bn)
211 if jg_is_frame(b,i,le)==1 { if jg_col(b,i,le,0,c0)==1 { if jg_lit_eq(b,c0[0],c0[1],name)==1 {
212 if jg_col(b,i,le,1,c1)==1 { var v: i64 = 0; var z: i64 = c1[0]; while z < c1[1] { let ch: i64 = b[z] as i64; if ch>=48 { if ch<=57 { v = v*10 + (ch-48) } } z=z+1 } return v }
213 } } }
214 i = le + 1
215 }
216 return -1
217}
218func jg_key(d: *u8, o: i64, name: *u8) -> i64 {
219 d[o]=34 as u8
220 var p: i64 = o + 1
221 p = jg_cat(d,p,name)
222 d[p]=34 as u8
223 p = p + 1
224 d[p]=58 as u8
225 p = p + 1
226 return p
227}
228func jg_qlit(d: *u8, o: i64, s: *u8) -> i64 {
229 var p: i64 = o
230 d[p]=34 as u8; p=p+1
231 p = jg_cat(d,p,s)
232 d[p]=34 as u8; p=p+1
233 return p
234}
235func jg_jesc(d: *u8, o: i64, q: *u8, s: i64, e: i64, maxb: i64) -> i64 {
236 var p: i64 = o
237 var i: i64 = s
238 var used: i64 = 0
239 while i < e {
240 if used >= maxb { i = e } else {
241 let c: i64 = q[i] as i64
242 if c == 34 { d[p]=92 as u8; p=p+1; d[p]=34 as u8; p=p+1 } else { if c == 92 { d[p]=92 as u8; p=p+1; d[p]=92 as u8; p=p+1 } else { if c < 32 { d[p]=32 as u8; p=p+1 } else { d[p]=c as u8; p=p+1 } } }
243 used = used + 2
244 i = i + 1
245 }
246 }
247 return p
248}
249// ---------- the verdict ----------
250// vx[0]=bytes_now vx[1]=bytes_base vx[2]=frames_now vx[3]=frames_base
251// vx[4]=malformed_now vx[5]=malformed_base vx[6]=keys_decreased vx[7]=keys_checked vx[8]=red
252func jg_verdict(q: *u8, n: i64, b: *u8, bn: i64, keycol: i64, vx: *i64, d: *u8, dpos: i64) -> i64 {
253 var p: i64 = dpos
254 vx[0]=n
255 vx[1]=jg_base_num(b,bn,"__bytes__" as *u8)
256 vx[2]=jg_frames(q,n)
257 vx[3]=jg_base_num(b,bn,"__frames__" as *u8)
258 vx[4]=jg_malformed(q,n)
259 vx[5]=jg_base_num(b,bn,"__malformed__" as *u8)
260 vx[6]=0
261 vx[7]=0
262 vx[8]=0
263 if vx[0] < vx[1] { vx[8] = 1 }
264 if vx[2] < vx[3] { vx[8] = 1 }
265 // per-key regression sweep: THE subtle clobber tooth
266 p = jg_key(d,p,"regressed_keys" as *u8)
267 d[p]=91 as u8; p=p+1
268 let c0: *i64 = sys_mmap(16) as *i64
269 let c1: *i64 = sys_mmap(16) as *i64
270 var first: i64 = 1
271 var i: i64 = 0
272 while i < bn {
273 let le: i64 = jg_le(b,i,bn)
274 if jg_is_frame(b,i,le)==1 { if jg_col(b,i,le,0,c0)==1 {
275 var reserved: i64 = 0
276 if c0[1]-c0[0] >= 2 { if b[c0[0]]==(95 as u8) { if b[c0[0]+1]==(95 as u8) { reserved = 1 } } }
277 if reserved == 0 { if jg_col(b,i,le,1,c1)==1 {
278 var basec: i64 = 0
279 var z: i64 = c1[0]
280 while z < c1[1] { let ch: i64 = b[z] as i64; if ch>=48 { if ch<=57 { basec = basec*10 + (ch-48) } } z=z+1 }
281 let nowc: i64 = jg_key_count_x(q,n,keycol,b,c0[0],c0[1])
282 vx[7] = vx[7] + 1
283 if nowc < basec {
284 vx[6] = vx[6] + 1
285 vx[8] = 1
286 if p < JG_SOFT {
287 if first==0 { d[p]=44 as u8; p=p+1 }
288 first=0
289 d[p]=123 as u8; p=p+1
290 p = jg_key(d,p,"key" as *u8)
291 d[p]=34 as u8; p=p+1
292 p = jg_jesc(d,p,b,c0[0],c0[1],JG_KEY_MAX)
293 d[p]=34 as u8; p=p+1
294 d[p]=44 as u8; p=p+1
295 p = jg_key(d,p,"was" as *u8)
296 p = jg_catn(d,p,basec)
297 d[p]=44 as u8; p=p+1
298 p = jg_key(d,p,"now" as *u8)
299 p = jg_catn(d,p,nowc)
300 d[p]=125 as u8; p=p+1
301 }
302 }
303 } }
304 } }
305 i = le + 1
306 }
307 d[p]=93 as u8; p=p+1
308 d[p]=44 as u8; p=p+1
309 return p
310}
311func jg_emit_common(d: *u8, dpos: i64, jp: *u8, n: i64, keycol: i64, trunc: i64, mode: *u8) -> i64 {
312 var p: i64 = dpos
313 d[p]=123 as u8; p=p+1
314 p = jg_key(d,p,"v" as *u8)
315 p = jg_catn(d,p,1)
316 d[p]=44 as u8; p=p+1
317 p = jg_key(d,p,"tool" as *u8)
318 p = jg_qlit(d,p,"nx_jrnlguard" as *u8)
319 d[p]=44 as u8; p=p+1
320 p = jg_key(d,p,"mode" as *u8)
321 p = jg_qlit(d,p,mode)
322 d[p]=44 as u8; p=p+1
323 p = jg_key(d,p,"journal" as *u8)
324 d[p]=34 as u8; p=p+1
325 p = jg_jesc(d,p,jp,0,jg_vlen(jp),200)
326 d[p]=34 as u8; p=p+1
327 d[p]=44 as u8; p=p+1
328 p = jg_key(d,p,"keycol" as *u8)
329 p = jg_catn(d,p,keycol)
330 d[p]=44 as u8; p=p+1
331 p = jg_key(d,p,"window_bytes" as *u8)
332 p = jg_catn(d,p,JG_WIN)
333 d[p]=44 as u8; p=p+1
334 p = jg_key(d,p,"truncated" as *u8)
335 p = jg_catn(d,p,trunc)
336 d[p]=44 as u8; p=p+1
337 return p
338}
339func jg_wr(path: *u8, body: *u8) -> i64 {
340 let fd: i64 = sys_openat_wr(path, 0x1a4)
341 if fd < 0 { return -1 }
342 sys_write(fd, body, jg_vlen(body))
343 sys_close(fd)
344 return 0
345}
346func jg_mkpath(dst: *u8, src: *u8, suf: *u8) -> i64 {
347 var p: i64 = 0
348 p = jg_cat(dst,p,src)
349 p = jg_cat(dst,p,suf)
350 dst[p]=0 as u8
351 return p
352}
353func jg_find(d: *u8, dn: i64, lit: *u8) -> i64 {
354 let ll: i64 = jg_vlen(lit)
355 if ll == 0 { return 0 }
356 var i: i64 = 0
357 while i + ll <= dn {
358 var k: i64 = 0
359 var ok: i64 = 1
360 while k < ll { if d[i+k]!=lit[k] { ok=0; k=ll } else { k=k+1 } }
361 if ok==1 { return 1 }
362 i = i + 1
363 }
364 return 0
365}
366
367// ---------- selftest gate ----------
368func jg_selftest(base: *u8) -> i64 {
369 let ctr: *i64 = gv_ctr()
370 gv_head("nx_jrnlguard selftest -- data-layer clobber teeth (caller pre-cleans paths)" as *u8)
371 let q: *u8 = sys_mmap(JG_WIN)
372 let bb: *u8 = sys_mmap(JG_BASE)
373 let vx: *i64 = sys_mmap(128) as *i64
374 let ch: *i64 = sys_mmap(16) as *i64
375 let d: *u8 = sys_mmap(JG_OUT)
376 let jp: *u8 = sys_mmap(JG_MAGIC_1024)
377 let bp: *u8 = sys_mmap(JG_MAGIC_1024)
378 let rcap: i64 = JG_WIN - 8
379 jg_mkpath(jp, base, ".jrnl" as *u8)
380 jg_mkpath(bp, base, ".base" as *u8)
381 // T1 absent journal -> honest ABSENT (-1), no fabrication
382 var n: i64 = jg_read(jp,q,rcap)
383 var ok1: i64 = 0
384 if n == (0-1) { ok1=1 }
385 gv_check("T1 absent journal reported ABSENT never fabricated" as *u8, ok1, ctr)
386 // seed a 4-frame journal, 2 verbs
387 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\n2\tBEAT\ta\tx\tnote2\n3\tBEAT\ta\tx\tnote3\n4\tDONE\ta\tx\tnote4\n" as *u8)
388 n = jg_read(jp,q,rcap)
389 let keys: i64 = jg_write_baseline(bp,q,n,1,ch)
390 var bn: i64 = jg_read(bp,bb,JG_BASE-8)
391 var ok2: i64 = 0
392 if keys==3 { if jg_base_num(bb,bn,"__frames__" as *u8)==4 { ok2=1 } }
393 gv_check("T2 baseline records frames and distinct keys" as *u8, ok2, ctr)
394 // T3 unchanged -> GREEN
395 jg_verdict(q,n,bb,bn,1,vx,d,0)
396 var ok3: i64 = 0
397 if vx[8]==0 { ok3=1 }
398 gv_check("T3 unchanged journal verdict GREEN" as *u8, ok3, ctr)
399 // T4 lawful append-only growth -> GREEN
400 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\n2\tBEAT\ta\tx\tnote2\n3\tBEAT\ta\tx\tnote3\n4\tDONE\ta\tx\tnote4\n5\tBEAT\ta\tx\tnote5\n" as *u8)
401 n = jg_read(jp,q,rcap)
402 jg_verdict(q,n,bb,bn,1,vx,d,0)
403 var ok4: i64 = 0
404 if vx[8]==0 { if vx[2]==5 { ok4=1 } }
405 gv_check("T4 append-only growth stays GREEN" as *u8, ok4, ctr)
406 // T5 TRUNCATION (bytes+frames shrink) -> RED
407 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\n" as *u8)
408 n = jg_read(jp,q,rcap)
409 jg_verdict(q,n,bb,bn,1,vx,d,0)
410 var ok5: i64 = 0
411 if vx[8]==1 { if vx[2] < vx[3] { ok5=1 } }
412 gv_check("T5 truncation detected RED" as *u8, ok5, ctr)
413 // T6 THE SUBTLE CLOBBER: file GROWS in bytes+frames but a key's frames were DROPPED
414 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1-rewritten-with-much-more-text-than-before\n9\tNOTE\ta\tx\tbrand new frame\n8\tNOTE\ta\tx\tanother brand new frame\n7\tNOTE\ta\tx\tand another one here\n6\tNOTE\ta\tx\tand yet another padding frame\n" as *u8)
415 n = jg_read(jp,q,rcap)
416 let dl6: i64 = jg_verdict(q,n,bb,bn,1,vx,d,0)
417 var ok6: i64 = 0
418 if vx[0] > vx[1] { if vx[2] >= vx[3] { if vx[8]==1 { if vx[6]==2 { ok6=1 } } } }
419 gv_check("T6 grew in bytes+frames yet lost keys detected RED" as *u8, ok6, ctr)
420 // T7 the RED report names the lost keys with was/now counts
421 var ok7: i64 = 0
422 if dl6 > 0 { if jg_find(d,dl6,"BEAT" as *u8)==1 { if jg_find(d,dl6,"DONE" as *u8)==1 { if jg_find(d,dl6,"was" as *u8)==1 { ok7=1 } } } }
423 gv_check("T7 report names each lost key with was and now" as *u8, ok7, ctr)
424 // T8 malformed-line audit counts column-shifted corruption
425 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\nthis-line-has-no-tabs-at-all\n2\tBEAT\ta\tx\tnote2\n" as *u8)
426 n = jg_read(jp,q,rcap)
427 var ok8: i64 = 0
428 if jg_malformed(q,n)==1 { if jg_frames(q,n)==3 { ok8=1 } }
429 gv_check("T8 malformed corrupted line audited not silently parsed" as *u8, ok8, ctr)
430 let rc: i64 = gv_verdict("JRNLGUARD-GATE" as *u8, ctr, "data-layer clobber teeth green" as *u8)
431 return rc
432}
433
434func main(argc: i64, argv: *i64) -> i64 {
435 if argc < 3 { gv_puts("usage: nx_jrnlguard {snapshot <journal> <baseline> [keycol] | check <journal> <baseline> [keycol] | scan <journal> [keycol] | selftest <base>}\n" as *u8); sys_exit(2); return 2 }
436 let verb: *u8 = argv[1] as *u8
437 let vl: i64 = jg_vlen(verb)
438 if jg_lit_eq(verb,0,vl,"selftest" as *u8)==1 { let rc: i64 = jg_selftest(argv[2] as *u8); sys_exit(rc); return rc }
439 let jp: *u8 = argv[2] as *u8
440 let q: *u8 = sys_mmap(JG_WIN)
441 let rcap: i64 = JG_WIN - 8
442 let nr: i64 = jg_read(jp,q,rcap)
443 if nr < 0 { gv_puts("NX-JRNLGUARD ABSENT journal=" as *u8); gv_puts(jp); gv_puts("\n" as *u8); sys_exit(4); return 4 }
444 let n: i64 = nr
445 var trunc: i64 = 0
446 if n >= rcap { trunc = 1 }
447 let d: *u8 = sys_mmap(JG_OUT)
448 let ch: *i64 = sys_mmap(16) as *i64
449 if jg_lit_eq(verb,0,vl,"scan" as *u8)==1 {
450 var kc: i64 = 1
451 if argc >= 4 { let v: i64 = jg_atoi_z(argv[3] as *u8); if v >= 0 { kc = v } }
452 var p: i64 = jg_emit_common(d,0,jp,n,kc,trunc,"scan" as *u8)
453 p = jg_key(d,p,"bytes" as *u8)
454 p = jg_catn(d,p,n)
455 d[p]=44 as u8; p=p+1
456 p = jg_key(d,p,"frames" as *u8)
457 p = jg_catn(d,p,jg_frames(q,n))
458 d[p]=44 as u8; p=p+1
459 p = jg_key(d,p,"malformed" as *u8)
460 p = jg_catn(d,p,jg_malformed(q,n))
461 d[p]=125 as u8; p=p+1
462 d[p]=10 as u8; p=p+1
463 sys_write(1,d,p)
464 sys_exit(0)
465 return 0
466 }
467 if argc < 4 { gv_puts("snapshot|check need <journal> <baseline>\n" as *u8); sys_exit(2); return 2 }
468 let bp: *u8 = argv[3] as *u8
469 var kc2: i64 = 1
470 if argc >= 5 { let v2: i64 = jg_atoi_z(argv[4] as *u8); if v2 >= 0 { kc2 = v2 } }
471 if jg_lit_eq(verb,0,vl,"snapshot" as *u8)==1 {
472 let keys: i64 = jg_write_baseline(bp,q,n,kc2,ch)
473 if keys < 0 { gv_puts("NX-JRNLGUARD BASELINE-WRITE-FAILED\n" as *u8); sys_exit(1); return 1 }
474 var p: i64 = jg_emit_common(d,0,jp,n,kc2,trunc,"snapshot" as *u8)
475 p = jg_key(d,p,"bytes" as *u8)
476 p = jg_catn(d,p,n)
477 d[p]=44 as u8; p=p+1
478 p = jg_key(d,p,"frames" as *u8)
479 p = jg_catn(d,p,jg_frames(q,n))
480 d[p]=44 as u8; p=p+1
481 p = jg_key(d,p,"distinct_keys" as *u8)
482 p = jg_catn(d,p,keys)
483 d[p]=44 as u8; p=p+1
484 p = jg_key(d,p,"keys_capped" as *u8)
485 p = jg_catn(d,p,ch[0])
486 d[p]=44 as u8; p=p+1
487 p = jg_key(d,p,"malformed" as *u8)
488 p = jg_catn(d,p,jg_malformed(q,n))
489 d[p]=125 as u8; p=p+1
490 d[p]=10 as u8; p=p+1
491 sys_write(1,d,p)
492 sys_exit(0)
493 return 0
494 }
495 if jg_lit_eq(verb,0,vl,"check" as *u8)==1 {
496 let bb: *u8 = sys_mmap(JG_BASE)
497 let bnr: i64 = jg_read(bp,bb,JG_BASE-8)
498 if bnr <= 0 { gv_puts("NX-JRNLGUARD NO-BASELINE (run snapshot first; fail-closed, nothing judged)\n" as *u8); sys_exit(4); return 4 }
499 let vx: *i64 = sys_mmap(128) as *i64
500 var p: i64 = jg_emit_common(d,0,jp,n,kc2,trunc,"check" as *u8)
501 p = jg_verdict(q,n,bb,bnr,kc2,vx,d,p)
502 p = jg_key(d,p,"bytes_now" as *u8)
503 p = jg_catn(d,p,vx[0])
504 d[p]=44 as u8; p=p+1
505 p = jg_key(d,p,"bytes_baseline" as *u8)
506 p = jg_catn(d,p,vx[1])
507 d[p]=44 as u8; p=p+1
508 p = jg_key(d,p,"frames_now" as *u8)
509 p = jg_catn(d,p,vx[2])
510 d[p]=44 as u8; p=p+1
511 p = jg_key(d,p,"frames_baseline" as *u8)
512 p = jg_catn(d,p,vx[3])
513 d[p]=44 as u8; p=p+1
514 p = jg_key(d,p,"malformed_now" as *u8)
515 p = jg_catn(d,p,vx[4])
516 d[p]=44 as u8; p=p+1
517 p = jg_key(d,p,"malformed_baseline" as *u8)
518 p = jg_catn(d,p,vx[5])
519 d[p]=44 as u8; p=p+1
520 p = jg_key(d,p,"keys_checked" as *u8)
521 p = jg_catn(d,p,vx[7])
522 d[p]=44 as u8; p=p+1
523 p = jg_key(d,p,"keys_regressed" as *u8)
524 p = jg_catn(d,p,vx[6])
525 d[p]=44 as u8; p=p+1
526 p = jg_key(d,p,"verdict" as *u8)
527 if vx[8]==1 { p = jg_qlit(d,p,"RED" as *u8) } else { p = jg_qlit(d,p,"GREEN" as *u8) }
528 d[p]=44 as u8; p=p+1
529 p = jg_key(d,p,"note" as *u8)
530 p = jg_qlit(d,p,"append-only journals may only grow; a RED means truncation or dropped frames -- if the shrink was an INTENTIONAL compaction, re-run snapshot to re-baseline" as *u8)
531 d[p]=125 as u8; p=p+1
532 d[p]=10 as u8; p=p+1
533 sys_write(1,d,p)
534 if vx[8]==1 { sys_exit(3); return 3 }
535 sys_exit(0)
536 return 0
537 }
538 gv_puts("unknown verb\n" as *u8)
539 sys_exit(2)
540 return 2
541}