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