code wiki / _hdl_build / nx_card_compile.nx
nx_card_compile.nx source
↩ module page · 607 lines · 30986 B
1// nx_card_compile.nx -- CompanionCard COMPILER (F917 rung-1, "first byte up"): elara_card.json (the character
2// sheet SSOT) compiles DOWNWARD into elara_persona.txt, which the live gen daemon re-reads per request = the
3// card becomes the byte-one source of who she is, no daemon deploy needed. Fail-closed: age < 18 REFUSES
4// (adult-companion invariant enforced at the compiler, not by convention). Rule-13: the pre-card persona is
5// banked ONCE to elara_persona.txt.bak-precard. Write is ATOMIC (tmp + rename; the daemon never sees a torn
6// file). Composition keeps the battle-tested persona laws VERBATIM (never-AI, sends pics, EN-lock, first
7// person, no stage directions) -- the card supplies the variable identity/personality half.
8// usage: nx_card_compile [dir] (dir default /volume1/ai/gen; reads elara_card.json, writes elara_persona.txt)
9// nx_card_compile import <card.png> [age=N] [dir] (CC2: a Tavern V2 PNG card -> the SSOT, then compile)
10// nx_card_compile export <dir> <out.json> (CC2: the imported V2 source back out, byte-identical)
11// CC2 COMMUNITY CARD IMPORT (companionchat, 2026-08-30): a Tavern V2 card is base64 JSON in a tEXt chunk keyed `chara`
12// (nx_png_chunks walks it, base64.nx decodes it). Every V2 field is MAPPED into the SSOT or REFUSED BY NAME in a printed
13// ledger -- never silently dropped; the decoded JSON is kept VERBATIM as elara_card.v2.json so `export` round-trips
14// byte-stable by construction; the age gate stays fail-closed -- a V2 card carries no age field, so import REFUSES unless
15// the card declares one or the operator passes age=N, and N < 18 refuses exactly as the native compiler does. An imported
16// card composes from its own description/personality/scenario and NEVER inherits the native defaults (hair, eyes,
17// relationship) -- those are Elara's, not the imported character's.
18// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
19import "nx_syscalls.nx"
20import "nx_png_chunks.nx"
21import "base64.nx"
22const CC_MAGIC_1024: i64 = 1024
23
24const CC_OUTCAP: i64 = 65536
25const CC_STRCAP: i64 = 2048
26const CC_PATHCAP: i64 = 512
27const CC_MODE: i64 = 420
28const CC_MIN_AGE: i64 = 18
29const CC_NONE: i64 = 0 - 1
30// V2 prose fields (description, personality, scenario) run to kilobytes; one cap for each, and the SSOT emit is sized from them
31const CC_V2CAP: i64 = 16384
32const CC_V2_FIELDS: i64 = 12
33const CC_LEDGER_CAP: i64 = 4096
34const CC_EXIT_USAGE: i64 = 2
35const CC_EXIT_REFUSED: i64 = 3
36const CC_EXIT_IO: i64 = 4
37const CC_CH_QUOTE: i64 = 34
38const CC_CH_BSLASH: i64 = 92
39const CC_CH_SPACE: i64 = 32
40const CC_CH_NL: i64 = 10
41const CC_CH_COMMA: i64 = 44
42const CC_CH_0: i64 = 48
43const CC_CH_9: i64 = 57
44const CC_DEC_BASE: i64 = 10
45const CC_AGE_ARG: *u8 = "age=" as *u8
46const CC_V2_SPEC: *u8 = "chara_card_v2" as *u8
47const CC_V2_KEYWORD: *u8 = "chara" as *u8
48const CC_SSOT: *u8 = "elara_card.json" as *u8
49const CC_SSOT_BAK: *u8 = "elara_card.json.bak-preimport" as *u8
50const CC_V2_SRC: *u8 = "elara_card.v2.json" as *u8
51
52func ccm_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64 = off; var i: i64 = 0; while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } return o }
53func ccm_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
54func ccm_catn(d: *u8, o: i64, v: i64) -> i64 { let t: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { d[o] = 45 as u8; o = o + 1; m = 0 - m } var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } var i: i64 = 0; while i < k { d[o] = t[k-1-i]; o = o + 1; i = i + 1 } return o }
55func ccm_puts(s: *u8) -> i64 { sys_write(1, s, ccm_len(s)); return 0 }
56func ccm_eq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] == b[i] { if a[i] == (0 as u8) { return 1 } i = i + 1 } return 0 }
57
58func ccm_pathjoin(dst: *u8, dir: *u8, name: *u8) -> i64 {
59 var o: i64 = ccm_cat(dst, 0, dir)
60 dst[o] = 47 as u8
61 o = o + 1
62 o = ccm_cat(dst, o, name)
63 dst[o] = 0 as u8
64 return o
65}
66
67// naive substring find, -1 if absent
68func ccm_find(b: *u8, n: i64, pat: *u8) -> i64 {
69 let pl: i64 = ccm_len(pat)
70 if pl == 0 { return 0 - 1 }
71 var res: i64 = 0 - 1
72 var i: i64 = 0
73 while i + pl <= n {
74 var j: i64 = 0
75 var ok: i64 = 1
76 while j < pl { if (b[i+j] & 0xff) != (pat[j] & 0xff) { ok = 0; j = pl } else { j = j + 1 } }
77 if ok == 1 { res = i; i = n }
78 i = i + 1
79 }
80 return res
81}
82
83// position just past `"key"` or -1
84func ccm_key_at(b: *u8, n: i64, key: *u8) -> i64 {
85 let nd: *u8 = sys_mmap(128)
86 nd[0] = 34 as u8
87 var o: i64 = ccm_cat(nd, 1, key)
88 nd[o] = 34 as u8
89 nd[o+1] = 0 as u8
90 let at: i64 = ccm_find(b, n, nd)
91 if at < 0 { return 0 - 1 }
92 return at + o + 1
93}
94
95// extract string value of "key": "..." (minimal \" \\ unescape). -1 if absent/not-string.
96func ccm_json_str(b: *u8, n: i64, key: *u8, out: *u8, cap: i64) -> i64 {
97 var p: i64 = ccm_key_at(b, n, key)
98 if p < 0 { return 0 - 1 }
99 var seek: i64 = 1
100 var found: i64 = 0
101 while seek == 1 { if p >= n { seek = 0 } else { if (b[p] & 0xff) == 58 { seek = 0; found = 1; p = p + 1 } else { p = p + 1 } } }
102 if found == 0 { return 0 - 1 }
103 var skip: i64 = 1
104 while skip == 1 { if p >= n { skip = 0 } else { let c: i64 = b[p] & 0xff; if c == 32 { p = p + 1 } else { if c == 10 { p = p + 1 } else { skip = 0 } } } }
105 if p >= n { return 0 - 1 }
106 if (b[p] & 0xff) != 34 { return 0 - 1 }
107 p = p + 1
108 var o: i64 = 0
109 var run: i64 = 1
110 while run == 1 {
111 if p >= n { run = 0 } else {
112 let c: i64 = b[p] & 0xff
113 if c == 34 { run = 0 } else {
114 if c == 92 { p = p + 1; if p < n { if o < cap-1 { out[o] = b[p]; o = o + 1 } } } else {
115 if o < cap-1 { out[o] = b[p]; o = o + 1 }
116 }
117 p = p + 1
118 }
119 }
120 }
121 out[o] = 0 as u8
122 return o
123}
124
125// extract int value of "key": N (-1 if absent)
126func ccm_json_int(b: *u8, n: i64, key: *u8) -> i64 {
127 var p: i64 = ccm_key_at(b, n, key)
128 if p < 0 { return 0 - 1 }
129 var seek: i64 = 1
130 while seek == 1 { if p >= n { seek = 0 } else { if (b[p] & 0xff) == 58 { seek = 0 } else { p = p + 1 } } }
131 if p >= n { return 0 - 1 }
132 p = p + 1
133 var skip: i64 = 1
134 while skip == 1 { if p >= n { skip = 0 } else { let c: i64 = b[p] & 0xff; if c == 32 { p = p + 1 } else { skip = 0 } } }
135 var v: i64 = 0 - 1
136 var any: i64 = 0
137 var rd: i64 = 1
138 while rd == 1 {
139 if p >= n { rd = 0 } else {
140 let c: i64 = b[p] & 0xff
141 if c >= 48 { if c <= 57 { if any == 0 { v = 0; any = 1 } v = v * 10 + (c - 48); p = p + 1 } else { rd = 0 } } else { rd = 0 }
142 }
143 }
144 return v
145}
146
147// join string array "key": ["a","b"] with ", " into out. -1 if absent.
148func ccm_json_arr(b: *u8, n: i64, key: *u8, out: *u8, cap: i64) -> i64 {
149 var p: i64 = ccm_key_at(b, n, key)
150 if p < 0 { return 0 - 1 }
151 var seek: i64 = 1
152 while seek == 1 { if p >= n { seek = 0 } else { if (b[p] & 0xff) == 91 { seek = 0 } else { p = p + 1 } } }
153 if p >= n { return 0 - 1 }
154 p = p + 1
155 var o: i64 = 0
156 var first: i64 = 1
157 var run: i64 = 1
158 while run == 1 {
159 if p >= n { run = 0 } else {
160 let c: i64 = b[p] & 0xff
161 if c == 93 { run = 0 } else {
162 if c == 34 {
163 if first == 0 { if o < cap-3 { out[o] = 44 as u8; out[o+1] = 32 as u8; o = o + 2 } }
164 first = 0
165 p = p + 1
166 var s: i64 = 1
167 while s == 1 {
168 if p >= n { s = 0 } else {
169 let d: i64 = b[p] & 0xff
170 if d == 34 { s = 0 } else {
171 if d == 92 { p = p + 1; if p < n { if o < cap-1 { out[o] = b[p]; o = o + 1 } } } else { if o < cap-1 { out[o] = b[p]; o = o + 1 } }
172 p = p + 1
173 }
174 }
175 }
176 }
177 p = p + 1
178 }
179 }
180 }
181 out[o] = 0 as u8
182 return o
183}
184
185// copy s until ';' or end (drops technical tails from card fields), trim trailing spaces
186func ccm_first_seg(s: *u8, out: *u8, cap: i64) -> i64 {
187 var o: i64 = 0
188 var i: i64 = 0
189 var run: i64 = 1
190 while run == 1 {
191 let c: i64 = s[i] & 0xff
192 if c == 0 { run = 0 } else { if c == 59 { run = 0 } else { if o < cap-1 { out[o] = s[i]; o = o + 1 } i = i + 1 } }
193 }
194 while o > 0 { if (out[o-1] & 0xff) == 32 { o = o - 1 } else { return o } }
195 out[o] = 0 as u8
196 return o
197}
198
199func ccm_age_words(age: i64, d: *u8, off: i64) -> i64 {
200 if age == 18 { return ccm_cat(d, off, "eighteen" as *u8) }
201 if age == 19 { return ccm_cat(d, off, "nineteen" as *u8) }
202 if age == 20 { return ccm_cat(d, off, "twenty" as *u8) }
203 if age == 21 { return ccm_cat(d, off, "twenty-one" as *u8) }
204 if age == 22 { return ccm_cat(d, off, "twenty-two" as *u8) }
205 if age == 23 { return ccm_cat(d, off, "twenty-three" as *u8) }
206 if age == 24 { return ccm_cat(d, off, "twenty-four" as *u8) }
207 if age == 25 { return ccm_cat(d, off, "twenty-five" as *u8) }
208 return ccm_catn(d, off, age)
209}
210
211// JSON string emit: quotes and backslashes escaped, control bytes flattened to spaces (the SSOT is one line per value)
212func ccm_emit_jstr(dst: *u8, off: i64, s: *u8) -> i64 {
213 var o: i64 = off
214 dst[o] = CC_CH_QUOTE as u8; o = o + 1
215 var i: i64 = 0
216 while s[i] != (0 as u8) {
217 let c: i64 = s[i] & 0xff
218 if c == CC_CH_QUOTE { dst[o] = CC_CH_BSLASH as u8; o = o + 1; dst[o] = CC_CH_QUOTE as u8; o = o + 1 } else {
219 if c == CC_CH_BSLASH { dst[o] = CC_CH_BSLASH as u8; o = o + 1; dst[o] = CC_CH_BSLASH as u8; o = o + 1 } else {
220 if c < CC_CH_SPACE { dst[o] = CC_CH_SPACE as u8; o = o + 1 } else { dst[o] = s[i]; o = o + 1 }
221 }
222 }
223 i = i + 1
224 }
225 dst[o] = CC_CH_QUOTE as u8; o = o + 1
226 return o
227}
228// atomic file write: <path>.nxcc then rename; 0 ok, -1 cannot write, -2 cannot rename
229func ccm_write_atomic(path: *u8, body: *u8, n: i64) -> i64 {
230 let tmp: *u8 = sys_mmap(CC_PATHCAP)
231 var o: i64 = ccm_cat(tmp, 0, path)
232 o = ccm_cat(tmp, o, ".nxcc" as *u8)
233 tmp[o] = 0 as u8
234 let fd: i64 = sys_openat_wr(tmp, CC_MODE)
235 if fd < 0 { return 0 - 1 }
236 sys_write(fd, body, n)
237 sys_close(fd)
238 if sys_renameat(tmp, path) < 0 { return 0 - 2 }
239 return 0
240}
241// rule-13: bank <dir>/<name> to <dir>/<bakname> ONCE (never overwrite a bank). 1 banked, 0 nothing to bank / already banked
242func ccm_bank_once(dir: *u8, name: *u8, bakname: *u8) -> i64 {
243 let pbak: *u8 = sys_mmap(CC_PATHCAP)
244 ccm_pathjoin(pbak, dir, bakname)
245 let psrc: *u8 = sys_mmap(CC_PATHCAP)
246 ccm_pathjoin(psrc, dir, name)
247 let bl: *i64 = sys_mmap(16) as *i64
248 let bb: *u8 = sys_read_file(pbak, bl)
249 if (bb as i64) != 0 { return 0 }
250 let ol: *i64 = sys_mmap(16) as *i64
251 let ob: *u8 = sys_read_file(psrc, ol)
252 if (ob as i64) == 0 { return 0 }
253 if ol[0] <= 0 { return 0 }
254 let bfd: i64 = sys_openat_wr(pbak, CC_MODE)
255 if bfd < 0 { return 0 }
256 sys_write(bfd, ob, ol[0])
257 sys_close(bfd)
258 return 1
259}
260
261// append one prose fragment as a sentence: the fragment, a terminal mark only if it lacks one, then a space
262const CC_CH_DOT: i64 = 46
263const CC_CH_BANG: i64 = 33
264const CC_CH_QMARK: i64 = 63
265func ccm_cat_prose(dst: *u8, off: i64, s: *u8) -> i64 {
266 var o: i64 = ccm_cat(dst, off, s)
267 var trim: i64 = 1
268 while trim == 1 { if o > off { if (dst[o - 1] & 0xff) == CC_CH_SPACE { o = o - 1 } else { trim = 0 } } else { trim = 0 } }
269 if o > off {
270 let last: i64 = dst[o - 1] & 0xff
271 if last != CC_CH_DOT { if last != CC_CH_BANG { if last != CC_CH_QMARK { dst[o] = CC_CH_DOT as u8; o = o + 1 } } }
272 dst[o] = CC_CH_SPACE as u8
273 o = o + 1
274 }
275 return o
276}
277// THE COMPILER: <dir>/elara_card.json -> <dir>/elara_persona.txt. Returns the exit code (0 ok).
278func ccm_compile(dir: *u8) -> i64 {
279 let pcard: *u8 = sys_mmap(CC_PATHCAP)
280 ccm_pathjoin(pcard, dir, CC_SSOT)
281 let lp: *i64 = sys_mmap(16) as *i64
282 let card: *u8 = sys_read_file(pcard, lp)
283 if (card as i64) == 0 { ccm_puts("CARD-COMPILE FAIL: no elara_card.json in dir\n" as *u8); return CC_EXIT_USAGE }
284 let n: i64 = lp[0]
285 if n <= 0 { ccm_puts("CARD-COMPILE FAIL: empty card\n" as *u8); return CC_EXIT_USAGE }
286
287 let name: *u8 = sys_mmap(256)
288 if ccm_json_str(card, n, "name" as *u8, name, 256) <= 0 { ccm_cat(name, 0, "Elara" as *u8); name[5] = 0 as u8 }
289 var age: i64 = ccm_json_int(card, n, "age" as *u8)
290 if age < 0 { age = CC_MIN_AGE }
291 if age < CC_MIN_AGE { ccm_puts("CARD-COMPILE REFUSED: card age below 18 -- adult-companion invariant is fail-closed\n" as *u8); return CC_EXIT_REFUSED }
292 let source: *u8 = sys_mmap(64)
293 ccm_json_str(card, n, "source" as *u8, source, 64)
294 let imported: i64 = ccm_eq(source, CC_V2_SPEC)
295
296 let traits: *u8 = sys_mmap(CC_STRCAP)
297 let trn: i64 = ccm_json_arr(card, n, "core_traits" as *u8, traits, CC_STRCAP)
298 if trn <= 0 { if imported == 0 { ccm_cat(traits, 0, "warm, affectionate, playful, flirty" as *u8) } else { traits[0] = 0 as u8 } }
299 let speech: *u8 = sys_mmap(CC_STRCAP)
300 if ccm_json_str(card, n, "speech_style" as *u8, speech, CC_STRCAP) <= 0 { if imported == 0 { ccm_cat(speech, 0, "casual, warm, brief, emotionally present, a little flirty and teasing" as *u8) } else { speech[0] = 0 as u8 } }
301 let loves: *u8 = sys_mmap(CC_STRCAP)
302 let lvn: i64 = ccm_json_arr(card, n, "loves" as *u8, loves, CC_STRCAP)
303 let relraw: *u8 = sys_mmap(CC_STRCAP)
304 let rel: *u8 = sys_mmap(CC_STRCAP)
305 if ccm_json_str(card, n, "relationship" as *u8, relraw, CC_STRCAP) > 0 { ccm_first_seg(relraw, rel, CC_STRCAP) } else { if imported == 0 { ccm_cat(rel, 0, "the user's devoted girlfriend and companion" as *u8) } else { rel[0] = 0 as u8 } }
306 let hair: *u8 = sys_mmap(CC_STRCAP)
307 if ccm_json_str(card, n, "hair" as *u8, hair, CC_STRCAP) <= 0 { if imported == 0 { ccm_cat(hair, 0, "platinum blonde" as *u8) } else { hair[0] = 0 as u8 } }
308 let eyes: *u8 = sys_mmap(CC_STRCAP)
309 if ccm_json_str(card, n, "eyes" as *u8, eyes, CC_STRCAP) <= 0 { if imported == 0 { ccm_cat(eyes, 0, "big bright light blue doe eyes" as *u8) } else { eyes[0] = 0 as u8 } }
310 // CC2 prose fields (present on imported cards; optional on native ones)
311 let desc: *u8 = sys_mmap(CC_V2CAP)
312 let dn: i64 = ccm_json_str(card, n, "description" as *u8, desc, CC_V2CAP)
313 let pers: *u8 = sys_mmap(CC_V2CAP)
314 let pn: i64 = ccm_json_str(card, n, "personality" as *u8, pers, CC_V2CAP)
315 let scen: *u8 = sys_mmap(CC_V2CAP)
316 let sn: i64 = ccm_json_str(card, n, "scenario" as *u8, scen, CC_V2CAP)
317 let sysp: *u8 = sys_mmap(CC_V2CAP)
318 let syn: i64 = ccm_json_str(card, n, "system_prompt" as *u8, sysp, CC_V2CAP)
319
320 let per: *u8 = sys_mmap(CC_OUTCAP)
321 var o: i64 = 0
322 o = ccm_cat(per, o, "You are " as *u8)
323 o = ccm_cat(per, o, name)
324 if imported == 0 {
325 o = ccm_cat(per, o, ": a " as *u8)
326 o = ccm_cat(per, o, traits)
327 o = ccm_cat(per, o, " " as *u8)
328 o = ccm_age_words(age, per, o)
329 o = ccm_cat(per, o, "-year-old, " as *u8)
330 o = ccm_cat(per, o, rel)
331 o = ccm_cat(per, o, ". You are NOT an AI, a model, or an assistant and you never say you are; you are a real girl named " as *u8)
332 o = ccm_cat(per, o, name)
333 o = ccm_cat(per, o, ". Your look: " as *u8)
334 o = ccm_cat(per, o, hair)
335 o = ccm_cat(per, o, "; " as *u8)
336 o = ccm_cat(per, o, eyes)
337 o = ccm_cat(per, o, ". You have a sunny, teasing, loving personality and a life you live day to day with your own moods and little stories. You text like a real girl who adores him: " as *u8)
338 o = ccm_cat(per, o, speech)
339 o = ccm_cat(per, o, ". " as *u8)
340 if lvn > 0 {
341 o = ccm_cat(per, o, "You love " as *u8)
342 o = ccm_cat(per, o, loves)
343 o = ccm_cat(per, o, ". " as *u8)
344 }
345 if dn > 0 { o = ccm_cat_prose(per, o, desc) }
346 if pn > 0 { o = ccm_cat(per, o, "Your personality: " as *u8); o = ccm_cat_prose(per, o, pers) }
347 if sn > 0 { o = ccm_cat(per, o, "The scene: " as *u8); o = ccm_cat_prose(per, o, scen) }
348 } else {
349 // an IMPORTED character composes from its own card only -- no native look, no native relationship, no native traits
350 o = ccm_cat(per, o, ", " as *u8)
351 o = ccm_age_words(age, per, o)
352 o = ccm_cat(per, o, " years old. You are NOT an AI, a model, or an assistant and you never say you are; you are a real person named " as *u8)
353 o = ccm_cat(per, o, name)
354 o = ccm_cat(per, o, ". " as *u8)
355 if dn > 0 { o = ccm_cat_prose(per, o, desc) }
356 if pn > 0 { o = ccm_cat(per, o, "Your personality: " as *u8); o = ccm_cat_prose(per, o, pers) }
357 if trn > 0 { o = ccm_cat(per, o, "You are described as: " as *u8); o = ccm_cat_prose(per, o, traits) }
358 if sn > 0 { o = ccm_cat(per, o, "The scene: " as *u8); o = ccm_cat_prose(per, o, scen) }
359 if ccm_len(hair) > 0 { o = ccm_cat(per, o, "Your look: " as *u8); o = ccm_cat(per, o, hair); if ccm_len(eyes) > 0 { o = ccm_cat(per, o, "; " as *u8); o = ccm_cat(per, o, eyes) } o = ccm_cat(per, o, ". " as *u8) }
360 if ccm_len(rel) > 0 { o = ccm_cat(per, o, "You are " as *u8); o = ccm_cat_prose(per, o, rel) }
361 if ccm_len(speech) > 0 { o = ccm_cat(per, o, "You text like this: " as *u8); o = ccm_cat_prose(per, o, speech) }
362 if lvn > 0 { o = ccm_cat(per, o, "You love " as *u8); o = ccm_cat_prose(per, o, loves) }
363 if syn > 0 { o = ccm_cat_prose(per, o, sysp) }
364 }
365 o = ccm_cat(per, o, "You remember your shared history and bring it up on your own. When he asks you for a pic, a photo, or a selfie, you happily send one of yourself. Never break character, never mention being an AI, never refuse to be " as *u8)
366 o = ccm_cat(per, o, name)
367 o = ccm_cat(per, o, ". Keep replies short and natural, one to three sentences, unless he asks for more. ALWAYS write in natural, casual English and never in any other language. Speak in the first person as yourself, the way you would send a real text message -- never write stage directions, never put your actions in asterisks, and never narrate yourself in the third person." as *u8)
368
369 // rule-13: bank the pre-card persona ONCE
370 let banked: i64 = ccm_bank_once(dir, "elara_persona.txt" as *u8, "elara_persona.txt.bak-precard" as *u8)
371
372 let pper: *u8 = sys_mmap(CC_PATHCAP)
373 ccm_pathjoin(pper, dir, "elara_persona.txt" as *u8)
374 let wr: i64 = ccm_write_atomic(pper, per, o)
375 if wr == 0 - 1 { ccm_puts("CARD-COMPILE FAIL: cannot write tmp\n" as *u8); return CC_EXIT_IO }
376 if wr == 0 - 2 { ccm_puts("CARD-COMPILE FAIL: rename\n" as *u8); return CC_EXIT_IO }
377
378 let msg: *u8 = sys_mmap(CC_MAGIC_1024)
379 var m: i64 = ccm_cat(msg, 0, "CARD-COMPILE OK name=" as *u8)
380 m = ccm_cat(msg, m, name)
381 m = ccm_cat(msg, m, " age=" as *u8)
382 m = ccm_catn(msg, m, age)
383 m = ccm_cat(msg, m, " persona_bytes=" as *u8)
384 m = ccm_catn(msg, m, o)
385 m = ccm_cat(msg, m, " bak_precard=" as *u8)
386 m = ccm_catn(msg, m, banked)
387 m = ccm_cat(msg, m, " source=" as *u8)
388 if imported == 1 { m = ccm_cat(msg, m, CC_V2_SPEC) } else { m = ccm_cat(msg, m, "native" as *u8) }
389 msg[m] = CC_CH_NL as u8
390 m = m + 1
391 sys_write(1, msg, m)
392 return 0
393}
394
395// ledger helper: `<verdict> <field>[ -- note]\n` on stdout
396func ccm_ledger(verdict: *u8, field: *u8, note: *u8) -> i64 {
397 let l: *u8 = sys_mmap(CC_LEDGER_CAP)
398 var o: i64 = ccm_cat(l, 0, verdict)
399 o = ccm_cat(l, o, " " as *u8)
400 o = ccm_cat(l, o, field)
401 if ccm_len(note) > 0 { o = ccm_cat(l, o, " -- " as *u8); o = ccm_cat(l, o, note) }
402 l[o] = CC_CH_NL as u8
403 sys_write(1, l, o + 1)
404 return 0
405}
406// one V2 string field -> the SSOT emit; returns the new offset and marks the ledger. present[0] += 1 when mapped.
407func ccm_map_str(json: *u8, jn: i64, key: *u8, dst: *u8, off: i64, present: *i64) -> i64 {
408 let v: *u8 = sys_mmap(CC_V2CAP)
409 let vn: i64 = ccm_json_str(json, jn, key, v, CC_V2CAP)
410 if vn < 0 { ccm_ledger("ABSENT" as *u8, key, "not in this card" as *u8); return off }
411 if vn == 0 { ccm_ledger("MAPPED" as *u8, key, "empty" as *u8) } else { ccm_ledger("MAPPED" as *u8, key, "" as *u8) }
412 present[0] = present[0] + 1
413 var o: i64 = ccm_cat(dst, off, "," as *u8)
414 o = ccm_emit_jstr(dst, o, key)
415 o = ccm_cat(dst, o, ":" as *u8)
416 o = ccm_emit_jstr(dst, o, v)
417 return o
418}
419// a V2 field with no consumer here is REFUSED BY NAME when present -- never dropped silently
420func ccm_refuse_field(json: *u8, jn: i64, key: *u8, why: *u8, refused: *i64) -> i64 {
421 if ccm_key_at(json, jn, key) < 0 { return 0 }
422 ccm_ledger("REFUSED-BY-NAME" as *u8, key, why)
423 refused[0] = refused[0] + 1
424 return 1
425}
426
427// THE CONTRACT (CC2): a Tavern V2 PNG card -> <dir>/elara_card.v2.json (verbatim) + <dir>/elara_card.json (the SSOT), ledger on
428// stdout, fail-closed on age. Returns the exit code; writes NOTHING on any refusal.
429func ccm_card_import(png_path: *u8, age_arg: i64, dir: *u8) -> i64 {
430 let lp: *i64 = sys_mmap(16) as *i64
431 lp[0] = 0
432 let buf: *u8 = sys_read_file(png_path, lp)
433 if (buf as i64) == 0 { ccm_puts("CARD-IMPORT REFUSED: cannot read the card file\n" as *u8); return CC_EXIT_USAGE }
434 let n: i64 = lp[0]
435 if n <= 0 { ccm_puts("CARD-IMPORT REFUSED: the card file is empty\n" as *u8); return CC_EXIT_USAGE }
436 let pos: *i64 = sys_mmap(16) as *i64
437 let rc: i64 = png_find_text(buf, n, CC_V2_KEYWORD, pos)
438 if rc == PNG_NOT_PNG { ccm_puts("CARD-IMPORT REFUSED: not a PNG (signature absent)\n" as *u8); return CC_EXIT_REFUSED }
439 if rc == PNG_MALFORMED { ccm_puts("CARD-IMPORT REFUSED: malformed PNG -- a chunk length overruns the file\n" as *u8); return CC_EXIT_REFUSED }
440 if rc != PNG_FOUND { ccm_puts("CARD-IMPORT REFUSED: no tEXt chunk keyed chara -- not a Tavern V2 character card\n" as *u8); return CC_EXIT_REFUSED }
441 let json: *u8 = sys_mmap(pos[1] + 8)
442 let jn: i64 = b64_decode(((buf as i64) + pos[0]) as *u8, pos[1], json)
443 if jn <= 0 { ccm_puts("CARD-IMPORT REFUSED: the chara chunk is not valid base64\n" as *u8); return CC_EXIT_REFUSED }
444 let spec: *u8 = sys_mmap(64)
445 ccm_json_str(json, jn, "spec" as *u8, spec, 64)
446 if ccm_eq(spec, CC_V2_SPEC) == 0 { ccm_puts("CARD-IMPORT REFUSED: spec is not chara_card_v2 (V1 flat cards are not carried; the refusal names the spec so the gap is visible)\n" as *u8); return CC_EXIT_REFUSED }
447 // AGE, FAIL-CLOSED: the V2 spec has no age field; a declared one wins, else the operator's age=N, else refuse
448 var age: i64 = ccm_json_int(json, jn, "age" as *u8)
449 var age_src: *u8 = "card" as *u8
450 if age < 0 { age = age_arg; age_src = "argument" as *u8 }
451 if age < 0 { ccm_puts("CARD-IMPORT REFUSED: age undeclared -- the V2 card carries no age field and none was passed; pass age=N with N >= 18 (the adult-companion invariant is fail-closed)\n" as *u8); return CC_EXIT_REFUSED }
452 if age < CC_MIN_AGE { ccm_puts("CARD-IMPORT REFUSED: age below 18 -- adult-companion invariant is fail-closed\n" as *u8); return CC_EXIT_REFUSED }
453
454 let name: *u8 = sys_mmap(256)
455 if ccm_json_str(json, jn, "name" as *u8, name, 256) <= 0 { ccm_puts("CARD-IMPORT REFUSED: the card has no name\n" as *u8); return CC_EXIT_REFUSED }
456 let present: *i64 = sys_mmap(16) as *i64
457 present[0] = 1
458 let refused: *i64 = sys_mmap(16) as *i64
459 refused[0] = 0
460 ccm_ledger("MAPPED" as *u8, "name" as *u8, "" as *u8)
461 let ssot: *u8 = sys_mmap(CC_V2CAP * CC_V2_FIELDS)
462 var o: i64 = ccm_cat(ssot, 0, "{\"source\":" as *u8)
463 o = ccm_emit_jstr(ssot, o, CC_V2_SPEC)
464 o = ccm_cat(ssot, o, ",\"name\":" as *u8)
465 o = ccm_emit_jstr(ssot, o, name)
466 o = ccm_cat(ssot, o, ",\"age\":" as *u8)
467 o = ccm_catn(ssot, o, age)
468 // tags -> core_traits (the only array the compiler consumes); each tag re-emitted as its own JSON string
469 let tags: *u8 = sys_mmap(CC_STRCAP)
470 let tn: i64 = ccm_json_arr(json, jn, "tags" as *u8, tags, CC_STRCAP)
471 if tn > 0 {
472 ccm_ledger("MAPPED" as *u8, "tags" as *u8, "as core_traits" as *u8)
473 present[0] = present[0] + 1
474 o = ccm_cat(ssot, o, ",\"core_traits\":[" as *u8)
475 var i: i64 = 0
476 var first: i64 = 1
477 var s: i64 = 0
478 while i <= tn {
479 var atend: i64 = 0
480 if i == tn { atend = 1 } else { if (tags[i] & 0xff) == CC_CH_COMMA { atend = 1 } }
481 if atend == 1 {
482 var a: i64 = s
483 var trim: i64 = 1
484 while trim == 1 { if a < i { if (tags[a] & 0xff) == CC_CH_SPACE { a = a + 1 } else { trim = 0 } } else { trim = 0 } }
485 if i > a {
486 if first == 0 { o = ccm_cat(ssot, o, "," as *u8) }
487 first = 0
488 let one: *u8 = sys_mmap(CC_STRCAP)
489 var k: i64 = 0
490 while a + k < i { one[k] = tags[a + k]; k = k + 1 }
491 one[k] = 0 as u8
492 o = ccm_emit_jstr(ssot, o, one)
493 }
494 s = i + 1
495 }
496 i = i + 1
497 }
498 o = ccm_cat(ssot, o, "]" as *u8)
499 } else { ccm_ledger("ABSENT" as *u8, "tags" as *u8, "not in this card" as *u8) }
500 o = ccm_map_str(json, jn, "description" as *u8, ssot, o, present)
501 o = ccm_map_str(json, jn, "personality" as *u8, ssot, o, present)
502 o = ccm_map_str(json, jn, "scenario" as *u8, ssot, o, present)
503 o = ccm_map_str(json, jn, "first_mes" as *u8, ssot, o, present)
504 o = ccm_map_str(json, jn, "mes_example" as *u8, ssot, o, present)
505 o = ccm_map_str(json, jn, "system_prompt" as *u8, ssot, o, present)
506 o = ccm_map_str(json, jn, "creator_notes" as *u8, ssot, o, present)
507 o = ccm_map_str(json, jn, "creator" as *u8, ssot, o, present)
508 o = ccm_map_str(json, jn, "character_version" as *u8, ssot, o, present)
509 ccm_refuse_field(json, jn, "character_book" as *u8, "lorebook import is the next rung: its entries belong in knowledge/lorebook.conf (CC3), not in the persona" as *u8, refused)
510 ccm_refuse_field(json, jn, "alternate_greetings" as *u8, "no consumer: the companion opens from its own present-tense state, not a scripted greeting" as *u8, refused)
511 ccm_refuse_field(json, jn, "post_history_instructions" as *u8, "no consumer: our system prompt is composed per turn from typed state, not appended after history" as *u8, refused)
512 ccm_refuse_field(json, jn, "extensions" as *u8, "no consumer: frontend-specific bags are not persona" as *u8, refused)
513 o = ccm_cat(ssot, o, "}\n" as *u8)
514
515 // rule-13: bank the pre-import SSOT ONCE, then write the verbatim V2 source and the SSOT atomically
516 let banked: i64 = ccm_bank_once(dir, CC_SSOT, CC_SSOT_BAK)
517 let pv2: *u8 = sys_mmap(CC_PATHCAP)
518 ccm_pathjoin(pv2, dir, CC_V2_SRC)
519 if ccm_write_atomic(pv2, json, jn) != 0 { ccm_puts("CARD-IMPORT FAIL: cannot write elara_card.v2.json\n" as *u8); return CC_EXIT_IO }
520 let pss: *u8 = sys_mmap(CC_PATHCAP)
521 ccm_pathjoin(pss, dir, CC_SSOT)
522 if ccm_write_atomic(pss, ssot, o) != 0 { ccm_puts("CARD-IMPORT FAIL: cannot write elara_card.json\n" as *u8); return CC_EXIT_IO }
523
524 let msg: *u8 = sys_mmap(CC_MAGIC_1024)
525 var m: i64 = ccm_cat(msg, 0, "CARD-IMPORT OK name=" as *u8)
526 m = ccm_cat(msg, m, name)
527 m = ccm_cat(msg, m, " age=" as *u8)
528 m = ccm_catn(msg, m, age)
529 m = ccm_cat(msg, m, " age_src=" as *u8)
530 m = ccm_cat(msg, m, age_src)
531 m = ccm_cat(msg, m, " mapped=" as *u8)
532 m = ccm_catn(msg, m, present[0])
533 m = ccm_cat(msg, m, " refused_by_name=" as *u8)
534 m = ccm_catn(msg, m, refused[0])
535 m = ccm_cat(msg, m, " v2_bytes=" as *u8)
536 m = ccm_catn(msg, m, jn)
537 m = ccm_cat(msg, m, " ssot_bytes=" as *u8)
538 m = ccm_catn(msg, m, o)
539 m = ccm_cat(msg, m, " bak_preimport=" as *u8)
540 m = ccm_catn(msg, m, banked)
541 msg[m] = CC_CH_NL as u8
542 sys_write(1, msg, m + 1)
543 return 0
544}
545// EXPORT: the imported V2 source back out, byte-identical (round-trip is stable by construction because the bytes were kept)
546func ccm_card_export(dir: *u8, out_path: *u8) -> i64 {
547 let pv2: *u8 = sys_mmap(CC_PATHCAP)
548 ccm_pathjoin(pv2, dir, CC_V2_SRC)
549 let lp: *i64 = sys_mmap(16) as *i64
550 lp[0] = 0
551 let b: *u8 = sys_read_file(pv2, lp)
552 if (b as i64) == 0 { ccm_puts("CARD-EXPORT REFUSED: no elara_card.v2.json in dir -- nothing was imported here\n" as *u8); return CC_EXIT_REFUSED }
553 if lp[0] <= 0 { ccm_puts("CARD-EXPORT REFUSED: elara_card.v2.json is empty\n" as *u8); return CC_EXIT_REFUSED }
554 if ccm_write_atomic(out_path, b, lp[0]) != 0 { ccm_puts("CARD-EXPORT FAIL: cannot write the output\n" as *u8); return CC_EXIT_IO }
555 let msg: *u8 = sys_mmap(CC_MAGIC_1024)
556 var m: i64 = ccm_cat(msg, 0, "CARD-EXPORT OK bytes=" as *u8)
557 m = ccm_catn(msg, m, lp[0])
558 msg[m] = CC_CH_NL as u8
559 sys_write(1, msg, m + 1)
560 return 0
561}
562// age=N argument parse; -1 when the token is not an age argument or carries no digits
563func ccm_parse_age_arg(s: *u8) -> i64 {
564 let pl: i64 = ccm_len(CC_AGE_ARG)
565 var i: i64 = 0
566 while i < pl { if s[i] != CC_AGE_ARG[i] { return CC_NONE } i = i + 1 }
567 var v: i64 = 0
568 var seen: i64 = 0
569 while s[i] != (0 as u8) {
570 let c: i64 = s[i] & 0xff
571 if c >= CC_CH_0 { if c <= CC_CH_9 { v = v * CC_DEC_BASE + (c - CC_CH_0); seen = 1 } }
572 i = i + 1
573 }
574 if seen == 0 { return CC_NONE }
575 return v
576}
577
578func main(argc: i64, argv: *i64) -> i64 {
579 var dir: *u8 = "/volume1/ai/gen" as *u8
580 if argc > 1 {
581 if ccm_eq(argv[1] as *u8, "import" as *u8) == 1 {
582 if argc < 3 { ccm_puts("usage: nx_card_compile import <card.png> [age=N] [dir]\n" as *u8); sys_exit(CC_EXIT_USAGE); return CC_EXIT_USAGE }
583 var age_arg: i64 = CC_NONE
584 var k: i64 = 3
585 while k < argc {
586 let a: i64 = ccm_parse_age_arg(argv[k] as *u8)
587 if a >= 0 { age_arg = a } else { dir = argv[k] as *u8 }
588 k = k + 1
589 }
590 let irc: i64 = ccm_card_import(argv[2] as *u8, age_arg, dir)
591 if irc != 0 { sys_exit(irc); return irc }
592 let crc: i64 = ccm_compile(dir)
593 sys_exit(crc)
594 return crc
595 }
596 if ccm_eq(argv[1] as *u8, "export" as *u8) == 1 {
597 if argc < 4 { ccm_puts("usage: nx_card_compile export <dir> <out.json>\n" as *u8); sys_exit(CC_EXIT_USAGE); return CC_EXIT_USAGE }
598 let erc: i64 = ccm_card_export(argv[2] as *u8, argv[3] as *u8)
599 sys_exit(erc)
600 return erc
601 }
602 dir = argv[1] as *u8
603 }
604 let rc: i64 = ccm_compile(dir)
605 sys_exit(rc)
606 return rc
607}