code wiki / (root) / nx_verdictlog_lib.nx

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}