code wiki / _hdl_build / nx_user_journey.nx

nx_user_journey.nx source

↩ module page · 442 lines · 18286 B

1// nx_user_journey.nx -- TEAM-OWNED real-user journey gate over the LIVE 2// Nishi sites (operator: "make sure our teams play testing isnt just api but 3// actually mimics real users interacting with the real site... the core 4// nishi ecosystem"). This is NOT an API check: it does what a visitor does, 5// over the real network, on the team's OWN sovereign HTTPS client 6// (nx_https_get: DNS -> TCP -> TLS 1.3 -> GET, validated certs from the 7// bits-up trust store; NO curl, NO OpenSSL): 8// 9// * load the homepage like a browser would, 10// * READ it: assert the content a human would see is actually there, 11// * CLICK what it shows: extract internal links from the live HTML and 12// follow them, asserting each lands on a real page, 13// * visit the published app contracts (e.g. /tictactoe must serve the 14// 9-cell board contract from the May C0 ship). 15// 16// First live run 2026-06-09 ALREADY caught a real user-facing regression: 17// nishifamily.com serves a 745B hub for EVERY path -- /tictactoe (smoke-green 18// in May, "every endpoint bits-up") returns the hub, not the game. A user 19// today cannot play. API-level gates never saw it; this gate exists so the 20// team always does. Composes [[feedback-real-playtest-loop-not-just-logs]]. 21// 22// TRANSPORT IS A TEMPORARY TRANSLATION LAYER (named honestly, Playwright 23// precedent): /usr/bin/curl as a forked child. The sovereign client 24// (nx_https_get + trust store) was tried FIRST and is BLOCKED by two filed 25// defects on this host (2026-06-09): (1) sovereign DNS resolve fails here 26// (known_good build returns rr=-3, env: WSL resolv stub); (2) the CURRENT 27// compiler lineage SEGFAULTS on that error path where known_good returns the 28// error -- compiler divergence repro = _uj_probe.nx. When both close, swap 29// uj_fetch's body back to nx_https_get with NO caller-side change 30// (Cardinal 19). Journey logic, contracts, parsing: all bits-up NishiLang. 31// 32// JOURNEY lines -> stdout + /tmp/nishi_user_journey.log; exit = fails. 33// Honest scope: HTML-over-the-wire user mimicry. Pixel/DOM-event mimicry in 34// a real browser engine still needs the SS22.5 sovereign browser-test-runner 35// (PM plan item; do NOT fake it with claims here). 36// 37// license_tier: ORIGINAL 38import "nx_syscalls.nx" 39const UJ_MAGIC_1200: i64 = 1200 40const UJ_MAGIC_2000: i64 = 2000 41const UJ_MAGIC_10000: i64 = 10000 42const UJ_MAGIC_50000: i64 = 50000 43const UJ_MAGIC_5000: i64 = 5000 44 45const UJ_BUF_CAP: i64 = 262144 46const UJ_PAUSE_MS: i64 = 400 // polite inter-request pacing, human-ish 47 48func uj_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 49func uj_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i } 50func uj_cat_n(dst: *u8, off: i64, v: i64) -> i64 { 51 var o: i64 = off 52 var m: i64 = v 53 if m < 0 { dst[o] = 45; o = o + 1; m = 0 - m } 54 let t: *u8 = sys_mmap(28) 55 var k: i64 = 0 56 if m == 0 { t[0] = 48; k = 1 } 57 while m > 0 { t[k] = 48 + (m % 10); m = m / 10; k = k + 1 } 58 var i: i64 = 0 59 while i < k { dst[o+i] = t[k-1-i]; i = i + 1 } 60 return o + k 61} 62 63// find needle in hay[0..n); returns first offset or -1 64func uj_find(hay: *u8, n: i64, needle: *u8) -> i64 { 65 var nl: i64 = 0 66 while needle[nl] != (0 as u8) { nl = nl + 1 } 67 if nl == 0 { return 0 - 1 } 68 var i: i64 = 0 69 while i + nl <= n { 70 var j: i64 = 0 71 var hit: i64 = 1 72 while j < nl { 73 if hay[i+j] != needle[j] { hit = 0; j = nl } 74 if j < nl { j = j + 1 } 75 } 76 if hit == 1 { return i } 77 i = i + 1 78 } 79 return 0 - 1 80} 81 82// count occurrences of needle in hay[0..n) 83func uj_count(hay: *u8, n: i64, needle: *u8) -> i64 { 84 var nl: i64 = 0 85 while needle[nl] != (0 as u8) { nl = nl + 1 } 86 if nl == 0 { return 0 } 87 var c: i64 = 0 88 var i: i64 = 0 89 while i + nl <= n { 90 var j: i64 = 0 91 var hit: i64 = 1 92 while j < nl { 93 if hay[i+j] != needle[j] { hit = 0; j = nl } 94 if j < nl { j = j + 1 } 95 } 96 if hit == 1 { c = c + 1; i = i + nl } 97 if hit == 0 { i = i + 1 } 98 } 99 return c 100} 101 102// HTTP response: find body offset (after \r\n\r\n); -1 if not found 103func uj_body_off(resp: *u8, n: i64) -> i64 { 104 var i: i64 = 0 105 while i + 4 <= n { 106 if resp[i] == 13 { if resp[i+1] == 10 { if resp[i+2] == 13 { if resp[i+3] == 10 { return i + 4 } } } } 107 i = i + 1 108 } 109 return 0 - 1 110} 111 112// parse "HTTP/1.1 NNN" status from the head of the response; -1 unknown 113func uj_status(resp: *u8, n: i64) -> i64 { 114 if n < 12 { return 0 - 1 } 115 // first space then 3 digits 116 var i: i64 = 0 117 while i < 20 { 118 if resp[i] == 32 { 119 let d0: i64 = resp[i+1] as i64 120 let d1: i64 = resp[i+2] as i64 121 let d2: i64 = resp[i+3] as i64 122 if d0 >= 48 { if d0 <= 57 { if d1 >= 48 { if d1 <= 57 { if d2 >= 48 { if d2 <= 57 { 123 return (d0-48)*100 + (d1-48)*10 + (d2-48) 124 } } } } } } 125 return 0 - 1 126 } 127 i = i + 1 128 } 129 return 0 - 1 130} 131 132// fetch url like a real visitor. TEMPORARY transport: forked curl child 133// (see module header for the two filed defects blocking the sovereign 134// client). Writes body ptr/len + HTTP status through out-params; returns 135// total body bytes (<=0 fail). 136func uj_fetch_once(url: *u8, buf: *u8, body_ptr: *i64, body_len: *i64, status: *i64) -> i64 { 137 body_ptr[0] = 0 138 body_len[0] = 0 139 status[0] = 0 - 1 140 let body_file: *u8 = "/tmp/uj_body.bin" as *u8 141 // truncate stale body first -- a failed fetch must NEVER inherit the 142 // previous page's bytes (silent false-PASS class) 143 let tfd: i64 = sys_openat_wr(body_file, 0x1a4) 144 if tfd >= 0 { sys_close(tfd) } 145 let code_fd: i64 = sys_openat_wr("/tmp/uj_code.txt" as *u8, 0x1a4) 146 let pid: i64 = sys_fork() 147 if pid == 0 { 148 if code_fd >= 0 { sys_dup3(code_fd, 1, 0) } 149 let argv: *i64 = sys_mmap(8*13) as *i64 150 argv[0] = "/usr/bin/curl" as *u8 as i64 151 argv[1] = "-s" as *u8 as i64 152 argv[2] = "-L" as *u8 as i64 // browsers follow redirects; so does a user 153 argv[3] = "-o" as *u8 as i64 154 argv[4] = body_file as i64 155 argv[5] = "-w" as *u8 as i64 156 argv[6] = "%{http_code}" as *u8 as i64 157 argv[7] = "-m" as *u8 as i64 158 argv[8] = "15" as *u8 as i64 159 argv[9] = "-A" as *u8 as i64 160 argv[10] = "NishiUserJourney/1 (team gate; mimics a visitor)" as *u8 as i64 161 argv[11] = url as i64 162 argv[12] = 0 163 let envp: *i64 = sys_mmap(16) as *i64 164 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0 165 sys_execve("/usr/bin/curl" as *u8, argv, envp) 166 sys_exit(127) 167 } 168 let st: *i64 = sys_mmap(16) as *i64 169 sys_wait4(pid, st, 0) 170 if code_fd >= 0 { sys_close(code_fd) } 171 // status from curl's -w output 172 let clen_p: *i64 = sys_mmap(8) as *i64 173 let cbuf: *u8 = sys_read_file("/tmp/uj_code.txt" as *u8, clen_p) 174 if (cbuf as i64) != 0 { 175 if clen_p[0] >= 3 { 176 let d0: i64 = cbuf[0] as i64 177 let d1: i64 = cbuf[1] as i64 178 let d2: i64 = cbuf[2] as i64 179 if d0 >= 48 { if d0 <= 57 { status[0] = (d0-48)*100 + (d1-48)*10 + (d2-48) } } 180 } 181 } 182 // body bytes 183 let blen_p: *i64 = sys_mmap(8) as *i64 184 let bbuf: *u8 = sys_read_file(body_file, blen_p) 185 if (bbuf as i64) == 0 { return 0 - 1 } 186 let n: i64 = blen_p[0] 187 if n <= 0 { return 0 - 1 } 188 body_ptr[0] = bbuf as i64 189 body_len[0] = n 190 sys_sleep_ms(UJ_PAUSE_MS) 191 return n 192} 193 194// fetch with ONE retry on connection-level failure (no HTTP status at all) 195// -- a user hits refresh on a blip. HTTP errors (404/500) never retry; 196// those are real verdicts. Repeated status-0 = an availability finding. 197func uj_fetch(url: *u8, buf: *u8, body_ptr: *i64, body_len: *i64, status: *i64) -> i64 { 198 var rr_final: i64 = uj_fetch_once(url, buf, body_ptr, body_len, status) 199 if status[0] <= 0 { 200 sys_sleep_ms(UJ_MAGIC_1200) 201 rr_final = uj_fetch_once(url, buf, body_ptr, body_len, status) 202 } 203 return rr_final 204} 205 206// one JOURNEY report line 207func uj_report(logfd: i64, ts: i64, label: *u8, ok: i64, detail: *u8, num: i64) -> i64 { 208 let line: *u8 = sys_mmap(512) 209 var o: i64 = 0 210 o = uj_cat(line, o, "JOURNEY ts=" as *u8) 211 o = uj_cat_n(line, o, ts) 212 o = uj_cat(line, o, " step=" as *u8) 213 o = uj_cat(line, o, label) 214 o = uj_cat(line, o, " verdict=" as *u8) 215 if ok == 1 { o = uj_cat(line, o, "PASS" as *u8) } 216 if ok != 1 { o = uj_cat(line, o, "FAIL" as *u8) } 217 o = uj_cat(line, o, " " as *u8) 218 o = uj_cat(line, o, detail) 219 o = uj_cat_n(line, o, num) 220 o = uj_cat(line, o, "\n" as *u8) 221 sys_write(1, line, o) 222 if logfd >= 0 { sys_write(logfd, line, o) } 223 return 0 224} 225 226func uj_streq(a: *u8, b: *u8) -> i64 { 227 var i: i64 = 0 228 while a[i] != (0 as u8) { 229 if a[i] != b[i] { return 0 } 230 i = i + 1 231 } 232 if b[i] != (0 as u8) { return 0 } 233 return 1 234} 235 236// follow up to `cap` distinct internal links found on page body (what a user 237// can click); each must come back 200 with a non-trivial body. Returns fails. 238func uj_click_links(host: *u8, body: *u8, blen: i64, logfd: i64, ts: i64, cap: i64) -> i64 { 239 var fails: i64 = 0 240 var followed: i64 = 0 241 var pos: i64 = 0 242 // dedup: a user doesn't open the same link twice; remember followed urls 243 let seen: *i64 = sys_mmap(8 * 8) as *i64 244 var n_seen: i64 = 0 245 while followed < cap { 246 let rel: i64 = uj_find(((body as i64) + pos) as *u8, blen - pos, "href=\"/" as *u8) 247 if rel < 0 { followed = cap } 248 if rel >= 0 { 249 let hstart: i64 = pos + rel + 6 // at the leading '/' 250 // capture path until closing quote (cap 180) 251 let path: *u8 = sys_mmap(200) 252 var pi: i64 = 0 253 while pi < 180 { 254 let c: u8 = body[hstart + pi] 255 if c == (34 as u8) { pi = 200 } // closing quote 256 if pi < 180 { 257 if pi != 200 { path[pi] = c; pi = pi + 1 } 258 } 259 } 260 // pi landed at 200 via quote, recompute real len 261 var plen: i64 = 0 262 while plen < 180 { 263 if path[plen] == (34 as u8) { plen = 180 } 264 if plen < 180 { 265 if path[plen] == (0 as u8) { plen = 180 } 266 } 267 if plen < 180 { plen = plen + 1 } 268 } 269 // build absolute url 270 let url: *u8 = sys_mmap(512) 271 var o: i64 = 0 272 o = uj_cat(url, o, "https://" as *u8) 273 o = uj_cat(url, o, host) 274 var ci: i64 = 0 275 while ci < 180 { 276 let c2: u8 = path[ci] 277 if c2 == (0 as u8) { ci = 180 } 278 if c2 == (34 as u8) { ci = 180 } 279 if ci < 180 { url[o] = c2; o = o + 1; ci = ci + 1 } 280 } 281 url[o] = 0 as u8 282 // skip if already clicked 283 var dup: i64 = 0 284 var si: i64 = 0 285 while si < n_seen { 286 if uj_streq(url, seen[si] as *u8) == 1 { dup = 1; si = n_seen } 287 if si < n_seen { si = si + 1 } 288 } 289 if dup == 0 { 290 if n_seen < 8 { seen[n_seen] = url as i64; n_seen = n_seen + 1 } 291 let buf: *u8 = sys_mmap(UJ_BUF_CAP) 292 let bp: *i64 = sys_mmap(8) as *i64 293 let bl: *i64 = sys_mmap(8) as *i64 294 let stx: *i64 = sys_mmap(8) as *i64 295 let rr: i64 = uj_fetch(url, buf, bp, bl, stx) 296 var ok: i64 = 0 297 if rr > 0 { if stx[0] == 200 { if bl[0] > 100 { ok = 1 } } } 298 uj_report(logfd, ts, url, ok, "click-follow status=" as *u8, stx[0]) 299 if ok == 0 { fails = fails + 1 } 300 followed = followed + 1 301 } 302 pos = hstart 303 } 304 } 305 return fails 306} 307 308func main() -> i64 { 309 let logfd: i64 = sys_openat_append("/tmp/nishi_user_journey.log" as *u8, 0x1a4) 310 let ts: i64 = sys_now_realtime_sec() 311 var fails: i64 = 0 312 313 // ---- visitor journey: andelinwest.com (the live legal site) ---- 314 let buf1: *u8 = sys_mmap(UJ_BUF_CAP) 315 let bp: *i64 = sys_mmap(8) as *i64 316 let bl: *i64 = sys_mmap(8) as *i64 317 let stx: *i64 = sys_mmap(8) as *i64 318 let rr1: i64 = uj_fetch("https://andelinwest.com/" as *u8, buf1, bp, bl, stx) 319 var ok1: i64 = 0 320 if rr1 > 0 { if stx[0] == 200 { if bl[0] > UJ_MAGIC_2000 { if uj_find(bp[0] as *u8, bl[0], "Law" as *u8) >= 0 { ok1 = 1 } } } } 321 uj_report(logfd, ts, "andelinwest-home" as *u8, ok1, "status+content-Law len=" as *u8, bl[0]) 322 if ok1 == 0 { fails = fails + 1 } 323 if ok1 == 1 { 324 fails = fails + uj_click_links("andelinwest.com" as *u8, bp[0] as *u8, bl[0], logfd, ts, 3) 325 } 326 327 // ---- visitor journey: nishifamily.com hub ---- 328 let buf2: *u8 = sys_mmap(UJ_BUF_CAP) 329 let rr2: i64 = uj_fetch("https://nishifamily.com/" as *u8, buf2, bp, bl, stx) 330 var ok2: i64 = 0 331 if rr2 > 0 { if stx[0] == 200 { if bl[0] > 400 { if uj_find(bp[0] as *u8, bl[0], "Nishi" as *u8) >= 0 { ok2 = 1 } } } } 332 uj_report(logfd, ts, "nishifamily-home" as *u8, ok2, "status+content-Nishi len=" as *u8, bl[0]) 333 if ok2 == 0 { fails = fails + 1 } 334 if ok2 == 1 { 335 fails = fails + uj_click_links("nishifamily.com" as *u8, bp[0] as *u8, bl[0], logfd, ts, 2) 336 } 337 338 // ---- published app contract: /tictactoe must serve the GAME ---- 339 // Current (mount-era) page contract: the ttt-root mount point must be in 340 // the HTML a user receives, AND the wasm the page loads must be served 341 // with valid WebAssembly magic (\0asm) at real size -- the user gets the 342 // gated game bytes, not a fallback hub page. (The old May contract was 343 // 9 static data-cell-idx cells; the page design moved to nishi-host 344 // mount 2026-05-20, cells are rendered by the bits-up runtime.) 345 let buf3: *u8 = sys_mmap(UJ_BUF_CAP) 346 let rr3: i64 = uj_fetch("https://nishifamily.com/tictactoe/" as *u8, buf3, bp, bl, stx) 347 var ok3: i64 = 0 348 if rr3 > 0 { if stx[0] == 200 { if uj_find(bp[0] as *u8, bl[0], "ttt-root" as *u8) >= 0 { ok3 = 1 } } } 349 uj_report(logfd, ts, "nishifamily-tictactoe-page" as *u8, ok3, "ttt-root-present len=" as *u8, bl[0]) 350 if ok3 == 0 { fails = fails + 1 } 351 352 let buf4: *u8 = sys_mmap(UJ_BUF_CAP) 353 let rr4: i64 = uj_fetch("https://nishifamily.com/tictactoe/nx_tictactoe.wasm" as *u8, buf4, bp, bl, stx) 354 var ok4: i64 = 0 355 if rr4 > UJ_MAGIC_10000 { 356 if stx[0] == 200 { 357 let wb: *u8 = bp[0] as *u8 358 if wb[0] == (0 as u8) { 359 if wb[1] == (97 as u8) { // 'a' 360 if wb[2] == (115 as u8) { // 's' 361 if wb[3] == (109 as u8) { ok4 = 1 } // 'm' 362 } 363 } 364 } 365 } 366 } 367 uj_report(logfd, ts, "nishifamily-tictactoe-wasm" as *u8, ok4, "wasm-magic+size=" as *u8, bl[0]) 368 if ok4 == 0 { fails = fails + 1 } 369 370 // ---- published app contract: /voxels must serve the SEEDABLE world ---- 371 // (operator 2026-06-10: seedable so people have unique experiences.) 372 // Contract: page has the canvas mount + the world-seed share element + 373 // calls init_seed; the runtime wasm serves with valid magic. 374 let buf5: *u8 = sys_mmap(UJ_BUF_CAP) 375 let rr5: i64 = uj_fetch("https://nishifamily.com/voxels/" as *u8, buf5, bp, bl, stx) 376 var ok5: i64 = 0 377 if rr5 > 0 { if stx[0] == 200 { 378 if uj_find(bp[0] as *u8, bl[0], "voxel-canvas" as *u8) >= 0 { 379 if uj_find(bp[0] as *u8, bl[0], "world-seed" as *u8) >= 0 { 380 if uj_find(bp[0] as *u8, bl[0], "init_seed" as *u8) >= 0 { ok5 = 1 } 381 } 382 } 383 } } 384 uj_report(logfd, ts, "nishifamily-voxels-seedable-page" as *u8, ok5, "canvas+seed-ui+init_seed len=" as *u8, bl[0]) 385 if ok5 == 0 { fails = fails + 1 } 386 387 let buf6: *u8 = sys_mmap(UJ_BUF_CAP) 388 let rr6: i64 = uj_fetch("https://nishifamily.com/voxels/nx_voxel_runtime.wasm" as *u8, buf6, bp, bl, stx) 389 var ok6: i64 = 0 390 if rr6 > UJ_MAGIC_50000 { 391 if stx[0] == 200 { 392 let vb: *u8 = bp[0] as *u8 393 if vb[0] == (0 as u8) { 394 if vb[1] == (97 as u8) { 395 if vb[2] == (115 as u8) { 396 if vb[3] == (109 as u8) { ok6 = 1 } 397 } 398 } 399 } 400 } 401 } 402 uj_report(logfd, ts, "nishifamily-voxels-wasm" as *u8, ok6, "wasm-magic+size=" as *u8, bl[0]) 403 if ok6 == 0 { fails = fails + 1 } 404 405 // ---- published app contract: /vn must serve the visual novel ---- 406 // (VN0/VN1, beat-Ren'Py arc: engine+story in a ~15KB wasm.) 407 let buf7: *u8 = sys_mmap(UJ_BUF_CAP) 408 let rr7: i64 = uj_fetch("https://nishifamily.com/vn/" as *u8, buf7, bp, bl, stx) 409 var ok7: i64 = 0 410 if rr7 > 0 { if stx[0] == 200 { if uj_find(bp[0] as *u8, bl[0], "vn-root" as *u8) >= 0 { ok7 = 1 } } } 411 uj_report(logfd, ts, "nishifamily-vn-page" as *u8, ok7, "vn-root-present len=" as *u8, bl[0]) 412 if ok7 == 0 { fails = fails + 1 } 413 414 let buf8: *u8 = sys_mmap(UJ_BUF_CAP) 415 let rr8: i64 = uj_fetch("https://nishifamily.com/vn/nx_visual_novel.wasm" as *u8, buf8, bp, bl, stx) 416 var ok8: i64 = 0 417 if rr8 > UJ_MAGIC_5000 { 418 if stx[0] == 200 { 419 let nb: *u8 = bp[0] as *u8 420 if nb[0] == (0 as u8) { 421 if nb[1] == (97 as u8) { 422 if nb[2] == (115 as u8) { 423 if nb[3] == (109 as u8) { ok8 = 1 } 424 } 425 } 426 } 427 } 428 } 429 uj_report(logfd, ts, "nishifamily-vn-wasm" as *u8, ok8, "wasm-magic+size=" as *u8, bl[0]) 430 if ok8 == 0 { fails = fails + 1 } 431 432 let sline: *u8 = sys_mmap(256) 433 var o: i64 = 0 434 o = uj_cat(sline, o, "JOURNEY SUMMARY ts=" as *u8) 435 o = uj_cat_n(sline, o, ts) 436 o = uj_cat(sline, o, " fails=" as *u8) 437 o = uj_cat_n(sline, o, fails) 438 o = uj_cat(sline, o, "\n" as *u8) 439 sys_write(1, sline, o) 440 if logfd >= 0 { sys_write(logfd, sline, o); sys_close(logfd) } 441 return fails 442}