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}