Files
el/lang/runtime/engram_cognition.c
T
Neuron 7a1501d097
El SDK CI - dev / build-and-test (pull_request) Failing after 3m59s
Grounding is the edge's weight, and the weight is a vector
A relation that keeps holding up strengthens; one that stops corresponding
decays. That is not analogous to grounding, it IS grounding — so it belongs on
the edge, not in a subsystem beside it. The graph was already the grounding
structure; this stops modelling it as something else.

Deleted, not refactored:
  - cog_ground_edge and the `grounded-by` relation type. A grounded-by edge
    models grounding as a relation BETWEEN nodes when it is a property OF a
    relation. #147 fixed which endpoints that edge landed on and left the wrong
    idea intact. Measured on the live store: the old path scored two nodes with
    ZERO edges between them at 0.925237 and wrote an edge for it.
  - ground() writing. It was a read that wrote — the eg_vindex_sync defect.
    Three identical calls produced three writes to the same edge id.
  - keystone_write_blocked. Its measured cost was 0.00% brier reduction over
    n_trials 0 on the keystone: the loop never ran, so the self was never
    calibrated and never falsifiable. Nothing replaces it — non-circularity of
    the reference frame is temporal, not a permission.
  - a graph predicate for "evidence downstream of itself", built and then
    withdrawn. Reachability from the self region covers 89.2% of the live graph
    (10,580 of 11,861 nodes), so any topological predicate marks nearly all
    evidence tainted and degenerates into the total block censorship began as.

The vector, carried in a GRD1 block on the edge's own metadata:
factual, relational, associative (the existing hebb), polarity (SIGNED — near
zero is "no support", negative is "actively contradicts"; `inhibitory` is that
distinction crushed to one bit), provenance class, and a timestamp. Confidence,
recency, staleness and volatility are DERIVED at read and never serialized.

Decay is one model, not two: cog_decay_factor is the single implementation and
engram_temporal_decay now delegates to it — proven bit-identical over 24
(age, reinforcement) points.

Values reference: thirteen regions, aggregate MIN, binding value named. Measured
— the 13 have pairwise centroid cosine min 0.1525 / mean 0.5199 / max 0.9278, so
they demonstrably are not one region, and a mean would let agreement with twelve
mask a violation of the thirteenth.

Supersession versions the whole vector jointly, gated by consequence and
salience with no epsilon anywhere: floor crossings and sign changes only.
Polarity flips and provenance-class changes are inherently significant and
bypass the salience gate.

Also fixed: the frame contract. Descriptors are built over L2-normalized member
embeddings; think() and the grounding path were fitting RAW vectors against them.
Measured on the self region, same data, same 106 members:
  magnitude 0.00283443 -> 0.536134, spread 18.7565 -> 0.930163.
Every fit score sat three decimal places below the 0.5 floors that gate on them.

assert() gates on both floors and computes still_held instead of returning a
hardcoded `true` — the old build reported still_held for a node that does not
exist.
2026-08-16 13:18:50 -05:00

683 lines
35 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 (py)² wrt each axis_gain.
* p = 1/(1+D²); ∂p/∂gain_k = 2 p² proj_k² / (ext_k² gain_k³) (>=0)
* ∂(err²)/∂gain_k = 2 (py) ∂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;
}