code wiki / _hdl_build / nx_atlas_graphrec.nx

nx_atlas_graphrec.nx source

↩ module page · 341 lines · 16328 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 39const GR_MAGIC_1024: i64 = 1024 40 41const GR_OUT: i64 = 262144 42const GR_HUB_THR: i64 = 24 43// ENDPOINT hub cap (added after the FIRST live run exposed the defect): suppressing hubs as 44// WITNESSES is not enough -- a hub must also be barred as a CANDIDATE ENDPOINT. nx_syscalls.nx 45// (Ca=14084) took 6 of the first top-20 slots purely because a universal import shares 46// neighbours with everything. Those rows are hub artifacts, not composition opportunities. 47// Infrastructure everyone already imports is not a recombination candidate, by definition. 48const GR_ENDPOINT_THR: i64 = 512 49const GR_NBUF: i64 = 64 50const GR_PAIRCAP: i64 = 1048576 51const GR_PAIRMASK: i64 = 1048575 52const GR_TOPN: i64 = 40 53const GR_MINSCORE: i64 = 3 54const GR_NAMEMAX: i64 = 128 55const GR_STDERR: i64 = 2 56const GR_ROWS: i64 = 65536 57// OWN PLANE BY DEFAULT, and that is a correctness decision, not a preference. sts_seed commits 58// the WHOLE plane, so seeding knowledge/store/discovery- here would CLOBBER every DSC row 59// nx_atlas_discover wrote -- the exact data-loss class nx_jrnlguard/planeguard exist to catch. 60// Two producers, two planes; a consumer can read both. Override with argv[4] if you mean to. 61const GR_OUTP_DEF: *u8 = "knowledge/store/discovery-graph-" as *u8 62 63func gr_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 64 65func gr_cat(o: *u8, at: i64, s: *u8) -> i64 { 66 var a: i64 = at 67 var i: i64 = 0 68 while s[i] != (0 as u8) { o[a] = s[i]; a = a + 1; i = i + 1 } 69 return a 70} 71 72func gr_catn(o: *u8, at: i64, v: i64) -> i64 { 73 if v == 0 { o[at] = 48 as u8; return at + 1 } 74 let tmp: *u8 = sys_mmap(32) 75 var n: i64 = 0 76 var x: i64 = v 77 while x > 0 { tmp[n] = ((x % 10) + 48) as u8; x = x / 10; n = n + 1 } 78 var a: i64 = at 79 while n > 0 { n = n - 1; o[a] = tmp[n]; a = a + 1 } 80 return a 81} 82 83// node name (NUL-terminated in the graph arena, written by eg_intern) 84func gr_name(g: *EcoGraph, idx: i64) -> *u8 { 85 let base: i64 = g.arena as i64 86 return (base + g.node_off[idx]) as *u8 87} 88 89func gr_deg(g: *EcoGraph, i: i64) -> i64 { return eg_ce(g, i) + eg_ca(g, i) } 90 91// collect N(i) = out-neighbours ++ in-neighbours into buf, bounded by GR_NBUF. returns count. 92func gr_nbrs(g: *EcoGraph, i: i64, buf: *i64) -> i64 { 93 var n: i64 = 0 94 var p: i64 = g.out_head[i] 95 let pe: i64 = g.out_head[i + 1] 96 while p < pe { if n < GR_NBUF { buf[n] = g.out_list[p]; n = n + 1 } p = p + 1 } 97 var q: i64 = g.in_head[i] 98 let qe: i64 = g.in_head[i + 1] 99 while q < qe { if n < GR_NBUF { buf[n] = g.in_list[q]; n = n + 1 } q = q + 1 } 100 return n 101} 102 103// is there a DIRECT edge a->b or b->a? (already composed -> never a proposal) 104func gr_has_edge(g: *EcoGraph, a: i64, b: i64) -> i64 { 105 var p: i64 = g.out_head[a] 106 let pe: i64 = g.out_head[a + 1] 107 while p < pe { if g.out_list[p] == b { return 1 } p = p + 1 } 108 var q: i64 = g.out_head[b] 109 let qe: i64 = g.out_head[b + 1] 110 while q < qe { if g.out_list[q] == a { return 1 } q = q + 1 } 111 return 0 112} 113 114func gr_hash_key(k: i64) -> i64 { 115 var h: i64 = k 116 h = (h ^ (h >> 33)) * GR_MAGIC_2654435761 117 h = h & 0x7fffffff 118 return h & GR_PAIRMASK 119} 120 121func main(argc: i64, argv: *i64) -> i64 { 122 let o: *u8 = sys_mmap(GR_OUT) 123 var b: i64 = 0 124 if argc < 2 { 125 b = gr_cat(o, b, "usage: nx_atlas_graphrec <graph-store-prefix> [minscore] [topn]\n link prediction over the eco graph: proposes UNLINKED pairs with many shared neighbours.\n" as *u8) 126 sys_write(1, o, b) 127 return 1 128 } 129 let prefix: *u8 = argv[1] as *u8 130 var minscore: i64 = GR_MINSCORE 131 if argc > 2 { 132 let ms: *u8 = argv[2] as *u8 133 var v: i64 = 0 134 var i: i64 = 0 135 while ms[i] != (0 as u8) { let c: i64 = ms[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } 136 if v > 0 { minscore = v } 137 } 138 var topn: i64 = GR_TOPN 139 if argc > 3 { 140 let tn: *u8 = argv[3] as *u8 141 var v2: i64 = 0 142 var j: i64 = 0 143 while tn[j] != (0 as u8) { let c: i64 = tn[j] as i64; if c >= 48 { if c <= 57 { v2 = v2 * 10 + (c - 48) } } j = j + 1 } 144 if v2 > 0 { if v2 <= GR_TOPN { topn = v2 } } 145 } 146 147 var outp: *u8 = GR_OUTP_DEF 148 if argc > 4 { outp = argv[4] as *u8 } 149 150 let g: *EcoGraph = eg_load(prefix) 151 if (g as i64) == 0 { 152 b = gr_cat(o, b, "GRAPHREC REFUSED -- eco graph would not load from prefix: " as *u8) 153 b = gr_cat(o, b, prefix) 154 b = gr_cat(o, b, "\n Refusing rather than printing '0 proposals', which reads as 'nothing to recombine'.\n Build/save one first: nx_eco_graph_build <root> <query> <save-prefix>\n" as *u8) 155 sys_write(GR_STDERR, o, b) 156 return 2 157 } 158 let n: i64 = g.node_count 159 if n < 2 { 160 b = gr_cat(o, b, "GRAPHREC REFUSED -- graph has fewer than 2 nodes (node_count=" as *u8) 161 b = gr_catn(o, b, n) 162 b = gr_cat(o, b, ")\n" as *u8) 163 sys_write(GR_STDERR, o, b) 164 return 2 165 } 166 if g.finalized != 1 { eg_finalize(g) } 167 168 // ---- accumulate common-neighbour counts, hub-suppressed ------------------------------ 169 let pk: *i64 = sys_mmap(GR_PAIRCAP * 8) as *i64 170 let pc: *i64 = sys_mmap(GR_PAIRCAP * 8) as *i64 171 let nb: *i64 = sys_mmap(GR_NBUF * 8) as *i64 172 var overflow: i64 = 0 173 var hubs_skipped: i64 = 0 174 var witnesses: i64 = 0 175 var w: i64 = 0 176 while w < n { 177 let d: i64 = gr_deg(g, w) 178 if d >= 2 { 179 if d > GR_HUB_THR { hubs_skipped = hubs_skipped + 1 } else { 180 witnesses = witnesses + 1 181 let cnt: i64 = gr_nbrs(g, w, nb) 182 var i: i64 = 0 183 while i < cnt { 184 var j: i64 = i + 1 185 while j < cnt { 186 var a: i64 = nb[i] 187 var c: i64 = nb[j] 188 if a != c { 189 if a > c { let t: i64 = a; a = c; c = t } 190 let key: i64 = a * n + c 191 var h: i64 = gr_hash_key(key) 192 var probes: i64 = 0 193 var placed: i64 = 0 194 // EXIT on placement -- a probe loop that keeps spinning after it has 195 // placed runs 64x per pair over ~18M pairs, which is not a micro-cost. 196 while placed == 0 { 197 if probes >= 64 { placed = 2 } else { 198 if pc[h] == 0 { pk[h] = key; pc[h] = 1; placed = 1 } else { 199 if pk[h] == key { pc[h] = pc[h] + 1; placed = 1 } else { 200 h = (h + 1) & GR_PAIRMASK 201 probes = probes + 1 202 } 203 } 204 } 205 } 206 if placed == 2 { overflow = overflow + 1 } 207 } 208 j = j + 1 209 } 210 i = i + 1 211 } 212 } 213 } 214 w = w + 1 215 } 216 217 // ---- select the top-N unlinked pairs ------------------------------------------------- 218 let ta: *i64 = sys_mmap((GR_TOPN + 2) * 8) as *i64 219 let tb: *i64 = sys_mmap((GR_TOPN + 2) * 8) as *i64 220 let ts: *i64 = sys_mmap((GR_TOPN + 2) * 8) as *i64 221 var have: i64 = 0 222 var considered: i64 = 0 223 var hub_endpoints: i64 = 0 224 var s: i64 = 0 225 while s < GR_PAIRCAP { 226 let sc: i64 = pc[s] 227 if sc >= minscore { 228 let key: i64 = pk[s] 229 let a: i64 = key / n 230 let c: i64 = key % n 231 if a != c { 232 considered = considered + 1 233 var da: i64 = gr_deg(g, a) 234 var dc: i64 = gr_deg(g, c) 235 var endpoint_ok: i64 = 1 236 if da > GR_ENDPOINT_THR { endpoint_ok = 0 } 237 if dc > GR_ENDPOINT_THR { endpoint_ok = 0 } 238 if endpoint_ok == 0 { hub_endpoints = hub_endpoints + 1 } 239 if endpoint_ok == 1 { if gr_has_edge(g, a, c) == 0 { 240 // insertion into a small DESCENDING top-N: append (or displace the weakest), 241 // then bubble the new row up while it outranks its predecessor. 242 var slot: i64 = 0 - 1 243 if have < topn { 244 slot = have 245 have = have + 1 246 } else { 247 if sc > ts[topn - 1] { slot = topn - 1 } 248 } 249 if slot >= 0 { 250 ta[slot] = a; tb[slot] = c; ts[slot] = sc 251 var k: i64 = slot 252 var go: i64 = 1 253 while go == 1 { 254 if k <= 0 { go = 0 } else { 255 if ts[k - 1] < ts[k] { 256 let xa: i64 = ta[k - 1]; let xb: i64 = tb[k - 1]; let xs: i64 = ts[k - 1] 257 ta[k - 1] = ta[k]; tb[k - 1] = tb[k]; ts[k - 1] = ts[k] 258 ta[k] = xa; tb[k] = xb; ts[k] = xs 259 k = k - 1 260 } else { go = 0 } 261 } 262 } 263 } 264 } } 265 } 266 } 267 s = s + 1 268 } 269 270 b = gr_cat(o, b, "=== ATLAS GRAPHREC -- native graph-adjacency recombination (F225a) ===\n graph: " as *u8) 271 b = gr_cat(o, b, prefix) 272 b = gr_cat(o, b, "\n nodes=" as *u8); b = gr_catn(o, b, n) 273 b = gr_cat(o, b, " edges=" as *u8); b = gr_catn(o, b, g.edge_count) 274 b = gr_cat(o, b, " witnesses=" as *u8); b = gr_catn(o, b, witnesses) 275 b = gr_cat(o, b, " hubs_skipped=" as *u8); b = gr_catn(o, b, hubs_skipped) 276 b = gr_cat(o, b, "\n minscore=" as *u8); b = gr_catn(o, b, minscore) 277 b = gr_cat(o, b, " candidates_considered=" as *u8); b = gr_catn(o, b, considered) 278 b = gr_cat(o, b, " emitted=" as *u8); b = gr_catn(o, b, have) 279 // NO SILENT CAPS 280 b = gr_cat(o, b, " hub_endpoints_suppressed=" as *u8); b = gr_catn(o, b, hub_endpoints) 281 b = gr_cat(o, b, "\n bounds: pair_table_overflow=" as *u8); b = gr_catn(o, b, overflow) 282 b = gr_cat(o, b, " witness_hub_thr=" as *u8); b = gr_catn(o, b, GR_HUB_THR) 283 b = gr_cat(o, b, " endpoint_hub_thr=" as *u8); b = gr_catn(o, b, GR_ENDPOINT_THR) 284 b = gr_cat(o, b, " nbr_buf=" as *u8); b = gr_catn(o, b, GR_NBUF) 285 b = gr_cat(o, b, "\n provenance: source=graph for EVERY row -- this organ has no name-guessing path,\n so its guess rate is 0 BY CONSTRUCTION (cf. nx_atlas_discover 20 guessed / 40).\n\n" as *u8) 286 287 // Rows are built in the SAME pass that prints them, so the plane and stdout can never 288 // disagree about what was proposed. 289 let rows: *u8 = sys_mmap(GR_ROWS) 290 var ro: i64 = 0 291 var emitted_rows: i64 = 0 292 var r: i64 = 0 293 while r < have { 294 if ts[r] > 0 { 295 let na: *u8 = gr_name(g, ta[r]) 296 let nbn: *u8 = gr_name(g, tb[r]) 297 b = gr_cat(o, b, " GRC" as *u8); b = gr_catn(o, b, r + 1) 298 b = gr_cat(o, b, " " as *u8) 299 b = gr_cat(o, b, na) 300 b = gr_cat(o, b, "->" as *u8) 301 b = gr_cat(o, b, nbn) 302 b = gr_cat(o, b, " cn=" as *u8); b = gr_catn(o, b, ts[r]) 303 b = gr_cat(o, b, " proposed graph unlinked-shared-neighbours\n" as *u8) 304 // plane row: GRC<n> <A>-><B> <cn> proposed graph <evidence> <try-hint> 305 if ro < GR_ROWS - GR_MAGIC_1024 { 306 ro = gr_cat(rows, ro, "GRC" as *u8); ro = gr_catn(rows, ro, r + 1) 307 ro = gr_cat(rows, ro, "\x09" as *u8) 308 ro = gr_cat(rows, ro, na); ro = gr_cat(rows, ro, "->" as *u8); ro = gr_cat(rows, ro, nbn) 309 ro = gr_cat(rows, ro, "\x09" as *u8); ro = gr_catn(rows, ro, ts[r]) 310 ro = gr_cat(rows, ro, "\x09proposed\x09graph\x09unlinked-shared-neighbours cn=" as *u8) 311 ro = gr_catn(rows, ro, ts[r]) 312 ro = gr_cat(rows, ro, "\x09try: plan 10 " as *u8); ro = gr_cat(rows, ro, na) 313 ro = gr_cat(rows, ro, " / 20 " as *u8); ro = gr_cat(rows, ro, nbn) 314 ro = gr_cat(rows, ro, "\n" as *u8) 315 emitted_rows = emitted_rows + 1 316 } 317 } 318 r = r + 1 319 } 320 // COMMIT: never write an EMPTY plane -- an empty discovery plane reads as "no opportunities 321 // exist", which is a different claim from "this run found none above threshold". 322 if emitted_rows > 0 { 323 if sts_seed(outp, rows, ro) < 0 { 324 b = gr_cat(o, b, "\n PLANE COMMIT FAILED: " as *u8) 325 b = gr_cat(o, b, outp) 326 b = gr_cat(o, b, "\n" as *u8) 327 sys_write(1, o, b) 328 return 1 329 } 330 b = gr_cat(o, b, "\n COMMITTED " as *u8); b = gr_catn(o, b, emitted_rows) 331 b = gr_cat(o, b, " rows -> " as *u8); b = gr_cat(o, b, outp) 332 b = gr_cat(o, b, " (status=proposed; promotion still requires a plan run to PROVE the combo)\n" as *u8) 333 } else { 334 b = gr_cat(o, b, "\n NO rows committed -- plane left untouched (this run found none above threshold,\n which is NOT the same claim as 'no opportunities exist').\n" as *u8) 335 } 336 if have == 0 { 337 b = gr_cat(o, b, " (no unlinked pair reached minscore -- raise topn/lower minscore, or the graph is saturated)\n" as *u8) 338 } 339 sys_write(1, o, b) 340 return 0 341}