nx_organ_ship_loss_lib_candidate_t370.nx source
↩ module page · 1008 lines · 58477 B
1// nx_organ_ship_lib.nx -- WHICH GATE PROVES THIS TARGET? The resolver the ship loop was missing.
2//
3// WHY (measured 2026-08-20). nx_organ_ship's PROVE stage looked for exactly ONE name, "<target>_gate",
4// and when that artifact did not exist it announced GATE=NONE and carried on. Shipping
5// nx_gate_roster_run therefore SKIPPED THE PROOF STAGE ENTIRELY -- its gate is nx_gate_roster_gate --
6// and the loop reported SHIPPED having proven nothing about the binary it installed.
7// A SHIP LOOP THAT SILENTLY SKIPS ITS PROOF STAGE WHEN A NAME DOES NOT MATCH IS THE VACUOUS-TEST
8// DEFECT IN THE ONE ORGAN EVERY OTHER SHIP DEPENDS ON.
9//
10// THE REMEDY IS NOT A RENAME. Renaming a gate to satisfy a classifier is dodging by luck; the estate
11// settled that question once already for a sibling classifier (organ_kind.conf: "renaming a third time
12// would be dodging classifiers by luck"). So: RESOLVE the gate, and PUBLISH THE SEARCH. Every candidate
13// path probed is appended to `tried` in the order probed, so a miss can NAME what it looked for instead
14// of being a silent skip, and a hit can NAME the rule that found it.
15//
16// RESOLUTION ORDER -- declaration first, derivation second (rule 17):
17// 1 explicit the caller passed gate=<name>. A caller ASSERTION: if it is absent the loop REFUSES,
18// because a declared gate that is not there is a caller error, not a missing gate.
19// 2 conf row knowledge/organ_gate.conf, "<target> TAB <gatename>". The declaration path: a target
20// whose gate cannot be derived from its name gets a ROW, never a rename.
21// 3 convention <target>_gate -- what the estate mostly does.
22// 4 self the target ITSELF when it ends in _gate. Shipping a gate used to skip PROVE for the
23// same reason (nx_foo_gate_gate does not exist); a gate proves itself by running.
24// 5 strip1 <target minus its LAST underscore segment>_gate. This is the measured shape:
25// nx_gate_roster_run -> nx_gate_roster_gate.
26// EXACTLY ONE segment, deliberately. Each further strip walks toward a DIFFERENT
27// organ's gate, and A GATE WHOSE SUBJECT IS NOT THE TARGET PROVES NOTHING ABOUT THE
28// TARGET -- a resolver that reached far enough would manufacture a FALSE proof, which
29// is strictly worse than the silent skip it replaces.
30//
31// ROOT ORDER -- WHICH COPY OF THE RESOLVED GATE RUNS (added 2026-08-25). Each candidate is probed at
32// two roots: the promoted serving root and the build scratch. Promoted-first is right for a gate that
33// is a DIFFERENT organ from the target, because this invocation did not build it. It is WRONG for the
34// one candidate that IS the target just built, and for an in-process gate -- whose subject is compiled
35// INTO the gate -- that means reporting a tooth count from code the loop did not build. `fresh` names
36// the target so osl_try_fresh can flip the order for that candidate alone; osl_root_of reports which
37// root any resolved path came from, so the number always travels with its provenance.
38//
39// WHY A LIB AND NOT A FUNCTION INSIDE THE LOOP: nx_organ_ship's main() BUILDS before it proves, so a
40// gate that drove main() would fork real compiles. The roots are PARAMETERS here (dir + suffix pairs)
41// precisely so nx_organ_ship_gate can point them at /tmp/<gate>/ fixtures and test every rule without
42// touching the estate root -- a gate must never share its fixture with a production surface.
43// license_tier: ORIGINAL Read-only: opens candidate paths to test existence and closes them. No hw writes (Rule 26).
44import "nx_syscalls.nx"
45
46const OSL_NAMECAP: i64 = 256
47const OSL_PATHCAP: i64 = 512
48const OSL_TAB: i64 = 9
49const OSL_SP: i64 = 32
50const OSL_NL: i64 = 10
51const OSL_CR: i64 = 13
52const OSL_SEMI: i64 = 59
53const OSL_HASH: i64 = 35
54const OSL_US: i64 = 95
55// the rule that resolved a gate, reported as a NUMBER so callers cannot disagree about spelling
56const OSL_WHY_NONE: i64 = 0
57const OSL_WHY_EXPLICIT: i64 = 1
58const OSL_WHY_CONF: i64 = 2
59const OSL_WHY_CONVENTION: i64 = 3
60const OSL_WHY_SELF: i64 = 4
61const OSL_WHY_STRIP1: i64 = 5
62const OSL_GATE_SUFFIX: *u8 = "_gate"
63const OSL_CONF_DEFAULT: *u8 = "knowledge/organ_gate.conf"
64// WHICH ROOT AN ARTIFACT PATH CAME FROM, reported as a NUMBER for the same reason the WHY codes are:
65// so no caller can disagree with another about spelling.
66const OSL_ROOT_NONE: i64 = 0
67const OSL_ROOT_A: i64 = 1
68const OSL_ROOT_B: i64 = 2
69
70func osl_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
71func osl_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var oo: i64 = o; while s[i] != (0 as u8) { d[oo] = s[i]; oo = oo + 1; i = i + 1 } d[oo] = 0 as u8; return oo }
72func osl_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] == b[i] { if a[i] == (0 as u8) { return 1 } i = i + 1 } return 0 }
73// BOUNDED cat for the search transcript: `tried` is published in a refusal, so it must never be the
74// thing that overruns. NO SILENT CAP -- the caller sizes it from the candidate count, and osl_tried_cap
75// below is the arithmetic, not a guess.
76func osl_catb(d: *u8, o: i64, cap: i64, s: *u8) -> i64 {
77 var i: i64 = 0
78 var oo: i64 = o
79 while s[i] != (0 as u8) { if oo < cap - 1 { d[oo] = s[i]; oo = oo + 1 } i = i + 1 }
80 d[oo] = 0 as u8
81 return oo
82}
83// 5 rules x 2 roots = 10 probes, each at most one path plus a separator. DERIVED, not hand-picked:
84// a hand-counted cap beside a growing rule list is a second copy of the rule count that drifts.
85func osl_tried_cap() -> i64 { return 10 * (OSL_PATHCAP + 1) }
86
87func osl_starts(s: *u8, p: *u8) -> i64 { var i: i64 = 0; while p[i] != (0 as u8) { if s[i] != p[i] { return 0 } i = i + 1 } return 1 }
88// WHICH ROOT DID THIS PATH COME FROM? Tested LONGEST-PREFIX-FIRST, and that is load-bearing rather
89// than tidy: in production root A is "./" and root B is "./buildroot/_build/", so ROOT B STARTS WITH
90// ROOT A. A first-match-A test therefore reports every fresh-build path as a promoted one, and the
91// provenance line would confidently name the wrong artifact -- which is the exact defect this
92// reporting exists to end. The order is DERIVED from the two prefix lengths, never hand-picked, so it
93// stays correct if the roots are ever reconfigured or swapped.
94func osl_root_of(path: *u8, dirA: *u8, dirB: *u8) -> i64 {
95 let la: i64 = osl_slen(dirA)
96 let lb: i64 = osl_slen(dirB)
97 if la >= lb {
98 if osl_starts(path, dirA) == 1 { return OSL_ROOT_A }
99 if osl_starts(path, dirB) == 1 { return OSL_ROOT_B }
100 return OSL_ROOT_NONE
101 }
102 if osl_starts(path, dirB) == 1 { return OSL_ROOT_B }
103 if osl_starts(path, dirA) == 1 { return OSL_ROOT_A }
104 return OSL_ROOT_NONE
105}
106func osl_root_name(r: i64) -> *u8 {
107 if r == OSL_ROOT_A { return "promoted-serving-root" as *u8 }
108 if r == OSL_ROOT_B { return "fresh-build-root" as *u8 }
109 return "unknown-root" as *u8
110}
111
112func osl_ends_gate(s: *u8) -> i64 {
113 let l: i64 = osl_slen(s)
114 let gl: i64 = osl_slen(OSL_GATE_SUFFIX)
115 if l < gl { return 0 }
116 var k: i64 = 0
117 while k < gl { if s[l - gl + k] != OSL_GATE_SUFFIX[k] { return 0 } k = k + 1 }
118 return 1
119}
120
121// <target> minus its LAST underscore segment, plus _gate. nx_gate_roster_run -> nx_gate_roster_gate.
122func osl_strip1(target: *u8, dst: *u8) -> i64 {
123 dst[0] = 0 as u8
124 let l: i64 = osl_slen(target)
125 var cut: i64 = 0 - 1
126 var i: i64 = l - 1
127 while i > 0 { if target[i] == (OSL_US as u8) { cut = i; i = 0 } else { i = i - 1 } }
128 if cut <= 0 { return 0 }
129 var o: i64 = 0
130 while o < cut { dst[o] = target[o]; o = o + 1 }
131 dst[o] = 0 as u8
132 o = osl_cat(dst, o, OSL_GATE_SUFFIX)
133 return 1
134}
135
136// knowledge/organ_gate.conf: "<target> TAB <gatename>" (spaces accepted too). A leading semicolon or
137// hash byte is a comment. Returns 1 and fills dst with the declared gate NAME.
138//
139// ONE TARGET MAY DECLARE SEVERAL GATES (2026-08-23). The compiler is proven by one gate PER LANGUAGE
140// RUNG (nx_chkarith_gate, nx_optenforce_gate, nx_boundscheck_gate, nx_opt_eqsat_wire_gate) and a
141// resolver that read only the FIRST row left the other three to "the lane's own checklist" -- the
142// conf said so in its own comment. A LAW THAT HAS TO BE REMEMBERED AT SHIP TIME IS A LAW THAT GETS
143// SKIPPED; the fix is in the path: osl_conf_scan walks EVERY row for the target, the idx-th match
144// (0-based) fills dst, and the total match count comes back through countp so the ship loop can prove
145// every declared gate. osl_conf_gate is exactly scan(idx=0): every existing caller and every existing
146// tooth sees byte-identical behaviour. Only rows with a non-empty gate name count as declarations.
147func osl_conf_scan(conf: *u8, target: *u8, idx: i64, dst: *u8, countp: *i64) -> i64 {
148 dst[0] = 0 as u8
149 countp[0] = 0
150 let lenp: *i64 = sys_mmap(16) as *i64
151 lenp[0] = 0
152 let b: *u8 = sys_read_file(conf, lenp)
153 let n: i64 = lenp[0]
154 if n <= 0 { return 0 }
155 let tl: i64 = osl_slen(target)
156 var hit: i64 = 0
157 var seen: i64 = 0
158 var i: i64 = 0
159 while i < n {
160 var e: i64 = i
161 var fe: i64 = 0
162 while fe == 0 { if e >= n { fe = 1 } else { if b[e] == (OSL_NL as u8) { fe = 1 } else { e = e + 1 } } }
163 var skip: i64 = 0
164 if e <= i { skip = 1 }
165 if skip == 0 { if b[i] == (OSL_SEMI as u8) { skip = 1 } }
166 if skip == 0 { if b[i] == (OSL_HASH as u8) { skip = 1 } }
167 if skip == 0 {
168 var t: i64 = i
169 var ft: i64 = 0
170 while ft == 0 {
171 if t >= e { ft = 1 } else {
172 if b[t] == (OSL_TAB as u8) { ft = 1 } else {
173 if b[t] == (OSL_SP as u8) { ft = 1 } else { t = t + 1 } } }
174 }
175 if t - i == tl {
176 var same: i64 = 1
177 var k: i64 = 0
178 while k < tl { if b[i + k] != target[k] { same = 0; k = tl } else { k = k + 1 } }
179 if same == 1 {
180 var g: i64 = t
181 var fg: i64 = 0
182 while fg == 0 {
183 if g >= e { fg = 1 } else {
184 if b[g] == (OSL_TAB as u8) { g = g + 1 } else {
185 if b[g] == (OSL_SP as u8) { g = g + 1 } else { fg = 1 } } }
186 }
187 // measure the gate name's extent first; copy it into dst ONLY for the idx-th
188 // declaration, so a later row can never overwrite the one the caller asked for
189 var o: i64 = 0
190 var fo: i64 = 0
191 let gs: i64 = g
192 while fo == 0 {
193 if g >= e { fo = 1 } else {
194 if b[g] == (OSL_CR as u8) { fo = 1 } else {
195 if o < OSL_NAMECAP - 1 { o = o + 1 }
196 g = g + 1 } }
197 }
198 if o > 0 {
199 if seen == idx {
200 var c: i64 = 0
201 while c < o { dst[c] = b[gs + c]; c = c + 1 }
202 dst[o] = 0 as u8
203 hit = 1
204 }
205 seen = seen + 1
206 }
207 }
208 }
209 }
210 i = e + 1
211 }
212 sys_free_file(b, n)
213 countp[0] = seen
214 return hit
215}
216func osl_conf_gate(conf: *u8, target: *u8, dst: *u8) -> i64 {
217 let cp: *i64 = sys_mmap(16) as *i64
218 cp[0] = 0
219 return osl_conf_scan(conf, target, 0, dst, cp)
220}
221// the idx-th (0-based) declared gate for target; 0 when fewer than idx+1 rows declare one
222func osl_conf_gate_nth(conf: *u8, target: *u8, idx: i64, dst: *u8) -> i64 {
223 let cp: *i64 = sys_mmap(16) as *i64
224 cp[0] = 0
225 return osl_conf_scan(conf, target, idx, dst, cp)
226}
227// how many rows declare a gate for target (0 = none declared; comments and empty names never count)
228func osl_conf_gate_count(conf: *u8, target: *u8) -> i64 {
229 let cp: *i64 = sys_mmap(16) as *i64
230 cp[0] = 0
231 let d: *u8 = sys_mmap(OSL_NAMECAP)
232 osl_conf_scan(conf, target, 0 - 1, d, cp)
233 return cp[0]
234}
235
236// probe ONE candidate name at ONE (dir, suffix) root. The exact path probed is APPENDED to `tried`
237// whether it hits or misses -- the transcript is the point: a miss that cannot say what it looked for
238// is the silent skip wearing a new name.
239func osl_probe1(cand: *u8, dir: *u8, suf: *u8, tried: *u8, tcap: i64, tp: *i64, dst: *u8) -> i64 {
240 var o: i64 = osl_cat(dst, 0, dir)
241 o = osl_cat(dst, o, cand)
242 o = osl_cat(dst, o, suf)
243 tp[0] = osl_catb(tried, tp[0], tcap, " " as *u8)
244 tp[0] = osl_catb(tried, tp[0], tcap, dst)
245 let fd: i64 = sys_openat_rd(dst)
246 if fd < 0 { return 0 }
247 sys_close(fd)
248 return 1
249}
250
251// try one candidate NAME at both roots; 1 on hit with dst holding the artifact path.
252func osl_try(cand: *u8, dirA: *u8, sufA: *u8, dirB: *u8, sufB: *u8, tried: *u8, tcap: i64, tp: *i64, dst: *u8) -> i64 {
253 if osl_probe1(cand, dirA, sufA, tried, tcap, tp, dst) == 1 { return 1 }
254 if osl_probe1(cand, dirB, sufB, tried, tcap, tp, dst) == 1 { return 1 }
255 return 0
256}
257
258// THE FRESHNESS SWAP. `fresh` is the name the CALLER JUST BUILT in this invocation (0 = nothing was).
259// When the candidate IS that name, root B (the build scratch) holds code from THIS run while root A
260// holds the PREVIOUS promote -- and for an IN-PROCESS gate the subject is compiled INTO the gate, so
261// probing promoted-first runs a tooth set from code the loop did not build.
262// MEASURED 2026-08-25 from nx_organ_ship's OWN journal, one target, ONE BUILD SHA 2d9c5011..., two
263// opposite verdicts decided entirely by which root was probed first:
264// PROVE RED ./nx_uvunwrap_gate.elf (the previous promote)
265// PROVE GREEN ./buildroot/_build/nx_uvunwrap_gate.sov.elf (the artifact just built)
266// A SHIP LOOP THAT PROVES THE PREVIOUS BINARY IS A VERDICT ABOUT THE WRONG SUBJECT, and its
267// FLATTERING direction is the dangerous one: a regression introduced by this very build is invisible
268// because the old binary still passes. Both directions are in the journal -- nx_editstack_gate and
269// nx_domain_map_gate each went GREEN against ./<name>.elf moments after a build.
270// THE ORDER IS DELIBERATELY UNCHANGED FOR EVERY OTHER CANDIDATE. A gate whose name is not the target
271// was NOT built by this invocation, so its root-B artifact is a fossil of unknown vintage while its
272// root-A artifact is the one the estate actually deploys and runs -- there, promoted-first is right.
273// And the swap is a PREFERENCE, not a requirement: when only root A holds the artifact it still
274// resolves, because refusing there would turn a working ship into a false RED.
275func osl_try_fresh(cand: *u8, fresh: *u8, dirA: *u8, sufA: *u8, dirB: *u8, sufB: *u8, tried: *u8, tcap: i64, tp: *i64, dst: *u8) -> i64 {
276 if (fresh as i64) != 0 { if osl_streq(cand, fresh) == 1 {
277 if osl_probe1(cand, dirB, sufB, tried, tcap, tp, dst) == 1 { return 1 }
278 if osl_probe1(cand, dirA, sufA, tried, tcap, tp, dst) == 1 { return 1 }
279 return 0
280 } }
281 return osl_try(cand, dirA, sufA, dirB, sufB, tried, tcap, tp, dst)
282}
283
284// THE RESOLVER. Returns 1 with dst = the gate ARTIFACT PATH and why[0] = the rule that matched;
285// 0 with dst empty and why[0] = OSL_WHY_NONE, and `tried` naming every path probed either way.
286//
287// `fresh` only ever changes WHICH ROOT IS PROBED FIRST for the single candidate that equals it
288// (osl_try_fresh above). The rule order, the search transcript and every other candidate are
289// untouched. osl_gate_resolve below is EXACTLY this function with fresh=0, so every pre-existing
290// caller and tooth sees byte-identical behaviour -- ONE resolver body, never a second copy that drifts.
291func osl_gate_resolve_fresh(target: *u8, explicit: *u8, fresh: *u8, conf: *u8, dirA: *u8, sufA: *u8, dirB: *u8, sufB: *u8, tried: *u8, tcap: i64, dst: *u8, why: *i64) -> i64 {
292 let tp: *i64 = sys_mmap(16) as *i64
293 tp[0] = 0
294 tried[0] = 0 as u8
295 why[0] = OSL_WHY_NONE
296 dst[0] = 0 as u8
297 let cand: *u8 = sys_mmap(OSL_NAMECAP)
298 // 1 explicit: the caller ASSERTED this gate. Do not fall through to derivation -- silently proving
299 // a DIFFERENT gate than the one named would be the false-proof this resolver exists to refuse.
300 if (explicit as i64) != 0 { if explicit[0] != (0 as u8) {
301 osl_cat(cand, 0, explicit)
302 if osl_try_fresh(cand, fresh, dirA, sufA, dirB, sufB, tried, tcap, tp, dst) == 1 { why[0] = OSL_WHY_EXPLICIT; return 1 }
303 dst[0] = 0 as u8
304 return 0
305 } }
306 // 2 declaration
307 if osl_conf_gate(conf, target, cand) == 1 {
308 if osl_try_fresh(cand, fresh, dirA, sufA, dirB, sufB, tried, tcap, tp, dst) == 1 { why[0] = OSL_WHY_CONF; return 1 }
309 }
310 // 3 convention
311 var co: i64 = osl_cat(cand, 0, target)
312 co = osl_cat(cand, co, OSL_GATE_SUFFIX)
313 if osl_try_fresh(cand, fresh, dirA, sufA, dirB, sufB, tried, tcap, tp, dst) == 1 { why[0] = OSL_WHY_CONVENTION; return 1 }
314 // 4 the target IS a gate
315 if osl_ends_gate(target) == 1 {
316 osl_cat(cand, 0, target)
317 if osl_try_fresh(cand, fresh, dirA, sufA, dirB, sufB, tried, tcap, tp, dst) == 1 { why[0] = OSL_WHY_SELF; return 1 }
318 }
319 // 5 one-segment strip
320 if osl_strip1(target, cand) == 1 {
321 if osl_try_fresh(cand, fresh, dirA, sufA, dirB, sufB, tried, tcap, tp, dst) == 1 { why[0] = OSL_WHY_STRIP1; return 1 }
322 }
323 dst[0] = 0 as u8
324 return 0
325}
326// The pre-2026-08-25 entry point: resolve with NOTHING declared fresh. Kept so that no existing caller
327// has to change, and so its own gate can prove the two are equivalent rather than assert it.
328func osl_gate_resolve(target: *u8, explicit: *u8, conf: *u8, dirA: *u8, sufA: *u8, dirB: *u8, sufB: *u8, tried: *u8, tcap: i64, dst: *u8, why: *i64) -> i64 {
329 return osl_gate_resolve_fresh(target, explicit, 0 as *u8, conf, dirA, sufA, dirB, sufB, tried, tcap, dst, why)
330}
331
332func osl_why_name(w: i64) -> *u8 {
333 if w == OSL_WHY_EXPLICIT { return "explicit-gate-arg" as *u8 }
334 if w == OSL_WHY_CONF { return "organ_gate.conf-declaration" as *u8 }
335 if w == OSL_WHY_CONVENTION { return "convention-target_gate" as *u8 }
336 if w == OSL_WHY_SELF { return "self-the-target-is-a-gate" as *u8 }
337 if w == OSL_WHY_STRIP1 { return "one-segment-strip" as *u8 }
338 return "none" as *u8
339}
340
341// ===========================================================================
342// PART 2 (2026-08-25) -- IS THE RESOLVED GATE'S VERDICT ABOUT *THIS* RUN?
343//
344// The freshness swap above closed ONE case: the target IS its own gate, so stage 1 built the artifact
345// and probing the build root first runs it. The larger hole stayed open, and the lane that closed the
346// first named it: when the target is NOT a gate (ship nx_foo, resolving nx_foo_gate) THE SHIP LOOP
347// NEVER REBUILDS THE GATE. Edit nx_foo_lib.nx, ship nx_foo, and PROVE runs whatever nx_foo_gate binary
348// was last built -- and because an IN-PROCESS gate STATICALLY LINKS the lib, that verdict is a claim
349// about the tree AS OF THE GATE'S OWN BUILD, not about the edit being shipped. The estate has measured
350// the scale of it: 715 gates have a binary and only 176 are current with their own source.
351//
352// THE BOUND IS THE WHOLE DESIGN, not a caveat on it. Rebuilding every resolved gate on every ship
353// doubles compile cost for every lane on a box whose governor already refuses builds under load, and
354// MOST GATES DO NOT EMBED THEIR TARGET AT ALL -- an end-to-end gate fork/execs the deployed elf, so
355// rebuilding it changes nothing about what it proves. The rebuild is therefore spent only when the
356// gate's OWN SOURCE imports what this run built: <target>.nx or <target>_lib.nx. One cheap read.
357//
358// DECLARED SCOPE, because a bound that is not published reads as completeness:
359// * DIRECT IMPORT EDGES ONLY, AND THE TARGET NAME IS THE ONLY EVIDENCE THIS LOOP HAS. A gate that
360// reaches the target through an intermediate lib (gate -> nx_bar_lib.nx -> nx_foo_lib.nx), and --
361// the commoner case -- a gate and a target that BOTH import some third lib which is what actually
362// got edited, both classify INDEPENDENT here and are NOT rebuilt. A FLOOR on the rebuild set,
363// never the transitive closure.
364// THE RESIDUAL IS NAMED WITH ITS MECHANISM, because a named absence is only worth naming if the
365// next lane can act on it: /api/build ALREADY RETURNS closure_sha256 for every target it compiles,
366// so the honest test is not an import scan at all -- it is "does this gate's CURRENT closure hash
367// differ from the one its artifact was built from", which needs one banked hash per built gate and
368// nothing else. Until that bank exists the import scan is the cheap approximation, and it is
369// deliberately the one that errs toward rebuilding. (nx_rebuild_plan walks the whole closure today
370// and is far too heavy to fork per ship.)
371// * COMMENTS ARE NOT CODE. A commented-out import is not a build edge, and a scanner that does not
372// skip comments measures the documentation rather than the code (banked, twice).
373// * EXACT MODULE NAMES. "nx_foo_other_lib.nx" must never answer for "nx_foo", so the module string is
374// compared by LENGTH AND BYTES. A substring test would follow the wrong organ's edit.
375//
376// WHY BOTH SOURCE ROOTS ARE READ, AND WHY THE ORDER IS NOT LOAD-BEARING HERE. nx_gatefresh's source
377// records that nx_sov_build_run probes buildroot/runtime/_hdl_build/ FIRST, so a basename present in
378// both dirs compiles from _hdl_build and the runtime copy is a SHADOW that never compiles; it carries
379// the law beside it (MATCH THE RESOLVER YOU ARE REPORTING ON). This scans in that same order AND reads
380// the shadow too, answering CONTAINS if EITHER copy names the target. Under a shadow the two copies can
381// disagree, and the only direction this decision is allowed to be wrong in is spending one extra
382// compile; being wrong the other way IS the stale-proof defect it exists to end. The count of readable
383// sources comes back through seenp so a shadow is ANNOUNCED instead of silently picked.
384// ===========================================================================
385
386const OSL_SRC_SUF: *u8 = ".nx"
387const OSL_LIB_SUFFIX: *u8 = "_lib"
388const OSL_KW_IMPORT: *u8 = "import"
389const OSL_CMT: *u8 = "//"
390const OSL_QUOTE: i64 = 34
391// the two source roots, IN THE BUILDER'S OWN ORDER (see the header note above)
392const OSL_SRC_DIR_HDL: *u8 = "./buildroot/runtime/_hdl_build/"
393const OSL_SRC_DIR_RT: *u8 = "./buildroot/runtime/"
394
395// WHAT A GATE'S BUILD CLOSURE HOLDS with respect to the target this run built. UNPROVEN is its OWN
396// state and must never collapse into INDEPENDENT: "I could not read the source" and "the source does
397// not import it" have opposite consequences for whether the verdict below is evidence.
398const OSL_CLO_UNPROVEN: i64 = 0
399const OSL_CLO_INDEPENDENT: i64 = 1
400const OSL_CLO_CONTAINS: i64 = 2
401
402// THE VINTAGE OF THE ARTIFACT A PROVE VERDICT CAME FROM, reported as a NUMBER for the same reason the
403// WHY and ROOT codes are: so no caller can disagree with another about spelling. Every state names its
404// own cause -- a single STALE bucket would leave the reader guessing which of three things happened.
405const OSL_VINT_UNKNOWN: i64 = 0
406const OSL_VINT_STAGE1: i64 = 1
407const OSL_VINT_REBUILT: i64 = 2
408const OSL_VINT_INDEPENDENT: i64 = 3
409const OSL_VINT_STALE_REFUSED: i64 = 4
410const OSL_VINT_STALE_NOARTIFACT: i64 = 5
411const OSL_VINT_UNKNOWN_NOSRC: i64 = 6
412const OSL_VINT_UNKNOWN_NONAME: i64 = 7
413const OSL_VINT_STALE_WRONGSRC: i64 = 8
414
415// ===========================================================================
416// PART 3 (2026-08-26) -- THE BANKED CLOSURE HASH DECIDES; THE IMPORT SCAN ONLY EVER ADDS.
417//
418// PART 2 named its own residual and named the fix in the same breath: "/api/build ALREADY RETURNS
419// closure_sha256 for every target it compiles, so the honest test is not an import scan at all -- it is
420// does this gate's CURRENT closure hash differ from the one its artifact was built from, which needs
421// one banked hash per built gate and nothing else."
422//
423// THE BANK ALREADY EXISTED AND NOTHING IN THIS LOOP HAD ASKED IT. /api/build writes <target>.provenance
424// at the serving root carrying closure_sha256=<64hex>, and nx_provcheck already recomputes the tree
425// closure through nx_closurehash and compares the two. So this is a WIRING, not a second ruler: no
426// closure walk and no sha256 is re-implemented here, and the comparison keeps exactly one owner.
427//
428// WHY THE HASH IS STRICTLY STRONGER THAN THE DIRECT-EDGE SCAN. A closure hash covers every TRANSITIVE
429// source, so the case PART 2 declared out of scope -- a gate and its target that BOTH import some third
430// lib, which is what actually got edited, neither importing the other directly -- lands inside the
431// gate's own closure and moves its hash. MEASURED 2026-08-26 on the live estate: nx_page_verify_gate
432// imports only nx_syscalls, nx_gate_verdict and nx_tool_run, so the edge scan calls it INDEPENDENT and
433// never rebuilds it, while its own bank reads
434// recorded=0960d15888fe52a0 ... now=1d07b01042d5621b ... verdict=DRIFTED
435// i.e. the teeth that would prove a ship of nx_page_verify are compiled from sources that have moved.
436// It also covers a blind spot the edge scan has by construction: nx_closurehash resolves through
437// runtime's SUBDIRECTORIES too, so a gate living in runtime/hub/ -- which osl_gate_closure can only
438// report UNPROVEN -- still gets a real answer from the bank.
439//
440// THE ORDERING RULE, AND IT IS THE WHOLE SAFETY ARGUMENT: THE HASH DECIDES, AND THE EDGE SCAN MAY ONLY
441// EVER SHORT-CIRCUIT TOWARD REBUILDING. osl_should_rebuild_prov returns 1 for every input on which the
442// incumbent osl_should_rebuild returns 1 -- it DELEGATES to it rather than restating it -- so NO SHIP
443// THAT REBUILT ITS GATE BEFORE STOPS DOING SO. The new test can add a rebuild and can never remove one.
444// A cheap approximation that is allowed to VETO an expensive proof is how a fast path becomes a false
445// green, and that direction is the one nobody audits.
446//
447// UNPROVEN NEVER FORCES A REBUILD, AND THAT IS THE PRODUCER'S OWN PUBLISHED ENVELOPE RATHER THAN A
448// CONVENIENCE. The build lane says it in its own source: the async worker does not write a sidecar yet,
449// "so a consumer MUST treat an absent sidecar as unknown, fall back to mtime, and never as stale."
450// Reading an absent bank as staleness would rebuild a gate on every ship whose gate was last built
451// asynchronously -- a permanent compile cost paid for no evidence at all. So UNPROVEN falls back to the
452// edge scan and is REPORTED as its own state rather than folded into either answer: "I could not look"
453// must never read as "current", and it must not read as "stale" either.
454// ===========================================================================
455
456// WHAT THE BANKED CLOSURE HASH SAYS ABOUT THE RESOLVED GATE'S ARTIFACT. Three states, because "the bank
457// disagrees with the tree" and "there is no bank to ask" have opposite consequences and opposite
458// remedies, and a single negative bucket would be read as the more alarming of the two.
459const OSL_PROV_UNPROVEN: i64 = 0
460const OSL_PROV_CURRENT: i64 = 1
461const OSL_PROV_DRIFTED: i64 = 2
462// nx_provcheck's PUBLISHED exit contract, named here so the call site never re-spells a bare integer
463// and so a change to that contract breaks in ONE place: 0 CURRENT, 1 DRIFTED, 2 UNRECORDED-or-NOSIDECAR,
464// 3 usage. Anything else -- 127 exec-missing, the -2/-3/-4 harness sentinels, -5 timeout -- is an
465// instrument that could not answer, which is UNPROVEN and must never be promoted into a verdict.
466const OSL_PC_EXIT_CURRENT: i64 = 0
467const OSL_PC_EXIT_DRIFTED: i64 = 1
468// THREE FURTHER VINTAGES. The bank is a DIFFERENT AXIS from the import scan and must not borrow its
469// names: STALE_PROV (we PROVED the artifact old) is not the same failure as STALE_REFUSED (the box
470// would not run the rebuild), and folding them would merge a fact about the gate with a fact about the
471// host -- two states with different remedies, which is the bucket-named-for-the-reader defect.
472const OSL_VINT_PROV_CURRENT: i64 = 9
473const OSL_VINT_REBUILT_PROV: i64 = 10
474const OSL_VINT_STALE_PROV: i64 = 11
475
476// 2 source roots x one path each plus a separator. DERIVED from the root count, like osl_tried_cap:
477// a hand-counted cap beside a growing root list is a second copy of that list that drifts.
478func osl_closure_tried_cap() -> i64 { return 2 * (OSL_PATHCAP + 1) }
479
480// THE NAME BEHIND AN ARTIFACT PATH -- the complement of osl_root_of, and the reason the ship loop can
481// ask anything at all about a gate it resolved: the resolver returns a PATH, and a source lookup needs
482// a NAME. Refuses (0) rather than guessing when the path is under neither root, does not end in that
483// root's suffix, or would overrun the name buffer -- NO SILENT CAP, because a truncated gate name would
484// resolve a DIFFERENT organ's source and the rebuild decision would follow it.
485func osl_name_of(path: *u8, dirA: *u8, sufA: *u8, dirB: *u8, sufB: *u8, dst: *u8) -> i64 {
486 dst[0] = 0 as u8
487 let r: i64 = osl_root_of(path, dirA, dirB)
488 if r == OSL_ROOT_NONE { return 0 }
489 var dir: *u8 = dirA
490 var suf: *u8 = sufA
491 if r == OSL_ROOT_B { dir = dirB; suf = sufB }
492 let pl: i64 = osl_slen(path)
493 let dl: i64 = osl_slen(dir)
494 let sl: i64 = osl_slen(suf)
495 if pl <= dl + sl { return 0 }
496 var k: i64 = 0
497 while k < sl { if path[pl - sl + k] != suf[k] { return 0 } k = k + 1 }
498 let nlen: i64 = pl - sl - dl
499 if nlen >= OSL_NAMECAP { return 0 }
500 var o: i64 = 0
501 while o < nlen { dst[o] = path[dl + o]; o = o + 1 }
502 dst[o] = 0 as u8
503 return 1
504}
505
506// ---- source scanning ------------------------------------------------------
507// end of the line starting at `from` (index of the newline, or n). The answer lives in its OWN
508// variable and the cursor is only ever used to stop: a loop that breaks by clobbering its own cursor
509// cannot also report where it stopped, and that idiom has erased an answer four times in this estate.
510func osl_eol(buf: *u8, n: i64, from: i64) -> i64 {
511 var k: i64 = from
512 var e: i64 = 0 - 1
513 while k < n { if buf[k] == (OSL_NL as u8) { e = k; k = n } else { k = k + 1 } }
514 if e < 0 { return n }
515 return e
516}
517// index of the next double-quote before `end`, or -1
518func osl_qend(buf: *u8, end: i64, from: i64) -> i64 {
519 var k: i64 = from
520 var q: i64 = 0 - 1
521 while k < end { if buf[k] == (OSL_QUOTE as u8) { q = k; k = end } else { k = k + 1 } }
522 return q
523}
524// skip spaces, tabs and CRs. The byte is read ONCE into c: with no else between the tests, an earlier
525// branch's k = k + 1 makes the next branch test a DIFFERENT byte -- a banked silent-failure idiom.
526func osl_skipws(buf: *u8, n: i64, from: i64) -> i64 {
527 var k: i64 = from
528 var go: i64 = 1
529 while go == 1 {
530 go = 0
531 if k < n {
532 let c: i64 = buf[k] as i64
533 var ws: i64 = 0
534 if c == OSL_SP { ws = 1 }
535 if c == OSL_TAB { ws = 1 }
536 if c == OSL_CR { ws = 1 }
537 if ws == 1 { k = k + 1; go = 1 }
538 }
539 }
540 return k
541}
542func osl_at(buf: *u8, end: i64, at: i64, s: *u8) -> i64 {
543 let l: i64 = osl_slen(s)
544 if at + l > end { return 0 }
545 var k: i64 = 0
546 while k < l { if buf[at + k] != s[k] { return 0 } k = k + 1 }
547 return 1
548}
549// ONE LINE: is it a LIVE `import "<name>.nx"`? Everything this returns 0 for is a case where treating
550// the line as a build edge would spend a compile on the wrong organ or on nothing at all.
551func osl_line_imports(buf: *u8, ls: i64, le: i64, name: *u8, nl: i64) -> i64 {
552 var i: i64 = osl_skipws(buf, le, ls)
553 if i >= le { return 0 }
554 if osl_at(buf, le, i, OSL_CMT) == 1 { return 0 }
555 if osl_at(buf, le, i, OSL_KW_IMPORT) == 0 { return 0 }
556 // the keyword must END here, or `importer_of(x)` reads as an import. The length is DERIVED from the
557 // keyword literal, never hand-counted beside it.
558 var j: i64 = i + osl_slen(OSL_KW_IMPORT)
559 if j >= le { return 0 }
560 let c: i64 = buf[j] as i64
561 var ws: i64 = 0
562 if c == OSL_SP { ws = 1 }
563 if c == OSL_TAB { ws = 1 }
564 if ws == 0 { return 0 }
565 j = osl_skipws(buf, le, j)
566 if j >= le { return 0 }
567 if buf[j] != (OSL_QUOTE as u8) { return 0 }
568 j = j + 1
569 let q: i64 = osl_qend(buf, le, j)
570 if q < 0 { return 0 }
571 let sufl: i64 = osl_slen(OSL_SRC_SUF)
572 // LENGTH FIRST, then bytes: this is what stops nx_foo_other_lib.nx answering for nx_foo.
573 if q - j != nl + sufl { return 0 }
574 var k: i64 = 0
575 while k < nl { if buf[j + k] != name[k] { return 0 } k = k + 1 }
576 var m: i64 = 0
577 while m < sufl { if buf[j + nl + m] != OSL_SRC_SUF[m] { return 0 } m = m + 1 }
578 return 1
579}
580// does this SOURCE TEXT carry a live import of <name>.nx?
581func osl_imports(buf: *u8, n: i64, name: *u8) -> i64 {
582 if n <= 0 { return 0 }
583 let nl: i64 = osl_slen(name)
584 if nl <= 0 { return 0 }
585 var ls: i64 = 0
586 var hit: i64 = 0
587 while ls < n {
588 let le: i64 = osl_eol(buf, n, ls)
589 if hit == 0 { if osl_line_imports(buf, ls, le, name, nl) == 1 { hit = 1 } }
590 ls = le + 1
591 }
592 return hit
593}
594// CONTAINS iff this source imports the target ITSELF or the target's lib. Those are the two shapes a
595// ship actually rebuilds: `ship nx_foo` after editing nx_foo.nx or nx_foo_lib.nx.
596func osl_closure_of_buf(buf: *u8, n: i64, target: *u8) -> i64 {
597 if osl_imports(buf, n, target) == 1 { return OSL_CLO_CONTAINS }
598 // a name that cannot fit its _lib form is refused TOWARD REBUILDING: the only error this decision
599 // may make is spending a compile, never skipping one.
600 if osl_slen(target) + osl_slen(OSL_LIB_SUFFIX) >= OSL_NAMECAP { return OSL_CLO_CONTAINS }
601 let lib: *u8 = sys_mmap(OSL_NAMECAP)
602 var o: i64 = osl_cat(lib, 0, target)
603 o = osl_cat(lib, o, OSL_LIB_SUFFIX)
604 if osl_imports(buf, n, lib) == 1 { return OSL_CLO_CONTAINS }
605 return OSL_CLO_INDEPENDENT
606}
607// THE ONE CALL THE SHIP LOOP MAKES. Probes both source roots in the builder's order, APPENDS every
608// path probed to `tried` whether it hits or misses (a decision that cannot say what it read is the
609// silent skip wearing a new name), fills srcdst with the source that DECIDED, and reports through
610// seenp how many sources were readable so a SHADOW (both roots populated) can be announced.
611func osl_gate_closure(gate: *u8, target: *u8, dirA: *u8, dirB: *u8, suf: *u8, tried: *u8, tcap: i64, srcdst: *u8, seenp: *i64) -> i64 {
612 tried[0] = 0 as u8
613 srcdst[0] = 0 as u8
614 seenp[0] = 0
615 let tp: *i64 = sys_mmap(16) as *i64
616 tp[0] = 0
617 let path: *u8 = sys_mmap(OSL_PATHCAP)
618 let lenp: *i64 = sys_mmap(16) as *i64
619 var verdict: i64 = OSL_CLO_UNPROVEN
620 var r: i64 = 0
621 while r < 2 {
622 var d: *u8 = dirA
623 if r == 1 { d = dirB }
624 var o: i64 = osl_cat(path, 0, d)
625 o = osl_cat(path, o, gate)
626 o = osl_cat(path, o, suf)
627 tp[0] = osl_catb(tried, tp[0], tcap, " " as *u8)
628 tp[0] = osl_catb(tried, tp[0], tcap, path)
629 let buf: *u8 = sys_read_file(path, lenp)
630 if (buf as i64) != 0 {
631 seenp[0] = seenp[0] + 1
632 if verdict != OSL_CLO_CONTAINS {
633 osl_cat(srcdst, 0, path)
634 verdict = osl_closure_of_buf(buf, lenp[0], target)
635 }
636 sys_free_file(buf, lenp[0])
637 }
638 r = r + 1
639 }
640 if verdict == OSL_CLO_CONTAINS { return OSL_CLO_CONTAINS }
641 if seenp[0] > 0 { return OSL_CLO_INDEPENDENT }
642 return OSL_CLO_UNPROVEN
643}
644// THE BOUND, as a predicate the gate can mutate. A rebuild is spent ONLY when the gate is a different
645// organ from the target AND its source imports what this run built. Without this, "rebuild every gate
646// on every ship" satisfies every other tooth -- which is why the INDEPENDENT case is a tooth of its own.
647func osl_should_rebuild(gate: *u8, target: *u8, clo: i64) -> i64 {
648 if osl_streq(gate, target) == 1 { return 0 }
649 if clo == OSL_CLO_CONTAINS { return 1 }
650 return 0
651}
652// IS THIS VERDICT EVIDENCE ABOUT THE RUN THAT PRODUCED IT? Exactly three vintages are: the artifact
653// stage 1 built, the artifact this run rebuilt, and a gate whose closure never contained the target at
654// all. Every other state is either STALE (an artifact older than the edit being shipped) or UNKNOWN
655// (undecidable) -- and both must read as NOT CURRENT, because an abstention that acquits is the
656// flattering failure nobody investigates.
657func osl_vint_current(v: i64) -> i64 {
658 if v == OSL_VINT_STAGE1 { return 1 }
659 if v == OSL_VINT_REBUILT { return 1 }
660 if v == OSL_VINT_INDEPENDENT { return 1 }
661 // THE BANK'S TWO CURRENT ANSWERS. OSL_VINT_STALE_PROV is deliberately ABSENT from this list: a gate
662 // whose recorded closure no longer matches the tree is the one case the loop may decline to rebuild
663 // and must still refuse to call current, because an abstention that acquits is the flattering
664 // failure nobody investigates.
665 if v == OSL_VINT_PROV_CURRENT { return 1 }
666 if v == OSL_VINT_REBUILT_PROV { return 1 }
667 return 0
668}
669func osl_vint_name(v: i64) -> *u8 {
670 if v == OSL_VINT_STAGE1 { return "FRESH-built-by-stage-1" as *u8 }
671 if v == OSL_VINT_REBUILT { return "FRESH-rebuilt-this-run" as *u8 }
672 if v == OSL_VINT_INDEPENDENT { return "INDEPENDENT-of-this-build" as *u8 }
673 if v == OSL_VINT_PROV_CURRENT { return "FRESH-closure-bank-CURRENT" as *u8 }
674 if v == OSL_VINT_REBUILT_PROV { return "FRESH-rebuilt-this-run-because-the-bank-DRIFTED" as *u8 }
675 if v == OSL_VINT_STALE_PROV { return "STALE-closure-bank-DRIFTED-and-the-rebuild-did-not-land" as *u8 }
676 if v == OSL_VINT_STALE_REFUSED { return "STALE-gate-rebuild-REFUSED" as *u8 }
677 if v == OSL_VINT_STALE_NOARTIFACT { return "STALE-fresh-artifact-absent-after-BUILT" as *u8 }
678 if v == OSL_VINT_STALE_WRONGSRC { return "STALE-gate-rebuild-compiled-WRONG-SOURCE" as *u8 }
679 if v == OSL_VINT_UNKNOWN_NOSRC { return "UNKNOWN-gate-source-absent" as *u8 }
680 if v == OSL_VINT_UNKNOWN_NONAME { return "UNKNOWN-gate-name-underivable" as *u8 }
681 return "UNKNOWN-cannot-decide" as *u8
682}
683func osl_closure_name(c: i64) -> *u8 {
684 if c == OSL_CLO_CONTAINS { return "CONTAINS-this-target" as *u8 }
685 if c == OSL_CLO_INDEPENDENT { return "INDEPENDENT-of-this-target" as *u8 }
686 return "UNPROVEN-no-source-readable" as *u8
687}
688
689// ---- PART 3 -- THE BANK, AS FUNCTIONS THE GATE CAN MUTATE -----------------
690// Every function below is PURE: it takes the comparator's exit code, not a file and not a fork. The
691// fork of nx_provcheck lives in nx_organ_ship's main(), for the same reason the resolver's roots are
692// parameters -- a lib that forked would drag a real compile into its own gate, and a decision that can
693// only be exercised by running the estate is a decision no mutation test can reach.
694
695// nx_provcheck's exit code -> the bank's verdict. EVERYTHING that is not one of the two decided exits
696// is UNPROVEN, deliberately including 127 (comparator absent) and -5 (its deadline fired): when the
697// instrument cannot answer, this loop must degrade to EXACTLY the incumbent's behaviour, never to a
698// refusal and never to a green.
699func osl_prov_of_exit(rc: i64) -> i64 {
700 if rc == OSL_PC_EXIT_CURRENT { return OSL_PROV_CURRENT }
701 if rc == OSL_PC_EXIT_DRIFTED { return OSL_PROV_DRIFTED }
702 return OSL_PROV_UNPROVEN
703}
704func osl_prov_name(p: i64) -> *u8 {
705 if p == OSL_PROV_CURRENT { return "BANK-CURRENT-artifact-is-its-sources" as *u8 }
706 if p == OSL_PROV_DRIFTED { return "BANK-DRIFTED-closure-moved-since-this-artifact-was-built" as *u8 }
707 return "BANK-UNPROVEN-no-recorded-closure-to-compare" as *u8
708}
709
710// THE DECISION. Read it as three lines in priority order, because that order IS the safety property:
711// 1 the target is its own gate -> stage 1 already built it; a second build is pure waste.
712// 2 the bank says DRIFTED -> REBUILD. The hash decides, and it decides regardless of imports.
713// 3 otherwise -> DELEGATE to the incumbent edge rule, which can only ADD a rebuild.
714// Because line 3 is a delegation and not a restatement, this function returns 1 on every input where
715// osl_should_rebuild returns 1. That is the no-regression guarantee, and it is a property of the code
716// rather than a promise about it: there is no path on which a CONTAINS closure stops rebuilding.
717// (2026-09-05: a CURRENT bank alone still vetoes nothing here, because a bank cannot say WHICH file it describes;
718// the veto lives in osl_should_rebuild_pick below, which takes the artifact identity as its extra argument.)
719func osl_should_rebuild_prov(gate: *u8, target: *u8, clo: i64, prov: i64) -> i64 {
720 if osl_streq(gate, target) == 1 { return 0 }
721 if prov == OSL_PROV_DRIFTED { return 1 }
722 return osl_should_rebuild(gate, target, clo)
723}
724
725// WHICH VINTAGE A GATE THAT WAS *NOT* REBUILT CARRIES. It lives beside the decision so the two can
726// never disagree about what "not rebuilt" meant. Only a bank that actually said CURRENT earns the
727// stronger name; an UNPROVEN bank falls back to whatever the edge scan could establish and keeps the
728// incumbent's own wording; and a DRIFTED bank that reaches here at all means the rebuild did not
729// happen, so it must read STALE even though this loop chose to proceed.
730// PROV_CURRENT is well-founded and not merely optimistic: nx_provcheck can only answer CURRENT after
731// nx_closurehash RESOLVED AND READ every source in the closure, so that answer already carries the
732// source-found precondition that osl_gate_closure reports separately as UNPROVEN.
733func osl_vint_norebuild(prov: i64, clo: i64) -> i64 {
734 if prov == OSL_PROV_DRIFTED { return OSL_VINT_STALE_PROV }
735 if prov == OSL_PROV_CURRENT { return OSL_VINT_PROV_CURRENT }
736 if clo == OSL_CLO_UNPROVEN { return OSL_VINT_UNKNOWN_NOSRC }
737 return OSL_VINT_INDEPENDENT
738}
739
740// WHICH VINTAGE A SUCCESSFUL REBUILD CARRIES. Both are current; they differ only in WHICH instrument
741// demanded the rebuild, and that is worth keeping because it is the measurement of how much the bank
742// is actually buying over the edge scan. A single REBUILT bucket would hide exactly that number.
743func osl_vint_rebuilt(prov: i64) -> i64 {
744 if prov == OSL_PROV_DRIFTED { return OSL_VINT_REBUILT_PROV }
745 return OSL_VINT_REBUILT
746}
747
748// THE HIGHEST VINTAGE CODE, so that a census over "every vintage" is bound to the LIST rather than to
749// a number hand-copied beside it. This exists because the distinctness tooth in nx_organ_ship_gate was
750// written against the then-last code and would have silently stopped covering the list the moment part
751// 3 added three more -- the tooth would still have passed, over a subset, while its name went on
752// claiming every code. A bound that has to be remembered at the moment of extension is a bound that
753// drifts, and it drifts toward understating coverage, which is the direction nobody audits.
754func osl_vint_max() -> i64 { return OSL_VINT_STALE_PROV }
755
756// ---- WHICH ARTIFACT DOES THE BANK VOUCH FOR? (2026-09-05) -------------------------------------------
757// nx_provcheck reads ./<gate>.provenance, the sidecar of the LAST BUILD -- root B, buildroot/_build/<gate>.sov.elf.
758// The resolver prefers root A (the promoted binary) for every gate the loop did not just build. When the two roots
759// hold DIFFERENT bytes, a CURRENT bank is a fact about root B while PROVE was about to run root A: one freshness
760// claim, two artifacts. MEASURED 2026-09-05 on nx_janitor_caps_gate: bank CURRENT, PROVE ran the 20,695 B promoted
761// binary (the pre-migration gate) while the banked artifact was 30,649 B -- and the receipt said
762// vintage=FRESH-closure-bank-CURRENT about a binary the bank had never described.
763// THE DECISION IS A PURE FUNCTION so its gate drives it in-process: the caller supplies which root the resolver
764// picked, both digests (empty when not computed or unreadable) and whether root B exists on disk.
765const OSL_PICK_KEEP: i64 = 0 // run what the resolver picked: root B already, or root A byte-identical to the banked artifact
766const OSL_PICK_SWITCH_B: i64 = 1 // root A differs from the banked root B: run root B, and say root A is BEHIND its own source
767const OSL_PICK_UNPROVEN: i64 = 2 // the bank cannot be matched to anything executable (root B absent -- a refused build eats the fossil and leaves the sidecar -- or a digest unreadable): UNPROVEN, never CURRENT
768func osl_pick_banked(resolved_root: i64, sha_a: *u8, sha_b: *u8, b_exists: i64) -> i64 {
769 if resolved_root != OSL_ROOT_A { return OSL_PICK_KEEP }
770 if b_exists == 0 { return OSL_PICK_UNPROVEN }
771 if sha_a[0] == (0 as u8) { return OSL_PICK_UNPROVEN }
772 if sha_b[0] == (0 as u8) { return OSL_PICK_UNPROVEN }
773 if osl_streq(sha_a, sha_b) == 1 { return OSL_PICK_KEEP }
774 return OSL_PICK_SWITCH_B
775}
776func osl_pick_name(p: i64) -> *u8 {
777 if p == OSL_PICK_KEEP { return "KEEP-RESOLVED" as *u8 }
778 if p == OSL_PICK_SWITCH_B { return "BANKED-BUILD-USED" as *u8 }
779 return "BANK-UNMATCHED" as *u8
780}
781// THE DECISION WITH THE ARTIFACT IN HAND (2026-09-05). osl_should_rebuild_prov deliberately lets a CURRENT bank veto
782// nothing, because a bank alone cannot say WHICH file it describes. This variant takes the pick (osl_pick_banked):
783// when the bank is CURRENT and the banked artifact is on disk and is the one PROVE will run (KEEP: identical to the
784// resolved binary, or SWITCH_B: about to be run instead of it), the rebuild the import scan demands can only
785// reproduce bytes already present, so it is skipped -- and a refused compile can no longer downgrade a proven
786// artifact to STALE. When the bank matches nothing executable (UNPROVEN) the incumbent rule decides, exactly as
787// before. Measured on the loop's own self-ship: bank CURRENT, import scan rebuilt, admission refused, PROVE ran the
788// stale promoted 43-tooth gate while the banked current 50-tooth build sat in root B.
789func osl_should_rebuild_pick(gate: *u8, target: *u8, clo: i64, prov: i64, pick: i64) -> i64 {
790 if osl_streq(gate, target) == 1 { return 0 }
791 if prov == OSL_PROV_DRIFTED { return 1 }
792 if prov == OSL_PROV_CURRENT { if pick != OSL_PICK_UNPROVEN { return 0 } }
793 return osl_should_rebuild(gate, target, clo)
794}
795
796// ---- AD2: THE HARNESS DISCLOSURE MANIFEST (2026-08-27) --------------------------------------------
797// Harness configuration governs more variance than model choice and must be DISCLOSED; two results are
798// directly comparable only when produced under the same harness. This is the PRODUCER half; the
799// consumer half (gv_envelope_comparable: differing tokens REFUSE a comparison) ships in the gate base
800// class. The manifest is ROWS `component|path|id`, truncate-written to a caller-named path, and the
801// TOKEN is "h"+16 hex of FNV-1a over the manifest BYTES -- any component change (a ruler binary, a
802// conf, a budget) changes the token, and an ABSENT component is NAMED in its row (absence is part of
803// the identity, never skipped, because a harness that lost a guard is a different harness).
804const OSL_HM_BUF: i64 = 16384
805const OSL_HM_MODE: i64 = 420
806// FNV-1a 64-bit (offset basis 14695981039346656037 as a signed literal; prime 1099511628211). The same
807// recipe nx_autofix_auto.af_harness_calc uses for the fix loop's token; folding both onto this one
808// copy is the named follow-on (that organ shipped hours before this lib grew the shared home).
809const OSL_FNV_OFFSET: i64 = 0 - 3750763034362895579
810const OSL_FNV_PRIME: i64 = 1099511628211
811const OSL_HM_HEX: i64 = 16
812const OSL_HM_HEX_A: i64 = 87
813const OSL_HM_ZERO: i64 = 48
814
815func osl_fnv64(h0: i64, buf: *u8, n: i64) -> i64 {
816 var h: i64 = h0
817 var i: i64 = 0
818 while i < n { h = (h ^ (buf[i] as i64)) * OSL_FNV_PRIME; i = i + 1 }
819 return h
820}
821func osl_fnv64_file(h0: i64, path: *u8) -> i64 {
822 let lb: *i64 = sys_mmap(16) as *i64
823 let b: *u8 = sys_read_file(path, lb)
824 if (b as i64) == 0 { return h0 }
825 return osl_fnv64(h0, b, lb[0])
826}
827func osl_hm_hex(h: i64, out: *u8) -> i64 {
828 out[0] = 104 as u8
829 var k: i64 = 0
830 while k < OSL_HM_HEX {
831 let nib: i64 = (h >> ((OSL_HM_HEX - 1 - k) * 4)) & 15
832 if nib < 10 { out[1 + k] = (OSL_HM_ZERO + nib) as u8 } else { out[1 + k] = (OSL_HM_HEX_A + nib) as u8 }
833 k = k + 1
834 }
835 out[1 + OSL_HM_HEX] = 0 as u8
836 return 1 + OSL_HM_HEX
837}
838func osl_hm_nl(d: *u8, o0: i64) -> i64 {
839 let nlb: *u8 = sys_mmap(2)
840 nlb[0] = 10 as u8
841 nlb[1] = 0 as u8
842 let o: i64 = osl_cat(d, o0, nlb)
843 sys_munmap(nlb, 2)
844 return o
845}
846// one component row: component|path|<h-token of the file bytes, or ABSENT (named, never skipped)>
847func osl_hm_row(d: *u8, o0: i64, comp: *u8, path: *u8) -> i64 {
848 var o: i64 = osl_cat(d, o0, comp)
849 o = osl_cat(d, o, "|" as *u8)
850 o = osl_cat(d, o, path)
851 o = osl_cat(d, o, "|" as *u8)
852 let fd: i64 = sys_openat_rd(path)
853 if fd < 0 { o = osl_cat(d, o, "ABSENT" as *u8) } else {
854 sys_close(fd)
855 let hx: *u8 = sys_mmap(24)
856 osl_hm_hex(osl_fnv64_file(OSL_FNV_OFFSET, path), hx)
857 o = osl_cat(d, o, hx)
858 sys_munmap(hx, 24)
859 }
860 return osl_hm_nl(d, o)
861}
862// one VALUE row (a budget or an argument): component|-|<decimal>
863func osl_hm_val(d: *u8, o0: i64, comp: *u8, v: i64) -> i64 {
864 var o: i64 = osl_cat(d, o0, comp)
865 o = osl_cat(d, o, "|-|" as *u8)
866 let t: *u8 = sys_mmap(32)
867 var m: i64 = v
868 var neg: i64 = 0
869 if m < 0 { neg = 1; m = 0 - m }
870 var k: i64 = 0
871 if m == 0 { t[0] = OSL_HM_ZERO as u8; k = 1 }
872 while m > 0 { t[k] = (OSL_HM_ZERO + (m % 10)) as u8; m = m / 10; k = k + 1 }
873 let t2: *u8 = sys_mmap(34)
874 var j: i64 = 0
875 if neg == 1 { t2[0] = 45 as u8; j = 1 }
876 var z: i64 = 0
877 while z < k { t2[j] = t[k - 1 - z]; j = j + 1; z = z + 1 }
878 t2[j] = 0 as u8
879 o = osl_cat(d, o, t2)
880 sys_munmap(t, 32)
881 sys_munmap(t2, 34)
882 return osl_hm_nl(d, o)
883}
884// THE MANIFEST: a row per component the ship's verdicts depend on, truncate-written to outpath; the
885// token (FNV over the manifest BYTES) lands in tok (>=18 bytes). `extra` may name ONE additional file
886// ("" = none) -- the gate's determinism and bite teeth turn on it. Returns the row count, <0 unwritable.
887func osl_harness_manifest(outpath: *u8, target_src_sha: *u8, gatepath: *u8, timeout_ms: i64, allow_loss: i64, extra: *u8, tok: *u8) -> i64 {
888 let d: *u8 = sys_mmap(OSL_HM_BUF)
889 var o: i64 = 0
890 var rows: i64 = 0
891 o = osl_cat(d, o, "# harness manifest -- the configuration this ship's verdicts were produced under" as *u8)
892 o = osl_hm_nl(d, o)
893 o = osl_hm_row(d, o, "builder" as *u8, "_offc/nx_sov_build_run.elf" as *u8); rows = rows + 1
894 o = osl_hm_row(d, o, "adopt-ruler" as *u8, "./nx_adoptgate.elf" as *u8); rows = rows + 1
895 o = osl_hm_row(d, o, "content-ruler" as *u8, "./nx_contentdiff.elf" as *u8); rows = rows + 1
896 o = osl_hm_row(d, o, "behave-ruler" as *u8, "./nx_behaveprobe.elf" as *u8); rows = rows + 1
897 o = osl_hm_row(d, o, "prove-gate" as *u8, gatepath); rows = rows + 1
898 o = osl_hm_row(d, o, "rigor-conf" as *u8, "knowledge/rigor.conf" as *u8); rows = rows + 1
899 o = osl_hm_row(d, o, "autofix-conf" as *u8, "knowledge/autofix.conf" as *u8); rows = rows + 1
900 o = osl_hm_row(d, o, "admit-conf" as *u8, "knowledge/build_admit.conf" as *u8); rows = rows + 1
901 o = osl_cat(d, o, "target-src-sha|-|" as *u8)
902 o = osl_cat(d, o, target_src_sha)
903 o = osl_hm_nl(d, o)
904 rows = rows + 1
905 o = osl_hm_val(d, o, "timeout-ms" as *u8, timeout_ms); rows = rows + 1
906 o = osl_hm_val(d, o, "allow-loss" as *u8, allow_loss); rows = rows + 1
907 if extra[0] != (0 as u8) { o = osl_hm_row(d, o, "extra" as *u8, extra); rows = rows + 1 }
908 osl_hm_hex(osl_fnv64(OSL_FNV_OFFSET, d, o), tok)
909 let fd: i64 = sys_openat_wr(outpath, OSL_HM_MODE)
910 if fd < 0 { return 0 - 1 }
911 sys_write(fd, d, o)
912 sys_close(fd)
913 return rows
914}
915
916
917// Exact loss-set approval binds complete evidence and both immutable artifacts.
918import "nx_tool_run.nx"
919import "nx_sha256.nx"
920struct OslLossSet { count:i64,record:NxBufOwned,digest:*u8 }
921func osls_len(s:*u8)->i64{var n:i64=0;while s[n]!=(0 as u8){n=n+1};return n}
922func osls_eq(a:*u8,b:*u8,n:i64)->i64{var i:i64=0;while i<n{if a[i]!=b[i]{return 0};i=i+1};return 1}
923func osls_prefix(a:*u8,n:i64,s:*u8)->i64{let m:i64=osls_len(s);if n<m{return 0};return osls_eq(a,s,m)}
924func osls_hex(s:*u8)->i64{if (s as i64)<=0{return 0};if osls_len(s)!=64{return 0};var i:i64=0;while i<64{let c:i64=s[i] as i64;if c<48{return 0};if c>57{if c<97||c>102{return 0}};i=i+1};return 1}
925// Decimal spans must be complete, checked and nonnegative; no ambiguous prefix parse.
926func osls_uint(s:*u8,n:i64)->i64{if n<=0{return -1};var v:i64=0;var i:i64=0;while i<n{let c:i64=s[i] as i64;if c<48||c>57{return -1};if v>(NX_BO_I64_MAX-(c-48))/10{return -1};v=v*10+c-48;i=i+1};return v}
927func osls_find(s:*u8,n:i64,key:*u8)->i64{let m:i64=osls_len(key);var i:i64=0;while i+m<=n{if osls_eq(s+i,key,m)==1{return i};i=i+1};return -1}
928func osls_token(s:*u8,n:i64,key:*u8,length:*i64)->i64{
929 length[0]=0;let kn:i64=osls_len(key);var found:i64=-1;var i:i64=0
930 while i<n{
931 while i<n{if s[i]!=(32 as u8){break};i=i+1};let start:i64=i
932 while i<n{if s[i]==(32 as u8){break};i=i+1}
933 if i-start>=kn{if osls_eq(s+start,key,kn)==1{if found>=0{return -1};found=start+kn;length[0]=i-found}}
934 };return found
935}
936func osls_field(s:*u8,n:i64,key:*u8)->i64{
937 var keyp:*u8=key;if keyp[0]==(32 as u8){keyp=keyp+1}
938 var vn:i64=0;let at:i64=osls_token(s,n,keyp,&vn);if at<0{return -1};return osls_uint(s+at,vn)
939}
940func osls_report(buf:*u8,n:i64)->i64{
941 if (buf as i64)<=0||n<=0{return -1};if buf[n-1]!=(10 as u8){return -1}
942 var pos:i64=0;var named:i64=0;var summary:i64=0;var total:i64=-1;var counted:i64=-1;var counts:i64=0;var verdict:i64=0
943 while pos<n{
944 var end:i64=pos;while end<n{if buf[end]==(10 as u8){break};end=end+1}
945 let ln:i64=end-pos;let line:*u8=buf+pos
946 if osls_prefix(line,ln," LOST: ")==1{if ln<=8{return -1};named=named+1}
947 if osls_prefix(line,ln,"display_class=LOST ")==1{
948 var cn:i64=0;let ci:i64=osls_token(line,ln,"display_class=",&cn);if ci<0||cn!=4{return -1};if osls_eq(line+ci,"LOST",4)!=1{return -1}
949 summary=summary+1;total=osls_field(line,ln," total=")
950 if osls_field(line,ln," shown=")!=total||osls_field(line,ln," omitted=")!=0{return -1}
951 }
952 if osls_prefix(line,ln,"runs_scanned=")==1{counts=counts+1;counted=osls_field(line,ln," lost_from_live=");if osls_field(line,ln,"runs_scanned=")<counted{return -1}}
953 if osls_prefix(line,ln,"NX-CONTENTDIFF ")==1{var vn:i64=0;let vp:i64=osls_token(line,ln,"verdict=",&vn);if vp<0||vn!=3{return -1};if osls_eq(line+vp,"RED",3)!=1{return -1};verdict=verdict+1;if end!=n-1{return -1}}
954 pos=end+1
955 }
956 if summary!=1||counts!=1||verdict!=1||named<=0||named!=total||counted!=total{return -1};return total
957}
958func osls_add(b:*NxBufOwned,s:*u8,n:i64)->i64{return nx_bo_append(b,s,n,0)}
959func osls_text(b:*NxBufOwned,s:*u8)->i64{return osls_add(b,s,osls_len(s))}
960func osls_build(live:*u8,candidate:*u8,report:*u8,n:i64,set:*OslLossSet)->i64{
961 set.count=-1;set.record.buf=0 as *u8;set.record.len=0;set.record.cap=0;set.digest=0 as *u8
962 if osls_hex(live)==0||osls_hex(candidate)==0{return -1}
963 let count:i64=osls_report(report,n);if count<0{return -1}
964 var rc:i64=osls_text(&set.record,"NISHI-SHIP-LOSS-SET/1\nlive_sha256=")
965 if rc==0{rc=osls_add(&set.record,live,64)};if rc==0{rc=osls_text(&set.record,"\ncandidate_sha256=")}
966 if rc==0{rc=osls_add(&set.record,candidate,64)};if rc==0{rc=osls_text(&set.record,"\nreport=nx_contentdiff-all\n\n")}
967 if rc==0{rc=osls_add(&set.record,report,n)}
968 if rc!=0{nx_bo_release(&set.record);return rc}
969 set.digest=sys_mmap_try(65);if (set.digest as i64)<=0{nx_bo_release(&set.record);return -1}
970 let raw:*u8=sys_mmap_try(32);if (raw as i64)<=0{osls_close(set);return -1};sha256_digest(set.record.buf,set.record.len,raw)
971 let alphabet:*u8="0123456789abcdef";var i:i64=0;while i<32{let b:i64=raw[i] as i64;set.digest[i*2]=alphabet[b/16];set.digest[i*2+1]=alphabet[b%16];i=i+1};set.digest[64]=0 as u8;sys_munmap_direct(raw,32)
972 set.count=count;return 0
973}
974func osls_close(set:*OslLossSet)->i64{var rc:i64=nx_bo_release(&set.record);if (set.digest as i64)>0{let r:i64=sys_munmap_direct(set.digest,65);if r<0{rc=r};set.digest=0 as *u8};return rc}
975func osls_approve(set:*OslLossSet,approval:*u8,live:*u8,candidate:*u8)->i64{
976 if set.count<=0{return 0};if osls_hex(approval)==0||osls_hex(live)==0||osls_hex(candidate)==0{return 0}
977 if osls_eq(set.digest,approval,64)==0{return 0}
978 let prefix:i64=osls_len("NISHI-SHIP-LOSS-SET/1\nlive_sha256=")
979 if osls_eq(set.record.buf+prefix,live,64)==0{return 0}
980 let off:i64=prefix+64+osls_len("\ncandidate_sha256=")
981 return osls_eq(set.record.buf+off,candidate,64)
982}
983// Verify bytes and fsync the SAME open descriptor before accepting an existing record.
984// This closes retry after a prior fsync failure; matching page-cache bytes alone are insufficient.
985func osls_sync_existing(fd:i64,set:*OslLossSet)->i64{
986 let n:i64=sys_lseek(fd,0,2);if n!=set.record.len||n<=0{return -1};if sys_lseek(fd,0,0)!=0{return -1}
987 let b:*u8=sys_mmap_try(n);if (b as i64)<=0{return -1}
988 var off:i64=0;var rc:i64=0
989 while off<n{let r:i64=sys_read(fd,b+off,n-off);if r==TR_EINTR{continue};if r<=0{rc=-1;break};off=off+r}
990 if rc==0{if osls_eq(b,set.record.buf,n)!=1{rc=-1}}
991 if rc==0{var extra:u8=0 as u8;let eof:i64=sys_read(fd,&extra,1);if eof!=0{rc=-1}}
992 if rc==0{if sys_fsync(fd)<0{rc=-1}}
993 let freed:i64=sys_munmap_direct(b,n);if freed<0{rc=freed};return rc
994}
995// An existing exact record is reusable. A partial or changed record is never repaired in place.
996func osls_retain(path:*u8,dir:*u8,set:*OslLossSet)->i64{
997 let fd:i64=sys_openat_exclusive(path,420)
998 if fd<0{
999 if fd!=(-17){return -1}
1000 let existing:i64=sys_openat_rd(path);if existing<0{return -1}
1001 let checked:i64=osls_sync_existing(existing,set);let closed:i64=sys_close(existing)
1002 if checked!=0||closed<0{return -1}
1003 }else{
1004 var off:i64=0;while off<set.record.len{let w:i64=sys_write(fd,set.record.buf+off,set.record.len-off);if w==TR_EINTR{continue};if w<=0{sys_close(fd);return -1};off=off+w}
1005 let flushed:i64=sys_fsync(fd);let closed:i64=sys_close(fd);if flushed<0||closed<0{return -1}
1006 }
1007 let d:i64=sys_openat_directory(dir);if d<0{return -1};let synced:i64=sys_fsync(d);let dc:i64=sys_close(d);if synced<0||dc<0{return -1};return 0
1008}