/* ───────────────────────────────────────────────────────────────────────── * gep_core.h — Grounded Edge-Propagation, the core mechanism (task #50). * * Edge-aware, dream-coupled consolidation. Runs DURING the consolidation/dream * beat (awareness.el hebb_consolidate → engram_ground_propagate). Grounding * propagates + strengthens/decays along edges, threshold-gated by CONVERGENT * INDEPENDENT corroboration from adjacent grounded nodes. * * This header is the single source of truth for the algorithm. It is pure C * (libm only — own-the-core, no new libraries) and operates on a compact graph * view (GepGraph) that both the proof harness and the runtime native populate * from the live EngramStore (nodes/edges flat arrays + adj_from/adj_to). * * SPEC (Will, 2026-08-15; memory 9e09a59f, refines 1a861007): * - A grounding is a VECTOR + its HEBBIAN WEIGHTS — a weighted structure over * the evidential neighborhood, NOT a scalar and NOT a flat list. It APPENDS * and GROWS on SIGNIFICANT change. => grounding = an APPEND-ONLY event ring * (GepGrounding), parallel to the ACT-R base-level access_ts ring already in * EngramNode. Current standing is DERIVED, recency-weighted, never stored. * - UPDATE = LTP/LTD with a THRESHOLD (the key nonlinearity). Sub-threshold = * recorded in history but TRANSIENT (no lasting shift). Cross the threshold * of convergent support → grounding STRENGTHENS. Contradiction/erosion past * threshold → grounding DECAYS. Automatic, event-driven, salience-gated. * - DRIVER = CONVERGENT INDEPENDENT CORROBORATION (coherentism, mechanized): * when N INDEPENDENT adjacent nodes ground as likely-true around a * conjecture (Will's example: 13), its grounding grows on its own. * - INDEPENDENCE is load-bearing: N DISTINCT corroborators, not one node * echoed N times. Guards against circular self-reinforcement. * - ANTI-DELUSION GRAVITY (memory 0b15017c): support flows only FROM already- * grounded neighbors. A belief cannot ground from ungrounded speculation, * however self-consistent — nothing tethers it to the grounded core. * - NOTHING IS SETTLED (memory 271f1163): grounded is strongly-held, still * falsifiable. Decay path stays open on every node; history is append-only, * supersede-not-delete. * ───────────────────────────────────────────────────────────────────────── */ #ifndef GEP_CORE_H #define GEP_CORE_H #include #include #include /* ── Constants ────────────────────────────────────────────────────────────── * GEP_DECAY_D matches ENGRAM_BLL_D (0.5, canonical ACT-R): the derived standing * is recency-weighted over the grounding-event collection exactly as the * base-level term is recency-weighted over the access ring (memory 1a861007: * "structurally the ACT-R base-level pattern, a sum over time-stamped events"). */ #define GEP_DECAY_D 0.5 /* ACT-R power-law recency exponent */ #define GEP_BASE 0.10 /* standing floor of a bare conjecture */ #define GEP_LIKELY_MIN 0.34 /* band: conjecture < LIKELY ≤ likely */ #define GEP_GROUNDED_MIN 0.66 /* band: likely < GROUNDED ≤ grounded */ #define GEP_N_MIN 3 /* min INDEPENDENT corroborators to cross */ #define GEP_THETA 0.30 /* min convergent-support MASS to cross */ #define GEP_MAG_GAIN 1.0 /* net-support → event-magnitude gain (tanh) */ #define GEP_EVENT_RING 32 /* grounding-history depth kept exactly */ /* A single grounding event — one contact with the evidential neighborhood. * Append-only; the ring is the collection-over-time, the standing is derived. */ typedef struct { int64_t ts; /* wall-clock ms of the grounding event */ int8_t sign; /* +1 = LTP (strengthen), -1 = LTD (decay) */ double mag; /* magnitude in (0,1], = tanh(gain·|net independent support|)*/ uint64_t sig; /* signature of the independent corroborator set (audit) */ } GepEvent; /* The grounding of one node: an append-only ring of events + transient counters. * older_count keeps the tail (events aged out of the ring) so the collection is * never silently lost — supersede-not-delete. subthreshold_hits records beats * where support was present but did NOT cross threshold (transient, no shift). */ typedef struct { GepEvent ev[GEP_EVENT_RING]; int head; /* next write slot */ int filled; /* valid entries (≤ GEP_EVENT_RING) */ int64_t older_count; /* durable events aged past the ring */ int subthreshold_hits; /* transient sub-threshold beats, no shift */ } GepGrounding; typedef struct { const char* id; GepGrounding gr; int is_belief; /* 1 = subject to propagation (conjecture/belief) */ } GepNode; /* An edge carries a HEBBIAN WEIGHT (EngramEdge.weight) and a polarity derived * from its relation: supportive (supports/corroborates/derived-from/hebbian- * associate) = +1, contradictory (contradicts/refutes) = -1. */ typedef struct { int from; /* node index */ int to; /* node index */ double weight; /* Hebbian edge weight, [0,1] */ int8_t polarity; /* +1 supportive, -1 contradictory */ } GepEdge; typedef struct { GepNode* nodes; int n_nodes; GepEdge* edges; int n_edges; } GepGraph; typedef struct { int strengthened; /* beliefs that took an LTP event this beat */ int decayed; /* beliefs that took an LTD event this beat */ int subthreshold; /* beliefs with support present but below threshold */ int graduations; /* band-up transitions (conjecture→likely→grounded) */ int demotions; /* band-down transitions */ int isolated; /* belief nodes with ZERO incident edges (sparse graph) */ int starved; /* belief nodes with edges but NO grounded corroborator */ } GepBeatStats; /* Real-graph note (live measurement 2026-08-15): 70.7% of nodes are isolated, * connected core ~28%. Grounded edge-propagation is definitionally scoped to * the connected core — a belief with no grounded neighbor has nothing to * tether to (anti-delusion gravity). isolated/starved are surfaced as an * interoceptive signal for the edge-formation / embedding pass (#20) to try to * connect them; #50 CONSUMES edges, it does not form them. */ /* ── Standing derivation: collection → scalar, recency-weighted ───────────── * standing = clamp( GEP_BASE + Σ_events sign·mag·age^(-D) , 0, 1 ). * Exactly the ACT-R base-level shape (Σ t^-d) but sign-carrying so LTD subtracts. * The value is a pure function of wall-clock time — idempotent, never stored. */ static inline double gep_standing(const GepGrounding* g, int64_t now_ms) { double raw = 0.0; for (int i = 0; i < g->filled; i++) { double age = (double)(now_ms - g->ev[i].ts) / 1000.0; if (age < 1.0) age = 1.0; /* clock-skew / same-beat → 1s */ raw += (double)g->ev[i].sign * g->ev[i].mag * pow(age, -GEP_DECAY_D); } double s = GEP_BASE + raw; if (s < 0.0) s = 0.0; if (s > 1.0) s = 1.0; return s; } /* Band label from a standing value. */ static inline const char* gep_band(double standing) { if (standing >= GEP_GROUNDED_MIN) return "grounded"; if (standing >= GEP_LIKELY_MIN) return "likely"; return "conjecture"; } static inline int gep_band_rank(double standing) { if (standing >= GEP_GROUNDED_MIN) return 2; if (standing >= GEP_LIKELY_MIN) return 1; return 0; } /* Append one grounding event to the ring (append-only; oldest slot recycles, * its loss counted in older_count so the collection's depth is never faked). */ static inline void gep_append(GepGrounding* g, int64_t ts, int8_t sign, double mag, uint64_t sig) { if (g->filled >= GEP_EVENT_RING) g->older_count++; g->ev[g->head].ts = ts; g->ev[g->head].sign = sign; g->ev[g->head].mag = mag; g->ev[g->head].sig = sig; g->head = (g->head + 1) % GEP_EVENT_RING; if (g->filled < GEP_EVENT_RING) g->filled++; } /* ── Independence via union-find over corroborators ───────────────────────── * Two corroborators are the SAME independent source if they are the same node, * or if a direct edge links them (mutually-derived / echoed through a chain). * Counting DISTINCT components — not raw corroborator count — is the guard * against one node echoed N times reading as N independent corroborations. */ #define GEP_MAX_CORR 256 typedef struct { int node_idx[GEP_MAX_CORR]; /* corroborator node index */ double contrib[GEP_MAX_CORR]; /* weight·standing(c) */ int parent[GEP_MAX_CORR]; /* union-find parent */ int n; } GepCorrSet; static int gep_uf_find(GepCorrSet* s, int x) { while (s->parent[x] != x) { s->parent[x] = s->parent[s->parent[x]]; x = s->parent[x]; } return x; } static void gep_uf_union(GepCorrSet* s, int a, int b) { int ra = gep_uf_find(s, a), rb = gep_uf_find(s, b); if (ra != rb) s->parent[ra] = rb; } /* index of node_idx within the corroborator set, or -1 */ static int gep_corr_index_of(const GepCorrSet* s, int node_idx) { for (int i = 0; i < s->n; i++) if (s->node_idx[i] == node_idx) return i; return -1; } /* ── The beat: grounded edge-propagation over one belief node ─────────────── * Returns +1 if an LTP event was appended, -1 if LTD, 0 if sub-threshold/none. * out_pos/out_neg/out_np/out_nn expose the raw support decomposition for the * proof ledger (mass and independent-component counts on each polarity). */ static int gep_propagate_node(GepGraph* g, int b, int64_t now_ms, double* out_pos, double* out_neg, int* out_np, int* out_nn, int* out_incident) { GepCorrSet cs; cs.n = 0; int incident = 0; /* any edge touching b at all — isolation detector */ /* 1. Gather corroborators along incident edges. Anti-delusion gravity: * only ALREADY-grounded neighbors (standing ≥ LIKELY_MIN) may corroborate. * Each contributes weight·standing; polarity kept via signed contrib. * 1-HOP ONLY — no BFS fan-out, so no per-hop breadth explosion. The * corroborator working set is hard-capped at GEP_MAX_CORR (beam bound * against hub belief nodes with thousands of incident edges). */ for (int e = 0; e < g->n_edges; e++) { int c = -1; int8_t pol = 0; if (g->edges[e].from == b) { c = g->edges[e].to; pol = g->edges[e].polarity; } else if (g->edges[e].to == b) { c = g->edges[e].from; pol = g->edges[e].polarity; } else continue; incident++; if (c < 0 || c == b) continue; double cs_standing = gep_standing(&g->nodes[c].gr, now_ms); if (cs_standing < GEP_LIKELY_MIN) continue; /* ungrounded ⇒ no pull */ double contribution = g->edges[e].weight * cs_standing * (double)pol; int existing = gep_corr_index_of(&cs, c); if (existing >= 0) { /* same corroborator id reached twice (multi-edge echo): keep the * strongest-magnitude contribution, do NOT add — one source, one vote */ if (fabs(contribution) > fabs(cs.contrib[existing])) cs.contrib[existing] = contribution; } else if (cs.n < GEP_MAX_CORR) { /* beam bound against hub belief nodes */ cs.node_idx[cs.n] = c; cs.contrib[cs.n] = contribution; cs.parent[cs.n] = cs.n; cs.n++; } } if (out_incident) *out_incident = incident; /* 2. Collapse mutually-derived corroborators (an edge between two of them = * echo chain / shared derivation) into one independent component. */ for (int e = 0; e < g->n_edges; e++) { int ia = gep_corr_index_of(&cs, g->edges[e].from); int ib = gep_corr_index_of(&cs, g->edges[e].to); if (ia >= 0 && ib >= 0) gep_uf_union(&cs, ia, ib); } /* 3. Per independent component, take the MAX-magnitude member (echoes don't * inflate mass either), split by polarity. Convergent INDEPENDENT support * = sum over components; independence count = number of components. */ double comp_best[GEP_MAX_CORR]; int comp_root[GEP_MAX_CORR]; int n_comp = 0; for (int i = 0; i < cs.n; i++) { int r = gep_uf_find(&cs, i); int slot = -1; for (int k = 0; k < n_comp; k++) if (comp_root[k] == r) { slot = k; break; } if (slot < 0) { slot = n_comp++; comp_root[slot] = r; comp_best[slot] = cs.contrib[i]; } else if (fabs(cs.contrib[i]) > fabs(comp_best[slot])) comp_best[slot] = cs.contrib[i]; } double pos = 0.0, neg = 0.0; int np = 0, nn = 0; uint64_t sig = 1469598103934665603ULL; /* FNV offset — signature of the set */ for (int k = 0; k < n_comp; k++) { if (comp_best[k] > 0.0) { pos += comp_best[k]; np++; } else if (comp_best[k] < 0.0) { neg += -comp_best[k]; nn++; } sig = (sig ^ (uint64_t)comp_root[k]) * 1099511628211ULL; } if (out_pos) *out_pos = pos; if (out_neg) *out_neg = neg; if (out_np) *out_np = np; if (out_nn) *out_nn = nn; double net = pos - neg; /* 4. Threshold gate. Convergent independent corroboration must clear BOTH a * MASS threshold (THETA) and an INDEPENDENCE-count threshold (N_MIN). * The count gate is the independence guard: echoed support collapses to * one component and never reaches N_MIN however large the raw fan-in. */ if (net > 0.0 && pos >= GEP_THETA && np >= GEP_N_MIN) { double mag = tanh(GEP_MAG_GAIN * net); gep_append(&g->nodes[b].gr, now_ms, +1, mag, sig); return +1; } if (net < 0.0 && neg >= GEP_THETA && nn >= GEP_N_MIN) { double mag = tanh(GEP_MAG_GAIN * (-net)); gep_append(&g->nodes[b].gr, now_ms, -1, mag, sig); return -1; } /* Sub-threshold: support seen but did not cross. Recorded, transient, no * lasting shift — exactly Will's "recorded in history but transient". */ if (np > 0 || nn > 0) g->nodes[b].gr.subthreshold_hits++; return 0; } /* Run one consolidation/dream beat over every belief node in the graph. */ static inline GepBeatStats gep_beat(GepGraph* g, int64_t now_ms) { GepBeatStats st; memset(&st, 0, sizeof st); for (int b = 0; b < g->n_nodes; b++) { if (!g->nodes[b].is_belief) continue; int before = gep_band_rank(gep_standing(&g->nodes[b].gr, now_ms)); double pos, neg; int np, nn, incident; int r = gep_propagate_node(g, b, now_ms, &pos, &neg, &np, &nn, &incident); int after = gep_band_rank(gep_standing(&g->nodes[b].gr, now_ms)); if (r > 0) st.strengthened++; else if (r < 0) st.decayed++; else if (np > 0 || nn > 0) st.subthreshold++; else if (incident == 0) st.isolated++; /* sparse-graph reality */ else st.starved++; /* has edges, no grounded neighbor */ if (after > before) st.graduations++; if (after < before) st.demotions++; } return st; } #endif /* GEP_CORE_H */