code wiki / _hdl_build / nx_atlas_graphrec_lib.nx
nx_atlas_graphrec_lib.nx source
↩ module page · 225 lines · 10594 B
1// nx_atlas_graphrec.nx -- NATIVE GRAPH-ADJACENCY RECOMBINATION over the eco graph (F225a).
2//
3// WHY (measured 2026-07-30, debt seq1489): nx_atlas_discover sources candidates from the
4// commontask catalog -- ~48 task rows yielding 27 classified organs -- while the eco graph
5// holds 16,580 nodes. A ~614x substrate gap, and the saturation shows in discover's OWN
6// output: 40 proposals with declared=1, trusted=7, GUESSED=20. Half of every proposal is a
7// kind INFERRED from the organ name because the real catalog pairs are exhausted. A
8// recombinator emitting 50pct guesses is not discovering, it is confabulating.
9// Corroborated externally (July 2026): agent skill-selection accuracy undergoes a PHASE
10// TRANSITION as library size grows -- routing over a FLAT catalog degrades combinatorially.
11// The fix is the SUBSTRATE, not the volume.
12//
13// THIS ORGAN DOES NOT REPLACE nx_atlas_discover. Its header rationale still holds: "the eco
14// graph sees STATIC import edges; plans compose tools at RUNTIME -- edges the graph cannot
15// see." So this is the SIBLING mechanism: discover mines RUNTIME composability from the
16// catalog, graphrec mines STRUCTURAL adjacency from the graph. Distinct ids (GRC vs DSC).
17//
18// METHOD -- classic link prediction (common neighbours) on the undirected import graph:
19// candidate (a,b) iff NO direct edge a<->b AND |N(a) INTERSECT N(b)| >= minscore.
20// A missing edge between two modules with many shared neighbours is the standard signal that
21// the edge SHOULD exist -- i.e. a real composition opportunity, DERIVED not guessed.
22// HUB SUPPRESSION: a witness whose degree exceeds GR_HUB_THR is skipped. Hubs (nx_syscalls
23// and friends) are imported by nearly everything, so they are evidence of nothing; counting
24// them would make every pair look related and rediscover the same saturation in a new costume.
25// This is also what bounds the cost: sum over witnesses of deg^2 instead of all-pairs.
26//
27// PROVENANCE: every row is source=graph. There is no name-guessing path in this organ AT ALL,
28// so its guess rate is 0 BY CONSTRUCTION -- that is the metric seq1489 says must fall.
29// Proposals stay status=proposed: promotion still requires a plan run to PROVE the combo.
30//
31// REFUSAL, NOT A REMINDER: an unloadable or empty graph EXITS 2 rather than printing
32// "0 proposals", which would read as "nothing to recombine" -- the exact lie to avoid.
33// NO SILENT CAPS: witnesses skipped as hubs and pair-table overflow are both REPORTED.
34// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
35import "nx_eco_graph.nx"
36import "nx_store_seed_lib.nx"
37import "nx_syscalls.nx"
38const GR_MAGIC_2654435761: i64 = 2654435761
39
40const GR_OUT: i64 = 262144
41const GR_HUB_THR: i64 = 24
42// ENDPOINT hub cap (added after the FIRST live run exposed the defect): suppressing hubs as
43// WITNESSES is not enough -- a hub must also be barred as a CANDIDATE ENDPOINT. nx_syscalls.nx
44// (Ca=14084) took 6 of the first top-20 slots purely because a universal import shares
45// neighbours with everything. Those rows are hub artifacts, not composition opportunities.
46// Infrastructure everyone already imports is not a recombination candidate, by definition.
47const GR_ENDPOINT_THR: i64 = 512
48const GR_NBUF: i64 = 64
49const GR_PAIRCAP: i64 = 1048576
50const GR_PAIRMASK: i64 = 1048575
51const GR_TOPN: i64 = 40
52const GR_MINSCORE: i64 = 3
53const GR_NAMEMAX: i64 = 128
54const GR_STDERR: i64 = 2
55const GR_ROWS: i64 = 65536
56// OWN PLANE BY DEFAULT, and that is a correctness decision, not a preference. sts_seed commits
57// the WHOLE plane, so seeding knowledge/store/discovery- here would CLOBBER every DSC row
58// nx_atlas_discover wrote -- the exact data-loss class nx_jrnlguard/planeguard exist to catch.
59// Two producers, two planes; a consumer can read both. Override with argv[4] if you mean to.
60const GR_OUTP_DEF: *u8 = "knowledge/store/discovery-graph-" as *u8
61
62func gr_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
63
64func gr_cat(o: *u8, at: i64, s: *u8) -> i64 {
65 var a: i64 = at
66 var i: i64 = 0
67 while s[i] != (0 as u8) { o[a] = s[i]; a = a + 1; i = i + 1 }
68 return a
69}
70
71func gr_catn(o: *u8, at: i64, v: i64) -> i64 {
72 if v == 0 { o[at] = 48 as u8; return at + 1 }
73 let tmp: *u8 = sys_mmap(32)
74 var n: i64 = 0
75 var x: i64 = v
76 while x > 0 { tmp[n] = ((x % 10) + 48) as u8; x = x / 10; n = n + 1 }
77 var a: i64 = at
78 while n > 0 { n = n - 1; o[a] = tmp[n]; a = a + 1 }
79 return a
80}
81
82// node name (NUL-terminated in the graph arena, written by eg_intern)
83func gr_name(g: *EcoGraph, idx: i64) -> *u8 {
84 let base: i64 = g.arena as i64
85 return (base + g.node_off[idx]) as *u8
86}
87
88func gr_deg(g: *EcoGraph, i: i64) -> i64 { return eg_ce(g, i) + eg_ca(g, i) }
89
90// collect N(i) = out-neighbours ++ in-neighbours into buf, bounded by GR_NBUF. returns count.
91func gr_nbrs(g: *EcoGraph, i: i64, buf: *i64) -> i64 {
92 var n: i64 = 0
93 var p: i64 = g.out_head[i]
94 let pe: i64 = g.out_head[i + 1]
95 while p < pe { if n < GR_NBUF { buf[n] = g.out_list[p]; n = n + 1 } p = p + 1 }
96 var q: i64 = g.in_head[i]
97 let qe: i64 = g.in_head[i + 1]
98 while q < qe { if n < GR_NBUF { buf[n] = g.in_list[q]; n = n + 1 } q = q + 1 }
99 return n
100}
101
102// is there a DIRECT edge a->b or b->a? (already composed -> never a proposal)
103func gr_has_edge(g: *EcoGraph, a: i64, b: i64) -> i64 {
104 var p: i64 = g.out_head[a]
105 let pe: i64 = g.out_head[a + 1]
106 while p < pe { if g.out_list[p] == b { return 1 } p = p + 1 }
107 var q: i64 = g.out_head[b]
108 let qe: i64 = g.out_head[b + 1]
109 while q < qe { if g.out_list[q] == a { return 1 } q = q + 1 }
110 return 0
111}
112
113func gr_hash_key(k: i64) -> i64 {
114 var h: i64 = k
115 h = (h ^ (h >> 33)) * GR_MAGIC_2654435761
116 h = h & 0x7fffffff
117 return h & GR_PAIRMASK
118}
119
120// ---- THE SCORING CORE, extracted so a GATE can prove it --------------------------------------
121// This logic was inline in main(), which meant the ONLY way to exercise it was to run the whole
122// organ against a real 17k-node graph -- so any gate over it would have been vacuous, and I
123// shipped the organ without one. A primitive's gate must own its proof, so the code that DECIDES
124// what gets proposed now lives in a function callable with a SYNTHETIC graph.
125// Fills ta/tb/ts (descending by score) and returns the count written.
126// stats: [0]=witnesses [1]=hubs_skipped [2]=pair_overflow [3]=considered [4]=hub_endpoints
127func gr_propose(g: *EcoGraph, minscore: i64, topn: i64, ta: *i64, tb: *i64, ts: *i64, stats: *i64) -> i64 {
128 if g.finalized != 1 { eg_finalize(g) }
129 let n: i64 = g.node_count
130 let pk: *i64 = sys_mmap(GR_PAIRCAP * 8) as *i64
131 let pc: *i64 = sys_mmap(GR_PAIRCAP * 8) as *i64
132 let nb: *i64 = sys_mmap(GR_NBUF * 8) as *i64
133 var overflow: i64 = 0
134 var hubs_skipped: i64 = 0
135 var witnesses: i64 = 0
136 var w: i64 = 0
137 while w < n {
138 let d: i64 = gr_deg(g, w)
139 if d >= 2 {
140 if d > GR_HUB_THR { hubs_skipped = hubs_skipped + 1 } else {
141 witnesses = witnesses + 1
142 let cnt: i64 = gr_nbrs(g, w, nb)
143 var i: i64 = 0
144 while i < cnt {
145 var j: i64 = i + 1
146 while j < cnt {
147 var a: i64 = nb[i]
148 var c: i64 = nb[j]
149 if a != c {
150 if a > c { let t: i64 = a; a = c; c = t }
151 let key: i64 = a * n + c
152 var h: i64 = gr_hash_key(key)
153 var probes: i64 = 0
154 var placed: i64 = 0
155 // EXIT on placement -- a probe loop that keeps spinning after it has
156 // placed runs 64x per pair over ~18M pairs, which is not a micro-cost.
157 while placed == 0 {
158 if probes >= 64 { placed = 2 } else {
159 if pc[h] == 0 { pk[h] = key; pc[h] = 1; placed = 1 } else {
160 if pk[h] == key { pc[h] = pc[h] + 1; placed = 1 } else {
161 h = (h + 1) & GR_PAIRMASK
162 probes = probes + 1
163 }
164 }
165 }
166 }
167 if placed == 2 { overflow = overflow + 1 }
168 }
169 j = j + 1
170 }
171 i = i + 1
172 }
173 }
174 }
175 w = w + 1
176 }
177 var have: i64 = 0
178 var considered: i64 = 0
179 var hub_endpoints: i64 = 0
180 var s: i64 = 0
181 while s < GR_PAIRCAP {
182 let sc: i64 = pc[s]
183 if sc >= minscore {
184 let key: i64 = pk[s]
185 let a: i64 = key / n
186 let c: i64 = key % n
187 if a != c {
188 considered = considered + 1
189 var endpoint_ok: i64 = 1
190 if gr_deg(g, a) > GR_ENDPOINT_THR { endpoint_ok = 0 }
191 if gr_deg(g, c) > GR_ENDPOINT_THR { endpoint_ok = 0 }
192 if endpoint_ok == 0 { hub_endpoints = hub_endpoints + 1 }
193 if endpoint_ok == 1 { if gr_has_edge(g, a, c) == 0 {
194 var slot: i64 = 0 - 1
195 if have < topn { slot = have; have = have + 1 } else {
196 if sc > ts[topn - 1] { slot = topn - 1 }
197 }
198 if slot >= 0 {
199 ta[slot] = a; tb[slot] = c; ts[slot] = sc
200 var k: i64 = slot
201 var go: i64 = 1
202 while go == 1 {
203 if k <= 0 { go = 0 } else {
204 if ts[k - 1] < ts[k] {
205 let xa: i64 = ta[k - 1]; let xb: i64 = tb[k - 1]; let xs: i64 = ts[k - 1]
206 ta[k - 1] = ta[k]; tb[k - 1] = tb[k]; ts[k - 1] = ts[k]
207 ta[k] = xa; tb[k] = xb; ts[k] = xs
208 k = k - 1
209 } else { go = 0 }
210 }
211 }
212 }
213 } }
214 }
215 }
216 s = s + 1
217 }
218 stats[0] = witnesses
219 stats[1] = hubs_skipped
220 stats[2] = overflow
221 stats[3] = considered
222 stats[4] = hub_endpoints
223 return have
224}
225