nx_imgsearch.nx source
↩ module page · 834 lines · 43119 B
1// nx_imgsearch.nx -- THE REVERSE-IMAGE SERVICE ORGAN: the MCP/API face of the multi-tier engine.
2//
3// Every verb emits STRICT JSON on stdout, because the callers are agents and workflows, not humans.
4// That drives three rules the whole file obeys:
5// * HONEST ABSENT. "not found" is `{"present":false}` with a reason, never an empty list that a
6// caller can mistake for "not searched" and never the nearest thing dressed up as a match.
7// * SELF-DESCRIBING. `status` returns the live tier roster, every threshold, and the MEASURED
8// per-class robustness with its gaps named, so an agent can decide whether this engine can answer
9// its question before asking. A tool that cannot state its own limits cannot be trusted with one.
10// * EVERY RESULT CARRIES ITS PROVENANCE -- which tier matched, at what distance, at which
11// orientation, and whether that tier was CONFIDENT (inside its own threshold) or merely nearest.
12//
13// INDEX FORMAT (versioned, fixed-stride, mmap-first -- the scale decision):
14// header 32B : magic "NXIMGIDX" | u32 version | u32 stride | i64 count | i64 cidblob_off
15// record 96B : i64 dhash | u8 visdesc[80] | u32 cid_off | u32 cid_len
16// then : the cid string blob
17// Fixed stride means O(1) random access and no parsing, so the index is opened with sys_map_file --
18// the same read-only file-backed map that moved the text shard off the RAM ceiling onto the page
19// cache. The 80-dimension descriptor is QUANTISED TO u8 AT INGEST (its dimensions are already on a
20// 0..256 scale, so this costs at most one unit on a saturated bin): 96 bytes per image on disk
21// against 648 bytes unquantised, which is the difference between a 10M-image index fitting a NAS
22// volume and not. Same integer-quantisation-at-ingest discipline the text engine rides.
23//
24// HONEST CEILING, reported in the JSON rather than hidden: `query` currently EXPANDS the mmap'd
25// records into the engine's i64 descriptor arrays, so serving RAM is ~648 bytes/image even though
26// storage is 96. The named fix is a u8-native similarity tier that computes L1 straight off the
27// mapped bytes, making serving disk-bound like the text side. Until that lands the ceiling is real
28// and this tool prints it.
29// license_tier: ORIGINAL
30import "nx_imgsearch_engine.nx"
31import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
32import "nx_imgcorpus.nx"
33import "nx_image_gray.nx"
34import "nx_dir.nx"
35const IS_MAGIC_1024: i64 = 1024
36const IS_MAGIC_4096: i64 = 4096
37const IS_MAGIC_99999: i64 = 99999
38
39const IS_MAGIC0: i64 = 78 // 'N'
40const IS_VERSION: i64 = 2 // v2 adds the LOCAL keypoint pack per record (occlusion-robust tier)
41// record: [0..8) dhash | [8..88) visdesc u8[80] | [88..92) cid_off u32 | [92..96) cid_len u32
42// | [96..1256) local keypoint pack = 145 x i64 (FAST+BRIEF, the crop/letterbox tier)
43const IS_STRIDE: i64 = 1256
44const IS_LOCAL_OFF: i64 = 96 // byte offset of the local pack within a record
45const IS_LOCAL_N: i64 = 145 // i64 slots in the local pack (kp_pack_dim)
46const IS_HDR: i64 = 32
47const IS_VDIM: i64 = 80
48const IS_MAXTOPK: i64 = 32
49const IS_RAM_PER_IMG: i64 = 1816 // i64 descriptor footprint per image once expanded for serving
50// WORKING RESOLUTION CAP. Full-resolution photos (multi-megapixel) make the per-pixel tiers -- the
51// LOCAL FAST keypoint scan especially -- exceed the edge's 15s synchronous budget on the NAS CPU.
52// Every descriptor is computed at a bounded resolution instead: dHash/aHash already downscale to 9x8
53// internally, and running the edge-orientation + keypoint tiers at a canonical scale is standard
54// practice (it also bounds work independent of input size = the real scale lever for ingest). Applied
55// identically at index and query time so descriptors stay comparable. Below the cap = untouched, so
56// the synthetic bench (64x64) is unaffected.
57const IS_WORKDIM: i64 = 512
58
59func s_puts(p: *u8) -> i64 { var n: i64 = 0; while p[n] != (0 as u8) { n = n + 1 } sys_write(1, p, n); return 0 }
60func s_putb(p: *u8, n: i64) -> i64 { if n > 0 { sys_write(1, p, n) } return 0 }
61// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
62// per call and never freed it. At page granularity that is 4096B leaked PER CALL -- the
63// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff. A BENCH is the worst home for
64// it: its purpose is millions of iterations. nxi_* is MSB-first and allocates NOTHING.
65func s_num(v: i64) -> i64 { nxi_out(v); return 0 }
66func s_strlen(p: *u8) -> i64 { var n: i64 = 0; while p[n] != (0 as u8) { n = n + 1 } return n }
67func s_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 }
68func s_atoi(p: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while p[i] != (0 as u8) { if p[i] >= (48 as u8) { if p[i] <= (57 as u8) { v = v * 10 + ((p[i] - (48 as u8)) as i64) } } i = i + 1 } return v }
69
70// JSON string body: escape backslash, quote, and control bytes. Callers are parsers, so a raw
71// quote inside a crawled URL must never be able to break the document.
72func s_jstr(p: *u8, n: i64) -> i64 {
73 var i: i64 = 0
74 while i < n {
75 let c: i64 = p[i] as i64
76 if c == 34 { s_puts("\\\"" as *u8) }
77 else { if c == 92 { s_puts("\\\\" as *u8) }
78 else { if c < 32 { s_puts(" " as *u8) }
79 else { sys_write(1, ((p as i64) + i) as *u8, 1) } } }
80 i = i + 1
81 }
82 return 0
83}
84func s_jkey(k: *u8) -> i64 { s_puts("\"" as *u8); s_puts(k); s_puts("\":" as *u8); return 0 }
85
86// ---- little-endian binary helpers ----
87func s_put_i64(b: *u8, off: i64, v: i64) -> i64 {
88 var i: i64 = 0
89 while i < 8 { b[off + i] = ((v >> (i * 8)) & 255) as u8; i = i + 1 }
90 return 0
91}
92func s_get_i64(b: *u8, off: i64) -> i64 {
93 var v: i64 = 0
94 var i: i64 = 7
95 while i >= 0 { v = (v << 8) | (b[off + i] as i64); i = i - 1 }
96 return v
97}
98func s_put_u32(b: *u8, off: i64, v: i64) -> i64 {
99 var i: i64 = 0
100 while i < 4 { b[off + i] = ((v >> (i * 8)) & 255) as u8; i = i + 1 }
101 return 0
102}
103func s_get_u32(b: *u8, off: i64) -> i64 {
104 var v: i64 = 0
105 var i: i64 = 3
106 while i >= 0 { v = (v << 8) | (b[off + i] as i64); i = i - 1 }
107 return v
108}
109
110// the engine configuration this service ships. ONE place, so status/query/index cannot drift apart.
111func s_build_engine(cap: i64) -> *nx_imgengine {
112 let e: *nx_imgengine = nx_imgengine_new(cap)
113 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_COPY, NX_IT_COPY_THRESH, 1000))
114 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_ORIENT, NX_IT_COPY_THRESH, 1000))
115 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_LOCAL, NX_IT_LOCAL_THRESH, 900))
116 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_SIMILAR, NX_IT_SIMILAR_THRESH, 800))
117 return e
118}
119
120func s_klass_name(k: i64) -> *u8 {
121 if k == NX_IT_CLASS_IDENTITY { return "identity" as *u8 }
122 return "similarity" as *u8
123}
124
125func s_emit_tiers(e: *nx_imgengine) -> i64 {
126 s_jkey("tiers" as *u8); s_puts("[" as *u8)
127 var i: i64 = 0
128 while i < nx_imgengine_ntiers(e) {
129 let t: *nx_imgtier = nx_imgengine_tier(e, i)
130 if i > 0 { s_puts("," as *u8) }
131 s_puts("{" as *u8)
132 s_jkey("name" as *u8); s_puts("\"" as *u8); s_puts(nx_imgtier_name(t)); s_puts("\"," as *u8)
133 s_jkey("class" as *u8); s_puts("\"" as *u8); s_puts(s_klass_name(nx_imgtier_klass(t))); s_puts("\"," as *u8)
134 s_jkey("threshold" as *u8); s_num(nx_imgtier_threshold(t)); s_puts("," as *u8)
135 s_jkey("index_dim" as *u8); s_num(nx_imgtier_idx_dim(t)); s_puts("," as *u8)
136 s_jkey("query_dim" as *u8); s_num(nx_imgtier_qry_dim(t)); s_puts("," as *u8)
137 s_jkey("weight" as *u8); s_num(nx_imgtier_weight(t))
138 s_puts("}" as *u8)
139 i = i + 1
140 }
141 s_puts("]" as *u8)
142 return 0
143}
144
145// ---- verb: status ------------------------------------------------------------------------------
146// Self-description INCLUDING the measured weaknesses. An agent must be able to learn that this
147// engine cannot yet answer a heavy-crop question without having to run the query and be misled.
148func s_verb_status() -> i64 {
149 let e: *nx_imgengine = s_build_engine(1)
150 s_puts("{" as *u8)
151 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8)
152 s_jkey("version" as *u8); s_num(IS_VERSION); s_puts("," as *u8)
153 s_jkey("verb" as *u8); s_puts("\"status\"," as *u8)
154 s_jkey("ok" as *u8); s_puts("true," as *u8)
155 s_jkey("engine" as *u8); s_puts("{" as *u8)
156 s_emit_tiers(e); s_puts("," as *u8)
157 s_jkey("fusion" as *u8); s_puts("\"reciprocal-rank-fusion, abstention-gated\"," as *u8)
158 s_jkey("rrf_k" as *u8); s_num(NX_IE_RRF_K); s_puts("," as *u8)
159 s_jkey("ranking_complexity" as *u8); s_puts("\"hash tiers: BK-tree sublinear candidate generation (exact within-threshold, ~6x fewer comparisons on clustered corpora); similarity tier: O(N) heap scan pending ANN graph (seq717)\"" as *u8)
160 s_puts("}," as *u8)
161 s_jkey("index_format" as *u8); s_puts("{" as *u8)
162 s_jkey("magic" as *u8); s_puts("\"NXIMGIDX\"," as *u8)
163 s_jkey("stride_bytes" as *u8); s_num(IS_STRIDE); s_puts("," as *u8)
164 s_jkey("open_mode" as *u8); s_puts("\"mmap (sys_map_file), page-cache resident\"," as *u8)
165 s_jkey("serving_ram_bytes_per_image" as *u8); s_num(IS_RAM_PER_IMG)
166 s_puts("}," as *u8)
167 // MEASURED robustness -- these are nx_imgbench numbers, not aspirations.
168 s_jkey("measured_robustness_permille" as *u8); s_puts("{" as *u8)
169 s_jkey("note" as *u8); s_puts("\"recall@1 vs 256 distractors; UPPER BOUND on real-web\"," as *u8)
170 s_jkey("identity" as *u8); s_puts("1000," as *u8)
171 s_jkey("rescale" as *u8); s_puts("1000," as *u8)
172 s_jkey("brightness_contrast" as *u8); s_puts("1000," as *u8)
173 s_jkey("noise_recompress" as *u8); s_puts("1000," as *u8)
174 s_jkey("watermark" as *u8); s_puts("1000," as *u8)
175 s_jkey("mirror_rotate" as *u8); s_puts("1000," as *u8)
176 s_jkey("crop_10" as *u8); s_puts("1000," as *u8)
177 s_jkey("crop_20" as *u8); s_puts("1000," as *u8)
178 s_jkey("crop_30" as *u8); s_puts("968," as *u8)
179 s_jkey("letterbox" as *u8); s_puts("1000," as *u8)
180 s_jkey("crop_rescale" as *u8); s_puts("906," as *u8)
181 s_jkey("overall" as *u8); s_puts("992" as *u8)
182 s_puts("}," as *u8)
183 s_jkey("classes_survived" as *u8); s_puts("17," as *u8)
184 s_jkey("classes_total" as *u8); s_puts("17," as *u8)
185 s_jkey("known_gaps" as *u8); s_puts("[" as *u8)
186 s_puts("{\"class\":\"crop+rescale\",\"permille\":906,\"needs\":\"CLOSED 2026-08-05 -- scale invariance ships on the QUERY side (index unchanged, no reindex): the query is described across a 5-step half-octave sweep and the tier takes the best, so a same-scale class cannot regress by construction. A SEARCHED scale must clear a higher inlier bar than the image own scale (multiple-comparisons correction) -- without it an out-of-corpus image claimed a false identity match.\"}," as *u8)
187 s_puts("{\"class\":\"semantic-lookalike\",\"permille\":-1,\"status\":\"UNMEASURED\",\"note\":\"-1 means NEVER MEASURED, not zero. No semantic class exists in the 17-transform ruler, so the previous literal 0 was a CLAIM, not a result. Order: (1) build a semantic ruler from labelled same-concept pairs, (2) measure the EXISTING descriptors as the honest baseline, (3) only then a quantised learned image-embedding tier (NX_IT_KIND_SEMANTIC slot reserved). WARNING nx_embedding.nx is the LLM token-ID lookup, NOT an image encoder.\"}," as *u8)
188 s_puts("{\"class\":\"avif-decode\",\"permille\":0,\"needs\":\"AVIF decoder (C5). WebP CLOSED 2026-08-05: VP8L lossless AND lossy VP8 keyframes both decode via the chokepoint (nx_vp8_kf, gate 8/8 oracle-anchored); julia corpus 120/120 indexed\"}" as *u8)
189 s_puts("]" as *u8)
190 s_puts("}\n" as *u8)
191 return 0
192}
193
194// ---- shared ingest: decode one image FILE and write its 4-tier record ----
195// One record-writer, used by BOTH `index` (manifest-driven) and `walkdir` (directory-driven), so the
196// two ingest paths can never drift in what they store. Holds the tier objects + scratch buffers so a
197// caller allocates them once and reuses across an entire corpus. Returns the NEW cid-blob offset on
198// success, or -1 if the image was undecodable (caller counts it as skipped -- never fatal).
199struct SwIngest {
200 tcopy: i64,
201 tsim: i64,
202 tlocal: i64,
203 dh: i64,
204 vd: i64,
205 lp: i64,
206 wh: i64,
207}
208const SWINGEST_BYTES: i64 = 56
209
210// Nearest-neighbour downscale a gray image so its larger side is at most `maxdim`, preserving aspect.
211// Returns the same buffer if already within bounds (no copy); otherwise a fresh bounded buffer.
212// outwh receives the working dimensions. This is the single choke point that bounds ALL tier work.
213func s_downscale_max(gray: *u8, w: i64, h: i64, maxdim: i64, outwh: *i64) -> *u8 {
214 if w <= maxdim { if h <= maxdim { outwh[0] = w; outwh[1] = h; return gray } }
215 var nw: i64 = w
216 var nh: i64 = h
217 if w >= h { nw = maxdim; nh = h * maxdim / w } else { nh = maxdim; nw = w * maxdim / h }
218 if nw < 1 { nw = 1 }
219 if nh < 1 { nh = 1 }
220 let out: *u8 = sys_mmap(nw * nh)
221 var oy: i64 = 0
222 while oy < nh {
223 let sy: i64 = (oy * h) / nh
224 var ox: i64 = 0
225 while ox < nw {
226 let sx: i64 = (ox * w) / nw
227 out[oy * nw + ox] = gray[sy * w + sx]
228 ox = ox + 1
229 }
230 oy = oy + 1
231 }
232 outwh[0] = nw
233 outwh[1] = nh
234 return out
235}
236
237func s_ingest_new() -> *SwIngest {
238 let g: *SwIngest = sys_mmap(SWINGEST_BYTES) as *SwIngest
239 g.tcopy = nx_imgtier_new(NX_IT_KIND_COPY, NX_IT_COPY_THRESH, 1000) as i64
240 g.tsim = nx_imgtier_new(NX_IT_KIND_SIMILAR, NX_IT_SIMILAR_THRESH, 800) as i64
241 g.tlocal = nx_imgtier_new(NX_IT_KIND_LOCAL, NX_IT_LOCAL_THRESH, 900) as i64
242 g.dh = sys_mmap(8) as i64
243 g.vd = sys_mmap(8 * IS_VDIM) as i64
244 g.lp = sys_mmap(8 * IS_LOCAL_N) as i64
245 g.wh = sys_mmap(16) as i64
246 return g
247}
248
249// write image at `path` into record slot nrec; cid string appended to cidb at cpos. Returns new cpos,
250// or -1 if undecodable. gray/rgb decoding + all four descriptors are computed exactly as at query time.
251func s_ingest_file(g: *SwIngest, recs: *u8, nrec: i64, cidb: *u8, cpos: i64, cid: *u8, path: *u8) -> i64 {
252 let wh: *i64 = g.wh as *i64
253 let graw: *u8 = nx_img_to_gray(path, wh)
254 if graw == (0 as *u8) { return 0 - 1 }
255 let gray: *u8 = s_downscale_max(graw, wh[0], wh[1], IS_WORKDIM, wh)
256 let ro: i64 = nrec * IS_STRIDE
257 let tcopy: *nx_imgtier = g.tcopy as *nx_imgtier
258 let tsim: *nx_imgtier = g.tsim as *nx_imgtier
259 let tlocal: *nx_imgtier = g.tlocal as *nx_imgtier
260 let dh: *i64 = g.dh as *i64
261 let vd: *i64 = g.vd as *i64
262 let lp: *i64 = g.lp as *i64
263 nx_imgtier_describe_index(tcopy, gray, 0 as *u8, wh[0], wh[1], dh)
264 s_put_i64(recs, ro, dh[0])
265 nx_imgtier_describe_index(tsim, gray, 0 as *u8, wh[0], wh[1], vd)
266 var d: i64 = 0
267 while d < IS_VDIM {
268 var q: i64 = vd[d]
269 if q < 0 { q = 0 }
270 if q > 255 { q = 255 }
271 recs[ro + 8 + d] = q as u8
272 d = d + 1
273 }
274 nx_imgtier_describe_index(tlocal, gray, 0 as *u8, wh[0], wh[1], lp)
275 var lq: i64 = 0
276 while lq < IS_LOCAL_N { s_put_i64(recs, ro + IS_LOCAL_OFF + lq*8, lp[lq]); lq = lq + 1 }
277 let cl: i64 = s_strlen(cid)
278 s_put_u32(recs, ro + 88, cpos)
279 s_put_u32(recs, ro + 92, cl)
280 var c: i64 = 0
281 while c < cl { cidb[cpos + c] = cid[c]; c = c + 1 }
282 return cpos + cl
283}
284
285// write the NXIMGIDX header + records + cid blob to outpath. Returns total bytes, or -1 on open fail.
286func s_write_index(outpath: *u8, recs: *u8, nrec: i64, cidb: *u8, cpos: i64) -> i64 {
287 let hdr: *u8 = sys_mmap(IS_HDR)
288 hdr[0] = 78 as u8; hdr[1] = 88 as u8; hdr[2] = 73 as u8; hdr[3] = 77 as u8
289 hdr[4] = 71 as u8; hdr[5] = 73 as u8; hdr[6] = 68 as u8; hdr[7] = 88 as u8
290 s_put_u32(hdr, 8, IS_VERSION)
291 s_put_u32(hdr, 12, IS_STRIDE)
292 s_put_i64(hdr, 16, nrec)
293 s_put_i64(hdr, 24, IS_HDR + nrec * IS_STRIDE)
294 let fd: i64 = sys_openat_wr(outpath, 0x1a4)
295 if fd < 0 { return 0 - 1 }
296 sys_write(fd, hdr, IS_HDR)
297 sys_write(fd, recs, nrec * IS_STRIDE)
298 sys_write(fd, cidb, cpos)
299 sys_close(fd)
300 return IS_HDR + nrec * IS_STRIDE + cpos
301}
302
303// ---- verb: index -------------------------------------------------------------------------------
304// manifest rows: cid<TAB>image_path. Undecodable images are SKIPPED and COUNTED, never fatal -- one
305// bad file must not abort an ingest of millions.
306func s_verb_index(manifest: *u8, outpath: *u8) -> i64 {
307 let mbox: *i64 = sys_mmap(16) as *i64
308 let mbuf: *u8 = sys_read_file(manifest, mbox)
309 if mbuf == (0 as *u8) {
310 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"index\",\"ok\":false,\"error\":\"manifest unreadable\"}\n" as *u8)
311 sys_exit(1); return 1
312 }
313 let mn: i64 = mbox[0]
314 var nrows: i64 = 0
315 var i: i64 = 0
316 while i < mn { if mbuf[i] == (10 as u8) { nrows = nrows + 1 } i = i + 1 }
317 nrows = nrows + 1
318
319 let recs: *u8 = sys_mmap(IS_STRIDE * (nrows + 2))
320 let cidb: *u8 = sys_mmap(IS_MAGIC_1024 * (nrows + 2))
321 var nrec: i64 = 0
322 var cpos: i64 = 0
323 var skipped: i64 = 0
324 let ing: *SwIngest = s_ingest_new()
325
326 var ls: i64 = 0
327 i = 0
328 while i <= mn {
329 var eol: i64 = 0
330 if i == mn { eol = 1 } else { if mbuf[i] == (10 as u8) { eol = 1 } }
331 if eol == 1 {
332 if i > ls {
333 var tab: i64 = 0 - 1
334 var k: i64 = ls
335 while k < i { if mbuf[k] == (9 as u8) { if tab < 0 { tab = k } } k = k + 1 }
336 if tab > ls {
337 mbuf[tab] = 0 as u8
338 if i < mn { mbuf[i] = 0 as u8 }
339 let cid: *u8 = ((mbuf as i64) + ls) as *u8
340 let path: *u8 = ((mbuf as i64) + tab + 1) as *u8
341 let ncp: i64 = s_ingest_file(ing, recs, nrec, cidb, cpos, cid, path)
342 if ncp < 0 { skipped = skipped + 1 } else { cpos = ncp; nrec = nrec + 1 }
343 }
344 }
345 ls = i + 1
346 }
347 i = i + 1
348 }
349
350 if s_write_index(outpath, recs, nrec, cidb, cpos) < 0 {
351 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"index\",\"ok\":false,\"error\":\"cannot open output\"}\n" as *u8)
352 sys_exit(1); return 1
353 }
354
355 s_puts("{" as *u8)
356 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8)
357 s_jkey("verb" as *u8); s_puts("\"index\"," as *u8)
358 s_jkey("ok" as *u8); s_puts("true," as *u8)
359 s_jkey("indexed" as *u8); s_num(nrec); s_puts("," as *u8)
360 s_jkey("skipped_undecodable" as *u8); s_num(skipped); s_puts("," as *u8)
361 s_jkey("bytes" as *u8); s_num(IS_HDR + nrec * IS_STRIDE + cpos); s_puts("," as *u8)
362 s_jkey("bytes_per_image" as *u8); s_num(IS_STRIDE); s_puts("," as *u8)
363 s_jkey("path" as *u8); s_puts("\"" as *u8); s_jstr(outpath, s_strlen(outpath)); s_puts("\"" as *u8)
364 s_puts("}\n" as *u8)
365 return 0
366}
367
368// ---- verb: walkdir (ASYNC) + walkdirsync + walkstatus ------------------------------------------
369// Point the engine at a REAL directory of images and build the index -- the bridge from a synthetic
370// bench to a production corpus. Walks one directory (nx_dir bounded getdents), indexes every .png /
371// .jpg / .jpeg, filename = cid. Undecodable images (incl. WebP/AVIF, a filed gap) are SKIPPED and
372// COUNTED, never fatal. BOUNDED by `max`, reported when capped.
373//
374// WHY ASYNC: a full-resolution PNG decode is expensive, and the sovereign edge drops any synchronous
375// backend response that takes over ~15s -- so a foreground walkdir over more than a couple of real
376// photos times out and the daemon reaps it. `walkdir` therefore SELF-DETACHES: it forks a child that
377// setsid()s into its own session (surviving the parent's return + the closed request pipe, the same
378// primitive nx_daemon/nx_hostctl use for NAS daemons), redirects its fds to a log, indexes in the
379// background with no time limit, and writes the result to `<index>.status` when done. The parent
380// returns STARTED immediately, well inside the edge budget. `walkstatus <index>` polls that marker.
381// `walkdirsync` runs it foreground (for small dirs / local tests). HONEST BOUND: the detached child
382// still inherits the executor's address-space RLIMIT, so a single run is bounded by memory, not time;
383// an unbounded 100k+ corpus is the hostctl-sub rung (filed) -- but async lifts the reachable count
384// from ~1 (foreground, time-bound) to as many as fit RLIMIT.
385const SW_WALK_DEFAULT: i64 = 400
386const SW_WALK_HARDCAP: i64 = 50000
387
388func s_path_join(root: *u8, name: *u8, out: *u8) -> i64 {
389 var o: i64 = 0
390 var i: i64 = 0
391 while root[i] != (0 as u8) { out[o] = root[i]; o = o + 1; i = i + 1 }
392 if o > 0 { if out[o-1] != (47 as u8) { out[o] = 47 as u8; o = o + 1 } }
393 i = 0
394 while name[i] != (0 as u8) { out[o] = name[i]; o = o + 1; i = i + 1 }
395 out[o] = 0 as u8
396 return o
397}
398
399func s_is_image_name(name: *u8, nl: i64) -> i64 {
400 if nx_dir_name_ends_with(name, nl, ".png" as *u8, 4) == 1 { return 1 }
401 if nx_dir_name_ends_with(name, nl, ".jpg" as *u8, 4) == 1 { return 1 }
402 if nx_dir_name_ends_with(name, nl, ".jpeg" as *u8, 5) == 1 { return 1 }
403 if nx_dir_name_ends_with(name, nl, ".PNG" as *u8, 4) == 1 { return 1 }
404 if nx_dir_name_ends_with(name, nl, ".JPG" as *u8, 4) == 1 { return 1 }
405 // .gif ADMITTED 2026-08-04: nx_gif_decode is now wired into nx_img_to_gray, so a GIF is decodable.
406 // .webp ADMITTED DELIBERATELY THOUGH LOSSY VP8 STILL FAILS: excluding it here made the file invisible
407 // BEFORE any decode, so walkdir reported skipped_undecodable=0 while silently ignoring 26 real images.
408 // Admitting it converts an invisible omission into an honest skipped_undecodable count. A zero from a
409 // counter that was never incremented is not evidence of success.
410 if nx_dir_name_ends_with(name, nl, ".gif" as *u8, 4) == 1 { return 1 }
411 if nx_dir_name_ends_with(name, nl, ".GIF" as *u8, 4) == 1 { return 1 }
412 if nx_dir_name_ends_with(name, nl, ".webp" as *u8, 5) == 1 { return 1 }
413 // .bmp/.tif ADMITTED 2026-08-05: nx_bmp_decode + nx_tiff_decode wired through the
414 // chokepoint, which nx_img_to_gray now projects -- both are decodable end-to-end.
415 if nx_dir_name_ends_with(name, nl, ".bmp" as *u8, 4) == 1 { return 1 }
416 if nx_dir_name_ends_with(name, nl, ".BMP" as *u8, 4) == 1 { return 1 }
417 if nx_dir_name_ends_with(name, nl, ".tif" as *u8, 4) == 1 { return 1 }
418 if nx_dir_name_ends_with(name, nl, ".tiff" as *u8, 5) == 1 { return 1 }
419 // 1990s roster ADMITTED 2026-08-05 (nx_legacy_codec_gate 12/12): nx_tga_decode,
420 // nx_pcx_decode, nx_ico_decode and nx_pnm_decode are wired at the chokepoint, so
421 // each is decodable end-to-end. .ico is the one with live-web demand -- every
422 // favicon.ico a crawler meets is this format.
423 if nx_dir_name_ends_with(name, nl, ".tga" as *u8, 4) == 1 { return 1 }
424 if nx_dir_name_ends_with(name, nl, ".TGA" as *u8, 4) == 1 { return 1 }
425 if nx_dir_name_ends_with(name, nl, ".pcx" as *u8, 4) == 1 { return 1 }
426 if nx_dir_name_ends_with(name, nl, ".ico" as *u8, 4) == 1 { return 1 }
427 if nx_dir_name_ends_with(name, nl, ".ICO" as *u8, 4) == 1 { return 1 }
428 if nx_dir_name_ends_with(name, nl, ".cur" as *u8, 4) == 1 { return 1 }
429 if nx_dir_name_ends_with(name, nl, ".ppm" as *u8, 4) == 1 { return 1 }
430 if nx_dir_name_ends_with(name, nl, ".pgm" as *u8, 4) == 1 { return 1 }
431 if nx_dir_name_ends_with(name, nl, ".pbm" as *u8, 4) == 1 { return 1 }
432 if nx_dir_name_ends_with(name, nl, ".pnm" as *u8, 4) == 1 { return 1 }
433 return 0
434}
435
436// append ".status" (or any suffix) to a path into out; returns out (NUL-terminated).
437func s_suffix_path(path: *u8, suf: *u8, out: *u8) -> *u8 {
438 var o: i64 = 0
439 while path[o] != (0 as u8) { out[o] = path[o]; o = o + 1 }
440 var j: i64 = 0
441 while suf[j] != (0 as u8) { out[o] = suf[j]; o = o + 1; j = j + 1 }
442 out[o] = 0 as u8
443 return out
444}
445
446// fd-aware JSON emit helpers (the async child writes to a status file, not stdout)
447func s_fw(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
448// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
449// per call and never freed it. At page granularity that is 4096B leaked PER CALL -- the
450// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff. A BENCH is the worst home for
451// it: its purpose is millions of iterations. nxi_* is MSB-first and allocates NOTHING.
452func s_fwn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
453func s_fwstr(fd: i64, p: *u8) -> i64 {
454 var i: i64 = 0
455 while p[i] != (0 as u8) {
456 let c: i64 = p[i] as i64
457 if c == 34 { s_fw(fd, "\\\"" as *u8) } else { if c == 92 { s_fw(fd, "\\\\" as *u8) } else { if c < 32 { s_fw(fd, " " as *u8) } else { sys_write(fd, ((p as i64) + i) as *u8, 1) } } }
458 i = i + 1
459 }
460 return 0
461}
462
463// THE CORE: enumerate + index + write index. Emits the result JSON to `fd`. Returns 0 ok / 1 error.
464func s_walkdir_run(root: *u8, outpath: *u8, maxn: i64, fd: i64) -> i64 {
465 let rowcap: i64 = maxn * 4 + 16
466 let rows: *NxDirRow = sys_mmap(NX_DIR_ROW_BYTES * rowcap) as *NxDirRow
467 let arena: *u8 = sys_mmap(256 * rowcap)
468 let res: *NxDirResult = sys_mmap(NX_DIR_RESULT_BYTES) as *NxDirResult
469 let dv: i64 = nx_dir_list(root, rows, rowcap, arena, 256 * rowcap, 0, res)
470 if dv == NX_DIR_OPEN_FAILED {
471 s_fw(fd, "{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":false,\"error\":\"directory unreadable\",\"root\":\"" as *u8)
472 s_fwstr(fd, root); s_fw(fd, "\"}\n" as *u8)
473 return 1
474 }
475 let nfilled: i64 = res.n_filled
476 var dir_truncated: i64 = 0
477 if dv == NX_DIR_TRUNCATED { dir_truncated = 1 }
478
479 let recs: *u8 = sys_mmap(IS_STRIDE * (maxn + 2))
480 let cidb: *u8 = sys_mmap(IS_MAGIC_1024 * (maxn + 2))
481 let pbuf: *u8 = sys_mmap(IS_MAGIC_4096)
482 let ing: *SwIngest = s_ingest_new()
483 var nrec: i64 = 0
484 var cpos: i64 = 0
485 var skipped: i64 = 0
486 var seen_imgs: i64 = 0
487 var capped: i64 = 0
488
489 var r: i64 = 0
490 while r < nfilled {
491 if nrec >= maxn { capped = 1; r = nfilled } else {
492 let row: *NxDirRow = nx_dir_row_at(rows, r)
493 if nx_dir_row_is_regular_file(row) == 1 {
494 if s_is_image_name(row.name_ptr, row.name_len) == 1 {
495 seen_imgs = seen_imgs + 1
496 s_path_join(root, row.name_ptr, pbuf)
497 let ncp: i64 = s_ingest_file(ing, recs, nrec, cidb, cpos, row.name_ptr, pbuf)
498 if ncp < 0 { skipped = skipped + 1 } else { cpos = ncp; nrec = nrec + 1 }
499 }
500 }
501 r = r + 1
502 }
503 }
504
505 if s_write_index(outpath, recs, nrec, cidb, cpos) < 0 {
506 s_fw(fd, "{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":false,\"error\":\"cannot open output\"}\n" as *u8)
507 return 1
508 }
509
510 s_fw(fd, "{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":true,\"done\":true," as *u8)
511 s_fw(fd, "\"root\":\"" as *u8); s_fwstr(fd, root); s_fw(fd, "\"," as *u8)
512 s_fw(fd, "\"dir_entries\":" as *u8); s_fwn(fd, nfilled)
513 s_fw(fd, ",\"image_files_seen\":" as *u8); s_fwn(fd, seen_imgs)
514 s_fw(fd, ",\"indexed\":" as *u8); s_fwn(fd, nrec)
515 s_fw(fd, ",\"skipped_undecodable\":" as *u8); s_fwn(fd, skipped)
516 s_fw(fd, ",\"capped_at_max\":" as *u8)
517 if capped == 1 { s_fw(fd, "true" as *u8) } else { s_fw(fd, "false" as *u8) }
518 s_fw(fd, ",\"dir_listing_truncated\":" as *u8)
519 if dir_truncated == 1 { s_fw(fd, "true" as *u8) } else { s_fw(fd, "false" as *u8) }
520 s_fw(fd, ",\"max\":" as *u8); s_fwn(fd, maxn)
521 s_fw(fd, ",\"bytes\":" as *u8); s_fwn(fd, IS_HDR + nrec * IS_STRIDE + cpos)
522 s_fw(fd, ",\"path\":\"" as *u8); s_fwstr(fd, outpath); s_fw(fd, "\"}\n" as *u8)
523 return 0
524}
525
526func s_clamp_max(max_in: i64) -> i64 {
527 var maxn: i64 = max_in
528 if maxn < 1 { maxn = SW_WALK_DEFAULT }
529 if maxn > SW_WALK_HARDCAP { maxn = SW_WALK_HARDCAP }
530 return maxn
531}
532
533// foreground (small dirs / local tests): run + emit to stdout.
534func s_verb_walkdirsync(root: *u8, outpath: *u8, max_in: i64) -> i64 {
535 return s_walkdir_run(root, outpath, s_clamp_max(max_in), 1)
536}
537
538// ASYNC: fork a detached child that indexes in the background and writes <index>.status; return now.
539func s_verb_walkdir(root: *u8, outpath: *u8, max_in: i64) -> i64 {
540 let maxn: i64 = s_clamp_max(max_in)
541 let statp: *u8 = s_suffix_path(outpath, ".status" as *u8, sys_mmap(IS_MAGIC_4096))
542 // a fresh run must not read a previous run's DONE marker -- clear it first (best-effort truncate).
543 let cfd: i64 = sys_openat_wr(statp, 0x1a4)
544 if cfd >= 0 { sys_close(cfd) }
545
546 let pid: i64 = sys_fork()
547 if pid == 0 {
548 // CHILD: detach into its own session so the daemon's request-pipe close / pgroup signal can't
549 // reap it, then redirect fds so its output can never corrupt the parent's JSON response.
550 nx_setsid()
551 let din: i64 = sys_openat_rd("/dev/null" as *u8)
552 let dlog: i64 = sys_openat_wr("/tmp/nx_imgsearch_walk.log" as *u8, 0x1a4)
553 if din >= 0 { sys_dup3(din, 0, 0) }
554 if dlog >= 0 { sys_dup3(dlog, 1, 0); sys_dup3(dlog, 2, 0) }
555 let sfd: i64 = sys_openat_wr(statp, 0x1a4)
556 s_walkdir_run(root, outpath, maxn, sfd)
557 if sfd >= 0 { sys_close(sfd) }
558 sys_exit(0)
559 return 0
560 }
561
562 // PARENT: return immediately (well under the edge's synchronous budget).
563 s_puts("{" as *u8)
564 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8)
565 s_jkey("verb" as *u8); s_puts("\"walkdir\"," as *u8)
566 s_jkey("ok" as *u8); s_puts("true," as *u8)
567 s_jkey("action" as *u8); s_puts("\"STARTED\"," as *u8)
568 s_jkey("mode" as *u8); s_puts("\"detached\"," as *u8)
569 s_jkey("pid" as *u8); s_num(pid); s_puts("," as *u8)
570 s_jkey("root" as *u8); s_puts("\"" as *u8); s_jstr(root, s_strlen(root)); s_puts("\"," as *u8)
571 s_jkey("max" as *u8); s_num(maxn); s_puts("," as *u8)
572 s_jkey("index" as *u8); s_puts("\"" as *u8); s_jstr(outpath, s_strlen(outpath)); s_puts("\"," as *u8)
573 s_jkey("status_marker" as *u8); s_puts("\"" as *u8); s_jstr(statp, s_strlen(statp)); s_puts("\"," as *u8)
574 s_jkey("poll_with" as *u8); s_puts("\"walkstatus <index>\"" as *u8)
575 s_puts("}\n" as *u8)
576 return 0
577}
578
579// poll an async walkdir: cat <index>.status if the child finished, else report running/absent.
580func s_verb_walkstatus(idxpath: *u8) -> i64 {
581 let statp: *u8 = s_suffix_path(idxpath, ".status" as *u8, sys_mmap(IS_MAGIC_4096))
582 let sbox: *i64 = sys_mmap(16) as *i64
583 let sbuf: *u8 = sys_read_file(statp, sbox)
584 if sbuf != (0 as *u8) { if sbox[0] > 0 {
585 // the child wrote its result JSON -- emit it verbatim (already a complete object)
586 sys_write(1, sbuf, sbox[0])
587 return 0
588 } }
589 // no completed marker yet: is the index (partial) present at all?
590 let ibox: *i64 = sys_mmap(16) as *i64
591 let ibuf: *u8 = sys_read_file(idxpath, ibox)
592 var have_idx: i64 = 0
593 if ibuf != (0 as *u8) { if ibox[0] >= IS_HDR { have_idx = 1 } }
594 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkstatus\",\"ok\":true,\"done\":false,\"status\":\"running-or-absent\",\"index_present\":" as *u8)
595 if have_idx == 1 { s_puts("true" as *u8) } else { s_puts("false" as *u8) }
596 s_puts(",\"index\":\"" as *u8); s_jstr(idxpath, s_strlen(idxpath)); s_puts("\"}\n" as *u8)
597 return 0
598}
599
600// ---- verb: query -------------------------------------------------------------------------------
601func s_verb_query(idxpath: *u8, imgpath: *u8, topk_in: i64) -> i64 {
602 let ibox: *i64 = sys_mmap(16) as *i64
603 let ib: *u8 = sys_map_file(idxpath, ibox)
604 if ib == (0 as *u8) {
605 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"index unreadable\"}\n" as *u8)
606 sys_exit(1); return 1
607 }
608 if ibox[0] < IS_HDR {
609 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"index truncated\"}\n" as *u8)
610 sys_exit(1); return 1
611 }
612 if ib[0] != (78 as u8) {
613 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"bad index magic\"}\n" as *u8)
614 sys_exit(1); return 1
615 }
616 // stride/version guard: a stale index built at a different record stride would be silently
617 // mis-sliced (every field read at the wrong offset). Reject it with a clear message instead.
618 let idx_stride: i64 = s_get_u32(ib, 12)
619 if idx_stride != IS_STRIDE {
620 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"index stride mismatch (stale index -- re-run index with this build)\",\"index_stride\":" as *u8)
621 s_num(idx_stride); s_puts(",\"expected_stride\":" as *u8); s_num(IS_STRIDE); s_puts("}\n" as *u8)
622 sys_exit(1); return 1
623 }
624 let count: i64 = s_get_i64(ib, 16)
625 let cbo: i64 = s_get_i64(ib, 24)
626 if count <= 0 {
627 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":true,\"index\":{\"count\":0},\"identity_match\":{\"present\":false,\"reason\":\"empty index\"},\"results\":[]}\n" as *u8)
628 return 0
629 }
630
631 let wh: *i64 = sys_mmap(16) as *i64
632 let graw: *u8 = nx_img_to_gray(imgpath, wh)
633 if graw == (0 as *u8) {
634 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"query image undecodable (PNG/JPEG supported; WebP/AVIF are a known gap)\"}\n" as *u8)
635 sys_exit(1); return 1
636 }
637 // same working-resolution cap as ingest, so query and index descriptors are computed identically
638 let gray: *u8 = s_downscale_max(graw, wh[0], wh[1], IS_WORKDIM, wh)
639
640 // expand the mapped records into the engine's tier descriptor arrays. Tier order matches
641 // s_build_engine exactly: 0=copy, 1=orient, 2=local, 3=similar. (Serving RAM ceiling documented
642 // in status; on-disk stays 1256 B/record, filed for a u8-native serve path as seq717.)
643 let e: *nx_imgengine = s_build_engine(count)
644 var i: i64 = 0
645 while i < count {
646 let ro: i64 = IS_HDR + i * IS_STRIDE
647 let dcopy: *i64 = ie_desc_at(e, 0, i)
648 dcopy[0] = s_get_i64(ib, ro)
649 let dor: *i64 = ie_desc_at(e, 1, i)
650 dor[0] = dcopy[0]
651 let dloc: *i64 = ie_desc_at(e, 2, i)
652 var lq: i64 = 0
653 while lq < IS_LOCAL_N { dloc[lq] = s_get_i64(ib, ro + IS_LOCAL_OFF + lq*8); lq = lq + 1 }
654 let dsim: *i64 = ie_desc_at(e, 3, i)
655 var d: i64 = 0
656 while d < IS_VDIM { dsim[d] = ib[ro + 8 + d] as i64; d = d + 1 }
657 let parr: *i64 = e.pays as *i64
658 parr[i] = i
659 i = i + 1
660 }
661 e.count = count
662
663 var topk: i64 = topk_in
664 if topk < 1 { topk = 5 }
665 if topk > IS_MAXTOPK { topk = IS_MAXTOPK }
666 let op: *i64 = sys_mmap(8 * topk) as *i64
667 let os: *i64 = sys_mmap(8 * topk) as *i64
668 let ot: *i64 = sys_mmap(8 * topk) as *i64
669 let od: *i64 = sys_mmap(8 * topk) as *i64
670 let n: i64 = nx_imgengine_query(e, gray, 0 as *u8, wh[0], wh[1], topk, op, os, ot, od)
671
672 let bt: *i64 = sys_mmap(8) as *i64
673 let bd: *i64 = sys_mmap(8) as *i64
674 let br: *i64 = sys_mmap(8) as *i64
675 let bm: i64 = nx_imgengine_best_match_r(e, gray, 0 as *u8, wh[0], wh[1], bt, bd, br)
676
677 s_puts("{" as *u8)
678 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8)
679 s_jkey("version" as *u8); s_num(IS_VERSION); s_puts("," as *u8)
680 s_jkey("verb" as *u8); s_puts("\"query\"," as *u8)
681 s_jkey("ok" as *u8); s_puts("true," as *u8)
682 s_jkey("index" as *u8); s_puts("{" as *u8)
683 s_jkey("count" as *u8); s_num(count); s_puts("," as *u8)
684 s_jkey("bytes" as *u8); s_num(ibox[0]); s_puts("," as *u8)
685 s_jkey("serving_ram_bytes" as *u8); s_num(count * IS_RAM_PER_IMG)
686 s_puts("}," as *u8)
687 s_jkey("query" as *u8); s_puts("{" as *u8)
688 s_jkey("width" as *u8); s_num(wh[0]); s_puts("," as *u8)
689 s_jkey("height" as *u8); s_num(wh[1])
690 s_puts("}," as *u8)
691
692 // the IDENTITY answer, kept separate from the similarity list on purpose
693 s_jkey("identity_match" as *u8); s_puts("{" as *u8)
694 if bm < 0 {
695 s_jkey("present" as *u8); s_puts("false," as *u8)
696 s_jkey("reason" as *u8)
697 if br[0] == 2 {
698 s_puts("\"query image carries too little visual structure for identity matching (flat or smooth-gradient); its fingerprint would collide with every other structureless image\"," as *u8)
699 s_jkey("reason_code" as *u8); s_puts("\"uninformative_query\"," as *u8)
700 } else {
701 s_puts("\"no identity-class tier within its threshold\"," as *u8)
702 s_jkey("reason_code" as *u8); s_puts("\"no_match\"," as *u8)
703 }
704 s_jkey("nearest_distance" as *u8); s_num(bd[0])
705 } else {
706 let ro: i64 = IS_HDR + bm * IS_STRIDE
707 let co: i64 = cbo + s_get_u32(ib, ro + 88)
708 let cl: i64 = s_get_u32(ib, ro + 92)
709 let mt: *nx_imgtier = nx_imgengine_tier(e, bt[0])
710 s_jkey("present" as *u8); s_puts("true," as *u8)
711 s_jkey("cid" as *u8); s_puts("\"" as *u8); s_jstr(((ib as i64) + co) as *u8, cl); s_puts("\"," as *u8)
712 s_jkey("tier" as *u8); s_puts("\"" as *u8); s_puts(nx_imgtier_name(mt)); s_puts("\"," as *u8)
713 s_jkey("distance" as *u8); s_num(bd[0])
714 }
715 s_puts("}," as *u8)
716
717 s_jkey("results" as *u8); s_puts("[" as *u8)
718 i = 0
719 while i < n {
720 if i > 0 { s_puts("," as *u8) }
721 let ro: i64 = IS_HDR + op[i] * IS_STRIDE
722 let co: i64 = cbo + s_get_u32(ib, ro + 88)
723 let cl: i64 = s_get_u32(ib, ro + 92)
724 let rt: *nx_imgtier = nx_imgengine_tier(e, ot[i])
725 s_puts("{" as *u8)
726 s_jkey("rank" as *u8); s_num(i + 1); s_puts("," as *u8)
727 s_jkey("cid" as *u8); s_puts("\"" as *u8); s_jstr(((ib as i64) + co) as *u8, cl); s_puts("\"," as *u8)
728 s_jkey("score" as *u8); s_num(os[i]); s_puts("," as *u8)
729 s_jkey("tier" as *u8); s_puts("\"" as *u8); s_puts(nx_imgtier_name(rt)); s_puts("\"," as *u8)
730 s_jkey("tier_class" as *u8); s_puts("\"" as *u8); s_puts(s_klass_name(nx_imgtier_klass(rt))); s_puts("\"," as *u8)
731 s_jkey("distance" as *u8); s_num(od[i]); s_puts("," as *u8)
732 s_jkey("similarity_permille" as *u8); s_num(nx_imgtier_score(rt, od[i])); s_puts("," as *u8)
733 // Confidence comes from the ENGINE's confident-score, not from re-deriving the threshold
734 // test here. Recomputing it locally is how the two disagreed: identity_match refused a
735 // structureless query while this list still labelled its collisions confident.
736 s_jkey("confident" as *u8)
737 if os[i] > 0 { s_puts("true" as *u8) } else { s_puts("false" as *u8) }
738 s_puts("}" as *u8)
739 i = i + 1
740 }
741 s_puts("]}\n" as *u8)
742 return 0
743}
744
745// ---- verb: selftest ----------------------------------------------------------------------------
746// A round trip an agent can run to confirm the service is functional without any corpus on disk:
747// build a synthetic index in memory, mirror one of its images, and require the engine to find it.
748func s_verb_selftest() -> i64 {
749 let N: i64 = 64
750 let W: i64 = 64
751 let e: *nx_imgengine = s_build_engine(N)
752 var i: i64 = 0
753 while i < N { nx_imgengine_add_image(e, nx_imgcorpus_new(i, W, W), 0 as *u8, W, W, i); i = i + 1 }
754 let src: *u8 = nx_imgcorpus_new(7, W, W)
755 let mir: *u8 = sys_mmap(W * W)
756 var y: i64 = 0
757 while y < W {
758 var x: i64 = 0
759 while x < W { mir[y * W + x] = src[y * W + (W - 1 - x)]; x = x + 1 }
760 y = y + 1
761 }
762 let bt: *i64 = sys_mmap(8) as *i64
763 let bd: *i64 = sys_mmap(8) as *i64
764 let hit: i64 = nx_imgengine_best_match(e, mir, 0 as *u8, W, W, bt, bd)
765 let stranger: i64 = nx_imgengine_best_match(e, nx_imgcorpus_new(IS_MAGIC_99999, W, W), 0 as *u8, W, W, bt, bd)
766 var ok: i64 = 0
767 if hit == 7 { if stranger < 0 { ok = 1 } }
768 s_puts("{" as *u8)
769 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8)
770 s_jkey("verb" as *u8); s_puts("\"selftest\"," as *u8)
771 s_jkey("ok" as *u8)
772 if ok == 1 { s_puts("true," as *u8) } else { s_puts("false," as *u8) }
773 s_jkey("mirrored_image_found" as *u8); s_num(hit); s_puts("," as *u8)
774 s_jkey("expected" as *u8); s_puts("7," as *u8)
775 s_jkey("stranger_correctly_absent" as *u8)
776 if stranger < 0 { s_puts("true," as *u8) } else { s_puts("false," as *u8) }
777 s_jkey("verdict" as *u8)
778 if ok == 1 { s_puts("\"GREEN\"" as *u8) } else { s_puts("\"RED\"" as *u8) }
779 s_puts("}\n" as *u8)
780 if ok == 1 { return 0 }
781 sys_exit(1)
782 return 1
783}
784
785func main(argc: i64, argv: *i64) -> i64 {
786 if argc < 2 { s_verb_status(); return 0 }
787 let verb: *u8 = argv[1] as *u8
788 if s_streq(verb, "status" as *u8) == 1 { s_verb_status(); return 0 }
789 if s_streq(verb, "selftest" as *u8) == 1 { return s_verb_selftest() }
790 if s_streq(verb, "index" as *u8) == 1 {
791 if argc < 4 {
792 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"index\",\"ok\":false,\"error\":\"usage: index <manifest.tsv> <out.nximgidx>\"}\n" as *u8)
793 sys_exit(2); return 2
794 }
795 return s_verb_index(argv[2] as *u8, argv[3] as *u8)
796 }
797 if s_streq(verb, "query" as *u8) == 1 {
798 if argc < 4 {
799 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"usage: query <index.nximgidx> <image> [topk]\"}\n" as *u8)
800 sys_exit(2); return 2
801 }
802 var k: i64 = 5
803 if argc >= 5 { k = s_atoi(argv[4] as *u8) }
804 return s_verb_query(argv[2] as *u8, argv[3] as *u8, k)
805 }
806 if s_streq(verb, "walkdir" as *u8) == 1 {
807 if argc < 4 {
808 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":false,\"error\":\"usage: walkdir <image-dir> <out.nximgidx> [max] (async -- poll with walkstatus)\"}\n" as *u8)
809 sys_exit(2); return 2
810 }
811 var mx: i64 = 0
812 if argc >= 5 { mx = s_atoi(argv[4] as *u8) }
813 return s_verb_walkdir(argv[2] as *u8, argv[3] as *u8, mx)
814 }
815 if s_streq(verb, "walkdirsync" as *u8) == 1 {
816 if argc < 4 {
817 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdirsync\",\"ok\":false,\"error\":\"usage: walkdirsync <image-dir> <out.nximgidx> [max]\"}\n" as *u8)
818 sys_exit(2); return 2
819 }
820 var mx: i64 = 0
821 if argc >= 5 { mx = s_atoi(argv[4] as *u8) }
822 return s_verb_walkdirsync(argv[2] as *u8, argv[3] as *u8, mx)
823 }
824 if s_streq(verb, "walkstatus" as *u8) == 1 {
825 if argc < 3 {
826 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkstatus\",\"ok\":false,\"error\":\"usage: walkstatus <index.nximgidx>\"}\n" as *u8)
827 sys_exit(2); return 2
828 }
829 return s_verb_walkstatus(argv[2] as *u8)
830 }
831 s_puts("{\"tool\":\"nx_imgsearch\",\"ok\":false,\"error\":\"unknown verb\",\"verbs\":[\"status\",\"selftest\",\"index\",\"query\",\"walkdir\",\"walkdirsync\",\"walkstatus\"]}\n" as *u8)
832 sys_exit(2)
833 return 2
834}