code wiki / (root) / nx_clockjobs_gate.nx

nx_clockjobs_gate.nx source

↩ module page · 179 lines · 8178 B

1// nx_clockjobs_gate.nx -- is the scheduler's registry WELL-FORMED, or is it carrying silent no-ops? 2// 3// WHY THIS EXISTS (debt 1785601721). clk_load in nx_clock_sched.nx parses clock_jobs.tsv with NO 4// validation of any kind. Every failure mode below is silent -- the row becomes a "job" that dispatches 5// nothing, forever, and no instrument reports it: 6// * fewer than 4 tab fields -> organ = "" -> dispatch is a no-op, scheduled for eternity 7// * non-digit chars in a number -> SKIPPED, so an interval of "3o0" parses as 30 (wrong cadence, 8// plausible value, no error) -- the silent-coercion class 9// * interval = 0 -> a job that is always due 10// * rows past CLK_MAXJOBS -> SILENTLY DROPPED (the truncating-cap class: the registry says 11// one thing, the scheduler runs another) 12// 13// A MALFORMED ROW MUST BE REFUSED AT ADMISSION, NOT PARSED INTO A SILENT NO-OP AT DISPATCH. This gate 14// is the DETECTION half of that: it cannot stop a bad write, but it makes one impossible to miss. The 15// admission half (a validating writer) is the next rung and is deliberately separate -- a reader-side 16// guard that is honest about being read-only beats a writer swap rushed under a live scheduler. 17// 18// SCOPE, STATED HONESTLY: this validates the REGISTRY FILE. It does NOT prove the named organ exists on 19// disk, and it does NOT prove a job ever dispatched -- fail=0 is not evidence that a beat moved what it 20// names. Those are separate claims and separate gates. 21// license_tier: ORIGINAL expect_exit: 0 22import "nx_syscalls.nx" 23import "nx_gate_verdict.nx" 24 25// MUST TRACK nx_clock_sched.nx:18. Mirrored rather than imported because that file is the SCHEDULER 26// library and importing it here would drag the dispatcher into a read-only gate. If the dispatcher's 27// cap changes and this is not updated, T3 measures the wrong bound -- so the number is named, not inline. 28const CJ_MAXJOBS: i64 = 128 29const CJ_REG: *u8 = "clock_jobs.tsv" as *u8 30const CJ_TAB: i64 = 9 31const CJ_NL: i64 = 10 32 33func cj_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 34func cj_p(s: *u8) -> i64 { let n: i64 = cj_len(s); sys_write(1, s, n); return 0 } 35func cj_pn(v: i64) -> i64 { 36 var m: i64 = v 37 if m < 0 { cj_p("-" as *u8); m = 0 - m } 38 let t: *u8 = sys_mmap(24) 39 var k: i64 = 0 40 if m == 0 { t[0] = 48 as u8; k = 1 } 41 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 42 let o: *u8 = sys_mmap(24) 43 var i: i64 = 0 44 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 45 sys_write(1, o, k) 46 return 0 47} 48 49// Is every byte in [lo,hi) a decimal digit, and is the span non-empty? 50func cj_all_digits(buf: *u8, lo: i64, hi: i64) -> i64 { 51 if hi <= lo { return 0 } 52 var i: i64 = lo 53 var ok: i64 = 1 54 while i < hi { 55 let c: i64 = buf[i] as i64 56 if c < 48 { ok = 0 } 57 if c > 57 { ok = 0 } 58 i = i + 1 59 } 60 return ok 61} 62 63// Decimal value of [lo,hi). Caller must have checked cj_all_digits first. 64func cj_val(buf: *u8, lo: i64, hi: i64) -> i64 { 65 var v: i64 = 0 66 var i: i64 = lo 67 while i < hi { v = v * 10 + ((buf[i] as i64) - 48); i = i + 1 } 68 return v 69} 70 71func main() -> i64 { 72 cj_p("=== nx_clockjobs_gate -- is the scheduler registry well-formed? (debt 1785601721) ===\n" as *u8) 73 74 let lenp: *i64 = sys_mmap(8) as *i64 75 let data: *u8 = sys_read_file(CJ_REG, lenp) 76 if (data as i64) == 0 { 77 cj_p(" REGISTRY UNREADABLE: " as *u8); cj_p(CJ_REG); cj_p("\n" as *u8) 78 cj_p(" Reporting zero malformed rows here would be indistinguishable from a clean registry,\n" as *u8) 79 cj_p(" so this is RED: the instrument measured NOTHING.\n" as *u8) 80 let ctr0: *i64 = gv_ctr() 81 gv_head("nx_clockjobs_gate -- scheduler registry well-formedness" as *u8) 82 gv_check("T1 the registry was readable (a blind scan must never render as a clean one)" as *u8, 0, ctr0) 83 let rc0: i64 = gv_verdict("CLOCKJOBS-GATE" as *u8, ctr0, "registry unreadable" as *u8) 84 sys_exit(rc0) 85 return rc0 86 } 87 88 let dn: i64 = lenp[0] 89 var rows: i64 = 0 90 var bad: i64 = 0 91 var i: i64 = 0 92 var ls: i64 = 0 93 94 while i <= dn { 95 var eol: i64 = 0 96 if i == dn { eol = 1 } 97 if i < dn { if (data[i] as i64) == CJ_NL { eol = 1 } } 98 if eol == 1 { 99 if i > ls { 100 rows = rows + 1 101 var t1: i64 = 0 - 1 102 var t2: i64 = 0 - 1 103 var t3: i64 = 0 - 1 104 var ntab: i64 = 0 105 var p: i64 = ls 106 while p < i { 107 if (data[p] as i64) == CJ_TAB { 108 ntab = ntab + 1 109 if ntab == 1 { t1 = p } 110 if ntab == 2 { t2 = p } 111 if ntab == 3 { t3 = p } 112 } 113 p = p + 1 114 } 115 var rowbad: i64 = 0 116 if ntab != 3 { 117 rowbad = 1 118 cj_p(" ROW " as *u8); cj_pn(rows) 119 cj_p(" MALFORMED: " as *u8); cj_pn(ntab); cj_p(" tabs, want 3 (name/interval/next_due/organ)\n" as *u8) 120 } 121 if ntab == 3 { 122 if t1 <= ls { 123 rowbad = 1 124 cj_p(" ROW " as *u8); cj_pn(rows); cj_p(" MALFORMED: empty NAME\n" as *u8) 125 } 126 if cj_all_digits(data, t1 + 1, t2) == 0 { 127 rowbad = 1 128 cj_p(" ROW " as *u8); cj_pn(rows) 129 cj_p(" MALFORMED: INTERVAL is not all digits -- clk_load SKIPS non-digits, so this\n" as *u8) 130 cj_p(" parses to a plausible WRONG cadence rather than erroring\n" as *u8) 131 } 132 if cj_all_digits(data, t1 + 1, t2) == 1 { 133 if cj_val(data, t1 + 1, t2) == 0 { 134 rowbad = 1 135 cj_p(" ROW " as *u8); cj_pn(rows); cj_p(" MALFORMED: INTERVAL=0 (always due)\n" as *u8) 136 } 137 } 138 if cj_all_digits(data, t2 + 1, t3) == 0 { 139 rowbad = 1 140 cj_p(" ROW " as *u8); cj_pn(rows); cj_p(" MALFORMED: NEXT_DUE is not all digits\n" as *u8) 141 } 142 if i <= (t3 + 1) { 143 rowbad = 1 144 cj_p(" ROW " as *u8); cj_pn(rows) 145 cj_p(" MALFORMED: empty ORGAN -- this row dispatches NOTHING, forever, silently\n" as *u8) 146 } 147 } 148 if rowbad == 1 { bad = bad + 1 } 149 } 150 ls = i + 1 151 } 152 i = i + 1 153 } 154 155 cj_p(" rows read = " as *u8); cj_pn(rows); cj_p("\n" as *u8) 156 cj_p(" malformed rows = " as *u8); cj_pn(bad); cj_p("\n" as *u8) 157 cj_p(" dispatcher cap = " as *u8); cj_pn(CJ_MAXJOBS); cj_p(" (rows beyond it are SILENTLY DROPPED by clk_load)\n" as *u8) 158 159 var t3ok: i64 = 0 160 if rows <= CJ_MAXJOBS { t3ok = 1 } 161 if t3ok == 0 { 162 cj_p(" OVER CAP: the registry declares more jobs than the scheduler will load. The file and the\n" as *u8) 163 cj_p(" running schedule DISAGREE, and nothing else reports it.\n" as *u8) 164 } 165 166 var t1ok: i64 = 0 167 if rows > 0 { t1ok = 1 } 168 var t2ok: i64 = 0 169 if bad == 0 { t2ok = 1 } 170 171 let ctr: *i64 = gv_ctr() 172 gv_head("nx_clockjobs_gate -- scheduler registry well-formedness" as *u8) 173 gv_check("T1 the scan read real rows (zero rows would render like a clean registry)" as *u8, t1ok, ctr) 174 gv_check("T2 every row is well-formed (4 fields, numeric interval>0, numeric next_due, non-empty organ)" as *u8, t2ok, ctr) 175 gv_check("T3 row count is within the dispatcher cap (beyond it, clk_load drops rows silently)" as *u8, t3ok, ctr) 176 let rc: i64 = gv_verdict("CLOCKJOBS-GATE" as *u8, ctr, "registry validated row-by-row; a malformed row is a permanent silent no-op" as *u8) 177 sys_exit(rc) 178 return rc 179}