code wiki / _hdl_build / nx_search_ship.nx
nx_search_ship.nx source
↩ module page · 488 lines · 33308 B
1// nx_search_ship.nx -- THE SEARCH SHIP LOOP AS ONE ORGAN (operator 2026-09-14: "make sure our primitives get
2// shifted into workflows get shifted to agents so we are 1 2 3 step"). On 2026-09-14 the seat shipped the docportal
3// search daemon four times and every ship was ten hand-pecked calls: build the daemon, build the judged-set referee,
4// run the promote ruler, run the promoted AND the staged referee and diff their per-query lines, run the sticky gate,
5// bank the live binary by hash, deploy through the health-checked /api/deploy with expect_sha256, wait out the
6// warm-before-listen window, probe cold two-common-term queries through the edge and read phase_us, then journal.
7// This organ is that loop as ONE call. It refuses at the first RED, and it composes the estate's rulers by FORK
8// (nx_mgmt_call, nx_contentdiff, nx_qrels_bench, nx_filehash, nx_filecopy, nx_https_get_cli2) -- it re-implements
9// none of them, so there is still exactly one of each. The pure parsers live in nx_search_ship_lib (gate-driven).
10// TWO MODES (the Codex method: a worker ends at a PRIVATE CANDIDATE + RECEIPT, only the root adopts):
11// nx_search_ship dry -- build + ruler + referee A/B + receipt; NOTHING is deployed (the candidate)
12// nx_search_ship deploy -- the same, then bank + deploy + warm-wait + cold probes + journal (the adoption)
13// OPTIONS: allow_loss=<n> (named lost runs the ruler may report; each is journalled) | bar=<ms> (cold-probe bar,
14// default the search-query row of knowledge/uat_journeys.conf, else 3000) | probes=<path> (one query per line,
15// default knowledge/search_probes.conf, else the journey's own query family photos).
16// REFEREE RULE: identical per-query lines = GREEN; lines moved with mean ndcg AND mrr not lower = PROCEEDS ANNOUNCED
17// with the first differing line printed and journalled (a ranking change the caller adjudicates); a lower mean
18// REFUSES. EXIT: 0 GREEN | 2 usage | 12 build refused | 11 wrong src_path | 31 ruler RED | 41 referee worse |
19// 53 bank failed | 51 deploy did not land | 52 deploy_status not GREEN | 61 daemon never answered | 71 a probe over
20// the bar. The LAST line is SEARCH-SHIP verdict=... for positional readers.
21// license_tier: ORIGINAL expect_exit: 0
22import "nx_syscalls.nx"
23import "nx_estate_path.nx"
24import "nx_tool_run.nx"
25import "nx_search_ship_lib.nx"
26
27const SS_CAP: i64 = 262144
28const SS_SMALL: i64 = 8192
29const SS_TMO_MS: i64 = 900000
30const SS_MGMT: *u8 = "./nx_mgmt_call.elf" as *u8
31const SS_CONTENTDIFF: *u8 = "./nx_contentdiff.elf" as *u8
32const SS_FILEHASH: *u8 = "./nx_filehash.elf" as *u8
33const SS_FILECOPY: *u8 = "./nx_filecopy.elf" as *u8
34const SS_HTTPS: *u8 = "./nx_https_get_cli2.elf" as *u8
35const SS_BENCH_LIVE: *u8 = "./nx_qrels_bench.elf" as *u8
36const SS_BENCH_STAGED: *u8 = "./nx_qrels_bench.sov.elf.new" as *u8
37const SS_DAEMON: *u8 = "nx_docportal_admin_daemon" as *u8
38const SS_DAEMON_ELF: *u8 = "nx_docportal_admin_daemon.elf" as *u8
39const SS_DAEMON_NEW: *u8 = "nx_docportal_admin_daemon.sov.elf.new" as *u8
40const SS_DAEMON_SRC: *u8 = "/nx_docportal_admin_daemon.nx" as *u8
41const SS_BENCH: *u8 = "nx_qrels_bench" as *u8
42const SS_BENCH_SRC: *u8 = "/nx_qrels_bench.nx" as *u8
43const SS_DEPLOY_BODY: *u8 = "target=docportal&confirm=yes&expect_sha256=" as *u8
44const SS_EDGE: *u8 = "127.0.0.1:8443" as *u8
45const SS_URL_HEAD: *u8 = "https://nishifamily.com/api/search?q=" as *u8
46const SS_URL_TAIL: *u8 = "&scope=web" as *u8
47const SS_JOURNEYS: *u8 = "knowledge/uat_journeys.conf" as *u8
48const SS_JOURNEY_ID: *u8 = "search-query" as *u8
49const SS_PROBES: *u8 = "knowledge/search_probes.conf" as *u8
50const SS_PROBE_DEFAULT: *u8 = "family photos" as *u8
51const SS_PLAN: *u8 = "buildroot/knowledge/compare/search.plan" as *u8
52const SS_JRNL: *u8 = "knowledge/status/search_ship.jrnl" as *u8
53const SS_BANK_DIR: *u8 = "knowledge/status/bank/" as *u8
54const SS_DEFAULT_BAR_MS: i64 = 3000
55const SS_WARM_TRIES: i64 = 30
56const SS_WARM_STEP_MS: i64 = 2000
57const SS_SETTLE_MS: i64 = 1500
58const SS_MAX_PROBES: i64 = 16
59const SS_PROBE_W: i64 = 96
60const SS_LINE: i64 = 4096
61const SS_MODE_0644: i64 = 420
62const SS_QPFX: *u8 = " q=" as *u8
63const SS_RUNG_DEFAULT: *u8 = "L3" as *u8 // the rung a ship journals under when the caller names none; rung=<id> overrides it
64static sx_rung_g: *u8
65
66func sx_puts(s: *u8) -> i64 { sys_write(1, s, sx_slen(s)); return 0 }
67func sx_pn(v: i64) -> i64 { let b: *u8 = sys_mmap(32); sx_catn(b, 0, v); sx_puts(b); sys_munmap(b, 32); return 0 }
68// print the last `lines` lines of a capture (a receipt's verdict is its tail)
69func sx_tail(buf: *u8, n: i64, lines: i64) -> i64 {
70 var i: i64 = n
71 var seen: i64 = 0
72 var go: i64 = 1
73 while go == 1 {
74 if i <= 0 { go = 0 } else {
75 if buf[i - 1] == (SX_LF as u8) { seen = seen + 1; if seen > lines { go = 0 } else { i = i - 1 } } else { i = i - 1 }
76 }
77 }
78 sys_write(1, (buf as i64 + i) as *u8, n - i)
79 if n > 0 { if buf[n - 1] != (SX_LF as u8) { sx_puts("\n" as *u8) } }
80 return 0
81}
82// fork with up to three args (a2/a3 may be 0), capturing stdout; returns the child's rc
83func sx_fork(path: *u8, a1: *u8, a2: *u8, a3: *u8, out: *u8, cap: i64, olen: *i64) -> i64 {
84 let av: *i64 = sys_mmap(8 * 6) as *i64
85 av[0] = path as i64
86 av[1] = a1 as i64
87 var k: i64 = 2
88 if (a2 as i64) != 0 { av[k] = a2 as i64; k = k + 1 }
89 if (a3 as i64) != 0 { av[k] = a3 as i64; k = k + 1 }
90 av[k] = 0
91 olen[0] = 0
92 let rc: i64 = tr_run_capture_to(path, av, out, cap - 1, olen, SS_TMO_MS)
93 sys_munmap(av as *u8, 8 * 6)
94 return rc
95}
96func sx_mgmt(method: *u8, path: *u8, body: *u8, out: *u8, olen: *i64) -> i64 { return sx_fork(SS_MGMT, method, path, body, out, SS_CAP, olen) }
97// the sha256 of a file by the ONE hash organ, read BY KEY from its JSON (never the first hex run)
98func sx_sha_of(path: *u8, dst: *u8) -> i64 {
99 let out: *u8 = sys_mmap(SS_SMALL)
100 let olen: *i64 = sys_mmap(16) as *i64
101 sx_fork(SS_FILEHASH, path, 0 as *u8, 0 as *u8, out, SS_SMALL, olen)
102 let r: i64 = sx_json_str(out, olen[0], "\"sha256\":" as *u8, dst, 80)
103 sys_munmap(out, SS_SMALL)
104 sys_munmap(olen as *u8, 16)
105 return r
106}
107func sx_append(path: *u8, line: *u8, n: i64) -> i64 {
108 let fd: i64 = sys_openat_append(path, SS_MODE_0644)
109 if fd < 0 { return 0 - 1 }
110 sys_write(fd, line, n)
111 sys_close(fd)
112 return 0
113}
114// one journal frame: epoch TAB nx_search_ship TAB stage TAB verdict TAB detail (a line never splits: detail is cut)
115func sx_jrnl(stage: *u8, verdict: *u8, detail: *u8) -> i64 {
116 let ln: *u8 = sys_mmap(SS_LINE)
117 var o: i64 = sx_catn(ln, 0, sys_now_realtime_sec())
118 ln[o] = 9 as u8; o = o + 1
119 o = sx_cat(ln, o, "nx_search_ship" as *u8); ln[o] = 9 as u8; o = o + 1
120 o = sx_cat(ln, o, stage); ln[o] = 9 as u8; o = o + 1
121 o = sx_cat(ln, o, verdict); ln[o] = 9 as u8; o = o + 1
122 var i: i64 = 0
123 while detail[i] != (0 as u8) { if o < SS_LINE - 2 { var c: i64 = detail[i] as i64; if c == SX_LF { c = SX_SPACE } ln[o] = c as u8; o = o + 1 } i = i + 1 }
124 ln[o] = SX_LF as u8; o = o + 1
125 sx_append(SS_JRNL, ln, o)
126 sys_munmap(ln, SS_LINE)
127 return 0
128}
129// the board is the ESTATE's ledger: one log row per ship on search.plan, pipes and newlines folded so the row parses
130func sx_plan_log(kind: *u8, text: *u8) -> i64 {
131 let ln: *u8 = sys_mmap(SS_LINE)
132 var o: i64 = sx_cat(ln, 0, "log|" as *u8)
133 o = sx_catn(ln, o, sys_now_realtime_sec())
134 o = sx_cat(ln, o, "|" as *u8)
135 o = sx_cat(ln, o, sx_rung_g)
136 o = sx_cat(ln, o, "|" as *u8)
137 o = sx_cat(ln, o, kind)
138 o = sx_cat(ln, o, "|" as *u8)
139 var i: i64 = 0
140 while text[i] != (0 as u8) { if o < SS_LINE - 2 { var c: i64 = text[i] as i64; if c == SX_PIPE { c = 47 } if c == SX_LF { c = SX_SPACE } ln[o] = c as u8; o = o + 1 } i = i + 1 }
141 ln[o] = SX_LF as u8; o = o + 1
142 sx_append(SS_PLAN, ln, o)
143 sys_munmap(ln, SS_LINE)
144 return 0
145}
146// bank the live daemon by hash under a tag; 1 = the copier re-read and verified byte-for-byte
147// pathout (0 = not wanted) receives the bank path, so the agent mode can restore exactly what it banked
148func sx_bank(tag: *u8, sha: *u8, pathout: *u8) -> i64 {
149 let dst: *u8 = sys_mmap(512)
150 var o: i64 = sx_cat(dst, 0, SS_BANK_DIR)
151 o = sx_cat(dst, o, SS_DAEMON_ELF)
152 o = sx_cat(dst, o, "." as *u8)
153 o = sx_cat(dst, o, tag)
154 o = sx_cat(dst, o, "-" as *u8)
155 var k: i64 = 0
156 while k < 8 { if sha[k] != (0 as u8) { dst[o] = sha[k]; o = o + 1 } k = k + 1 }
157 dst[o] = 0 as u8
158 o = sx_cat(dst, o, "-" as *u8)
159 o = sx_catn(dst, o, sys_now_realtime_sec())
160 dst[o] = 0 as u8
161 if (pathout as i64) != 0 { sx_cat(pathout, 0, dst) }
162 let out: *u8 = sys_mmap(SS_SMALL)
163 let olen: *i64 = sys_mmap(16) as *i64
164 sx_fork(SS_FILECOPY, SS_DAEMON_ELF, dst, 0 as *u8, out, SS_SMALL, olen)
165 sx_puts(" BANK " as *u8); sx_puts(dst); sx_puts(" " as *u8); sx_tail(out, olen[0], 1)
166 let ok: i64 = sx_find(out, olen[0], "verified=1" as *u8)
167 sys_munmap(out, SS_SMALL)
168 sys_munmap(olen as *u8, 16)
169 sys_munmap(dst, 512)
170 if ok >= 0 { return 1 }
171 return 0
172}
173// build one target through the mgmt API; fills sha; 0 ok | 12 refused | 11 src_path names another target
174func sx_build(target: *u8, srcsuffix: *u8, stage: *u8, sha: *u8, out: *u8, olen: *i64) -> i64 {
175 let body: *u8 = sys_mmap(512)
176 var bo: i64 = sx_cat(body, 0, "target=" as *u8)
177 bo = sx_cat(body, bo, target)
178 bo = sx_cat(body, bo, "&bust=r" as *u8)
179 bo = sx_catn(body, bo, sys_now_realtime_sec())
180 sx_mgmt("POST" as *u8, "/api/build" as *u8, body, out, olen)
181 sys_munmap(body, 512)
182 sx_puts(" [" as *u8); sx_puts(stage); sx_puts("] BUILD " as *u8); sx_puts(target); sx_puts(" " as *u8)
183 if sx_find(out, olen[0], "\"BUILT\"" as *u8) < 0 { sx_puts("REFUSED by /api/build (exit 12) -- the API's own reason:\n" as *u8); sx_tail(out, olen[0], 6); sx_jrnl("BUILD" as *u8, "REFUSED" as *u8, target); return 12 }
184 sx_json_str(out, olen[0], "\"sha256\":" as *u8, sha, 80)
185 let srcp: *u8 = sys_mmap(512)
186 sx_json_str(out, olen[0], "\"src_path\":" as *u8, srcp, 512)
187 if sx_find(srcp, sx_slen(srcp), srcsuffix) < 0 { sx_puts("REFUSED: src_path does not name the target (exit 11): " as *u8); sx_puts(srcp); sx_puts("\n" as *u8); sx_jrnl("BUILD" as *u8, "WRONG-SRC" as *u8, srcp); return 11 }
188 sx_puts("BUILT sha=" as *u8); sx_puts(sha); sx_puts(" src_path=" as *u8); sx_puts(srcp); sx_puts("\n" as *u8)
189 sx_jrnl("BUILD" as *u8, target, sha)
190 sys_munmap(srcp, 512)
191 return 0
192}
193
194// The DEPLOY watchdog's up budget, read from the SAME conf nx_mgmt_data md_probe_conf reads (deploy_probe.conf in the nishihost
195// root: `up_tries N` / `up_tick_ms N`, '#' lines are comments, a zero or unparsable value keeps the default), so the ship waits
196// exactly as long as the watchdog may. Defaults mirror the watchdog's compiled-in 10 x 3000 ms.
197const SS_PROBE_CONF: *u8 = "deploy_probe.conf" as *u8
198const SS_PROBE_UP_TRIES_DFLT: i64 = 10
199const SS_PROBE_UP_TICK_MS_DFLT: i64 = 3000
200func sx_conf_key_int(buf: *u8, n: i64, key: *u8, dflt: i64) -> i64 {
201 var i: i64 = 0
202 var v: i64 = dflt
203 while i < n {
204 // end of this line: the LF index or n (a flag, never a sentinel written into the cursor)
205 var e: i64 = i
206 var hit: i64 = 0
207 while hit == 0 { if e >= n { hit = 1 } else { if buf[e] == (SX_LF as u8) { hit = 1 } else { e = e + 1 } } }
208 if e > i { if (buf[i] as i64) != SX_HASH {
209 if sx_starts((buf as i64 + i) as *u8, key) == 1 {
210 let got: i64 = sx_int_after((buf as i64 + i) as *u8, e - i, key)
211 if got > 0 { v = got }
212 }
213 } }
214 i = e + 1
215 }
216 return v
217}
218func sx_probe_budget_ms() -> i64 {
219 let szp: *i64 = sys_mmap(16) as *i64
220 szp[0] = 0
221 let buf: *u8 = sys_read_file(SS_PROBE_CONF, szp)
222 var tries: i64 = SS_PROBE_UP_TRIES_DFLT
223 var tick: i64 = SS_PROBE_UP_TICK_MS_DFLT
224 if (buf as i64) != 0 { if szp[0] > 0 {
225 tries = sx_conf_key_int(buf, szp[0], "up_tries " as *u8, tries)
226 tick = sx_conf_key_int(buf, szp[0], "up_tick_ms " as *u8, tick)
227 } }
228 return tries * tick
229}
230func main(argc: i64, argv: *i64) -> i64 {
231 ep_anchor()
232 if argc < 2 { sx_puts("usage: nx_search_ship dry|deploy|agent [allow_loss=<n>] [bar=<ms>] [probes=<path>] [rung=<id>]\n" as *u8); return 2 }
233 let mode: *u8 = argv[1] as *u8
234 // do_deploy: 0 = dry (private candidate + receipt), 1 = deploy (the root adopts), 2 = agent (W2: adopts on its
235 // own ONLY a candidate the ruler and referee prove is a pure speed change; holds a moved ranking for the root)
236 var do_deploy: i64 = 0
237 if sx_streq(mode, "deploy" as *u8) == 1 { do_deploy = 1 } else { if sx_streq(mode, "agent" as *u8) == 1 { do_deploy = 2 } else { if sx_streq(mode, "dry" as *u8) == 0 { sx_puts("usage: nx_search_ship dry|deploy|agent [allow_loss=<n>] [bar=<ms>] [probes=<path>] [rung=<id>]\n" as *u8); return 2 } } }
238 var allow_loss: i64 = 0
239 var bar: i64 = 0
240 var probes_path: *u8 = SS_PROBES
241 sx_rung_g = SS_RUNG_DEFAULT
242 var ai: i64 = 2
243 while ai < argc {
244 let a: *u8 = argv[ai] as *u8
245 if sx_starts(a, "allow_loss=" as *u8) == 1 { allow_loss = sx_int_after(a, sx_slen(a), "allow_loss=" as *u8) }
246 else { if sx_starts(a, "bar=" as *u8) == 1 { bar = sx_int_after(a, sx_slen(a), "bar=" as *u8) }
247 else { if sx_starts(a, "probes=" as *u8) == 1 { probes_path = (a as i64 + 7) as *u8 }
248 else { if sx_starts(a, "rung=" as *u8) == 1 { sx_rung_g = (a as i64 + sx_slen("rung=" as *u8)) as *u8 } } } }
249 ai = ai + 1
250 }
251 if bar <= 0 {
252 let jb: *i64 = sys_mmap(16) as *i64
253 let jcf: *u8 = sys_read_file(SS_JOURNEYS, jb)
254 bar = SS_DEFAULT_BAR_MS
255 if (jcf as i64) != 0 { bar = sx_journey_bar(jcf, jb[0], SS_JOURNEY_ID, SS_DEFAULT_BAR_MS) }
256 }
257 sx_puts("=== NX-SEARCH-SHIP mode=" as *u8); sx_puts(mode); sx_puts(" bar_ms=" as *u8); sx_pn(bar); sx_puts(" (one loop: build daemon > build referee > ruler > referee A/B > [bank > deploy > warm > cold probes > journal]; refuses at the first RED)\n" as *u8)
258 let out: *u8 = sys_mmap(SS_CAP)
259 let olen: *i64 = sys_mmap(16) as *i64
260
261 // [1/8] + [2/8] BUILD the daemon and the referee on the same tree
262 let sha: *u8 = sys_mmap(80)
263 let rc1: i64 = sx_build(SS_DAEMON, SS_DAEMON_SRC, "1/8" as *u8, sha, out, olen)
264 if rc1 != 0 { return rc1 }
265 let shab: *u8 = sys_mmap(80)
266 let rc2: i64 = sx_build(SS_BENCH, SS_BENCH_SRC, "2/8" as *u8, shab, out, olen)
267 if rc2 != 0 { return rc2 }
268
269 // [3/8] RULER: the staged daemon must keep every printable run the live one has (or every loss is NAMED and allowed)
270 sx_fork(SS_CONTENTDIFF, SS_DAEMON_ELF, SS_DAEMON_NEW, 0 as *u8, out, SS_CAP, olen)
271 let lost: i64 = sx_int_after(out, olen[0], "lost_from_live=" as *u8)
272 var named: i64 = 0
273 var li: i64 = 0
274 while li < olen[0] { let p: i64 = sx_find((out as i64 + li) as *u8, olen[0] - li, "LOST:" as *u8); if p < 0 { li = olen[0] } else { named = named + 1; li = li + p + 5 } }
275 let green: i64 = sx_find(out, olen[0], "verdict=GREEN" as *u8)
276 sx_puts(" [3/8] RULER lost_from_live=" as *u8); sx_pn(lost); sx_puts(" named=" as *u8); sx_pn(named); sx_puts(" " as *u8); sx_tail(out, olen[0], 1)
277 if green < 0 {
278 var bar_lost: i64 = named
279 if lost > bar_lost { bar_lost = lost }
280 if allow_loss > 0 { if named > 0 { if bar_lost <= allow_loss {
281 sx_puts(" ruler RED on " as *u8); sx_pn(bar_lost); sx_puts(" named lost run(s), caller allowed " as *u8); sx_pn(allow_loss); sx_puts(" -- proceeding, every named run journalled:\n" as *u8); sx_tail(out, olen[0], 12)
282 sx_jrnl("RULER" as *u8, "ALLOWED" as *u8, sha)
283 } else { sx_puts(" REFUSED (exit 31): lost runs exceed allow_loss\n" as *u8); sx_tail(out, olen[0], 12); sx_jrnl("RULER" as *u8, "RED" as *u8, sha); return 31 } }
284 else { sx_puts(" REFUSED (exit 31): the ruler is RED and named no lost run -- an allowance cannot cover a ruler that did not speak\n" as *u8); sx_tail(out, olen[0], 12); sx_jrnl("RULER" as *u8, "RED" as *u8, sha); return 31 } }
285 else { sx_puts(" REFUSED (exit 31): the staged daemon loses printable content the live one has. Ruler tail:\n" as *u8); sx_tail(out, olen[0], 12); sx_jrnl("RULER" as *u8, "RED" as *u8, sha); return 31 }
286 } else { sx_jrnl("RULER" as *u8, "GREEN" as *u8, sha) }
287
288 // [4/8] REFEREE A/B: the promoted referee and the staged one on the same index, per-query lines compared byte-for-byte
289 let outa: *u8 = sys_mmap(SS_CAP)
290 let alen: *i64 = sys_mmap(16) as *i64
291 let outb: *u8 = sys_mmap(SS_CAP)
292 let blen: *i64 = sys_mmap(16) as *i64
293 sx_fork(SS_BENCH_LIVE, "ab-live" as *u8, 0 as *u8, 0 as *u8, outa, SS_CAP, alen)
294 sx_fork(SS_BENCH_STAGED, "ab-staged" as *u8, 0 as *u8, 0 as *u8, outb, SS_CAP, blen)
295 let qa: *u8 = sys_mmap(SS_SMALL)
296 let qb: *u8 = sys_mmap(SS_SMALL)
297 let qal: *i64 = sys_mmap(16) as *i64
298 let qbl: *i64 = sys_mmap(16) as *i64
299 let na: i64 = sx_lines_with(outa, alen[0], SS_QPFX, qa, SS_SMALL, qal)
300 let nb: i64 = sx_lines_with(outb, blen[0], SS_QPFX, qb, SS_SMALL, qbl)
301 let ca: i64 = sx_int_after(outa, alen[0], "corpus_docs=" as *u8)
302 let cb: i64 = sx_int_after(outb, blen[0], "corpus_docs=" as *u8)
303 let ndcg_a: i64 = sx_int_after(outa, alen[0], "diag_mean_ndcg_permil_UNPUBLISHABLE=" as *u8)
304 let ndcg_b: i64 = sx_int_after(outb, blen[0], "diag_mean_ndcg_permil_UNPUBLISHABLE=" as *u8)
305 let mrr_a: i64 = sx_int_after(outa, alen[0], "diag_mean_mrr_permil_UNPUBLISHABLE=" as *u8)
306 let mrr_b: i64 = sx_int_after(outb, blen[0], "diag_mean_mrr_permil_UNPUBLISHABLE=" as *u8)
307 let same: i64 = sx_bytes_eq(qa, qal[0], qb, qbl[0])
308 var referee: *u8 = "IDENTICAL" as *u8
309 sx_puts(" [4/8] REFEREE live=" as *u8); sx_pn(na); sx_puts(" staged=" as *u8); sx_pn(nb); sx_puts(" queries, corpus_docs " as *u8); sx_pn(ca); sx_puts("/" as *u8); sx_pn(cb); sx_puts(" ndcg " as *u8); sx_pn(ndcg_a); sx_puts("->" as *u8); sx_pn(ndcg_b); sx_puts(" mrr " as *u8); sx_pn(mrr_a); sx_puts("->" as *u8); sx_pn(mrr_b)
310 if na == 0 { sx_puts(" REFUSED (exit 41): the live referee scored NO query -- read its tail:\n" as *u8); sx_tail(outa, alen[0], 6); sx_jrnl("REFEREE" as *u8, "NO-LIVE-ARM" as *u8, sha); return 41 }
311 if nb == 0 { sx_puts(" REFUSED (exit 41): the staged referee scored NO query -- read its tail:\n" as *u8); sx_tail(outb, blen[0], 6); sx_jrnl("REFEREE" as *u8, "NO-STAGED-ARM" as *u8, sha); return 41 }
312 if ca != cb { sx_puts(" WARNING index moved between the arms (a crawler ship): compare with that in mind" as *u8) }
313 if same == 1 { sx_puts(" IDENTICAL on every per-query line\n" as *u8); sx_jrnl("REFEREE" as *u8, "IDENTICAL" as *u8, sha) }
314 else {
315 let dl: i64 = sx_first_diff_line(qa, qal[0], qb, qbl[0])
316 sx_puts(" MOVED first differing line=" as *u8); sx_pn(dl); sx_puts("\n live:\n" as *u8); sys_write(1, qa, qal[0]); sx_puts(" staged:\n" as *u8); sys_write(1, qb, qbl[0])
317 if ndcg_b < ndcg_a { sx_puts(" REFUSED (exit 41): mean ndcg fell -- a ranking regression, not a speed change\n" as *u8); sx_jrnl("REFEREE" as *u8, "WORSE-NDCG" as *u8, sha); return 41 }
318 if mrr_b < mrr_a { sx_puts(" REFUSED (exit 41): mean mrr fell -- a ranking regression, not a speed change\n" as *u8); sx_jrnl("REFEREE" as *u8, "WORSE-MRR" as *u8, sha); return 41 }
319 sx_puts(" PROCEEDING ANNOUNCED: the ranking moved and neither mean fell -- this is a ranking change the caller adjudicates against its pre-declared accept rule; the differing lines are the receipt\n" as *u8)
320 sx_jrnl("REFEREE" as *u8, "MOVED-NOT-WORSE" as *u8, sha)
321 referee = "MOVED-NOT-WORSE" as *u8
322 }
323
324 // AGENT MODE (W2, 2026-09-14): the worker adopts on its own ONLY the class whose safety is mechanical -- the
325 // candidate differs from live, the ruler lost nothing and the referee is byte-identical on every judged query
326 // (a pure speed change). A MOVED ranking is HELD as a private candidate for the root, never adopted by an
327 // agent: that is the RACI, not caution theatre. The decision is the lib's pure function under the gate's T8.
328 if do_deploy == 2 {
329 let live0: *u8 = sys_mmap(80)
330 sx_sha_of(SS_DAEMON_ELF, live0)
331 let dec: i64 = sx_agent_decide(sx_streq(live0, sha), same, lost)
332 if dec == SX_AGENT_NOCHANGE { sx_jrnl("AGENT" as *u8, "NOCHANGE" as *u8, sha); sx_puts("SEARCH-SHIP verdict=GREEN mode=agent NO-CHANGE: the candidate is byte-identical to live " as *u8); sx_puts(sha); sx_puts(" -- nothing to adopt\n" as *u8); return 0 }
333 if dec == SX_AGENT_HOLD { sx_jrnl("AGENT" as *u8, "HOLD" as *u8, sha); sx_puts("SEARCH-SHIP verdict=GREEN mode=agent HELD: a PRIVATE CANDIDATE " as *u8); sx_puts(sha); sx_puts(" whose ranking moved (referee=" as *u8); sx_puts(referee); sx_puts(") waits for the root: nx_search_ship deploy\n" as *u8); return 0 }
334 sx_jrnl("AGENT" as *u8, "ADOPT" as *u8, sha)
335 sx_puts(" AGENT ADOPT: the candidate differs from live, the ruler lost 0 and the referee is IDENTICAL -- a pure speed change, adopting through the same stages as deploy\n" as *u8)
336 allow_loss = 0
337 }
338 if do_deploy == 0 {
339 sx_jrnl("CANDIDATE" as *u8, "READY" as *u8, sha)
340 sx_puts("SEARCH-SHIP verdict=GREEN mode=dry daemon_staged=" as *u8); sx_puts(sha); sx_puts(" referee=" as *u8); sx_puts(referee); sx_puts(" -- a PRIVATE CANDIDATE with its receipt; nothing deployed. Adopt with: nx_search_ship deploy\n" as *u8)
341 return 0
342 }
343
344 // [5/8] BANK the live daemon by hash BEFORE anything replaces it (the rollback slot holds the previous deploy, not the last known good)
345 let livesha: *u8 = sys_mmap(80)
346 sx_sha_of(SS_DAEMON_ELF, livesha)
347 let prebank: *u8 = sys_mmap(512)
348 sx_puts(" [5/8] BANK live=" as *u8); sx_puts(livesha); sx_puts("\n" as *u8)
349 if sx_bank("pre" as *u8, livesha, prebank) == 0 { sx_puts(" REFUSED (exit 53): the pre-change binary could not be banked and verified -- never deploy without a way back\n" as *u8); sx_jrnl("BANK" as *u8, "FAILED" as *u8, livesha); return 53 }
350 sx_jrnl("BANK" as *u8, "PRE" as *u8, livesha)
351
352 // [6/8] DEPLOY through the health-checked lane with expect_sha256, then READ THE ARTIFACT
353 let db: *u8 = sys_mmap(512)
354 var dbo: i64 = sx_cat(db, 0, SS_DEPLOY_BODY)
355 dbo = sx_cat(db, dbo, sha)
356 if allow_loss > 0 { dbo = sx_cat(db, dbo, "&allow_capability_loss=yes" as *u8) }
357 sx_mgmt("POST" as *u8, "/api/deploy" as *u8, db, out, olen)
358 sx_puts(" [6/8] DEPLOY " as *u8); sx_tail(out, olen[0], 1)
359 if sx_find(out, olen[0], "\"PROMOTED\"" as *u8) < 0 { sx_puts(" REFUSED (exit 51): /api/deploy did not answer PROMOTED -- its reason is above; LIVE IS UNTOUCHED\n" as *u8); sx_jrnl("DEPLOY" as *u8, "REFUSED" as *u8, sha); return 51 }
360 let nowsha: *u8 = sys_mmap(80)
361 sx_sha_of(SS_DAEMON_ELF, nowsha)
362 if sx_streq(nowsha, sha) == 0 { sys_sleep_ms(SS_SETTLE_MS); sx_sha_of(SS_DAEMON_ELF, nowsha) }
363 if sx_streq(nowsha, sha) == 0 { sx_puts(" REFUSED (exit 51): live artifact != staged sha after deploy -- the receipt is not the artifact. live=" as *u8); sx_puts(nowsha); sx_puts("\n" as *u8); sx_jrnl("DEPLOY" as *u8, "NOT-LANDED" as *u8, nowsha); return 51 }
364 // [6b] WAIT FOR THE WATCHDOG'S TERMINAL STATE (2026-09-16, measured): /api/deploy_status reads RUNNING for as long as the
365 // watchdog's up budget lets the daemon warm BEFORE it listens (deploy_probe.conf, else the compiled-in 10 x 3000 ms; the
366 // docportal guard itself tolerates 600 s). Two reads 4.5 s after the deploy took a referee-IDENTICAL build for a failure
367 // (exit 52) while the watchdog was still counting -- a level read of a transient, the deploy-time twin of the health-probe
368 // law. Poll until the status leaves RUNNING, bounded by the SAME budget the watchdog reads plus one settle, so the ship and
369 // the watchdog share one bound instead of two typed constants; the wait is announced.
370 let bud_ms: i64 = sx_probe_budget_ms() + SS_SETTLE_MS * 2
371 var waited_ms: i64 = 0
372 var dst_done: i64 = 0
373 sys_sleep_ms(SS_SETTLE_MS)
374 while dst_done == 0 {
375 sx_mgmt("GET" as *u8, "/api/deploy_status" as *u8, 0 as *u8, out, olen)
376 if sx_find(out, olen[0], "RUNNING" as *u8) < 0 { dst_done = 1 } else {
377 if waited_ms >= bud_ms { dst_done = 1 } else { sys_sleep_ms(SS_WARM_STEP_MS); waited_ms = waited_ms + SS_WARM_STEP_MS }
378 }
379 }
380 sx_puts(" watchdog_wait_ms=" as *u8); sx_pn(waited_ms); sx_puts(" budget_ms=" as *u8); sx_pn(bud_ms); sx_puts("\n" as *u8)
381 sx_puts(" deploy_status " as *u8); sx_tail(out, olen[0], 1)
382 if sx_find(out, olen[0], "DEPLOYED-GREEN" as *u8) < 0 { sx_puts(" REFUSED (exit 52): the watchdog did not report DEPLOYED-GREEN -- read its status; the auto-rollback may be acting\n" as *u8); sx_jrnl("DEPLOY" as *u8, "NOT-GREEN" as *u8, sha); return 52 }
383 sx_jrnl("DEPLOY" as *u8, "GREEN" as *u8, sha)
384 sx_bank("ship" as *u8, sha, 0 as *u8)
385
386 // [7/8] WARM-WAIT then COLD PROBES: the daemon opens its port only after the shard is warm, and the restart empties
387 // the result memo, so the first answer of every probe is a true cold read through the edge with phase_us
388 let probes: *u8 = sys_mmap(SS_PROBE_W * SS_MAX_PROBES)
389 var np: i64 = 0
390 let pb: *i64 = sys_mmap(16) as *i64
391 let pcf: *u8 = sys_read_file(probes_path, pb)
392 if (pcf as i64) != 0 { np = sx_probes(pcf, pb[0], probes, SS_PROBE_W, SS_MAX_PROBES) }
393 if np == 0 { sx_cat(probes, 0, SS_PROBE_DEFAULT); np = 1 }
394 let url: *u8 = sys_mmap(1024)
395 let ph: *i64 = sys_mmap(8 * SX_PHASE_SLOTS) as *i64
396 var over: i64 = 0
397 var maxrecv: i64 = 0
398 var maxq: *u8 = probes
399 var ready: i64 = 0
400 let summary: *u8 = sys_mmap(SS_LINE)
401 var so: i64 = sx_cat(summary, 0, "nx_search_ship " as *u8)
402 so = sx_cat(summary, so, mode)
403 so = sx_cat(summary, so, ": daemon " as *u8)
404 so = sx_cat(summary, so, sha)
405 so = sx_cat(summary, so, " live (ruler GREEN, referee " as *u8)
406 so = sx_cat(summary, so, referee)
407 so = sx_cat(summary, so, ", pre-change " as *u8)
408 so = sx_cat(summary, so, livesha)
409 so = sx_cat(summary, so, " banked). COLD PROBES through the edge, recv ms / phase_us total,prep,s1,l0dec,auth: " as *u8)
410 var pi: i64 = 0
411 while pi < np {
412 let q: *u8 = (probes as i64 + pi * SS_PROBE_W) as *u8
413 var uo: i64 = sx_cat(url, 0, SS_URL_HEAD)
414 uo = sx_urlenc(url, uo, q)
415 uo = sx_cat(url, uo, SS_URL_TAIL)
416 var tries: i64 = 0
417 var got: i64 = 0
418 while got == 0 {
419 sx_fork(SS_HTTPS, url, SS_EDGE, 0 as *u8, out, SS_CAP, olen)
420 if sx_find(out, olen[0], "HTTP/1.1 200" as *u8) >= 0 { got = 1; ready = 1 } else {
421 tries = tries + 1
422 if ready == 1 { got = 2 } else { if tries >= SS_WARM_TRIES { got = 2 } else { sys_sleep_ms(SS_WARM_STEP_MS) } }
423 }
424 }
425 if got == 2 { if ready == 0 { sx_puts(" [7/8] PROBE the daemon never answered 200 inside the warm window (exit 61). Last answer:\n" as *u8); sx_tail(out, olen[0], 3); sx_jrnl("PROBE" as *u8, "NEVER-ANSWERED" as *u8, q); return 61 } }
426 sx_phase(out, olen[0], ph)
427 sx_puts(" [7/8] PROBE q=" as *u8); sx_puts(q); sx_puts(" recv_ms=" as *u8); sx_pn(ph[0]); sx_puts(" total_us=" as *u8); sx_pn(ph[6]); sx_puts(" prep=" as *u8); sx_pn(ph[1]); sx_puts(" s1=" as *u8); sx_pn(ph[2]); sx_puts(" l0dec=" as *u8); sx_pn(ph[3]); sx_puts(" auth=" as *u8); sx_pn(ph[4]); sx_puts(" cand=" as *u8); sx_pn(ph[7])
428 if got == 2 { sx_puts(" NO-200" as *u8); over = over + 1 }
429 if ph[0] > bar { sx_puts(" OVER-BAR" as *u8); over = over + 1 }
430 if ph[0] > maxrecv { maxrecv = ph[0]; maxq = q }
431 sx_puts("\n" as *u8)
432 so = sx_cat(summary, so, q); so = sx_cat(summary, so, " " as *u8); so = sx_catn(summary, so, ph[0]); so = sx_cat(summary, so, "/" as *u8); so = sx_catn(summary, so, ph[6]); so = sx_cat(summary, so, "," as *u8); so = sx_catn(summary, so, ph[1]); so = sx_cat(summary, so, "," as *u8); so = sx_catn(summary, so, ph[2]); so = sx_cat(summary, so, "," as *u8); so = sx_catn(summary, so, ph[3]); so = sx_cat(summary, so, "," as *u8); so = sx_catn(summary, so, ph[4]); so = sx_cat(summary, so, "; " as *u8)
433 pi = pi + 1
434 }
435 // [7c/8] HTML SERP WITNESS: the consumer journey's own URL and must_contain, through the same edge. The JSON
436 // probes above exercise only the /api/search branch of the daemon's router; on 2026-09-14 a daemon whose JSON
437 // probes all read GREEN had broken the HTML /search branch through the buffered edge, and no stage saw it.
438 // A failed witness counts as an over-bar probe so the agent rollback and the exit code both fire on it.
439 let hjb: *i64 = sys_mmap(16) as *i64
440 let hjcf: *u8 = sys_read_file(SS_JOURNEYS, hjb)
441 let hu: *u8 = sys_mmap(SS_LINE)
442 let hm: *u8 = sys_mmap(SS_LINE)
443 var hun: i64 = 0
444 var hmn: i64 = 0
445 if (hjcf as i64) != 0 { hun = sx_journey_field(hjcf, hjb[0], SS_JOURNEY_ID, SX_JF_URL, hu, SS_LINE); hmn = sx_journey_field(hjcf, hjb[0], SS_JOURNEY_ID, SX_JF_MUST, hm, SS_LINE) }
446 var hokw: *u8 = "FAIL" as *u8
447 if hun == 0 || hmn == 0 { sx_puts(" [7c/8] HTML SERP WITNESS UNAVAILABLE: journey " as *u8); sx_puts(SS_JOURNEY_ID); sx_puts(" has no url or no must_contain in " as *u8); sx_puts(SS_JOURNEYS); sx_puts(" -- counted as a failed probe\n" as *u8); over = over + 1; sx_jrnl("PROBE" as *u8, "HTML-NO-JOURNEY" as *u8, SS_JOURNEY_ID) }
448 else {
449 sx_fork(SS_HTTPS, hu, SS_EDGE, 0 as *u8, out, SS_CAP, olen)
450 let hok: i64 = sx_html_ok(out, olen[0], hm)
451 sx_puts(" [7c/8] HTML SERP through the edge url=" as *u8); sx_puts(hu); sx_puts(" must_contain=" as *u8); sx_puts(hm); sx_puts(" bytes=" as *u8); sx_pn(olen[0])
452 if hok == 1 { hokw = "OK" as *u8; sx_puts(" OK\n" as *u8) }
453 else { sx_puts(" FAIL -- the 200 status line or the marker is missing; last lines:\n" as *u8); sx_tail(out, olen[0], 3); over = over + 1; sx_jrnl("PROBE" as *u8, "HTML-FAIL" as *u8, hu) }
454 }
455 so = sx_cat(summary, so, "html_serp=" as *u8); so = sx_cat(summary, so, hokw); so = sx_cat(summary, so, " " as *u8)
456 so = sx_cat(summary, so, "bar_ms=" as *u8); so = sx_catn(summary, so, bar); so = sx_cat(summary, so, " over_bar=" as *u8); so = sx_catn(summary, so, over)
457
458 // AGENT ROLLBACK (W2): an adopted candidate that answers over the bar is not left live for a seat to find in the
459 // morning. The pre-change binary banked in [5/8] is staged back and deployed through the same health-checked
460 // lane, verified by hash, and the outcome is journalled either way; the run still exits 71 because the
461 // candidate failed its bar -- a rollback is a remedy, not a pass.
462 if do_deploy == 2 { if over > 0 {
463 sx_puts(" [7b/8] AGENT ROLLBACK: " as *u8); sx_pn(over); sx_puts(" probe(s) over the bar -- restoring the banked pre-change binary " as *u8); sx_puts(livesha); sx_puts(" from " as *u8); sx_puts(prebank); sx_puts("\n" as *u8)
464 sx_fork(SS_FILECOPY, prebank, SS_DAEMON_NEW, "overwrite" as *u8, out, SS_CAP, olen)
465 if sx_find(out, olen[0], "verified=1" as *u8) < 0 { sx_puts(" ROLLBACK FAILED: the bank could not be staged -- read the copier's tail:\n" as *u8); sx_tail(out, olen[0], 2); sx_jrnl("ROLLBACK" as *u8, "STAGE-FAILED" as *u8, livesha) } else {
466 var rbo: i64 = sx_cat(db, 0, SS_DEPLOY_BODY)
467 rbo = sx_cat(db, rbo, livesha)
468 sx_mgmt("POST" as *u8, "/api/deploy" as *u8, db, out, olen)
469 sx_puts(" rollback deploy " as *u8); sx_tail(out, olen[0], 1)
470 sys_sleep_ms(SS_SETTLE_MS)
471 let rbsha: *u8 = sys_mmap(80)
472 sx_sha_of(SS_DAEMON_ELF, rbsha)
473 if sx_streq(rbsha, livesha) == 1 { sx_puts(" ROLLBACK LANDED: live is the pre-change binary again\n" as *u8); sx_jrnl("ROLLBACK" as *u8, "LANDED" as *u8, livesha) }
474 else { sx_puts(" ROLLBACK NOT LANDED: live=" as *u8); sx_puts(rbsha); sx_puts(" -- the watchdog's own rollback may be acting; read /api/deploy_status\n" as *u8); sx_jrnl("ROLLBACK" as *u8, "NOT-LANDED" as *u8, rbsha) }
475 }
476 so = sx_cat(summary, so, " AGENT-ROLLBACK-ATTEMPTED" as *u8)
477 } }
478
479 // [8/8] JOURNAL: the estate's ledger (search.plan) and this organ's own frame, then the positional verdict line
480 var verdict: *u8 = "GREEN" as *u8
481 if over > 0 { verdict = "RED" as *u8 }
482 sx_plan_log("land" as *u8, summary)
483 sx_jrnl("SHIPPED" as *u8, verdict, summary)
484 sx_puts(" [8/8] JOURNAL search.plan log row appended + " as *u8); sx_puts(SS_JRNL); sx_puts("\n" as *u8)
485 sx_puts("SEARCH-SHIP verdict=" as *u8); sx_puts(verdict); sx_puts(" daemon=" as *u8); sx_puts(sha); sx_puts(" referee=" as *u8); sx_puts(referee); sx_puts(" probes=" as *u8); sx_pn(np); sx_puts(" over_bar=" as *u8); sx_pn(over); sx_puts(" html_serp=" as *u8); sx_puts(hokw); sx_puts(" max_recv_ms=" as *u8); sx_pn(maxrecv); sx_puts(" (" as *u8); sx_puts(maxq); sx_puts(") bar_ms=" as *u8); sx_pn(bar); sx_puts(" -- every stage above carries its evidence line\n" as *u8)
486 if over > 0 { return 71 }
487 return 0
488}