code wiki / _hdl_build / nx_ownlib.nx

nx_ownlib.nx source

↩ module page · 1721 lines · 91512 B

1// nx_ownlib.nx -- SOVEREIGN PRIVATE OWNERSHIP LEDGER: "do I already own this?" 2// Answers the one question a storefront CANNOT answer for you. Steam refuses to sell you a game you 3// already have -- but a bundle (Humble/Fanatical) hands you a KEY, and until that key is redeemed no 4// storefront knows you own it. That is the actual mechanism behind almost every accidental rebuy. 5// So ENTITLEMENT (you hold the right) and REDEMPTION (it is in a library) are SEPARATE columns here, 6// never one boolean -- an unredeemed key is the single most valuable thing this ledger can tell you. 7// 8// Plane: knowledge/store/ownlib- Row (TSV, 8 cols): 9// <epoch_id>\t<kind>\t<store>\t<state>\t<norm_title>\t<title>\t<ext_id>\t<note> 10// kind = game|movie|tv|book|audiobook 11// state = redeemed|unredeemed|refunded|wishlist 12// norm_title = ol_norm(title) -- the MATCH KEY, edition/article/roman-numeral insensitive. 13// 14// MATCH POLICY (deliberate, and the reason this tool can be trusted): EXACT normalized-key equality 15// asserts OWNED. Anything looser is reported as a NEAR match for human review and NEVER claims 16// ownership -- naive token-subset matching would announce you own "Hollow Knight Silksong" because 17// you own "Hollow Knight". A ledger that lies once is a ledger nobody consults again. 18// 19// nx_ownlib add <kind> <store> <state> <title> [ext_id] [note] 20// nx_ownlib check <title words...> 21// nx_ownlib list [kind] 22// nx_ownlib stats 23// nx_ownlib selftest 24// Fail-closed: unknown kind/state commits NOTHING; a SHORT plane load refuses to answer at all 25// (answering "not owned" from a partial ledger is exactly how you buy a thing twice). 26// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 27import "nx_store_seed_lib.nx" 28import "nx_seg_store.nx" 29import "nx_syscalls.nx" 30 31const OL_CAP: i64 = 8388608 32const OL_TAB: i64 = 9 33const OL_NL: i64 = 10 34const OL_SP: i64 = 32 35const OL_ROWCAP: i64 = 8192 36const OL_TITLECAP: i64 = 2048 37const OL_SMALL: i64 = 64 38 39func ol_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 40func ol_w(s: *u8, n: i64) -> i64 { sys_write(1, s, n); return 0 } 41func ol_puts(s: *u8) -> i64 { ol_w(s, ol_slen(s)); return 0 } 42func ol_num(v: i64) -> i64 { 43 let t: *u8 = sys_mmap(OL_SMALL) 44 var m: i64 = v 45 if m < 0 { ol_puts("-" as *u8); m = 0 - m } 46 var k: i64 = 0 47 if m == 0 { t[0] = 48 as u8; k = 1 } 48 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 49 let o: *u8 = sys_mmap(OL_SMALL) 50 var i: i64 = 0 51 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 52 ol_w(o, k) 53 return 0 54} 55func ol_prefix() -> *u8 { return "knowledge/store/ownlib-" as *u8 } 56func ol_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o+i] = s[i]; i = i + 1 } return o + i } 57func ol_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { d[o+i] = s[i]; i = i + 1 } return o + n } 58func ol_catn(d: *u8, o: i64, v: i64) -> i64 { 59 let t: *u8 = sys_mmap(OL_SMALL) 60 var m: i64 = v 61 var k: i64 = 0 62 if m == 0 { t[0] = 48 as u8; k = 1 } 63 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 64 var i: i64 = 0 65 while i < k { d[o+i] = t[k-1-i]; i = i + 1 } 66 return o + k 67} 68func ol_streq(a: *u8, b: *u8) -> i64 { 69 var i: i64 = 0 70 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 71 if b[i] == (0 as u8) { return 1 } 72 return 0 73} 74// length-delimited token vs NUL-terminated literal, exact in BOTH directions 75func ol_tokeq(t: *u8, n: i64, lit: *u8) -> i64 { 76 var i: i64 = 0 77 while i < n { if lit[i] == (0 as u8) { return 0 } if t[i] != lit[i] { return 0 } i = i + 1 } 78 if lit[n] == (0 as u8) { return 1 } 79 return 0 80} 81func ol_bufeq(a: *u8, an: i64, b: *u8, bn: i64) -> i64 { 82 if an != bn { return 0 } 83 var i: i64 = 0 84 while i < an { if a[i] != b[i] { return 0 } i = i + 1 } 85 return 1 86} 87// PACKAGING NOISE: words that never distinguish one WORK from another. "Hollow Knight" and "Hollow 88// Knight: Definitive Edition" are the same purchase decision. Articles go too. Deliberately SHORT -- 89// every word added here is a word that can no longer tell two products apart, so this list is a 90// liability that must earn each entry, not a dumping ground. 91func ol_drop_tok(t: *u8, n: i64) -> i64 { 92 if ol_tokeq(t, n, "the" as *u8) == 1 { return 1 } 93 if ol_tokeq(t, n, "a" as *u8) == 1 { return 1 } 94 if ol_tokeq(t, n, "an" as *u8) == 1 { return 1 } 95 if ol_tokeq(t, n, "edition" as *u8) == 1 { return 1 } 96 if ol_tokeq(t, n, "goty" as *u8) == 1 { return 1 } 97 if ol_tokeq(t, n, "deluxe" as *u8) == 1 { return 1 } 98 if ol_tokeq(t, n, "definitive" as *u8) == 1 { return 1 } 99 if ol_tokeq(t, n, "remastered" as *u8) == 1 { return 1 } 100 if ol_tokeq(t, n, "complete" as *u8) == 1 { return 1 } 101 if ol_tokeq(t, n, "ultimate" as *u8) == 1 { return 1 } 102 if ol_tokeq(t, n, "enhanced" as *u8) == 1 { return 1 } 103 if ol_tokeq(t, n, "redux" as *u8) == 1 { return 1 } 104 if ol_tokeq(t, n, "anniversary" as *u8) == 1 { return 1 } 105 return 0 106} 107// Roman numerals fold to digits so "Final Fantasy VII" and "Final Fantasy 7" are ONE key. 108// NEVER applied to the FIRST token: "V Rising" and "I Am Alive" open with a letter that is only 109// coincidentally a numeral, and folding those would merge unrelated titles. 110func ol_roman_val(t: *u8, n: i64) -> i64 { 111 if ol_tokeq(t, n, "viii" as *u8) == 1 { return 8 } 112 if ol_tokeq(t, n, "xiii" as *u8) == 1 { return 13 } 113 if ol_tokeq(t, n, "xii" as *u8) == 1 { return 12 } 114 if ol_tokeq(t, n, "iii" as *u8) == 1 { return 3 } 115 if ol_tokeq(t, n, "vii" as *u8) == 1 { return 7 } 116 if ol_tokeq(t, n, "xi" as *u8) == 1 { return 11 } 117 if ol_tokeq(t, n, "iv" as *u8) == 1 { return 4 } 118 if ol_tokeq(t, n, "vi" as *u8) == 1 { return 6 } 119 if ol_tokeq(t, n, "ix" as *u8) == 1 { return 9 } 120 if ol_tokeq(t, n, "ii" as *u8) == 1 { return 2 } 121 if ol_tokeq(t, n, "x" as *u8) == 1 { return 10 } 122 if ol_tokeq(t, n, "v" as *u8) == 1 { return 5 } 123 return 0 124} 125// THE MATCH KEY. lowercase -> non-alphanumeric becomes a token break -> packaging words dropped -> 126// non-leading roman numerals folded to digits -> tokens joined by single spaces. 127// ---- UNICODE FOLDING FOR THE MATCH KEY --------------------------------------------------------- 128// ⚠MEASURED REBUY 2026-08-07: ol_norm treated EVERY byte >= 128 as a token separator, so the ledger 129// stored "Ōkami HD" under the key "kami hd" while an operator typing "Okami HD" produced "okami hd". 130// check "Okami HD" answered NOT-OWNED for a game that was in the ledger. ★★★★★★**A MATCH KEY THAT 131// DROPS CHARACTERS INSTEAD OF FOLDING THEM IS ASYMMETRIC BY CONSTRUCTION: THE STOREFRONT SPELLS IT 132// WITH THE DIACRITIC AND THE HUMAN TYPES IT WITHOUT.** 133// The second failure is worse and silent: an all-CJK title normalized to the EMPTY key, so distinct 134// titles could collide into a FALSE OWNED -- and false-owned means you skip buying something you do 135// not own. Hence: fold what folds, KEEP what does not, drop nothing. 136// 137// Two bytes per codepoint, space = unused slot. Latin-1 Supplement U+00C0..U+00FF (64 entries, 128 B) 138// and Latin Extended-A U+0100..U+017F (128 entries, 256 B) cover Western European plus romanized 139// Japanese macrons. Multi-letter folds that actually matter are spelled out (ae, ss, th, ij, oe) -- 140// "Strasse" must match "Straße" or the fold has simply moved the mismatch somewhere quieter. 141const OL_FOLD_L1: *u8 = "a a a a a a aec e e e e i i i i d n o o o o o o u u u u y thssa a a a a a aec e e e e i i i i d n o o o o o o u u u u y thy " as *u8 142const OL_FOLD_LA: *u8 = "a a a a a a c c c c c c c c d d d d e e e e e e e e e e g g g g g g g g h h h h i i i i i i i i i i ijijj j k k k l l l l l l l l l l n n n n n n n n n o o o o o o oeoer r r r r r s s s s s s s s t t t t t t u u u u u u u u u u u u w w y y y z z z z z z s " as *u8 143 144// Is this non-ASCII codepoint a LETTER (something that carries meaning in a title) as opposed to a 145// SYMBOL (decoration a human will not type)? 146// ⚠THIS IS A REGRESSION FIX FOR MY OWN CHANGE, FOUND BY THE VARIANT SWEEP AN HOUR LATER. Making 147// unfoldable codepoints "keep their bytes" was right for CJK and WRONG for ™ ® © °: those have no 148// fold either, so "Portal 2™" started keying as `portal 2™` while a human types `portal 2`. It had 149// been a separator before I touched it. ★★★★★★**A FIX AIMED AT ONE CLASS OF CHARACTER SILENTLY 150// RESHAPES EVERY OTHER CLASS THAT REACHED THE SAME BRANCH** -- and only a sweep across the whole 151// input space finds it, never the case that motivated the change. 152// WHITELIST, not blacklist: an unknown codepoint is treated as decoration and dropped, which fails 153// toward the OLD behaviour rather than toward a novel one. 154func ol_is_letter_cp(cp: i64) -> i64 { 155 if cp >= 384 { if cp <= 591 { return 1 } } // Latin Extended-B 156 if cp >= 880 { if cp <= 1023 { return 1 } } // Greek 157 if cp >= 1024 { if cp <= 1279 { return 1 } } // Cyrillic 158 if cp >= 1424 { if cp <= 1535 { return 1 } } // Hebrew 159 if cp >= 1536 { if cp <= 1791 { return 1 } } // Arabic 160 if cp >= 12352 { if cp <= 12447 { return 1 } } // Hiragana 161 if cp >= 12448 { if cp <= 12543 { return 1 } } // Katakana 162 if cp >= 19968 { if cp <= 40959 { return 1 } } // CJK Unified Ideographs 163 if cp >= 44032 { if cp <= 55203 { return 1 } } // Hangul syllables 164 return 0 165} 166 167// decode one UTF-8 character at i; return the NEXT index, write the codepoint to cpout. 168// A malformed or truncated sequence decodes as its lead byte -- one bad byte must not cost a title. 169func ol_utf8_next(src: *u8, sn: i64, i: i64, cpout: *i64) -> i64 { 170 let b0: i64 = src[i] as i64 171 if b0 < 128 { cpout[0] = b0; return i + 1 } 172 if b0 >= 240 { 173 if i + 3 < sn { 174 cpout[0] = ((b0 - 240) * 262144) + (((src[i + 1] as i64) - 128) * 4096) + (((src[i + 2] as i64) - 128) * 64) + ((src[i + 3] as i64) - 128) 175 return i + 4 176 } 177 } 178 if b0 >= 224 { 179 if i + 2 < sn { 180 cpout[0] = ((b0 - 224) * 4096) + (((src[i + 1] as i64) - 128) * 64) + ((src[i + 2] as i64) - 128) 181 return i + 3 182 } 183 } 184 if b0 >= 192 { 185 if i + 1 < sn { 186 cpout[0] = ((b0 - 192) * 64) + ((src[i + 1] as i64) - 128) 187 return i + 2 188 } 189 } 190 cpout[0] = b0 191 return i + 1 192} 193 194// fold cp to up to 2 lowercase ASCII letters. Returns the count written; 0 means NO FOLD EXISTS, 195// which is a DIFFERENT answer from "folds to nothing" and the caller must treat it differently. 196func ol_fold(cp: i64, out: *u8) -> i64 { 197 var tbl: *u8 = 0 as *u8 198 var idx: i64 = -1 199 if cp >= 192 { if cp <= 255 { tbl = OL_FOLD_L1; idx = (cp - 192) * 2 } } 200 if cp >= 256 { if cp <= 383 { tbl = OL_FOLD_LA; idx = (cp - 256) * 2 } } 201 if idx < 0 { return 0 } 202 var n: i64 = 0 203 let a: u8 = tbl[idx] 204 let b: u8 = tbl[idx + 1] 205 if a != (32 as u8) { out[n] = a; n = n + 1 } 206 if b != (32 as u8) { out[n] = b; n = n + 1 } 207 return n 208} 209 210func ol_norm(src: *u8, sn: i64, dst: *u8, dcap: i64) -> i64 { 211 var o: i64 = 0 212 var i: i64 = 0 213 var ti: i64 = 0 214 var tn: i64 = 0 215 let tok: *u8 = sys_mmap(OL_TITLECAP) 216 let cpp: *i64 = sys_mmap(OL_SMALL) as *i64 217 let fb: *u8 = sys_mmap(OL_SMALL) 218 var done: i64 = 0 219 while done == 0 { 220 var isal: i64 = 0 221 var nexti: i64 = i + 1 222 if i >= sn { done = 1 } else { 223 cpp[0] = 0 224 nexti = ol_utf8_next(src, sn, i, cpp) 225 let cp: i64 = cpp[0] 226 // isal: 0 = SEPARATOR (flush the token) · 1 = part of the token · 2 = TRANSPARENT 227 // (contributes nothing but must NOT split the token). The third state is the whole point: 228 // treating an apostrophe as a separator turned "Baldur's Gate" into `baldur s gate` while 229 // a human types `Baldurs Gate` -> different keys -> a rebuy. ★★★★★**PUNCTUATION INSIDE A 230 // WORD IS NOT A WORD BOUNDARY, AND A NORMALIZER WITH ONLY TWO STATES CANNOT SAY SO.** 231 if cp < 128 { 232 var c: i64 = cp 233 if c >= 48 { if c <= 57 { isal = 1 } } 234 if c >= 65 { if c <= 90 { c = c + 32; isal = 1 } } 235 if c >= 97 { if c <= 122 { isal = 1 } } 236 if isal == 1 { if tn < (OL_TITLECAP - 2) { tok[tn] = c as u8; tn = tn + 1 } } else { 237 if c == 39 { isal = 2 } // ' apostrophe: Baldur's == Baldurs 238 if c == 46 { isal = 2 } // . dot: S.T.A.L.K.E.R. == STALKER (spaces still split) 239 if c == 38 { 240 // & spelled out, so "Command & Conquer" == "Command and Conquer". 241 isal = 1 242 if tn < (OL_TITLECAP - 4) { tok[tn] = 97 as u8; tok[tn+1] = 110 as u8; tok[tn+2] = 100 as u8; tn = tn + 3 } 243 } 244 } 245 } else { 246 if cp == 8217 { isal = 2 } else { // U+2019 curly apostrophe: storefronts use it, humans type ' 247 let fc: i64 = ol_fold(cp, fb) 248 if fc > 0 { 249 isal = 1 250 var k: i64 = 0 251 while k < fc { 252 if tn < (OL_TITLECAP - 2) { tok[tn] = fb[k]; tn = tn + 1 } 253 k = k + 1 254 } 255 } else { 256 if ol_is_letter_cp(cp) == 1 { 257 // A LETTER with no ASCII fold (CJK, Cyrillic...). KEEP THE BYTES: dropping 258 // them collapsed distinct titles onto one empty key -> a FALSE OWNED. 259 isal = 1 260 var m: i64 = i 261 while m < nexti { 262 if tn < (OL_TITLECAP - 2) { tok[tn] = src[m]; tn = tn + 1 } 263 m = m + 1 264 } 265 } 266 // else: a SYMBOL (™ ® © °...) -- isal stays 0, so it separates and vanishes, 267 // exactly as it did before the folding work. 268 } 269 } 270 } 271 } 272 if isal == 0 { 273 if tn > 0 { 274 if ol_drop_tok(tok, tn) == 0 { 275 var rv: i64 = 0 276 if ti > 0 { rv = ol_roman_val(tok, tn) } 277 if o > 0 { if o < dcap { dst[o] = OL_SP as u8; o = o + 1 } } 278 if rv > 0 { 279 if o + 4 < dcap { o = ol_catn(dst, o, rv) } 280 } else { 281 if o + tn < dcap { o = ol_catb(dst, o, tok, tn) } 282 } 283 ti = ti + 1 284 } 285 tn = 0 286 } 287 } 288 i = nexti 289 } 290 dst[o] = 0 as u8 291 return o 292} 293// exact TOKEN membership in a space-joined key (never a substring test: "art" must not match "arts") 294func ol_has_tok(hay: *u8, hn: i64, t: *u8, tl: i64) -> i64 { 295 if tl == 0 { return 0 } 296 var s: i64 = 0 297 while s < hn { 298 var e: i64 = s 299 var go: i64 = 1 300 while go == 1 { 301 if e >= hn { go = 0 } else { if hay[e] == (OL_SP as u8) { go = 0 } else { e = e + 1 } } 302 } 303 let ln: i64 = e - s 304 if ln == tl { 305 var m: i64 = 1 306 var k: i64 = 0 307 while k < ln { if hay[s+k] != t[k] { m = 0; k = ln } else { k = k + 1 } } 308 if m == 1 { return 1 } 309 } 310 s = e + 1 311 } 312 return 0 313} 314// every token of a appears in b (used ONLY to flag NEAR matches for review, never to assert ownership) 315func ol_subset(a: *u8, an: i64, b: *u8, bn: i64) -> i64 { 316 if an == 0 { return 0 } 317 if bn == 0 { return 0 } 318 var s: i64 = 0 319 while s < an { 320 var e: i64 = s 321 var go: i64 = 1 322 while go == 1 { 323 if e >= an { go = 0 } else { if a[e] == (OL_SP as u8) { go = 0 } else { e = e + 1 } } 324 } 325 let ln: i64 = e - s 326 if ln > 0 { 327 if ol_has_tok(b, bn, ((a as i64) + s) as *u8, ln) == 0 { return 0 } 328 } 329 s = e + 1 330 } 331 return 1 332} 333// TSV field idx of a row; lenout[0] = length; 0 pointer when the row has too few columns 334func ol_field(row: *u8, rn: i64, idx: i64, lenout: *i64) -> *u8 { 335 var f: i64 = 0 336 var s: i64 = 0 337 var i: i64 = 0 338 lenout[0] = 0 339 while i <= rn { 340 var isend: i64 = 0 341 if i == rn { isend = 1 } else { if row[i] == (OL_TAB as u8) { isend = 1 } } 342 if isend == 1 { 343 if f == idx { lenout[0] = i - s; return ((row as i64) + s) as *u8 } 344 f = f + 1 345 s = i + 1 346 } 347 i = i + 1 348 } 349 return 0 as *u8 350} 351func ol_kind_ok(k: *u8) -> i64 { 352 if ol_streq(k, "game" as *u8) == 1 { return 1 } 353 if ol_streq(k, "movie" as *u8) == 1 { return 1 } 354 if ol_streq(k, "tv" as *u8) == 1 { return 1 } 355 if ol_streq(k, "book" as *u8) == 1 { return 1 } 356 if ol_streq(k, "audiobook" as *u8) == 1 { return 1 } 357 return 0 358} 359func ol_state_ok(s: *u8) -> i64 { 360 if ol_streq(s, "redeemed" as *u8) == 1 { return 1 } 361 if ol_streq(s, "unredeemed" as *u8) == 1 { return 1 } 362 if ol_streq(s, "refunded" as *u8) == 1 { return 1 } 363 if ol_streq(s, "wishlist" as *u8) == 1 { return 1 } 364 return 0 365} 366// reject anything that would corrupt the TSV row shape 367func ol_clean(s: *u8) -> i64 { 368 var i: i64 = 0 369 if s[0] == (0 as u8) { return 0 } 370 while s[i] != (0 as u8) { 371 if s[i] == (OL_TAB as u8) { return 0 } 372 if s[i] == (OL_NL as u8) { return 0 } 373 i = i + 1 374 } 375 return 1 376} 377func ol_load(buf: *u8, flags: *i64) -> i64 { 378 return sts_load_honest(ol_prefix(), buf, OL_CAP, flags) 379} 380// A SHORT LOAD MUST NEVER ANSWER. Reporting "not owned" from a partial ledger is precisely the 381// failure that makes you buy a thing twice -- the tool would be actively worse than no tool. 382func ol_load_guard(flags: *i64) -> i64 { 383 if flags[1] < flags[0] { 384 ol_puts("REFUSED: ledger load is SHORT -- declared " as *u8); ol_num(flags[0]) 385 ol_puts(" rows, found " as *u8); ol_num(flags[1]) 386 ol_puts(".\n Refusing to answer from a partial ledger (a false NOT-OWNED is how you buy it twice).\n" as *u8) 387 return 0 388 } 389 return 1 390} 391func ol_add(kind: *u8, store: *u8, state: *u8, title: *u8, ext: *u8, note: *u8) -> i64 { 392 if ol_kind_ok(kind) == 0 { ol_puts("REFUSED: kind must be game|movie|tv|book|audiobook\n" as *u8); return 2 } 393 if ol_state_ok(state) == 0 { ol_puts("REFUSED: state must be redeemed|unredeemed|refunded|wishlist\n" as *u8); return 2 } 394 if ol_clean(store) == 0 { ol_puts("REFUSED: store must be non-empty and free of tab/newline\n" as *u8); return 2 } 395 if ol_clean(title) == 0 { ol_puts("REFUSED: title must be non-empty and free of tab/newline\n" as *u8); return 2 } 396 let tn: i64 = ol_slen(title) 397 let nk: *u8 = sys_mmap(OL_TITLECAP) 398 let nkn: i64 = ol_norm(title, tn, nk, OL_TITLECAP - 2) 399 if nkn == 0 { ol_puts("REFUSED: title normalizes to an empty match key\n" as *u8); return 2 } 400 let row: *u8 = sys_mmap(OL_ROWCAP) 401 var o: i64 = 0 402 o = ol_catn(row, o, sys_now_us() / 1000000) 403 row[o] = OL_TAB as u8; o = o + 1 404 o = ol_cat(row, o, kind) 405 row[o] = OL_TAB as u8; o = o + 1 406 o = ol_cat(row, o, store) 407 row[o] = OL_TAB as u8; o = o + 1 408 o = ol_cat(row, o, state) 409 row[o] = OL_TAB as u8; o = o + 1 410 o = ol_catb(row, o, nk, nkn) 411 row[o] = OL_TAB as u8; o = o + 1 412 o = ol_cat(row, o, title) 413 row[o] = OL_TAB as u8; o = o + 1 414 o = ol_cat(row, o, ext) 415 row[o] = OL_TAB as u8; o = o + 1 416 o = ol_cat(row, o, note) 417 // sts_seed RETURNS THE NEW ROW COUNT, not 0 -- a POSITIVE rc is SUCCESS. The first cut tested 418 // `rc != 0` and so reported three writes that had in fact all committed as REFUSED. Only a 419 // NEGATIVE rc is an error. (A status API that returns a count is not a status API that returns 0.) 420 // Prefer the O(1) append: sts_seed REWRITES THE WHOLE PLANE from whatever the loader could reach, 421 // so a partial read silently deletes every row past the declared count. An append touches no 422 // existing key and therefore cannot destroy a row it never saw. A plane with no q:n yet refuses 423 // (-1) by design -- fall back to the seeding writer ONCE to bootstrap, then appends take over. 424 var rc: i64 = sts_append_fast_locked(ol_prefix(), row, o) 425 if rc < 0 { rc = sts_append_row(ol_prefix(), row, o, OL_CAP) } 426 if rc < 0 { ol_puts("REFUSED: plane append failed rc=" as *u8); ol_num(rc); ol_puts("\n" as *u8); return 4 } 427 ol_puts("ADDED " as *u8); ol_puts(kind); ol_puts(" " as *u8); ol_puts(store); ol_puts(" " as *u8); ol_puts(state) 428 ol_puts(" key=" as *u8); ol_w(nk, nkn); ol_puts("\n" as *u8) 429 return 0 430} 431func ol_check(q: *u8, qn: i64) -> i64 { 432 let nq: *u8 = sys_mmap(OL_TITLECAP) 433 let nqn: i64 = ol_norm(q, qn, nq, OL_TITLECAP - 2) 434 ol_puts("=== nx_ownlib check ===\n query: " as *u8); ol_w(q, qn) 435 ol_puts("\n key: " as *u8); ol_w(nq, nqn); ol_puts("\n" as *u8) 436 if nqn == 0 { ol_puts("REFUSED: query normalizes to an empty match key\n" as *u8); return 0 - 1 } 437 let flags: *i64 = sys_mmap(OL_SMALL) as *i64 438 let buf: *u8 = sys_mmap(OL_CAP) 439 let n: i64 = ol_load(buf, flags) 440 if ol_load_guard(flags) == 0 { return 0 - 1 } 441 let lb: *i64 = sys_mmap(OL_SMALL) as *i64 442 var owned: i64 = 0 443 var unred: i64 = 0 444 var near: i64 = 0 445 var rows: i64 = 0 446 var s: i64 = 0 447 while s < n { 448 var e: i64 = s 449 var go: i64 = 1 450 while go == 1 { 451 if e >= n { go = 0 } else { if buf[e] == (OL_NL as u8) { go = 0 } else { e = e + 1 } } 452 } 453 let rn: i64 = e - s 454 if rn > 0 { 455 rows = rows + 1 456 let row: *u8 = ((buf as i64) + s) as *u8 457 let kd: *u8 = ol_field(row, rn, 1, lb); let kdn: i64 = lb[0] 458 let st: *u8 = ol_field(row, rn, 2, lb); let stn: i64 = lb[0] 459 let sv: *u8 = ol_field(row, rn, 3, lb); let svn: i64 = lb[0] 460 let nm: *u8 = ol_field(row, rn, 4, lb); let nmn: i64 = lb[0] 461 let ti: *u8 = ol_field(row, rn, 5, lb); let tin: i64 = lb[0] 462 if (nm as i64) != 0 { 463 var refunded: i64 = 0 464 if (sv as i64) != 0 { if ol_tokeq(sv, svn, "refunded" as *u8) == 1 { refunded = 1 } } 465 if refunded == 0 { 466 var hit: i64 = 0 467 if ol_bufeq(nq, nqn, nm, nmn) == 1 { hit = 2 } else { 468 if ol_subset(nq, nqn, nm, nmn) == 1 { hit = 1 } else { 469 if ol_subset(nm, nmn, nq, nqn) == 1 { hit = 1 } 470 } 471 } 472 if hit == 2 { 473 owned = owned + 1 474 var isun: i64 = 0 475 if ol_tokeq(sv, svn, "unredeemed" as *u8) == 1 { isun = 1 } 476 if isun == 1 { unred = unred + 1; ol_puts(" [!! UNREDEEMED KEY] " as *u8) } else { ol_puts(" [OWNED] " as *u8) } 477 ol_w(kd, kdn); ol_puts(" " as *u8); ol_w(st, stn); ol_puts(" " as *u8); ol_w(sv, svn) 478 ol_puts(" " as *u8); ol_w(ti, tin); ol_puts("\n" as *u8) 479 } 480 if hit == 1 { 481 near = near + 1 482 ol_puts(" [near - review] " as *u8) 483 ol_w(kd, kdn); ol_puts(" " as *u8); ol_w(st, stn); ol_puts(" " as *u8); ol_w(sv, svn) 484 ol_puts(" " as *u8); ol_w(ti, tin); ol_puts("\n" as *u8) 485 } 486 } 487 } 488 } 489 s = e + 1 490 } 491 ol_puts(" scanned=" as *u8); ol_num(rows) 492 ol_puts(" exact=" as *u8); ol_num(owned) 493 ol_puts(" near=" as *u8); ol_num(near); ol_puts("\n" as *u8) 494 // RETURNS THE OWNERSHIP LEVEL so `advise` can gate on it: 2 = owned-unredeemed, 1 = owned, 495 // 0 = not owned, -1 = refused. Levels, not exit codes -- main() maps them, because a shell that 496 // reads exit 2 as "error" must not see "you own this" as a failure. 497 if owned > 0 { 498 if unred > 0 { 499 ol_puts("verdict=OWNED-UNREDEEMED (you already hold a key -- redeem it, do NOT buy)\n" as *u8) 500 return 2 501 } 502 ol_puts("verdict=OWNED (do not buy)\n" as *u8) 503 return 1 504 } 505 if near > 0 { 506 ol_puts("verdict=NOT-OWNED (but review the near matches above before buying)\n" as *u8) 507 return 0 508 } 509 ol_puts("verdict=NOT-OWNED\n" as *u8) 510 return 0 511} 512// ---- PRICE HISTORY (the deal half) ------------------------------------------------------------- 513// Separate plane, SAME match key -- so "do I own it" and "is this price good" can never disagree 514// about what "it" is. Row: <epoch>\t<store>\t<norm_title>\t<title>\t<price_cents>\t<currency> 515func ol_offer_prefix() -> *u8 { return "knowledge/store/ownoffer-" as *u8 } 516func ol_atoi(s: *u8) -> i64 { 517 var v: i64 = 0 518 var i: i64 = 0 519 var seen: i64 = 0 520 while s[i] != (0 as u8) { 521 let c: i64 = s[i] as i64 522 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); seen = 1 } } 523 i = i + 1 524 } 525 if seen == 0 { return 0 - 1 } 526 return v 527} 528func ol_money(cents: i64) -> i64 { 529 ol_num(cents / 100) 530 ol_puts("." as *u8) 531 let r: i64 = cents % 100 532 if r < 10 { ol_puts("0" as *u8) } 533 ol_num(r) 534 return 0 535} 536func ol_offer(store: *u8, title: *u8, pricearg: *u8, cur: *u8) -> i64 { 537 if ol_clean(store) == 0 { ol_puts("REFUSED: store must be non-empty and free of tab/newline\n" as *u8); return 2 } 538 if ol_clean(title) == 0 { ol_puts("REFUSED: title must be non-empty and free of tab/newline\n" as *u8); return 2 } 539 let price: i64 = ol_atoi(pricearg) 540 if price < 0 { ol_puts("REFUSED: price must be an integer in CENTS (1999 = 19.99)\n" as *u8); return 2 } 541 let nk: *u8 = sys_mmap(OL_TITLECAP) 542 let nkn: i64 = ol_norm(title, ol_slen(title), nk, OL_TITLECAP - 2) 543 if nkn == 0 { ol_puts("REFUSED: title normalizes to an empty match key\n" as *u8); return 2 } 544 let row: *u8 = sys_mmap(OL_ROWCAP) 545 var o: i64 = 0 546 o = ol_catn(row, o, sys_now_us() / 1000000) 547 row[o] = OL_TAB as u8; o = o + 1 548 o = ol_cat(row, o, store) 549 row[o] = OL_TAB as u8; o = o + 1 550 o = ol_catb(row, o, nk, nkn) 551 row[o] = OL_TAB as u8; o = o + 1 552 o = ol_cat(row, o, title) 553 row[o] = OL_TAB as u8; o = o + 1 554 o = ol_catn(row, o, price) 555 row[o] = OL_TAB as u8; o = o + 1 556 o = ol_cat(row, o, cur) 557 var rc: i64 = sts_append_fast_locked(ol_offer_prefix(), row, o) 558 if rc < 0 { rc = sts_append_row(ol_offer_prefix(), row, o, OL_CAP) } 559 if rc < 0 { ol_puts("REFUSED: offer plane append failed rc=" as *u8); ol_num(rc); ol_puts("\n" as *u8); return 4 } 560 ol_puts("OFFER " as *u8); ol_puts(store); ol_puts(" " as *u8); ol_money(price); ol_puts(" " as *u8); ol_puts(cur) 561 ol_puts(" key=" as *u8); ol_w(nk, nkn); ol_puts("\n" as *u8) 562 return 0 563} 564// ADVISE = the whole product in one call: ownership FIRST (it dominates -- no price is good for a 565// thing you already have), then the price against everything this ledger has ever seen. 566const OL_ADV_UNKNOWN: i64 = 0 567const OL_ADV_DONTBUY: i64 = 1 568const OL_ADV_BUY: i64 = 2 569const OL_ADV_WAIT: i64 = 3 570 571// THE ADVICE DECISION, extracted so it can be pinned by a tooth. 572// ★★★★★★OWNERSHIP SHORT-CIRCUITS PRICE, AND IT MUST DO SO *BEFORE* THE NO-HISTORY BRANCH. 573// MEASURED 2026-08-07: a title that IS in the ledger but has no recorded offers printed 574// verdict=OWNED (do not buy) 575// advice=UNKNOWN (no price history -- record offers first) 576// -- the SUMMARY LINE CONTRADICTED THE DETAIL LINE DIRECTLY ABOVE IT, and the summary is the line a 577// hurried human or a script actually takes. ★★★★★★**A SUMMARY THAT CONTRADICTS ITS OWN DETAIL IS 578// WORSE THAN NO SUMMARY: IT IS READ *INSTEAD OF* THE DETAIL, NOT ALONGSIDE IT.** 579// Structurally identical to the ingest-verdict bug found an hour earlier: a decision computed from 580// ONE of its inputs while a second, DOMINANT input sat unread. ★★★★★**WHEN TWO INPUTS CAN EACH 581// DECIDE AN OUTCOME, THE ORDER YOU TEST THEM IN *IS* THE POLICY -- WRITE IT DOWN AND TEST IT.** 582func ol_advice_code(lvl: i64, seen: i64, cheaper: i64) -> i64 { 583 if lvl > 0 { return OL_ADV_DONTBUY } // owned/entitled: price cannot change the answer 584 if seen == 0 { return OL_ADV_UNKNOWN } 585 if cheaper == 0 { return OL_ADV_BUY } 586 return OL_ADV_WAIT 587} 588 589func ol_advise(pricearg: *u8, q: *u8, qn: i64) -> i64 { 590 let price: i64 = ol_atoi(pricearg) 591 if price < 0 { ol_puts("REFUSED: price must be an integer in CENTS (1999 = 19.99)\n" as *u8); return 2 } 592 let lvl: i64 = ol_check(q, qn) 593 if lvl < 0 { return 3 } 594 ol_puts(" asking: " as *u8); ol_money(price); ol_puts("\n" as *u8) 595 let nq: *u8 = sys_mmap(OL_TITLECAP) 596 let nqn: i64 = ol_norm(q, qn, nq, OL_TITLECAP - 2) 597 let flags: *i64 = sys_mmap(OL_SMALL) as *i64 598 let buf: *u8 = sys_mmap(OL_CAP) 599 let n: i64 = sts_load_honest(ol_offer_prefix(), buf, OL_CAP, flags) 600 if flags[1] < flags[0] { ol_puts("REFUSED: offer plane load is SHORT -- refusing to price from a partial history\n" as *u8); return 3 } 601 let lb: *i64 = sys_mmap(OL_SMALL) as *i64 602 var seen: i64 = 0 603 var lo: i64 = 0 604 var hi: i64 = 0 605 var cheaper: i64 = 0 606 var s: i64 = 0 607 while s < n { 608 var e: i64 = s 609 var go: i64 = 1 610 while go == 1 { 611 if e >= n { go = 0 } else { if buf[e] == (OL_NL as u8) { go = 0 } else { e = e + 1 } } 612 } 613 let rn: i64 = e - s 614 if rn > 0 { 615 let row: *u8 = ((buf as i64) + s) as *u8 616 let nm: *u8 = ol_field(row, rn, 2, lb); let nmn: i64 = lb[0] 617 if (nm as i64) != 0 { 618 if ol_bufeq(nq, nqn, nm, nmn) == 1 { 619 let pf: *u8 = ol_field(row, rn, 4, lb); let pfn: i64 = lb[0] 620 if (pf as i64) != 0 { 621 var pv: i64 = 0 622 var pi: i64 = 0 623 while pi < pfn { let c: i64 = pf[pi] as i64; if c >= 48 { if c <= 57 { pv = pv * 10 + (c - 48) } } pi = pi + 1 } 624 if seen == 0 { lo = pv; hi = pv } else { 625 if pv < lo { lo = pv } 626 if pv > hi { hi = pv } 627 } 628 seen = seen + 1 629 if pv < price { cheaper = cheaper + 1 } 630 } 631 } 632 } 633 } 634 s = e + 1 635 } 636 if seen == 0 { 637 ol_puts(" price history: NONE for this key\n" as *u8) 638 } else { 639 ol_puts(" price history: " as *u8); ol_num(seen); ol_puts(" observation(s), low " as *u8); ol_money(lo) 640 ol_puts(", high " as *u8); ol_money(hi); ol_puts("\n" as *u8) 641 } 642 let adv: i64 = ol_advice_code(lvl, seen, cheaper) 643 if adv == OL_ADV_DONTBUY { ol_puts("advice=DO-NOT-BUY (you already own it -- price is irrelevant)\n" as *u8); return 0 } 644 if adv == OL_ADV_UNKNOWN { 645 // NO HISTORY IS NOT A GOOD DEAL. Saying "lowest ever" off a sample of one is how a price 646 // tracker becomes a marketing tool for whoever quoted first. 647 ol_puts("advice=UNKNOWN (no price history -- record offers first; one quote is not a baseline)\n" as *u8) 648 return 0 649 } 650 if adv == OL_ADV_BUY { ol_puts("advice=BUY (matches or beats every price this ledger has seen)\n" as *u8); return 0 } 651 ol_puts("advice=WAIT (seen cheaper " as *u8); ol_num(cheaper); ol_puts("x, low " as *u8); ol_money(lo); ol_puts(")\n" as *u8) 652 return 0 653} 654func ol_list(filter: *u8) -> i64 { 655 let flags: *i64 = sys_mmap(OL_SMALL) as *i64 656 let buf: *u8 = sys_mmap(OL_CAP) 657 let n: i64 = ol_load(buf, flags) 658 if ol_load_guard(flags) == 0 { return 3 } 659 let lb: *i64 = sys_mmap(OL_SMALL) as *i64 660 var shown: i64 = 0 661 var s: i64 = 0 662 while s < n { 663 var e: i64 = s 664 var go: i64 = 1 665 while go == 1 { 666 if e >= n { go = 0 } else { if buf[e] == (OL_NL as u8) { go = 0 } else { e = e + 1 } } 667 } 668 let rn: i64 = e - s 669 if rn > 0 { 670 let row: *u8 = ((buf as i64) + s) as *u8 671 let kd: *u8 = ol_field(row, rn, 1, lb); let kdn: i64 = lb[0] 672 var want: i64 = 1 673 if (filter as i64) != 0 { want = 0; if ol_tokeq(kd, kdn, filter) == 1 { want = 1 } } 674 if want == 1 { 675 let st: *u8 = ol_field(row, rn, 2, lb); let stn: i64 = lb[0] 676 let sv: *u8 = ol_field(row, rn, 3, lb); let svn: i64 = lb[0] 677 let ti: *u8 = ol_field(row, rn, 5, lb); let tin: i64 = lb[0] 678 ol_puts(" " as *u8); ol_w(kd, kdn); ol_puts(" " as *u8); ol_w(st, stn) 679 ol_puts(" " as *u8); ol_w(sv, svn); ol_puts(" " as *u8); ol_w(ti, tin); ol_puts("\n" as *u8) 680 shown = shown + 1 681 } 682 } 683 s = e + 1 684 } 685 ol_puts("-- " as *u8); ol_num(shown); ol_puts(" row(s)\n" as *u8) 686 return 0 687} 688func ol_stats() -> i64 { 689 let flags: *i64 = sys_mmap(OL_SMALL) as *i64 690 let buf: *u8 = sys_mmap(OL_CAP) 691 let n: i64 = ol_load(buf, flags) 692 if ol_load_guard(flags) == 0 { return 3 } 693 let lb: *i64 = sys_mmap(OL_SMALL) as *i64 694 var tot: i64 = 0 695 var red: i64 = 0 696 var unr: i64 = 0 697 var ref: i64 = 0 698 var wsh: i64 = 0 699 var s: i64 = 0 700 while s < n { 701 var e: i64 = s 702 var go: i64 = 1 703 while go == 1 { 704 if e >= n { go = 0 } else { if buf[e] == (OL_NL as u8) { go = 0 } else { e = e + 1 } } 705 } 706 let rn: i64 = e - s 707 if rn > 0 { 708 let row: *u8 = ((buf as i64) + s) as *u8 709 let sv: *u8 = ol_field(row, rn, 3, lb); let svn: i64 = lb[0] 710 if (sv as i64) != 0 { 711 tot = tot + 1 712 if ol_tokeq(sv, svn, "redeemed" as *u8) == 1 { red = red + 1 } 713 if ol_tokeq(sv, svn, "unredeemed" as *u8) == 1 { unr = unr + 1 } 714 if ol_tokeq(sv, svn, "refunded" as *u8) == 1 { ref = ref + 1 } 715 if ol_tokeq(sv, svn, "wishlist" as *u8) == 1 { wsh = wsh + 1 } 716 } 717 } 718 s = e + 1 719 } 720 ol_puts("=== nx_ownlib stats ===\n entitlements: " as *u8); ol_num(tot) 721 ol_puts("\n redeemed: " as *u8); ol_num(red) 722 ol_puts("\n UNREDEEMED: " as *u8); ol_num(unr); ol_puts(" <- keys you hold but no storefront knows about" as *u8) 723 ol_puts("\n refunded: " as *u8); ol_num(ref) 724 ol_puts("\n wishlist: " as *u8); ol_num(wsh); ol_puts("\n" as *u8) 725 return 0 726} 727// ---- ACQUISITION HANDLER REGISTRY (the "magnet link" rung) -------------------------------------- 728// The operator's stated endgame: ingestion should become a NATIVE Nishi OS / Nishi browser capability 729// you toggle on, the way a browser already knows what to do with a magnet: link. That is only possible 730// if "which page can I ingest?" is a REGISTERED, DATA-DRIVEN CLAIM rather than a hardcoded scraper -- 731// so the claim table ships FIRST, before any extractor exists. A handler row says: this host + this 732// path shape is a library/order page of <kind> for <store>. `handler match <url>` is the dispatch a 733// browser calls; today it answers on the CLI, and the answer does not change when a browser asks it. 734// Row: <epoch>\t<id>\t<host>\t<pathpat>\t<kind>\t<store>\t<enabled>\t<hint> 735func ol_handler_prefix() -> *u8 { return "knowledge/store/ownhandler-" as *u8 } 736// host of a url: after "://", up to the next '/' or ':'. Lowercased. 0 when there is no scheme. 737func ol_url_host(url: *u8, un: i64, out: *u8, cap: i64) -> i64 { 738 var i: i64 = 0 739 var st: i64 = 0 - 1 740 while i + 2 < un { 741 if url[i] == (58 as u8) { if url[i+1] == (47 as u8) { if url[i+2] == (47 as u8) { st = i + 3; i = un } } } 742 i = i + 1 743 } 744 if st < 0 { return 0 } 745 var o: i64 = 0 746 var j: i64 = st 747 var go: i64 = 1 748 while go == 1 { 749 if j >= un { go = 0 } else { 750 var c: i64 = url[j] as i64 751 if c == 47 { go = 0 } else { 752 if c == 58 { go = 0 } else { 753 if c >= 65 { if c <= 90 { c = c + 32 } } 754 if o < cap { out[o] = c as u8; o = o + 1 } 755 j = j + 1 756 } 757 } 758 } 759 } 760 out[o] = 0 as u8 761 return o 762} 763// REGISTRABLE-DOMAIN SUFFIX MATCH, never a bare substring. "evilhumblebundle.com" must NOT match 764// "humblebundle.com", and a substring test would hand a look-alike host your order pages. 765func ol_host_claims(host: *u8, hn: i64, pat: *u8, pn: i64) -> i64 { 766 if pn == 0 { return 0 } 767 if hn == pn { return ol_bufeq(host, hn, pat, pn) } 768 if hn < pn + 1 { return 0 } 769 if host[hn - pn - 1] != (46 as u8) { return 0 } 770 return ol_bufeq(((host as i64) + hn - pn) as *u8, pn, pat, pn) 771} 772func ol_contains_b(hay: *u8, hn: i64, ndl: *u8, nn: i64) -> i64 { 773 if nn == 0 { return 1 } 774 if nn > hn { return 0 } 775 var i: i64 = 0 776 while i + nn <= hn { 777 var m: i64 = 1 778 var k: i64 = 0 779 while k < nn { if hay[i+k] != ndl[k] { m = 0; k = nn } else { k = k + 1 } } 780 if m == 1 { return 1 } 781 i = i + 1 782 } 783 return 0 784} 785func ol_handler_add(id: *u8, host: *u8, pathpat: *u8, kind: *u8, store: *u8, hint: *u8) -> i64 { 786 if ol_kind_ok(kind) == 0 { ol_puts("REFUSED: kind must be game|movie|tv|book|audiobook\n" as *u8); return 2 } 787 if ol_clean(id) == 0 { ol_puts("REFUSED: id must be non-empty and free of tab/newline\n" as *u8); return 2 } 788 if ol_clean(host) == 0 { ol_puts("REFUSED: host must be non-empty and free of tab/newline\n" as *u8); return 2 } 789 if ol_clean(store) == 0 { ol_puts("REFUSED: store must be non-empty and free of tab/newline\n" as *u8); return 2 } 790 let row: *u8 = sys_mmap(OL_ROWCAP) 791 var o: i64 = 0 792 o = ol_catn(row, o, sys_now_us() / 1000000) 793 row[o] = OL_TAB as u8; o = o + 1 794 o = ol_cat(row, o, id) 795 row[o] = OL_TAB as u8; o = o + 1 796 o = ol_cat(row, o, host) 797 row[o] = OL_TAB as u8; o = o + 1 798 o = ol_cat(row, o, pathpat) 799 row[o] = OL_TAB as u8; o = o + 1 800 o = ol_cat(row, o, kind) 801 row[o] = OL_TAB as u8; o = o + 1 802 o = ol_cat(row, o, store) 803 row[o] = OL_TAB as u8; o = o + 1 804 o = ol_cat(row, o, "1" as *u8) 805 row[o] = OL_TAB as u8; o = o + 1 806 o = ol_cat(row, o, hint) 807 var rc: i64 = sts_append_fast_locked(ol_handler_prefix(), row, o) 808 if rc < 0 { rc = sts_append_row(ol_handler_prefix(), row, o, OL_CAP) } 809 if rc < 0 { ol_puts("REFUSED: handler plane append failed rc=" as *u8); ol_num(rc); ol_puts("\n" as *u8); return 4 } 810 ol_puts("HANDLER " as *u8); ol_puts(id); ol_puts(" " as *u8); ol_puts(host); ol_puts(pathpat) 811 ol_puts(" -> " as *u8); ol_puts(kind); ol_puts("/" as *u8); ol_puts(store); ol_puts("\n" as *u8) 812 return 0 813} 814// THE DISPATCH. `match` is what a browser/OS integration calls: given a URL, who claims it? 815func ol_handler_scan(url: *u8, un: i64, listall: i64) -> i64 { 816 let host: *u8 = sys_mmap(OL_TITLECAP) 817 var hn: i64 = 0 818 if listall == 0 { 819 hn = ol_url_host(url, un, host, OL_TITLECAP - 2) 820 if hn == 0 { ol_puts("REFUSED: url must include a scheme, e.g. https://host/path\n" as *u8); return 2 } 821 } 822 let flags: *i64 = sys_mmap(OL_SMALL) as *i64 823 let buf: *u8 = sys_mmap(OL_CAP) 824 let n: i64 = sts_load_honest(ol_handler_prefix(), buf, OL_CAP, flags) 825 if flags[1] < flags[0] { ol_puts("REFUSED: handler plane load is SHORT\n" as *u8); return 3 } 826 let lb: *i64 = sys_mmap(OL_SMALL) as *i64 827 var hits: i64 = 0 828 var s: i64 = 0 829 while s < n { 830 var e: i64 = s 831 var go: i64 = 1 832 while go == 1 { 833 if e >= n { go = 0 } else { if buf[e] == (OL_NL as u8) { go = 0 } else { e = e + 1 } } 834 } 835 let rn: i64 = e - s 836 if rn > 0 { 837 let row: *u8 = ((buf as i64) + s) as *u8 838 let id: *u8 = ol_field(row, rn, 1, lb); let idn: i64 = lb[0] 839 let hp: *u8 = ol_field(row, rn, 2, lb); let hpn: i64 = lb[0] 840 let pp: *u8 = ol_field(row, rn, 3, lb); let ppn: i64 = lb[0] 841 let kd: *u8 = ol_field(row, rn, 4, lb); let kdn: i64 = lb[0] 842 let st: *u8 = ol_field(row, rn, 5, lb); let stn: i64 = lb[0] 843 let en: *u8 = ol_field(row, rn, 6, lb); let enn: i64 = lb[0] 844 if (hp as i64) != 0 { 845 var claim: i64 = listall 846 if listall == 0 { 847 if ol_host_claims(host, hn, hp, hpn) == 1 { 848 if ol_contains_b(url, un, pp, ppn) == 1 { claim = 1 } 849 } 850 } 851 var on: i64 = 1 852 if (en as i64) != 0 { if ol_tokeq(en, enn, "0" as *u8) == 1 { on = 0 } } 853 if claim == 1 { 854 hits = hits + 1 855 if on == 1 { ol_puts(" [CLAIMED] " as *u8) } else { ol_puts(" [disabled] " as *u8) } 856 ol_w(id, idn); ol_puts(" " as *u8); ol_w(hp, hpn); ol_w(pp, ppn) 857 ol_puts(" -> " as *u8); ol_w(kd, kdn); ol_puts("/" as *u8); ol_w(st, stn); ol_puts("\n" as *u8) 858 } 859 } 860 } 861 s = e + 1 862 } 863 if listall == 1 { ol_puts("-- " as *u8); ol_num(hits); ol_puts(" handler(s)\n" as *u8); return 0 } 864 if hits == 0 { 865 ol_puts("verdict=UNCLAIMED (no handler ingests this url -- register one with: nx_ownlib handler add ...)\n" as *u8) 866 return 0 867 } 868 ol_puts("verdict=CLAIMED (" as *u8); ol_num(hits); ol_puts(" handler(s); this is the hook a Nishi browser/OS integration calls)\n" as *u8) 869 return 0 870} 871// ---- BUNDLE SCAN: "what in this bundle do I already own?" --------------------------------------- 872// The consumer the handler registry existed for. A Humble bundle page carries STRUCTURED JSON, not 873// just rendered HTML -- each item object has "human_name" (display title) and "item_content_type" 874// (game|ebook|audio|...). MEASURED on a live page 2026-08-06: 19 human_name values, all content type 875// "game". So extraction is a FIELD READ, not a scrape -- and because the content type travels WITH 876// the title, a book bundle classifies itself with no new code. That is why this reads the JSON field 877// rather than tile markup: markup is a layout decision the vendor can change on a whim, the data 878// contract behind it changes far less often. 879// 880// It deliberately lives INSIDE nx_ownlib rather than in a sibling organ: the match key (ol_norm) is 881// the correctness-critical part, and a second binary with its own copy of it is exactly how two 882// components drift into disagreeing about what "the same game" means. 883func ol_lit_at(buf: *u8, n: i64, i: i64, lit: *u8) -> i64 { 884 var k: i64 = 0 885 while lit[k] != (0 as u8) { 886 if i + k >= n { return 0 } 887 if buf[i + k] != lit[k] { return 0 } 888 k = k + 1 889 } 890 return k 891} 892// ledger lookup for an ALREADY-NORMALIZED key: 2 = owned-unredeemed, 1 = owned, 0 = not owned. 893// nearout[0] gets the count of near (review) matches. Refunded rows never count as owned. 894func ol_lookup(nq: *u8, nqn: i64, buf: *u8, n: i64, nearout: *i64) -> i64 { 895 nearout[0] = 0 896 if nqn == 0 { return 0 } 897 let lb: *i64 = sys_mmap(OL_SMALL) as *i64 898 var lvl: i64 = 0 899 var s: i64 = 0 900 while s < n { 901 var e: i64 = s 902 var go: i64 = 1 903 while go == 1 { 904 if e >= n { go = 0 } else { if buf[e] == (OL_NL as u8) { go = 0 } else { e = e + 1 } } 905 } 906 let rn: i64 = e - s 907 if rn > 0 { 908 let row: *u8 = ((buf as i64) + s) as *u8 909 let sv: *u8 = ol_field(row, rn, 3, lb); let svn: i64 = lb[0] 910 let nm: *u8 = ol_field(row, rn, 4, lb); let nmn: i64 = lb[0] 911 if (nm as i64) != 0 { 912 var refunded: i64 = 0 913 if (sv as i64) != 0 { if ol_tokeq(sv, svn, "refunded" as *u8) == 1 { refunded = 1 } } 914 if refunded == 0 { 915 if ol_bufeq(nq, nqn, nm, nmn) == 1 { 916 var l2: i64 = 1 917 if ol_tokeq(sv, svn, "unredeemed" as *u8) == 1 { l2 = 2 } 918 if l2 > lvl { lvl = l2 } 919 } else { 920 var near: i64 = 0 921 if ol_subset(nq, nqn, nm, nmn) == 1 { near = 1 } 922 if near == 0 { if ol_subset(nm, nmn, nq, nqn) == 1 { near = 1 } } 923 if near == 1 { nearout[0] = nearout[0] + 1 } 924 } 925 } 926 } 927 } 928 s = e + 1 929 } 930 return lvl 931} 932// ---- JSON string-escape decoding ------------------------------------------------------------ 933// WHY THIS IS LOAD-BEARING PRODUCT LOGIC, NOT POLISH: the handler this replaces took "the NEXT byte 934// literally" on a backslash. That is correct for \" and \\ and WRONG for \uXXXX -- it drops the 935// backslash and leaves the digits, so Ōkami becomes the literal text u014ckami. Storefront JSON 936// escapes non-ASCII AS A MATTER OF COURSE, so that defect renames every title carrying an accent or 937// a CJK character, and ***A RENAMED TITLE MATCHES NOTHING IN THE LEDGER***, so the checker answers 938// NEW and the operator rebuys a game they already own -- the exact failure this organ exists to 939// prevent. Measured 2026-08-07 on the estate's own JSON path: a wire é landed on disk as the 940// literal text u00e9. The old comment called that "degrades to its literal digits rather than 941// corrupting the scan"; it IS the corruption, stated as a reassurance. 942 943// one hex digit -> 0..15, or -1. Both cases, because storefronts emit both. 944func ol_hexval(c: i64) -> i64 { 945 if c >= 48 { if c <= 57 { return c - 48 } } 946 if c >= 97 { if c <= 102 { return c - 87 } } 947 if c >= 65 { if c <= 70 { return c - 55 } } 948 return -1 949} 950 951// exactly 4 hex digits at j, or -1. A short or malformed run is REFUSED whole rather than partially 952// consumed: a decoder that guesses is how you get a plausible WRONG title, which is worse than none. 953// -1 is unambiguous here because every valid result is 0..65535, so this needs no out-param and thus 954// performs no allocation -- it is called once per escape inside the scan loop. 955func ol_hex4(page: *u8, pn: i64, j: i64) -> i64 { 956 if j + 3 >= pn { return -1 } 957 var v: i64 = 0 958 var k: i64 = 0 959 while k < 4 { 960 let d: i64 = ol_hexval(page[j + k] as i64) 961 if d < 0 { return -1 } 962 v = v * 16 + d 963 k = k + 1 964 } 965 return v 966} 967 968// append codepoint cp as UTF-8, bounded by cap. A codepoint that does not fit is dropped WHOLE 969// rather than truncated mid-sequence: half a UTF-8 character is not a shorter title, it is an 970// invalid one, and it would poison the normalized key it feeds. 971func ol_utf8(cp: i64, dst: *u8, tnp: *i64, cap: i64) { 972 var n: i64 = tnp[0] 973 if cp < 128 { 974 if n < cap { dst[n] = cp as u8; n = n + 1 } 975 } else { 976 if cp < 2048 { 977 if n + 1 < cap { 978 dst[n] = (192 + (cp / 64)) as u8 979 dst[n + 1] = (128 + (cp - ((cp / 64) * 64))) as u8 980 n = n + 2 981 } 982 } else { 983 if cp < 65536 { 984 if n + 2 < cap { 985 let r1: i64 = cp - ((cp / 4096) * 4096) 986 dst[n] = (224 + (cp / 4096)) as u8 987 dst[n + 1] = (128 + (r1 / 64)) as u8 988 dst[n + 2] = (128 + (r1 - ((r1 / 64) * 64))) as u8 989 n = n + 3 990 } 991 } else { 992 if n + 3 < cap { 993 let r2: i64 = cp - ((cp / 262144) * 262144) 994 let r3: i64 = r2 - ((r2 / 4096) * 4096) 995 dst[n] = (240 + (cp / 262144)) as u8 996 dst[n + 1] = (128 + (r2 / 4096)) as u8 997 dst[n + 2] = (128 + (r3 / 64)) as u8 998 dst[n + 3] = (128 + (r3 - ((r3 / 64) * 64))) as u8 999 n = n + 4 1000 } 1001 } 1002 } 1003 } 1004 tnp[0] = n 1005} 1006 1007// decode the escape whose BACKSLASH sits at j; append to dst; return the index just past it. 1008func ol_json_esc(page: *u8, pn: i64, j: i64, dst: *u8, tnp: *i64, cap: i64) -> i64 { 1009 if j + 1 >= pn { return j + 1 } 1010 let e: i64 = page[j + 1] as i64 1011 var lit: i64 = -1 1012 if e == 110 { lit = 10 } 1013 if e == 116 { lit = 9 } 1014 if e == 114 { lit = 13 } 1015 if e == 98 { lit = 8 } 1016 if e == 102 { lit = 12 } 1017 if e == 34 { lit = 34 } 1018 if e == 92 { lit = 92 } 1019 if e == 47 { lit = 47 } 1020 if lit >= 0 { 1021 var n: i64 = tnp[0] 1022 if n < cap { dst[n] = lit as u8; tnp[0] = n + 1 } 1023 return j + 2 1024 } 1025 if e != 117 { 1026 // UNKNOWN escape: emit the character itself. Matches the old permissive behaviour on 1027 // purpose -- a storefront quirk must not abort a scan that is otherwise fine. 1028 var n2: i64 = tnp[0] 1029 if n2 < cap { dst[n2] = e as u8; tnp[0] = n2 + 1 } 1030 return j + 2 1031 } 1032 var cp: i64 = ol_hex4(page, pn, j + 2) 1033 if cp < 0 { 1034 // malformed \u: emit a literal 'u' and resume. Refusing the whole title would let one bad 1035 // byte hide an entire purchase from the ledger. 1036 var n3: i64 = tnp[0] 1037 if n3 < cap { dst[n3] = 117 as u8; tnp[0] = n3 + 1 } 1038 return j + 2 1039 } 1040 var next: i64 = j + 6 1041 // SURROGATE PAIR. A BMP-only decoder emits garbage for every astral-plane character, and those 1042 // DO appear in real storefront titles. Pair them before encoding, never after. 1043 if cp >= 55296 { 1044 if cp <= 56319 { 1045 if next + 1 < pn { 1046 if page[next] == (92 as u8) { 1047 if page[next + 1] == (117 as u8) { 1048 let lo: i64 = ol_hex4(page, pn, next + 2) 1049 if lo >= 56320 { 1050 if lo <= 57343 { 1051 cp = 65536 + ((cp - 55296) * 1024) + (lo - 56320) 1052 next = next + 6 1053 } 1054 } 1055 } 1056 } 1057 } 1058 } 1059 } 1060 ol_utf8(cp, dst, tnp, cap) 1061 return next 1062} 1063 1064func ol_scan(path: *u8, skipfirst: i64) -> i64 { 1065 let szp: *i64 = sys_mmap(OL_SMALL) as *i64 1066 szp[0] = 0 1067 let page: *u8 = ss_loadfile(path, szp, 1) 1068 if (page as i64) == 0 { ol_puts("REFUSED: cannot read page file\n" as *u8); return 2 } 1069 let pn: i64 = szp[0] 1070 if pn <= 0 { ol_puts("REFUSED: page file is empty\n" as *u8); return 2 } 1071 let flags: *i64 = sys_mmap(OL_SMALL) as *i64 1072 let led: *u8 = sys_mmap(OL_CAP) 1073 let ln: i64 = ol_load(led, flags) 1074 if ol_load_guard(flags) == 0 { return 3 } 1075 ol_puts("=== nx_ownlib scan ===\n page bytes: " as *u8); ol_num(pn) 1076 ol_puts(" ledger rows: " as *u8); ol_num(flags[1]); ol_puts("\n" as *u8) 1077 let seen: *u8 = sys_mmap(OL_CAP) 1078 var so: i64 = 0 1079 let title: *u8 = sys_mmap(OL_TITLECAP) 1080 let nk: *u8 = sys_mmap(OL_TITLECAP) 1081 let nearb: *i64 = sys_mmap(OL_SMALL) as *i64 1082 // scratch cell for the escape decoder's running output length. Allocated ONCE here rather than 1083 // inside ol_json_esc, which runs per-escape inside the scan loop. 1084 let tnp: *i64 = sys_mmap(OL_SMALL) as *i64 1085 var found: i64 = 0 1086 var owned: i64 = 0 1087 var unred: i64 = 0 1088 var newc: i64 = 0 1089 var nearc: i64 = 0 1090 // end of the PREVIOUS title's value -- the backward scan may not cross it, so an object can never 1091 // borrow the kind of the one before it either. Bounded in both directions or it is not a bound. 1092 var prevend: i64 = 0 1093 var i: i64 = 0 1094 while i < pn { 1095 let hit: i64 = ol_lit_at(page, pn, i, "\"human_name\"" as *u8) 1096 if hit == 0 { i = i + 1 } else { 1097 var j: i64 = i + hit 1098 // skip whitespace and the colon, then land on the opening quote of the value 1099 var g2: i64 = 1 1100 while g2 == 1 { 1101 if j >= pn { g2 = 0 } else { 1102 let c: i64 = page[j] as i64 1103 if c == 34 { g2 = 0 } else { j = j + 1 } 1104 } 1105 } 1106 var tn: i64 = 0 1107 if j < pn { 1108 j = j + 1 1109 var g3: i64 = 1 1110 while g3 == 1 { 1111 if j >= pn { g3 = 0 } else { 1112 let c2: i64 = page[j] as i64 1113 if c2 == 34 { g3 = 0 } else { 1114 // JSON escape: FULLY decoded, including \uXXXX and surrogate pairs. See 1115 // ol_json_esc -- getting this wrong renames titles and causes rebuys. 1116 if c2 == 92 { 1117 tnp[0] = tn 1118 j = ol_json_esc(page, pn, j, title, tnp, OL_TITLECAP - 2) 1119 tn = tnp[0] 1120 } else { 1121 if tn < (OL_TITLECAP - 2) { title[tn] = c2 as u8; tn = tn + 1 } 1122 j = j + 1 1123 } 1124 } 1125 } 1126 } 1127 } 1128 title[tn] = 0 as u8 1129 // IS THIS AN ITEM, OR PAGE FURNITURE? A bundle page carries human_name values that are NOT 1130 // purchasable items -- MEASURED on a live page: "Direct Relief" (the charity) and 1131 // "Month-to-Month Plan" (a Choice upsell) both appeared in the first cut's checklist. 1132 // A denylist of those two strings would be a lie that works once; the STRUCTURAL truth is 1133 // that a real item object carries "item_content_type" and furniture does not. So: accept 1134 // the title only if a content type appears BEFORE the next human_name. That also hands us 1135 // the kind for free, which is why a book bundle needs no new code. 1136 // ⚠SCAN BACKWARD, NOT FORWARD. The first cut scanned FORWARD from the title for a content 1137 // type and got two things wrong at once, both visible in one run: "Direct Relief" (the 1138 // charity) inherited (game) from the NEXT object, and "Sakura Knight 2" was labelled 1139 // (soundtrack_listing) from a LATER one. ***A FORWARD SCAN MAKES PAGE FURNITURE INHERIT THE 1140 // IDENTITY OF WHATEVER FOLLOWS IT.*** Humble emits item_content_type BEFORE human_name 1141 // within each object, so the owning field is BEHIND the title -- bounded by the previous 1142 // title so one object can never borrow another's kind. 1143 var isitem: i64 = 0 1144 var kindp: i64 = 0 1145 var kindn: i64 = 0 1146 var k9: i64 = i 1147 var g9: i64 = 1 1148 while g9 == 1 { 1149 if k9 <= prevend { g9 = 0 } else { 1150 k9 = k9 - 1 1151 let ct: i64 = ol_lit_at(page, pn, k9, "\"item_content_type\"" as *u8) 1152 if ct > 0 { 1153 isitem = 1 1154 var m9: i64 = k9 + ct 1155 var g10: i64 = 1 1156 while g10 == 1 { 1157 if m9 >= pn { g10 = 0 } else { if page[m9] == (34 as u8) { g10 = 0 } else { m9 = m9 + 1 } } 1158 } 1159 if m9 < pn { 1160 m9 = m9 + 1 1161 kindp = m9 1162 var g11: i64 = 1 1163 while g11 == 1 { 1164 if m9 >= pn { g11 = 0 } else { if page[m9] == (34 as u8) { g11 = 0 } else { m9 = m9 + 1 } } 1165 } 1166 kindn = m9 - kindp 1167 } 1168 g9 = 0 1169 } 1170 } 1171 } 1172 if isitem == 0 { tn = 0 } 1173 if tn > 0 { 1174 let nkn: i64 = ol_norm(title, tn, nk, OL_TITLECAP - 2) 1175 // DEDUPE on the normalized key: the same item appears in several JSON blocks on one 1176 // page, and a checklist that lists a game five times is not a checklist. 1177 var dup: i64 = 0 1178 var q: i64 = 0 1179 while q < so { 1180 var e2: i64 = q 1181 var g4: i64 = 1 1182 while g4 == 1 { 1183 if e2 >= so { g4 = 0 } else { if seen[e2] == (OL_NL as u8) { g4 = 0 } else { e2 = e2 + 1 } } 1184 } 1185 if ol_bufeq(nk, nkn, ((seen as i64) + q) as *u8, e2 - q) == 1 { dup = 1 } 1186 q = e2 + 1 1187 } 1188 if dup == 0 { 1189 if so + nkn + 2 < OL_CAP { so = ol_catb(seen, so, nk, nkn); seen[so] = OL_NL as u8; so = so + 1 } 1190 found = found + 1 1191 var show: i64 = 1 1192 // ⚠THE OLD COMMENT HERE CLAIMED "the FIRST human_name on a bundle page is the 1193 // BUNDLE ITSELF, not an item" AND THAT IS DISPROVEN. This code only ever sees 1194 // names that ALREADY PASSED the item_content_type filter above, and the bundle's 1195 // own title does not carry a content type -- so it never reaches here at all. 1196 // MEASURED on the live page (718,816 B, 64 human_name / 31 item_content_type): 1197 // the skip dropped "Leveling up girls in another world", a REAL GAME -- 29 items 1198 // reported of 30, while the bundle name "Even Steamier Sakura Special" appeared 1199 // ZERO times either way. ★★★★★A POSITIONAL HACK GUARDING AGAINST SOMETHING A 1200 // STRUCTURAL FILTER ALREADY REMOVED IS PURE LOSS -- IT CANNOT HELP AND IT ALWAYS 1201 // COSTS ONE ITEM. Now OPT-IN ONLY, so the default cannot silently hide a title. 1202 if skipfirst == 1 { if found == 1 { show = 0 } } 1203 if show == 1 { 1204 let lvl: i64 = ol_lookup(nk, nkn, led, ln, nearb) 1205 if lvl == 2 { unred = unred + 1; ol_puts(" [!! UNREDEEMED KEY] " as *u8) } 1206 if lvl == 1 { owned = owned + 1; ol_puts(" [OWNED] " as *u8) } 1207 if lvl == 0 { 1208 newc = newc + 1 1209 if nearb[0] > 0 { nearc = nearc + 1; ol_puts(" [ NEW - near match] " as *u8) } else { ol_puts(" [ NEW ] " as *u8) } 1210 } 1211 ol_w(title, tn) 1212 if kindn > 0 { ol_puts(" (" as *u8); ol_w(((page as i64) + kindp) as *u8, kindn); ol_puts(")" as *u8) } 1213 ol_puts("\n" as *u8) 1214 } 1215 } 1216 } 1217 prevend = j 1218 i = j 1219 } 1220 } 1221 ol_puts(" ---\n items=" as *u8); ol_num(owned + unred + newc) 1222 ol_puts(" owned=" as *u8); ol_num(owned) 1223 ol_puts(" unredeemed=" as *u8); ol_num(unred) 1224 ol_puts(" new=" as *u8); ol_num(newc) 1225 ol_puts(" near=" as *u8); ol_num(nearc); ol_puts("\n" as *u8) 1226 if owned + unred == 0 { 1227 ol_puts("verdict=ALL-NEW (nothing in this bundle matches the ledger)\n" as *u8) 1228 return 0 1229 } 1230 ol_puts("verdict=OVERLAP (" as *u8); ol_num(owned + unred) 1231 ol_puts(" of " as *u8); ol_num(owned + unred + newc) 1232 ol_puts(" already yours -- check UNREDEEMED first, those are keys no storefront can see)\n" as *u8) 1233 return 0 1234} 1235func ol_t(name: *u8, got: i64, want: i64, pass: *i64, tot: *i64) -> i64 { 1236 tot[0] = tot[0] + 1 1237 if got == want { pass[0] = pass[0] + 1; ol_puts(" PASS " as *u8) } else { ol_puts(" FAIL " as *u8) } 1238 ol_puts(name) 1239 if got != want { ol_puts(" (got " as *u8); ol_num(got); ol_puts(" want " as *u8); ol_num(want); ol_puts(")" as *u8) } 1240 ol_puts("\n" as *u8) 1241 return 0 1242} 1243func ol_nrm_is(src: *u8, want: *u8, pass: *i64, tot: *i64, label: *u8) -> i64 { 1244 let d: *u8 = sys_mmap(OL_TITLECAP) 1245 let dn: i64 = ol_norm(src, ol_slen(src), d, OL_TITLECAP - 2) 1246 var ok: i64 = 0 1247 if ol_bufeq(d, dn, want, ol_slen(want)) == 1 { ok = 1 } 1248 tot[0] = tot[0] + 1 1249 if ok == 1 { pass[0] = pass[0] + 1; ol_puts(" PASS " as *u8) } else { ol_puts(" FAIL " as *u8) } 1250 ol_puts(label); ol_puts(" -> '" as *u8); ol_w(d, dn); ol_puts("'\n" as *u8) 1251 return 0 1252} 1253// INGEST VERDICT: 0 GREEN · 1 RED-nothing-matched · 2 RED-writes-failed. 1254// ⚠THIS EXISTS BECAUSE MY FIRST CUT GOT IT WRONG AND THE RUN PROVED IT: with every single append 1255// failing (`refused=2 added=0`) it still printed verdict=GREEN, because the check only asked whether 1256// the KEY matched. ★★★★★★**"I PARSED THE INPUT" IS NOT "I DID THE WORK" — A VERDICT THAT MEASURES 1257// ONLY THE READ SIDE BLESSES A RUN THAT WROTE NOTHING.** The partition line reconciled perfectly the 1258// whole time, which is the trap: ★★★★★**A PARTITION THAT SUMS IS A PROOF OF ACCOUNTING, NOT A PROOF 1259// OF SUCCESS.** Extracted so a tooth can pin it; the bug was in the DECISION, so the tooth tests the 1260// decision rather than the plumbing around it. 1261func ol_ingest_verdict(found: i64, refused: i64) -> i64 { 1262 if found == 0 { return 1 } 1263 if refused > 0 { return 2 } 1264 return 0 1265} 1266 1267// ---- INGEST: load a storefront export and append one ledger row per distinct title ------------- 1268// DATA-DRIVEN BY DESIGN (rule 6): the ONLY thing that differs between storefronts is which JSON key 1269// holds the title -- Steam's IPlayerService/GetOwnedGames says "name", Humble's order JSON says 1270// "human_name". So the key is an ARGUMENT, not a branch, and adding GOG or Fanatical needs no code. 1271// ★A PER-STORE PARSER IS A PER-STORE BUG; ONE PARSER WITH A PER-STORE KEY IS A CONFIG ROW. 1272// 1273// IDEMPOTENT (rule 10) THROUGH TWO SEPARATE GUARDS, because they catch different things: 1274// - the LEDGER guard stops a SECOND RUN re-adding everything (re-ingest must be a no-op), 1275// - the in-run SEEN guard stops one export's internal repeats (exports do repeat titles). 1276// Neither subsumes the other; dropping either makes a re-run silently double the ledger. 1277func ol_ingest(kind: *u8, store: *u8, state: *u8, path: *u8, key: *u8) -> i64 { 1278 if ol_kind_ok(kind) == 0 { ol_puts("REFUSED: kind must be game|movie|tv|book|audiobook\n" as *u8); return 2 } 1279 if ol_state_ok(state) == 0 { ol_puts("REFUSED: state must be redeemed|unredeemed|refunded|wishlist\n" as *u8); return 2 } 1280 if ol_clean(store) == 0 { ol_puts("REFUSED: store must be non-empty and free of tab/newline\n" as *u8); return 2 } 1281 if ol_slen(key) == 0 { ol_puts("REFUSED: title key must be non-empty\n" as *u8); return 2 } 1282 let szp: *i64 = sys_mmap(OL_SMALL) as *i64 1283 szp[0] = 0 1284 let page: *u8 = ss_loadfile(path, szp, 1) 1285 if (page as i64) == 0 { ol_puts("REFUSED: cannot read export file\n" as *u8); return 2 } 1286 let pn: i64 = szp[0] 1287 if pn <= 0 { ol_puts("REFUSED: export file is empty\n" as *u8); return 2 } 1288 let led: *u8 = sys_mmap(OL_CAP) 1289 let flags: *i64 = sys_mmap(OL_SMALL) as *i64 1290 let ln: i64 = ol_load(led, flags) 1291 if ol_load_guard(flags) == 0 { return 3 } 1292 // Search for the QUOTED key. An unquoted needle matches the word wherever it appears in prose or 1293 // inside another key ("name" would hit "human_name", "filename", "nickname"), which is how a 1294 // parser quietly ingests the wrong field and reports a confident count. 1295 let needle: *u8 = sys_mmap(OL_TITLECAP) 1296 var nn: i64 = 0 1297 needle[nn] = 34 as u8; nn = nn + 1 1298 nn = ol_cat(needle, nn, key) 1299 needle[nn] = 34 as u8; nn = nn + 1 1300 needle[nn] = 0 as u8 1301 ol_puts("=== nx_ownlib ingest ===\n file bytes: " as *u8); ol_num(pn) 1302 ol_puts(" ledger rows before: " as *u8); ol_num(flags[1]) 1303 ol_puts(" key=" as *u8); ol_puts(key) 1304 ol_puts(" kind=" as *u8); ol_puts(kind) 1305 ol_puts(" store=" as *u8); ol_puts(store) 1306 ol_puts(" state=" as *u8); ol_puts(state); ol_puts("\n" as *u8) 1307 let title: *u8 = sys_mmap(OL_TITLECAP) 1308 let nk: *u8 = sys_mmap(OL_TITLECAP) 1309 let seen: *u8 = sys_mmap(OL_CAP) 1310 let nearb: *i64 = sys_mmap(OL_SMALL) as *i64 1311 let tnp: *i64 = sys_mmap(OL_SMALL) as *i64 1312 var so: i64 = 0 1313 var found: i64 = 0 1314 var added: i64 = 0 1315 var already: i64 = 0 1316 var dupped: i64 = 0 1317 var refused: i64 = 0 1318 var i: i64 = 0 1319 while i < pn { 1320 let hit: i64 = ol_lit_at(page, pn, i, needle) 1321 if hit == 0 { i = i + 1 } else { 1322 var j: i64 = i + hit 1323 var g2: i64 = 1 1324 while g2 == 1 { 1325 if j >= pn { g2 = 0 } else { 1326 let c: i64 = page[j] as i64 1327 if c == 34 { g2 = 0 } else { j = j + 1 } 1328 } 1329 } 1330 var tn: i64 = 0 1331 if j < pn { 1332 j = j + 1 1333 tnp[0] = 0 1334 var g3: i64 = 1 1335 while g3 == 1 { 1336 if j >= pn { g3 = 0 } else { 1337 let c2: i64 = page[j] as i64 1338 if c2 == 34 { g3 = 0 } else { 1339 if c2 == 92 { j = ol_json_esc(page, pn, j, title, tnp, OL_TITLECAP - 2) } else { 1340 let t2: i64 = tnp[0] 1341 if t2 < (OL_TITLECAP - 2) { title[t2] = c2 as u8; tnp[0] = t2 + 1 } 1342 j = j + 1 1343 } 1344 } 1345 } 1346 } 1347 tn = tnp[0] 1348 } 1349 title[tn] = 0 as u8 1350 if tn > 0 { 1351 found = found + 1 1352 let nkn: i64 = ol_norm(title, tn, nk, OL_TITLECAP - 2) 1353 if nkn == 0 { refused = refused + 1 } else { 1354 var skip: i64 = 0 1355 if ol_lookup(nk, nkn, led, ln, nearb) > 0 { already = already + 1; skip = 1 } 1356 if skip == 0 { 1357 var q: i64 = 0 1358 while q < so { 1359 var e2: i64 = q 1360 var g4: i64 = 1 1361 while g4 == 1 { 1362 if e2 >= so { g4 = 0 } else { if seen[e2] == (OL_NL as u8) { g4 = 0 } else { e2 = e2 + 1 } } 1363 } 1364 if ol_bufeq(nk, nkn, ((seen as i64) + q) as *u8, e2 - q) == 1 { skip = 1; dupped = dupped + 1 } 1365 q = e2 + 1 1366 } 1367 } 1368 if skip == 0 { 1369 if so + nkn + 2 < OL_CAP { so = ol_catb(seen, so, nk, nkn); seen[so] = OL_NL as u8; so = so + 1 } 1370 if ol_add(kind, store, state, title, "" as *u8, "ingest" as *u8) == 0 { added = added + 1 } else { refused = refused + 1 } 1371 } 1372 } 1373 } 1374 i = j 1375 } 1376 } 1377 // A PARTITION IS A CLAIM: these four MUST sum to `found`, and the sum is PRINTED so a reader can 1378 // check it rather than trust it. An unexplained residual here means titles went missing silently. 1379 ol_puts(" ---\n found=" as *u8); ol_num(found) 1380 ol_puts(" added=" as *u8); ol_num(added) 1381 ol_puts(" already_in_ledger=" as *u8); ol_num(already) 1382 ol_puts(" dup_within_file=" as *u8); ol_num(dupped) 1383 ol_puts(" refused=" as *u8); ol_num(refused) 1384 ol_puts("\n partition_sum=" as *u8); ol_num(added + already + dupped + refused) 1385 if (added + already + dupped + refused) == found { ol_puts(" (== found, reconciled)\n" as *u8) } else { ol_puts(" (!= found -- LEAK)\n" as *u8) } 1386 let v: i64 = ol_ingest_verdict(found, refused) 1387 if v == 1 { 1388 // ★★★★★A LOADER THAT CANNOT TELL "I FOUND NOTHING" FROM "THERE IS NOTHING" REPORTS A WRONG 1389 // KEY AS AN EMPTY LIBRARY. Refuse rather than report a cheerful zero. 1390 ol_puts("verdict=RED (key \"" as *u8); ol_puts(key) 1391 ol_puts("\" matched NOTHING -- wrong key or wrong file, NOT an empty library)\n" as *u8) 1392 return 1 1393 } 1394 if v == 2 { 1395 ol_puts("verdict=RED (" as *u8); ol_num(refused) 1396 ol_puts(" title(s) could not be written -- the ledger is INCOMPLETE; read the REFUSED lines above)\n" as *u8) 1397 return 4 1398 } 1399 ol_puts("verdict=GREEN\n" as *u8) 1400 return 0 1401} 1402 1403// SCAN POLICY: keep EVERY item that passed the structural filter. Extracted into a function purely so 1404// the default is TESTABLE -- the defect it replaces was a DEFAULT, not an algorithm, and a default 1405// that lives only inside main() cannot be pinned by a tooth. ★★★★★A POLICY THAT NO TEST CAN REACH 1406// WILL BE REVERTED BY THE NEXT PERSON WHO FINDS IT SURPRISING. 1407// The legacy 4th arg was "keepfirst" (any value meant keep); it is now inert, which is the safe 1408// direction -- an old caller passing it gets the full list rather than a silently shortened one. 1409func ol_skipflag(argc: i64, arg3: *u8) -> i64 { 1410 if argc < 4 { return 0 } 1411 if (arg3 as i64) == 0 { return 0 } 1412 if ol_streq(arg3, "skipfirst" as *u8) == 1 { return 1 } 1413 return 0 1414} 1415 1416// do two spellings produce ONE match key? This is the whole product in one predicate: 1 means the 1417// checker will say OWNED for either spelling, 0 means one of them is a rebuy waiting to happen. 1418func ol_same_key(a: *u8, b: *u8) -> i64 { 1419 let ka: *u8 = sys_mmap(OL_TITLECAP) 1420 let kb: *u8 = sys_mmap(OL_TITLECAP) 1421 let na: i64 = ol_norm(a, ol_slen(a), ka, OL_TITLECAP - 2) 1422 let nb: i64 = ol_norm(b, ol_slen(b), kb, OL_TITLECAP - 2) 1423 return ol_bufeq(ka, na, kb, nb) 1424} 1425 1426// decode a whole JSON string BODY (no surrounding quotes) through the REAL escape path, so these 1427// teeth exercise ol_json_esc itself rather than a convenient copy of it. 1428func ol_dectest(src: *u8, dst: *u8, cap: i64) -> i64 { 1429 var sn: i64 = 0 1430 while src[sn] != (0 as u8) { sn = sn + 1 } 1431 let tnp: *i64 = sys_mmap(OL_SMALL) as *i64 1432 tnp[0] = 0 1433 var i: i64 = 0 1434 while i < sn { 1435 if src[i] == (92 as u8) { i = ol_json_esc(src, sn, i, dst, tnp, cap) } else { 1436 var n: i64 = tnp[0] 1437 if n < cap { dst[n] = src[i]; tnp[0] = n + 1 } 1438 i = i + 1 1439 } 1440 } 1441 dst[tnp[0]] = 0 as u8 1442 return tnp[0] 1443} 1444 1445func ol_selftest() -> i64 { 1446 let pass: *i64 = sys_mmap(OL_SMALL) as *i64 1447 let tot: *i64 = sys_mmap(OL_SMALL) as *i64 1448 pass[0] = 0 1449 tot[0] = 0 1450 ol_puts("=== nx_ownlib selftest -- does the match key survive the ways storefronts spell a title? ===\n" as *u8) 1451 ol_nrm_is("The Witcher 3: Wild Hunt" as *u8, "witcher 3 wild hunt" as *u8, pass, tot, "T1 article + punctuation" as *u8) 1452 ol_nrm_is("Witcher III Wild Hunt" as *u8, "witcher 3 wild hunt" as *u8, pass, tot, "T2 roman numeral folds to the SAME key as T1" as *u8) 1453 ol_nrm_is("Hollow Knight: Definitive Edition" as *u8, "hollow knight" as *u8, pass, tot, "T3 packaging words dropped" as *u8) 1454 ol_nrm_is("HOLLOW knight" as *u8, "hollow knight" as *u8, pass, tot, "T4 case folded" as *u8) 1455 ol_nrm_is("Final Fantasy VII" as *u8, "final fantasy 7" as *u8, pass, tot, "T5 FF VII == FF 7" as *u8) 1456 ol_nrm_is("V Rising" as *u8, "v rising" as *u8, pass, tot, "T6 LEADING numeral-letter NOT folded (V Rising is not 5 Rising)" as *u8) 1457 // the liar-killer: the match policy must REFUSE to call a sequel the same product 1458 let a: *u8 = sys_mmap(OL_TITLECAP) 1459 let b: *u8 = sys_mmap(OL_TITLECAP) 1460 let an: i64 = ol_norm("Hollow Knight" as *u8, 13, a, OL_TITLECAP - 2) 1461 let bn: i64 = ol_norm("Hollow Knight Silksong" as *u8, 22, b, OL_TITLECAP - 2) 1462 ol_t("T7 NEG-CONTROL: 'Hollow Knight' is NOT byte-equal to 'Hollow Knight Silksong'" as *u8, ol_bufeq(a, an, b, bn), 0, pass, tot) 1463 ol_t("T8 subset DOES fire between them (so it must only ever mean 'review', never 'owned')" as *u8, ol_subset(a, an, b, bn), 1, pass, tot) 1464 ol_t("T9 token match is whole-token, not substring ('art' does not match 'arts')" as *u8, ol_has_tok("arts crafts" as *u8, 11, "art" as *u8, 3), 0, pass, tot) 1465 ol_t("T10 whole-token match does fire on a real token" as *u8, ol_has_tok("arts crafts" as *u8, 11, "crafts" as *u8, 6), 1, pass, tot) 1466 ol_t("T11 fail-closed: unknown kind refused" as *u8, ol_kind_ok("dvd" as *u8), 0, pass, tot) 1467 ol_t("T12 fail-closed: unknown state refused" as *u8, ol_state_ok("maybe" as *u8), 0, pass, tot) 1468 ol_t("T13 fail-closed: tab in a field refused (TSV shape cannot be corrupted)" as *u8, ol_clean("bad\ttitle" as *u8), 0, pass, tot) 1469 // handler-registry teeth. The look-alike-host case is the one that matters: a bare substring 1470 // test would hand your order pages to evilhumblebundle.com, so the claim must be a registrable 1471 // DOMAIN SUFFIX match, and a test that cannot fail that way is decoration. 1472 let hb: *u8 = sys_mmap(OL_TITLECAP) 1473 let hbn: i64 = ol_url_host("https://www.humblebundle.com/home/library" as *u8, 41, hb, OL_TITLECAP - 2) 1474 ol_t("T14 url host extracted from a full url" as *u8, ol_bufeq(hb, hbn, "www.humblebundle.com" as *u8, 20), 1, pass, tot) 1475 ol_t("T15 subdomain claims the registrable domain" as *u8, ol_host_claims(hb, hbn, "humblebundle.com" as *u8, 16), 1, pass, tot) 1476 ol_t("T16 NEG-CONTROL: evilhumblebundle.com does NOT claim humblebundle.com" as *u8, ol_host_claims("evilhumblebundle.com" as *u8, 20, "humblebundle.com" as *u8, 16), 0, pass, tot) 1477 ol_t("T17 exact host claims itself" as *u8, ol_host_claims("humblebundle.com" as *u8, 16, "humblebundle.com" as *u8, 16), 1, pass, tot) 1478 ol_t("T18 path pattern matches inside the url" as *u8, ol_contains_b("https://www.humblebundle.com/home/library" as *u8, 41, "/home/library" as *u8, 13), 1, pass, tot) 1479 ol_t("T19 NEG-CONTROL: a path the url does not contain does not match" as *u8, ol_contains_b("https://www.humblebundle.com/home/library" as *u8, 41, "/orders" as *u8, 7), 0, pass, tot) 1480 // ---- T20..T28 JSON ESCAPE DECODING. Asserted as BYTE VALUES, never as source literals: a test 1481 // that embeds the character it checks can be silently rewritten by the very defect it exists to 1482 // catch, and would then agree with the bug. Numbers cannot be mangled in transit. 1483 let db: *u8 = sys_mmap(OL_TITLECAP) 1484 let dn1: i64 = ol_dectest("\\u00e9" as *u8, db, OL_TITLECAP - 2) 1485 ol_t("T20 \\u00e9 decodes to 2 UTF-8 bytes (not the 5 literal chars u00e9)" as *u8, dn1, 2, pass, tot) 1486 ol_t("T21 ... and those bytes are exactly C3 A9" as *u8, 1487 (db[0] as i64) * 1000 + (db[1] as i64), 195 * 1000 + 169, pass, tot) 1488 // THE REGRESSION TOOTH: the defect this replaced emitted a literal 'u' first. If that ever comes 1489 // back, this fires even if the length check above were somehow satisfied. 1490 ol_t("T22 REGRESSION: first decoded byte is NOT a literal 'u'" as *u8, 1491 db[0] as i64, 195, pass, tot) 1492 let dn2: i64 = ol_dectest("\\u2605" as *u8, db, OL_TITLECAP - 2) 1493 ol_t("T23 3-byte BMP codepoint decodes to 3 bytes" as *u8, dn2, 3, pass, tot) 1494 ol_t("T24 ... bytes are E2 98 85" as *u8, 1495 (db[0] as i64) * 1000000 + (db[1] as i64) * 1000 + (db[2] as i64), 1496 226 * 1000000 + 152 * 1000 + 133, pass, tot) 1497 // SURROGATE PAIR: a BMP-only decoder produces garbage here, and astral characters DO appear in 1498 // real storefront titles, so this is a live case and not a curiosity. 1499 let dn3: i64 = ol_dectest("\\ud83c\\udf89" as *u8, db, OL_TITLECAP - 2) 1500 ol_t("T25 surrogate pair decodes to ONE 4-byte character" as *u8, dn3, 4, pass, tot) 1501 ol_t("T26 ... and it is U+1F389 (F0 9F 8E 89)" as *u8, 1502 (db[0] as i64) * 1000000 + (db[1] as i64) * 1000 + (db[2] as i64), 1503 240 * 1000000 + 159 * 1000 + 142, pass, tot) 1504 let dn4: i64 = ol_dectest("a\\nb\\tc\\\\d\\\"e" as *u8, db, OL_TITLECAP - 2) 1505 ol_t("T27 the simple two-char escapes still decode (9 bytes out)" as *u8, dn4, 9, pass, tot) 1506 // MALFORMED INPUT MUST NOT EAT THE REST OF THE TITLE. A decoder that swallows on bad input hides 1507 // whole purchases from the ledger, which fails in the same direction as the bug above: toward NEW. 1508 let dn5: i64 = ol_dectest("\\uZZZZok" as *u8, db, OL_TITLECAP - 2) 1509 ol_t("T28 malformed \\u emits a literal u and KEEPS the following text" as *u8, dn5, 7, pass, tot) 1510 // ---- T29..T31 THE SCAN DEFAULT. This pins a REGRESSION THAT COST A REAL ITEM on a live page: 1511 // the default used to drop the first item, so a bundle of 30 games was reported as 29 and one 1512 // title simply never appeared. The bug was a DEFAULT, so the tooth asserts the DEFAULT. 1513 ol_t("T29 default keeps EVERY item (no positional skip)" as *u8, ol_skipflag(3, 0 as *u8), 0, pass, tot) 1514 ol_t("T30 skipping is opt-in and explicit" as *u8, ol_skipflag(4, "skipfirst" as *u8), 1, pass, tot) 1515 ol_t("T31 NEG-CONTROL: the legacy 'keepfirst' arg does NOT re-enable skipping" as *u8, 1516 ol_skipflag(4, "keepfirst" as *u8), 0, pass, tot) 1517 // ---- T32..T35 THE INGEST VERDICT. Pins a defect caught by RUNNING the thing: every append 1518 // failed and it reported GREEN. T34 is the one that matters and it is a REGRESSION tooth. 1519 ol_t("T32 a clean run is GREEN" as *u8, ol_ingest_verdict(10, 0), 0, pass, tot) 1520 ol_t("T33 nothing matched -> RED (wrong key is not an empty library)" as *u8, ol_ingest_verdict(0, 0), 1, pass, tot) 1521 ol_t("T34 REGRESSION: writes failed -> RED even though titles WERE found" as *u8, ol_ingest_verdict(3, 2), 2, pass, tot) 1522 ol_t("T35 NEG-CONTROL: one refusal is enough to withhold GREEN" as *u8, ol_ingest_verdict(1000, 1), 2, pass, tot) 1523 // ---- T36..T45 UNICODE FOLDING IN THE MATCH KEY. Pins a MEASURED rebuy: the ledger held 1524 // "Ōkami HD" and `check "Okami HD"` answered NOT-OWNED. 1525 // T36/T37 assert the TABLE LENGTHS. A lookup table is indexed arithmetic: one missing byte 1526 // silently shifts every codepoint after it and yields plausible WRONG letters, which is far 1527 // harder to notice than a crash. ★★★★★A TABLE WHOSE LENGTH IS NOT ASSERTED IS A SILENT OFF-BY-ONE 1528 // WAITING FOR ITS FIRST ACCENTED TITLE. 1529 ol_t("T36 Latin-1 fold table is exactly 128 B (2 x 64 codepoints)" as *u8, ol_slen(OL_FOLD_L1), 128, pass, tot) 1530 ol_t("T37 Latin-Ext-A fold table is exactly 256 B (2 x 128 codepoints)" as *u8, ol_slen(OL_FOLD_LA), 256, pass, tot) 1531 let fb2: *u8 = sys_mmap(OL_SMALL) 1532 ol_t("T38 U+014C O-macron folds to exactly one letter" as *u8, ol_fold(332, fb2), 1, pass, tot) 1533 ol_t("T39 ... and that letter is 'o'" as *u8, fb2[0] as i64, 111, pass, tot) 1534 ol_t("T40 U+00E9 e-acute folds to 'e'" as *u8, ol_fold(233, fb2) * 1000 + (fb2[0] as i64), 1101, pass, tot) 1535 ol_t("T41 U+00DF folds to TWO letters starting 's' (Strasse must match Strasse)" as *u8, 1536 ol_fold(223, fb2) * 1000 + (fb2[0] as i64), 2115, pass, tot) 1537 ol_t("T42 NEG-CONTROL: a codepoint with NO fold reports 0, never a blank letter" as *u8, ol_fold(19968, fb2), 0, pass, tot) 1538 // THE REBUY TOOTH, assembled from BYTES so no editor, transport or encoding can weaken it. 1539 let m1: *u8 = sys_mmap(OL_TITLECAP) 1540 let k1: *u8 = sys_mmap(OL_TITLECAP) 1541 let k2: *u8 = sys_mmap(OL_TITLECAP) 1542 m1[0] = 197 as u8; m1[1] = 140 as u8 // U+014C in UTF-8 1543 m1[2] = 107 as u8; m1[3] = 97 as u8; m1[4] = 109 as u8; m1[5] = 105 as u8 // kami 1544 m1[6] = 32 as u8; m1[7] = 72 as u8; m1[8] = 68 as u8; m1[9] = 0 as u8 // " HD" 1545 let k1n: i64 = ol_norm(m1, 9, k1, OL_TITLECAP - 2) 1546 let k2n: i64 = ol_norm("Okami HD" as *u8, 8, k2, OL_TITLECAP - 2) 1547 ol_t("T43 REBUY: macron title and plain title share ONE match key" as *u8, ol_bufeq(k1, k1n, k2, k2n), 1, pass, tot) 1548 // CJK has no ASCII fold, so the ONLY safe behaviour is to keep the bytes. Dropping them made an 1549 // all-CJK title normalize to EMPTY, and empty keys collide -- a FALSE OWNED, which is the worse 1550 // direction: it tells you to skip buying something you do not actually have. 1551 let c1: *u8 = sys_mmap(OL_TITLECAP) 1552 let c2: *u8 = sys_mmap(OL_TITLECAP) 1553 c1[0] = 228 as u8; c1[1] = 184 as u8; c1[2] = 128 as u8; c1[3] = 0 as u8 // U+4E00 1554 c2[0] = 228 as u8; c2[1] = 186 as u8; c2[2] = 140 as u8; c2[3] = 0 as u8 // U+4E8C 1555 let g1: i64 = ol_norm(c1, 3, k1, OL_TITLECAP - 2) 1556 let g2: i64 = ol_norm(c2, 3, k2, OL_TITLECAP - 2) 1557 ol_t("T44 CJK is PRESERVED not dropped (3-byte key, not empty)" as *u8, g1, 3, pass, tot) 1558 ol_t("T45 NEG-CONTROL: two different CJK titles do NOT collide" as *u8, ol_bufeq(k1, g1, k2, g2), 0, pass, tot) 1559 // ---- T46..T54 THE VARIANT SWEEP, pinned. Each of these was a MEASURED disagreement between the 1560 // storefront's spelling and the one a human types -- i.e. a rebuy -- found by sweeping the input 1561 // space rather than by thinking of cases. ★★★★★★**THE CASE THAT MOTIVATED A FIX IS THE ONE CASE 1562 // GUARANTEED TO PASS AFTERWARDS; ONLY A SWEEP FINDS THE REST.** 1563 // T47/T48 are the REGRESSION teeth for a bug I introduced myself: making unfoldable codepoints 1564 // keep their bytes was right for CJK letters and wrong for symbols. 1565 ol_t("T46 CJK counts as a letter (keep its bytes)" as *u8, ol_is_letter_cp(19968), 1, pass, tot) 1566 ol_t("T47 REGRESSION: U+2122 trademark is NOT a letter (must vanish)" as *u8, ol_is_letter_cp(8482), 0, pass, tot) 1567 ol_t("T48 REGRESSION: U+00AE registered is NOT a letter (must vanish)" as *u8, ol_is_letter_cp(174), 0, pass, tot) 1568 ol_t("T49 apostrophe is transparent: Baldur's Gate 3 == Baldurs Gate 3" as *u8, 1569 ol_same_key("Baldur's Gate 3" as *u8, "Baldurs Gate 3" as *u8), 1, pass, tot) 1570 ol_t("T50 dots are transparent: S.T.A.L.K.E.R. == STALKER" as *u8, 1571 ol_same_key("S.T.A.L.K.E.R." as *u8, "STALKER" as *u8), 1, pass, tot) 1572 ol_t("T51 ampersand spells out: Command & Conquer == Command and Conquer" as *u8, 1573 ol_same_key("Command & Conquer" as *u8, "Command and Conquer" as *u8), 1, pass, tot) 1574 // NEG-CONTROLS: every rule above LOOSENS matching, and a loosening that goes too far produces a 1575 // FALSE OWNED -- you skip buying something you do not have. These must stay DIFFERENT. 1576 ol_t("T52 NEG-CONTROL: transparency did NOT merge Portal and Portal 2" as *u8, 1577 ol_same_key("Portal" as *u8, "Portal 2" as *u8), 0, pass, tot) 1578 ol_t("T53 NEG-CONTROL: nor Hollow Knight and Hollow Knight Silksong" as *u8, 1579 ol_same_key("Hollow Knight" as *u8, "Hollow Knight Silksong" as *u8), 0, pass, tot) 1580 ol_t("T54 NEG-CONTROL: nor Final Fantasy VII and VIII" as *u8, 1581 ol_same_key("Final Fantasy VII" as *u8, "Final Fantasy VIII" as *u8), 0, pass, tot) 1582 // ---- T55..T59 ADVICE PRECEDENCE. T55 is the REGRESSION tooth for a MEASURED contradiction: 1583 // an owned title with no recorded offers advised UNKNOWN while the line above it said OWNED. 1584 ol_t("T55 REGRESSION: OWNED with NO price history still says DO-NOT-BUY" as *u8, 1585 ol_advice_code(1, 0, 0), OL_ADV_DONTBUY, pass, tot) 1586 ol_t("T56 OWNED with price history says DO-NOT-BUY" as *u8, ol_advice_code(2, 5, 3), OL_ADV_DONTBUY, pass, tot) 1587 ol_t("T57 not owned + no history = UNKNOWN (one quote is not a baseline)" as *u8, 1588 ol_advice_code(0, 0, 0), OL_ADV_UNKNOWN, pass, tot) 1589 ol_t("T58 not owned + nothing cheaper seen = BUY" as *u8, ol_advice_code(0, 3, 0), OL_ADV_BUY, pass, tot) 1590 ol_t("T59 NEG-CONTROL: not owned + seen cheaper = WAIT, never BUY" as *u8, 1591 ol_advice_code(0, 3, 2), OL_ADV_WAIT, pass, tot) 1592 ol_puts("---- nx_ownlib selftest " as *u8); ol_num(pass[0]); ol_puts(" / " as *u8); ol_num(tot[0]); ol_puts(" ----\n" as *u8) 1593 if pass[0] == tot[0] { ol_puts("verdict=GREEN\n" as *u8); return 0 } 1594 ol_puts("verdict=RED\n" as *u8) 1595 return 1 1596} 1597func main(argc: i64, argv: *i64) -> i64 { 1598 if argc < 2 { 1599 ol_puts("usage: nx_ownlib add <kind> <store> <state> <title> [ext] [note] | check <title...> | list [kind] | stats | selftest\n" as *u8) 1600 sys_exit(0); return 0 1601 } 1602 let verb: *u8 = argv[1] as *u8 1603 if ol_streq(verb, "selftest" as *u8) == 1 { let rc: i64 = ol_selftest(); sys_exit(rc); return rc } 1604 if ol_streq(verb, "stats" as *u8) == 1 { let rc: i64 = ol_stats(); sys_exit(rc); return rc } 1605 if ol_streq(verb, "list" as *u8) == 1 { 1606 var f: *u8 = 0 as *u8 1607 if argc >= 3 { f = argv[2] as *u8 } 1608 let rc: i64 = ol_list(f) 1609 sys_exit(rc); return rc 1610 } 1611 if ol_streq(verb, "add" as *u8) == 1 { 1612 if argc < 6 { ol_puts("usage: nx_ownlib add <kind> <store> <state> <title> [ext] [note]\n" as *u8); sys_exit(2); return 2 } 1613 var ext: *u8 = "-" as *u8 1614 var note: *u8 = "-" as *u8 1615 if argc >= 7 { ext = argv[6] as *u8 } 1616 if argc >= 8 { note = argv[7] as *u8 } 1617 let rc: i64 = ol_add(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, ext, note) 1618 sys_exit(rc); return rc 1619 } 1620 if ol_streq(verb, "check" as *u8) == 1 { 1621 if argc < 3 { ol_puts("usage: nx_ownlib check <title words...>\n" as *u8); sys_exit(2); return 2 } 1622 let q: *u8 = sys_mmap(OL_TITLECAP) 1623 var o: i64 = 0 1624 var i: i64 = 2 1625 while i < argc { 1626 if o > 0 { q[o] = OL_SP as u8; o = o + 1 } 1627 o = ol_cat(q, o, argv[i] as *u8) 1628 i = i + 1 1629 } 1630 q[o] = 0 as u8 1631 // ol_check returns an ownership LEVEL (0/1/2) or -1 refused. Exit 0 unless refused: a level is 1632 // information, not an error -- a caller must never read "you own this" as a failed command. 1633 let rc: i64 = ol_check(q, o) 1634 if rc < 0 { sys_exit(2); return 2 } 1635 sys_exit(0); return 0 1636 } 1637 if ol_streq(verb, "offer" as *u8) == 1 { 1638 if argc < 5 { ol_puts("usage: nx_ownlib offer <store> <title> <price_cents> [currency]\n" as *u8); sys_exit(2); return 2 } 1639 var cur: *u8 = "USD" as *u8 1640 if argc >= 6 { cur = argv[5] as *u8 } 1641 let rc: i64 = ol_offer(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, cur) 1642 sys_exit(rc); return rc 1643 } 1644 if ol_streq(verb, "advise" as *u8) == 1 { 1645 if argc < 4 { ol_puts("usage: nx_ownlib advise <price_cents> <title words...>\n" as *u8); sys_exit(2); return 2 } 1646 let aq: *u8 = sys_mmap(OL_TITLECAP) 1647 var ao: i64 = 0 1648 var ai: i64 = 3 1649 while ai < argc { 1650 if ao > 0 { aq[ao] = OL_SP as u8; ao = ao + 1 } 1651 ao = ol_cat(aq, ao, argv[ai] as *u8) 1652 ai = ai + 1 1653 } 1654 aq[ao] = 0 as u8 1655 let rc: i64 = ol_advise(argv[2] as *u8, aq, ao) 1656 sys_exit(rc); return rc 1657 } 1658 if ol_streq(verb, "scan" as *u8) == 1 { 1659 if argc < 3 { ol_puts("usage: nx_ownlib scan <fetched-page-file> [skipfirst]\n" as *u8); sys_exit(2); return 2 } 1660 var a3: *u8 = 0 as *u8 1661 if argc >= 4 { a3 = argv[3] as *u8 } 1662 let rc: i64 = ol_scan(argv[2] as *u8, ol_skipflag(argc, a3)) 1663 sys_exit(rc); return rc 1664 } 1665 // `key` exposes the MATCH KEY for a title. Every wrong answer this organ can give is ultimately 1666 // "two spellings produced different keys" (false NEW -> rebuy) or "two titles produced the same 1667 // key" (false OWNED -> you skip something you don't have). Without a way to SEE the key those are 1668 // both unfalsifiable folklore. ★A MATCHER WITH NO WAY TO PRINT ITS KEY CANNOT BE DEBUGGED, ONLY 1669 // ARGUED WITH -- and it is the instrument that makes a variant sweep possible at all. 1670 if ol_streq(verb, "key" as *u8) == 1 { 1671 if argc < 3 { ol_puts("usage: nx_ownlib key <title...>\n" as *u8); sys_exit(2); return 2 } 1672 let jb: *u8 = sys_mmap(OL_TITLECAP) 1673 var jn: i64 = 0 1674 var ai: i64 = 2 1675 while ai < argc { 1676 if jn > 0 { if jn < (OL_TITLECAP - 2) { jb[jn] = OL_SP as u8; jn = jn + 1 } } 1677 jn = ol_cat(jb, jn, argv[ai] as *u8) 1678 ai = ai + 1 1679 } 1680 jb[jn] = 0 as u8 1681 let kb: *u8 = sys_mmap(OL_TITLECAP) 1682 let kbn: i64 = ol_norm(jb, jn, kb, OL_TITLECAP - 2) 1683 ol_w(kb, kbn); ol_puts("\n" as *u8) 1684 sys_exit(0); return 0 1685 } 1686 if ol_streq(verb, "ingest" as *u8) == 1 { 1687 if argc < 7 { 1688 ol_puts("usage: nx_ownlib ingest <kind> <store> <state> <export.json> <title-key>\n" as *u8) 1689 ol_puts(" steam : ingest game steam redeemed steam_owned.json name\n" as *u8) 1690 // Humble rows default to UNREDEEMED on purpose: a bundle hands you a KEY, and until it is 1691 // redeemed no storefront can see it. That gap IS the rebuy this organ exists to prevent. 1692 ol_puts(" humble: ingest game humble unredeemed humble_orders.json human_name\n" as *u8) 1693 sys_exit(2); return 2 1694 } 1695 let rc: i64 = ol_ingest(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8) 1696 sys_exit(rc); return rc 1697 } 1698 if ol_streq(verb, "handler" as *u8) == 1 { 1699 if argc < 3 { ol_puts("usage: nx_ownlib handler add <id> <host> <pathpat> <kind> <store> [hint] | list | match <url>\n" as *u8); sys_exit(2); return 2 } 1700 let sub: *u8 = argv[2] as *u8 1701 if ol_streq(sub, "list" as *u8) == 1 { let rc: i64 = ol_handler_scan("" as *u8, 0, 1); sys_exit(rc); return rc } 1702 if ol_streq(sub, "match" as *u8) == 1 { 1703 if argc < 4 { ol_puts("usage: nx_ownlib handler match <url>\n" as *u8); sys_exit(2); return 2 } 1704 let u: *u8 = argv[3] as *u8 1705 let rc: i64 = ol_handler_scan(u, ol_slen(u), 0) 1706 sys_exit(rc); return rc 1707 } 1708 if ol_streq(sub, "add" as *u8) == 1 { 1709 if argc < 8 { ol_puts("usage: nx_ownlib handler add <id> <host> <pathpat> <kind> <store> [hint]\n" as *u8); sys_exit(2); return 2 } 1710 var hint: *u8 = "-" as *u8 1711 if argc >= 9 { hint = argv[8] as *u8 } 1712 let rc: i64 = ol_handler_add(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8, argv[7] as *u8, hint) 1713 sys_exit(rc); return rc 1714 } 1715 ol_puts("unknown handler sub (add|list|match)\n" as *u8); sys_exit(2); return 2 1716 } 1717 ol_puts("unknown verb\n" as *u8) 1718 sys_exit(2) 1719 return 2 1720} 1721