/* ─────────────────────────────────────────────────────────────────────────
 * engram_ground_propagate.staged.c — GATED runtime native for task #50.
 *
 * STAGED, NOT COMPILED INTO THE LIVE BINARY. This mirrors the
 * geometric_retrieve.staged.c staging pattern (memory 1cc231ec): it references
 * runtime-internal types (EngramStore, EngramNode, EngramEdge, engram_global,
 * engram_now_ms, the adj cache) and therefore compiles ONLY when spliced into
 * lang/releases/v1.0.0-20260501/el_runtime.c. Splice + promotion is sequenced
 * by the main loop AFTER the engine+HNSW cutover settles — do NOT hand-apply.
 *
 * It is the production form of the mechanism proven in gep_proof.c: the SAME
 * gep_core.h primitives (GepGrounding ring, gep_standing, gep_append,
 * union-find independence), wired directly to the live flat node/edge arrays.
 *
 * ── SPLICE PLAN (three additive edits to el_runtime.c; nothing removed) ──────
 *
 *  [1] EngramNode struct (~line 6061, after hebb_elig_ts): add the grounding
 *      collection. Additive; zero-initialized by the existing calloc/memset
 *      paths, so legacy snapshots degrade gracefully to an empty history.
 *
 *          GepGrounding grounding;   // task #50 — append-only grounding ring
 *
 *  [2] #include "gep_core.h" near the other engram includes, and paste the
 *      body of this file below the Hebbian section (after engram_hebb_drain_json).
 *
 *  [3] Persistence (engram_save node JSON ~7934 / engram_load parser ~8186):
 *      serialize the grounding ring as a compact "grounding" array of
 *      [ts,sign,mag] triples + subthreshold_hits so standing survives a
 *      round-trip. Helpers gep_grounding_to_json / gep_grounding_parse below.
 *      Until wired, grounding is in-RAM only (like the Hebbian eligibility
 *      trace) — correct for a first gated rollout, but standing resets on boot.
 *
 *  [4] EL surface: declare engram_ground_propagate in el_runtime.h + el_seed.c,
 *      add route_ground_propagate to engram/src/server.el, called from the
 *      awareness.el consolidation beat (see awareness.beat.patch.el).
 * ───────────────────────────────────────────────────────────────────────── */

#include "gep_core.h"

/* Relation → evidential polarity. Supportive relations transmit grounding
 * gravity (+1); contradictory relations erode it (-1); everything else is a
 * NON-evidential edge (structural / navigational) and is ignored (0) — an
 * association is not a corroboration. Extend deliberately; a mis-classified
 * relation is a false corroboration. */
static int8_t gep_relation_polarity(const char* rel) {
    if (!rel) return 0;
    if (!strcmp(rel, "supports")     || !strcmp(rel, "corroborates") ||
        !strcmp(rel, "derived-from") || !strcmp(rel, "hebbian-associate") ||
        !strcmp(rel, "grounds")      || !strcmp(rel, "confirms")) return +1;
    if (!strcmp(rel, "contradicts")  || !strcmp(rel, "refutes") ||
        !strcmp(rel, "negates")      || !strcmp(rel, "conflicts-with")) return -1;
    return 0;
}

/* Which nodes are BELIEFS/CONJECTURES subject to grounding propagation. Facts
 * imported as knowledge are already grounded by provenance; identity/safety
 * layers are never re-graded here. Gate on node_type + the conjecture tag. */
static int gep_is_belief(const EngramNode* n) {
    if (!n || !n->node_type) return 0;
    if (n->layer_id == ENGRAM_LAYER_SAFETY) return 0;   /* never re-grade safety */
    return  !strcmp(n->node_type, "Memory")     ||
            !strcmp(n->node_type, "Conjecture") ||
            !strcmp(n->node_type, "Hypothesis") ||
            !strcmp(n->node_type, "Belief")     ||
            (n->tags && istr_contains(n->tags, "conjecture"));
}

/* Grounding standing of an engram node, derived from its collection. This is
 * the value the verifier (#43) and realizer (calibrated assertion, 0041d917)
 * read — and it is written back into epistemic_confidence-equivalent surfaces
 * so "never speak above the grounding" is enforced from one source of truth. */
double engram_grounding_standing(const EngramNode* n, int64_t now_ms) {
    return gep_standing(&n->grounding, now_ms);
}

/* ── The beat: one pass of grounded edge-propagation over the whole store ────
 * Called from the consolidation/dream heartbeat. 1-hop, beam-capped, salience-
 * ordered so a bounded slice of the highest-salience beliefs is processed per
 * beat (the rest next beat) — never a full-graph blow-up on a 12k-node store.
 * Returns JSON telemetry for the heartbeat stream. */
#define GEP_BELIEFS_PER_BEAT 512   /* bound work per beat; salience-prioritized */

el_val_t engram_ground_propagate(void) {
    EngramStore* g = engram_get();
    int64_t now = engram_now_ms();
    engram_adj_rebuild(g);   /* ensure adj_from/adj_to are current */

    int strengthened = 0, decayed = 0, subthreshold = 0;
    int graduations = 0, demotions = 0, isolated = 0, starved = 0, processed = 0;

    for (int64_t bi = 0; bi < g->node_count && processed < GEP_BELIEFS_PER_BEAT; bi++) {
        EngramNode* b = &g->nodes[bi];
        if (!gep_is_belief(b)) continue;
        processed++;

        int before = gep_band_rank(gep_standing(&b->grounding, now));

        /* Gather independent corroborators over incident edges (both directions),
         * anti-delusion gated (neighbor must already be ≥ LIKELY_MIN). */
        GepCorrSet cs; cs.n = 0; int incident = 0;
        int* out = g->adj_from[bi]; int out_n = g->adj_from_len[bi];
        int* in  = g->adj_to[bi];   int in_n  = g->adj_to_len[bi];
        for (int pass = 0; pass < 2; pass++) {
            int* lst = pass ? in : out; int ln = pass ? in_n : out_n;
            for (int k = 0; k < ln; k++) {
                EngramEdge* e = &g->edges[lst[k]];
                int8_t pol = gep_relation_polarity(e->relation);
                if (pol == 0) continue;
                incident++;
                const char* cid = pass ? e->from_id : e->to_id;
                int64_t ci = engram_find_node_index(cid);
                if (ci < 0 || ci == bi) continue;
                double cstand = gep_standing(&g->nodes[ci].grounding, now);
                if (cstand < GEP_LIKELY_MIN) continue;            /* no tether */
                double contrib = e->weight * cstand * (double)pol;
                int ex = -1;
                for (int q = 0; q < cs.n; q++) if (cs.node_idx[q] == (int)ci) { ex = q; break; }
                if (ex >= 0) { if (fabs(contrib) > fabs(cs.contrib[ex])) cs.contrib[ex] = contrib; }
                else if (cs.n < GEP_MAX_CORR) {
                    cs.node_idx[cs.n] = (int)ci; cs.contrib[cs.n] = contrib;
                    cs.parent[cs.n] = cs.n; cs.n++;
                }
            }
        }

        /* Collapse mutually-derived corroborators (an edge between two of them)
         * into one independent component — the independence guard. */
        for (int x = 0; x < cs.n; x++) {
            int64_t nx = cs.node_idx[x];
            int* xout = g->adj_from[nx]; int xn = g->adj_from_len[nx];
            for (int k = 0; k < xn; k++) {
                const char* tid = g->edges[xout[k]].to_id;
                int64_t ti = engram_find_node_index(tid);
                for (int y = 0; y < cs.n; y++)
                    if (cs.node_idx[y] == (int)ti) { gep_uf_union(&cs, x, y); break; }
            }
        }

        /* Per-component max-magnitude, split by polarity → convergent independent
         * support mass + independence count. */
        double comp_best[GEP_MAX_CORR]; int comp_root[GEP_MAX_CORR], ncomp = 0;
        for (int i = 0; i < cs.n; i++) {
            int r = gep_uf_find(&cs, i), slot = -1;
            for (int kk = 0; kk < ncomp; kk++) if (comp_root[kk] == r) { slot = kk; break; }
            if (slot < 0) { slot = ncomp++; 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, neg = 0; int np = 0, nn = 0; uint64_t sig = 1469598103934665603ULL;
        for (int k = 0; k < ncomp; k++) {
            if (comp_best[k] > 0) { pos += comp_best[k]; np++; }
            else if (comp_best[k] < 0) { neg += -comp_best[k]; nn++; }
            sig = (sig ^ (uint64_t)comp_root[k]) * 1099511628211ULL;
        }

        double net = pos - neg;
        if (net > 0 && pos >= GEP_THETA && np >= GEP_N_MIN) {
            gep_append(&b->grounding, now, +1, tanh(GEP_MAG_GAIN * net), sig);
            strengthened++;
        } else if (net < 0 && neg >= GEP_THETA && nn >= GEP_N_MIN) {
            gep_append(&b->grounding, now, -1, tanh(GEP_MAG_GAIN * (-net)), sig);
            decayed++;
        } else if (np > 0 || nn > 0) {
            b->grounding.subthreshold_hits++; subthreshold++;
        } else if (incident == 0) { isolated++; }
        else { starved++; }

        /* Mirror the derived standing onto confidence so downstream reads
         * (activate epistemic_confidence, realizer calibration) never exceed the
         * grounding. Faithful representation, single source of truth. */
        double stand = gep_standing(&b->grounding, now);
        b->confidence = stand;
        b->updated_at = now;

        int after = gep_band_rank(stand);
        if (after > before) graduations++;
        if (after < before) demotions++;
    }

    /* Heartbeat telemetry — the gep_* line, sibling to the hebb_* gauges. */
    char buf[512];
    snprintf(buf, sizeof buf,
        "{\"gep_processed\":%d,\"gep_strengthened\":%d,\"gep_decayed\":%d,"
        "\"gep_subthreshold\":%d,\"gep_graduations\":%d,\"gep_demotions\":%d,"
        "\"gep_isolated\":%d,\"gep_starved\":%d}",
        processed, strengthened, decayed, subthreshold,
        graduations, demotions, isolated, starved);
    return EL_STR(el_strdup(buf));
}
