nx_rosterconf_lib.nx source
↩ module page · 395 lines · 17884 B
1// nx_rosterconf_lib.nx -- WHICH CONFS ARE GATE ROSTERS, AND IS A GATE A ROW OF ONE (2026-09-18, ecosystem EC57).
2// THE DEFECT THIS CLOSES: the heavy roster (knowledge/gateroster_heavy.conf, clock row gaterosterheavy) runs its gates into the
3// SAME production journal as the main roster, while the two organs that ask "does a roster run this gate" each read ONE conf
4// by name: nx_rungclose printed rostered=0 for a gate the heavy beat runs daily, and nx_execsurface credited it no roster
5// surface. A NAME LIST OF CONFS WOULD LAG THE NEXT ROSTER THE SAME WAY, so the list is DERIVED from the resource that causes
6// the execution: a ROSTER is a (conf, journal) PAIR -- the runner's default pair (what `beat` reads and writes when it names
7// neither) plus every pair a clock row runs through `nx_gate_roster_run beat <conf> <journal>`. A third roster is read by
8// declaring its clock row -- no edit here and none in any consumer.
9// THE JOURNAL IS HALF OF THE PAIR (measured 2026-09-18 on the live plane: four beats, and two of them -- gateroster-slow and
10// nofloatslow -- write knowledge/status/gateroster_slow.jrnl and nofloat_slow.jrnl, each keeping its own heartbeat beside its
11// journal, so they must NOT be repointed onto the shared one). A reader that credits a roster must read ITS journal: the close
12// ruler proves TESTED and OPERATED from the production journal plus the journal of every pair whose conf lists the gate, so a
13// slow beat's evidence is read where that beat writes it. RCF_META_OTHER still counts the beats off the production journal.
14// ONE GRAMMAR, MIRRORED FROM ITS AUTHORITY (nx_gate_roster_run.nx: main's argv parse, grr_load, grr_isid), so every organ
15// that asks reads a conf exactly the way the beat that runs it does:
16// a beat the program nx_gate_roster_run (any path, .elf or not; a longer or PREFIX name is another program) with the
17// verb beat; its next argument, when present, is the conf (else RCF_CONF_DEFAULT) and the one after it the journal
18// (else RCF_JRNL). trial and admit run candidates and argless runs nothing, so none of them names a roster.
19// a row leading spaces skipped; a line opening with # or ; is a comment; the gate is the maximal run of name
20// characters [A-Za-z0-9_./-] from there -- so an @directive names nothing, a CR a row carries from another host
21// ends the name, and a longer or PREFIX name is another gate.
22// Pure: every function takes buffers and numbers, so a gate drives it in-process (nx_rungclose_gate).
23// license_tier: ORIGINAL No hw writes (Rule 26).
24import "nx_syscalls.nx"
25
26const RCF_I64: i64 = 8
27const RCF_RUNNER: *u8 = "nx_gate_roster_run"
28const RCF_VERB: *u8 = "beat"
29const RCF_CONF_DEFAULT: *u8 = "knowledge/gateroster.conf" // the runner's GRR_CONF: what beat reads when it names no conf
30// the PRODUCTION roster journal: the runner's GRR_JRNL (what beat writes when it names no journal), the journal of the default
31// pair, and the one journal the close ruler ALWAYS reads (a GREEN earned there counts even after its gate leaves the conf)
32const RCF_JRNL: *u8 = "knowledge/status/gateroster.jrnl"
33const RCF_ARG_CONF: i64 = 0 // beat [conf] [journal] [ms]: the arguments after the verb, by position
34const RCF_ARG_JRNL: i64 = 1
35const RCF_ELF_SFX: *u8 = ".elf"
36const RCF_NOT_A_BEAT: i64 = 0 - 1
37const RCF_NONE: i64 = 0 - 1
38// a conf text is a pair of slots: its bytes (0 when it could not be read) and its length
39const RCF_TEXT_P: i64 = 0
40const RCF_TEXT_N: i64 = 1
41const RCF_TEXT_STRIDE: i64 = 2
42// what rcf_confs reports beside the pair list
43const RCF_META_BEATS: i64 = 0 // plane rows that are roster beats, whatever journal they write
44const RCF_META_BYTES: i64 = 1 // the pair list's length in bytes
45const RCF_META_OTHER: i64 = 2 // of them, the beats writing a journal other than RCF_JRNL (read from that journal)
46const RCF_META_SLOTS: i64 = 3
47// UNIT: the character codes the grammar names
48const RCF_CH_TAB: i64 = 9
49const RCF_CH_NL: i64 = 10
50const RCF_CH_CR: i64 = 13
51const RCF_CH_SP: i64 = 32
52const RCF_CH_HASH: i64 = 35
53const RCF_CH_COMMA: i64 = 44
54const RCF_CH_MINUS: i64 = 45
55const RCF_CH_DOT: i64 = 46
56const RCF_CH_SLASH: i64 = 47
57const RCF_CH_0: i64 = 48
58const RCF_CH_9: i64 = 57
59const RCF_CH_SEMI: i64 = 59
60const RCF_CH_UA: i64 = 65
61const RCF_CH_UZ: i64 = 90
62const RCF_CH_US: i64 = 95
63const RCF_CH_LA: i64 = 97
64const RCF_CH_LZ: i64 = 122
65
66func rcf_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
67
68// the characters of a gate name, exactly the runner's grr_isid
69func rcf_isid(c: i64) -> i64 {
70 if c >= RCF_CH_LA { if c <= RCF_CH_LZ { return 1 } }
71 if c >= RCF_CH_UA { if c <= RCF_CH_UZ { return 1 } }
72 if c >= RCF_CH_0 { if c <= RCF_CH_9 { return 1 } }
73 if c == RCF_CH_US { return 1 }
74 if c == RCF_CH_MINUS { return 1 }
75 if c == RCF_CH_DOT { return 1 }
76 if c == RCF_CH_SLASH { return 1 }
77 return 0
78}
79
80// a separator inside a clock row's command: a space, a tab, or a CR a row written on another host carries
81func rcf_isblank(c: i64) -> i64 {
82 if c == RCF_CH_SP { return 1 }
83 if c == RCF_CH_TAB { return 1 }
84 if c == RCF_CH_CR { return 1 }
85 return 0
86}
87
88// the end of the line that starts at p: its newline, a NUL, or n
89func rcf_line_end(buf: *u8, n: i64, p: i64) -> i64 {
90 var e: i64 = p
91 while e < n {
92 if buf[e] == (RCF_CH_NL as u8) { return e }
93 if buf[e] == (0 as u8) { return e }
94 e = e + 1
95 }
96 return n
97}
98
99// the span [off, off+len) of buf is the literal: the same length and the same bytes
100func rcf_span_is(buf: *u8, off: i64, len: i64, lit: *u8) -> i64 {
101 if len < 0 { return 0 }
102 if rcf_slen(lit) != len { return 0 }
103 var i: i64 = 0
104 while i < len { if buf[off + i] != lit[i] { return 0 } i = i + 1 }
105 return 1
106}
107
108// two spans in two buffers are equal
109func rcf_span_eq(a: *u8, ao: i64, al: i64, b: *u8, bo: i64, bl: i64) -> i64 {
110 if al != bl { return 0 }
111 var i: i64 = 0
112 while i < al { if a[ao + i] != b[bo + i] { return 0 } i = i + 1 }
113 return 1
114}
115
116// the next separator-delimited token at or after p inside [p, e): its length, its offset in off[0]
117func rcf_token(buf: *u8, p: i64, e: i64, off: *i64) -> i64 {
118 var s: i64 = p
119 while s < e { if rcf_isblank(buf[s] as i64) == 1 { s = s + 1 } else { break } }
120 var t: i64 = s
121 while t < e { if rcf_isblank(buf[t] as i64) == 1 { break } t = t + 1 }
122 off[0] = s
123 return t - s
124}
125
126// IS THIS COMMAND A ROSTER BEAT, AND WHAT IS ITS ARGUMENT k (RCF_ARG_CONF, RCF_ARG_JRNL)? [coff, coff+clen) is a clock row's
127// organ command. Returns the argument's length with its offset in off[0]; 0 with off[0] = RCF_NONE when the beat does not name
128// it (the runner then uses its default); RCF_NOT_A_BEAT when the program is not the runner or its verb is not beat.
129func rcf_beat_arg(buf: *u8, coff: i64, clen: i64, k: i64, off: *i64) -> i64 {
130 let e: i64 = coff + clen
131 let pl: i64 = rcf_token(buf, coff, e, off)
132 let po: i64 = off[0]
133 off[0] = RCF_NONE
134 if pl <= 0 { return RCF_NOT_A_BEAT }
135 // the program's basename, less a trailing .elf
136 var s: i64 = po
137 var q: i64 = po
138 while q < po + pl { if buf[q] == (RCF_CH_SLASH as u8) { s = q + 1 } q = q + 1 }
139 var bl: i64 = po + pl - s
140 let xl: i64 = rcf_slen(RCF_ELF_SFX)
141 if bl > xl { if rcf_span_is(buf, po + pl - xl, xl, RCF_ELF_SFX) == 1 { bl = bl - xl } }
142 if rcf_span_is(buf, s, bl, RCF_RUNNER) == 0 { return RCF_NOT_A_BEAT }
143 let vl: i64 = rcf_token(buf, po + pl, e, off)
144 let vo: i64 = off[0]
145 off[0] = RCF_NONE
146 if rcf_span_is(buf, vo, vl, RCF_VERB) == 0 { return RCF_NOT_A_BEAT }
147 var at: i64 = vo + vl
148 var al: i64 = 0
149 var i: i64 = 0
150 while i <= k {
151 al = rcf_token(buf, at, e, off)
152 if al <= 0 { off[0] = RCF_NONE; return 0 }
153 at = off[0] + al
154 i = i + 1
155 }
156 return al
157}
158
159// the conf a roster beat runs (RCF_CONF_DEFAULT when it names none), and the journal it writes (RCF_JRNL when it names none)
160func rcf_beat_conf(buf: *u8, coff: i64, clen: i64, off: *i64) -> i64 { return rcf_beat_arg(buf, coff, clen, RCF_ARG_CONF, off) }
161func rcf_beat_jrnl(buf: *u8, coff: i64, clen: i64, off: *i64) -> i64 { return rcf_beat_arg(buf, coff, clen, RCF_ARG_JRNL, off) }
162
163// does the beat write the production journal? jl/jo are rcf_beat_jrnl's answer: a beat that names no journal writes RCF_JRNL
164func rcf_jrnl_is_prod(buf: *u8, jo: i64, jl: i64) -> i64 {
165 if jl == 0 { return 1 }
166 return rcf_span_is(buf, jo, jl, RCF_JRNL)
167}
168
169// does the list (one conf per line, n bytes) hold the span as a whole line?
170func rcf_list_has(list: *u8, n: i64, buf: *u8, off: i64, len: i64) -> i64 {
171 var p: i64 = 0
172 while p < n {
173 let e: i64 = rcf_line_end(list, n, p)
174 if rcf_span_eq(list, p, e - p, buf, off, len) == 1 { return 1 }
175 p = e + 1
176 }
177 return 0
178}
179
180// append the span as one line unless the list already holds it; returns the list's new length (NUL-terminated after it)
181func rcf_list_add(list: *u8, n: i64, buf: *u8, off: i64, len: i64) -> i64 {
182 if len <= 0 { return n }
183 if rcf_list_has(list, n, buf, off, len) == 1 { return n }
184 var o: i64 = n
185 var i: i64 = 0
186 while i < len { list[o] = buf[off + i]; o = o + 1; i = i + 1 }
187 list[o] = RCF_CH_NL as u8
188 o = o + 1
189 list[o] = 0 as u8
190 return o
191}
192
193// how many lines the list holds
194func rcf_lines(list: *u8, n: i64) -> i64 {
195 var c: i64 = 0
196 var p: i64 = 0
197 while p < n { c = c + 1; p = rcf_line_end(list, n, p) + 1 }
198 return c
199}
200
201// line k of the list: its length with its offset in off[0]; RCF_NONE when the list has no line k
202func rcf_line(list: *u8, n: i64, k: i64, off: *i64) -> i64 {
203 var i: i64 = 0
204 var p: i64 = 0
205 while p < n {
206 let e: i64 = rcf_line_end(list, n, p)
207 if i == k { off[0] = p; return e - p }
208 i = i + 1
209 p = e + 1
210 }
211 off[0] = RCF_NONE
212 return RCF_NONE
213}
214
215// the bytes a pair list derived from a plane of pn bytes can take: every line of the plane adds at most one pair -- a conf and a
216// journal, each a span of that line or the default standing in for it, with its tab and newline -- plus the default pair and the NUL
217func rcf_confs_bound(plane: *u8, pn: i64) -> i64 {
218 let per: i64 = rcf_slen(RCF_CONF_DEFAULT) + rcf_slen(RCF_JRNL) + 2
219 return pn + (rcf_lines(plane, pn) + 1) * per + 1
220}
221
222// one pair line, conf TAB journal, composed into dst (a path never holds a tab: the command splits on it); its length
223func rcf_pair_compose(dst: *u8, cb: *u8, co: i64, cl: i64, jb: *u8, jo: i64, jl: i64) -> i64 {
224 var o: i64 = 0
225 var i: i64 = 0
226 while i < cl { dst[o] = cb[co + i]; o = o + 1; i = i + 1 }
227 dst[o] = RCF_CH_TAB as u8
228 o = o + 1
229 i = 0
230 while i < jl { dst[o] = jb[jo + i]; o = o + 1; i = i + 1 }
231 dst[o] = 0 as u8
232 return o
233}
234
235// THE ROSTERS, one (conf, journal) pair per line into list (rcf_confs_bound bytes), conf TAB journal: the runner's default pair
236// FIRST -- the conf and journal every incumbent reader already read, so an unreadable or empty plane costs no roster they saw --
237// then every pair a plane row's command runs through the beat, once each, in plane order (a beat naming no conf runs the default
238// conf, one naming no journal writes RCF_JRNL). A clock plane row's command is its LAST tab field (the planes carry the name, the
239// interval, the live plane's next due, then the command); a line with no tab is not a row. meta receives the beats, the list's
240// bytes and the beats off the production journal (RCF_META_*). Returns the pairs listed.
241func rcf_confs(plane: *u8, pn: i64, list: *u8, meta: *i64) -> i64 {
242 let off: *i64 = sys_mmap(RCF_I64) as *i64
243 let dl: i64 = rcf_slen(RCF_CONF_DEFAULT)
244 let jdl: i64 = rcf_slen(RCF_JRNL)
245 let scn: i64 = pn + dl + jdl + 3
246 let sc: *u8 = sys_mmap(scn)
247 var sl: i64 = rcf_pair_compose(sc, RCF_CONF_DEFAULT, 0, dl, RCF_JRNL, 0, jdl)
248 var n: i64 = rcf_list_add(list, 0, sc, 0, sl)
249 meta[RCF_META_BEATS] = 0
250 meta[RCF_META_OTHER] = 0
251 var p: i64 = 0
252 while p < pn {
253 let e: i64 = rcf_line_end(plane, pn, p)
254 var s: i64 = RCF_NONE
255 var i: i64 = p
256 while i < e { if plane[i] == (RCF_CH_TAB as u8) { s = i + 1 } i = i + 1 }
257 if s >= 0 {
258 let cl: i64 = rcf_beat_conf(plane, s, e - s, off)
259 let co: i64 = off[0]
260 if cl >= 0 {
261 meta[RCF_META_BEATS] = meta[RCF_META_BEATS] + 1
262 let jl: i64 = rcf_beat_jrnl(plane, s, e - s, off)
263 let jo: i64 = off[0]
264 if rcf_jrnl_is_prod(plane, jo, jl) == 0 { meta[RCF_META_OTHER] = meta[RCF_META_OTHER] + 1 }
265 var cb: *u8 = plane
266 var cs: i64 = co
267 var cn: i64 = cl
268 if cl == 0 { cb = RCF_CONF_DEFAULT; cs = 0; cn = dl }
269 var jb: *u8 = plane
270 var js: i64 = jo
271 var jn: i64 = jl
272 if jl == 0 { jb = RCF_JRNL; js = 0; jn = jdl }
273 sl = rcf_pair_compose(sc, cb, cs, cn, jb, js, jn)
274 n = rcf_list_add(list, n, sc, 0, sl)
275 }
276 }
277 p = e + 1
278 }
279 meta[RCF_META_BYTES] = n
280 sys_munmap(sc, scn)
281 sys_munmap(off as *u8, RCF_I64)
282 return rcf_lines(list, n)
283}
284
285// the conf of pair line k: its length with its offset in off[0]; RCF_NONE when the list has no line k
286func rcf_pair_conf(list: *u8, n: i64, k: i64, off: *i64) -> i64 {
287 let ll: i64 = rcf_line(list, n, k, off)
288 if ll < 0 { return RCF_NONE }
289 let p: i64 = off[0]
290 var t: i64 = p
291 while t < p + ll { if list[t] == (RCF_CH_TAB as u8) { return t - p } t = t + 1 }
292 return ll
293}
294
295// the journal of pair line k: its length with its offset in off[0]; RCF_NONE when the list has no line k
296func rcf_pair_jrnl(list: *u8, n: i64, k: i64, off: *i64) -> i64 {
297 let ll: i64 = rcf_line(list, n, k, off)
298 if ll < 0 { return RCF_NONE }
299 let p: i64 = off[0]
300 var t: i64 = p
301 while t < p + ll { if list[t] == (RCF_CH_TAB as u8) { off[0] = t + 1; return p + ll - (t + 1) } t = t + 1 }
302 off[0] = p + ll
303 return 0
304}
305
306// THE FIRST PAIR SHARING PAIR k's CONF (RCF_PART_CONF) or JOURNAL (RCF_PART_JRNL): the key one read is shared under, so a conf or
307// a journal two pairs name is read once (the main and heavy beats both write the production journal)
308const RCF_PART_CONF: i64 = 0
309const RCF_PART_JRNL: i64 = 1
310func rcf_pair_part(list: *u8, n: i64, k: i64, which: i64, off: *i64) -> i64 {
311 if which == RCF_PART_CONF { return rcf_pair_conf(list, n, k, off) }
312 return rcf_pair_jrnl(list, n, k, off)
313}
314func rcf_pair_origin(list: *u8, n: i64, k: i64, which: i64) -> i64 {
315 let ko: *i64 = sys_mmap(RCF_I64) as *i64
316 let io: *i64 = sys_mmap(RCF_I64) as *i64
317 let kl: i64 = rcf_pair_part(list, n, k, which, ko)
318 var best: i64 = k
319 var i: i64 = 0
320 while i < k {
321 let il: i64 = rcf_pair_part(list, n, i, which, io)
322 if best == k { if rcf_span_eq(list, io[0], il, list, ko[0], kl) == 1 { best = i } }
323 i = i + 1
324 }
325 sys_munmap(ko as *u8, RCF_I64)
326 sys_munmap(io as *u8, RCF_I64)
327 return best
328}
329
330// THE GATE ONE LINE [p, e) OF A CONF NAMES, read the way the beat reads it (grr_load), in two pure steps: where its name would start
331// (leading spaces skipped), then how long it is -- 0 when the line names none (blank, a comment, an @directive). Every reader of a
332// roster conf reads rows through these, so the organ that credits the roster surface and the organ that asks "is it rostered"
333// cannot read one row two ways.
334func rcf_row_start(text: *u8, p: i64, e: i64) -> i64 {
335 var c0: i64 = p
336 while c0 < e { if text[c0] == (RCF_CH_SP as u8) { c0 = c0 + 1 } else { break } }
337 return c0
338}
339func rcf_row_len(text: *u8, c0: i64, e: i64) -> i64 {
340 if c0 >= e { return 0 }
341 if text[c0] == (RCF_CH_HASH as u8) { return 0 }
342 if text[c0] == (RCF_CH_SEMI as u8) { return 0 }
343 var t: i64 = c0
344 while t < e { if rcf_isid(text[t] as i64) == 1 { t = t + 1 } else { break } }
345 return t - c0
346}
347
348// IS THE GATE A ROW OF THIS CONF? text may be 0 with tn 0: an unread conf holds no rows.
349func rcf_has_gate(text: *u8, tn: i64, gate: *u8, gl: i64) -> i64 {
350 if gl <= 0 { return 0 }
351 var hit: i64 = 0
352 var p: i64 = 0
353 while p < tn {
354 let e: i64 = rcf_line_end(text, tn, p)
355 let c0: i64 = rcf_row_start(text, p, e)
356 if rcf_span_eq(text, c0, rcf_row_len(text, c0, e), gate, 0, gl) == 1 { hit = 1 }
357 if hit == 1 { p = tn } else { p = e + 1 }
358 }
359 return hit
360}
361
362// THE CONF THAT ROSTERS THE GATE: the index of the first conf text holding it, RCF_NONE when none does. texts holds k pairs
363// (RCF_TEXT_STRIDE slots each) in list order.
364func rcf_find(texts: *i64, k: i64, gate: *u8, gl: i64) -> i64 {
365 var i: i64 = 0
366 while i < k {
367 let b: i64 = i * RCF_TEXT_STRIDE
368 if rcf_has_gate(texts[b + RCF_TEXT_P] as *u8, texts[b + RCF_TEXT_N], gate, gl) == 1 { return i }
369 i = i + 1
370 }
371 return RCF_NONE
372}
373
374// the DISTINCT confs of the pair list as one comma-joined word at o0 in dst (a list of n bytes needs n + 1): the roster_confs= a
375// report prints
376func rcf_join_confs(list: *u8, n: i64, dst: *u8, o0: i64) -> i64 {
377 let po: *i64 = sys_mmap(RCF_I64) as *i64
378 let k: i64 = rcf_lines(list, n)
379 var o: i64 = o0
380 var any: i64 = 0
381 var i: i64 = 0
382 while i < k {
383 if rcf_pair_origin(list, n, i, RCF_PART_CONF) == i {
384 let cl: i64 = rcf_pair_conf(list, n, i, po)
385 if any == 1 { dst[o] = RCF_CH_COMMA as u8; o = o + 1 }
386 var q: i64 = 0
387 while q < cl { dst[o] = list[po[0] + q]; o = o + 1; q = q + 1 }
388 any = 1
389 }
390 i = i + 1
391 }
392 dst[o] = 0 as u8
393 sys_munmap(po as *u8, RCF_I64)
394 return o
395}