code wiki / _hdl_build / nx_gatescan.nx
nx_gatescan.nx source
↩ module page · 324 lines · 15214 B
1// nx_gatescan.nx -- FIND THE ORGANS NOTHING IS CHECKING.
2//
3// ===== WHY THIS EXISTS ============================================
4//
5// Two defects were found by luck in this ecosystem within one session:
6//
7// nx_pow10 -- no gate of its own. Its header claimed an interpolation
8// error of <=0.3%; measured, it is 0.66%. That figure had
9// been inherited into a FOOD-SAFETY margin note.
10// nx_arrhenius -- no gate AND nothing imported it. It returned exactly 1.0
11// for every input, i.e. "temperature does not affect
12// reaction rate", from a file whose own comment claimed it
13// carefully managed precision across the reciprocal step.
14//
15// Both were caught because someone happened to look. That is not a control.
16// A session must remember; a tool cannot forget. This organ turns
17// "no gate + no callers = an unverified intention" from a law into a scan.
18//
19// ===== THE MEASUREMENT ============================================
20//
21// Composes the existing graph engine rather than rebuilding it: the whole-
22// tree walk and accurate import edges already exist and are proven, so this
23// adds exactly one idea on top of them.
24//
25// HAS_GATE -- a node named <base>_test.nx or <base>_gate.nx exists.
26// CONSUMERS -- importers that are NOT that organ's own gate.
27//
28// ★THE SECOND HALF IS THE LOAD-BEARING ONE. Raw fan-in (Ca) counts the gate
29// as an importer, so the moment you write a gate for a dead organ its Ca
30// becomes 1 and a naive orphan check calls it "used". That is exactly what
31// would have hidden nx_arrhenius the instant it was gated. Consumers are
32// therefore counted with gate/test importers EXCLUDED.
33//
34// ===== RISK IS RANKED, NOT LISTED =================================
35//
36// A flat list of ungated files invites working the easy end. The two classes
37// are not equally dangerous and the ranking says so:
38//
39// UNGATED-LOAD-BEARING (no gate, N consumers) -- an error here propagates
40// into N callers and is attributed to whichever one notices first.
41// This is the nx_pow10 class. Risk RISES with fan-in.
42// DEAD-RISK (no gate, no consumers) -- harmless today because nothing
43// calls it, dangerous the moment somebody wires it up believing the
44// header. This is the nx_arrhenius class.
45//
46// So severity = fan-in for the first class, and a fixed floor for the
47// second: never zero, because "nothing calls it" is a reason to distrust it,
48// not a reason to ignore it.
49//
50// nx_gatescan <root-dir> [min-consumers] [topk]
51// default root runtime, min-consumers 1 (load-bearing only; pass 0
52// to include organs nothing calls), topk 40
53//
54// license_tier: ORIGINAL
55import "nx_syscalls.nx"
56import "nx_eco_graph.nx"
57import "nx_import_scan.nx"
58import "nx_gatelib.nx"
59const GS_MAGIC_4096: i64 = 4096
60const GS_MAGIC_1024: i64 = 1024
61
62const GS_MAXNODE: i64 = 24000
63const GS_MAXEDGE: i64 = 300000
64const GS_ARENA: i64 = 4194304
65const GS_HASH: i64 = 65536
66const GS_FILECAP: i64 = 262144
67
68// ⚠DEBT, NAMED RATHER THAN HIDDEN: the getdents recursion below is the THIRD
69// copy of the same tree walk in this tree (nx_eco_graph_build's egb_walk and
70// nx_dr_tree's tr_walk are the others). It is duplicated here because both
71// of those live in modules that own a main(), so neither can be imported by
72// a second entry point -- the walk was never extracted into a library. The
73// right fix is a shared walk lib, which touches every consumer and needs its
74// own re-gating, so it is filed instead of smuggled into this change. An
75// organ whose whole purpose is finding unchecked code should not quietly add
76// unchecked duplication while doing it.
77
78// A dead organ still scores this much, so it can never rank as "no risk".
79//
80// ⚠⚠THIS FLOOR HID A REAL DEFECT AND THE FIX IS NOT TO RAISE IT. Extracting
81// nx_gatelib created an ungated organ with two consumers -- exactly the class
82// this tool exists to find -- and it did not appear anywhere in the report,
83// because severity 2 ranks BELOW the floor of 3 handed to files nothing calls
84// at all. It sat under five thousand dead scratch files.
85//
86// A load-bearing organ with two consumers matters MORE than a dead one with
87// none, so the two classes cannot share one scale: comparing "reach" against
88// "nothing calls it" is comparing different quantities. They are now ranked
89// SEPARATELY and both printed, with the load-bearing list first, so a small
90// but live finding can never be buried under a large dead one.
91const GS_DEAD_FLOOR: i64 = 3
92
93func gw(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
94func gn(v: i64) -> i64 { let b: *u8 = sys_mmap(24); var m: i64 = v; if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } let t: *u8 = sys_mmap(24); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } var i: i64 = 0; while i < k { b[i] = t[k - 1 - i]; i = i + 1 } sys_write(1, b, k); return 0 }
95
96// Recursive walk: interns every .nx basename as a node and adds its imports
97// as edges. st[0] = files scanned.
98
99// Does `name` (length n) end with `suf`? Used to recognise gate files by
100// convention -- both suffixes are in live use in this tree.
101
102// Is this node itself a gate/test file rather than an organ?
103
104// Write <base><suffix> into out, where base is name minus its ".nx".
105func gs_make_gate_name(name: *u8, n: i64, suffix: *u8, out: *u8) -> i64 {
106 let stem: i64 = n - 3
107 var i: i64 = 0
108 while i < stem { out[i] = name[i]; i = i + 1 }
109 let m: i64 = gl_len(suffix)
110 var j: i64 = 0
111 while j < m { out[stem + j] = suffix[j]; j = j + 1 }
112 out[stem + m] = 0 as u8
113 return stem + m
114}
115
116// ★★THE NAMING RULE COST ME A WRONG HEADLINE, SO IT IS WORTH STATING.
117//
118// The first cut of this scan asked only whether <base>_test.nx or
119// <base>_gate.nx existed, and reported 1019 ungated load-bearing organs.
120// Spot-checking the top rows before filing showed the number was inflated:
121// nx_sha256 IS gated, by nx_sha256_kat_test.nx; nx_tls13 is gated by a dozen
122// files like nx_tls13_alpn_test.nx. This tree's real convention is
123// <organ>_<aspect>_test.nx, and a strict suffix match cannot see it.
124//
125// So a gate for organ X is any file that starts with X's stem FOLLOWED BY AN
126// UNDERSCORE and ends in _test.nx or _gate.nx. The underscore is what keeps
127// the rule honest in the other direction: without it nx_str would be claimed
128// as gated by nx_strategy_test.nx, which tests a different organ entirely.
129//
130// Attribution runs from the GATE side rather than the organ side: strip the
131// suffix, then walk the underscore split points longest-first and attribute
132// the gate to the most specific organ that exists. That is O(gates x parts)
133// with hash lookups instead of O(organs x nodes) of string comparison, and
134// longest-first means nx_foo_bar_test.nx credits nx_foo_bar when that organ
135// exists and only falls back to nx_foo when it does not.
136
137func main(argc: i64, argv: *i64) -> i64 {
138 var root: *u8 = "runtime" as *u8
139 // ★DEFAULT IS 1, NOT 0, AND THAT IS THE FIX FOR THE BURIED FINDING.
140 // With 0 the board fills with five thousand dead scratch files and a
141 // live two-consumer library ranks beneath all of them. Load-bearing is
142 // what a reader should see first; pass 0 explicitly to include the dead.
143 var min_consumers: i64 = 1
144 var topk: i64 = 40
145 if argc >= 2 { root = argv[1] as *u8 }
146 if argc >= 3 {
147 let s: *u8 = argv[2] as *u8
148 var v: i64 = 0
149 var i: i64 = 0
150 while s[i] != (0 as u8) { let d: i64 = s[i] as i64; if d >= 48 { if d <= 57 { v = v * 10 + (d - 48) } } i = i + 1 }
151 min_consumers = v
152 }
153 if argc >= 4 {
154 let s2: *u8 = argv[3] as *u8
155 var v2: i64 = 0
156 var i2: i64 = 0
157 while s2[i2] != (0 as u8) { let d2: i64 = s2[i2] as i64; if d2 >= 48 { if d2 <= 57 { v2 = v2 * 10 + (d2 - 48) } } i2 = i2 + 1 }
158 if v2 > 0 { topk = v2 }
159 }
160
161 let g: *EcoGraph = eg_new(GS_MAXNODE, GS_MAXEDGE, GS_ARENA, GS_HASH)
162 let files: *i64 = sys_mmap(16)
163 files[0] = 0
164 let path: *u8 = sys_mmap(GS_MAGIC_4096)
165 let filebuf: *u8 = sys_mmap(GS_FILECAP)
166 let aoff: *i64 = sys_mmap(GS_MAGIC_1024 * 8)
167 let alen: *i64 = sys_mmap(GS_MAGIC_1024 * 8)
168 var rn: i64 = 0
169 while root[rn] != (0 as u8) { path[rn] = root[rn]; rn = rn + 1 }
170 gl_walk(g, path, rn, filebuf, aoff, alen, files, 1, 0 as *u8, 0 as *i64, 0 as *i64, 0)
171 eg_finalize(g)
172
173 // PASS A: attribute every gate file to the most specific organ it covers.
174 let gatedmap: *u8 = sys_mmap(g.node_count + 8)
175 var z: i64 = 0
176 while z < g.node_count { gatedmap[z] = 0 as u8; z = z + 1 }
177 let nbuf: *u8 = sys_mmap(512)
178 var gi: i64 = 0
179 var gatefiles: i64 = 0
180 while gi < g.node_count {
181 let gn2: *u8 = ((g.arena as i64) + g.node_off[gi]) as *u8
182 let gl: i64 = gl_len(gn2)
183 if gl_is_gate_name(gn2, gl) == 1 {
184 gatefiles = gatefiles + 1
185 let ao: i64 = gl_attribute(g, gn2, gl, nbuf)
186 if ao >= 0 { gatedmap[ao] = 1 as u8 }
187 }
188 gi = gi + 1
189 }
190
191 gw("=== NISHI GATESCAN -- organs nothing is checking ===\n" as *u8)
192 gw("root=" as *u8); gw(root)
193 gw(" files=" as *u8); gn(files[0])
194 gw(" nodes=" as *u8); gn(g.node_count)
195 gw(" edges=" as *u8); gn(g.edge_count)
196 gw(" gatefiles=" as *u8); gn(gatefiles)
197 gw("\n" as *u8)
198
199 // Bounded top-K, ranked by severity. Declared, never silent (scale law).
200 let bk_idx: *i64 = sys_mmap((topk + 2) * 8)
201 let bk_sev: *i64 = sys_mmap((topk + 2) * 8)
202 let bk_con: *i64 = sys_mmap((topk + 2) * 8)
203 var bn: i64 = 0
204
205 var ungated_loadbearing: i64 = 0
206 var dead_risk: i64 = 0
207 var organs: i64 = 0
208 var gated: i64 = 0
209
210 let gname: *u8 = sys_mmap(512)
211 var v: i64 = 0
212 while v < g.node_count {
213 let nm: *u8 = ((g.arena as i64) + g.node_off[v]) as *u8
214 let nlen: i64 = gl_len(nm)
215 if gl_is_gate_name(nm, nlen) == 0 {
216 organs = organs + 1
217
218 // --- HAS_GATE: exact convention, or any <organ>_<aspect>_test/gate
219 // attributed to this organ in pass A.
220 var has_gate: i64 = 0
221 if gatedmap[v] == (1 as u8) { has_gate = 1 }
222 if has_gate == 0 {
223 gs_make_gate_name(nm, nlen, "_test.nx" as *u8, gname)
224 if eg_find(g, gname, gl_len(gname)) >= 0 { has_gate = 1 }
225 }
226 if has_gate == 0 {
227 gs_make_gate_name(nm, nlen, "_gate.nx" as *u8, gname)
228 if eg_find(g, gname, gl_len(gname)) >= 0 { has_gate = 1 }
229 }
230
231 // --- CONSUMERS: importers that are not gate/test files.
232 // Counting raw Ca here would let a freshly-written gate disguise
233 // a dead organ as a used one, which is the whole failure mode.
234 var consumers: i64 = 0
235 var p: i64 = g.in_head[v]
236 let e: i64 = g.in_head[v + 1]
237 while p < e {
238 let src: i64 = g.in_list[p]
239 let sn: *u8 = ((g.arena as i64) + g.node_off[src]) as *u8
240 if gl_is_gate_name(sn, gl_len(sn)) == 0 { consumers = consumers + 1 }
241 p = p + 1
242 }
243
244 if has_gate == 1 { gated = gated + 1 }
245 if has_gate == 0 {
246 // Severity: propagation reach for the load-bearing class, a
247 // floor for the dead class. Never zero -- "nothing calls it"
248 // is a reason to distrust an organ, not to ignore it.
249 var sev: i64 = GS_DEAD_FLOOR
250 if consumers > 0 { sev = consumers; ungated_loadbearing = ungated_loadbearing + 1 }
251 if consumers == 0 { dead_risk = dead_risk + 1 }
252 // DEAD-RISK is counted but never competes with load-bearing
253 // for board space -- different quantities, separate scales.
254 if consumers > 0 {
255 if consumers >= min_consumers {
256 // Bounded top-K, descending by severity. PLACE THEN
257 // BUBBLE, never shift-then-write: the first cut of this
258 // shifted entries down and then skipped the write on the
259 // path that found the insertion point, which duplicated
260 // one row across the whole board. Swapping cannot lose
261 // or duplicate an entry by construction.
262 var start: i64 = 0 - 1
263 if bn < topk {
264 start = bn
265 bk_sev[bn] = sev; bk_idx[bn] = v; bk_con[bn] = consumers
266 bn = bn + 1
267 }
268 if bn >= topk {
269 if start < 0 {
270 if sev > bk_sev[topk - 1] {
271 start = topk - 1
272 bk_sev[start] = sev; bk_idx[start] = v; bk_con[start] = consumers
273 }
274 }
275 }
276 var q: i64 = start
277 while q > 0 {
278 if bk_sev[q - 1] < bk_sev[q] {
279 let ts: i64 = bk_sev[q - 1]; bk_sev[q - 1] = bk_sev[q]; bk_sev[q] = ts
280 let ti: i64 = bk_idx[q - 1]; bk_idx[q - 1] = bk_idx[q]; bk_idx[q] = ti
281 let tc: i64 = bk_con[q - 1]; bk_con[q - 1] = bk_con[q]; bk_con[q] = tc
282 q = q - 1
283 } else { q = 0 }
284 }
285 }
286 }
287 }
288 }
289 v = v + 1
290 }
291
292 gw("organs=" as *u8); gn(organs)
293 gw(" gated=" as *u8); gn(gated)
294 gw(" UNGATED-LOAD-BEARING=" as *u8); gn(ungated_loadbearing)
295 gw(" DEAD-RISK(no gate,no consumers)=" as *u8); gn(dead_risk)
296 gw("\n" as *u8)
297
298 var shown: i64 = bn
299 if shown > topk { shown = topk }
300 gw("-- ranked by severity (top " as *u8); gn(shown)
301 gw(" of " as *u8); gn(ungated_loadbearing)
302 gw(" ungated; bound DECLARED not silent) --\n" as *u8)
303 var r: i64 = 0
304 while r < shown {
305 let nm2: *u8 = ((g.arena as i64) + g.node_off[bk_idx[r]]) as *u8
306 gw(" sev=" as *u8); gn(bk_sev[r])
307 if bk_con[r] == 0 { gw(" DEAD-RISK " as *u8) } else { gw(" UNGATED-LOAD-BEARING" as *u8) }
308 gw(" consumers=" as *u8); gn(bk_con[r])
309 gw(" " as *u8); gw(nm2)
310 gw("\n" as *u8)
311 r = r + 1
312 }
313
314 // A scan finding is a HYPOTHESIS. It says nothing checks this organ; it
315 // does not say the organ is wrong. Whoever acts on a row still has to
316 // read the file -- nx_pow10 turned out to be subtly wrong and
317 // nx_arrhenius completely dead, and only reading them established that.
318 gw("\nNOTE: each row is a HYPOTHESIS (nothing checks this organ), not a defect.\n" as *u8)
319 gw(" Verify by reading before filing. verdict=" as *u8)
320 if ungated_loadbearing + dead_risk == 0 { gw("GREEN\n" as *u8); sys_exit(0); return 0 }
321 gw("FINDINGS\n" as *u8)
322 sys_exit(0)
323 return 0
324}