M-INTEROCEPTION P3 (partial): descriptor-displacement drift-sensor primitive; self-anchor prerequisite flagged

Adds engram_geo_displacement(A, B, core_frac) — a read-only interoceptive
primitive that measures how far a neighborhood descriptor B has drifted from a
baseline A and decomposes it into GROWTH (periphery extends, core fixed) vs
CORRUPTION (the invariant core displaces). The core is the top core_frac of A's
members by centrality; per shared member (matched by id) the displacement is the
change in radial position (dist_centroid). Centroid separation (L2 + cosine) and
radius delta give the aggregate move. Pure function, no store mutation, no flag.

HONESTLY PARTIAL: a live self-drift reading needs a persisted SelfAnchor
baseline to compare "now" against, and no persisted self node / anchored
self-neighborhood exists in this store yet. The primitive takes an EXPLICIT
baseline so it is real and testable today; capturing a durable SelfAnchor and
wiring the ENGRAM_DRIFT_SENSOR live reading is a flagged follow-up. We do not
fabricate a self silently.

MEASURED on synthetic descriptors:
- GROWTH (periphery 0.50->0.90, core fixed): core_disp=0.000, periph_disp=0.400,
  centroid_sep=0.000, radius_delta=0.400.
- CORRUPTION (core 0.10->0.60, periphery fixed): core_disp=0.500,
  periph_disp=0.000, centroid_sep=0.566.
- Identity A vs A: zero drift.
The sensor discriminates cleanly (corruption core_disp >> growth core_disp).
ASan+UBSan clean.
This commit is contained in:
2026-08-12 23:44:39 -05:00
parent 0af39df16f
commit 816b258255
4 changed files with 209 additions and 0 deletions
+64
View File
@@ -521,6 +521,70 @@ void engram_geo_free(GeoDescriptor* g){
free(g);
}
/* ═══════════════════════════════════════════════════════════════════════════
* M-INTEROCEPTION P3 — DRIFT SENSOR primitive (descriptor displacement).
* Read-only. Measures how far descriptor B has drifted from a baseline A and
* decomposes it into GROWTH (periphery extends, core fixed) vs CORRUPTION (the
* invariant core displaces). The core is the top `core_frac` of A's members by
* centrality; the periphery is the rest. Per shared member (matched by id), the
* displacement is the change in its radial position (dist_centroid) between A
* and B; centroid separation + radius delta give the aggregate move.
*
* PREREQUISITE FLAGGED (honesty rail, design §2/§6): a LIVE self-drift reading
* needs a persisted SelfAnchor baseline descriptor to compare "now" against.
* That anchor does NOT exist yet — there is no persisted self node / anchored
* self-neighborhood in this store. This primitive therefore takes an EXPLICIT
* baseline so it is real and testable today; capturing a durable SelfAnchor
* snapshot and wiring the ENGRAM_DRIFT_SENSOR live reading is a follow-up. We do
* NOT fabricate a self silently.
* ═══════════════════════════════════════════════════════════════════════════ */
static double eg_geo_l2(const float* x, const float* y, int dim){
double s=0; for(int i=0;i<dim;i++){ double d=(double)x[i]-(double)y[i]; s+=d*d; } return sqrt(s);
}
static double eg_geo_cosv(const float* x, const float* y, int dim){
double dot=0,nx=0,ny=0;
for(int i=0;i<dim;i++){ dot+=(double)x[i]*y[i]; nx+=(double)x[i]*x[i]; ny+=(double)y[i]*y[i]; }
if(nx<=0.0||ny<=0.0) return 0.0;
return dot/(sqrt(nx)*sqrt(ny));
}
void engram_geo_displacement(const GeoDescriptor* a, const GeoDescriptor* b,
double core_frac, GeoDisplacement* out){
if(!out) return;
memset(out,0,sizeof(*out));
if(!a||!b) return;
if(a->centroid && b->centroid && a->dim==b->dim && a->dim>0){
out->centroid_sep = eg_geo_l2(a->centroid,b->centroid,a->dim);
out->centroid_cos = 1.0 - eg_geo_cosv(a->centroid,b->centroid,a->dim);
}
out->radius_delta = fabs(a->radius - b->radius);
if(!(core_frac>0.0 && core_frac<=1.0)) core_frac=0.3;
int na=a->n_members;
if(na<=0) return;
int* order=malloc((size_t)na*sizeof(int));
if(!order) return;
for(int i=0;i<na;i++) order[i]=i;
/* insertion sort by centrality desc (neighborhoods are small) */
for(int i=1;i<na;i++){ int k=order[i]; int j=i-1;
while(j>=0 && a->members[order[j]].centrality < a->members[k].centrality){ order[j+1]=order[j]; j--; }
order[j+1]=k; }
int ncore=(int)(core_frac*na+0.5); if(ncore<1) ncore=1; if(ncore>na) ncore=na;
double core_sum=0, periph_sum=0; int core_n=0, periph_n=0;
for(int r=0;r<na;r++){
const GeoMember* ma=&a->members[order[r]];
const GeoMember* mb=NULL;
for(int j=0;j<b->n_members;j++){
if(b->members[j].id && ma->id && strcmp(b->members[j].id,ma->id)==0){ mb=&b->members[j]; break; }
}
if(!mb) continue;
double disp=fabs(ma->dist_centroid - mb->dist_centroid);
if(r<ncore){ core_sum+=disp; core_n++; } else { periph_sum+=disp; periph_n++; }
}
free(order);
out->core_matched=core_n; out->periph_matched=periph_n;
out->core_disp = core_n ? core_sum/core_n : 0.0;
out->periph_disp = periph_n ? periph_sum/periph_n : 0.0;
}
/* ═══════════════════════════════════════════════════════════════════════════
* M10 — REIFICATION: persist / load / lookup first-class neighborhood records.
* ═══════════════════════════════════════════════════════════════════════════ */
+16
View File
@@ -158,6 +158,22 @@ GeoDescriptor* engram_geometry_descriptor(
void engram_geo_free(GeoDescriptor* g);
/* ── M-INTEROCEPTION P3: drift-sensor primitive (descriptor displacement) ────
* Read-only. GROWTH vs CORRUPTION split of how far B drifted from baseline A.
* See engram_geometry.c for the honesty note on the missing SelfAnchor. */
typedef struct {
double centroid_sep; /* L2 distance between centroids (same frame) */
double centroid_cos; /* 1 - cosine(centroidA, centroidB) */
double radius_delta; /* |radiusA - radiusB| — neighborhood scale change */
double core_disp; /* mean radial displacement of the invariant core */
double periph_disp; /* mean radial displacement of the periphery */
int core_matched; /* # core members matched by id across A,B */
int periph_matched; /* # periphery members matched by id across A,B */
} GeoDisplacement;
void engram_geo_displacement(const GeoDescriptor* a, const GeoDescriptor* b,
double core_frac, GeoDisplacement* out);
/* ═══════════════════════════════════════════════════════════════════════════
* M10 — REIFICATION: densely co-wired relational neighborhoods crystallized into
* FIRST-CLASS, PERSISTED store records (design doc §2; memory 885f5945). This is