code wiki / (root) / nx_organ_ship_lib.nx

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}