5f3ddb8b8d
El SDK CI - dev / build-and-test (pull_request) Failing after 14m31s
LTP/LTD-style belief grounding propagated along graph edges, with union-find independence-guarded corroboration. Package: core C algorithm (gep_core.h), a self-contained deterministic proof harness with recorded output, staged runtime integration, and gated .el patches for the beat hook and HTTP route. Per the author's own LEDGER.md: built + proven on a clone, GATED pending the engine/HNSW cutover — not wired into the live beat or routes. Preserved here as a spec/reference artifact, not a request to merge into the live path.
300 lines
16 KiB
C
300 lines
16 KiB
C
/* ─────────────────────────────────────────────────────────────────────────
|
|
* 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 <stdint.h>
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
/* ── 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 */
|