code wiki / (root) / nx_comparetree_lib.nx

nx_comparetree_lib.nx source

↩ module page · 261 lines · 15857 B

1// nx_comparetree_lib.nx -- THE ONE RESOLVER FOR A /compare DATA FILE THAT LIVES IN TWO TREES. 2// 3// WHY THIS EXISTS (measured 2026-08-25). The estate keeps compare data in TWO forked trees: 4// knowledge/compare/ 45 matrices 5// buildroot/knowledge/compare/ 85 matrices (a strict SUPERSET -- all 45 names appear here) 6// Every reader that hard-codes ONE of them is blind to whatever the other holds, and the blindness is 7// silent: the reader reports a property OF THE DOMAIN when the true statement is about ITS OWN PATH. 8// 9// MEASURED COST, THE CASE THAT PRODUCED THIS LIB: nx_swcompare_evidence -- the referee that judges 10// every domain's honesty -- built its matrix path from a single hard-coded "knowledge/compare/" prefix. 11// For the 40 buildroot-only domains it printed 12// EVIDENCE: matrix file missing -- fail loud / present_axes=0 grounded=0 ungrounded=0 / verdict=RED 13// having examined NOT ONE ROW. A CONTROL PAIR settled that the cause was the tree and not the domain: 14// typography (present in BOTH trees) graded MEASURED-HONEST 9/9, while procgen and dcc (buildroot-only) 15// returned the identical matrix-file-missing RED from the same binary minutes apart. 16// 17// THIS IS THE THIRD INSTANCE OF ONE ROOT CAUSE IN A SINGLE DAY: nx_domain_admit's C3 read one tree, its 18// C4 rebuilt the gates path a second time, and then ev_scan. A two-tree estate needs a two-tree resolver 19// in EVERY reader, and each reader that lacks one fails silently -- flatteringly or alarmingly, never 20// visibly. So the resolution lives HERE, once, instead of being re-derived a fourth time. 21// 22// ORDER -- SETTLED PER CLASS ON 2026-09-01, AND THERE IS NO LONGER ONE ORDER. .matrix resolves 23// PUBLISHED-first (buildroot) and .gates resolves AUTHORED-first (knowledge). The census behind that 24// split, and the reason a BLANKET flip would have been wrong in half the estate, are in the 25// adjudication block above ct_first_published(). This header now records only the standing fact both 26// orders rest on, and the caveat that turned out to decide the matter: 27// 28// THE TWO TREES ARE NOT COPIES. Sampled with nx_filehash cmp, every shared domain DIFFERS, and the 29// buildroot copy is the larger one every time: 30// typography 3632 vs 3805 . warden 5570 vs 6095 . mangagen 7096 vs 7125 (identical:0, all three) 31// buildroot/knowledge/compare/ is the tree nx_compare_regen publishes from and the tree the ranker and 32// nx_gateadjudicate read (GA_CMP2), and nx_domain_admit's gate asserts it by name as "the publishing 33// tree". PRIMARY-first preserved every existing verdict at the price of grading a file the published 34// page does not render, on 45 boards. The prior text called that "a SECOND, SEPARATE defect ... named 35// here rather than silently decided" -- THIS CHANGE IS THAT DECISION, and the verdicts it moves are 36// re-censused and published with it rather than left to be discovered by whoever reads a board next. 37// 38// EVERY ORDER IS PINNED BY GATE TEETH IN BOTH POSITIONS (first AND fallback), so the NEXT flip is also 39// an adjudication and not a tweak -- which is the property the original author built this file to have. 40// 41// SCOPE, DELIBERATELY NARROW: this lib owns PATH RESOLUTION ONLY. It does not classify rows -- that is 42// nx_matrix_sym_lib.nx, which already owns the symbol-field format. Adding a second classifier here 43// would be the duplicate-ruler defect this file exists to stop. 44// license_tier: ORIGINAL 45import "nx_syscalls.nx" 46 47const CT_PATH_CAP: i64 = 512 48 49// THE TWO TREES, AS DATA. Both carry the trailing slash: ct_build_path concatenates dir+domain+suffix 50// and does not insert one, so a dir without it silently produces "comparedcc.matrix". 51const CT_DIR_PRIMARY: *u8 = "knowledge/compare/" 52const CT_DIR_SECONDARY: *u8 = "buildroot/knowledge/compare/" 53 54// WHICH TREE ANSWERED. A resolver that returns bytes without saying where they came from reproduces the 55// original defect one layer up: the caller cannot tell a graded domain from a differently-graded one. 56const CT_TREE_NONE: i64 = 0 57const CT_TREE_PRIMARY: i64 = 1 58const CT_TREE_SECONDARY: i64 = 2 59 60// NOTE: this lib deliberately exports NO generic string helper. `ct_slen` was drafted here and removed: 61// three shipped organs already define a ct_slen of their own, and a lib intended for import by every 62// compare reader must not carry a name that collides on adoption. Consumers keep their own. 63func ct_cat(dst: *u8, off: i64, s: *u8) -> i64 { 64 var o: i64 = off 65 var i: i64 = 0 66 while s[i] != (0 as u8) { dst[o] = s[i]; o = o + 1; i = i + 1 } 67 dst[o] = 0 as u8 68 return o 69} 70 71// <dir><domain><suffix>, NUL-terminated. Returns the length written, so a caller can bound-check. 72func ct_build_path(dir: *u8, dom: *u8, suf: *u8, dst: *u8) -> i64 { 73 var o: i64 = ct_cat(dst, 0, dir) 74 o = ct_cat(dst, o, dom) 75 o = ct_cat(dst, o, suf) 76 return o 77} 78 79// THE DECISION, ISOLATED AND PURE so it can be gated without touching a filesystem: given what each 80// read returned, which tree owns the answer. A non-positive count means "nothing readable there" -- 81// ct_read returns 0-1 for an unopenable path and 0 for an empty file, and BOTH must fall through, since 82// an empty matrix grades exactly as badly as a missing one and would otherwise mask the fallback. 83func ct_pick(n1: i64, n2: i64) -> i64 { 84 if n1 > 0 { return CT_TREE_PRIMARY } 85 if n2 > 0 { return CT_TREE_SECONDARY } 86 return CT_TREE_NONE 87} 88 89func ct_read(path: *u8, buf: *u8, cap: i64) -> i64 { 90 let fd: i64 = sys_openat_rd(path) 91 if fd < 0 { return 0 - 1 } 92 var tot: i64 = 0 93 var go: i64 = 1 94 while go == 1 { 95 let n: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot) 96 if n <= 0 { go = 0 } else { tot = tot + n; if tot >= cap { go = 0 } } 97 } 98 sys_close(fd) 99 return tot 100} 101 102// d1 is tried FIRST and wins ties; d2 second. Both dirs are PARAMETERS, deliberately: a resolver that 103// can only ever read its own two hard-coded roots cannot be tested without editing production data, and 104// an untestable resolver is exactly the component that rots into the defect above. 105// `which` receives CT_TREE_*; it is written on EVERY path including the miss. 106func ct_read_2dir(d1: *u8, d2: *u8, dom: *u8, suf: *u8, buf: *u8, cap: i64, which: *i64) -> i64 { 107 let path: *u8 = sys_mmap(CT_PATH_CAP) 108 ct_build_path(d1, dom, suf, path) 109 let n1: i64 = ct_read(path, buf, cap) 110 if n1 > 0 { which[0] = CT_TREE_PRIMARY; return n1 } 111 ct_build_path(d2, dom, suf, path) 112 let n2: i64 = ct_read(path, buf, cap) 113 which[0] = ct_pick(n1, n2) 114 return n2 115} 116 117// --------------------------------------------------------------------------------------------- 118// THE ORDER IS A PROPERTY OF THE CLASS, NOT OF THE ESTATE -- ADJUDICATED 2026-09-01, ON A CENSUS. 119// 120// The header above named this as an open question and deliberately refused to decide it silently. 121// It is now decided, PER CLASS, and the two classes came out OPPOSITE ways. Every count below is 122// corpus_complete=1 over both trees: 123// 124// .matrix PRIMARY 45 . SECONDARY 96 . and PRIMARY is a STRICT SUBSET -- primary_only = 0. 125// The published page is emitted by nx_compare_regen from buildroot/knowledge/compare/, 126// and nx_domain_admit's own gate tooth names that tree "the publishing tree" (DA_CDIRS). 127// So under PRIMARY-first this referee graded a DIFFERENT DOCUMENT from the one the page 128// renders, on all 45 shared boards -- byte-proven on deploy (6,327 B graded vs 6,618 B 129// published) and media (7,816 vs 8,268). The trees also disagree about the WORK ITSELF: 130// 20 _ABSENT_ build contracts in PRIMARY against 939 in buildroot. => PUBLISHED ORDER. 131// 132// .gates PRIMARY 60 . SECONDARY 5 . NO page renders a .gates document at all -- it is an INPUT 133// to this referee, which forks the gate elfs it names. "Grade what is published" has no 134// purchase on a file nothing publishes, and the class's home is the 58 maps that exist 135// only in PRIMARY. Flipping it would change exactly two domains (gen, synthroom) for no 136// reason anyone could state. => AUTHORED ORDER, DELIBERATELY UNCHANGED. 137// 138// A BLANKET FLIP WOULD HAVE BEEN WRONG IN HALF THE ESTATE, AND THE HALF IT BROKE IS THE HALF WITH 139// NO PUBLISHER TO ARGUE FROM. The lib already took both dirs as PARAMETERS for exactly this reason. 140// 141// WHY TWO NAMED FUNCTIONS AND NOT ONE FUNCTION WITH AN ORDER ARGUMENT: an order token has an 142// unknown-value case, and its only available default is one of the two real answers -- so a typo 143// would SILENTLY pick a tree, which is the very defect this lib exists to stop. With two names a 144// wrong call is a COMPILE ERROR. For the same reason the old unqualified ct_compare_read is GONE 145// rather than kept as an alias: an unnamed default is an invitation to resolve the next class 146// blind. Both of its call sites move in this same change (corpus_complete=1, exactly two sites). 147// --------------------------------------------------------------------------------------------- 148 149// WHICH TREE EACH NAMED ORDER TRIES FIRST AND SECOND, AS GATEABLE FACTS. These exist so a gate can 150// pin the order with a string compare and no filesystem, exactly as it already pins the two tree 151// constants -- and the wrappers below COMPOSE them, so the pinned fact and the shipped behaviour 152// cannot drift apart. Pinning BOTH positions is deliberate: an edit that changed only the first 153// would leave both probes aimed at one tree, silently disabling the fallback that keeps every board 154// resolvable, and a first-position-only tooth would still read green. 155func ct_first_published() -> *u8 { 156 return CT_DIR_SECONDARY 157} 158func ct_second_published() -> *u8 { 159 return CT_DIR_PRIMARY 160} 161func ct_first_authored() -> *u8 { 162 return CT_DIR_PRIMARY 163} 164func ct_second_authored() -> *u8 { 165 return CT_DIR_SECONDARY 166} 167 168// PUBLISHED ORDER -- for any class the /compare page RENDERS. Tries the publishing tree first. 169// For .matrix this cannot make a board go dark: every PRIMARY matrix has a buildroot twin today 170// (primary_only = 0). The fallback is kept anyway, because that subset relation is a measurement of 171// today's data and not a guarantee about tomorrow's. 172// POSITION IS NOT TREE, AND CONFLATING THEM WOULD HAVE MADE THE STAMP LIE. 173// ct_read_2dir writes CT_TREE_PRIMARY whenever its FIRST argument answered -- it is handed arbitrary 174// dirs (the gate passes /tmp fixtures), so the only thing it can honestly report is WHICH POSITION 175// won. That was harmless while position 1 was always the primary tree. Under the published order it 176// is not: position 1 IS the secondary tree, so a caller stamping the raw token would label every 177// buildroot read "mroot=PRIMARY" -- the referee would grade the right document and then MISNAME it, 178// re-creating, one field over, the exact defect this adjudication exists to close. Caught by reading 179// the resolver rather than trusting the wrapper; it is the reason this translation exists at all. 180// Pure, so the gate can cover it exhaustively with no filesystem. CT_TREE_NONE and any unknown token 181// pass through UNCHANGED: a miss must stay a miss, and inventing a tree for an unrecognised value is 182// how a resolver starts answering questions it was never asked. 183func ct_swap_tree(t: i64) -> i64 { 184 if t == CT_TREE_PRIMARY { return CT_TREE_SECONDARY } 185 if t == CT_TREE_SECONDARY { return CT_TREE_PRIMARY } 186 return t 187} 188 189func ct_compare_read_published(dom: *u8, suf: *u8, buf: *u8, cap: i64, which: *i64) -> i64 { 190 let n: i64 = ct_read_2dir(ct_first_published(), ct_second_published(), dom, suf, buf, cap, which) 191 which[0] = ct_swap_tree(which[0]) 192 return n 193} 194 195// AUTHORED ORDER -- for a class no page renders, where the real question is "where is this written". 196func ct_compare_read_authored(dom: *u8, suf: *u8, buf: *u8, cap: i64, which: *i64) -> i64 { 197 return ct_read_2dir(ct_first_authored(), ct_second_authored(), dom, suf, buf, cap, which) 198} 199 200// --------------------------------------------------------------------------------------------- 201// FULL-FILE RESOLUTION -- THERE IS NO NUMBER TO GUESS (2026-09-01). 202// 203// ct_read above fills a CALLER-SUPPLIED buffer and STOPS AT ITS CAP WITH NO SIGNAL. That is the 204// silent-truncation class, and it was live in the referee: nx_swcompare_evidence reserved 65,536 B 205// for a matrix, charsim.matrix measures 67,965 B, and the referee graded a 65,536-byte PREFIX -- 206// dropping five rows including that board's own LIAR-KILL row and its operator-acceptance row -- 207// while the page published "grounded 25/25 | unsupported 0". A BUFFER CAP IS NOT A NUMBER TO TUNE: 208// raising it only moves the guess, and a bigger silent cap is the same defect with a longer fuse. 209// 210// For a LOCAL FILE THIS PROCESS OWNS there is nothing to guess. sys_read_file sizes its buffer from 211// the file itself (lseek END) and CANNOT short-read, so truncation stops being a thing that has to be 212// detected and becomes a thing that cannot happen. ct_read is deliberately KEPT, not deleted: a 213// caller that genuinely owns a fixed reserve (this lib's own gate fills /tmp fixtures that way) still 214// has one. It is simply no longer the /compare data path. 215// 216// RESOURCE ENVELOPE, STATED RATHER THAN ASSUMED. This allocates ONE mapping of (file bytes + 16) per 217// call and nothing else. Measured against the old fixed 65,536 B reserve it is SMALLER on every board 218// under 64 KiB -- 94 of the 96 stamped today, down to 5,016 B for the smallest -- and larger by 2,445 219// B on the largest board in the estate. Release it with sys_free_file(buf, *outn). A one-shot caller 220// may let exit reclaim it; A CALLER IN A LOOP MUST FREE IT or it leaks one mapping per iteration, 221// which is the nx_sites_daemon shape sys_free_file was written for. 222// 223// A MISS STAYS A MISS. When the file resolves in NEITHER tree this returns 0 with *outn = 0 and 224// which[0] = CT_TREE_NONE, exactly as the capped pair does, and "empty" is still distinguished from 225// "absent" on the byte count and not on the pointer. 226// --------------------------------------------------------------------------------------------- 227func ct_readall_2dir(d1: *u8, d2: *u8, dom: *u8, suf: *u8, outn: *i64, which: *i64) -> *u8 { 228 let path: *u8 = sys_mmap(CT_PATH_CAP) 229 let nc: *i64 = sys_mmap(16) as *i64 230 outn[0] = 0 231 which[0] = CT_TREE_NONE 232 ct_build_path(d1, dom, suf, path) 233 nc[0] = 0 234 let b1: *u8 = sys_read_file(path, nc) 235 let n1: i64 = nc[0] 236 if n1 > 0 { which[0] = CT_TREE_PRIMARY; outn[0] = n1; return b1 } 237 // POSITION 1 ANSWERED NOTHING: hand its mapping back before trying position 2, or resolving one 238 // document across two trees costs two mappings. sys_free_file is null-safe by construction, so 239 // this is correct for both the open-failure (pointer 0) and empty-file (16-byte mapping) cases. 240 sys_free_file(b1, n1) 241 ct_build_path(d2, dom, suf, path) 242 nc[0] = 0 243 let b2: *u8 = sys_read_file(path, nc) 244 let n2: i64 = nc[0] 245 which[0] = ct_pick(n1, n2) 246 outn[0] = n2 247 if n2 > 0 { return b2 } 248 sys_free_file(b2, n2) 249 return 0 as *u8 250} 251 252// PUBLISHED ORDER, UNCAPPED. Composes the SAME ct_first_published()/ct_second_published() facts and the 253// SAME ct_swap_tree position-to-tree translation as the capped wrapper above, so the resolution order 254// and the tree label cannot drift between the two readers: one order, two buffer strategies, never two 255// orders. An uncapped reader that re-spelled the order would be the duplicate-ruler defect this whole 256// lib exists to prevent. 257func ct_compare_readall_published(dom: *u8, suf: *u8, outn: *i64, which: *i64) -> *u8 { 258 let b: *u8 = ct_readall_2dir(ct_first_published(), ct_second_published(), dom, suf, outn, which) 259 which[0] = ct_swap_tree(which[0]) 260 return b 261}