/* engram_cognition.c — THE ONE OPERATION. See engram_cognition.h.
 * Pure over its inputs (think/warp/express); persistence is additive/supersede
 * only. stdlib + libm + engram_store/reason/geometry. Touches no live daemon. */
#include "engram_cognition.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>

/* ── small helpers ──────────────────────────────────────────────────────────── */
static double vdot(const float* a, const float* b, int dim) {
    double s = 0; for (int i = 0; i < dim; i++) s += (double)a[i] * (double)b[i]; return s;
}
static double vnorm(const float* a, int dim) { return sqrt(vdot(a, a, dim)); }
static char* dupstr(const char* s) {
    if (!s) return NULL; size_t n = strlen(s) + 1; char* p = malloc(n);
    if (p) memcpy(p, s, n); return p;
}
static double clampd(double x, double lo, double hi){ return x<lo?lo:(x>hi?hi:x); }

/* ═══════════════════════════════════════════════ Stance lifecycle ════════════ */
int cog_stance_init(CogStance* s, const char* id, const char* faculty,
                    const char* anchor_region, const char* for_whom,
                    const GeoDescriptor* region) {
    if (!s || !region) return -1;
    memset(s, 0, sizeof *s);
    s->id = dupstr(id); s->faculty = dupstr(faculty);
    s->anchor_region = dupstr(anchor_region); s->for_whom = dupstr(for_whom);
    s->dim = region->dim;
    s->n_axes = region->n_axes > COG_MAX_AXES ? COG_MAX_AXES : region->n_axes;
    for (int k = 0; k < COG_MAX_AXES; k++) s->axis_gain[k] = 1.0;
    s->ext_floor = 1.0; s->drop_frac = 0.5; s->assoc_floor = 0.2;
    s->bias_dir = NULL;
    s->reliability = 0.5;   /* uninformed prior on our own track record */
    return 0;
}
void cog_stance_set_frozen_defaults(CogStance* s) {
    if (!s) return;
    for (int k = 0; k < COG_MAX_AXES; k++) s->axis_gain[k] = 1.0;
    s->ext_floor = 1.0; s->drop_frac = 0.5; s->assoc_floor = 0.2;
    free(s->bias_dir); s->bias_dir = NULL;
}
void cog_stance_free(CogStance* s) {
    if (!s) return;
    free(s->id); free(s->faculty); free(s->anchor_region); free(s->for_whom);
    free(s->bias_dir);
    s->id = s->faculty = s->anchor_region = s->for_whom = NULL; s->bias_dir = NULL;
}
int cog_is_keystone(const CogKeystoneSet* ks, const CogStance* s) {
    if (!s) return 0;
    if (s->keystone) return 1;
    if (!ks || !s->id) return 0;
    for (int i = 0; i < ks->n; i++)
        if (ks->ids[i] && (strcmp(ks->ids[i], s->id) == 0 ||
            (s->anchor_region && strcmp(ks->ids[i], s->anchor_region) == 0))) return 1;
    return 0;
}

/* ═══════════════════════════════════════════════ warped fit (think step 2) ══ */
int cog_warped_fit(const GeoDescriptor* g, const float* x,
                   const CogStance* st, GeoFit* out) {
    if (!g || !x || !out || g->dim <= 0 || !g->centroid) return -1;
    double ext_floor = (st && st->ext_floor > 0) ? st->ext_floor : 1.0;
    int dim = g->dim;
    double rr = 0;
    float* r = malloc((size_t)dim * sizeof(float));
    if (!r) return -1;
    for (int i = 0; i < dim; i++) { double d = (double)x[i] - (double)g->centroid[i]; r[i] = (float)d; rr += d * d; }
    double maha2 = 0, ss_in = 0;
    for (int k = 0; k < g->n_axes; k++) {
        const float* ax = g->axes[k].axis; if (!ax) continue;
        double proj = vdot(r, ax, dim);
        double gain = (st && k < st->n_axes && st->axis_gain[k] > 0) ? st->axis_gain[k] : 1.0;
        double den = g->axes[k].extent * gain; if (den < ext_floor) den = ext_floor;
        maha2 += (proj / den) * (proj / den);
        ss_in += proj * proj;
    }
    double ortho2 = rr - ss_in; if (ortho2 < 0) ortho2 = 0;
    double dist2 = maha2 + ortho2 / (ext_floor * ext_floor);
    out->mahalanobis = sqrt(maha2); out->ortho_residual = sqrt(ortho2);
    out->distance = sqrt(dist2);    out->score = 1.0 / (1.0 + dist2);
    free(r);
    return 0;
}

/* ═══════════════════════════════════════════════ think (the ONE operation) ══ */
void engram_gradient_free(GeoGradient* g) {
    if (!g) return; free(g->direction); g->direction = NULL;
}
int engram_think(const GeoDescriptor* region, const float* anchor,
                 const CogStance* stance, GeoGradient* out) {
    if (!region || !out || region->dim <= 0 || !region->centroid) return -1;
    int dim = region->dim;
    memset(out, 0, sizeof *out);
    out->dim = dim;
    const float* x = anchor ? anchor : region->centroid;   /* re-origin (step 1) */

    GeoFit f;
    if (cog_warped_fit(region, x, stance, &f) != 0) return -1;   /* fit (step 2) */

    /* step 3 — emit a GRADIENT: warped steepest DESCENT of the fit distance². */
    double ext_floor = (stance && stance->ext_floor > 0) ? stance->ext_floor : 1.0;
    float* grad = calloc((size_t)dim, sizeof(float));   /* ∇ dist² wrt x         */
    float* r    = malloc((size_t)dim * sizeof(float));
    out->direction = malloc((size_t)dim * sizeof(float));
    if (!grad || !r || !out->direction) { free(grad); free(r); free(out->direction); out->direction = NULL; return -1; }
    for (int i = 0; i < dim; i++) r[i] = (float)((double)x[i] - (double)region->centroid[i]);
    /* in-subspace: Σ_k 2 (proj/den²) a_k ; also accumulate Σ proj a_k for ortho part */
    float* proj_sum = calloc((size_t)dim, sizeof(float));
    if (!proj_sum) { free(grad); free(r); free(out->direction); out->direction = NULL; return -1; }
    for (int k = 0; k < region->n_axes; k++) {
        const float* ax = region->axes[k].axis; if (!ax) continue;
        double proj = vdot(r, ax, dim);
        double gain = (stance && k < stance->n_axes && stance->axis_gain[k] > 0) ? stance->axis_gain[k] : 1.0;
        double den = region->axes[k].extent * gain; if (den < ext_floor) den = ext_floor;
        double coef = 2.0 * proj / (den * den);
        for (int i = 0; i < dim; i++) { grad[i] += (float)(coef * ax[i]); proj_sum[i] += (float)(proj * ax[i]); }
    }
    /* orthogonal: (2 r − 2 Σ proj a_k) / ext_floor² */
    double inv_f2 = 1.0 / (ext_floor * ext_floor);
    for (int i = 0; i < dim; i++)
        grad[i] += (float)((2.0 * (double)r[i] - 2.0 * (double)proj_sum[i]) * inv_f2);
    free(proj_sum);

    /* steering = −grad (descent), seeded by the stance's bias_dir. */
    for (int i = 0; i < dim; i++) out->direction[i] = -grad[i];
    if (stance && stance->bias_dir) {
        double gn = vnorm(grad, dim), bn = vnorm(stance->bias_dir, dim);
        if (bn > 1e-12) {
            double scale = (gn > 1e-12 ? gn : 1.0);   /* seed at the gradient's scale */
            for (int i = 0; i < dim; i++)
                out->direction[i] += (float)(scale * (double)stance->bias_dir[i] / bn);
        }
    }
    double dn = vnorm(out->direction, dim);
    if (dn > 1e-12) for (int i = 0; i < dim; i++) out->direction[i] /= (float)dn;
    else            for (int i = 0; i < dim; i++) out->direction[i] = 0.0f;   /* at rest */

    out->spread     = f.distance;                       /* spiked (0) .. diffuse   */
    out->confidence = stance ? stance->reliability : 0.5;
    out->magnitude  = f.score;                          /* the read's membership   */
    out->anchor_id  = region->hub_id;                   /* borrowed vantage id     */
    out->n_support  = region->n_members;
    out->stance_id  = stance ? stance->id : NULL;
    free(grad); free(r);
    return 0;
}

/* EXPRESSION — the ONLY collapse to a point (a separate faculty from think). */
int engram_express(const GeoGradient* g, const float* anchor, float* out_point) {
    if (!g || !anchor || !out_point || !g->direction) return -1;
    double commit = clampd(g->confidence, 0.0, 1.0);    /* confident => commit far */
    for (int i = 0; i < g->dim; i++)
        out_point[i] = anchor[i] + g->direction[i] * (float)commit;
    return 0;
}

/* ═══════════════════════════════════════════════ Stance serialization ════════ */
/* Compact line schema "STNC1" (mirrors the reify "GEO1" precedent). */
char* cog_stance_to_metadata(const CogStance* s) {
    if (!s) return NULL;
    size_t cap = 256 + (size_t)s->n_axes * 24 + (size_t)(s->bias_dir ? s->dim * 16 : 0);
    char* buf = malloc(cap); if (!buf) return NULL;
    size_t o = 0;
    o += (size_t)snprintf(buf + o, cap - o, "%s\n", COG_STANCE_META_MAGIC);
    o += (size_t)snprintf(buf + o, cap - o, "f %s\n", s->faculty ? s->faculty : "-");
    o += (size_t)snprintf(buf + o, cap - o, "r %s\n", s->anchor_region ? s->anchor_region : "-");
    o += (size_t)snprintf(buf + o, cap - o, "w %s\n", s->for_whom ? s->for_whom : "-");
    o += (size_t)snprintf(buf + o, cap - o, "k %d\n", s->keystone);
    o += (size_t)snprintf(buf + o, cap - o, "d %d %d\n", s->dim, s->n_axes);
    o += (size_t)snprintf(buf + o, cap - o, "s %.9g %.9g %.9g\n", s->ext_floor, s->drop_frac, s->assoc_floor);
    o += (size_t)snprintf(buf + o, cap - o, "g");
    for (int k = 0; k < s->n_axes; k++) o += (size_t)snprintf(buf + o, cap - o, " %.9g", s->axis_gain[k]);
    o += (size_t)snprintf(buf + o, cap - o, "\n");
    o += (size_t)snprintf(buf + o, cap - o, "c %lld %.9g %.9g %.9g %.9g\n",
                          (long long)s->n_trials, s->brier_sum, s->reliability, s->ema_error, s->last_error);
    if (s->bias_dir) {
        o += (size_t)snprintf(buf + o, cap - o, "b");
        for (int i = 0; i < s->dim; i++) o += (size_t)snprintf(buf + o, cap - o, " %.9g", (double)s->bias_dir[i]);
        o += (size_t)snprintf(buf + o, cap - o, "\n");
    }
    (void)o;
    return buf;
}
int cog_stance_to_node(const CogStance* s, StoreNode* out) {
    if (!s || !out) return -1;
    memset(out, 0, sizeof *out);
    out->id        = dupstr(s->id);
    out->node_type = dupstr(COG_STANCE_NODE_TYPE);
    out->content   = dupstr(s->faculty ? s->faculty : "stance");
    out->label     = dupstr(s->faculty ? s->faculty : "stance");
    out->metadata  = cog_stance_to_metadata(s);
    out->importance = s->reliability;    /* cached denormalized readout (§2.1)     */
    out->confidence = s->reliability;
    out->temporal_decay_rate = 0.0;
    return (out->id && out->node_type && out->metadata) ? 0 : -1;
}
static int parse_floats(const char* line, double* out, int max) {
    int n = 0; const char* p = line;
    while (*p && n < max) {
        while (*p == ' ') p++;
        if (!*p) break;
        char* end; double v = strtod(p, &end);
        if (end == p) break;
        out[n++] = v; p = end;
    }
    return n;
}
int cog_stance_from_node(const StoreNode* n, CogStance* out) {
    if (!n || !out || !n->metadata) return -1;
    memset(out, 0, sizeof *out);
    for (int k = 0; k < COG_MAX_AXES; k++) out->axis_gain[k] = 1.0;
    out->ext_floor = 1.0; out->drop_frac = 0.5; out->assoc_floor = 0.2; out->reliability = 0.5;
    out->id = dupstr(n->id);
    /* verify magic on first line */
    const char* m = n->metadata;
    if (strncmp(m, COG_STANCE_META_MAGIC, strlen(COG_STANCE_META_MAGIC)) != 0) return -1;
    char* copy = dupstr(m); if (!copy) return -1;
    for (char* line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) {
        if (line[0] == '\0' || line[1] != ' ') {
            if (line[0] == 'g' || line[0] == 'b') { /* vector lines: tag then values */ }
            else continue;
        }
        char tag = line[0];
        const char* rest = line + 1; while (*rest == ' ') rest++;
        if (tag == 'f')      { free(out->faculty); out->faculty = (strcmp(rest, "-") ? dupstr(rest) : NULL); }
        else if (tag == 'r') { free(out->anchor_region); out->anchor_region = (strcmp(rest, "-") ? dupstr(rest) : NULL); }
        else if (tag == 'w') { free(out->for_whom); out->for_whom = (strcmp(rest, "-") ? dupstr(rest) : NULL); }
        else if (tag == 'k') { out->keystone = atoi(rest); }
        else if (tag == 'd') { int a=0,b=0; sscanf(rest, "%d %d", &a, &b); out->dim = a; out->n_axes = b > COG_MAX_AXES ? COG_MAX_AXES : b; }
        else if (tag == 's') { double v[3]={1,0.5,0.2}; parse_floats(rest, v, 3); out->ext_floor=v[0]; out->drop_frac=v[1]; out->assoc_floor=v[2]; }
        else if (tag == 'g') { double v[COG_MAX_AXES]; int c=parse_floats(rest, v, COG_MAX_AXES); for(int k=0;k<c;k++) out->axis_gain[k]=v[k]; }
        else if (tag == 'c') { double v[5]={0,0,0.5,0,0}; parse_floats(rest, v, 5); out->n_trials=(int64_t)v[0]; out->brier_sum=v[1]; out->reliability=v[2]; out->ema_error=v[3]; out->last_error=v[4]; }
        else if (tag == 'b') { if (out->dim>0){ out->bias_dir=calloc((size_t)out->dim,sizeof(float)); double v[4096]; int c=parse_floats(rest,v,out->dim<4096?out->dim:4096); for(int i=0;i<c;i++) out->bias_dir[i]=(float)v[i]; } }
    }
    free(copy);
    return 0;
}

/* ═══════════════════════════════════════════════ grounding as a RELATION ═════ */
static int put_edge(EngramPagedStore* s, const char* id, const char* from, const char* to,
                    const char* relation, double weight, const char* meta) {
    StoreEdge e; memset(&e, 0, sizeof e);
    e.id = (char*)id; e.from_id = (char*)from; e.to_id = (char*)to;
    e.relation = (char*)relation; e.weight = weight; e.confidence = weight;
    e.metadata = (char*)meta;
    return store_put_edge(s, &e);
}
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
                     const char* observer_id, double salience) {
    if (!s || !node_id || !observer_id) return -1;
    char id[512];
    snprintf(id, sizeof id, "st-%s-%s", node_id, observer_id);
    return put_edge(s, id, node_id, observer_id, COG_SALIENT_TO_RELATION, salience, NULL);
}
/* ═══════════════════════════════════════════════════════════════════════════
 * §7  GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR.
 * See engram_cognition.h §7 for the model and for the measurements the two
 * design decisions (thirteen regions, min aggregate) rest on.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── The one decay model. Moved here verbatim from el_runtime.c's
 * engram_temporal_decay so nodes and edges share a single implementation and a
 * single set of constants; engram_temporal_decay now delegates. Bit-identical
 * for nodes: reinforcements := activation_count, lambda_override :=
 * temporal_decay_rate.
 *
 * This is what makes decay ANALYTIC rather than sampled: between two recorded
 * versions the trajectory is not unknown, it is known in closed form from the
 * last point and elapsed time. Store the point, read the curve. */
double cog_decay_factor(int64_t age_ms, double reinforcements, double lambda_override) {
    if (age_ms <= 0) return 1.0;
    double lambda = (lambda_override > 0.0) ? lambda_override : COG_DECAY_LAMBDA;
    double age_hours = (double)age_ms / 3600000.0;
    if (reinforcements < 0) reinforcements = 0;
    double t_half = COG_T_HALF_HOURS * (1.0 + log(1.0 + reinforcements));
    double factor = exp(-lambda * age_hours / t_half);
    if (factor < COG_DECAY_FLOOR) factor = COG_DECAY_FLOOR;
    return factor;
}

const char* cog_prov_name(CogProvClass p) {
    switch (p) {
        case COG_PROV_OBSERVED:  return "observed";
        case COG_PROV_INFERRED:  return "inferred";
        case COG_PROV_TOLD:      return "told";
        case COG_PROV_IMPRINTED: return "imprinted";
        default:                 return "unset";
    }
}
CogProvClass cog_prov_parse(const char* s) {
    if (!s) return COG_PROV_UNSET;
    if (!strcmp(s, "observed"))  return COG_PROV_OBSERVED;
    if (!strcmp(s, "inferred"))  return COG_PROV_INFERRED;
    if (!strcmp(s, "told"))      return COG_PROV_TOLD;
    if (!strcmp(s, "imprinted")) return COG_PROV_IMPRINTED;
    return COG_PROV_UNSET;
}

/* Locate the GRD1 block in an edge's metadata. It is always the tail; anything
 * ahead of it is the edge's pre-existing metadata, preserved verbatim. */
static const char* cog_grd_find(const char* meta) {
    if (!meta) return NULL;
    size_t ml = strlen(COG_GROUNDING_META_MAGIC);
    if (strncmp(meta, COG_GROUNDING_META_MAGIC, ml) == 0) return meta;
    const char* p = meta;
    while ((p = strstr(p, COG_GROUNDING_META_MAGIC)) != NULL) {
        if (p > meta && p[-1] == '\n') return p;
        p += ml;
    }
    return NULL;
}

int cog_grounding_parse(const StoreEdge* e, int64_t now_ms, CogGrounding* out) {
    if (!e || !out) return -1;
    memset(out, 0, sizeof *out);

    /* Two dimensions exist on every edge whether or not grounding has ever been
     * established, because they ARE existing substrate rather than new fields:
     *   associative — the accrued hebb, with its existing dynamics;
     *   polarity    — the signed authored weight. `inhibitory` is precisely this
     *                 distinction crushed to one bit, so it is the seed sign. */
    out->associative = e->hebb;
    out->polarity    = e->inhibitory ? -e->weight : e->weight;
    out->prov        = COG_PROV_UNSET;
    out->ts          = e->last_fired > 0 ? e->last_fired : e->updated_at;

    const char* blk = cog_grd_find(e->metadata);
    if (blk) {
        out->present = 1;
        char* copy = dupstr(blk);
        if (!copy) return -1;
        for (char* line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) {
            if (line[0] == '\0') continue;
            char tag = line[0];
            const char* rest = line + 1; while (*rest == ' ') rest++;
            if (tag == 'w') {          /* the four numeric dimensions */
                double v[4] = {0,0,0,0}; parse_floats(rest, v, 4);
                out->factual = v[0]; out->relational = v[1];
                out->associative = v[2]; out->polarity = v[3];
            } else if (tag == 'k') {   /* provenance class */
                out->prov = cog_prov_parse(rest);
            } else if (tag == 't') {   /* timestamp + seq + reinforcements */
                double v[3] = {0,0,0}; parse_floats(rest, v, 3);
                out->ts = (int64_t)v[0]; out->seq = (int64_t)v[1]; out->reinforcements = v[2];
            } else if (tag == 'd') {
                double v[3] = {0,0,0}; parse_floats(rest, v, 3);
                out->fac_proj = v[0]; out->rel_proj = v[1]; out->cos_angle = v[2];
            } else if (tag == 'v') {
                snprintf(out->binding_value, sizeof out->binding_value, "%s", rest);
            } else if (tag == 'c') {
                double v[2] = {0,0}; parse_floats(rest, v, 2);
                out->floor_at_record = v[0]; out->rel_floor_at_record = v[1];
            } else if (tag == 'p') {
                snprintf(out->prev_edge, sizeof out->prev_edge, "%s", rest);
            }
        }
        free(copy);
    }
    out->agreement = (out->cos_angle > 0) ? 1 : (out->cos_angle < 0 ? -1 : 0);

    /* ── DERIVED. Nothing below this line is ever serialized. Recency, decay and
     * staleness are read off the curve; storing them is how a number ends up
     * asserting something nothing computed (§8.1 / spec §2). */
    out->age_ms = (out->ts > 0 && now_ms > out->ts) ? (now_ms - out->ts) : 0;
    out->decay  = cog_decay_factor(out->age_ms, out->reinforcements, 0.0);
    out->factual_now     = out->factual     * out->decay;
    out->relational_now  = out->relational  * out->decay;
    out->associative_now = out->associative * out->decay;
    out->stale = (out->present && out->floor_at_record > 0 &&
                  out->factual_now < out->floor_at_record) ? 1 : 0;
    return 0;
}

char* cog_grounding_metadata(const char* base_meta, const CogGrounding* g) {
    if (!g) return NULL;
    size_t keep = 0;
    if (base_meta) {
        const char* blk = cog_grd_find(base_meta);
        keep = blk ? (size_t)(blk - base_meta) : strlen(base_meta);
        while (keep > 0 && base_meta[keep - 1] == '\n') keep--;
    }
    size_t cap = keep + 1024;
    char* buf = malloc(cap); if (!buf) return NULL;
    size_t o = 0;
    if (keep) { memcpy(buf, base_meta, keep); o = keep; buf[o++] = '\n'; }
    o += (size_t)snprintf(buf + o, cap - o, "%s\n", COG_GROUNDING_META_MAGIC);
    /* STORED ONLY. factual / relational / associative / polarity / provenance /
     * timestamp — plus the joint state a decision saw. No confidence, no
     * recency, no staleness, no volatility: those are read off the curve. */
    o += (size_t)snprintf(buf + o, cap - o, "w %.9g %.9g %.9g %.9g\n",
                          g->factual, g->relational, g->associative, g->polarity);
    o += (size_t)snprintf(buf + o, cap - o, "k %s\n", cog_prov_name(g->prov));
    o += (size_t)snprintf(buf + o, cap - o, "t %lld %lld %.9g\n",
                          (long long)g->ts, (long long)g->seq, g->reinforcements);
    o += (size_t)snprintf(buf + o, cap - o, "d %.9g %.9g %.9g\n",
                          g->fac_proj, g->rel_proj, g->cos_angle);
    o += (size_t)snprintf(buf + o, cap - o, "v %s\n", g->binding_value[0] ? g->binding_value : "-");
    o += (size_t)snprintf(buf + o, cap - o, "c %.9g %.9g\n", g->floor_at_record, g->rel_floor_at_record);
    if (g->prev_edge[0]) o += (size_t)snprintf(buf + o, cap - o, "p %s\n", g->prev_edge);
    (void)o;
    return buf;
}

/* ── Consequence, not epsilon. Every test is a floor crossing or a sign change,
 * both exact. Ordered so the two INHERENT (discrete) moves are reported in
 * preference to the graded ones, because they bypass the salience gate. */
CogSignificance cog_grounding_significant(const CogGrounding* prev,
                                          const CogGrounding* now,
                                          double floor, double rel_floor) {
    if (!now) return COG_SIG_NONE;
    if (!prev || !prev->present) return COG_SIG_FIRST_RECORD;

    /* INHERENT 1 — polarity sign flip. Ignorance and disagreement are different
     * states, and support → contradiction is a change of state rather than a
     * drift, so no threshold applies. Comparing signs, with zero its own class. */
    {
        int sp = prev->polarity > 0 ? 1 : (prev->polarity < 0 ? -1 : 0);
        int sn = now->polarity  > 0 ? 1 : (now->polarity  < 0 ? -1 : 0);
        if (sp != sn) return COG_SIG_POLARITY_FLIP;
    }
    /* INHERENT 2 — provenance class change. told → observed is a categorical
     * upgrade in what the relation is entitled to, not a movement along an axis. */
    if (prev->prov != now->prov) return COG_SIG_PROVENANCE_CHANGE;

    /* Crossing an assert floor — the move changes whether this relation can be
     * spoken. Compared on the DECAYED values, because that is what the gate reads. */
    if ((prev->factual_now    >= floor)     != (now->factual_now    >= floor))     return COG_SIG_FACTUAL_FLOOR;
    if ((prev->relational_now >= rel_floor) != (now->relational_now >= rel_floor)) return COG_SIG_RELATIONAL_FLOOR;

    /* Flipping factual/relational agreement — the relation stops being "true and
     * meaningful" and becomes "true and misapplied", or the reverse. This is the
     * 911/CPS contradiction as a measured event rather than a reviewable one. */
    if (prev->agreement != now->agreement) return COG_SIG_AGREEMENT_FLIP;

    /* A gradient reversing — the evidence stopped pulling the claim toward it and
     * began pushing it away, or the same on the values axis. */
    if ((prev->fac_proj > 0) != (now->fac_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;
    if ((prev->rel_proj > 0) != (now->rel_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;

    return COG_SIG_NONE;
}

int cog_significance_inherent(CogSignificance s) {
    return (s == COG_SIG_FIRST_RECORD || s == COG_SIG_POLARITY_FLIP ||
            s == COG_SIG_PROVENANCE_CHANGE) ? 1 : 0;
}

const char* cog_significance_name(CogSignificance s) {
    switch (s) {
        case COG_SIG_FIRST_RECORD:       return "first-record";
        case COG_SIG_POLARITY_FLIP:      return "polarity-sign-flip";
        case COG_SIG_PROVENANCE_CHANGE:  return "provenance-class-change";
        case COG_SIG_FACTUAL_FLOOR:      return "factual-floor-crossed";
        case COG_SIG_RELATIONAL_FLOOR:   return "relational-floor-crossed";
        case COG_SIG_AGREEMENT_FLIP:     return "agreement-sign-flip";
        case COG_SIG_DIRECTION_REVERSAL: return "gradient-direction-reversal";
        default:                         return "none";
    }
}

/* ── Recording: a NEW edge record. The predecessor is never touched. ────────── */
int cog_grounding_record(EngramPagedStore* s, const StoreEdge* base,
                         const CogGrounding* g, char* out_id, size_t out_id_cap) {
    if (!s || !base || !base->id || !g) return -1;
    char root[192];
    snprintf(root, sizeof root, "%s", base->id);
    char* hash = strchr(root, '#'); if (hash) *hash = '\0';

    int seq = (int)g->seq + 1;
    char vid[224];
    snprintf(vid, sizeof vid, "%s#%d", root, seq);

    CogGrounding rec = *g;
    rec.seq = seq;
    snprintf(rec.prev_edge, sizeof rec.prev_edge, "%s", base->id);

    char* meta = cog_grounding_metadata(base->metadata, &rec);
    if (!meta) return -1;

    StoreEdge e; memset(&e, 0, sizeof e);
    e.id = vid; e.from_id = base->from_id; e.to_id = base->to_id;
    e.relation = base->relation; e.metadata = meta;
    /* The vector IS the weight, so the scalar fields carry their dimensions:
     * `weight` the magnitude of polarity, `inhibitory` its sign, `hebb` the
     * associative strength. Nothing here is a second copy of a derived value. */
    e.weight     = rec.polarity < 0 ? -rec.polarity : rec.polarity;
    e.inhibitory = rec.polarity < 0 ? 1 : 0;
    e.hebb       = rec.associative;
    e.confidence = base->confidence;
    e.created_at = base->created_at;
    e.updated_at = rec.ts;
    e.last_fired = rec.ts;
    e.layer_id   = base->layer_id;
    int rc = store_put_edge(s, &e);
    free(meta);
    if (rc != 0) return -1;
    if (out_id && out_id_cap) snprintf(out_id, out_id_cap, "%s", vid);
    return seq;
}

int cog_grounding_head(EngramPagedStore* s, const char* base_id,
                       StoreEdge* out, int max_versions) {
    if (!s || !base_id || !out) return -1;
    if (max_versions <= 0) max_versions = 64;
    char root[192]; snprintf(root, sizeof root, "%s", base_id);
    char* hash = strchr(root, '#'); if (hash) *hash = '\0';

    StoreEdge cur; memset(&cur, 0, sizeof cur);
    if (store_get_edge(s, root, &cur) != 1) return -1;
    int found = 0;
    for (int v = 1; v <= max_versions; v++) {
        char vid[224]; snprintf(vid, sizeof vid, "%s#%d", root, v);
        StoreEdge nx;
        if (store_get_edge(s, vid, &nx) != 1) break;
        store_edge_free(&cur); cur = nx; found = v;
    }
    *out = cur;
    return found;
}

/* ── VOLATILITY AND DRIFT: derived from the chain, stored nowhere. The series
 * exists only because nothing was destroyed, which is the whole return on
 * immutability — a derivative for free. */
int cog_grounding_trajectory(EngramPagedStore* s, const char* base_id,
                             int64_t now_ms, CogTrajectory* out) {
    if (!s || !base_id || !out) return -1;
    memset(out, 0, sizeof *out);
    char root[192]; snprintf(root, sizeof root, "%s", base_id);
    char* hash = strchr(root, '#'); if (hash) *hash = '\0';

    double pf = 0, pr = 0, f0 = 0, r0 = 0, fN = 0, rN = 0;
    double sum_df = 0, sum_dr = 0;
    int n = 0;
    for (int v = 0; v <= 64; v++) {
        char vid[224];
        if (v == 0) snprintf(vid, sizeof vid, "%s", root);
        else        snprintf(vid, sizeof vid, "%s#%d", root, v);
        StoreEdge e;
        if (store_get_edge(s, vid, &e) != 1) { if (v) break; else continue; }
        CogGrounding g;
        if (cog_grounding_parse(&e, now_ms, &g) == 0) {
            if (n == 0) { f0 = g.factual; r0 = g.relational; }
            else { sum_df += fabs(g.factual - pf); sum_dr += fabs(g.relational - pr); }
            pf = g.factual; pr = g.relational; fN = pf; rN = pr;
            n++;
        }
        store_edge_free(&e);
    }
    out->n_versions = n;
    if (n > 1) {
        out->factual_volatility    = sum_df / (double)(n - 1);
        out->relational_volatility = sum_dr / (double)(n - 1);
    }
    out->factual_drift    = fN - f0;
    out->relational_drift = rN - r0;
    /* "STAYED TRUE, BECAME WRONG" — the event the joint record makes visible and
     * that per-dimension versioning would have destroyed: the fact held while
     * the meaning degraded. Expressed as signs, so there is no tolerance here
     * either: factual did not fall, relational did. */
    out->stayed_true_became_wrong =
        (n > 1 && out->factual_drift >= 0 && out->relational_drift < 0) ? 1 : 0;
    return 0;
}

/* ── Assertion gates on BOTH floors. Traversal is untouched: activation still
 * conducts on the factual/associative side, so a relation can remain thinkable
 * while ceasing to be assertable. That gap is where the wide angles live. ──── */
int cog_assert_two_axis(EngramPagedStore* s, const char* claim_id,
                        double floor, double rel_floor, int64_t now_ms,
                        CogAssertion* out) {
    if (!s || !claim_id || !out) return -1;
    memset(out, 0, sizeof *out);
    if (!(floor > 0))     floor = 0.5;
    if (!(rel_floor > 0)) rel_floor = floor;

    /* still_held is DERIVED, not a literal (§8.1). Holding is unconditional —
     * the store gates nothing — so the question the field actually answers is
     * whether the content is present and live. */
    StoreNode n;
    if (store_get_node(s, claim_id, &n) == 1) { out->still_held = !n.tombstoned; store_node_free(&n); }
    else out->still_held = 0;

    double best = -1.0;
    for (int dir = 0; dir < 2; dir++) {
        StoreEdge* edges = NULL; size_t ne = 0;
        int rc = dir == 0 ? store_get_edges_from(s, claim_id, &edges, &ne)
                          : store_get_edges_to  (s, claim_id, &edges, &ne);
        if (rc < 0) continue;
        for (size_t i = 0; i < ne; i++) {
            if (edges[i].tombstoned) continue;
            CogGrounding g;
            if (cog_grounding_parse(&edges[i], now_ms, &g) != 0) continue;
            out->n_edges++;
            out->found = 1;
            if (g.factual_now > best) {
                best = g.factual_now;
                out->factual    = g.factual_now;
                out->relational = g.relational_now;   /* the SAME edge, not a max */
                out->polarity   = g.polarity;
                out->cos_angle  = g.cos_angle;
                out->agreement  = g.agreement;
                out->prov       = g.prov;
                out->relational_established = g.present;
                snprintf(out->best_edge, sizeof out->best_edge, "%s", edges[i].id ? edges[i].id : "");
                snprintf(out->binding_value, sizeof out->binding_value, "%s", g.binding_value);
            }
        }
        store_edges_free(edges, ne);
    }
    /* BOTH floors, and an unestablished relational axis does NOT pass by default
     * — defaulting it to passing is the exemption §0 forbids. A negative polarity
     * is a relation that actively contradicts and can never license assertion. */
    out->may_assert = (out->found && out->relational_established &&
                       out->polarity > 0 &&
                       out->factual >= floor && out->relational >= rel_floor) ? 1 : 0;
    return 0;
}


/* ═══════════════════════════════════════════════ THE CORRESPONDENCE-LOOP ═════ */
int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
                               double outcome_y, CogStance* stance,
                               int learn, double max_step, CogBeatResult* out) {
    if (!region || !stance || !out) return -1;
    memset(out, 0, sizeof *out);
    /* 2026-08-16: the keystone block is GONE. It refused to learn about the
     * reference frame, which does not make it a good reference — it makes it
     * unexaminable, trading circular calibration for an ungroundable one (spec
     * §2). Measured cost of the block: on the keystone region the beat reported
     * 0.00% brier reduction over n_trials 0 — it never ran, so nothing about the
     * self was ever calibrated OR falsifiable. What replaces it is a provenance
     * constraint, not a permission: cog_grounding_downstream refuses evidence
     * that is downstream of the region being calibrated, for every region alike.
     * `wrote_keystone` is retained as a reporting field only and is always 0. */

    GeoGradient g;
    if (engram_think(region, anchor, stance, &g) != 0) return -1;   /* PREDICTION */
    double p = g.magnitude;
    double y = clampd(outcome_y, 0.0, 1.0);
    double err = fabs(p - y);
    out->correspondence = 1.0 - err;
    out->error = err;
    out->brier = (p - y) * (p - y);

    if (learn) {
        /* refine warp: gradient descent of (p−y)² wrt each axis_gain.
         *   p = 1/(1+D²);  ∂p/∂gain_k = 2 p² proj_k² / (ext_k² gain_k³)  (>=0)
         *   ∂(err²)/∂gain_k = 2 (p−y) ∂p/∂gain_k
         * step = −lr · ∂(err²)/∂gain_k, bounded to ±max_step (metastability). */
        int dim = region->dim;
        const float* x = anchor ? anchor : region->centroid;
        float* r = malloc((size_t)dim * sizeof(float));
        if (r) {
            for (int i = 0; i < dim; i++) r[i] = (float)((double)x[i] - (double)region->centroid[i]);
            double lr = 0.5;
            double bound = (max_step > 0) ? max_step : 0.05;   /* bounded update rate */
            for (int k = 0; k < region->n_axes && k < stance->n_axes; k++) {
                const float* ax = region->axes[k].axis; if (!ax) continue;
                double proj = vdot(r, ax, dim);
                double ext = region->axes[k].extent; if (ext < 1e-9) ext = 1e-9;
                double gain = stance->axis_gain[k]; if (gain < 1e-6) gain = 1e-6;
                double dp_dgain = 2.0 * p * p * (proj * proj) / (ext * ext * gain * gain * gain);
                double dErr_dgain = 2.0 * (p - y) * dp_dgain;
                double step = -lr * dErr_dgain;
                step = clampd(step, -bound, bound);
                stance->axis_gain[k] = clampd(gain + step, 0.1, 50.0);
            }
            free(r);
        }
        /* calibration */
        stance->n_trials += 1;
        stance->brier_sum += out->brier;
        stance->last_error = err;
        stance->ema_error = (stance->n_trials == 1) ? err : 0.9 * stance->ema_error + 0.1 * err;
        double mean_brier = stance->brier_sum / (double)stance->n_trials;
        stance->reliability = clampd(1.0 - sqrt(mean_brier), 0.0, 1.0);
    }
    out->reliability = stance->reliability;
    engram_gradient_free(&g);
    return 0;
}
