code wiki / _hdl_build / nx_galx_join.nx
nx_galx_join.nx source
↩ module page · 181 lines · 7094 B
1// nx_galx_join.nx -- join a gen sidecar into the gallery's READER index, idempotently.
2//
3// MEASURED DEFECT THIS CLOSES (2026-08-07). The gen pipeline appends every image it produces to
4// /volume1/ai/gen/cids.tsv. The gallery server resolves /img/<cid> from a DIFFERENT file --
5// /volume1/ai/galx/knowledge/status/galx_cid_paths.tsv, compiled into a sovereign O(1) idx+blob at
6// startup. Nothing ever joined the two. Measured:
7// gen sidecar rows ................ 109
8// gen blobs present on disk ....... 109 (nothing lost)
9// gen CIDs in the reader index ..... 0 of 109
10// So EVERY image the gen pipeline has ever produced 404s in the gallery, and has since at least the
11// reader TSV's mtime (2026-06-18). The images were fine; the index simply never learned about them.
12// ★★★★★★ TWO INDEXES, ONE WRITTEN AND ONE READ, WITH NOTHING JOINING THEM, IS INDISTINGUISHABLE
13// FROM AN EMPTY GALLERY -- and BOTH FILES LOOK HEALTHY IN ISOLATION, which is why it survived.
14// ★ THE WRITER'S SUCCESS AND THE READER'S INPUT ARE DIFFERENT CLAIMS; ONLY THE READER'S DECIDES.
15//
16// Idempotent by construction (rule 10): a CID already in the reader index is never appended again,
17// so this is safe to run on every gen, on a clock, or twice by accident.
18//
19// ⚠THE APPEND ALONE DOES NOT PUBLISH. The server compiles the TSV into knowledge/index/galx_cid.*
20// ONCE AT STARTUP, so a joined row is invisible until the gallery restarts. This organ says so
21// explicitly rather than reporting a success that the reader cannot yet see.
22//
23// Usage: nx_galx_join <reader_tsv> <sidecar_tsv> [--apply]
24// Default is a DRY RUN -- it reports what it would join and changes nothing.
25// license_tier: ORIGINAL
26
27import "nx_syscalls.nx"
28import "nx_strconv.nx"
29import "nx_galx_cid_index.nx"
30
31const GJ_BUCKETS: i64 = 1048576 // 2^20 >> 2*235k entries; open addressing wants headroom
32
33func gj_puts(s: *u8) -> i64 {
34 var n: i64 = 0
35 while s[n] != (0 as u8) { n = n + 1 }
36 return sys_write(1, s, n)
37}
38func gj_i(v: i64) -> i64 {
39 let b: *u8 = sys_mmap(32)
40 return sys_write(1, b, nx_strconv_format_i64(v, b))
41}
42func gj_eq(s: *u8) -> i64 {
43 var n: i64 = 0
44 while s[n] != (0 as u8) { n = n + 1 }
45 return n
46}
47
48// End of the line starting at `from` (index of the '\n', or n).
49// ⚠THIS IS A HELPER BECAUSE I GOT IT WRONG TWICE. The tempting inline form --
50// while e < n { if buf[e] == NL { e = n + 1 } else { e = e + 1 } }
51// -- uses the sentinel to exit and THROWS AWAY THE INDEX IT JUST FOUND, so every file parses as a
52// single line. It read 235,445 rows as 1. I had already fixed this exact shape in nx_genpipe.nx and
53// reproduced it here from memory an hour later.
54// ★★★★★ A BUG YOU FIXED BY REWRITING THE LINE, RATHER THAN BY EXTRACTING THE FIX, IS A BUG YOU
55// WILL WRITE AGAIN -- the correction has to live somewhere callable or it only ever fixed one site.
56func gj_eol(buf: *u8, n: i64, from: i64) -> i64 {
57 var e: i64 = from
58 var eol: i64 = n
59 var scanning: i64 = 1
60 while scanning == 1 {
61 if e >= n { eol = n; scanning = 0 }
62 else {
63 if buf[e] == (0x0A as u8) { eol = e; scanning = 0 } else { e = e + 1 }
64 }
65 }
66 return eol
67}
68// Length of the CID field: up to the first tab, else the whole line.
69func gj_cidlen(buf: *u8, from: i64, eol: i64) -> i64 {
70 var q: i64 = from
71 while q < eol {
72 if buf[q] == (0x09 as u8) { return q - from }
73 q = q + 1
74 }
75 return eol - from
76}
77
78// open-addressed membership set over FNV-1a(cid). hash 0 means empty, so remap it -- the same
79// convention the gallery's own index builder uses, deliberately, so the two agree.
80func gj_ins(tab: *i64, h0: i64) -> i64 {
81 var h: i64 = h0
82 if h == 0 { h = 1 }
83 var b: i64 = h & (GJ_BUCKETS - 1)
84 var go: i64 = 1
85 while go == 1 {
86 if tab[b] == 0 { tab[b] = h; return 1 }
87 if tab[b] == h { return 0 } // already present
88 b = (b + 1) & (GJ_BUCKETS - 1)
89 }
90 return 0
91}
92func gj_has(tab: *i64, h0: i64) -> i64 {
93 var h: i64 = h0
94 if h == 0 { h = 1 }
95 var b: i64 = h & (GJ_BUCKETS - 1)
96 var go: i64 = 1
97 while go == 1 {
98 if tab[b] == 0 { return 0 }
99 if tab[b] == h { return 1 }
100 b = (b + 1) & (GJ_BUCKETS - 1)
101 }
102 return 0
103}
104
105func main(argc: i64, argv: *i64) -> i64 {
106 if argc < 3 {
107 gj_puts("usage: nx_galx_join <reader_tsv> <sidecar_tsv> [--apply]\n" as *u8)
108 return 2
109 }
110 var apply: i64 = 0
111 if argc > 3 {
112 let f: *u8 = argv[3] as *u8
113 if f[0] == (0x2D as u8) { apply = 1 }
114 }
115
116 let rp: *i64 = sys_mmap(16) as *i64
117 let rbuf: *u8 = sys_read_file(argv[1] as *u8, rp)
118 if (rbuf as i64) == 0 { gj_puts("reader index unreadable\n" as *u8); return 3 }
119 let rn: i64 = rp[0]
120
121 let sp: *i64 = sys_mmap(16) as *i64
122 let sbuf: *u8 = sys_read_file(argv[2] as *u8, sp)
123 if (sbuf as i64) == 0 { gj_puts("sidecar unreadable\n" as *u8); return 4 }
124 let sn: i64 = sp[0]
125
126 // ---- load every reader CID into the set ----
127 let tab: *i64 = sys_mmap(GJ_BUCKETS * 8 + 64) as *i64
128 var have: i64 = 0
129 var p: i64 = 0
130 while p < rn {
131 let eol: i64 = gj_eol(rbuf, rn, p)
132 let cl: i64 = gj_cidlen(rbuf, p, eol)
133 if cl > 0 {
134 gj_ins(tab, cidx_fnv1a(((rbuf as i64) + p) as *u8, cl))
135 have = have + 1
136 }
137 p = eol + 1
138 }
139
140 // ---- stream the sidecar, join what is missing ----
141 var joined: i64 = 0
142 var already: i64 = 0
143 var fd: i64 = 0 - 1
144 if apply == 1 {
145 fd = sys_openat_append(argv[1] as *u8, 0x1a4)
146 if fd < 0 { gj_puts("cannot open reader index for append\n" as *u8); return 5 }
147 }
148 p = 0
149 while p < sn {
150 let eol: i64 = gj_eol(sbuf, sn, p)
151 let cl: i64 = gj_cidlen(sbuf, p, eol)
152 if cl > 0 {
153 let h: i64 = cidx_fnv1a(((sbuf as i64) + p) as *u8, cl)
154 if gj_has(tab, h) == 1 { already = already + 1 }
155 else {
156 gj_ins(tab, h) // guards duplicates WITHIN the sidecar too
157 joined = joined + 1
158 if apply == 1 {
159 sys_write(fd, ((sbuf as i64) + p) as *u8, eol - p)
160 sys_write(fd, "\n" as *u8, 1)
161 }
162 }
163 }
164 p = eol + 1
165 }
166 if fd >= 0 { sys_close(fd) }
167
168 gj_puts("reader_rows=" as *u8); gj_i(have)
169 gj_puts(" already_present=" as *u8); gj_i(already)
170 gj_puts(" joined=" as *u8); gj_i(joined)
171 if apply == 1 { gj_puts(" mode=APPLY\n" as *u8) } else { gj_puts(" mode=DRY-RUN (pass --apply to write)\n" as *u8) }
172
173 if apply == 1 {
174 if joined > 0 {
175 // Say the unfinished part out loud. A row in the TSV is not yet a served image.
176 gj_puts("NOT YET SERVED: the gallery compiles this TSV into knowledge/index/galx_cid.* at STARTUP.\n" as *u8)
177 gj_puts("Restart nx_gallery_serve (cwd /volume1/ai/galx) before these CIDs resolve.\n" as *u8)
178 }
179 }
180 return 0
181}