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}