code wiki / _hdl_build / nx_staging_guard.nx
nx_staging_guard.nx source
↩ module page · 415 lines · 18458 B
1// nx_staging_guard.nx -- REFUSE A PROMOTE THAT WOULD WALK A TARGET BACKWARDS.
2//
3// WHY (measured 2026-08-06): the deploy queue is armed to destroy work. md_promote_staged already
4// carries md_prov_ok, which refuses a promote whose CONTENT this target ALREADY HELD (seq1484).
5// That guard is blind to the COMMON CASE: a staged .new that was never itself promoted is a FIRST
6// SIGHTING, so md_prov_ok records it as the baseline and returns ALLOW -- even when the staged file
7// PREDATES the live binary it would replace. A .new older than its live .elf is not a pending
8// upgrade, it is A ROLLBACK WEARING AN UPGRADE FILENAME, and nothing in the staging area marks it
9// as one. It sits there looking deployable forever.
10//
11// *SIZE IS A SCREEN, NOT PROOF (nx_treecanon_gate:15, and the banked law: a byte DECREASE after an
12// addition is a REVERT). A legitimate refactor can shrink a binary, and an operator-swap fix is
13// BYTE-IDENTICAL. So this module never decides on size alone: it also measures CAPABILITY-TOKEN
14// LOSS -- how many long printable tokens present in the LIVE binary are ABSENT from the staged one.
15// That is the honest reading of would-lose-capability; size is only the cheap first screen.
16// (2026-08-18: that sentence became TRUE IN CODE, not only in prose -- see the SHRINK note in
17// sg_classify. Whole-program DCE in nx_cc made "smaller" the normal direction of every rebuild.)
18//
19// *IT REFUSES, IT NEVER DELETES. Every verdict is fail-closed and reversible: a refusal leaves live,
20// .prev and the staged file untouched (Rule 13 additive-only, Rule 26 never-brick). Nothing in this
21// module writes persistent state of any kind.
22//
23// *THRESHOLDS ARE DATA (Rule 11), read from knowledge/staging_hygiene.conf; the consts below are
24// bootstrap defaults only, and every one of them is overridable without a rebuild (Rule 17).
25//
26// DIALECT: this module is IMPORTED by nx_mgmt_data -> nx_mgmt_api, so it must compile under today's
27// hub compiler: plain-if (NO else -- seq533 desyncs the imported parser), no empty string literals
28// (seq907 aliases the next literal), <=6 params (seq239). ONE implementation, every consumer
29// (Rule 15) -- a guard that only the CLI imports is not a guard the deploy plane has.
30// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
31import "nx_syscalls.nx"
32import "_hdl_build/nx_contentdiff_lib.nx" // THE content ruler -- shared with the nx_contentdiff oracle, never a second copy
33
34const SG_STAT_SIZE_OFF: i64 = 48
35const SG_STAT_MTIME_OFF: i64 = 88
36const SG_STATBUF: i64 = 256
37const SG_DAY: i64 = 86400
38const SG_FILECAP: i64 = 8388608
39const SG_CONFCAP: i64 = 8192
40
41// ---- sealed verdict classes ----
42const SG_NONE: i64 = 0
43const SG_FORWARD: i64 = 1
44const SG_ORPHAN: i64 = 2
45const SG_BACKDATE: i64 = 3
46const SG_EXPIRED: i64 = 4
47const SG_SHRINK: i64 = 5
48const SG_CAPLOSS: i64 = 6
49const SG_UNQUALIFIED:i64=7
50
51// ---- out[] slots ----
52const SG_O_LIVESZ: i64 = 0
53const SG_O_NEWSZ: i64 = 1
54const SG_O_LIVEMT: i64 = 2
55const SG_O_NEWMT: i64 = 3
56const SG_O_LOSTPERMIL: i64 = 4
57const SG_O_AGEDAYS: i64 = 5
58const SG_O_FLAGS: i64 = 6
59const SG_O_CHECKED: i64 = 7
60const SG_O_MISSING: i64 = 8
61const SG_O_SHRINKPERMIL: i64 = 9
62// 1 when staged and live are BYTE-IDENTICAL. Distinguishes a harmless re-stage from the mutation-class
63// blind spot: same size + differing content + zero capability delta is the signature nothing else here sees.
64const SG_O_IDENTICAL: i64 = 10
65const SG_O_GAINED: i64 = 11 // tokens staged has that live lacks -- the half a loss detector cannot see
66const SG_O_LIVERUNS: i64 = 12 // qualifying runs in live
67const SG_O_CANDRUNS: i64 = 13 // qualifying runs in staged; >= liveruns means nothing was net-removed
68const SG_O_CONTENT_STATE:i64=14
69const SG_O_SOURCE_REMOVED:i64=15
70const SG_OUT_SLOTS: i64 = 16
71
72// ---- cfg[] slots ----
73const SG_C_MAXAGE: i64 = 0
74const SG_C_SHRINKTOL: i64 = 1
75const SG_C_CAPLOSS: i64 = 2
76const SG_C_MINTOK: i64 = 3
77const SG_C_MAXTOK: i64 = 4
78const SG_C_CAPLOSSABS: i64 = 5
79const SG_C_CAPLOSSMIN: i64 = 6
80const SG_C_SLOTS: i64 = 8
81
82// ---- bootstrap defaults (lowest priority; conf overrides each) ----
83const SG_D_MAXAGE: i64 = 14
84const SG_D_SHRINKTOL: i64 = 20
85// DELIBERATELY DIFFERENT from the shipped knowledge/staging_hygiene.conf value. If these two were equal,
86// a gate asserting the conf was read would be VACUOUS -- it would pass identically when the file is
87// missing. The conf is what operates; this is only the no-file fallback.
88const SG_D_CAPLOSS: i64 = 25
89// ABSOLUTE floor, OR'd with the permil one. TUNED FROM THE MEASURED DISTRIBUTION, not from taste:
90// permil alone is SIZE-DEPENDENT (one lost run is 24 permil in a 41-run binary but 4 permil in a 250-run
91// one), so a single threshold cannot separate an ordinary string edit from a real capability loss. It
92// caught its own author within the hour: an edit that renamed ONE report line scored 24 permil and this
93// guard refused the build that contained it. THAT MATTERS MORE THAN THE FALSE POSITIVE -- a guard which
94// fires on routine work teaches operators to pass the override by reflex, and a bypass that becomes
95// routine is a guard that has already been removed.
96const SG_D_CAPLOSSABS: i64 = 5
97// Minimum ABSOLUTE absent-run count before the PERMIL bar is allowed to fire. Added 2026-08-06 after a
98// SECOND false positive, on nx_offc_install: ONE deliberately reworded message string scored 125 permil
99// because that binary has only EIGHT sampled runs. The OR added in the previous round did NOT fix
100// size-dependence -- it only added a second way to FIRE, and the permil bar alone still trips on any
101// small binary where a single edit is a large fraction. The T11 anti-bypass tooth missed it because its
102// fixture carries 40 runs, not 8. A SINGLE ABSENT RUN IS AN ORDINARY EDIT AT EVERY SAMPLE SIZE.
103const SG_D_CAPLOSSMIN: i64 = 2
104const SG_D_MINTOK: i64 = 12
105const SG_D_MAXTOK: i64 = 256
106// longest prefix of a run this path will search for. Bounds the promote-time cost; the CLI oracle
107// passes 0 (unbounded) because it runs to completion and can afford the whole run.
108const SG_MAXTOKLEN: i64 = 255
109
110func sg_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
111
112// size in bytes, -1 when absent/unstattable. Fail-closed: never invents a size.
113func sg_size(path: *u8) -> i64 {
114 let sb: *u8 = sys_mmap(SG_STATBUF)
115 if sys_fstatat(path, sb) != 0 { sys_munmap(sb, SG_STATBUF); return 0 - 1 }
116 let p: *i64 = ((sb as i64) + SG_STAT_SIZE_OFF) as *i64
117 let v: i64 = p[0]
118 sys_munmap(sb, SG_STATBUF)
119 return v
120}
121
122// st_mtime seconds, -1 when absent. Offset 88 is the channel proven by _freshness_gate T5.
123func sg_mtime(path: *u8) -> i64 {
124 let sb: *u8 = sys_mmap(SG_STATBUF)
125 if sys_fstatat(path, sb) != 0 { sys_munmap(sb, SG_STATBUF); return 0 - 1 }
126 let p: *i64 = ((sb as i64) + SG_STAT_MTIME_OFF) as *i64
127 let v: i64 = p[0]
128 sys_munmap(sb, SG_STATBUF)
129 return v
130}
131
132func sg_slurp(path: *u8, buf: *u8, cap: i64) -> i64 {
133 let fd: i64 = sys_openat_rd(path)
134 if fd < 0 { return 0 - 1 }
135 var tot: i64 = 0
136 var go: i64 = 1
137 while go == 1 {
138 if tot >= cap { go = 0 }
139 if go == 1 {
140 let r: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot)
141 if r <= 0 { go = 0 }
142 if r > 0 { tot = tot + r }
143 }
144 }
145 sys_close(fd)
146 return tot
147}
148
149// NOTE: the printable-run scanner that used to live here was DELETED, not kept alongside. It was a
150// second implementation of the same ruler nx_contentdiff already specified, and two rulers that can
151// disagree about the same question are worse than one. Both consumers now call cdl_lost.
152
153// 1 when the nl bytes at nd appear anywhere in the bn bytes at b
154func sg_contains(b: *u8, bn: i64, nd: *u8, nl: i64) -> i64 {
155 if nl <= 0 { return 1 }
156 if nl > bn { return 0 }
157 let first: u8 = nd[0]
158 let last: i64 = bn - nl
159 var i: i64 = 0
160 while i <= last {
161 if b[i] == first {
162 var k: i64 = 1
163 var ok: i64 = 1
164 while k < nl {
165 if b[i + k] != nd[k] { ok = 0; k = nl }
166 if ok == 1 { k = k + 1 }
167 }
168 if ok == 1 { return 1 }
169 }
170 i = i + 1
171 }
172 return 0
173}
174
175func sg_eq_at(b: *u8, pos: i64, key: *u8, kl: i64) -> i64 {
176 var k: i64 = 0
177 while k < kl {
178 if b[pos + k] != key[k] { return 0 }
179 k = k + 1
180 }
181 return 1
182}
183
184func sg_parse_int_at(b: *u8, n: i64, pos: i64) -> i64 {
185 var v: i64 = 0
186 var seen: i64 = 0
187 var i: i64 = pos
188 var go: i64 = 1
189 while go == 1 {
190 if i >= n { go = 0 }
191 if go == 1 {
192 let c: i64 = b[i] as i64
193 if c < 48 { go = 0 }
194 if c > 57 { go = 0 }
195 if go == 1 { v = (v * 10) + (c - 48); seen = 1; i = i + 1 }
196 }
197 }
198 if seen == 0 { return 0 - 1 }
199 return v
200}
201
202// value of a line-anchored `key=<int>` row, or -1 when the key is absent
203func sg_conf_get(b: *u8, n: i64, key: *u8) -> i64 {
204 let kl: i64 = sg_strlen(key)
205 if kl <= 0 { return 0 - 1 }
206 let last: i64 = n - kl
207 var i: i64 = 0
208 while i <= last {
209 if sg_eq_at(b, i, key, kl) == 1 {
210 var atstart: i64 = 0
211 if i == 0 { atstart = 1 }
212 if i > 0 { if b[i - 1] == (10 as u8) { atstart = 1 } }
213 if atstart == 1 {
214 if (i + kl) < n {
215 if b[i + kl] == (61 as u8) { return sg_parse_int_at(b, n, i + kl + 1) }
216 }
217 }
218 }
219 i = i + 1
220 }
221 return 0 - 1
222}
223
224// Rule 17 chain: bootstrap defaults, then knowledge/staging_hygiene.conf overrides each key it names.
225// Returns 1 when a conf file was read, 0 when running on defaults alone.
226func sg_conf_load(cfg: *i64) -> i64 {
227 cfg[SG_C_MAXAGE] = SG_D_MAXAGE
228 cfg[SG_C_SHRINKTOL] = SG_D_SHRINKTOL
229 cfg[SG_C_CAPLOSS] = SG_D_CAPLOSS
230 cfg[SG_C_MINTOK] = SG_D_MINTOK
231 cfg[SG_C_MAXTOK] = SG_D_MAXTOK
232 cfg[SG_C_CAPLOSSABS] = SG_D_CAPLOSSABS
233 cfg[SG_C_CAPLOSSMIN] = SG_D_CAPLOSSMIN
234 let buf: *u8 = sys_mmap(SG_CONFCAP)
235 let n: i64 = sg_slurp("knowledge/staging_hygiene.conf" as *u8, buf, SG_CONFCAP - 2)
236 if n <= 0 { sys_munmap(buf, SG_CONFCAP); return 0 }
237 let a: i64 = sg_conf_get(buf, n, "max_stage_age_days" as *u8)
238 if a >= 0 { cfg[SG_C_MAXAGE] = a }
239 let s: i64 = sg_conf_get(buf, n, "shrink_tolerance_permil" as *u8)
240 if s >= 0 { cfg[SG_C_SHRINKTOL] = s }
241 let c: i64 = sg_conf_get(buf, n, "caploss_refuse_permil" as *u8)
242 if c >= 0 { cfg[SG_C_CAPLOSS] = c }
243 let t: i64 = sg_conf_get(buf, n, "min_token_len" as *u8)
244 if t >= 0 { cfg[SG_C_MINTOK] = t }
245 let m: i64 = sg_conf_get(buf, n, "max_tokens_sampled" as *u8)
246 if m >= 0 { cfg[SG_C_MAXTOK] = m }
247 let ca: i64 = sg_conf_get(buf, n, "caploss_refuse_absolute" as *u8)
248 if ca >= 0 { cfg[SG_C_CAPLOSSABS] = ca }
249 let cm: i64 = sg_conf_get(buf, n, "caploss_min_absent" as *u8)
250 if cm >= 0 { cfg[SG_C_CAPLOSSMIN] = cm }
251 sys_munmap(buf, SG_CONFCAP)
252 return 1
253}
254
255// Structured comparison shares the CLI iterator; errors never mean zero loss.
256struct SgContentEvidence { state:i64, checked:i64, missing:i64, gained:i64, live_runs:i64, candidate_runs:i64, source_removed:i64, permil:i64 }
257func sg_measure_bytes(lb:*u8,ln:i64,nb:*u8,nn:i64,cfg:*i64,e:*SgContentEvidence)->i64{
258 e.state=CDS_INVALID;e.checked=-1;e.missing=-1;e.gained=-1;e.live_runs=-1;e.candidate_runs=-1;e.source_removed=-1;e.permil=-1
259 var x:NxCdStructured;var y:NxCdStructured
260 let xi:i64=cds_init(&x,lb,ln);let yi:i64=cds_init(&y,nb,nn)
261 let p:*i64=sys_mmap_try(CDL_P_SLOTS*__size_of(i64)) as *i64
262 var rc:i64=xi;if rc==0{rc=yi};if (p as i64)<=0{rc=CDS_RESOURCE}
263 var f:NxCdResult;var r:NxCdResult
264 if rc==0{
265 p[CDL_P_MINLEN]=cfg[SG_C_MINTOK];p[CDL_P_MAXSAMPLES]=cfg[SG_C_MAXTOK];p[CDL_P_MAXTOKLEN]=SG_MAXTOKLEN
266 rc=cds_measure(&x,&y,p,&f);if rc==0{rc=f.state}
267 if rc==0{rc=cds_measure(&y,&x,p,&r);if rc==0{rc=r.state}}
268 if rc==0{
269 e.checked=f.checked;e.missing=f.lost;e.gained=r.lost;e.live_runs=f.runs;e.candidate_runs=r.runs;e.source_removed=f.source_removed
270 e.permil=cds_permil(f.lost,f.checked);if e.permil<0{rc=e.permil}
271 }
272 }
273 cds_close(&x);cds_close(&y)
274 if (p as i64)>0{sys_munmap_direct(p as *u8,CDL_P_SLOTS*__size_of(i64))}
275 e.state=rc;return rc
276}
277// Legacy raw-buffer API retained; structured classification uses sg_measure_bytes.
278func sg_caploss(lb: *u8, ln: i64, nb: *u8, nn: i64, cfg: *i64, out2: *i64) -> i64 {
279 out2[0] = 0
280 out2[1] = 0
281 let prm: *i64 = sys_mmap(8 * CDL_P_SLOTS) as *i64
282 prm[CDL_P_MINLEN] = cfg[SG_C_MINTOK]
283 prm[CDL_P_MAXSAMPLES] = cfg[SG_C_MAXTOK]
284 prm[CDL_P_MAXTOKLEN] = SG_MAXTOKLEN
285 let o3: *i64 = sys_mmap(8 * CDL_O_SLOTS) as *i64
286 let permil: i64 = cdl_lost(lb, ln, nb, nn, prm, o3)
287 out2[0] = o3[CDL_O_CHECKED]
288 out2[1] = o3[CDL_O_LOST]
289 // SYMMETRIC PASS 2026-08-06. THREE TIMES this session this guard refused a legitimate edit of its
290 // own author's source, and every time the "lost" token had merely been RENAMED (fork_literals ->
291 // elf_literals): the old string vanished from live, an equally distinctive one appeared in staged,
292 // and a ONE-SIDED measure can only ever see the vanishing half.
293 // A LOSS DETECTOR THAT COUNTS ONLY WHAT VANISHED CANNOT TELL A RENAME FROM A REMOVAL.
294 // No second ruler is needed and none is written: cdl_lost is SYMMETRIC BY CONSTRUCTION, so the very
295 // same call with its arguments SWAPPED yields the tokens the STAGED binary has that live lacks.
296 // out2[2]=gained out2[3]=live runs out2[4]=candidate runs.
297 let o4: *i64 = sys_mmap(8 * CDL_O_SLOTS) as *i64
298 cdl_lost(nb, nn, lb, ln, prm, o4)
299 out2[2] = o4[CDL_O_LOST]
300 sys_munmap(o4 as *u8, 8 * CDL_O_SLOTS)
301 out2[3] = cdl_count_runs(lb, ln, cfg[SG_C_MINTOK])
302 out2[4] = cdl_count_runs(nb, nn, cfg[SG_C_MINTOK])
303 sys_munmap(prm as *u8, 8 * CDL_P_SLOTS)
304 sys_munmap(o3 as *u8, 8 * CDL_O_SLOTS)
305 return permil
306}
307
308const SG_ERR_NOENT:i64=-2
309const SG_IO:i64=-204
310struct SgFileEvidence { state:i64,size:i64,mtime:i64 }
311func sg_file_evidence(path:*u8,e:*SgFileEvidence)->i64{
312 e.state=CDS_RESOURCE;e.size=-1;e.mtime=-1
313 let sb:*u8=sys_mmap_try(SG_STATBUF)
314 if (sb as i64)<=0{return e.state}
315 let rc:i64=sys_fstatat(path,sb)
316 if rc==0{e.size=*(((sb as i64)+SG_STAT_SIZE_OFF) as *i64);e.mtime=*(((sb as i64)+SG_STAT_MTIME_OFF) as *i64)}
317 sys_munmap_direct(sb,SG_STATBUF);e.state=rc;return rc
318}
319func sg_content_paths(livep:*u8,newp:*u8,lsz:i64,nsz:i64,cfg:*i64,out:*i64)->i64{
320 var an:i64=0;var bn:i64=0
321 let b:*u8=sys_map_file(newp,&bn)
322 if (b as i64)<=0{return SG_IO}
323 var a:*u8=0 as *u8
324 if lsz>=0{a=sys_map_file(livep,&an)}
325 var rc:i64=0
326 if bn!=nsz{rc=SG_IO}
327 if lsz>=0{if (a as i64)<=0{rc=SG_IO};if an!=lsz{rc=SG_IO}}
328 var e:SgContentEvidence
329 if rc==0{
330 if lsz<0{rc=sg_measure_bytes(b,bn,b,bn,cfg,&e)}
331 if lsz>=0{rc=sg_measure_bytes(a,an,b,bn,cfg,&e)}
332 if rc==0{
333 out[SG_O_CHECKED]=e.checked;out[SG_O_MISSING]=e.missing;out[SG_O_GAINED]=e.gained
334 out[SG_O_LIVERUNS]=e.live_runs;out[SG_O_CANDRUNS]=e.candidate_runs
335 out[SG_O_SOURCE_REMOVED]=e.source_removed;out[SG_O_LOSTPERMIL]=e.permil
336 if lsz>=0{out[SG_O_IDENTICAL]=cdl_bytes_eq(a,an,b,bn)}
337 }
338 }
339 if (a as i64)>0{sys_munmap_direct(a,an)}
340 sys_munmap_direct(b,bn);return rc
341}
342// No writes. Complete mappings are process-local and reclaimed on every return.
343func sg_classify(livep:*u8,newp:*u8,now:i64,cfg:*i64,out:*i64)->i64{
344 var i:i64=0;while i<SG_OUT_SLOTS{out[i]=0;i=i+1}
345 out[SG_O_CONTENT_STATE]=CDS_INVALID;out[SG_O_MISSING]=-1;out[SG_O_CHECKED]=-1;out[SG_O_SOURCE_REMOVED]=-1
346 var n:SgFileEvidence;var l:SgFileEvidence
347 let ns:i64=sg_file_evidence(newp,&n)
348 out[SG_O_NEWSZ]=n.size;out[SG_O_NEWMT]=n.mtime;out[SG_O_LIVESZ]=-1
349 if ns==SG_ERR_NOENT{out[SG_O_CONTENT_STATE]=ns;return SG_NONE}
350 if ns!=0{out[SG_O_CONTENT_STATE]=ns;return SG_UNQUALIFIED}
351 let ls:i64=sg_file_evidence(livep,&l)
352 out[SG_O_LIVESZ]=l.size;out[SG_O_LIVEMT]=l.mtime
353 if ls!=0{if ls!=SG_ERR_NOENT{out[SG_O_CONTENT_STATE]=ls;return SG_UNQUALIFIED}}
354 let rc:i64=sg_content_paths(livep,newp,l.size,n.size,cfg,out)
355 out[SG_O_CONTENT_STATE]=rc
356 if rc!=0{return SG_UNQUALIFIED}
357 var age:i64=0;if now>n.mtime{age=(now-n.mtime)/SG_DAY};out[SG_O_AGEDAYS]=age
358 var shrink:i64=0;if l.size>0{if n.size<l.size{shrink=cds_permil(l.size-n.size,l.size)}}
359 out[SG_O_SHRINKPERMIL]=shrink
360 var flags:i64=0
361 if ls==0{if n.mtime<l.mtime{flags=flags+1}}
362 if age>cfg[SG_C_MAXAGE]{flags=flags+2}
363 var caphit:i64=0
364 if out[SG_O_MISSING]>=cfg[SG_C_CAPLOSSABS]{caphit=1}
365 if out[SG_O_LOSTPERMIL]>cfg[SG_C_CAPLOSS]{if out[SG_O_MISSING]>=cfg[SG_C_CAPLOSSMIN]{caphit=1}}
366 // Preserve existing small-exchange policy; it is not proof of behavioral equivalence.
367 if out[SG_O_GAINED]>=out[SG_O_MISSING]{if out[SG_O_CANDRUNS]>=out[SG_O_LIVERUNS]{if out[SG_O_MISSING]<cfg[SG_C_CAPLOSSABS]{caphit=0}}}
368 // File-table removals are exhaustive and cannot disappear through sampling or exchange.
369 if out[SG_O_SOURCE_REMOVED]>0{caphit=1}
370 if caphit==1{flags=flags+8};out[SG_O_FLAGS]=flags
371 if ls==SG_ERR_NOENT{return SG_ORPHAN}
372 if flags%2==1{return SG_BACKDATE}
373 if (flags/2)%2==1{return SG_EXPIRED}
374 if (flags/8)%2==1{return SG_CAPLOSS}
375 return SG_FORWARD
376}
377
378// 1 = this staged artifact may be promoted. Anything unrecognised is REFUSED (fail-closed).
379func sg_allows(c: i64) -> i64 {
380 if c == SG_FORWARD { return 1 }
381 if c == SG_ORPHAN { return 1 }
382 return 0
383}
384
385func sg_class_name(c: i64) -> *u8 {
386 if c == SG_NONE { return "NONE" as *u8 }
387 if c == SG_FORWARD { return "FORWARD" as *u8 }
388 if c == SG_ORPHAN { return "ORPHAN" as *u8 }
389 if c == SG_BACKDATE { return "BACKDATE" as *u8 }
390 if c == SG_EXPIRED { return "EXPIRED" as *u8 }
391 if c == SG_SHRINK { return "SHRINK" as *u8 }
392 if c == SG_CAPLOSS { return "CAPLOSS" as *u8 }
393 if c == SG_UNQUALIFIED { return "UNQUALIFIED" as *u8 }
394 return "UNKNOWN" as *u8
395}
396func sg_allows_override(c:i64,explicitApproval:i64)->i64{
397 if sg_allows(c)==1{return 1}
398 if explicitApproval!=1{return 0}
399 if c==SG_BACKDATE{return 1}
400 if c==SG_EXPIRED{return 1}
401 if c==SG_SHRINK{return 1}
402 if c==SG_CAPLOSS{return 1}
403 return 0
404}
405
406// Legacy scratch entrypoint retained; structured classification does not use it.
407static sg_lb: *u8
408static sg_nb: *u8
409
410func sg_scratch() -> i64 {
411 if (sg_lb as i64) == 0 { sg_lb = sys_mmap(SG_FILECAP) }
412 if (sg_nb as i64) == 0 { sg_nb = sys_mmap(SG_FILECAP) }
413 return 1
414}
415