code wiki / (root) / nx_logtail.nx

nx_logtail.nx source

↩ module page · 112 lines · 7519 B

1// nx_logtail.nx -- THE shared HONEST TAIL READER for append-only logs and journals. ONE copy, so the 2// corpus stops re-deriving a bounded read that silently keeps the WRONG END of the file. 3// 4// ***A BOUNDED READ OF AN APPEND-ONLY LOG THAT KEEPS THE HEAD IS NOT A SMALLER TRUTH, IT IS THE 5// OPPOSITE ONE.*** Freshness, last-state and terminal-closure all live at the TAIL, so a reader that 6// bounds its read from offset 0 reports THE PAST AS THE PRESENT -- and because the dropped fraction 7// grows with the file, IT FAILS TOWARD FALSE VERDICTS ON THE MOST ACTIVE LOGS FIRST, which are exactly 8// the ones anyone cares about. Filed as a class with 4 instances in debt 1786054115. 9// 10// LIFTED, NEVER COPIED (2026-08-06, ws=resume-surface-rearm). This body is not new code: it is the 11// reader already proven GREEN 12/12 inside nx_ws_kickoff_sync.nx, including a neg-control tooth that 12// FORCES truncation through a deliberately tiny window and asserts newest-kept AND oldest-dropped. 13// It is lifted here verbatim so the second and third consumers inherit the proof instead of re-earning 14// it. MEASURED cost of NOT having this: nx_ws_kickoff_sync read 262140 of a 1364805-byte journal (19.2%) 15// from offset 0 and never compared the count to the file size, which did not merely hide rows -- it 16// FABRICATED, because the closure and last-activity predicates test WITHIN the buffer, so lanes closed 17// past the cap read IN-FLIGHT FOREVER and every age was a false staleness age. Two seats then acted on 18// those numbers. nx_cron_watch had the same geometry in a different organ (head served as tail) and 19// produced a false 12.5-day dead-beat alarm that a second seat propagated. 20// 21// IMPORT COST IS DELIBERATELY THE MINIMUM POSSIBLE -- nx_syscalls.nx and nothing else -- per the law in 22// nx_itoa_lib.nx: WHEN A CORRECT PRIMITIVE IS RETYPED, MEASURE ITS IMPORT COST BEFORE BLAMING 23// DISCOVERABILITY, because people do not retype what is CHEAP to reach. That is also why this lib does 24// NOT format the envelope: every consumer already owns a catn (ks_catn, cw_catn, st_catn) and each emits 25// a different shape (text vs JSON), so formatting here would either force a format or drag in an emitter 26// and price the lib out of the very sites it exists to serve. The numbers are handed back as data. 27// 28// ENV CONTRACT -- caller passes a *i64 with at least LT_ENV_SLOTS slots (>=32 bytes): 29// env[0] = file_bytes EXACT total size, because the whole file is streamed. This is strictly more 30// than nx_fs can report: nx_fs honestly declares FILE-EXCEEDS-SCAN-WINDOW but 31// cannot say BY HOW MUCH, which is why bounding a journal by hand cost twelve 32// probes on 2026-08-06 (debt 1786054029). 33// env[1] = scanned bytes RETAINED in buf and safe to parse (always a whole number of records) 34// env[2] = dropped_oldest file_bytes - scanned 35// env[3] = truncated 0 or 1. THE CALLER MUST SURFACE THIS. A partial read reported as complete is 36// the defect this file exists to end; declare it or fail loud, never imply it. 37// license_tier: ORIGINAL No hw writes (Rule 26). 38import "nx_syscalls.nx" 39 40const LT_ENV_SLOTS: i64 = 4 41const LT_NL: i64 = 10 42 43// move len bytes down to offset 0 from `from`. Forward copy is safe here because dst < src always. 44func lt_shift_down(b: *u8, from: i64, len: i64) -> i64 { 45 var i: i64 = 0 46 while i < len { b[i] = b[from + i]; i = i + 1 } 47 return len 48} 49 50// Seek to the tail of `path`, RETAIN the newest <=cap bytes in buf, and declare the envelope in env. 51// Returns bytes retained, or -1 IF THE FILE CANNOT BE OPENED. That -1 is not decoration: ABSENT and 52// EMPTY are different facts and a reader that collapses them cannot report the difference. v1 returned 0 53// for both, which would have silently downgraded nx_govern_sweep's `verdict=ABSENT path=...` (exit 4) into 54// a GOVERNED verdict over zero bytes -- a missing supervisor log reported as a healthy one. gv_tail, the 55// hand-rolled copy being retired, already returned -1; that is the SECOND thing the known good got right 56// that this lib had to be corrected to match. env is zeroed in both cases, so a caller that ignores the 57// return still sees file_bytes=0 rather than a fabricated size. 58// 59// ***CORRECTION, 2026-08-06, SAME SESSION AS THIS FILE'S FIRST VERSION.*** v1 streamed the ENTIRE file 60// through a halving window and justified it in this very comment with "it needs no lseek/fstat helper". 61// THAT JUSTIFICATION WAS FALSE. sys_lseek has been in nx_syscalls all along, and nx_govern_sweep.gv_tail 62// -- one of the very hand-rolled copies this lib exists to absorb -- was already using 63// sys_lseek(fd,0,2) to size the file and seek straight to sz-win. I did not check before writing a 64// rationale, so I shipped a plausible-sounding reason for a slower design and put it in a SHARED header 65// where every future reader would have inherited it. 66// ***A DESIGN NOTE IS A CLAIM AND MUST BE CHECKED LIKE ONE. An unverified rationale in a shared lib does 67// not just mislead one author, it teaches the whole corpus.*** The retyped copy I set out to retire was 68// better than mine on the axis I rationalised away -- which is exactly why the doctrine is MATCH THE 69// KNOWN GOOD FIRST: it is also the thing most likely to correct you. 70// v2 COMPOSES both halves rather than picking a winner: THEIR seek (file_bytes in one syscall, and I/O 71// bounded by cap instead of by file size -- on the 1.37MB journal that is ~4x less reading, and unbounded 72// savings as any log grows) plus MINE (record-boundary trim so a torn first line can never be parsed as a 73// whole record, and a DECLARED envelope so a partial read can never pass as complete). Neither copy had 74// both. The gate is the oracle for this swap: same contract, better implementation, so 8/8 must still hold. 75func lt_read_tail(path: *u8, buf: *u8, cap: i64, env: *i64) -> i64 { 76 env[0] = 0; env[1] = 0; env[2] = 0; env[3] = 0 77 let fd: i64 = sys_openat_rd(path) 78 if fd < 0 { return 0 - 1 } 79 let sz: i64 = sys_lseek(fd, 0, 2) 80 if sz <= 0 { sys_close(fd); return 0 } 81 var start: i64 = 0 82 var trunc: i64 = 0 83 if sz > cap { start = sz - cap; trunc = 1 } 84 sys_lseek(fd, start, 0) 85 var n: i64 = 0 86 var go: i64 = 1 87 while go == 1 { 88 let r: i64 = sys_read(fd, ((buf as i64) + n) as *u8, cap - n) 89 if r <= 0 { go = 0 } else { n = n + r } 90 if n >= cap { go = 0 } 91 } 92 sys_close(fd) 93 // A tail window that started mid-file almost certainly begins MID-RECORD. Advance past the first 94 // newline so the caller can never parse a half record as a whole one -- a torn first line is not a 95 // smaller truth either. env[2] therefore counts BOTH the pre-window bytes and this trimmed remnant, 96 // which is why the gate asserts dropped == file_bytes - scanned rather than == start. 97 if trunc == 1 { 98 var f: i64 = 0 99 var s: i64 = 1 100 while s == 1 { if f >= n { s = 0 } else { if buf[f] == (LT_NL as u8) { s = 0 } else { f = f + 1 } } } 101 if f < n { n = lt_shift_down(buf, f + 1, n - (f + 1)) } 102 } 103 env[0] = sz; env[1] = n; env[2] = sz - n; env[3] = trunc 104 return n 105} 106 107// 1 iff the whole file was retained. Provided so a caller can FAIL LOUD in one line rather than 108// forgetting env[3] exists -- the omission that made every instance of this class silent. 109func lt_complete(env: *i64) -> i64 { 110 if env[3] == 0 { return 1 } 111 return 0 112}