nx_verdictlog_lib.nx source
↩ module page · 1480 lines · 67154 B
1// nx_verdictlog_lib.nx -- THE VERDICT-LOG RULER: read a detector's own recorded verdicts and say
2// what its output is worth.
3//
4// FM3 and FM4 of /compare/failmodes, and ONE ruler for both because both ask the same primitive
5// question of the same artifacts: WHAT VERDICT DID THIS DETECTOR EMIT, AND WHEN. FM3 reads one
6// stream through time (is this detector's output informative). FM4 reads two streams of the same
7// subject side by side (do two vantages agree). Splitting the parser between them would be the
8// duplicate-ruler defect, and the census and its gate would then be free to disagree about what a
9// verdict IS.
10//
11// ---------------------------------------------------------------------------------------------
12// FM3 -- THE ACCEPT RULE, QUOTED VERBATIM FROM failmodes.plan BEFORE THE WORK BEGAN:
13// "run it over the EXISTING detector fleet and publish the four numbers per detector; the rung is
14// accepted only if at least one currently-shipping detector is shown to score badly enough to
15// justify changing or retiring it. A scorer that flatters every incumbent has measured nothing."
16//
17// THE FOUR NUMBERS ARE SRE'S -- precision, recall, detection time, reset time -- and EACH IS
18// THREE-STATE. That is not a softening of the rule, it is the only honest way to publish it, and
19// the reason is measured rather than assumed: A VERDICT LOG RECORDS WHAT THE DETECTOR SAID AND
20// NEVER WHETHER IT WAS RIGHT. Nothing in this estate joins a detector's firing to an adjudication,
21// so a precision figure computed from these files would be a CONSTANT WEARING THE SHAPE OF A
22// MEASUREMENT. Each axis therefore carries either a value or a NAMED abstention:
23//
24// precision UNINFORMATIVE-CONSTANT the output never varies, so it carries no
25// information and its precision equals the base rate.
26// This is an EXACT statement, not an estimate.
27// UNOBSERVABLE-NO-ADJUDICATION otherwise: nothing joins a firing to a truth.
28// recall UNVERIFIED-NEVER-FIRED the detector has produced no positive in its whole
29// recorded history, so nothing has demonstrated it
30// CAN fire. A green that never had a corresponding
31// red is unverified.
32// UNOBSERVABLE-NO-GROUND-TRUTH otherwise: a missed event leaves no trace here.
33// detect_bound_s <n> the largest observed interval between consecutive
34// runs. A BOUND, and labelled one: a condition that
35// becomes visible just after a run waits at most this
36// long to be seen.
37// UNOBSERVABLE-NO-TIMESTAMP the stream carries no declared timestamp key.
38// reset <n> the largest observed number of runs from a
39// non-green to the next green.
40// LATCHED the trailing non-green episode never returns to
41// green. SRE's short window exists exactly so an
42// alert STOPS when the burn stops; one that cannot
43// stop is the detector everyone learns to ignore.
44// NO-EPISODE it has never left green, so there is nothing to
45// reset from.
46//
47// NO THRESHOLD IS INTRODUCED ANYWHERE. Every predicate here is exact -- constant, never-fired,
48// still-open -- so there is no bar to tune and no bar to flatter. The DISTRIBUTION of firing rates
49// is published beside the partition so a reader can see for themselves whether the signal saturates.
50//
51// ---------------------------------------------------------------------------------------------
52// FM4 -- THE ACCEPT RULE, QUOTED VERBATIM FROM failmodes.plan BEFORE THE WORK BEGAN:
53// "the organ must report DISAGREE only when the two vantages are observed within a bounded window
54// of each other, and must emit UNOBSERVABLE rather than AGREE when either vantage is missing --
55// an axis that cannot see must abstain, not acquit."
56//
57// Both halves are enforced structurally rather than by discipline. There is exactly one place where
58// AGREE or DISAGREE can be returned, and control only reaches it after both observations exist,
59// both carry a timestamp, both classify to a known state, and their separation is inside the
60// declared window. Every earlier exit returns an UNOBSERVABLE that NAMES WHICH conjunct failed,
61// because a compound assertion that will not name its failing conjunct is a false-alarm generator.
62//
63// ⚠ THE DETECTOR'S OWN PATH HAS THE DEFECT IT HUNTS, and saying so is part of shipping it: this
64// organ reads two RECORDINGS. If a vantage stops recording, its stream simply stops -- which is the
65// heartbeat traversing the working link. That failure is visible here only as UNOBSERVABLE, never
66// as DISAGREE, which is the conservative direction but is not the same as seeing it.
67//
68// ---------------------------------------------------------------------------------------------
69// WHY THIS IS A READER OF EXISTING LOGS AND NOT A NEW COLLECTOR: a second timescale is usually a new
70// reader of an old log. Every byte this organ consumes is already being written by beats that
71// already run, so it adds no probe traffic, no polling and no load to a box whose saturation is
72// itself a shipping criterion.
73//
74// SEARCHED BEFORE BUILDING (the retrieval check, not a discovery): nx_capsearch was asked for both
75// capabilities. "alert quality precision recall detection reset score detector" returns
76// nx_recall_eval (an IR ruler over ranked results), nx_recall_board and nx_recall_bench -- all about
77// retrieval recall, none about detector quality. "two vantage gray failure differential
78// observability compare health" returns nx_edge_vantage, which PROBES ONE vantage from the edge and
79// is a producer this ruler could later be pointed at, plus nx_route_diff which is a route-set shrink
80// detector. There is no incumbent for either question.
81// ---------------------------------------------------------------------------------------------
82
83import "nx_syscalls.nx"
84import "nx_gatekit_lib.nx"
85
86// ---- byte constants ---------------------------------------------------------------------------
87const VL_TAB: i64 = 9
88const VL_NL: i64 = 10
89const VL_QUOTE: i64 = 34
90const VL_HASH: i64 = 35
91const VL_MINUS: i64 = 45
92const VL_DOT: i64 = 46
93const VL_ZERO: i64 = 48
94const VL_NINE: i64 = 57
95const VL_SEMI: i64 = 59
96const VL_UC_A: i64 = 65
97const VL_UC_Z: i64 = 90
98const VL_USCORE: i64 = 95
99const VL_LC_A: i64 = 97
100const VL_LC_Z: i64 = 122
101const VL_BAR: i64 = 124
102const VL_CASEGAP: i64 = 32
103const VL_B10: i64 = 10
104const VL_MODE_0644: i64 = 420
105const VL_NUMBUF: i64 = 32
106const VL_SMALL: i64 = 16
107const VL_PATHBUF: i64 = 4096
108const VL_DENTBUF: i64 = 65536
109const VL_DT_DIR: i64 = 4
110const VL_DENT_RECLEN_OFF: i64 = 16
111const VL_DENT_TYPE_OFF: i64 = 18
112const VL_DENT_NAME_OFF: i64 = 19
113const VL_BYTE_SHIFT: i64 = 8
114// getdents64. NAMED because a raw syscall number is the one magic value whose every wrong variant
115// fails identically: this estate has already measured a case where the NUMBER was wrong and every
116// argument variant was equally wrong, so their agreement read as a confident finding about the host.
117const VL_SYS_GETDENTS64: i64 = 217
118
119// The vantage record's field offsets. A HAND-WRITTEN FIELD INDEX BESIDE AN N-SLOT RECORD IS A SECOND
120// COPY OF THAT RECORD'S SHAPE, and the two drift silently: add a field, forget one index, and the
121// parser reads the wrong slot while still compiling and still appearing to work.
122const VL_VAN_SUBJ_OFF: i64 = 0
123const VL_VAN_SUBJ_LEN: i64 = 1
124const VL_VAN_A_OFF: i64 = 2
125const VL_VAN_A_LEN: i64 = 3
126const VL_VAN_B_OFF: i64 = 4
127const VL_VAN_B_LEN: i64 = 5
128const VL_VAN_WIN: i64 = 6
129
130// ---- the three states, plus the two things that are NEITHER a state nor an error ---------------
131const VL_GREEN: i64 = 0
132const VL_RED: i64 = 1
133const VL_ABSTAIN: i64 = 2
134const VL_UNKWORD: i64 = 3 // a verdict key was present and its word is not declared vocabulary
135const VL_NOVERDICT: i64 = 4 // the line carries no verdict key at all
136
137// ---- vocabulary layout ------------------------------------------------------------------------
138const VL_MAXVOC: i64 = 256
139const VL_MAXVAN: i64 = 64
140const VL_VANSLOTS: i64 = 7
141
142const VL_C_NVKEY: i64 = 0
143const VL_C_NTSKEY: i64 = 1
144const VL_C_NGREEN: i64 = 2
145const VL_C_NRED: i64 = 3
146const VL_C_NABST: i64 = 4
147const VL_C_NEXT: i64 = 5
148const VL_C_NVAN: i64 = 6
149const VL_C_REJECT: i64 = 7
150const VL_C_OVER: i64 = 8
151const VL_C_MINRUNS: i64 = 9
152const VL_C_MAXFILES: i64 = 10
153const VL_C_STRIDE: i64 = 11
154
155const VL_C_VKBASE: i64 = 32
156const VL_C_TSBASE: i64 = 544
157const VL_C_GRBASE: i64 = 1056
158const VL_C_RDBASE: i64 = 1568
159const VL_C_ABBASE: i64 = 2080
160const VL_C_EXBASE: i64 = 2592
161const VL_C_VANBASE: i64 = 3104
162const VL_C_LEN: i64 = 3584
163
164// ---- per-file series result -------------------------------------------------------------------
165const VL_R_RUNS: i64 = 0
166const VL_R_GREEN: i64 = 1
167const VL_R_RED: i64 = 2
168const VL_R_ABST: i64 = 3
169const VL_R_UNK: i64 = 4
170const VL_R_TSRUNS: i64 = 5
171const VL_R_TSFIRST: i64 = 6
172const VL_R_TSLAST: i64 = 7
173const VL_R_MAXGAP: i64 = 8
174const VL_R_EPISODES: i64 = 9
175const VL_R_MAXRESET: i64 = 10
176const VL_R_LATCHED: i64 = 11
177const VL_R_LASTSTATE: i64 = 12
178const VL_R_LASTTS: i64 = 13
179const VL_R_BYTES: i64 = 14
180const VL_R_NONMONO: i64 = 15
181const VL_R_UNKOFF: i64 = 16 // offset of the first undeclared verdict word, into the file buffer
182const VL_R_UNKLEN: i64 = 17
183const VL_R_LEN: i64 = 20
184
185// ---- the partition over enumerated files ------------------------------------------------------
186const VL_B_SCORED: i64 = 0 // has both green and non-green runs: the output varies
187const VL_B_CONSTNG: i64 = 1 // every run non-green: never once green -- OFFENDER
188const VL_B_CONSTGR: i64 = 2 // every run green: never fired, so its recall is unverified
189const VL_B_UNKVOCAB: i64 = 3 // speaks a verdict dialect this ruler cannot read
190const VL_B_SNAPSHOT: i64 = 4 // one run: a level cannot express a trajectory
191const VL_B_NOVERDICT: i64 = 5 // carries no verdict vocabulary at all
192const VL_B_UNREADABLE: i64 = 6
193const VL_NBUCKET: i64 = 7
194
195// ---- census totals ----------------------------------------------------------------------------
196const VL_T_FILES: i64 = 0
197const VL_T_SKIPPED: i64 = 1
198const VL_T_BYTES: i64 = 2
199const VL_T_BUCKET0: i64 = 3 // .. VL_T_BUCKET0 + VL_NBUCKET - 1
200const VL_T_LATCHED: i64 = 10 // a SEPARATE AXIS, deliberately not a partition member
201const VL_T_OFFEND: i64 = 11
202const VL_T_HIST0: i64 = 12 // .. VL_T_HIST0 + VL_NHIST - 1
203const VL_NHIST: i64 = 11
204const VL_T_LEN: i64 = 32
205
206// ---- two-vantage outcomes ---------------------------------------------------------------------
207const VL_V_AGREE: i64 = 0
208const VL_V_DISAGREE: i64 = 1
209const VL_V_U_AMISS: i64 = 2
210const VL_V_U_BMISS: i64 = 3
211const VL_V_U_ANOOBS: i64 = 4
212const VL_V_U_BNOOBS: i64 = 5
213const VL_V_U_WINDOW: i64 = 6
214const VL_V_U_ABSTAIN: i64 = 7
215const VL_NVOUT: i64 = 8
216
217const VL_RATCHET_ALLOW: i64 = 0
218const VL_RATCHET_REFUSE: i64 = 1
219const VL_RATCHET_UNMEAS: i64 = 3
220
221const VL_FNV_OFFSET: i64 = 2166136261
222const VL_FNV_PRIME: i64 = 16777619
223const VL_FNV_MASK32: i64 = 4294967295
224const VL_HASH_LOAD: i64 = 4
225
226// ================================================================================================
227// primitives
228// ================================================================================================
229func vl_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
230func vl_err(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 }
231
232func vl_putn(v: i64) -> i64 {
233 let b: *u8 = sys_mmap(VL_NUMBUF)
234 var x: i64 = v
235 var neg: i64 = 0
236 if x < 0 { neg = 1; x = 0 - x }
237 var i: i64 = VL_NUMBUF
238 if x == 0 { i = i - 1; b[i] = VL_ZERO as u8 }
239 while x > 0 {
240 i = i - 1
241 b[i] = ((x % VL_B10) + VL_ZERO) as u8
242 x = x / VL_B10
243 }
244 if neg == 1 { i = i - 1; b[i] = VL_MINUS as u8 }
245 sys_write(1, ((b as i64) + i) as *u8, VL_NUMBUF - i)
246 sys_munmap(b, VL_NUMBUF)
247 return 0
248}
249
250func vl_nl() -> i64 {
251 let b: *u8 = sys_mmap(VL_SMALL)
252 b[0] = VL_NL as u8
253 sys_write(1, b, 1)
254 sys_munmap(b, VL_SMALL)
255 return 0
256}
257
258func vl_lower(c: i64) -> i64 {
259 if c >= VL_UC_A {
260 if c <= VL_UC_Z { return c + VL_CASEGAP }
261 }
262 return c
263}
264
265func vl_is_alpha(c: i64) -> i64 {
266 if c >= VL_UC_A { if c <= VL_UC_Z { return 1 } }
267 if c >= VL_LC_A { if c <= VL_LC_Z { return 1 } }
268 return 0
269}
270
271func vl_wordch(c: i64) -> i64 {
272 if vl_is_alpha(c) == 1 { return 1 }
273 if c == VL_MINUS { return 1 }
274 if c == VL_USCORE { return 1 }
275 return 0
276}
277
278func vl_find_byte(buf: *u8, s: i64, e: i64, b: i64) -> i64 {
279 var i: i64 = s
280 while i < e {
281 if (buf[i] as i64) == b { return i }
282 i = i + 1
283 }
284 return 0 - 1
285}
286
287// Exact compare of buf[s,e) against a NUL-terminated literal, case SENSITIVE.
288func vl_range_eq(buf: *u8, s: i64, e: i64, lit: *u8) -> i64 {
289 var i: i64 = 0
290 while lit[i] != (0 as u8) {
291 if s + i >= e { return 0 }
292 if buf[s + i] != lit[i] { return 0 }
293 i = i + 1
294 }
295 if s + i != e { return 0 }
296 return 1
297}
298
299// Case-INSENSITIVE compare of buf[s,e) against conf[off, off+len).
300func vl_ci_eq(buf: *u8, s: i64, e: i64, conf: *u8, off: i64, len: i64) -> i64 {
301 if e - s != len { return 0 }
302 var i: i64 = 0
303 while i < len {
304 if vl_lower(buf[s + i] as i64) != vl_lower(conf[off + i] as i64) { return 0 }
305 i = i + 1
306 }
307 return 1
308}
309
310// Earliest case-insensitive occurrence of conf[off,off+len) inside buf[s,e), or -1.
311func vl_ci_find(buf: *u8, s: i64, e: i64, conf: *u8, off: i64, len: i64) -> i64 {
312 if len <= 0 { return 0 - 1 }
313 if e - s < len { return 0 - 1 }
314 let last: i64 = e - len
315 var i: i64 = s
316 while i <= last {
317 var j: i64 = 0
318 var ok: i64 = 1
319 while j < len {
320 if vl_lower(buf[i + j] as i64) != vl_lower(conf[off + j] as i64) { ok = 0; j = len } else { j = j + 1 }
321 }
322 if ok == 1 { return i }
323 i = i + 1
324 }
325 return 0 - 1
326}
327
328func vl_atoi(buf: *u8, s: i64, e: i64) -> i64 {
329 var v: i64 = 0
330 var any: i64 = 0
331 var i: i64 = s
332 while i < e {
333 let c: i64 = buf[i] as i64
334 if c >= VL_ZERO {
335 if c <= VL_NINE { v = v * VL_B10 + (c - VL_ZERO); any = 1 }
336 }
337 i = i + 1
338 }
339 if any == 0 { return 0 - 1 }
340 return v
341}
342
343func vl_fnv_buf(buf: *u8, n: i64) -> i64 {
344 var h: i64 = VL_FNV_OFFSET
345 var i: i64 = 0
346 while i < n {
347 h = h ^ (buf[i] as i64)
348 h = (h * VL_FNV_PRIME) & VL_FNV_MASK32
349 i = i + 1
350 }
351 return h
352}
353
354// Content hash of a whole file, or -1 when it cannot be read. Exists so a guard that means
355// "unchanged" can HASH: SIZE IS NEVER AN IDENTITY, and a same-size rewrite sails past a byte-count
356// comparison. The ratchet's do-not-launder tooth uses this and not a size.
357func vl_fnv_file(path: *u8) -> i64 {
358 let n: *i64 = sys_mmap(VL_SMALL) as *i64
359 let b: *u8 = sys_read_file(path, n)
360 if n[0] < 0 { return 0 - 1 }
361 if n[0] == 0 { return 0 }
362 return vl_fnv_buf(b, n[0])
363}
364
365// ================================================================================================
366// the conf
367// ================================================================================================
368func vl_take(conf: *u8, voc: *i64, cnt_slot: i64, base: i64, s: i64, e: i64) -> i64 {
369 let k: i64 = voc[cnt_slot]
370 if k >= VL_MAXVOC { voc[VL_C_OVER] = voc[VL_C_OVER] + 1; return 0 }
371 voc[base + k * 2] = s
372 voc[base + k * 2 + 1] = e - s
373 voc[cnt_slot] = k + 1
374 return 1
375}
376
377func vl_conf_load(confp: *u8, clen: *i64, voc: *i64) -> *u8 {
378 var z: i64 = 0
379 while z < VL_C_LEN { voc[z] = 0; z = z + 1 }
380 voc[VL_C_MINRUNS] = 0 - 1
381 voc[VL_C_MAXFILES] = 0 - 1
382 voc[VL_C_STRIDE] = 0 - 1
383 let conf: *u8 = sys_read_file(confp, clen)
384 if clen[0] <= 0 {
385 vl_err("nx_verdictlog: cannot read the vocabulary conf at " as *u8)
386 vl_err(confp)
387 vl_err(" -- pass the path to knowledge/verdictlog.conf (a bare path resolves against the CWD, so run from the nishihost root or give an absolute path)\n" as *u8)
388 return 0 as *u8
389 }
390 var p: i64 = 0
391 while p < clen[0] {
392 let eol: i64 = gk_eol(conf, p, clen[0])
393 if eol > p {
394 let f: i64 = conf[p] as i64
395 var skip: i64 = 0
396 if f == VL_SEMI { skip = 1 }
397 if f == VL_HASH { skip = 1 }
398 if skip == 0 {
399 let b1: i64 = vl_find_byte(conf, p, eol, VL_BAR)
400 if b1 < 0 { voc[VL_C_REJECT] = voc[VL_C_REJECT] + 1 } else {
401 var handled: i64 = 0
402 if vl_range_eq(conf, p, b1, "vkey" as *u8) == 1 { handled = 1; vl_take(conf, voc, VL_C_NVKEY, VL_C_VKBASE, b1 + 1, eol) }
403 if vl_range_eq(conf, p, b1, "tskey" as *u8) == 1 { handled = 1; vl_take(conf, voc, VL_C_NTSKEY, VL_C_TSBASE, b1 + 1, eol) }
404 if vl_range_eq(conf, p, b1, "green" as *u8) == 1 { handled = 1; vl_take(conf, voc, VL_C_NGREEN, VL_C_GRBASE, b1 + 1, eol) }
405 if vl_range_eq(conf, p, b1, "red" as *u8) == 1 { handled = 1; vl_take(conf, voc, VL_C_NRED, VL_C_RDBASE, b1 + 1, eol) }
406 if vl_range_eq(conf, p, b1, "abstain" as *u8) == 1 { handled = 1; vl_take(conf, voc, VL_C_NABST, VL_C_ABBASE, b1 + 1, eol) }
407 if vl_range_eq(conf, p, b1, "ext" as *u8) == 1 { handled = 1; vl_take(conf, voc, VL_C_NEXT, VL_C_EXBASE, b1 + 1, eol) }
408 if vl_range_eq(conf, p, b1, "vantage" as *u8) == 1 {
409 handled = 1
410 let b2: i64 = vl_find_byte(conf, b1 + 1, eol, VL_BAR)
411 var bad: i64 = 0
412 if b2 < 0 { bad = 1 }
413 if bad == 0 {
414 let b3: i64 = vl_find_byte(conf, b2 + 1, eol, VL_BAR)
415 if b3 < 0 { bad = 1 }
416 if bad == 0 {
417 let b4: i64 = vl_find_byte(conf, b3 + 1, eol, VL_BAR)
418 if b4 < 0 { bad = 1 }
419 if bad == 0 {
420 let w: i64 = vl_atoi(conf, b4 + 1, eol)
421 if w <= 0 { bad = 1 }
422 if bad == 0 {
423 let k: i64 = voc[VL_C_NVAN]
424 if k >= VL_MAXVAN { voc[VL_C_OVER] = voc[VL_C_OVER] + 1 } else {
425 let vb: i64 = VL_C_VANBASE + k * VL_VANSLOTS
426 voc[vb + VL_VAN_SUBJ_OFF] = b1 + 1
427 voc[vb + VL_VAN_SUBJ_LEN] = b2 - b1 - 1
428 voc[vb + VL_VAN_A_OFF] = b2 + 1
429 voc[vb + VL_VAN_A_LEN] = b3 - b2 - 1
430 voc[vb + VL_VAN_B_OFF] = b3 + 1
431 voc[vb + VL_VAN_B_LEN] = b4 - b3 - 1
432 voc[vb + VL_VAN_WIN] = w
433 voc[VL_C_NVAN] = k + 1
434 }
435 }
436 }
437 }
438 }
439 if bad == 1 { voc[VL_C_REJECT] = voc[VL_C_REJECT] + 1 }
440 }
441 if vl_range_eq(conf, p, b1, "param" as *u8) == 1 {
442 handled = 1
443 let pb: i64 = vl_find_byte(conf, b1 + 1, eol, VL_BAR)
444 if pb < 0 { voc[VL_C_REJECT] = voc[VL_C_REJECT] + 1 } else {
445 let v: i64 = vl_atoi(conf, pb + 1, eol)
446 var known: i64 = 0
447 if vl_range_eq(conf, b1 + 1, pb, "min_runs_series" as *u8) == 1 { voc[VL_C_MINRUNS] = v; known = 1 }
448 if vl_range_eq(conf, b1 + 1, pb, "max_files" as *u8) == 1 { voc[VL_C_MAXFILES] = v; known = 1 }
449 if vl_range_eq(conf, b1 + 1, pb, "path_stride" as *u8) == 1 { voc[VL_C_STRIDE] = v; known = 1 }
450 if known == 0 { voc[VL_C_REJECT] = voc[VL_C_REJECT] + 1 }
451 }
452 }
453 if handled == 0 { voc[VL_C_REJECT] = voc[VL_C_REJECT] + 1 }
454 }
455 }
456 }
457 p = eol + 1
458 }
459 // FAIL CLOSED, AND NAME THE MISSING CONJUNCT. Each of these would silently corrupt the census in
460 // a different direction, so each refuses separately rather than through one compound message.
461 if voc[VL_C_NVKEY] <= 0 {
462 vl_err("nx_verdictlog: the conf declares ZERO verdict keys, so every line would read NO-VERDICT and every log would be scored empty. Add vkey| rows to " as *u8); vl_err(confp); vl_err("\n" as *u8)
463 return 0 as *u8
464 }
465 if voc[VL_C_NGREEN] <= 0 {
466 vl_err("nx_verdictlog: the conf declares ZERO green words, so every detector would score CONSTANT-NONGREEN and the whole fleet would read as an offender. Add green| rows to " as *u8); vl_err(confp); vl_err("\n" as *u8)
467 return 0 as *u8
468 }
469 if voc[VL_C_NRED] <= 0 {
470 vl_err("nx_verdictlog: the conf declares ZERO red words, so no firing could ever be recognised and every detector would score CONSTANT-GREEN. Add red| rows to " as *u8); vl_err(confp); vl_err("\n" as *u8)
471 return 0 as *u8
472 }
473 if voc[VL_C_NEXT] <= 0 {
474 vl_err("nx_verdictlog: the conf declares ZERO file extensions, so the census would open nothing and report a healthy fleet of size zero. Add ext| rows to " as *u8); vl_err(confp); vl_err("\n" as *u8)
475 return 0 as *u8
476 }
477 if voc[VL_C_MINRUNS] < 0 {
478 vl_err("nx_verdictlog: the conf sets no min_runs_series -- add param|min_runs_series|<n> to " as *u8); vl_err(confp); vl_err("\n" as *u8)
479 return 0 as *u8
480 }
481 if voc[VL_C_MAXFILES] < 0 {
482 vl_err("nx_verdictlog: the conf sets no max_files -- add param|max_files|<n> to " as *u8); vl_err(confp); vl_err("\n" as *u8)
483 return 0 as *u8
484 }
485 if voc[VL_C_STRIDE] < 0 {
486 vl_err("nx_verdictlog: the conf sets no path_stride -- add param|path_stride|<n> to " as *u8); vl_err(confp); vl_err("\n" as *u8)
487 return 0 as *u8
488 }
489 return conf
490}
491
492// ================================================================================================
493// reading one line
494// ================================================================================================
495// Earliest occurrence of ANY key in a declared table. Returns the position and writes the matched
496// key's length into klen[0]. EARLIEST, not last: a greedy scan takes whatever a JSON payload
497// repeated at the end of the line, and a parser that reads the DATA as the ANSWER is a defect this
498// estate has already paid for three times in one day.
499func vl_key_at(conf: *u8, voc: *i64, base: i64, n: i64, buf: *u8, s: i64, e: i64, klen: *i64) -> i64 {
500 var best: i64 = 0 - 1
501 var bl: i64 = 0
502 var k: i64 = 0
503 while k < n {
504 let off: i64 = voc[base + k * 2]
505 let len: i64 = voc[base + k * 2 + 1]
506 let p: i64 = vl_ci_find(buf, s, e, conf, off, len)
507 if p >= 0 {
508 var take: i64 = 0
509 if best < 0 { take = 1 }
510 if best >= 0 { if p < best { take = 1 } }
511 if take == 1 { best = p; bl = len }
512 }
513 k = k + 1
514 }
515 klen[0] = bl
516 return best
517}
518
519func vl_tok_in(conf: *u8, voc: *i64, base: i64, n: i64, buf: *u8, s: i64, e: i64) -> i64 {
520 var k: i64 = 0
521 while k < n {
522 if vl_ci_eq(buf, s, e, conf, voc[base + k * 2], voc[base + k * 2 + 1]) == 1 { return 1 }
523 k = k + 1
524 }
525 return 0
526}
527
528// The verdict state of ONE line. span[0],span[1] receive the verdict word's range so the report can
529// NAME an undeclared word instead of merely counting it -- the reason must travel with the count.
530func vl_line_state(conf: *u8, voc: *i64, buf: *u8, s: i64, e: i64, span: *i64) -> i64 {
531 span[0] = 0 - 1
532 span[1] = 0 - 1
533 let klen: *i64 = sys_mmap(VL_SMALL) as *i64
534 let at: i64 = vl_key_at(conf, voc, VL_C_VKBASE, voc[VL_C_NVKEY], buf, s, e, klen)
535 if at < 0 { sys_munmap(klen as *u8, VL_SMALL); return VL_NOVERDICT }
536 var ws: i64 = at + klen[0]
537 sys_munmap(klen as *u8, VL_SMALL)
538 if ws < e { if (buf[ws] as i64) == VL_QUOTE { ws = ws + 1 } }
539 var we: i64 = ws
540 var stop: i64 = 0
541 while stop == 0 {
542 if we >= e { stop = 1 } else {
543 if vl_wordch(buf[we] as i64) == 1 { we = we + 1 } else { stop = 1 }
544 }
545 }
546 span[0] = ws
547 span[1] = we
548 if we <= ws { return VL_UNKWORD }
549 if vl_tok_in(conf, voc, VL_C_GRBASE, voc[VL_C_NGREEN], buf, ws, we) == 1 { return VL_GREEN }
550 if vl_tok_in(conf, voc, VL_C_RDBASE, voc[VL_C_NRED], buf, ws, we) == 1 { return VL_RED }
551 if vl_tok_in(conf, voc, VL_C_ABBASE, voc[VL_C_NABST], buf, ws, we) == 1 { return VL_ABSTAIN }
552 return VL_UNKWORD
553}
554
555func vl_line_ts(conf: *u8, voc: *i64, buf: *u8, s: i64, e: i64) -> i64 {
556 let klen: *i64 = sys_mmap(VL_SMALL) as *i64
557 let at: i64 = vl_key_at(conf, voc, VL_C_TSBASE, voc[VL_C_NTSKEY], buf, s, e, klen)
558 if at < 0 { sys_munmap(klen as *u8, VL_SMALL); return 0 - 1 }
559 var p: i64 = at + klen[0]
560 sys_munmap(klen as *u8, VL_SMALL)
561 if p < e { if (buf[p] as i64) == VL_QUOTE { p = p + 1 } }
562 var v: i64 = 0
563 var any: i64 = 0
564 var stop: i64 = 0
565 while stop == 0 {
566 if p >= e { stop = 1 } else {
567 let c: i64 = buf[p] as i64
568 var digit: i64 = 0
569 if c >= VL_ZERO { if c <= VL_NINE { digit = 1 } }
570 if digit == 1 { v = v * VL_B10 + (c - VL_ZERO); any = 1; p = p + 1 } else { stop = 1 }
571 }
572 }
573 if any == 0 { return 0 - 1 }
574 return v
575}
576
577// ================================================================================================
578// reading one stream
579// ================================================================================================
580func vl_series_reset(sr: *i64) -> i64 {
581 var i: i64 = 0
582 while i < VL_R_LEN { sr[i] = 0; i = i + 1 }
583 sr[VL_R_TSFIRST] = 0 - 1
584 sr[VL_R_TSLAST] = 0 - 1
585 sr[VL_R_LASTSTATE] = VL_NOVERDICT
586 sr[VL_R_LASTTS] = 0 - 1
587 sr[VL_R_UNKOFF] = 0 - 1
588 sr[VL_R_UNKLEN] = 0
589 return 0
590}
591
592// Scan one verdict stream. Returns the file buffer (so the caller can print a named word out of it)
593// or 0 when unreadable. EVERY line is examined: no cap, no window, no sample.
594func vl_series(path: *u8, conf: *u8, voc: *i64, sr: *i64) -> *u8 {
595 vl_series_reset(sr)
596 // AN ABSENT FILE IS NOT A SILENT ONE, AND MERGING THEM WOULD BE THE BUCKET-NAMED-FOR-HOW-THE-
597 // READER-FAILED DEFECT. Measured on the first smoke run: sys_read_file returns a zero-length
598 // buffer for a path that does not exist, so without this the census reported a missing stream as
599 // NO-VERDICT -- a real detector and a nonexistent one scoring identically.
600 if gk_exists(path) == 0 { return 0 as *u8 }
601 let n: *i64 = sys_mmap(VL_SMALL) as *i64
602 let buf: *u8 = sys_read_file(path, n)
603 if n[0] < 0 { return 0 as *u8 }
604 sr[VL_R_BYTES] = n[0]
605 let span: *i64 = sys_mmap(VL_SMALL) as *i64
606 var open_at: i64 = 0 - 1 // run index at which the current non-green episode opened
607 var p: i64 = 0
608 while p < n[0] {
609 let eol: i64 = gk_eol(buf, p, n[0])
610 if eol > p {
611 let st: i64 = vl_line_state(conf, voc, buf, p, eol, span)
612 if st != VL_NOVERDICT {
613 let idx: i64 = sr[VL_R_RUNS]
614 sr[VL_R_RUNS] = idx + 1
615 sr[VL_R_LASTSTATE] = st
616 if st == VL_GREEN { sr[VL_R_GREEN] = sr[VL_R_GREEN] + 1 }
617 if st == VL_RED { sr[VL_R_RED] = sr[VL_R_RED] + 1 }
618 if st == VL_ABSTAIN { sr[VL_R_ABST] = sr[VL_R_ABST] + 1 }
619 if st == VL_UNKWORD {
620 sr[VL_R_UNK] = sr[VL_R_UNK] + 1
621 if sr[VL_R_UNKOFF] < 0 { sr[VL_R_UNKOFF] = span[0]; sr[VL_R_UNKLEN] = span[1] - span[0] }
622 }
623 // EPISODES: only RED and ABSTAIN open one. An UNDECLARED word is NOT evidence of
624 // firing -- an unrecognised verdict must never fall into a known bucket, and the
625 // bucket it lands in becomes the number somebody plans against.
626 var nongreen: i64 = 0
627 if st == VL_RED { nongreen = 1 }
628 if st == VL_ABSTAIN { nongreen = 1 }
629 if nongreen == 1 {
630 if open_at < 0 { open_at = idx; sr[VL_R_EPISODES] = sr[VL_R_EPISODES] + 1 }
631 }
632 if st == VL_GREEN {
633 if open_at >= 0 {
634 let reset: i64 = idx - open_at
635 if reset > sr[VL_R_MAXRESET] { sr[VL_R_MAXRESET] = reset }
636 open_at = 0 - 1
637 }
638 }
639 let ts: i64 = vl_line_ts(conf, voc, buf, p, eol)
640 if ts >= 0 {
641 sr[VL_R_TSRUNS] = sr[VL_R_TSRUNS] + 1
642 if sr[VL_R_TSFIRST] < 0 { sr[VL_R_TSFIRST] = ts }
643 if sr[VL_R_LASTTS] >= 0 {
644 let gap: i64 = ts - sr[VL_R_LASTTS]
645 if gap >= 0 {
646 if gap > sr[VL_R_MAXGAP] { sr[VL_R_MAXGAP] = gap }
647 } else { sr[VL_R_NONMONO] = sr[VL_R_NONMONO] + 1 }
648 }
649 sr[VL_R_LASTTS] = ts
650 sr[VL_R_TSLAST] = ts
651 }
652 }
653 }
654 p = eol + 1
655 }
656 if open_at >= 0 { sr[VL_R_LATCHED] = 1 }
657 sys_munmap(span as *u8, VL_SMALL)
658 return buf
659}
660
661// ================================================================================================
662// classification -- ONE definition of every bucket and of "offender", asked by the census, by the
663// ratchet and by the gate. THE ORDER CANNOT BE RIGHT IN ONE PLACE AND WRONG IN ANOTHER BECAUSE
664// THERE IS ONLY ONE OF IT.
665// ================================================================================================
666func vl_bucket(sr: *i64, minruns: i64) -> i64 {
667 if sr[VL_R_RUNS] == 0 { return VL_B_NOVERDICT }
668 if sr[VL_R_RUNS] < minruns { return VL_B_SNAPSHOT }
669 if sr[VL_R_UNK] > 0 { return VL_B_UNKVOCAB }
670 if sr[VL_R_GREEN] == 0 { return VL_B_CONSTNG }
671 if sr[VL_R_RED] + sr[VL_R_ABST] == 0 { return VL_B_CONSTGR }
672 return VL_B_SCORED
673}
674
675func vl_bucket_name(b: i64) -> *u8 {
676 if b == VL_B_SCORED { return "SCORED-INFORMATIVE" as *u8 }
677 if b == VL_B_CONSTNG { return "CONSTANT-NONGREEN" as *u8 }
678 if b == VL_B_CONSTGR { return "CONSTANT-GREEN" as *u8 }
679 if b == VL_B_UNKVOCAB { return "UNKNOWN-VOCAB" as *u8 }
680 if b == VL_B_SNAPSHOT { return "SNAPSHOT" as *u8 }
681 if b == VL_B_NOVERDICT { return "NO-VERDICT" as *u8 }
682 return "UNREADABLE" as *u8
683}
684
685func vl_bucket_id(buf: *u8, s: i64, e: i64) -> i64 {
686 var b: i64 = 0
687 while b < VL_NBUCKET {
688 if vl_range_eq(buf, s, e, vl_bucket_name(b)) == 1 { return b }
689 b = b + 1
690 }
691 return 0 - 1
692}
693
694// AN OFFENDER IS A DETECTOR WHOSE OUTPUT CANNOT BE ACTED ON.
695// CONSTANT-NONGREEN -- it has never once been green in its whole recorded history, so its output
696// is a constant and carries no information; this is the permanently-red
697// detector everyone learns to ignore.
698// LATCHED -- it left green and never came back, so it cannot say "again" and cannot
699// say "recovered".
700// LATCHED IS A SEPARATE AXIS, NOT AN EIGHTH BUCKET: a file can be SCORED-INFORMATIVE and latched at
701// the same time, and folding an overlapping class into a partition breaks its reconciliation.
702// CONSTANT-GREEN IS DELIBERATELY NOT AN OFFENDER. A detector that has never fired may simply be
703// watching something healthy; calling that a defect would flag a known-good guard, and a shape
704// checker that flags a known-good guard is worse than none because someone will act on it.
705func vl_is_offender(bucket: i64, latched: i64) -> i64 {
706 if bucket == VL_B_CONSTNG { return 1 }
707 if latched == 1 { return 1 }
708 return 0
709}
710
711// The ADJUDICATED latch. vl_series records the raw observation "the trailing episode never closed",
712// which is true of a one-line file carrying a single RED -- and a single sample cannot express a
713// trajectory, so calling that latched would flag every snapshot in the estate. The axis is therefore
714// bound to the same minimum-run denominator the partition uses: BIND EVERY AGGREGATE ASSERTION TO
715// ITS DENOMINATOR rather than printing the count beside it.
716func vl_latched(sr: *i64, minruns: i64) -> i64 {
717 if sr[VL_R_RUNS] < minruns { return 0 }
718 // 🔴 FOUND BY HAND-ADJUDICATING REAL DATA, NOT BY A FIXTURE, AND IT WAS A FALSE POSITIVE IN v1.
719 // knowledge/status/uiq_sentinel.log emits `verdict=CLEAN` -- a SUCCESS SPELLED OTHERWISE. The
720 // ruler correctly refused to read that word and put the stream in UNKNOWN-VOCAB, whose precision
721 // and recall already report UNSCORED-UNDECLARED-VERDICT-DIALECT. But the latch axis went on
722 // asserting an outcome anyway: an undeclared word neither opens nor closes an episode, so a
723 // trailing run of CLEAN leaves the last RED episode open forever and the stream was named an
724 // offender. 652 runs, 339 of them healthy, flagged as a detector that never recovers.
725 // ⇒ A STREAM WHOSE DIALECT THE RULER HAS DECLARED UNREADABLE MUST BE UNSCORED ON EVERY AXIS, NOT
726 // JUST THE ONES THAT WERE CONVENIENT TO ABSTAIN ON. Asserting a verdict about a subject you have
727 // already said you cannot read is the abstain-not-acquit law broken in the other direction.
728 if sr[VL_R_UNK] > 0 { return 0 }
729 return sr[VL_R_LATCHED]
730}
731
732// ================================================================================================
733// the four numbers, each with its named abstention
734// ================================================================================================
735// A FILE THAT WAS NEVER SCORED MUST NOT REPORT A SCORED ABSTENTION. "UNOBSERVABLE-NO-ADJUDICATION"
736// on a file carrying no verdicts at all would be a bucket named for how the READER failed rather
737// than for what the subject IS, and it would merge a detector we genuinely cannot adjudicate with a
738// file that is not a detector. Each unscorable bucket therefore names its own reason.
739func vl_unscored(bucket: i64) -> *u8 {
740 if bucket == VL_B_NOVERDICT { return "UNSCORED-NOT-A-VERDICT-STREAM" as *u8 }
741 if bucket == VL_B_UNREADABLE { return "UNSCORED-UNREADABLE" as *u8 }
742 if bucket == VL_B_SNAPSHOT { return "UNSCORED-SNAPSHOT-A-LEVEL-CANNOT-EXPRESS-A-TRAJECTORY" as *u8 }
743 if bucket == VL_B_UNKVOCAB { return "UNSCORED-UNDECLARED-VERDICT-DIALECT" as *u8 }
744 return 0 as *u8
745}
746
747func vl_precision(bucket: i64) -> *u8 {
748 let u: *u8 = vl_unscored(bucket)
749 if (u as i64) != 0 { return u }
750 if bucket == VL_B_CONSTNG { return "UNINFORMATIVE-CONSTANT" as *u8 }
751 if bucket == VL_B_CONSTGR { return "UNINFORMATIVE-CONSTANT" as *u8 }
752 return "UNOBSERVABLE-NO-ADJUDICATION" as *u8
753}
754
755func vl_recall(bucket: i64, sr: *i64) -> *u8 {
756 let u: *u8 = vl_unscored(bucket)
757 if (u as i64) != 0 { return u }
758 if sr[VL_R_RED] + sr[VL_R_ABST] == 0 { return "UNVERIFIED-NEVER-FIRED" as *u8 }
759 return "UNOBSERVABLE-NO-GROUND-TRUTH" as *u8
760}
761
762// ================================================================================================
763// enumeration
764// ================================================================================================
765func vl_hasext(name: *u8, conf: *u8, voc: *i64) -> i64 {
766 let l: i64 = gk_len(name)
767 var k: i64 = 0
768 while k < voc[VL_C_NEXT] {
769 let off: i64 = voc[VL_C_EXBASE + k * 2]
770 let len: i64 = voc[VL_C_EXBASE + k * 2 + 1]
771 if l > len + 1 {
772 if (name[l - len - 1] as i64) == VL_DOT {
773 if vl_ci_eq(name, l - len, l, conf, off, len) == 1 { return 1 }
774 }
775 }
776 k = k + 1
777 }
778 return 0
779}
780
781// Walk ONE directory, appending absolute paths of files whose suffix the conf declares.
782// Returns the count, or negative: -1 dir unopenable, -2 cap reached, -3 a path exceeds the stride.
783// EVERY negative must be branched on by the caller; the point of this primitive is that it REFUSES
784// instead of under-reporting.
785//
786// ⚠ WHY gk_dirscan IS NOT COMPOSED HERE, stated rather than left to look like an oversight: that
787// primitive hard-filters `.nx` through gk_isnx and its signature carries no filter slot, so reusing
788// it would mean changing a shared enumerator every gate and census in the estate already forks.
789// The one rule worth copying is copied exactly: ONE getdents64 CALL IS NOT A DIRECTORY LISTING --
790// loop until it returns zero, or a big directory is silently read as a prefix and its total
791// published as fact.
792func vl_dirscan(dir: *u8, conf: *u8, voc: *i64, names: *u8, stride: i64, cap: i64) -> i64 {
793 let fd: i64 = sys_openat_rd(dir)
794 if fd < 0 { return 0 - 1 }
795 let gbuf: *u8 = sys_mmap(VL_DENTBUF)
796 var cnt: i64 = 0
797 var bad: i64 = 0
798 var nread: i64 = __syscall(VL_SYS_GETDENTS64, fd, gbuf, VL_DENTBUF, 0, 0, 0)
799 while nread > 0 {
800 var off: i64 = 0
801 while off < nread {
802 let reclen: i64 = (gbuf[off + VL_DENT_RECLEN_OFF] as i64) | ((gbuf[off + VL_DENT_RECLEN_OFF + 1] as i64) << VL_BYTE_SHIFT)
803 if reclen <= 0 { off = nread }
804 if reclen > 0 {
805 let dtype: i64 = gbuf[off + VL_DENT_TYPE_OFF] as i64
806 let nm: *u8 = ((gbuf as i64) + off + VL_DENT_NAME_OFF) as *u8
807 var take: i64 = 0
808 if dtype != VL_DT_DIR {
809 if vl_hasext(nm, conf, voc) == 1 { take = 1 }
810 }
811 if take == 1 {
812 if cnt >= cap { bad = 0 - 2 }
813 if bad == 0 {
814 let need: i64 = gk_len(dir) + gk_len(nm) + 1
815 if need > stride { bad = 0 - 3 }
816 if bad == 0 {
817 let dst: *u8 = ((names as i64) + cnt * stride) as *u8
818 var o: i64 = gk_cat(dst, 0, dir)
819 o = gk_cat(dst, o, nm)
820 dst[o] = 0 as u8
821 cnt = cnt + 1
822 }
823 }
824 }
825 if take == 0 {
826 if dtype != VL_DT_DIR { voc[VL_C_REJECT] = voc[VL_C_REJECT] + 0 }
827 }
828 off = off + reclen
829 }
830 }
831 if bad != 0 { nread = 0 }
832 if bad == 0 { nread = __syscall(VL_SYS_GETDENTS64, fd, gbuf, VL_DENTBUF, 0, 0, 0) }
833 }
834 sys_close(fd)
835 if bad != 0 { return bad }
836 return cnt
837}
838
839// Count every non-directory entry, so the census can publish how many files its declared extension
840// list SKIPPED. A scope that is not published reads as completeness.
841func vl_dircount(dir: *u8) -> i64 {
842 let fd: i64 = sys_openat_rd(dir)
843 if fd < 0 { return 0 - 1 }
844 let gbuf: *u8 = sys_mmap(VL_DENTBUF)
845 var cnt: i64 = 0
846 var nread: i64 = __syscall(VL_SYS_GETDENTS64, fd, gbuf, VL_DENTBUF, 0, 0, 0)
847 while nread > 0 {
848 var off: i64 = 0
849 while off < nread {
850 let reclen: i64 = (gbuf[off + VL_DENT_RECLEN_OFF] as i64) | ((gbuf[off + VL_DENT_RECLEN_OFF + 1] as i64) << VL_BYTE_SHIFT)
851 if reclen <= 0 { off = nread }
852 if reclen > 0 {
853 let dtype: i64 = gbuf[off + VL_DENT_TYPE_OFF] as i64
854 if dtype != VL_DT_DIR { cnt = cnt + 1 }
855 off = off + reclen
856 }
857 }
858 nread = __syscall(VL_SYS_GETDENTS64, fd, gbuf, VL_DENTBUF, 0, 0, 0)
859 }
860 sys_close(fd)
861 return cnt
862}
863
864// ================================================================================================
865// FM3 -- the census
866// ================================================================================================
867func vl_row(out: *u8, oo: *i64, cap: i64, s: *u8) -> i64 {
868 var o: i64 = oo[0]
869 var i: i64 = 0
870 while s[i] != (0 as u8) {
871 if o < cap { out[o] = s[i]; o = o + 1 }
872 i = i + 1
873 }
874 oo[0] = o
875 return 0
876}
877
878func vl_rown(out: *u8, oo: *i64, cap: i64, v: i64) -> i64 {
879 let b: *u8 = sys_mmap(VL_NUMBUF)
880 var x: i64 = v
881 var neg: i64 = 0
882 if x < 0 { neg = 1; x = 0 - x }
883 var i: i64 = VL_NUMBUF
884 if x == 0 { i = i - 1; b[i] = VL_ZERO as u8 }
885 while x > 0 { i = i - 1; b[i] = ((x % VL_B10) + VL_ZERO) as u8; x = x / VL_B10 }
886 if neg == 1 { i = i - 1; b[i] = VL_MINUS as u8 }
887 var o: i64 = oo[0]
888 while i < VL_NUMBUF {
889 if o < cap { out[o] = b[i]; o = o + 1 }
890 i = i + 1
891 }
892 oo[0] = o
893 sys_munmap(b, VL_NUMBUF)
894 return 0
895}
896
897func vl_rowb(out: *u8, oo: *i64, cap: i64, src: *u8, s: i64, e: i64) -> i64 {
898 var o: i64 = oo[0]
899 var i: i64 = s
900 while i < e {
901 if o < cap { out[o] = src[i]; o = o + 1 }
902 i = i + 1
903 }
904 oo[0] = o
905 return 0
906}
907
908func vl_rowtab(out: *u8, oo: *i64, cap: i64) -> i64 {
909 if oo[0] < cap { out[oo[0]] = VL_TAB as u8; oo[0] = oo[0] + 1 }
910 return 0
911}
912
913func vl_rownl(out: *u8, oo: *i64, cap: i64) -> i64 {
914 if oo[0] < cap { out[oo[0]] = VL_NL as u8; oo[0] = oo[0] + 1 }
915 return 0
916}
917
918// The per-detector worklist row. THE REASON TRAVELS WITH THE COUNT: every number the report
919// aggregates is present here per row, so a worklist entry can be triaged without re-running
920// anything. Field 0 is the bucket and field 1 the latched axis, which is exactly what the ratchet
921// re-asks vl_is_offender with -- it never recomputes the classification for itself.
922func vl_emit_row(out: *u8, oo: *i64, cap: i64, path: *u8, sr: *i64, bucket: i64, fbuf: *u8, latched: i64) -> i64 {
923 vl_row(out, oo, cap, vl_bucket_name(bucket))
924 vl_rowtab(out, oo, cap)
925 vl_rown(out, oo, cap, latched)
926 vl_rowtab(out, oo, cap)
927 vl_row(out, oo, cap, path)
928 vl_rowtab(out, oo, cap)
929 vl_row(out, oo, cap, "runs=" as *u8); vl_rown(out, oo, cap, sr[VL_R_RUNS])
930 vl_rowtab(out, oo, cap)
931 vl_row(out, oo, cap, "green=" as *u8); vl_rown(out, oo, cap, sr[VL_R_GREEN])
932 vl_rowtab(out, oo, cap)
933 vl_row(out, oo, cap, "red=" as *u8); vl_rown(out, oo, cap, sr[VL_R_RED])
934 vl_rowtab(out, oo, cap)
935 vl_row(out, oo, cap, "abstain=" as *u8); vl_rown(out, oo, cap, sr[VL_R_ABST])
936 vl_rowtab(out, oo, cap)
937 vl_row(out, oo, cap, "undeclared=" as *u8); vl_rown(out, oo, cap, sr[VL_R_UNK])
938 vl_rowtab(out, oo, cap)
939 vl_row(out, oo, cap, "precision=" as *u8); vl_row(out, oo, cap, vl_precision(bucket))
940 vl_rowtab(out, oo, cap)
941 vl_row(out, oo, cap, "recall=" as *u8); vl_row(out, oo, cap, vl_recall(bucket, sr))
942 vl_rowtab(out, oo, cap)
943 vl_row(out, oo, cap, "detect_bound_s=" as *u8)
944 if sr[VL_R_TSRUNS] >= 2 { vl_rown(out, oo, cap, sr[VL_R_MAXGAP]) } else { vl_row(out, oo, cap, "UNOBSERVABLE-NO-TIMESTAMP" as *u8) }
945 vl_rowtab(out, oo, cap)
946 vl_row(out, oo, cap, "reset=" as *u8)
947 if latched == 1 { vl_row(out, oo, cap, "LATCHED" as *u8) } else {
948 if sr[VL_R_EPISODES] == 0 { vl_row(out, oo, cap, "NO-EPISODE" as *u8) } else { vl_rown(out, oo, cap, sr[VL_R_MAXRESET]) }
949 }
950 if sr[VL_R_UNKOFF] >= 0 {
951 vl_rowtab(out, oo, cap)
952 vl_row(out, oo, cap, "undeclared_word=" as *u8)
953 vl_rowb(out, oo, cap, fbuf, sr[VL_R_UNKOFF], sr[VL_R_UNKOFF] + sr[VL_R_UNKLEN])
954 }
955 vl_rownl(out, oo, cap)
956 return 0
957}
958
959// Reconciliation and spread, exposed so the referee asks the SAME arithmetic the report prints
960// rather than re-deriving it. A gate that recomputes a partition sum for itself is a second ruler.
961func vl_bucket_sum(tot: *i64) -> i64 {
962 var s: i64 = 0
963 var b: i64 = 0
964 while b < VL_NBUCKET { s = s + tot[VL_T_BUCKET0 + b]; b = b + 1 }
965 return s
966}
967
968func vl_bucket_distinct(tot: *i64) -> i64 {
969 var d: i64 = 0
970 var b: i64 = 0
971 while b < VL_NBUCKET {
972 if tot[VL_T_BUCKET0 + b] > 0 { d = d + 1 }
973 b = b + 1
974 }
975 return d
976}
977
978// How many firing-rate bins are occupied. A SIGNAL THAT FIRES ON EVERYTHING DISCRIMINATES NOTHING,
979// and the only way to see that is the spread -- so the referee can assert the spread rather than
980// trusting that a bar was chosen well. There is no bar here at all, which is the point.
981func vl_hist_distinct(tot: *i64) -> i64 {
982 var d: i64 = 0
983 var h: i64 = 0
984 while h < VL_NHIST {
985 if tot[VL_T_HIST0 + h] > 0 { d = d + 1 }
986 h = h + 1
987 }
988 return d
989}
990
991func vl_hist_bin(sr: *i64) -> i64 {
992 let ng: i64 = sr[VL_R_RED] + sr[VL_R_ABST]
993 if sr[VL_R_RUNS] <= 0 { return 0 }
994 let permil: i64 = ng * 1000 / sr[VL_R_RUNS]
995 var bin: i64 = permil / 100
996 if bin >= VL_NHIST { bin = VL_NHIST - 1 }
997 return bin
998}
999
1000func vl_alert_report(tot: *i64, voc: *i64, dirp: *u8, outp: *u8, wrote: i64) -> i64 {
1001 vl_puts("nx_verdictlog alert_score -- SRE'S FOUR NUMBERS PER DETECTOR, EACH THREE-STATE. NUMBERS AND NO VERDICT: the bar lives in the ratchet, which is a SET OF NAMES.\n" as *u8)
1002 vl_puts("dir=" as *u8); vl_puts(dirp)
1003 vl_puts(" files_scored=" as *u8); vl_putn(tot[VL_T_FILES])
1004 vl_puts(" files_skipped_by_extension=" as *u8); vl_putn(tot[VL_T_SKIPPED])
1005 vl_puts(" bytes=" as *u8); vl_putn(tot[VL_T_BYTES])
1006 vl_puts(" min_runs_series=" as *u8); vl_putn(voc[VL_C_MINRUNS])
1007 vl_puts(" conf_rows_rejected=" as *u8); vl_putn(voc[VL_C_REJECT])
1008 vl_nl()
1009 vl_puts("-- PARTITION over every file the extension list admitted (each lands in exactly one) --\n" as *u8)
1010 var sum: i64 = 0
1011 var b: i64 = 0
1012 while b < VL_NBUCKET {
1013 vl_puts("BUCKET " as *u8); vl_puts(vl_bucket_name(b))
1014 vl_puts(" = " as *u8); vl_putn(tot[VL_T_BUCKET0 + b])
1015 vl_nl()
1016 sum = sum + tot[VL_T_BUCKET0 + b]
1017 b = b + 1
1018 }
1019 vl_puts("PARTITION SUM = " as *u8); vl_putn(sum)
1020 vl_puts(" files_scored = " as *u8); vl_putn(tot[VL_T_FILES])
1021 if sum == tot[VL_T_FILES] { vl_puts(" RECONCILES\n" as *u8) } else { vl_puts(" DOES NOT RECONCILE -- do not publish these numbers\n" as *u8) }
1022 vl_puts("AXIS LATCHED = " as *u8); vl_putn(tot[VL_T_LATCHED])
1023 vl_puts(" -- a SEPARATE AXIS, never a partition member: a stream can be SCORED-INFORMATIVE and latched at once, and folding an overlapping class into a partition breaks its sum\n" as *u8)
1024 vl_puts("offenders (CONSTANT-NONGREEN union LATCHED; CONSTANT-GREEN is deliberately NOT one) = " as *u8)
1025 vl_putn(tot[VL_T_OFFEND]); vl_nl()
1026 // THE DISTRIBUTION IS PUBLISHED BEFORE ANY BAR, and there is no bar: every predicate above is
1027 // exact. A signal that fires on everything discriminates nothing, and the only way to see that
1028 // is to look at the spread.
1029 vl_puts("-- FIRING-RATE DISTRIBUTION over scored streams, per-mil of runs that were non-green (bins of 100) --\n" as *u8)
1030 var h: i64 = 0
1031 while h < VL_NHIST {
1032 vl_puts("HIST " as *u8); vl_putn(h * 100)
1033 vl_puts(" = " as *u8); vl_putn(tot[VL_T_HIST0 + h])
1034 vl_nl()
1035 h = h + 1
1036 }
1037 if voc[VL_C_OVER] > 0 {
1038 vl_puts("VOCABULARY-TABLE-FULL rows_dropped=" as *u8); vl_putn(voc[VL_C_OVER])
1039 vl_puts(" -- the counts above are a FLOOR, not a total\n" as *u8)
1040 }
1041 vl_puts("worklist=" as *u8); vl_puts(outp)
1042 vl_puts(" bytes=" as *u8); vl_putn(wrote)
1043 vl_nl()
1044 return 0
1045}
1046
1047func vl_alert_scan(confp: *u8, dirp: *u8, outp: *u8, tot: *i64) -> i64 {
1048 let clen: *i64 = sys_mmap(VL_SMALL) as *i64
1049 let voc: *i64 = sys_mmap(VL_C_LEN * 8) as *i64
1050 let conf: *u8 = vl_conf_load(confp, clen, voc)
1051 if (conf as i64) == 0 { return 3 }
1052 var z: i64 = 0
1053 while z < VL_T_LEN { tot[z] = 0; z = z + 1 }
1054
1055 let stride: i64 = voc[VL_C_STRIDE]
1056 let cap: i64 = voc[VL_C_MAXFILES]
1057 let names: *u8 = sys_mmap(cap * stride)
1058 let n: i64 = vl_dirscan(dirp, conf, voc, names, stride, cap)
1059 if n < 0 {
1060 vl_puts("ENUMERATION-REFUSED rc=" as *u8); vl_putn(n)
1061 vl_puts(" dir=" as *u8); vl_puts(dirp)
1062 vl_puts(" (-1 the directory could not be opened, -2 max_files reached, -3 a path exceeds path_stride) -- no census is published from a partial enumeration. Raise the parameter in the conf deliberately, or point the census at the directory that exists.\n" as *u8)
1063 return 3
1064 }
1065 // A ZERO-SUBJECT RUN MUST NOT RETURN THE SAME CODE AS A COMPLETE ONE. A census that examined
1066 // nothing and exited 0 is the vacuous-test defect arriving through the front door: every
1067 // downstream reader sees a clean partition of size zero and calls it healthy.
1068 if n == 0 {
1069 vl_puts("EXAMINED-NOTHING dir=" as *u8); vl_puts(dirp)
1070 vl_puts(" -- the enumeration succeeded and admitted ZERO files, so this run says nothing about the fleet. Check the directory path and the ext| rows in the conf.\n" as *u8)
1071 return 3
1072 }
1073 let all: i64 = vl_dircount(dirp)
1074 if all >= n { tot[VL_T_SKIPPED] = all - n }
1075
1076 let outcap: i64 = n * VL_PATHBUF + VL_PATHBUF
1077 let out: *u8 = sys_mmap(outcap)
1078 let oo: *i64 = sys_mmap(VL_SMALL) as *i64
1079 oo[0] = 0
1080 let sr: *i64 = sys_mmap(VL_R_LEN * 8) as *i64
1081
1082 var i: i64 = 0
1083 while i < n {
1084 let pth: *u8 = ((names as i64) + i * stride) as *u8
1085 let fbuf: *u8 = vl_series(pth, conf, voc, sr)
1086 var bucket: i64 = VL_B_UNREADABLE
1087 if (fbuf as i64) != 0 { bucket = vl_bucket(sr, voc[VL_C_MINRUNS]) }
1088 let latched: i64 = vl_latched(sr, voc[VL_C_MINRUNS])
1089 tot[VL_T_FILES] = tot[VL_T_FILES] + 1
1090 tot[VL_T_BUCKET0 + bucket] = tot[VL_T_BUCKET0 + bucket] + 1
1091 tot[VL_T_BYTES] = tot[VL_T_BYTES] + sr[VL_R_BYTES]
1092 if latched == 1 { tot[VL_T_LATCHED] = tot[VL_T_LATCHED] + 1 }
1093 if vl_is_offender(bucket, latched) == 1 { tot[VL_T_OFFEND] = tot[VL_T_OFFEND] + 1 }
1094 if sr[VL_R_RUNS] >= voc[VL_C_MINRUNS] {
1095 let bin: i64 = vl_hist_bin(sr)
1096 tot[VL_T_HIST0 + bin] = tot[VL_T_HIST0 + bin] + 1
1097 }
1098 vl_emit_row(out, oo, outcap, pth, sr, bucket, fbuf, latched)
1099 i = i + 1
1100 }
1101
1102 var wrote: i64 = 0
1103 let fd: i64 = sys_openat_wr(outp, VL_MODE_0644)
1104 if fd >= 0 { wrote = sys_write(fd, out, oo[0]); sys_close(fd) }
1105 if fd < 0 {
1106 vl_puts("WORKLIST-WRITE-FAILED path=" as *u8); vl_puts(outp)
1107 vl_puts(" -- the counts below are complete but nothing can be triaged from them. Check the directory exists and is writable.\n" as *u8)
1108 }
1109 vl_alert_report(tot, voc, dirp, outp, wrote)
1110 return 0
1111}
1112
1113// ================================================================================================
1114// FM3 -- the ratchet. A SET OF NAMES, never a count, self-baselining, and deliberately wrong in the
1115// direction of DOING NOTHING: on a rise it REFUSES and does NOT touch the baseline, because a
1116// ratchet that rewrites its baseline on a rise launders itself green and can never fire twice.
1117// ================================================================================================
1118func vl_row_name(buf: *u8, s: i64, e: i64, span: *i64) -> i64 {
1119 let t1: i64 = vl_find_byte(buf, s, e, VL_TAB)
1120 if t1 < 0 { return 0 - 2 }
1121 let t2: i64 = vl_find_byte(buf, t1 + 1, e, VL_TAB)
1122 if t2 < 0 { return 0 - 2 }
1123 let t3: i64 = vl_find_byte(buf, t2 + 1, e, VL_TAB)
1124 if t3 < 0 { return 0 - 2 }
1125 let b: i64 = vl_bucket_id(buf, s, t1)
1126 if b < 0 { return 0 - 3 }
1127 let latched: i64 = vl_atoi(buf, t1 + 1, t2)
1128 span[0] = t2 + 1
1129 span[1] = t3
1130 return vl_is_offender(b, latched)
1131}
1132
1133func vl_name_hash(buf: *u8, s: i64, e: i64) -> i64 {
1134 var h: i64 = VL_FNV_OFFSET
1135 var i: i64 = s
1136 while i < e {
1137 h = h ^ (buf[i] as i64)
1138 h = (h * VL_FNV_PRIME) & VL_FNV_MASK32
1139 i = i + 1
1140 }
1141 return h
1142}
1143
1144func vl_name_eq(a: *u8, as0: i64, ae: i64, b: *u8, bs: i64, be: i64) -> i64 {
1145 if ae - as0 != be - bs { return 0 }
1146 var i: i64 = 0
1147 while i < ae - as0 {
1148 if a[as0 + i] != b[bs + i] { return 0 }
1149 i = i + 1
1150 }
1151 return 1
1152}
1153
1154func vl_name_index(bl: *u8, blen: i64, hs: *i64, he: *i64, hcap: i64) -> i64 {
1155 var i: i64 = 0
1156 while i < hcap { hs[i] = 0 - 1; he[i] = 0 - 1; i = i + 1 }
1157 var n: i64 = 0
1158 var p: i64 = 0
1159 while p < blen {
1160 let eol: i64 = gk_eol(bl, p, blen)
1161 if eol > p {
1162 var slot: i64 = vl_name_hash(bl, p, eol) % hcap
1163 var placed: i64 = 0
1164 while placed == 0 {
1165 if hs[slot] < 0 { hs[slot] = p; he[slot] = eol; n = n + 1; placed = 1 } else {
1166 if vl_name_eq(bl, hs[slot], he[slot], bl, p, eol) == 1 { placed = 1 } else {
1167 slot = slot + 1
1168 if slot >= hcap { slot = 0 }
1169 }
1170 }
1171 }
1172 }
1173 p = eol + 1
1174 }
1175 return n
1176}
1177
1178func vl_name_in(bl: *u8, hs: *i64, he: *i64, hcap: i64, needle: *u8, ns: i64, ne: i64) -> i64 {
1179 var slot: i64 = vl_name_hash(needle, ns, ne) % hcap
1180 var guard: i64 = 0
1181 while guard < hcap {
1182 if hs[slot] < 0 { return 0 }
1183 if vl_name_eq(bl, hs[slot], he[slot], needle, ns, ne) == 1 { return 1 }
1184 slot = slot + 1
1185 if slot >= hcap { slot = 0 }
1186 guard = guard + 1
1187 }
1188 return 0
1189}
1190
1191func vl_write_set(path: *u8, wl: *u8, cs: *i64, ce: *i64, n: i64) -> i64 {
1192 let fd: i64 = sys_openat_wr(path, VL_MODE_0644)
1193 if fd < 0 { return 0 - 1 }
1194 let nlb: *u8 = sys_mmap(VL_SMALL)
1195 nlb[0] = VL_NL as u8
1196 var i: i64 = 0
1197 while i < n {
1198 sys_write(fd, ((wl as i64) + cs[i]) as *u8, ce[i] - cs[i])
1199 sys_write(fd, nlb, 1)
1200 i = i + 1
1201 }
1202 sys_munmap(nlb, VL_SMALL)
1203 sys_close(fd)
1204 return 0
1205}
1206
1207func vl_alert_ratchet(worklistp: *u8, baselinep: *u8) -> i64 {
1208 let wlen: *i64 = sys_mmap(VL_SMALL) as *i64
1209 let wl: *u8 = sys_read_file(worklistp, wlen)
1210 if wlen[0] <= 0 {
1211 vl_puts("RATCHET UNMEASURED -- no worklist at " as *u8); vl_puts(worklistp)
1212 vl_puts(" . Run the census first: nx_failclass alert_score <conf> <status-dir> <worklist>\n" as *u8)
1213 return VL_RATCHET_UNMEAS
1214 }
1215 let cap: i64 = wlen[0] / 8 + 8
1216 let cs: *i64 = sys_mmap(cap * 8) as *i64
1217 let ce: *i64 = sys_mmap(cap * 8) as *i64
1218 let span: *i64 = sys_mmap(VL_SMALL) as *i64
1219 var ncur: i64 = 0
1220 var nrows: i64 = 0
1221 var nmalformed: i64 = 0
1222 var nunrecognised: i64 = 0
1223 var p: i64 = 0
1224 while p < wlen[0] {
1225 let eol: i64 = gk_eol(wl, p, wlen[0])
1226 if eol > p {
1227 nrows = nrows + 1
1228 let r: i64 = vl_row_name(wl, p, eol, span)
1229 if r == 1 {
1230 if ncur < cap { cs[ncur] = span[0]; ce[ncur] = span[1]; ncur = ncur + 1 }
1231 }
1232 if r == 0 - 2 { nmalformed = nmalformed + 1 }
1233 if r == 0 - 3 { nunrecognised = nunrecognised + 1 }
1234 }
1235 p = eol + 1
1236 }
1237 vl_puts("RATCHET-INPUT rows=" as *u8); vl_putn(nrows)
1238 vl_puts(" offenders=" as *u8); vl_putn(ncur)
1239 vl_puts(" malformed_rows=" as *u8); vl_putn(nmalformed)
1240 vl_puts(" unrecognised_bucket_labels=" as *u8); vl_putn(nunrecognised)
1241 vl_puts(" (offenders here MUST equal the census offenders line -- both ask vl_is_offender, there is only one of it)\n" as *u8)
1242
1243 let blen: *i64 = sys_mmap(VL_SMALL) as *i64
1244 let bl: *u8 = sys_read_file(baselinep, blen)
1245 if blen[0] <= 0 {
1246 vl_write_set(baselinep, wl, cs, ce, ncur)
1247 vl_puts("RATCHET SELF-BASELINED baseline=0 current=" as *u8); vl_putn(ncur)
1248 vl_puts(" new=0 file=" as *u8); vl_puts(baselinep)
1249 vl_puts(" -- adopting this ratchet is non-breaking BY CONSTRUCTION: a detector that is permanently red on day one is one everyone learns to ignore\n" as *u8)
1250 return VL_RATCHET_ALLOW
1251 }
1252
1253 var nbase: i64 = 0
1254 var q: i64 = 0
1255 while q < blen[0] {
1256 let e2: i64 = gk_eol(bl, q, blen[0])
1257 if e2 > q { nbase = nbase + 1 }
1258 q = e2 + 1
1259 }
1260 var hcap: i64 = 1
1261 while hcap < nbase * VL_HASH_LOAD + VL_HASH_LOAD { hcap = hcap * 2 }
1262 let hs: *i64 = sys_mmap(hcap * 8) as *i64
1263 let he: *i64 = sys_mmap(hcap * 8) as *i64
1264 let nidx: i64 = vl_name_index(bl, blen[0], hs, he, hcap)
1265
1266 var nnew: i64 = 0
1267 var i: i64 = 0
1268 while i < ncur {
1269 if vl_name_in(bl, hs, he, hcap, wl, cs[i], ce[i]) == 0 {
1270 nnew = nnew + 1
1271 vl_puts("RATCHET-NEW " as *u8)
1272 sys_write(1, ((wl as i64) + cs[i]) as *u8, ce[i] - cs[i])
1273 vl_nl()
1274 }
1275 i = i + 1
1276 }
1277 vl_puts("RATCHET baseline=" as *u8); vl_putn(nbase)
1278 vl_puts(" distinct_baseline_names=" as *u8); vl_putn(nidx)
1279 vl_puts(" current=" as *u8); vl_putn(ncur)
1280 vl_puts(" new=" as *u8); vl_putn(nnew)
1281 if nnew > 0 {
1282 vl_puts(" REFUSE -- the baseline is NOT rewritten: a ratchet that rewrites on a rise launders itself green. Fix the named detectors, or accept them deliberately by editing " as *u8)
1283 vl_puts(baselinep)
1284 vl_nl()
1285 return VL_RATCHET_REFUSE
1286 }
1287 if ncur < nbase {
1288 vl_write_set(baselinep, wl, cs, ce, ncur)
1289 vl_puts(" TIGHTENED -- the baseline now records the smaller set\n" as *u8)
1290 return VL_RATCHET_ALLOW
1291 }
1292 vl_puts(" ALLOW\n" as *u8)
1293 return VL_RATCHET_ALLOW
1294}
1295
1296// ================================================================================================
1297// FM4 -- two vantages
1298// ================================================================================================
1299// The LAST observation in a stream that carries BOTH a classifiable verdict and a timestamp.
1300// Returns 0 and fills obs[0]=state obs[1]=ts, or -1 unreadable, or -2 no such observation.
1301// BOTH conjuncts are required together: a verdict with no timestamp cannot be placed in time, and a
1302// timestamp with no verdict says nothing about health.
1303func vl_last_obs(path: *u8, conf: *u8, voc: *i64, obs: *i64) -> i64 {
1304 obs[0] = VL_NOVERDICT
1305 obs[1] = 0 - 1
1306 // Same distinction as vl_series: A-UNREADABLE means the stream is not there at all, which is a
1307 // different remedy from a stream that is there and carries nothing timestamped.
1308 if gk_exists(path) == 0 { return 0 - 1 }
1309 let n: *i64 = sys_mmap(VL_SMALL) as *i64
1310 let buf: *u8 = sys_read_file(path, n)
1311 if n[0] < 0 { return 0 - 1 }
1312 let span: *i64 = sys_mmap(VL_SMALL) as *i64
1313 var found: i64 = 0
1314 var p: i64 = 0
1315 while p < n[0] {
1316 let eol: i64 = gk_eol(buf, p, n[0])
1317 if eol > p {
1318 let st: i64 = vl_line_state(conf, voc, buf, p, eol, span)
1319 if st != VL_NOVERDICT {
1320 let ts: i64 = vl_line_ts(conf, voc, buf, p, eol)
1321 if ts >= 0 { obs[0] = st; obs[1] = ts; found = 1 }
1322 }
1323 }
1324 p = eol + 1
1325 }
1326 sys_munmap(span as *u8, VL_SMALL)
1327 if found == 0 { return 0 - 2 }
1328 return 0
1329}
1330
1331func vl_vout_name(v: i64) -> *u8 {
1332 if v == VL_V_AGREE { return "AGREE" as *u8 }
1333 if v == VL_V_DISAGREE { return "DISAGREE" as *u8 }
1334 if v == VL_V_U_AMISS { return "UNOBSERVABLE-A-UNREADABLE" as *u8 }
1335 if v == VL_V_U_BMISS { return "UNOBSERVABLE-B-UNREADABLE" as *u8 }
1336 if v == VL_V_U_ANOOBS { return "UNOBSERVABLE-A-NO-TIMESTAMPED-VERDICT" as *u8 }
1337 if v == VL_V_U_BNOOBS { return "UNOBSERVABLE-B-NO-TIMESTAMPED-VERDICT" as *u8 }
1338 if v == VL_V_U_WINDOW { return "UNOBSERVABLE-OUTSIDE-WINDOW" as *u8 }
1339 return "UNOBSERVABLE-A-VANTAGE-ABSTAINS" as *u8
1340}
1341
1342func vl_state_name(s: i64) -> *u8 {
1343 if s == VL_GREEN { return "GREEN" as *u8 }
1344 if s == VL_RED { return "RED" as *u8 }
1345 if s == VL_ABSTAIN { return "ABSTAIN" as *u8 }
1346 if s == VL_UNKWORD { return "UNDECLARED-WORD" as *u8 }
1347 return "NO-VERDICT" as *u8
1348}
1349
1350// THE ONLY PLACE AGREE OR DISAGREE CAN BE RETURNED. Control reaches the comparison only after every
1351// conjunct holds, and each earlier exit NAMES the conjunct that failed. That is structural, not a
1352// discipline the next editor has to remember.
1353func vl_vantage_cmp(ra: i64, rb: i64, oa: *i64, ob: *i64, window: i64) -> i64 {
1354 if ra == 0 - 1 { return VL_V_U_AMISS }
1355 if rb == 0 - 1 { return VL_V_U_BMISS }
1356 if ra == 0 - 2 { return VL_V_U_ANOOBS }
1357 if rb == 0 - 2 { return VL_V_U_BNOOBS }
1358 if oa[0] == VL_ABSTAIN { return VL_V_U_ABSTAIN }
1359 if ob[0] == VL_ABSTAIN { return VL_V_U_ABSTAIN }
1360 if oa[0] == VL_UNKWORD { return VL_V_U_ABSTAIN }
1361 if ob[0] == VL_UNKWORD { return VL_V_U_ABSTAIN }
1362 var d: i64 = oa[1] - ob[1]
1363 if d < 0 { d = 0 - d }
1364 if d > window { return VL_V_U_WINDOW }
1365 if oa[0] == ob[0] { return VL_V_AGREE }
1366 return VL_V_DISAGREE
1367}
1368
1369func vl_vantage_scan(confp: *u8, outp: *u8, res: *i64) -> i64 {
1370 let clen: *i64 = sys_mmap(VL_SMALL) as *i64
1371 let voc: *i64 = sys_mmap(VL_C_LEN * 8) as *i64
1372 let conf: *u8 = vl_conf_load(confp, clen, voc)
1373 if (conf as i64) == 0 { return 3 }
1374 var z: i64 = 0
1375 while z < VL_NVOUT { res[z] = 0; z = z + 1 }
1376
1377 let nv: i64 = voc[VL_C_NVAN]
1378 vl_puts("nx_verdictlog two_vantage -- GRAY FAILURE IS DIFFERENTIAL OBSERVABILITY AND CANNOT BE SEEN FROM ONE VANTAGE. DISAGREE is returned only when both vantages are observed inside the declared window; every other answer is an UNOBSERVABLE that names its failing conjunct.\n" as *u8)
1379 vl_puts("declared_subjects=" as *u8); vl_putn(nv)
1380 vl_puts(" conf_rows_rejected=" as *u8); vl_putn(voc[VL_C_REJECT])
1381 vl_nl()
1382 if nv <= 0 {
1383 vl_puts("NO SUBJECTS DECLARED -- this organ examined nothing, so its silence is not a clean bill of health. Add vantage| rows to " as *u8)
1384 vl_puts(confp); vl_nl()
1385 return 3
1386 }
1387
1388 let oa: *i64 = sys_mmap(VL_SMALL) as *i64
1389 let ob: *i64 = sys_mmap(VL_SMALL) as *i64
1390 let sa: *i64 = sys_mmap(VL_R_LEN * 8) as *i64
1391 let sb: *i64 = sys_mmap(VL_R_LEN * 8) as *i64
1392 let pa: *u8 = sys_mmap(VL_PATHBUF)
1393 let pb: *u8 = sys_mmap(VL_PATHBUF)
1394 let out: *u8 = sys_mmap(nv * VL_PATHBUF + VL_PATHBUF)
1395 let outcap: i64 = nv * VL_PATHBUF + VL_PATHBUF
1396 let oo: *i64 = sys_mmap(VL_SMALL) as *i64
1397 oo[0] = 0
1398
1399 var i: i64 = 0
1400 while i < nv {
1401 let vb: i64 = VL_C_VANBASE + i * VL_VANSLOTS
1402 var k: i64 = 0
1403 while k < voc[vb + VL_VAN_A_LEN] { pa[k] = conf[voc[vb + VL_VAN_A_OFF] + k]; k = k + 1 }
1404 pa[voc[vb + VL_VAN_A_LEN]] = 0 as u8
1405 var k2: i64 = 0
1406 while k2 < voc[vb + VL_VAN_B_LEN] { pb[k2] = conf[voc[vb + VL_VAN_B_OFF] + k2]; k2 = k2 + 1 }
1407 pb[voc[vb + VL_VAN_B_LEN]] = 0 as u8
1408 let window: i64 = voc[vb + VL_VAN_WIN]
1409
1410 let ra: i64 = vl_last_obs(pa, conf, voc, oa)
1411 let rb: i64 = vl_last_obs(pb, conf, voc, ob)
1412 let verdict: i64 = vl_vantage_cmp(ra, rb, oa, ob, window)
1413 res[verdict] = res[verdict] + 1
1414
1415 vl_puts("VANTAGE " as *u8)
1416 sys_write(1, ((conf as i64) + voc[vb + VL_VAN_SUBJ_OFF]) as *u8, voc[vb + VL_VAN_SUBJ_LEN])
1417 vl_puts(" A=" as *u8); vl_puts(pa)
1418 vl_puts(" B=" as *u8); vl_puts(pb)
1419 vl_puts(" window_s=" as *u8); vl_putn(window)
1420 vl_puts(" a=" as *u8); vl_puts(vl_state_name(oa[0]))
1421 vl_puts("@" as *u8); vl_putn(oa[1])
1422 vl_puts(" b=" as *u8); vl_puts(vl_state_name(ob[0]))
1423 vl_puts("@" as *u8); vl_putn(ob[1])
1424 vl_puts(" RESULT=" as *u8); vl_puts(vl_vout_name(verdict))
1425 vl_nl()
1426
1427 // CADENCE AND BOUND ARE A DECIDABLE PAIR, so the window is auditable rather than magic: if a
1428 // stream's own observed interval between runs is wider than the window, the two vantages can
1429 // never be co-observed and the row is UNOBSERVABLE BY CONSTRUCTION no matter how healthy the
1430 // subject is. That is a property of the declaration, not of the system, and it must be said.
1431 vl_series(pa, conf, voc, sa)
1432 vl_series(pb, conf, voc, sb)
1433 var tight: i64 = 0
1434 if sa[VL_R_TSRUNS] >= 2 { if sa[VL_R_MAXGAP] > window { tight = 1 } }
1435 if sb[VL_R_TSRUNS] >= 2 { if sb[VL_R_MAXGAP] > window { tight = 1 } }
1436 if tight == 1 {
1437 vl_puts(" WINDOW-NARROWER-THAN-OBSERVED-CADENCE a_max_gap_s=" as *u8); vl_putn(sa[VL_R_MAXGAP])
1438 vl_puts(" b_max_gap_s=" as *u8); vl_putn(sb[VL_R_MAXGAP])
1439 vl_puts(" window_s=" as *u8); vl_putn(window)
1440 vl_puts(" -- this row can never report AGREE or DISAGREE while that holds. Widen the window deliberately, or speed up the slower producer.\n" as *u8)
1441 }
1442
1443 vl_rowb(out, oo, outcap, conf, voc[vb + VL_VAN_SUBJ_OFF], voc[vb + VL_VAN_SUBJ_OFF] + voc[vb + VL_VAN_SUBJ_LEN])
1444 vl_rowtab(out, oo, outcap)
1445 vl_row(out, oo, outcap, vl_vout_name(verdict))
1446 vl_rowtab(out, oo, outcap)
1447 vl_row(out, oo, outcap, "a=" as *u8); vl_row(out, oo, outcap, vl_state_name(oa[0]))
1448 vl_rowtab(out, oo, outcap)
1449 vl_row(out, oo, outcap, "b=" as *u8); vl_row(out, oo, outcap, vl_state_name(ob[0]))
1450 vl_rowtab(out, oo, outcap)
1451 vl_row(out, oo, outcap, "window_s=" as *u8); vl_rown(out, oo, outcap, window)
1452 vl_rownl(out, oo, outcap)
1453 i = i + 1
1454 }
1455
1456 vl_puts("-- PARTITION over the declared subjects (each lands in exactly one) --\n" as *u8)
1457 var sum: i64 = 0
1458 var v: i64 = 0
1459 while v < VL_NVOUT {
1460 vl_puts("OUTCOME " as *u8); vl_puts(vl_vout_name(v))
1461 vl_puts(" = " as *u8); vl_putn(res[v])
1462 vl_nl()
1463 sum = sum + res[v]
1464 v = v + 1
1465 }
1466 vl_puts("PARTITION SUM = " as *u8); vl_putn(sum)
1467 vl_puts(" declared_subjects = " as *u8); vl_putn(nv)
1468 if sum == nv { vl_puts(" RECONCILES\n" as *u8) } else { vl_puts(" DOES NOT RECONCILE -- do not publish these numbers\n" as *u8) }
1469
1470 if (outp as i64) != 0 {
1471 var wrote: i64 = 0
1472 let fd: i64 = sys_openat_wr(outp, VL_MODE_0644)
1473 if fd >= 0 { wrote = sys_write(fd, out, oo[0]); sys_close(fd) }
1474 if fd < 0 { vl_puts("WORKLIST-WRITE-FAILED path=" as *u8); vl_puts(outp); vl_nl() }
1475 vl_puts("worklist=" as *u8); vl_puts(outp)
1476 vl_puts(" bytes=" as *u8); vl_putn(wrote)
1477 vl_nl()
1478 }
1479 return 0
1480}