code wiki / _hdl_build / nx_galx_cidput.nx

nx_galx_cidput.nx source

↩ module page · 173 lines · 8200 B

1// nx_galx_cidput.nx -- NATIVE incremental insert into the sovereign cid->path store. No TSV. 2// 3// Operator 2026-08-07: *"we shouldnt be using tsvs thats probably why it should be nishi native"*. 4// Correct, and the estate had already said so: nx_galx_cid_index's own header reads *"The .idx + 5// .blob ARE the runtime store; the TSV is only a one-time build input (and can then be retired)."* 6// It was never retired -- because the store shipped READ-ONLY. gs_cidindex_build() can only build 7// the whole thing FROM a TSV, and there was no way to add one entry. So every addition fell back 8// to appending 70 bytes of text to a 34MB file and recompiling all 235,445 rows. 9// ★★★★★★ A STORE YOU CAN ONLY BUILD, NEVER APPEND TO, KEEPS ITS OWN LEGACY INPUT FORMAT ALIVE 10// FOREVER -- the "retired" format is load-bearing precisely because the replacement has no writer. 11// ★ THE MISSING PRIMITIVE WAS NOT THE INDEX, IT WAS INSERT. 12// 13// The format already supports O(1) insert; nobody had written it. Open-addressed, fixed buckets: 14// idx: [8 n_buckets][8 n_entries] then n_buckets * 24: [8 cid_hash][8 path_off][8 path_len] (big-endian) 15// blob: concatenated path bytes 16// So an insert is: append the path to blob, linear-probe to an empty bucket, write 24 bytes, bump 17// n_entries. Three small writes at known offsets -- sys_lseek + sys_openat_rdwr, no file rewrite. 18// 19// ⚠LOAD FACTOR IS A HARD LIMIT, NOT A SUGGESTION. n_buckets is fixed at build time; an open- 20// addressed table that passes ~0.5 degrades toward O(n) and at 1.0 never terminates its probe. 21// This REFUSES past the builder's own nent*2 <= nb rule and names the remedy (rebuild, which 22// re-sizes), because ★ A GUARD THAT CANNOT BE SATISFIED PRODUCES A BYPASS. 23// license_tier: ORIGINAL 24 25import "nx_syscalls.nx" 26import "nx_galx_cid_index.nx" 27 28const CP_SEEK_SET: i64 = 0 29const CP_SEEK_END: i64 = 2 30 31// verdicts 32const CP_INSERTED: i64 = 1 33const CP_PRESENT: i64 = 0 34const CP_EIO: i64 = 0 - 1 35const CP_EFULL: i64 = 0 - 2 // load factor exhausted -> rebuild to resize 36const CP_EBADIDX: i64 = 0 - 3 37 38func cp_w64(b: *u8, o: i64, v: i64) -> i64 { 39 b[o] = (v >> 56) as u8 40 b[o+1] = (v >> 48) as u8 41 b[o+2] = (v >> 40) as u8 42 b[o+3] = (v >> 32) as u8 43 b[o+4] = (v >> 24) as u8 44 b[o+5] = (v >> 16) as u8 45 b[o+6] = (v >> 8) as u8 46 b[o+7] = v as u8 47 return 8 48} 49func cp_len(s: *u8) -> i64 { 50 var n: i64 = 0 51 while s[n] != (0 as u8) { n = n + 1 } 52 return n 53} 54 55// Insert by HASH. The table is keyed by hash, never by the cid text, so this is the real primitive 56// -- and it is what lets a rebuild re-adopt entries whose cid string is no longer available (the 57// index stores hashes and paths, not cids). 58func nx_galx_hashput(idx_path: *u8, blob_path: *u8, h_in: i64, path: *u8, plen_in: i64) -> i64 { 59 let fi: i64 = sys_openat_rdwr(idx_path, 0x1a4) 60 if fi < 0 { return CP_EIO } 61 let hdr: *u8 = sys_mmap(64) 62 if sys_read(fi, hdr, 16) != 16 { sys_close(fi); return CP_EBADIDX } 63 let nb: i64 = cidx_rd64(hdr, 0) 64 let nent: i64 = cidx_rd64(hdr, 8) 65 if nb <= 0 { sys_close(fi); return CP_EBADIDX } 66 // the builder sizes nb as the smallest power of two >= nent*2; hold that same invariant here 67 // rather than inventing a looser one, or the two writers would disagree about when it is full. 68 if (nent + 1) * 2 > nb { sys_close(fi); return CP_EFULL } 69 70 var h: i64 = h_in 71 if h == 0 { h = 1 } 72 let ent: *u8 = sys_mmap(64) 73 74 // ---- probe for an empty bucket (or find it already present) ---- 75 var bk: i64 = h & (nb - 1) 76 var bo: i64 = 0 - 1 77 var scanning: i64 = 1 78 var probes: i64 = 0 79 while scanning == 1 { 80 let at: i64 = 16 + bk * 24 81 if sys_lseek(fi, at, CP_SEEK_SET) < 0 { sys_close(fi); return CP_EIO } 82 if sys_read(fi, ent, 24) != 24 { sys_close(fi); return CP_EIO } 83 let cur: i64 = cidx_rd64(ent, 0) 84 if cur == 0 { bo = at; scanning = 0 } 85 else { 86 if cur == h { sys_close(fi); return CP_PRESENT } 87 bk = (bk + 1) & (nb - 1) 88 probes = probes + 1 89 // Belt and braces: the load-factor check above already makes a full wrap impossible, 90 // but a corrupt header (nent understated) would otherwise spin forever. 91 if probes > nb { sys_close(fi); return CP_EFULL } 92 } 93 } 94 95 // ---- append the path to the blob; its END offset is the entry's path_off ---- 96 let fb: i64 = sys_openat_rdwr(blob_path, 0x1a4) 97 if fb < 0 { sys_close(fi); return CP_EIO } 98 let poff: i64 = sys_lseek(fb, 0, CP_SEEK_END) 99 if poff < 0 { sys_close(fb); sys_close(fi); return CP_EIO } 100 var plen: i64 = plen_in 101 if plen < 0 { plen = cp_len(path) } 102 if sys_write(fb, path, plen) != plen { sys_close(fb); sys_close(fi); return CP_EIO } 103 sys_close(fb) 104 105 // ---- commit the bucket, THEN the counter ---- 106 // Order matters: the bucket is what lookups read. Bumping n_entries first would advertise an 107 // entry that is not yet resolvable if we died in between; this way a crash leaves an entry 108 // that works but is uncounted, which is recoverable by a rebuild rather than a dangling read. 109 cp_w64(ent, 0, h) 110 cp_w64(ent, 8, poff) 111 cp_w64(ent, 16, plen) 112 if sys_lseek(fi, bo, CP_SEEK_SET) < 0 { sys_close(fi); return CP_EIO } 113 if sys_write(fi, ent, 24) != 24 { sys_close(fi); return CP_EIO } 114 cp_w64(hdr, 0, nent + 1) 115 if sys_lseek(fi, 8, CP_SEEK_SET) < 0 { sys_close(fi); return CP_EIO } 116 if sys_write(fi, hdr, 8) != 8 { sys_close(fi); return CP_EIO } 117 sys_close(fi) 118 return CP_INSERTED 119} 120 121// Insert cid -> path. Idempotent: a cid whose hash is already in the table is left alone. This 122// matches the builder's own identity rule (it compares hashes, not full cids -- argued safe there 123// because the cid is itself a 64-hex content hash, so a 64-bit FNV collision across 235k entries 124// is ~3e-9). plen -1 = NUL-terminated. 125func nx_galx_cidput(idx_path: *u8, blob_path: *u8, cid: *u8, path: *u8) -> i64 { 126 return nx_galx_hashput(idx_path, blob_path, cidx_fnv1a(cid, CIDX_CIDLEN), path, 0 - 1) 127} 128 129// Rebuild from the TSV, then RE-ADOPT every entry the old index had that the TSV does not mention. 130// 131// ⚠WITHOUT THIS, THE CONDITIONAL REBUILD IS A LANDMINE. Bite-proven: touching the TSV made it 132// newer, the server rebuilt, and every natively-inserted cid went from 200 to 404 -- correct by the 133// rule, and catastrophic by intent, because the destruction is triggered by merely touching an 134// unrelated legacy file and nothing reports it. 135// ★★★★★★ "AUTHORITATIVE SOURCE WINS" IS A DATA-LOSS POLICY WHENEVER THE OTHER WRITER IS ALSO 136// LEGITIMATE. A rebuild must be a MERGE, or the second writer is a bug the first writer erases. 137// The old index is walked by BUCKET, not by cid, because the store keeps hashes and paths only -- 138// which is exactly why insert-by-hash is the primitive and insert-by-cid the convenience wrapper. 139func gs_cidindex_build_preserving(tsv_path: *u8, idx_path: *u8, blob_path: *u8) -> i64 { 140 // Snapshot BEFORE the build overwrites both files. 141 let ip: *i64 = sys_mmap(16) as *i64 142 let oidx: *u8 = sys_read_file(idx_path, ip) 143 let bp: *i64 = sys_mmap(16) as *i64 144 let oblob: *u8 = sys_read_file(blob_path, bp) 145 146 let rc: i64 = gs_cidindex_build(tsv_path, idx_path, blob_path) 147 if rc < 0 { return rc } 148 if (oidx as i64) == 0 { return rc } 149 if (oblob as i64) == 0 { return rc } 150 151 let onb: i64 = cidx_rd64(oidx, 0) 152 if onb <= 0 { return rc } 153 var readopted: i64 = 0 154 var b: i64 = 0 155 while b < onb { 156 let bo: i64 = 16 + b * 24 157 let h: i64 = cidx_rd64(oidx, bo) 158 if h != 0 { 159 let poff: i64 = cidx_rd64(oidx, bo + 8) 160 let plen: i64 = cidx_rd64(oidx, bo + 16) 161 if plen > 0 { 162 if poff + plen <= bp[0] { 163 if nx_galx_hashput(idx_path, blob_path, h, 164 ((oblob as i64) + poff) as *u8, plen) == CP_INSERTED { 165 readopted = readopted + 1 166 } 167 } 168 } 169 } 170 b = b + 1 171 } 172 return rc + readopted 173}