code wiki / _hdl_build / nx_ecomat_trend_lib.nx

nx_ecomat_trend_lib.nx source

↩ module page · 230 lines · 8971 B

1// nx_ecomat_trend_lib.nx -- ADVANCEMENT vs EXPANSION: the honest growth decomposition over the 2// durable maturity ledger (knowledge/status/ecosystem_maturity.log). 3// 4// THE DEFECT THIS EXISTS TO KILL (measured 2026-07-31): the rollup headline is sum_cur/sum_bar, so 5// DECLARING a new domain raises the denominator and the grade FALLS while capability is unchanged. 6// Read naively the ledger says 460 -> 434 permil = "we regressed". Read honestly it says the bar 7// grew by 20 levels because 4 domains were declared. Conflating ADVANCEMENT (an existing domain 8// climbing a rung) with EXPANSION (a new domain declared at the bottom) is the design flaw: a grade 9// that punishes expansion trains the ecosystem to stop declaring frontiers. 10// 11// WHAT IS EXACT AND WHAT IS NOT -- this lib never invents precision (L011 self-ceiling honesty): 12// * a transition where the domain COUNT DID NOT CHANGE has a stable cohort, so the entire delta 13// in sum_cur IS advancement. EXACT, no assumption. 14// * a transition that ADDED domains cannot be attributed from aggregates alone -- the ledger did 15// not keep who contributed what. That span is counted AMBIGUOUS and is never silently folded 16// into either bucket. 17// The permanent fix is a per-domain ledger line (then every span is exact). Until that ships this 18// lib reports its decomposable coverage explicitly instead of guessing. 19// 20// Pairs with nx_ecomat_lib (the rollup core that writes the ledger this reads). 21// license_tier: ORIGINAL No hw writes (Rule 26). 22import "nx_ecomat_lib.nx" 23 24const TR_LEDGER: *u8 = "knowledge/status/ecosystem_maturity.log" 25 26// Bound on ledger rows held in memory. Named so the ALLOCATION and the BOUND are one symbol and 27// cannot drift; when it binds, em_trend reports capped=1 rather than silently truncating. 28const TR_MAX_ROWS: i64 = 4096 29const TR_I64: i64 = 8 30const TR_OUT_SLOTS: i64 = 24 31 32const TR_NL: i64 = 10 33const TR_ASC0: i64 = 48 34const TR_ASC9: i64 = 57 35 36// out[] slot names -- the ledger of this lib's own contract. 37const TRO_ROWS: i64 = 0 38const TRO_SCANNED: i64 = 1 39const TRO_CAPPED: i64 = 2 40const TRO_FIRST_CUR: i64 = 3 41const TRO_FIRST_BAR: i64 = 4 42const TRO_FIRST_DOM: i64 = 5 43const TRO_LAST_CUR: i64 = 6 44const TRO_LAST_BAR: i64 = 7 45const TRO_LAST_DOM: i64 = 8 46const TRO_ADVANCE: i64 = 9 47const TRO_STABLE_SPANS: i64 = 10 48const TRO_EXPAND_BAR: i64 = 11 49const TRO_AMBIG_CUR: i64 = 12 50const TRO_FIRST_EPOCH: i64 = 13 51const TRO_LAST_EPOCH: i64 = 14 52const TRO_FIRST_PERMIL: i64 = 15 53const TRO_LAST_PERMIL: i64 = 16 54const TRO_ADDED_DOM: i64 = 17 55const TRO_DECOMP_PERMIL: i64 = 18 56 57// findings about the SUBJECT (never conflated with the instrument's own verdict) 58const TR_UNDECIDABLE: i64 = 0 59const TR_RISING: i64 = 1 60const TR_FLAT: i64 = 2 61const TR_FALLING: i64 = 3 62 63const TR_PERMIL: i64 = 1000 64 65// return codes 66const TR_ERR_NOLOG: i64 = 0 - 1 67const TR_ERR_THIN: i64 = 0 - 2 68 69// The number that follows `pat` within the line span [ls,le) of buf; -1 when the field is absent. 70// Line-scoped on purpose: a whole-buffer scan would let one row's field answer for another's. 71func tr_field(buf: *u8, ls: i64, le: i64, pat: *u8) -> i64 { 72 let pl: i64 = el_len(pat) 73 if pl == 0 { return 0 - 1 } 74 var i: i64 = ls 75 while i + pl <= le { 76 if el_match(buf, i, pat, pl) == 1 { 77 var j: i64 = i + pl 78 var v: i64 = 0 79 var any: i64 = 0 80 var go: i64 = 1 81 while go == 1 { 82 if j >= le { go = 0 } else { 83 let c: i64 = buf[j] as i64 84 var d: i64 = 0 85 if c >= TR_ASC0 { if c <= TR_ASC9 { d = 1 } } 86 if d == 1 { v = v * 10 + (c - TR_ASC0); j = j + 1; any = 1 } else { go = 0 } 87 } 88 } 89 if any == 1 { return v } 90 return 0 - 1 91 } 92 i = i + 1 93 } 94 return 0 - 1 95} 96 97// Decompose an already-loaded ledger buffer. PURE (no file IO) so the gate can prove it on 98// in-memory fixtures -- a core that can only be exercised through the filesystem is a core that 99// gets tested by its own side effects. 100// Returns 0 on success, TR_ERR_NOLOG when the buffer is empty, TR_ERR_THIN when fewer than two 101// measurable rows exist (nothing to difference). REFUSES rather than returning a shape that reads 102// like a real answer. 103func em_trend_buf(buf: *u8, n: i64, out: *i64) -> i64 { 104 var i: i64 = 0 105 while i < TR_OUT_SLOTS { out[i] = 0; i = i + 1 } 106 if n <= 0 { return TR_ERR_NOLOG } 107 108 let ep: *i64 = sys_mmap(TR_MAX_ROWS * TR_I64) as *i64 109 let dm: *i64 = sys_mmap(TR_MAX_ROWS * TR_I64) as *i64 110 let cu: *i64 = sys_mmap(TR_MAX_ROWS * TR_I64) as *i64 111 let ba: *i64 = sys_mmap(TR_MAX_ROWS * TR_I64) as *i64 112 113 var rows: i64 = 0 114 var scanned: i64 = 0 115 var capped: i64 = 0 116 117 var p: i64 = 0 118 while p < n { 119 var e: i64 = p 120 var go: i64 = 1 121 while go == 1 { 122 if e >= n { go = 0 } else { 123 if (buf[e] as i64) == TR_NL { go = 0 } else { e = e + 1 } 124 } 125 } 126 if e > p { 127 scanned = scanned + 1 128 // Only rows carrying the full triple are measurable. The cadence `src=beat` rows repeat 129 // the headline without sum_cur/sum_bar; treating their absence as zero would invent a 130 // catastrophic regression on every heartbeat. 131 let c: i64 = tr_field(buf, p, e, "sum_cur=" as *u8) 132 let b: i64 = tr_field(buf, p, e, "sum_bar=" as *u8) 133 let d: i64 = tr_field(buf, p, e, "domains=" as *u8) 134 if c >= 0 { if b > 0 { if d > 0 { 135 if rows < TR_MAX_ROWS { 136 ep[rows] = tr_field(buf, p, e, "epoch=" as *u8) 137 dm[rows] = d 138 cu[rows] = c 139 ba[rows] = b 140 rows = rows + 1 141 } else { capped = 1 } 142 } } } 143 } 144 p = e + 1 145 } 146 147 out[TRO_ROWS] = rows 148 out[TRO_SCANNED] = scanned 149 out[TRO_CAPPED] = capped 150 if rows < 2 { return TR_ERR_THIN } 151 152 out[TRO_FIRST_EPOCH] = ep[0] 153 out[TRO_FIRST_CUR] = cu[0] 154 out[TRO_FIRST_BAR] = ba[0] 155 out[TRO_FIRST_DOM] = dm[0] 156 out[TRO_LAST_EPOCH] = ep[rows-1] 157 out[TRO_LAST_CUR] = cu[rows-1] 158 out[TRO_LAST_BAR] = ba[rows-1] 159 out[TRO_LAST_DOM] = dm[rows-1] 160 if ba[0] > 0 { out[TRO_FIRST_PERMIL] = (TR_PERMIL * cu[0]) / ba[0] } 161 if ba[rows-1] > 0 { out[TRO_LAST_PERMIL] = (TR_PERMIL * cu[rows-1]) / ba[rows-1] } 162 163 var advance: i64 = 0 164 var stable: i64 = 0 165 var expand: i64 = 0 166 var ambig: i64 = 0 167 var added: i64 = 0 168 var k: i64 = 1 169 while k < rows { 170 let dc: i64 = cu[k] - cu[k-1] 171 let db: i64 = ba[k] - ba[k-1] 172 let dd: i64 = dm[k] - dm[k-1] 173 if dd == 0 { 174 // stable cohort: the whole sum_cur delta is a real rung climbed (or lost). EXACT. 175 if dc != 0 { advance = advance + dc } 176 stable = stable + 1 177 } else { 178 if dd > 0 { added = added + dd } 179 if db > 0 { expand = expand + db } 180 if dc != 0 { ambig = ambig + dc } 181 } 182 k = k + 1 183 } 184 out[TRO_ADVANCE] = advance 185 out[TRO_STABLE_SPANS] = stable 186 out[TRO_EXPAND_BAR] = expand 187 out[TRO_AMBIG_CUR] = ambig 188 out[TRO_ADDED_DOM] = added 189 190 // What share of the observed sum_cur movement this lib could attribute EXACTLY. The honesty 191 // number: a high headline confidence on top of a low coverage is the self-ceiling defect. 192 var mag_adv: i64 = advance 193 if mag_adv < 0 { mag_adv = 0 - mag_adv } 194 var mag_amb: i64 = ambig 195 if mag_amb < 0 { mag_amb = 0 - mag_amb } 196 let total: i64 = mag_adv + mag_amb 197 if total > 0 { out[TRO_DECOMP_PERMIL] = (TR_PERMIL * mag_adv) / total } else { out[TRO_DECOMP_PERMIL] = TR_PERMIL } 198 return 0 199} 200 201// Thin file wrapper over the pure core. A missing/unreadable ledger is TR_ERR_NOLOG, never a 202// silently-empty success -- an absent instrument reads identically to a flat one otherwise. 203func em_trend(logpath: *u8, out: *i64) -> i64 { 204 let szp: *i64 = sys_mmap(16) as *i64 205 let buf: *u8 = ss_readall(logpath, szp) 206 var n: i64 = szp[0] 207 if n < 0 { n = 0 } 208 if n == 0 { 209 var i: i64 = 0 210 while i < TR_OUT_SLOTS { out[i] = 0; i = i + 1 } 211 return TR_ERR_NOLOG 212 } 213 return em_trend_buf(buf, n, out) 214} 215 216// The finding about the SUBJECT. Deliberately separate from the instrument's own GREEN/RED so a 217// truthful "the ecosystem is flat" can never be mistaken for "the measurement broke". 218func tr_finding(out: *i64) -> i64 { 219 if out[TRO_STABLE_SPANS] == 0 { return TR_UNDECIDABLE } 220 if out[TRO_ADVANCE] > 0 { return TR_RISING } 221 if out[TRO_ADVANCE] < 0 { return TR_FALLING } 222 return TR_FLAT 223} 224 225func tr_finding_label(f: i64) -> *u8 { 226 if f == TR_RISING { return "RISING" as *u8 } 227 if f == TR_FLAT { return "FLAT" as *u8 } 228 if f == TR_FALLING { return "FALLING" as *u8 } 229 return "UNDECIDABLE" as *u8 230}