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}