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}