code wiki / (root) / nx_artlicense.nx

nx_artlicense.nx source

↩ module page · 337 lines · 16340 B

1// nx_artlicense.nx -- THE LICENSE CHAIN AND THE ROYALTY LEDGER AS DATA (2026-08-24, /compare/phototwin PT4). 2// license_tier: ORIGINAL No hw writes (Rule 26). 3// 4// nx_artlicense check <sku> [now_epoch] [territory] [conf] -> 0 SHIP-OK | 3 REVIEW | 4 REFUSE | 2 usage 5// nx_artlicense royalty <ledger.tsv> <sku> <sale_id> <sale_cents> [conf] -> appends ONE row (idempotent on sale_id) 6// nx_artlicense total <ledger.tsv> <sku> -> royalty cents owed for the sku 7// 8// THE DEFECT IT PREVENTS. A garment print is a REPRODUCTION of the photograph, the gamut-mapped twin is a DERIVATIVE 9// work, and selling it is DISTRIBUTION -- three of the exclusive rights the copyright owner holds by default 10// (knowledge/fetched/cmp_phototwin_copyright.html). A SKU carries those rights only through a chain row that grants 11// all three, inside its term, inside its territory, with the artist's approval of the soft proof on record. 12// FAIL-CLOSED like nx_licgate: no chain, an expired or not-yet-started term, a missing right, or an unknown 13// territory against a restricted chain -> REFUSE (4). A chain without proof approval -> REVIEW (3): the artist, 14// not a machine, settles that. Silence is never permission. The exit vocabulary is nx_licgate's (0/3/4) so one 15// caller can compose both verdicts with one rule. 16// 17// CHAIN ROWS (knowledge/artlicense.conf), pipe-separated, one per SKU: 18// chain|<sku>|<owner>|<rights letters from r d s>|<term_start_epoch>|<term_end_epoch>|<territories csv or *>|<royalty_permil>|<proof_approved yes or no> 19// ROYALTY LEDGER (tab-separated, APPEND-ONLY -- history is sacred, rule 13): epoch sku sale_id sale_cents royalty_cents owner 20// royalty = sale_cents * permil / 1000 in INTEGER CENTS (floor), never float. 21import "nx_syscalls.nx" 22 23const AL_EXIT_OK: i64 = 0 24const AL_EXIT_USAGE: i64 = 2 25const AL_EXIT_REVIEW: i64 = 3 26const AL_EXIT_REFUSE: i64 = 4 27const AL_CONF_DEFAULT: *u8 = "knowledge/artlicense.conf" 28const AL_PIPE: i64 = 124 29const AL_NL: i64 = 10 30const AL_HASH: i64 = 35 31const AL_TAB: i64 = 9 32const AL_COMMA: i64 = 44 33const AL_STAR: i64 = 42 34const AL_ASCII_ZERO: i64 = 48 35const AL_ASCII_NINE: i64 = 57 36const AL_DECIMAL: i64 = 10 37const AL_F_SKU: i64 = 1 38const AL_F_OWNER: i64 = 2 39const AL_F_RIGHTS: i64 = 3 40const AL_F_START: i64 = 4 41const AL_F_END: i64 = 5 42const AL_F_TERR: i64 = 6 43const AL_F_PERMIL: i64 = 7 44const AL_F_APPROVED: i64 = 8 45const AL_FIELDS: i64 = 9 46const AL_RIGHT_REPRODUCE: i64 = 114 // r 47const AL_RIGHT_DERIVE: i64 = 100 // d 48const AL_RIGHT_DISTRIBUTE: i64 = 115 // s 49const AL_PERMIL: i64 = 1000 50const AL_LINE_CAP: i64 = 1024 51const AL_MODE_0644: i64 = 420 52const AL_NOW_UNSET: i64 = 0 - 1 53const AL_LEDGER_F_SKU: i64 = 1 54const AL_LEDGER_F_SALE: i64 = 2 55const AL_LEDGER_F_ROYALTY: i64 = 4 56const AL_DUP: i64 = 0 - 1 57// reason codes 58const AL_R_OK: i64 = 0 59const AL_R_NO_CHAIN: i64 = 1 60const AL_R_NOT_STARTED: i64 = 2 61const AL_R_EXPIRED: i64 = 3 62const AL_R_RIGHT_MISSING: i64 = 4 63const AL_R_TERRITORY: i64 = 5 64const AL_R_NOT_APPROVED: i64 = 6 65const AL_R_MALFORMED: i64 = 7 66const AL_R_NO_CONF: i64 = 8 67const AL_OUT_REASON: i64 = 0 68const AL_OUT_PERMIL: i64 = 1 69const AL_OUT_OWNER_OFF: i64 = 2 70const AL_OUT_OWNER_LEN: i64 = 3 71const AL_OUT_SLOTS: i64 = 8 72const AL_SLOT: i64 = 8 73 74func al_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 75func al_wn(v: i64) -> i64 { 76 var m: i64 = v 77 if m < 0 { al_w("-" as *u8); m = 0 - m } 78 let t: *u8 = sys_mmap(32) 79 var k: i64 = 0 80 if m == 0 { t[0] = 48 as u8; k = 1 } 81 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 82 let o: *u8 = sys_mmap(32) 83 var i: i64 = 0 84 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 85 sys_write(1, o, k) 86 return 0 87} 88func al_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 } 89func al_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 90func al_atoi(s: *u8) -> i64 { 91 var v: i64 = 0 92 var i: i64 = 0 93 var neg: i64 = 0 94 if (s[0] as i64) == 45 { neg = 1; i = 1 } 95 while (s[i] as i64) >= AL_ASCII_ZERO { if (s[i] as i64) > AL_ASCII_NINE { if neg == 1 { return 0 - v } return v } v = v * AL_DECIMAL + ((s[i] as i64) - AL_ASCII_ZERO); i = i + 1 } 96 if neg == 1 { return 0 - v } 97 return v 98} 99func al_span_num(buf: *u8, off: i64, len: i64) -> i64 { 100 var v: i64 = 0 101 var i: i64 = 0 102 while i < len { let c: i64 = buf[off + i] as i64; if c >= AL_ASCII_ZERO { if c <= AL_ASCII_NINE { v = v * AL_DECIMAL + (c - AL_ASCII_ZERO) } } i = i + 1 } 103 return v 104} 105func al_span_eq(buf: *u8, off: i64, len: i64, s: *u8) -> i64 { 106 var i: i64 = 0 107 while i < len { if s[i] == (0 as u8) { return 0 } if buf[off + i] != s[i] { return 0 } i = i + 1 } 108 if s[len] != (0 as u8) { return 0 } 109 return 1 110} 111// field idx of line [s,e) split on sep -> box[0]=off box[1]=len; returns 1 if present 112func al_field(buf: *u8, s: i64, e: i64, idx: i64, sep: i64, box: *i64) -> i64 { 113 var p: i64 = s 114 var f: i64 = 0 115 while f < idx { 116 var go: i64 = 1 117 while go == 1 { if p >= e { go = 0 } else { if (buf[p] as i64) == sep { p = p + 1; go = 0 } else { p = p + 1 } } } 118 f = f + 1 119 } 120 if p > e { box[0] = 0; box[1] = 0; return 0 } 121 var q: i64 = p 122 var g2: i64 = 1 123 while g2 == 1 { if q >= e { g2 = 0 } else { if (buf[q] as i64) == sep { g2 = 0 } else { q = q + 1 } } } 124 box[0] = p 125 box[1] = q - p 126 return 1 127} 128func al_span_has_char(buf: *u8, off: i64, len: i64, c: i64) -> i64 { 129 var i: i64 = 0 130 while i < len { if (buf[off + i] as i64) == c { return 1 } i = i + 1 } 131 return 0 132} 133// csv territory list contains the token? ("*" grants everywhere) 134func al_territory_ok(buf: *u8, off: i64, len: i64, terr: *u8) -> i64 { 135 if len == 1 { if (buf[off] as i64) == AL_STAR { return 1 } } 136 if (terr as i64) == 0 { return 0 } 137 var p: i64 = off 138 let e: i64 = off + len 139 while p < e { 140 var q: i64 = p 141 while q < e { if (buf[q] as i64) == AL_COMMA { break } q = q + 1 } 142 if al_span_eq(buf, p, q - p, terr) == 1 { return 1 } 143 p = q + 1 144 } 145 return 0 146} 147 148// THE CHAIN VERDICT. buf/n = the conf bytes. out: reason, permil, owner span. Returns the exit code. 149func al_license_chain(buf: *u8, n: i64, sku: *u8, now: i64, terr: *u8, out: *i64) -> i64 { 150 let box: *i64 = sys_mmap(AL_SLOT * 2) as *i64 151 out[AL_OUT_REASON] = AL_R_NO_CHAIN 152 out[AL_OUT_PERMIL] = 0 153 out[AL_OUT_OWNER_OFF] = 0 154 out[AL_OUT_OWNER_LEN] = 0 155 var p: i64 = 0 156 while p < n { 157 var e: i64 = p 158 while e < n { if (buf[e] as i64) == AL_NL { break } e = e + 1 } 159 if e > p { if (buf[p] as i64) != AL_HASH { 160 al_field(buf, p, e, 0, AL_PIPE, box) 161 if al_span_eq(buf, box[0], box[1], "chain" as *u8) == 1 { 162 al_field(buf, p, e, AL_F_SKU, AL_PIPE, box) 163 if al_span_eq(buf, box[0], box[1], sku) == 1 { 164 // the row for this sku: every conjunct named 165 if al_field(buf, p, e, AL_F_APPROVED, AL_PIPE, box) == 0 { out[AL_OUT_REASON] = AL_R_MALFORMED; return AL_EXIT_REFUSE } 166 al_field(buf, p, e, AL_F_OWNER, AL_PIPE, box); out[AL_OUT_OWNER_OFF] = box[0]; out[AL_OUT_OWNER_LEN] = box[1] 167 al_field(buf, p, e, AL_F_PERMIL, AL_PIPE, box); out[AL_OUT_PERMIL] = al_span_num(buf, box[0], box[1]) 168 al_field(buf, p, e, AL_F_START, AL_PIPE, box) 169 let start: i64 = al_span_num(buf, box[0], box[1]) 170 al_field(buf, p, e, AL_F_END, AL_PIPE, box) 171 let fin: i64 = al_span_num(buf, box[0], box[1]) 172 if now != AL_NOW_UNSET { 173 if now < start { out[AL_OUT_REASON] = AL_R_NOT_STARTED; return AL_EXIT_REFUSE } 174 if now > fin { out[AL_OUT_REASON] = AL_R_EXPIRED; return AL_EXIT_REFUSE } 175 } 176 al_field(buf, p, e, AL_F_RIGHTS, AL_PIPE, box) 177 var rights: i64 = 1 178 if al_span_has_char(buf, box[0], box[1], AL_RIGHT_REPRODUCE) == 0 { rights = 0 } 179 if al_span_has_char(buf, box[0], box[1], AL_RIGHT_DERIVE) == 0 { rights = 0 } 180 if al_span_has_char(buf, box[0], box[1], AL_RIGHT_DISTRIBUTE) == 0 { rights = 0 } 181 if rights == 0 { out[AL_OUT_REASON] = AL_R_RIGHT_MISSING; return AL_EXIT_REFUSE } 182 al_field(buf, p, e, AL_F_TERR, AL_PIPE, box) 183 if al_territory_ok(buf, box[0], box[1], terr) == 0 { out[AL_OUT_REASON] = AL_R_TERRITORY; return AL_EXIT_REFUSE } 184 al_field(buf, p, e, AL_F_APPROVED, AL_PIPE, box) 185 if al_span_eq(buf, box[0], box[1], "yes" as *u8) == 0 { out[AL_OUT_REASON] = AL_R_NOT_APPROVED; return AL_EXIT_REVIEW } 186 out[AL_OUT_REASON] = AL_R_OK 187 return AL_EXIT_OK 188 } 189 } 190 } } 191 p = e + 1 192 } 193 return AL_EXIT_REFUSE 194} 195func al_reason_text(r: i64) -> *u8 { 196 if r == AL_R_OK { return "OK" as *u8 } 197 if r == AL_R_NO_CHAIN { return "NO-CHAIN (no chain row for this sku)" as *u8 } 198 if r == AL_R_NOT_STARTED { return "NOT-STARTED (term_start is in the future)" as *u8 } 199 if r == AL_R_EXPIRED { return "EXPIRED (term_end has passed)" as *u8 } 200 if r == AL_R_RIGHT_MISSING { return "RIGHT-MISSING (needs r reproduce, d derive, s distribute)" as *u8 } 201 if r == AL_R_TERRITORY { return "TERRITORY (sale territory not granted, or unknown against a restricted chain)" as *u8 } 202 if r == AL_R_NOT_APPROVED { return "NOT-APPROVED (the artist has not approved the soft proof: REVIEW, a person settles it)" as *u8 } 203 if r == AL_R_MALFORMED { return "MALFORMED (chain row lacks fields)" as *u8 } 204 if r == AL_R_NO_CONF { return "NO-CONF (chain file unreadable)" as *u8 } 205 return "UNKNOWN" as *u8 206} 207 208// THE ROYALTY LEDGER: append one row unless sale_id already exists for the sku (idempotent). Returns royalty cents, AL_DUP on repeat. 209func al_royalty_ledger(ledger: *u8, sku: *u8, sale_id: *u8, sale_cents: i64, permil: i64, owner_buf: *u8, owner_off: i64, owner_len: i64, now: i64) -> i64 { 210 let royalty: i64 = (sale_cents * permil) / AL_PERMIL 211 let lp: *i64 = sys_mmap(AL_SLOT * 2) as *i64 212 let b: *u8 = sys_read_file(ledger, lp) 213 let box: *i64 = sys_mmap(AL_SLOT * 2) as *i64 214 if (b as i64) != 0 { 215 let n: i64 = lp[0] 216 var p: i64 = 0 217 while p < n { 218 var e: i64 = p 219 while e < n { if (b[e] as i64) == AL_NL { break } e = e + 1 } 220 if e > p { if (b[p] as i64) != AL_HASH { 221 al_field(b, p, e, AL_LEDGER_F_SKU, AL_TAB, box) 222 if al_span_eq(b, box[0], box[1], sku) == 1 { 223 al_field(b, p, e, AL_LEDGER_F_SALE, AL_TAB, box) 224 if al_span_eq(b, box[0], box[1], sale_id) == 1 { return AL_DUP } 225 } 226 } } 227 p = e + 1 228 } 229 } 230 let line: *u8 = sys_mmap(AL_LINE_CAP) 231 var o: i64 = 0 232 o = al_catn(line, o, now); line[o] = AL_TAB as u8; o = o + 1 233 o = al_cat(line, o, sku); line[o] = AL_TAB as u8; o = o + 1 234 o = al_cat(line, o, sale_id); line[o] = AL_TAB as u8; o = o + 1 235 o = al_catn(line, o, sale_cents); line[o] = AL_TAB as u8; o = o + 1 236 o = al_catn(line, o, royalty); line[o] = AL_TAB as u8; o = o + 1 237 var i: i64 = 0 238 while i < owner_len { line[o] = owner_buf[owner_off + i]; o = o + 1; i = i + 1 } 239 line[o] = AL_NL as u8; o = o + 1 240 let fd: i64 = sys_openat_append(ledger, AL_MODE_0644) 241 if fd < 0 { return 0 - 2 } 242 sys_write(fd, line, o) 243 sys_close(fd) 244 return royalty 245} 246func al_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p } 247func al_catn(d: *u8, o: i64, v: i64) -> i64 { 248 var m: i64 = v 249 var p: i64 = o 250 if m < 0 { d[p] = 45 as u8; p = p + 1; m = 0 - m } 251 let t: *u8 = sys_mmap(32) 252 var k: i64 = 0 253 if m == 0 { t[0] = 48 as u8; k = 1 } 254 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 255 var i: i64 = 0 256 while i < k { d[p] = t[k - 1 - i]; p = p + 1; i = i + 1 } 257 return p 258} 259func al_total(ledger: *u8, sku: *u8, rows_out: *i64) -> i64 { 260 let lp: *i64 = sys_mmap(AL_SLOT * 2) as *i64 261 let b: *u8 = sys_read_file(ledger, lp) 262 rows_out[0] = 0 263 if (b as i64) == 0 { return 0 } 264 let box: *i64 = sys_mmap(AL_SLOT * 2) as *i64 265 let n: i64 = lp[0] 266 var total: i64 = 0 267 var p: i64 = 0 268 while p < n { 269 var e: i64 = p 270 while e < n { if (b[e] as i64) == AL_NL { break } e = e + 1 } 271 if e > p { if (b[p] as i64) != AL_HASH { 272 al_field(b, p, e, AL_LEDGER_F_SKU, AL_TAB, box) 273 if al_span_eq(b, box[0], box[1], sku) == 1 { 274 al_field(b, p, e, AL_LEDGER_F_ROYALTY, AL_TAB, box) 275 total = total + al_span_num(b, box[0], box[1]) 276 rows_out[0] = rows_out[0] + 1 277 } 278 } } 279 p = e + 1 280 } 281 return total 282} 283 284func al_usage() -> i64 { 285 al_w("usage: nx_artlicense check <sku> [now_epoch] [territory] [conf] | royalty <ledger.tsv> <sku> <sale_id> <sale_cents> [conf] | total <ledger.tsv> <sku>\n" as *u8) 286 return AL_EXIT_USAGE 287} 288 289func main(argc: i64, argv: *i64) -> i64 { 290 if argc < 3 { return al_usage() } 291 let verb: *u8 = argv[1] as *u8 292 if al_streq(verb, "total" as *u8) == 1 { 293 if argc < 4 { return al_usage() } 294 let rows: *i64 = sys_mmap(AL_SLOT) as *i64 295 let t: i64 = al_total(argv[2] as *u8, argv[3] as *u8, rows) 296 al_w("sku=" as *u8); al_w(argv[3] as *u8); al_w(" rows=" as *u8); al_wn(rows[0]); al_w(" royalty_cents=" as *u8); al_wn(t); al_w("\n" as *u8) 297 return AL_EXIT_OK 298 } 299 var conf: *u8 = AL_CONF_DEFAULT 300 var now: i64 = AL_NOW_UNSET 301 var terr: *u8 = 0 as *u8 302 let sku_idx: i64 = 2 303 if al_streq(verb, "check" as *u8) == 1 { 304 if argc >= 4 { now = al_atoi(argv[3] as *u8) } 305 if argc >= 5 { terr = argv[4] as *u8 } 306 if argc >= 6 { conf = argv[5] as *u8 } 307 } 308 if al_streq(verb, "royalty" as *u8) == 1 { 309 if argc < 6 { return al_usage() } 310 if argc >= 7 { conf = argv[6] as *u8 } 311 } 312 if al_streq(verb, "check" as *u8) == 0 { if al_streq(verb, "royalty" as *u8) == 0 { return al_usage() } } 313 var sku: *u8 = argv[sku_idx] as *u8 314 if al_streq(verb, "royalty" as *u8) == 1 { sku = argv[3] as *u8 } 315 let lp: *i64 = sys_mmap(AL_SLOT * 2) as *i64 316 let b: *u8 = sys_read_file(conf, lp) 317 let out: *i64 = sys_mmap(AL_SLOT * AL_OUT_SLOTS) as *i64 318 if (b as i64) == 0 { al_w("sku=" as *u8); al_w(sku); al_w(" verdict=REFUSE reason=" as *u8); al_w(al_reason_text(AL_R_NO_CONF)); al_w("\n" as *u8); return AL_EXIT_REFUSE } 319 if now == AL_NOW_UNSET { now = sys_now_realtime_sec() } 320 let rc: i64 = al_license_chain(b, lp[0], sku, now, terr, out) 321 al_w("sku=" as *u8); al_w(sku); al_w(" now=" as *u8); al_wn(now) 322 if (terr as i64) != 0 { al_w(" territory=" as *u8); al_w(terr) } 323 al_w(" royalty_permil=" as *u8); al_wn(out[AL_OUT_PERMIL]) 324 if rc == AL_EXIT_OK { al_w(" verdict=SHIP-OK" as *u8) } 325 if rc == AL_EXIT_REVIEW { al_w(" verdict=REVIEW" as *u8) } 326 if rc == AL_EXIT_REFUSE { al_w(" verdict=REFUSE" as *u8) } 327 al_w(" reason=" as *u8); al_w(al_reason_text(out[AL_OUT_REASON])); al_w("\n" as *u8) 328 if al_streq(verb, "check" as *u8) == 1 { return rc } 329 // royalty: only a SHIP-OK chain earns a ledger row -- a refused sale must not book a royalty 330 if rc != AL_EXIT_OK { al_w("ledger=UNCHANGED (chain not SHIP-OK)\n" as *u8); return rc } 331 let cents: i64 = al_atoi(argv[5] as *u8) 332 let r: i64 = al_royalty_ledger(argv[2] as *u8, sku, argv[4] as *u8, cents, out[AL_OUT_PERMIL], b, out[AL_OUT_OWNER_OFF], out[AL_OUT_OWNER_LEN], now) 333 if r == AL_DUP { al_w("ledger=DUPLICATE sale_id already booked, nothing appended\n" as *u8); return AL_EXIT_OK } 334 if r < 0 { al_w("ledger=UNWRITABLE\n" as *u8); return AL_EXIT_REFUSE } 335 al_w("ledger=APPENDED sale_cents=" as *u8); al_wn(cents); al_w(" royalty_cents=" as *u8); al_wn(r); al_w("\n" as *u8) 336 return AL_EXIT_OK 337}