code wiki / _hdl_build / nx_docportal_search_sticky_gate.nx
nx_docportal_search_sticky_gate.nx source
↩ module page · 219 lines · 14584 B
1// nx_docportal_search_sticky_gate.nx -- does the web-shard cache KEEP its working handle when a reopen
2// comes back EMPTY, and does it refuse to MEMOISE the signature of that empty open?
3//
4// SUBJECT: nx_docportal_search_seg.nx dss_open_maybe_cached, the 2026-08-22 REFUSE-BEFORE-DESTROY fix:
5// let nh: *i64 = ss_open2(prefix, 1)
6// if nh[0] == 0 { ss_close(nh); sys_munmap(cur as *u8, DSC_SIGBUF); return dsc_handle }
7// Until this gate existed that fix was proven BY CONSTRUCTION only. MEASURED 2026-08-22 (pre-fix): one
8// transient empty manifest read destroyed the good handle, installed an empty one, memoised it against the
9// current signature, and every request served total=0 for ~8 minutes on a 1.8 GB / 17-segment index.
10//
11// HOW THE REAL CACHE PATH IS DRIVEN AGAINST A FIXTURE: the cache engages ONLY when the prefix is EXACTLY
12// "knowledge/store/dp-web-pub-" (dsc_web_prefix_is compares the whole literal; dss_prefix builds it from the
13// domain "web"), and that path is RELATIVE. So the gate sys_chdir's into /tmp/<gate>/ and builds
14// knowledge/store/ underneath it: the relative prefix then resolves into the fixture tree and every line of
15// the production open / refresh / guard code runs UNCHANGED -- no test hook, no prefix-override static,
16// no production path touched. The entry CWD (sys_getcwd) is restored before the verdict so gv_journal's
17// relative knowledge/status/harness.jrnl lands where every other gate's does.
18// FIXTURE-RATCHET LAW: all scratch under /tmp/nx_docportal_search_sticky_gate/, created by sys_mkdir at
19// SETUP (a teardown does not run when a run crashes); setup is IDEMPOTENT (the manifest is truncated before
20// the seed commit, so ss_commit -- max_segid+1 under the plane lock -- re-derives seg-1 on every run instead
21// of appending seg-N; stale segment FILES from an earlier run are overwritten or simply never named).
22// TEETH (each states itself; the verdict note does not recount them):
23// T0 accessor reports UNOPENED (-1) before any open (the -1 arm of dss_web_index_segments is real)
24// T1 fixture-reached-condition: healthy fixture opens with segments == 1 (a fixture that cannot fail is not a test)
25// T1b fixture-reached-condition: the cache memoised the HEALTHY manifest signature (size>0, == stat)
26// T2a fixture-reached-condition: the emptied manifest CHANGED the signature, so refresh reaches the reopen branch
27// T5 anti-vacuity: a direct ss_open2 of the emptied manifest returns ns==0 (T2 tests the condition, not a no-op)
28// T2 refresh-keeps-working-handle: refresh over the empty manifest -> still ready, SAME handle, still 1 segment
29// T3 signature-not-memoised: dsc_sig (read via dss_web_cache_sig) still equals the healthy signature, not the empty one
30// T3b WEAK-regression (passes on the pre-fix code too -- named so nobody reads it as discrimination)
31// T4 neg-control-reopen-on-real-change: a manifest naming REAL new segment bytes DOES replace the handle (1 -> 2)
32// T4b the replacement handle really serves the new bytes (ss_hget doc:2 == 1), so T4 counted segments, not names only
33// THIRD STATE: every setup step (mkdir / chdir / prefix / seed) is a gv_need; if one is missing the teeth are
34// NOT run and the gate ends SKIP, never RED -- a broken fixture must not read as a broken subject.
35// WHAT THIS GATE REQUIRES: the two accessors dss_web_index_segments / dss_web_cache_sig in
36// nx_docportal_search_seg.nx (round2 patch A1). Without them it does not compile -- by design: the gate
37// reads STATE, and the state is only readable through them (no importer in the estate reads another
38// module's static; measured over buildroot/runtime, every cross-file mention of a static is a comment).
39// license_tier: ORIGINAL Writes only under /tmp/nx_docportal_search_sticky_gate/. No hw writes (Rule 26).
40import "nx_docportal_search_seg.nx"
41import "nx_gate_verdict.nx"
42import "nx_syscalls.nx"
43
44// path scratch: the longest path built here is /tmp/<gate>/knowledge/store/dp-web-pub-manifest.txt (~70 B);
45// DSC_PREFIXBUF (512) is the seg file's own prefix scratch size and is reused for every path buffer so no
46// second size is invented.
47const STK_MANIFEST_SMALL: i64 = 16 // "seg-1" + LF is 6 bytes; 16 is one cache line, never a cap reached
48const STK_LF: i64 = 10 // the manifest row terminator ss_commit_body writes (nb[no] = 10)
49const STK_PUT: i64 = 1 // ss_add kind=1 = put (the kind every seeding gate uses)
50const STK_SEG1: i64 = 1 // first seeded segment id
51const STK_BOX: i64 = 16 // one out-box (ptr or len) -- a page-rounded mmap; the value is the box size, not a cap
52const STK_SEG2: i64 = 2 // second seeded segment id (the neg-control's REAL change)
53
54func stk_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
55func stk_path(out: *u8, a: *u8, b: *u8) -> i64 {
56 var o: i64 = ss_cat(out, 0, a)
57 o = ss_cat(out, o, b)
58 out[o] = 0 as u8
59 return o
60}
61// one seeded segment through the SAME writer path the crawler and every store uses (ss_begin/ss_add/ss_commit).
62// Returns ss_commit's rc (>= 0 committed).
63func stk_seed(prefix: *u8, segid: i64, key: *u8, text: *u8) -> i64 {
64 let w: *i64 = ss_begin()
65 ss_add(w, STK_PUT, key, text, stk_len(text))
66 return ss_commit(prefix, w, segid)
67}
68// print " <label> a=<v1> b=<v2>\n" so every tooth carries its VALUES, not only PASS/FAIL
69func stk_show2(label: *u8, an: *u8, a: i64, bn: *u8, b: i64) -> i64 {
70 gv_puts(" " as *u8); gv_puts(label); gv_puts(" " as *u8)
71 gv_puts(an); gv_puts("=" as *u8); gv_num(a); gv_puts(" " as *u8)
72 gv_puts(bn); gv_puts("=" as *u8); gv_num(b); gv_puts("\n" as *u8)
73 return 0
74}
75func stk_exists(path: *u8) -> i64 {
76 let stb: *u8 = sys_mmap(DSC_STATBUF)
77 var ok: i64 = 0
78 if sys_fstatat(path, stb) == 0 { ok = 1 }
79 sys_munmap(stb, DSC_STATBUF)
80 return ok
81}
82
83func main(argc: i64, argv: *i64) -> i64 {
84 let ctr: *i64 = gv_ctr()
85 gv_head("NX-DOCPORTAL-SEARCH-STICKY-GATE -- an EMPTY reopen must keep the working web handle and must not memoise its signature" as *u8)
86
87 // ---- SETUP: /tmp/<gate>/knowledge/store/, then chdir so the PRODUCTION relative prefix resolves there ----
88 let cwd0: *u8 = sys_mmap(SYS_PATH_MAX)
89 let cwdn: i64 = sys_getcwd(cwd0, SYS_PATH_MAX)
90 let root: *u8 = "/tmp/nx_docportal_search_sticky_gate" as *u8
91 let rk: *u8 = "/tmp/nx_docportal_search_sticky_gate/knowledge" as *u8
92 let rks: *u8 = "/tmp/nx_docportal_search_sticky_gate/knowledge/store" as *u8
93 sys_mkdir(root, MODE_0755)
94 sys_mkdir(rk, MODE_0755)
95 sys_mkdir(rks, MODE_0755)
96 let dirok: i64 = stk_exists(rks)
97 var chok: i64 = 0
98 if sys_chdir(root) == 0 { chok = 1 }
99 // the ONE prefix the cache engages on, built by the production builder from the production domain
100 let prefix: *u8 = sys_mmap(DSC_PREFIXBUF)
101 dss_prefix("web" as *u8, prefix)
102 let isweb: i64 = dsc_web_prefix_is(prefix)
103 let mpath: *u8 = sys_mmap(DSC_PREFIXBUF)
104 stk_path(mpath, prefix, "manifest.txt" as *u8)
105 // IDEMPOTENT SEED: truncate the manifest (O_TRUNC, n=0) so ss_max_segid reads "no segments" and ss_commit
106 // re-derives seg-1; then commit ONE real segment through the production writer.
107 ss_writefile(mpath, prefix, 0)
108 let seed1: i64 = stk_seed(prefix, STK_SEG1, "doc:1" as *u8, "stickyseed alpha document one" as *u8)
109 var seedok: i64 = 0
110 if seed1 >= 0 { if stk_exists(mpath) == 1 { seedok = 1 } }
111
112 gv_need("fixture root /tmp/nx_docportal_search_sticky_gate/knowledge/store exists (sys_mkdir at setup)" as *u8, dirok, ctr)
113 gv_need("chdir into the fixture root succeeded (the production RELATIVE prefix must resolve there)" as *u8, chok, ctr)
114 gv_need("prefix under test is the web prefix the cache engages on (dsc_web_prefix_is == 1)" as *u8, isweb, ctr)
115 gv_need("seed commit seg-1 succeeded through ss_commit (rc >= 0, manifest present)" as *u8, seedok, ctr)
116 var setup: i64 = 0
117 if dirok == 1 { if chok == 1 { if isweb == 1 { if seedok == 1 { setup = 1 } } } }
118
119 if setup == 1 {
120 // ---- T0: the accessor's UNOPENED arm is real (fresh process, nothing opened yet) ----
121 let s0: i64 = dss_web_index_segments()
122 stk_show2("T0" as *u8, "segments_before_any_open" as *u8, s0, "expected" as *u8, 0 - 1)
123 var t0: i64 = 0
124 if s0 == (0 - 1) { t0 = 1 }
125 gv_check("T0 accessor reports UNOPENED (-1) before the first open" as *u8, t0, ctr)
126
127 // ---- T1: healthy fixture opens through the PARENT refresh path ----
128 let r1: i64 = dss_web_cache_refresh()
129 let s1: i64 = dss_web_index_segments()
130 // request-path read of the cached handle pointer: with dsc_may_reopen == 0 and a handle present,
131 // dss_open_maybe_cached returns dsc_handle WITHOUT reopening (seg L187 / L192)
132 let p1: *i64 = dss_open_maybe_cached(prefix)
133 stk_show2("T1" as *u8, "refresh_rc" as *u8, r1, "segments" as *u8, s1)
134 var t1: i64 = 0
135 if r1 == 1 { if s1 == 1 { if (p1 as i64) != 0 { if p1[0] == s1 { t1 = 1 } } } }
136 gv_check("T1 fixture-reached-condition: healthy fixture opens with segments == 1 (accessor == handle h[0])" as *u8, t1, ctr)
137 let sig1: *i64 = sys_mmap(DSC_SIGBUF) as *i64
138 let hs1: i64 = dss_web_cache_sig(sig1)
139 let mf1: *i64 = sys_mmap(DSC_SIGBUF) as *i64
140 dsc_manifest_sig(prefix, mf1)
141 stk_show2("T1b" as *u8, "memoised_size" as *u8, sig1[0], "stat_size" as *u8, mf1[0])
142 var t1b: i64 = 0
143 if hs1 == 1 { if sig1[0] > 0 { if sig1[0] == mf1[0] { if sig1[1] == mf1[1] { t1b = 1 } } } }
144 gv_check("T1b fixture-reached-condition: cache memoised the HEALTHY manifest signature (size>0, == stat)" as *u8, t1b, ctr)
145
146 // ---- THE CONDITION: the manifest reads EMPTY (0 bytes: ss_manifest_dyn sz<=0 -> 0 segments) ----
147 ss_writefile(mpath, prefix, 0)
148 let mf2: *i64 = sys_mmap(DSC_SIGBUF) as *i64
149 dsc_manifest_sig(prefix, mf2)
150 stk_show2("T2a" as *u8, "empty_stat_size" as *u8, mf2[0], "healthy_stat_size" as *u8, mf1[0])
151 var t2a: i64 = 0
152 if mf2[0] == 0 { if mf1[0] > 0 { t2a = 1 } }
153 gv_check("T2a fixture-reached-condition: emptied manifest CHANGED the signature (st_size >0 -> 0), so refresh reaches the reopen branch" as *u8, t2a, ctr)
154 // ---- T5 anti-vacuity: the production open REALLY sees ns==0 on this fixture ----
155 let nh5: *i64 = ss_open2(prefix, 1)
156 let ns5: i64 = nh5[0]
157 ss_close(nh5)
158 stk_show2("T5" as *u8, "direct_ss_open2_ns" as *u8, ns5, "expected" as *u8, 0)
159 var t5: i64 = 0
160 if ns5 == 0 { t5 = 1 }
161 gv_check("T5 anti-vacuity: a direct ss_open2 of the emptied manifest returns ns==0 (T2 tests the real condition, not a no-op)" as *u8, t5, ctr)
162
163 // ---- T2: the parent refresh over the EMPTY manifest keeps the working handle ----
164 let r2: i64 = dss_web_cache_refresh()
165 let s2: i64 = dss_web_index_segments()
166 let p2: *i64 = dss_open_maybe_cached(prefix)
167 stk_show2("T2" as *u8, "refresh_rc" as *u8, r2, "segments_after_empty_reopen" as *u8, s2)
168 var t2: i64 = 0
169 if r2 == 1 { if s2 == 1 { if (p2 as i64) == (p1 as i64) { t2 = 1 } } }
170 gv_check("T2 refresh-keeps-working-handle: after the EMPTY reopen, refresh is still ready and the SAME handle still holds 1 segment" as *u8, t2, ctr)
171
172 // ---- T3: the empty open did NOT pin dsc_sig (the next refresh will RETRY instead of waiting) ----
173 let sig3: *i64 = sys_mmap(DSC_SIGBUF) as *i64
174 let hs3: i64 = dss_web_cache_sig(sig3)
175 stk_show2("T3" as *u8, "memoised_size_now" as *u8, sig3[0], "healthy_size" as *u8, sig1[0])
176 var t3: i64 = 0
177 if hs3 == 1 { if sig3[0] == sig1[0] { if sig3[1] == sig1[1] { if sig3[0] != mf2[0] { t3 = 1 } } } }
178 gv_check("T3 signature-not-memoised: dsc_sig still equals the HEALTHY signature, not the empty manifest's" as *u8, t3, ctr)
179
180 // ---- T3b: restore the manifest and refresh -> a 1-segment handle (behavioural; also true pre-fix) ----
181 let mb: *u8 = sys_mmap(STK_MANIFEST_SMALL)
182 var mo: i64 = ss_cat(mb, 0, "seg-1" as *u8)
183 mb[mo] = STK_LF as u8
184 mo = mo + 1
185 ss_writefile(mpath, mb, mo)
186 let r3: i64 = dss_web_cache_refresh()
187 let s3: i64 = dss_web_index_segments()
188 stk_show2("T3b" as *u8, "refresh_rc" as *u8, r3, "segments_after_restore" as *u8, s3)
189 var t3b: i64 = 0
190 if r3 == 1 { if s3 == 1 { t3b = 1 } }
191 gv_check("T3b WEAK-regression (passes on the pre-fix code too): manifest restored, refresh yields a 1-segment handle" as *u8, t3b, ctr)
192
193 // ---- T4 neg-control: a REAL change (new segment, different bytes) DOES replace the handle ----
194 let seed2: i64 = stk_seed(prefix, STK_SEG2, "doc:2" as *u8, "stickyseed beta document two with different bytes" as *u8)
195 let mf4: *i64 = sys_mmap(DSC_SIGBUF) as *i64
196 dsc_manifest_sig(prefix, mf4)
197 let r4: i64 = dss_web_cache_refresh()
198 let s4: i64 = dss_web_index_segments()
199 let sig4: *i64 = sys_mmap(DSC_SIGBUF) as *i64
200 dss_web_cache_sig(sig4)
201 stk_show2("T4" as *u8, "segments_after_real_change" as *u8, s4, "memoised_size" as *u8, sig4[0])
202 var t4: i64 = 0
203 if seed2 >= 0 { if r4 == 1 { if s4 == 2 { if sig4[0] == mf4[0] { if sig4[1] == mf4[1] { t4 = 1 } } } } }
204 gv_check("T4 neg-control-reopen-on-real-change: a manifest naming REAL new segment bytes replaces the handle (segments 1 -> 2) and memoises the NEW signature" as *u8, t4, ctr)
205 let p4: *i64 = dss_open_maybe_cached(prefix)
206 let dptr: *i64 = sys_mmap(STK_BOX) as *i64
207 let dlen: *i64 = sys_mmap(STK_BOX) as *i64
208 var got2: i64 = 0
209 if (p4 as i64) != 0 { if ss_hget(p4, "doc:2" as *u8, dptr, dlen) == 1 { got2 = 1 } }
210 stk_show2("T4b" as *u8, "hget_doc2" as *u8, got2, "handle_changed" as *u8, ((p4 as i64) != (p1 as i64)) as i64)
211 gv_check("T4b the replacement handle really serves the new bytes (ss_hget doc:2 == 1) -- T4 counted segments, not names only" as *u8, got2, ctr)
212 }
213
214 // restore the entry CWD so gv_journal appends to the estate's knowledge/status/harness.jrnl, not the fixture's
215 if cwdn > 0 { sys_chdir(cwd0) }
216 let rc: i64 = gv_verdict("DOCPORTAL-SEARCH-STICKY-GATE" as *u8, ctr, "an empty reopen keeps the working web handle and does not memoise its signature" as *u8)
217 sys_exit(rc)
218 return rc
219}