code wiki / _hdl_build / nx_ecomat_domtrend_lib.nx

nx_ecomat_domtrend_lib.nx source

↩ module page · 285 lines · 11909 B

1// nx_ecomat_domtrend_lib.nx -- EXACT growth attribution from the per-domain ledger. 2// 3// This is the consumer that closes debt 1785514540. nx_ecomat_trend reads the AGGREGATE ledger and 4// can only attribute a span whose domain count held still -- measured 406 permil of real movement. 5// The per-domain ledger (nx_ecomat_domledger) records who held what, so the same question becomes 6// arithmetic instead of inference: diff two beats BY NAME. 7// 8// ADVANCEMENT = sum over domains present in BOTH beats of (cur_last - cur_first) 9// EXPANSION = the levels and bar carried in by domains present ONLY in the later beat 10// RETIRED = levels carried out by domains present ONLY in the earlier beat 11// 12// Nothing is ambiguous, because nothing is inferred from a total. A domain that was declared at the 13// bottom can no longer be mistaken for a domain that climbed, which was the entire defect: the 14// headline sum_cur/sum_bar falls when you declare a frontier, so the ecosystem's own self-grade 15// punished expansion and made honest growth unreadable. 16// 17// SNAPSHOT CHOICE: first beat vs last beat. Intermediate beats are counted and reported but not 18// diffed pairwise -- the endpoints answer "where are we vs where we started", and reporting the 19// beat count lets a reader see how much history backs that. 20// license_tier: ORIGINAL No hw writes (Rule 26). 21import "nx_ecomat_trend_lib.nx" 22 23const DT_LOG: *u8 = "knowledge/status/ecomat_domains.log" 24const DT_MAX_D: i64 = 256 25// MUST match the WRITER's cap (ECOMAT_DOM_CAP, used by edl_walk via ec_str). It was 64 while the 26// writer emitted up to 128: a 70-char name would be written whole and read back as a 63-char 27// PREFIX, and two names sharing that prefix would collide in dt_slot -- silently merging two 28// domains into one and corrupting every attribution derived from them. 29const DT_NAME_CAP: i64 = 128 30const DT_OUT_SLOTS: i64 = 24 31const DT_SPACE: i64 = 32 32 33const DTO_BEATS: i64 = 0 34const DTO_ROWS: i64 = 1 35const DTO_FIRST_EPOCH: i64 = 2 36const DTO_LAST_EPOCH: i64 = 3 37const DTO_FIRST_N: i64 = 4 38const DTO_LAST_N: i64 = 5 39const DTO_ADVANCE: i64 = 6 40const DTO_EXPAND_CUR: i64 = 7 41const DTO_EXPAND_BAR: i64 = 8 42const DTO_RETIRED_CUR: i64 = 9 43const DTO_COMMON: i64 = 10 44const DTO_NEW: i64 = 11 45const DTO_DECOMP_PERMIL: i64 = 12 46const DTO_CAPPED: i64 = 13 47 48const DT_ERR_NOLOG: i64 = 0 - 1 49const DT_ERR_ONEBEAT: i64 = 0 - 2 50// An endpoint beat whose ECOMATDOM-END terminator is missing, incomplete, or disagrees with the 51// rows actually present. Diffing a PARTIAL snapshot is the worst failure this lib has: every domain 52// missing from a truncated final beat reads as RETIRED, fabricating a retirement, with a number. 53const DT_ERR_TRUNCATED: i64 = 0 - 3 54// The READER hit DT_MAX_D. MUST NOT be reported as TRUNCATED: truncated means the WRITER did not 55// finish, capped means I could not hold what the writer correctly wrote. Opposite causes, opposite 56// remedies (fix the writer vs raise the cap) -- collapsing them is the conflation this lib prevents. 57const DT_ERR_CAPPED: i64 = 0 - 4 58 59const DT_END_TAG: *u8 = "ECOMATDOM-END" 60const DT_END_TAG_LEN: i64 = 13 61 62// copy the value of `domain=` from the line span into out (NUL-terminated); 1 on success. 63func dt_name(buf: *u8, ls: i64, le: i64, out: *u8) -> i64 { 64 let pat: *u8 = "domain=" as *u8 65 let pl: i64 = el_len(pat) 66 var i: i64 = ls 67 while i + pl <= le { 68 if el_match(buf, i, pat, pl) == 1 { 69 var j: i64 = i + pl 70 var o: i64 = 0 71 var over: i64 = 0 72 while j < le { 73 if (buf[j] as i64) == DT_SPACE { j = le } else { 74 // REFUSE, never truncate. A silently shortened name can collide with another 75 // domain sharing its prefix, merging two domains in dt_slot and corrupting 76 // attribution with no signal -- strictly worse than skipping the row loudly. 77 if o < DT_NAME_CAP - 1 { out[o] = buf[j]; o = o + 1 } else { over = 1; j = le } 78 j = j + 1 79 } 80 } 81 out[o] = 0 as u8 82 if over == 1 { out[0] = 0 as u8; return 0 - 1 } 83 if o > 0 { return 1 } 84 return 0 85 } 86 i = i + 1 87 } 88 return 0 89} 90 91// Declared domain count from the ECOMATDOM-END terminator for `epoch`; -1 when that beat has no 92// terminator or is not flagged complete. THIS IS THE SIGNAL edl_end WRITES AND NOTHING READ: 93// a beat without a valid END is a beat the writer did not finish, and must never be differenced. 94// Bounds: the tag compare is length-checked against the LINE, because el_match takes no haystack 95// length and will happily read past a short line (the defect filed against el_last_green). 96func dt_end_count(buf: *u8, n: i64, epoch: i64) -> i64 { 97 var p: i64 = 0 98 while p < n { 99 var e: i64 = p 100 var go: i64 = 1 101 while go == 1 { 102 if e >= n { go = 0 } else { if (buf[e] as i64) == TR_NL { go = 0 } else { e = e + 1 } } 103 } 104 if e - p >= DT_END_TAG_LEN { 105 if el_match(buf, p, DT_END_TAG, DT_END_TAG_LEN) == 1 { 106 if tr_field(buf, p, e, "epoch=" as *u8) == epoch { 107 if tr_field(buf, p, e, "coverage_complete=" as *u8) == 1 { 108 return tr_field(buf, p, e, "domains=" as *u8) 109 } 110 return 0 - 1 111 } 112 } 113 } 114 p = e + 1 115 } 116 return 0 - 1 117} 118 119func dt_slot(names: *u8, n: i64, want: *u8) -> i64 { 120 var i: i64 = 0 121 while i < n { 122 if el_streq(names + i * DT_NAME_CAP, want) == 1 { return i } 123 i = i + 1 124 } 125 return 0 - 1 126} 127 128// Exact decomposition between the first and last beat in the per-domain ledger. 129// PURE core -- the gate proves this on in-memory fixtures, so it can never pass by reading back a 130// file it produced itself. 131func em_domtrend_buf(buf: *u8, n: i64, out: *i64) -> i64 { 132 var z: i64 = 0 133 while z < DT_OUT_SLOTS { out[z] = 0; z = z + 1 } 134 if n <= 0 { return DT_ERR_NOLOG } 135 136 // pass 1 -- find the first and last DATA epoch (END rows carry no domain= and are skipped) 137 var first_e: i64 = 0 - 1 138 var last_e: i64 = 0 - 1 139 var rows: i64 = 0 140 let nm: *u8 = sys_mmap(DT_NAME_CAP) 141 var p: i64 = 0 142 while p < n { 143 var e: i64 = p 144 var go: i64 = 1 145 while go == 1 { 146 if e >= n { go = 0 } else { if (buf[e] as i64) == TR_NL { go = 0 } else { e = e + 1 } } 147 } 148 if e > p { if dt_name(buf, p, e, nm) == 1 { 149 let ep: i64 = tr_field(buf, p, e, "epoch=" as *u8) 150 if ep >= 0 { 151 rows = rows + 1 152 if first_e < 0 { first_e = ep } 153 if ep < first_e { first_e = ep } 154 if ep > last_e { last_e = ep } 155 } 156 } } 157 p = e + 1 158 } 159 out[DTO_ROWS] = rows 160 out[DTO_FIRST_EPOCH] = first_e 161 out[DTO_LAST_EPOCH] = last_e 162 if rows == 0 { return DT_ERR_NOLOG } 163 if last_e == first_e { return DT_ERR_ONEBEAT } 164 165 // pass 2 -- collect the two endpoint snapshots by name 166 let an: *u8 = sys_mmap(DT_MAX_D * DT_NAME_CAP) 167 let ac: *i64 = sys_mmap(DT_MAX_D * 8) as *i64 168 let bn: *u8 = sys_mmap(DT_MAX_D * DT_NAME_CAP) 169 let bc: *i64 = sys_mmap(DT_MAX_D * 8) as *i64 170 let bb: *i64 = sys_mmap(DT_MAX_D * 8) as *i64 171 var na: i64 = 0 172 var nb: i64 = 0 173 var capped: i64 = 0 174 var beats: i64 = 0 175 var seen_last: i64 = 0 - 1 176 177 p = 0 178 while p < n { 179 var e2: i64 = p 180 var g2: i64 = 1 181 while g2 == 1 { 182 if e2 >= n { g2 = 0 } else { if (buf[e2] as i64) == TR_NL { g2 = 0 } else { e2 = e2 + 1 } } 183 } 184 if e2 > p { if dt_name(buf, p, e2, nm) == 1 { 185 let ep2: i64 = tr_field(buf, p, e2, "epoch=" as *u8) 186 let cu: i64 = tr_field(buf, p, e2, "cur=" as *u8) 187 let ba: i64 = tr_field(buf, p, e2, "bar=" as *u8) 188 if ep2 != seen_last { beats = beats + 1; seen_last = ep2 } 189 if ep2 == first_e { if cu >= 0 { 190 if na < DT_MAX_D { 191 var q: i64 = 0 192 while q < DT_NAME_CAP { an[na * DT_NAME_CAP + q] = nm[q]; q = q + 1 } 193 ac[na] = cu 194 na = na + 1 195 } else { capped = 1 } 196 } } 197 if ep2 == last_e { if cu >= 0 { 198 if nb < DT_MAX_D { 199 var r: i64 = 0 200 while r < DT_NAME_CAP { bn[nb * DT_NAME_CAP + r] = nm[r]; r = r + 1 } 201 bc[nb] = cu 202 bb[nb] = ba 203 nb = nb + 1 204 } else { capped = 1 } 205 } } 206 } } 207 p = e2 + 1 208 } 209 210 out[DTO_BEATS] = beats 211 out[DTO_FIRST_N] = na 212 out[DTO_LAST_N] = nb 213 out[DTO_CAPPED] = capped 214 215 // Capacity FIRST, before the terminator comparison -- otherwise a store larger than DT_MAX_D 216 // makes na/nb cap at the limit while the terminator honestly declares more, the counts mismatch, 217 // and we would blame the WRITER for the READER's limit. Diagnose the right one. 218 if capped == 1 { return DT_ERR_CAPPED } 219 220 // BOTH endpoints must carry a valid terminator whose declared count matches the rows we read. 221 // Refuse rather than diff a partial snapshot: a truncated final beat would classify every 222 // missing domain as RETIRED and report it as fact. REFUSE, NEVER SILENTLY SHRINK. 223 let end_a: i64 = dt_end_count(buf, n, first_e) 224 let end_b: i64 = dt_end_count(buf, n, last_e) 225 if end_a != na { return DT_ERR_TRUNCATED } 226 if end_b != nb { return DT_ERR_TRUNCATED } 227 228 var advance: i64 = 0 229 var expcur: i64 = 0 230 var expbar: i64 = 0 231 var common: i64 = 0 232 var fresh: i64 = 0 233 var i2: i64 = 0 234 while i2 < nb { 235 let s: i64 = dt_slot(an, na, bn + i2 * DT_NAME_CAP) 236 if s >= 0 { 237 advance = advance + (bc[i2] - ac[s]) 238 common = common + 1 239 } else { 240 expcur = expcur + bc[i2] 241 expbar = expbar + bb[i2] 242 fresh = fresh + 1 243 } 244 i2 = i2 + 1 245 } 246 247 var retired: i64 = 0 248 var i3: i64 = 0 249 while i3 < na { 250 if dt_slot(bn, nb, an + i3 * DT_NAME_CAP) < 0 { retired = retired + ac[i3] } 251 i3 = i3 + 1 252 } 253 254 out[DTO_ADVANCE] = advance 255 out[DTO_EXPAND_CUR] = expcur 256 out[DTO_EXPAND_BAR] = expbar 257 out[DTO_RETIRED_CUR] = retired 258 out[DTO_COMMON] = common 259 out[DTO_NEW] = fresh 260 // Every level is attributed by NAME here, so attribution is total -- unless the domain cap bound, 261 // in which case say so rather than claim completeness over a truncated read. 262 // 1000 here is EARNED, not asserted. We only reach this line after BOTH endpoint beats proved 263 // complete against their own ECOMATDOM-END counts, so every level is attributed to a NAMED 264 // bucket (climbed / declared / retired). Before that check existed this was a bare constant 265 // returned whenever the walk finished -- a number that could not go down, printed on /sota 266 // beside genuinely computed figures. If the cap bound, coverage is UNKNOWN: emit -1 UNMEASURED, 267 // never 0, because 0 reads as "nothing was attributable", a different and false claim. 268 if capped == 1 { out[DTO_DECOMP_PERMIL] = 0 - 1 } else { out[DTO_DECOMP_PERMIL] = TR_PERMIL } 269 return 0 270} 271 272// Thin file wrapper. A missing ledger is DT_ERR_NOLOG, never a silent zero -- "no per-domain 273// history yet" and "nothing advanced" are opposite statements and must never share a return value. 274func em_domtrend(logpath: *u8, out: *i64) -> i64 { 275 let szp: *i64 = sys_mmap(16) as *i64 276 let buf: *u8 = ss_readall(logpath, szp) 277 var n: i64 = szp[0] 278 if n < 0 { n = 0 } 279 if n == 0 { 280 var z2: i64 = 0 281 while z2 < DT_OUT_SLOTS { out[z2] = 0; z2 = z2 + 1 } 282 return DT_ERR_NOLOG 283 } 284 return em_domtrend_buf(buf, n, out) 285}