nx_field_beat_lib.nx source
↩ module page · 193 lines · 9295 B
1// nx_field_beat_lib.nx -- THE DECISION CORE OF THE FIELD BEAT (/compare/fieldwatch FW4, 2026-09-05). Pure functions
2// only, so nx_field_beat_gate drives every decision in-process on fixtures assembled at runtime: which domains a list
3// names, what a discover receipt says, how one run is classified, where the stamp goes and what it says. The program
4// (nx_field_beat) owns the forks and the file writes; nothing here touches the network or the evidence store.
5// license_tier: ORIGINAL No hw writes (Rule 26).
6import "nx_syscalls.nx"
7
8const FB_NAME_CAP: i64 = 64 // one domain name slot; the longest name on regen.list is 16 bytes, a longer one is cut and the cut is visible in the stamp's domains= count staying honest
9const FB_MAX_DOMAINS: i64 = 256 // regen.list carries 111 domains today; a list past this sets capped=1 (a FLOOR, announced), never a silent cut
10const FB_CH_NL: i64 = 10
11const FB_CH_CR: i64 = 13
12const FB_CH_HASH: i64 = 35
13const FB_CH_SP: i64 = 32
14const FB_CH_EQ: i64 = 61
15const FB_CH_MINUS: i64 = 45
16const FB_CH_0: i64 = 48
17const FB_CH_9: i64 = 57
18const FB_NUM_BASE: i64 = 10
19const FB_NUM_W: i64 = 24
20const FB_UNPARSED: i64 = 0 - 1 // a field the receipt did not carry: its own bucket, never a zero
21const FB_CLASS_OK: i64 = 1
22const FB_CLASS_SKIP: i64 = 2 // no seeds file: the domain has declared no field yet, so there is nothing to re-read
23const FB_CLASS_FAIL: i64 = 3
24const FB_STAMP_DEFAULT: *u8 = "knowledge/status/fieldbeat.stamp"
25const FB_STAMP_SUFFIX: *u8 = ".stamp"
26const FB_OK_LINE: *u8 = "FIELD-OK "
27const FB_STAMP_HEAD: *u8 = "fieldbeat" // every stamp key is space-preceded, so one span reader serves receipt and stamp
28
29func fb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
30func fb_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 }
31func fb_puts(fd: i64, s: *u8) -> i64 { sys_write(fd, s, fb_slen(s)); return 0 }
32func fb_putn(fd: i64, v: i64) -> i64 {
33 let b: *u8 = sys_mmap(FB_NUM_W)
34 var x: i64 = v
35 var neg: i64 = 0
36 if x < 0 { neg = 1; x = 0 - x }
37 var i: i64 = FB_NUM_W
38 if x == 0 { i = i - 1; b[i] = FB_CH_0 as u8 }
39 while x > 0 { i = i - 1; b[i] = (FB_CH_0 + (x - (x / FB_NUM_BASE) * FB_NUM_BASE)) as u8; x = x / FB_NUM_BASE }
40 if neg == 1 { i = i - 1; b[i] = FB_CH_MINUS as u8 }
41 sys_write(fd, (b as i64 + i) as *u8, FB_NUM_W - i)
42 return 0
43}
44func fb_cat(dst: *u8, off: i64, s: *u8, cap: i64) -> i64 {
45 var i: i64 = 0
46 while s[i] != (0 as u8) { if off + i < cap - 1 { dst[off + i] = s[i] } i = i + 1 }
47 var o: i64 = off + i
48 if o > cap - 1 { o = cap - 1 }
49 dst[o] = 0 as u8
50 return o
51}
52func fb_domain_at(names: *u8, i: i64) -> *u8 { return ((names as i64) + i * FB_NAME_CAP) as *u8 }
53
54// PURE: one domain per line; lines whose first non-space byte is '#' and blank lines are skipped; CR and surrounding
55// spaces are trimmed. names is an arena of FB_NAME_CAP * FB_MAX_DOMAINS bytes. capped[0]=1 when the arena filled.
56func fb_list_domains(b: *u8, n: i64, names: *u8, capped: *i64) -> i64 {
57 capped[0] = 0
58 var cnt: i64 = 0
59 var i: i64 = 0
60 while i < n {
61 var e: i64 = i
62 while e < n { if (b[e] as i64) == FB_CH_NL { break } e = e + 1 }
63 var s: i64 = i
64 var t: i64 = e
65 var trimming: i64 = 1
66 while trimming == 1 {
67 trimming = 0
68 if t > s { if (b[t - 1] as i64) == FB_CH_CR { t = t - 1; trimming = 1 } }
69 if t > s { if (b[t - 1] as i64) == FB_CH_SP { t = t - 1; trimming = 1 } }
70 }
71 while s < t { if (b[s] as i64) == FB_CH_SP { s = s + 1 } else { break } }
72 if t > s { if (b[s] as i64) != FB_CH_HASH {
73 if cnt < FB_MAX_DOMAINS {
74 let d: *u8 = fb_domain_at(names, cnt)
75 var k: i64 = 0
76 while s + k < t { if k < FB_NAME_CAP - 1 { d[k] = b[s + k] } k = k + 1 }
77 if k > FB_NAME_CAP - 1 { k = FB_NAME_CAP - 1 }
78 d[k] = 0 as u8
79 cnt = cnt + 1
80 } else { capped[0] = 1 }
81 } }
82 i = e + 1
83 }
84 return cnt
85}
86
87func fb_prefix_at(b: *u8, s: i64, e: i64, lit: *u8) -> i64 {
88 var k: i64 = 0
89 while lit[k] != (0 as u8) { if s + k >= e { return 0 } if b[s + k] != lit[k] { return 0 } k = k + 1 }
90 return 1
91}
92// PURE: the LAST line that starts with FB_OK_LINE (seed lines may mention the same keys, so the receipt line is anchored
93// by its own prefix, never by a key search over the whole capture). Returns 1 and the span, or 0.
94func fb_ok_line(cap: *u8, n: i64, s_out: *i64, e_out: *i64) -> i64 {
95 var found: i64 = 0
96 var i: i64 = 0
97 while i < n {
98 var e: i64 = i
99 while e < n { if (cap[e] as i64) == FB_CH_NL { break } e = e + 1 }
100 if fb_prefix_at(cap, i, e, FB_OK_LINE) == 1 { s_out[0] = i; e_out[0] = e; found = 1 }
101 i = e + 1
102 }
103 return found
104}
105// PURE: the integer after " <key>=" inside [s,e); FB_UNPARSED when the key is absent or carries no digits.
106func fb_span_int(b: *u8, s: i64, e: i64, key: *u8) -> i64 {
107 let kl: i64 = fb_slen(key)
108 var i: i64 = s
109 while i + kl + 1 < e {
110 if (b[i] as i64) == FB_CH_SP { if fb_prefix_at(b, i + 1, e, key) == 1 { if (b[i + 1 + kl] as i64) == FB_CH_EQ {
111 var j: i64 = i + 2 + kl
112 var neg: i64 = 0
113 if j < e { if (b[j] as i64) == FB_CH_MINUS { neg = 1; j = j + 1 } }
114 var v: i64 = 0
115 var nd: i64 = 0
116 var scanning: i64 = 1
117 while scanning == 1 {
118 scanning = 0
119 if j < e { let c: i64 = b[j] as i64; if c >= FB_CH_0 { if c <= FB_CH_9 { v = v * FB_NUM_BASE + (c - FB_CH_0); nd = nd + 1; j = j + 1; scanning = 1 } } }
120 }
121 if nd == 0 { return FB_UNPARSED }
122 if neg == 1 { return 0 - v }
123 return v
124 } } }
125 i = i + 1
126 }
127 return FB_UNPARSED
128}
129func fb_receipt_ok(cap: *u8, n: i64) -> i64 {
130 let so: *i64 = sys_mmap(16) as *i64
131 let eo: *i64 = sys_mmap(16) as *i64
132 return fb_ok_line(cap, n, so, eo)
133}
134func fb_receipt_int(cap: *u8, n: i64, key: *u8) -> i64 {
135 let so: *i64 = sys_mmap(16) as *i64
136 let eo: *i64 = sys_mmap(16) as *i64
137 if fb_ok_line(cap, n, so, eo) == 0 { return FB_UNPARSED }
138 return fb_span_int(cap, so[0], eo[0], key)
139}
140// PURE: one run's class. No seeds file -> SKIP (nothing declared). Exit 0 WITH a FIELD-OK line -> OK. Everything else,
141// including a clean exit that printed no receipt (the silent manifestation), is FAIL.
142func fb_classify(rc: i64, ok: i64, seeds_present: i64) -> i64 {
143 if seeds_present == 0 { return FB_CLASS_SKIP }
144 if rc == 0 { if ok == 1 { return FB_CLASS_OK } }
145 return FB_CLASS_FAIL
146}
147func fb_class_name(c: i64) -> *u8 {
148 if c == FB_CLASS_OK { return "OK" as *u8 }
149 if c == FB_CLASS_SKIP { return "SKIP-NO-SEEDS" as *u8 }
150 return "FAIL" as *u8
151}
152// PURE: the production stamp only for the default list; any other list stamps beside itself, so a trial run can never
153// forge the heartbeat the watch plane reads (the nx_gate_roster_run lesson, 2026-08-20).
154func fb_stamp_path(list: *u8, is_default: i64, dst: *u8, cap: i64) -> i64 {
155 if is_default == 1 { return fb_cat(dst, 0, FB_STAMP_DEFAULT, cap) }
156 let o: i64 = fb_cat(dst, 0, list, cap)
157 return fb_cat(dst, o, FB_STAMP_SUFFIX, cap)
158}
159func fb_stamp_verdict(ok: i64, failed: i64) -> *u8 {
160 if ok + failed == 0 { return "EMPTY" as *u8 }
161 if failed > 0 { return "RED" as *u8 }
162 return "GREEN" as *u8
163}
164// PURE over an fd: the stamp line. Every key is space-preceded; the partition ok + failed + skipped = sum is printed.
165func fb_stamp_write(fd: i64, ts: i64, list: *u8, domains: i64, ok: i64, failed: i64, skipped: i64, cands: i64, capped: i64, drifted: i64) -> i64 {
166 fb_puts(fd, FB_STAMP_HEAD)
167 fb_puts(fd, " ts=" as *u8); fb_putn(fd, ts)
168 fb_puts(fd, " list=" as *u8); fb_puts(fd, list)
169 fb_puts(fd, " domains=" as *u8); fb_putn(fd, domains)
170 fb_puts(fd, " ran=" as *u8); fb_putn(fd, ok + failed)
171 fb_puts(fd, " ok=" as *u8); fb_putn(fd, ok)
172 fb_puts(fd, " failed=" as *u8); fb_putn(fd, failed)
173 fb_puts(fd, " skipped_no_seeds=" as *u8); fb_putn(fd, skipped)
174 fb_puts(fd, " sum=" as *u8); fb_putn(fd, ok + failed + skipped)
175 fb_puts(fd, " candidates_total=" as *u8); fb_putn(fd, cands)
176 fb_puts(fd, " drifted_total=" as *u8); fb_putn(fd, drifted)
177 fb_puts(fd, " capped=" as *u8); fb_putn(fd, capped)
178 fb_puts(fd, " verdict=" as *u8); fb_puts(fd, fb_stamp_verdict(ok, failed))
179 fb_puts(fd, "\n" as *u8)
180 return 0
181}
182// PURE over an fd: one journal row per domain per run.
183func fb_jrnl_row(fd: i64, ts: i64, dom: *u8, cls: i64, rc: i64, cands: i64, named: i64, failed_seeds: i64) -> i64 {
184 fb_puts(fd, "fieldbeat|" as *u8); fb_putn(fd, ts)
185 fb_puts(fd, "|" as *u8); fb_puts(fd, dom)
186 fb_puts(fd, "|" as *u8); fb_puts(fd, fb_class_name(cls))
187 fb_puts(fd, "|rc=" as *u8); fb_putn(fd, rc)
188 fb_puts(fd, "|candidates=" as *u8); fb_putn(fd, cands)
189 fb_puts(fd, "|named=" as *u8); fb_putn(fd, named)
190 fb_puts(fd, "|failed_seeds=" as *u8); fb_putn(fd, failed_seeds)
191 fb_puts(fd, "\n" as *u8)
192 return 0
193}