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
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# M-INTEROCEPTION P3 gate: drift-sensor primitive engram_geo_displacement.
# Read-only pure primitive; no store, no flag. Throwaway /tmp only.
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
RT="$HERE/../../lang/runtime/el_runtime.c"
ST="$HERE/../../lang/runtime/engram_store.c"
GEO="$HERE/../../lang/runtime/engram_geometry.c"
VIDX="$HERE/../../lang/runtime/engram_vindex.c"
INC="$HERE/../../lang/runtime"
WORK="$(mktemp -d /tmp/engram-p3-XXXXXX)"
export HOME="$WORK/home"; mkdir -p "$HOME"
fail=0
echo "== compile =="
gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
-lcurl -lm -o "$WORK/p3" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; }
"$WORK/p3" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; }
cat "$WORK/out.txt" | sed 's/^/ /'
echo
echo "== assertions =="
python3 - "$WORK/out.txt" <<'PY'
import sys,re
rows={}
for line in open(sys.argv[1]):
m=re.match(r'(\w+) (.*)',line.strip())
if not m: continue
tag=m.group(1); kv=dict(re.findall(r'(\w+)=([-\d.]+)',m.group(2)))
rows[tag]={k:float(v) for k,v in kv.items()}
rc=0
def check(c,msg):
global rc; print((" PASS: " if c else " FAIL: ")+msg)
if not c: rc=1
g=rows["GROWTH"]; c=rows["CORRUPTION"]; i=rows["IDENTITY"]
check(g["core_disp"]<0.05, f"GROWTH: core displacement ~0 (core fixed) = {g['core_disp']}")
check(g["periph_disp"]>0.30, f"GROWTH: periphery extended = {g['periph_disp']}")
check(g["centroid_sep"]<1e-6, f"GROWTH: centroid unmoved = {g['centroid_sep']}")
check(abs(g["radius_delta"]-0.4)<1e-4, f"GROWTH: radius grew by ~0.4 = {g['radius_delta']}")
check(c["core_disp"]>0.40, f"CORRUPTION: core displaced strongly = {c['core_disp']}")
check(c["periph_disp"]<0.05, f"CORRUPTION: periphery fixed = {c['periph_disp']}")
check(c["centroid_sep"]>0.1, f"CORRUPTION: centroid moved = {c['centroid_sep']}")
check(c["core_disp"] > 8*g["core_disp"]+0.3,
f"SENSOR DISCRIMINATES: corruption core_disp ({c['core_disp']}) >> growth core_disp ({g['core_disp']})")
check(i["core_disp"]==0 and i["periph_disp"]==0 and i["centroid_sep"]<1e-6,
"IDENTITY: A vs A -> zero drift")
sys.exit(rc)
PY
[ $? -ne 0 ] && fail=1
echo
echo "== ASan+UBSan =="
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
-I "$INC" "$HERE/test_interoception_p3_drift.c" "$RT" "$ST" "$GEO" "$VIDX" \
-lcurl -lm -o "$WORK/p3.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; }
if [ -x "$WORK/p3.san" ]; then
export ASAN_OPTIONS=detect_leaks=0
"$WORK/p3.san" >/dev/null 2>"$WORK/san.log"
if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san.log"; then
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san.log" | head; fail=1
else echo " ok: ASan+UBSan clean"; fi
fi
echo
if [ "$fail" -eq 0 ]; then echo "====== P3 DRIFT-SENSOR GATE: PASS ======"; else echo "====== P3 DRIFT-SENSOR GATE: FAIL ======"; fi
rm -rf "$WORK"
exit $fail
+61
View File
@@ -0,0 +1,61 @@
/* test_interoception_p3_drift.c — M-INTEROCEPTION Priority 3 (PARTIAL).
* Drift-sensor primitive engram_geo_displacement: GROWTH vs CORRUPTION split.
*
* Constructs synthetic GeoDescriptors (the struct is public) — a baseline and
* two perturbations — and checks the sensor reports LOW core-displacement for a
* periphery-only change (growth) and HIGH core-displacement for a core change
* (corruption). No store / embeddings needed: this exercises the primitive in
* isolation, which is the honest scope given there is no persisted SelfAnchor
* yet (see engram_geometry.c). */
#include "engram_geometry.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static GeoMember MK(const char* id, double centrality, double dist){
GeoMember m; memset(&m,0,sizeof m);
m.id=strdup(id); m.centrality=centrality; m.dist_centroid=dist;
m.membership=1.0; m.embedded=1; return m;
}
/* 4 members: 2 core (high centrality), 2 periphery (low). */
static GeoDescriptor* mkdesc(float cx,float cy,double radius,
double c1,double c2,double p1,double p2){
GeoDescriptor* g=calloc(1,sizeof *g);
g->dim=4;
g->centroid=calloc(4,sizeof(float));
g->centroid[0]=cx; g->centroid[1]=cy;
g->radius=radius;
g->n_members=4;
g->members=calloc(4,sizeof(GeoMember));
g->members[0]=MK("core1",10.0,c1);
g->members[1]=MK("core2", 9.0,c2);
g->members[2]=MK("per1", 1.0,p1);
g->members[3]=MK("per2", 0.9,p2);
return g;
}
int main(void){
GeoDisplacement d;
/* baseline: core at 0.10, periphery at 0.50, centroid [1,0], radius 1.0 */
GeoDescriptor* A = mkdesc(1.0f,0.0f,1.0, 0.10,0.10, 0.50,0.50);
/* (i) GROWTH: periphery extends 0.50->0.90; core fixed; radius grows. */
GeoDescriptor* G = mkdesc(1.0f,0.0f,1.4, 0.10,0.10, 0.90,0.90);
engram_geo_displacement(A,G,0.5,&d);
printf("GROWTH centroid_sep=%.4f centroid_cos=%.4f radius_delta=%.4f core_disp=%.4f periph_disp=%.4f core_n=%d periph_n=%d\n",
d.centroid_sep,d.centroid_cos,d.radius_delta,d.core_disp,d.periph_disp,d.core_matched,d.periph_matched);
/* (ii) CORRUPTION: core displaces 0.10->0.60; periphery fixed; centroid shifts. */
GeoDescriptor* C = mkdesc(0.6f,0.4f,1.0, 0.60,0.60, 0.50,0.50);
engram_geo_displacement(A,C,0.5,&d);
printf("CORRUPTION centroid_sep=%.4f centroid_cos=%.4f radius_delta=%.4f core_disp=%.4f periph_disp=%.4f core_n=%d periph_n=%d\n",
d.centroid_sep,d.centroid_cos,d.radius_delta,d.core_disp,d.periph_disp,d.core_matched,d.periph_matched);
/* identity: A vs A -> zero drift */
engram_geo_displacement(A,A,0.5,&d);
printf("IDENTITY centroid_sep=%.4f core_disp=%.4f periph_disp=%.4f\n",
d.centroid_sep,d.core_disp,d.periph_disp);
engram_geo_free(A); engram_geo_free(G); engram_geo_free(C);
return 0;
}
+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