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}