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}