code wiki / (root) / nx_forkfresh_lib.nx

nx_forkfresh_lib.nx source

↩ module page · 212 lines · 12704 B

1// nx_forkfresh_lib.nx -- FORK FRESHNESS: is the binary an organ is ABOUT TO FORK the same bytes as the 2// artifact that was PROMOTED? The decision core, split into a lib so a gate can drive it IN-PROCESS 3// (nothing can import a main(), and a gate that fork/execs a deployed elf reports NOT-REACHED for every 4// mutant -- organ_gate.conf records that lesson five times over). 5// 6// WHY THIS EXISTS, MEASURED 2026-08-31 AND REGENERATED THE SAME DAY. 7// nx_compare_regen chdirs into buildroot and forks the BARE paths _offc/nx_swcompare_{matrix,sota,hub}.elf, 8// so the PUBLISHER of every /compare page executes buildroot/_offc/. But /api/promote writes the SERVING 9// ROOT (and nishihost/_offc); buildroot/_offc updates ONLY via a manual nx_restage. A regen therefore 10// returned published=201 fails=0 verdict=GREEN and CHANGED NOTHING -- every publish line read prev= EQUAL 11// to the new byte count -- because all three generators were a generation behind. 12// FOUR AGENTS EACH VERIFIED THEIR PROMOTE AGAINST THE SERVING ROOT AND/OR nishihost/_offc AND ALL FOUR 13// MISSED THE COPY THE PUBLISHER ACTUALLY RUNS. nx_catalog CANNOT see buildroot/_offc, so every row read 14// BUILT==PROMOTED, with_gaps=0, VERDICT LIVE -- the clean-looking reading that hid it. 15// (STAR)A CATALOGUE THAT CANNOT SEE THE COPY THAT EXECUTES WILL REPORT A STALE FLEET AS LIVE. 16// The remedy so far was "remember to nx_restage", which is O(seats) forever. The estate standing law is 17// that a law which must be RECALLED at the moment of temptation is not a control -- only a mechanism in 18// the path is, and writing another rule is the weakest available fix. This lib is that mechanism. 19// 20// WHY THE PROMOTED SERVING-ROOT ARTIFACT IS THE REFERENCE, AND NOT THE OTHER TWO CANDIDATES: 21// * NOT _build/<t>.sov.elf -- that is a BUILD FOSSIL. It is whatever the last build anybody ran left 22// behind, it can predate the source, and a REFUSED build DELETES it. A BEHIND row whose reference is 23// a fossil points at exactly the wrong action, and re-baselining against an absence reports a live 24// subject as never-built. 25// * NOT a fresh rebuild from source -- that is what nx_stale_check does, and it answers a DIFFERENT 26// question (is this behind SOURCE). Three compiles inside the publisher hot path, on a box whose 27// build admission already refuses under load, would make the publish surface hostage to the compiler. 28// * THE PROMOTED ARTIFACT IS THE AUTHORITY because /api/promote is what installs it, nx_catalog calls 29// that path PROMOTED, and -- decisively -- nx_compare_regen ALREADY forks its gapmap generator from 30// ../nx_swcompare_gapmap.elf with the comment "promoted-elf path (nishihost root, CWD is buildroot): 31// /api/build + /api/promote place it". The convention is the organ own, established, not invented 32// here. That is also why the gapmap fork needs no check: it already reads the authority directly. 33// 34// COMPARE BY HASH, NEVER BY SIZE. Measured on the day this shipped: buildroot/_offc/nx_maturity_board.elf 35// and its promoted twin differ by TWENTY-SIX BYTES with different digests, and the buildroot copy is the 36// LARGER of the two -- so both a size-equality test and an is-the-fork-smaller heuristic acquit it. The 37// full-width digest is the only ruler that convicts. 38// 39// FAIL DIRECTION, ESTABLISHED BEFORE SHIPPING: every unreadable input degrades to a NAMED third state 40// (FF_UNPROVEN / FF_NOFORK), never to FRESH and never to STALE. An axis that cannot see must abstain, not 41// acquit -- and equally must not accuse, because a false STALE would send a seat to restage a generator 42// that is already correct. Nothing in this lib writes, deletes, forks or refuses; it only classifies. 43// license_tier: ORIGINAL No hw writes (Rule 26). 44import "nx_syscalls.nx" 45import "nx_sha256.nx" 46 47// The four states. A NEGATIVE ANSWER THAT CANNOT NAME WHICH NEGATIVE IT IS WILL BE READ AS THE MOST 48// ALARMING ONE AVAILABLE, so the fork target is missing and the reference is missing are separate 49// values with separate remedies -- one is a broken build tree, the other is an unpromoted organ. 50const FF_FRESH: i64 = 0 // fork target and promoted artifact are byte-identical 51const FF_STALE: i64 = 1 // both readable, digests DIFFER -- the publisher would run the wrong code 52const FF_UNPROVEN: i64 = 2 // the PROMOTED reference could not be read: could not look, no conclusion 53const FF_NOFORK: i64 = 3 // the fork target itself could not be read: the fork will fail anyway 54 55const FF_DIGEST_BYTES: i64 = 32 // SHA-256, FIPS 180-4 -- the whole digest is compared, always 56const FF_HEX_BYTES: i64 = 65 // FF_DIGEST_BYTES * 2 plus one for the NUL 57const FF_PATH_BYTES: i64 = 512 58const FF_SIZES_SLOTS: i64 = 2 // [0] fork bytes, [1] reference bytes; -1 = not measured 59const FF_HEX_DIGIT_0: i64 = 48 // ASCII zero 60const FF_HEX_ALPHA_A: i64 = 87 // lowercase a minus 10, so 10..15 render a..f 61const FF_NIBBLE: i64 = 16 62const FF_TEN: i64 = 10 63const FF_SLASH: i64 = 47 // ASCII forward slash 64const FF_SCRATCH_PTR: i64 = 16 65 66// The offset of the last path segment. /api/promote installs binaries FLAT at the serving root, so the 67// reference path is the fork path BASENAME under the promoted prefix -- derived, never hand-written a 68// second time, because a hand-written twin of a path literal is a second copy that drifts silently. 69func ff_basename(p: *u8) -> i64 { 70 var i: i64 = 0 71 var b: i64 = 0 72 while p[i] != (0 as u8) { 73 if p[i] == (FF_SLASH as u8) { b = i + 1 } 74 i = i + 1 75 } 76 return b 77} 78 79// Build prefix followed by basename(forkpath). The prefix is a PARAMETER and not a hardcoded parent-dir 80// literal on purpose: a resolver that can only ever produce one path cannot be tested without editing 81// production layout, and an untestable resolver is exactly the component that rots into the defect this 82// file exists to stop. nx_artifact_root.nx reached the same conclusion for the same reason. 83func ff_promoted_path(forkpath: *u8, out: *u8, prefix: *u8) -> i64 { 84 var o: i64 = 0 85 var i: i64 = 0 86 while prefix[i] != (0 as u8) { out[o] = prefix[i]; o = o + 1; i = i + 1 } 87 var b: i64 = ff_basename(forkpath) 88 while forkpath[b] != (0 as u8) { out[o] = forkpath[b]; o = o + 1; b = b + 1 } 89 out[o] = 0 as u8 90 return o 91} 92 93// Full-width lowercase hex, so the field a reader sees is byte-for-byte what nx_filehash and nx_catalog 94// print. A DISPLAYED PREFIX would invite the reader to believe the COMPARISON was a prefix too; it never 95// is -- ff_classify walks all FF_DIGEST_BYTES. 96func ff_hex(dig: *u8, out: *u8, nbytes: i64) -> i64 { 97 var i: i64 = 0 98 var o: i64 = 0 99 while i < nbytes { 100 let v: i64 = dig[i] as i64 101 let hi: i64 = v / FF_NIBBLE 102 let lo: i64 = v % FF_NIBBLE 103 if hi < FF_TEN { out[o] = (FF_HEX_DIGIT_0 + hi) as u8 } else { out[o] = (FF_HEX_ALPHA_A + hi) as u8 } 104 o = o + 1 105 if lo < FF_TEN { out[o] = (FF_HEX_DIGIT_0 + lo) as u8 } else { out[o] = (FF_HEX_ALPHA_A + lo) as u8 } 106 o = o + 1 107 i = i + 1 108 } 109 out[o] = 0 as u8 110 return o 111} 112 113// Digest one file. COMPOSES sys_read_file, which sizes its buffer from the file itself (lseek END) and 114// CANNOT short-read -- so there is no cap to guess here and none to tune later. A ceiling that has to be 115// guessed is a defect generator in both directions, and for a FILE read there is no guess to make. 116// Returns 1 with dig and szout filled, or 0 if the file could not be read. 117// AN EMPTY FILE IS A READ FAILURE FOR THIS PURPOSE, DELIBERATELY: sys_read_file hands back a valid 118// pointer with length 0 for an empty regular file, and two empty files digest IDENTICALLY -- so treating 119// empty as readable would report a pair of zero-byte stubs as FORK-FRESH. That is the vacuous-compare 120// defect, and it is one line to close. 121func ff_digest_file(path: *u8, dig: *u8, szout: *i64) -> i64 { 122 szout[0] = 0 - 1 123 let l: *i64 = sys_mmap(FF_SCRATCH_PTR) as *i64 124 let b: *u8 = sys_read_file(path, l) 125 if (b as i64) == 0 { return 0 } 126 let n: i64 = l[0] 127 if n <= 0 { sys_free_file(b, n); return 0 } 128 sha256_digest(b, n, dig) 129 sys_free_file(b, n) 130 szout[0] = n 131 return 1 132} 133 134// THE DECISION. fdig and rdig are FF_DIGEST_BYTES each; sizes is FF_SIZES_SLOTS i64 slots, left at -1 for 135// any side that was never measured so a caller can never print a fabricated size beside an abstention. 136func ff_classify(forkpath: *u8, refpath: *u8, fdig: *u8, rdig: *u8, sizes: *i64) -> i64 { 137 let fs: *i64 = sys_mmap(FF_SCRATCH_PTR) as *i64 138 let rs: *i64 = sys_mmap(FF_SCRATCH_PTR) as *i64 139 sizes[0] = 0 - 1 140 sizes[1] = 0 - 1 141 if ff_digest_file(forkpath, fdig, fs) == 0 { return FF_NOFORK } 142 sizes[0] = fs[0] 143 if ff_digest_file(refpath, rdig, rs) == 0 { return FF_UNPROVEN } 144 sizes[1] = rs[0] 145 var i: i64 = 0 146 while i < FF_DIGEST_BYTES { 147 if fdig[i] != rdig[i] { return FF_STALE } 148 i = i + 1 149 } 150 return FF_FRESH 151} 152 153// One word per state, so producer and reader cannot disagree on spelling. When two organs must agree, 154// make disagreement impossible by construction rather than by discipline. 155func ff_state_word(st: i64) -> *u8 { 156 if st == FF_FRESH { return "FORK-FRESH" as *u8 } 157 if st == FF_STALE { return "FORK-STALE" as *u8 } 158 if st == FF_NOFORK { return "FORK-ABSENT" as *u8 } 159 return "FORK-UNPROVEN" as *u8 160} 161 162// Is this state one a consumer should act on? STALE is the only actionable one; the two abstentions are 163// reported and counted but never presented as a finding. Kept here rather than in each consumer so the 164// question has exactly one answer estate-wide. 165func ff_is_actionable(st: i64) -> i64 { 166 if st == FF_STALE { return 1 } 167 return 0 168} 169 170// THE DIRECTION OF A STALE FORK -- AND IT IS THE FIELD THAT DECIDES WHICH REMEDY IS SAFE. 171// ff_classify says the two artifacts DIFFER. It does not say WHICH WAY, and the two directions want 172// OPPOSITE actions: restaging a fork that is BEHIND pulls the executing copy up to a fresh build, while 173// restaging a fork that is AHEAD overwrites the copy carrying runs the promoted artifact lacks -- the one 174// action that destroys capability. A remedy printed without this distinction is correct half the time and 175// destructive the other half, so the distinction lives HERE, beside the classification that already made 176// the comparison, and not in each consumer's message text where two consumers could answer differently. 177// SIZE IS NEVER AN IDENTITY -- AND THIS IS NOT USING IT AS ONE. The digests have ALREADY decided that the 178// pair differs; size is then asked only the weaker question of which side carries more bytes. Where size 179// cannot answer -- equal sizes, differing digests -- the honest answer is UNDECIDED, never a guess: that 180// same-size divergence is precisely the case a size comparison is blind to, and it is NAMED rather than 181// folded into either action. A coin flip over a destructive action is not a remedy. 182const FF_DIR_NA: i64 = 0 // not a STALE pair, or a side was never measured: no direction exists 183const FF_DIR_BEHIND: i64 = 1 // fork SMALLER than promoted -- restage brings the executing copy up 184const FF_DIR_AHEAD: i64 = 2 // fork LARGER than promoted -- restage would DESTROY what it carries 185const FF_DIR_UNDECIDED: i64 = 3 // sizes equal, digests differ -- size cannot discriminate; say so 186 187func ff_direction(st: i64, sizes: *i64) -> i64 { 188 if st != FF_STALE { return FF_DIR_NA } 189 // NEVER DERIVE A DIRECTION FROM AN UNMEASURED SIZE. ff_classify leaves -1 in any slot it did not 190 // measure, and a fabricated direction printed beside an abstention is worse than no direction at all. 191 if sizes[0] < 0 { return FF_DIR_NA } 192 if sizes[1] < 0 { return FF_DIR_NA } 193 if sizes[0] < sizes[1] { return FF_DIR_BEHIND } 194 if sizes[0] > sizes[1] { return FF_DIR_AHEAD } 195 return FF_DIR_UNDECIDED 196} 197 198// One word per direction, the same discipline as ff_state_word: producer and reader cannot disagree on 199// spelling because there is only one place the spelling exists. 200func ff_direction_word(d: i64) -> *u8 { 201 if d == FF_DIR_BEHIND { return "FORK-BEHIND" as *u8 } 202 if d == FF_DIR_AHEAD { return "FORK-AHEAD" as *u8 } 203 if d == FF_DIR_UNDECIDED { return "FORK-DIVERGED-SAME-SIZE" as *u8 } 204 return "FORK-DIRECTION-NA" as *u8 205} 206 207// IS RESTAGE THE SAFE REMEDY FOR THIS DIRECTION? Only for a fork that is BEHIND. Kept beside the 208// direction so a second consumer cannot answer this question differently from the first one. 209func ff_restage_is_safe(d: i64) -> i64 { 210 if d == FF_DIR_BEHIND { return 1 } 211 return 0 212}