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