code wiki / _hdl_build / nx_builddeploy_lib_qualify_20260910.nx
nx_builddeploy_lib_qualify_20260910.nx source
↩ module page · 150 lines · 8771 B
1// nx_builddeploy_lib.nx -- THE ONE RULER FOR "MAY THIS BUILD WRITE A CONSUMER-VISIBLE BINARY?"
2//
3// WHY THIS EXISTS. nx_sov_build_run decides, inline and in three scattered places, whether a successful
4// build ALSO replaces the binary consumers fork. Those guards were written on three different days for
5// three different incidents -- toolchain (rule 26), the daemon supervisor-respawn class, and now the
6// queued-build class -- and each only ever covered the case that motivated it. A LAW APPLIED IN ONE VERB
7// AND NOT ITS SIBLING IS HALF A LAW: the flag literally named --build-only was never part of the decision
8// at all, so it built AND deployed, and nothing in the estate could test that it did.
9//
10// MEASURED 2026-08-26 from the queue's OWN artifact (knowledge/store/planrun-build-nx_gate_roster_run-):
11// [nx_sov_build_run] nx_gate_roster_run: DEPLOYED to serving root (twin refresh, live for the next
12// fork) -> ../nx_gate_roster_run.elf
13// That row was seeded by /api/build's own load-refusal path: nx_mgmt_api ma_build_refusal_body forks
14// nx_job_run nx_buildq add, and nx_buildq seeds the plan step "nx_sov_build_run <target> --build-only".
15// The refusal receipt tells the caller the result "lands in knowledge/store/planrun-build-<target>-;
16// promote deliberately afterwards". By the time the caller reads that row the binary is ALREADY LIVE:
17// no nx_contentdiff, no nx_behaveprobe, no capability-loss check, no .prev bank.
18// A CALLER TOLD ITS WORK WAS MERELY DEFERRED HAD ITS SERVING BINARY REPLACED, WITH NO SEAT IN THE LOOP.
19//
20// THE HIDDEN VARIABLE IS THE CALLER'S WORKING DIRECTORY, AND IT IS WHY THIS SURVIVED SO LONG.
21// The same binary, the same flag and the same target deploy or do not deploy depending only on where the
22// caller stood. /api/build (nx_hostctl cmd_buildrun) chdir's INTO buildroot before exec, so the builder's
23// CWD anchor never fires, root_pfx stays empty, the twin probe asks for buildroot/<name>.elf -- which does
24// not exist -- and nothing is installed, which is why the build/promote split has held there and why
25// hundreds of BEHIND rows exist on the drift board. The queue's runner starts in the SERVING ROOT, the
26// anchor fires, root_pfx becomes "../", and the identical command deploys. The "../" in the receipt above
27// is the PROOF the anchor fired: no other branch can emit that prefix.
28// A FLAG WHOSE MEANING DEPENDS ON THE CALLER'S CWD CANNOT BE REASONED ABOUT FROM ITS NAME.
29//
30// WHAT THIS LIB IS. A pure integer decision -- no I/O, no paths, no forks -- so a gate holds every
31// combination of its inputs with a fixture instead of reproducing a host state. The CALLER keeps its
32// probes (does a twin exist, is this a toolchain, is this a declared daemon); this owns the JUDGEMENT.
33// Because there is only ONE of it, the queued lane and the seat lane cannot disagree.
34//
35// SCOPE STATED HONESTLY -- WHAT THIS DELIBERATELY DOES NOT CHANGE:
36// (1) dkind < 0 (organ_kind.conf unreadable, so daemon-ness UNPROVEN) still permits a serving-root
37// write on the DEFAULT path, exactly as the incumbent does, and the caller still warns. That is an
38// abstention-that-acquits and it is a real residual, but it is a DIFFERENT lane from this one and
39// narrowing it would change the default build path for every caller. It is encoded faithfully here
40// and pinned by its own tooth so the next reader adjudicates it instead of inheriting it silently.
41// (2) A daemon's _offc twin is still refreshed (only the SERVING-ROOT write is held). Preserved from
42// the incumbent deliberately: this lane must not change any path that previously succeeded.
43// license_tier: ORIGINAL No hw writes (Rule 26).
44
45// ---- consumer locations, as a bitmask ----
46const BD_LOC_NONE: i64 = 0
47const BD_LOC_OFFC: i64 = 1
48const BD_LOC_ROOT: i64 = 2
49
50// ---- which rule decided; the receipt must NAME it, never just report the outcome ----
51const BD_WHY_TOOLCHAIN: i64 = 1 // rule 26: only /api/promote_toolchain may move the toolchain
52const BD_WHY_BUILD_ONLY: i64 = 2 // the caller said build only, so this build installs nothing
53const BD_WHY_FORCE: i64 = 3 // --install: explicit, deliberate intent to make it permanent
54const BD_WHY_REFRESH: i64 = 4 // refresh-IF-PRESENT (LM-026): a twin exists and is being kept fresh
55const BD_WHY_NO_TWIN: i64 = 5 // nothing consumes this name yet; nothing to refresh
56
57// Does the caller's intent permit ANY consumer-visible write at all?
58// Split out because it is the whole of the new rule and a gate can hold it alone.
59// --install is DELIBERATE INTENT and outranks --build-only when both are passed, so a caller that asks
60// for an install in the same breath still gets one; --build-only alone can never install anything.
61func bd_intent_permits_install(build_only: i64, force_install: i64) -> i64 {
62 if force_install == 1 { return 1 }
63 if build_only == 1 { return 0 }
64 return 1
65}
66
67// THE DECISION. Returns a BD_LOC_* bitmask of the consumer locations this build may write.
68func bd_locations(has_offc: i64, has_root: i64, force_install: i64, build_only: i64, toolchain: i64, dkind: i64) -> i64 {
69 // Rule 26 first and unconditionally: a build must never be able to replace the compiler that
70 // produced it, not even under an explicit --install. A self-modifying build path has no rollback.
71 if toolchain == 1 { return BD_LOC_NONE }
72 if bd_intent_permits_install(build_only, force_install) == 0 { return BD_LOC_NONE }
73 var loc: i64 = BD_LOC_NONE
74 if has_offc == 1 { loc = loc + BD_LOC_OFFC }
75 if force_install == 1 {
76 if has_offc == 0 { loc = loc + BD_LOC_OFFC }
77 }
78 if has_root == 1 {
79 // A DECLARED DAEMON'S SERVING-ROOT FILE IS NOT AN ARTIFACT, IT IS A DEPLOY: the supervisor
80 // respawns from disk, so writing it ships with no canary, no health gate and no .prev.
81 if dkind != 1 { loc = loc + BD_LOC_ROOT }
82 }
83 return loc
84}
85
86func bd_writes_offc(loc: i64) -> i64 { if (loc & BD_LOC_OFFC) != 0 { return 1 } return 0 }
87func bd_writes_root(loc: i64) -> i64 { if (loc & BD_LOC_ROOT) != 0 { return 1 } return 0 }
88
89// WHY the decision came out as it did, so the receipt can name the rule rather than the mechanism.
90func bd_why(has_offc: i64, has_root: i64, force_install: i64, build_only: i64, toolchain: i64, dkind: i64) -> i64 {
91 if toolchain == 1 { return BD_WHY_TOOLCHAIN }
92 if bd_intent_permits_install(build_only, force_install) == 0 { return BD_WHY_BUILD_ONLY }
93 if force_install == 1 { return BD_WHY_FORCE }
94 let loc: i64 = bd_locations(has_offc, has_root, force_install, build_only, toolchain, dkind)
95 if loc == BD_LOC_NONE { return BD_WHY_NO_TWIN }
96 return BD_WHY_REFRESH
97}
98
99// A SERVING-ROOT TWIN EXISTED AND SOMETHING DELIBERATELY DID NOT WRITE IT.
100// This is the line the caller owes its reader: silence here is what made the queued-build class
101// invisible for weeks. "I built it and left your live binary alone" is a RESULT, not an absence.
102func bd_root_held(has_root: i64, force_install: i64, build_only: i64, toolchain: i64, dkind: i64) -> i64 {
103 if has_root != 1 { return 0 }
104 if toolchain == 1 { return 1 }
105 if bd_intent_permits_install(build_only, force_install) == 0 { return 1 }
106 if dkind == 1 { return 1 }
107 return 0
108}
109
110// Qualification runs the built target but grants no consumer-install intent.
111// Conflicting explicit intents refuse rather than depend on flag order.
112func bd_qualification_mode(qualify:i64, build_only:i64, force_install:i64)->i64 {
113 if qualify==0 { return 0 }
114 if qualify!=1 { return -1 }
115 if build_only==1 || force_install==1 { return -1 }
116 return 1
117}
118func bd_qualification_flag(flag:*u8)->i64 {
119 let wanted:*u8="--qualify" as *u8
120 var i:i64=0
121 while wanted[i]!=0 as u8 { if flag[i]!=wanted[i] { return 0 }; i=i+1 }
122 if flag[i]!=0 as u8 { return 0 }
123 return 1
124}
125
126// Preserve documented incumbent flag families, but never silently run on an unknown flag.
127func bd_lane_flag_known(flag:*u8)->i64 {
128 if flag[0]!=45 as u8 || flag[1]!=45 as u8 { return 0 }
129 let c:i64=flag[2] as i64
130 if c==98 || c==105 || c==100 || c==110 || c==114 || c==116 { return 1 }
131 return bd_qualification_flag(flag)
132}
133
134func bd_run_timeout_flag(flag:*u8)->i64 {
135 let wanted:*u8="--run-timeout-ms" as *u8
136 var i:i64=0;while wanted[i]!=0 as u8 { if flag[i]!=wanted[i] { return 0 };i=i+1 }
137 if flag[i]!=0 as u8 { return 0 };return 1
138}
139
140// Decimal input must fit the signed runtime counter before any arithmetic.
141func bd_positive_ms(s:*u8)->i64 {
142 var v:i64=0;var i:i64=0
143 while s[i]!=0 as u8 {
144 let digit:i64=(s[i] as i64)-48
145 if digit<0 || digit>9 { return -1 }
146 if v>922337203685477580 || (v==922337203685477580 && digit>7) { return -1 }
147 v=v*10+digit;i=i+1
148 }
149 if i==0 || v<=0 { return -1 };return v
150}