code wiki / (root) / nx_rosterconf_lib.nx

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}