From 816b2582555c4b501b08c199bfb48fc44d9e53bd Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 12 Aug 2026 23:44:39 -0500 Subject: [PATCH] M-INTEROCEPTION P3 (partial): descriptor-displacement drift-sensor primitive; self-anchor prerequisite flagged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- engram/test/run_interoception_p3.sh | 68 +++++++++++++++++++++++ engram/test/test_interoception_p3_drift.c | 61 ++++++++++++++++++++ lang/runtime/engram_geometry.c | 64 +++++++++++++++++++++ lang/runtime/engram_geometry.h | 16 ++++++ 4 files changed, 209 insertions(+) create mode 100755 engram/test/run_interoception_p3.sh create mode 100644 engram/test/test_interoception_p3_drift.c diff --git a/engram/test/run_interoception_p3.sh b/engram/test/run_interoception_p3.sh new file mode 100755 index 0000000..26d2fd6 --- /dev/null +++ b/engram/test/run_interoception_p3.sh @@ -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 diff --git a/engram/test/test_interoception_p3_drift.c b/engram/test/test_interoception_p3_drift.c new file mode 100644 index 0000000..22e5fb6 --- /dev/null +++ b/engram/test/test_interoception_p3_drift.c @@ -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 +#include +#include + +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; +} diff --git a/lang/runtime/engram_geometry.c b/lang/runtime/engram_geometry.c index e033e4b..5efbb3a 100644 --- a/lang/runtime/engram_geometry.c +++ b/lang/runtime/engram_geometry.c @@ -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;icentroid && 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=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;rmembers[order[r]]; + const GeoMember* mb=NULL; + for(int j=0;jn_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(rcore_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. * ═══════════════════════════════════════════════════════════════════════════ */ diff --git a/lang/runtime/engram_geometry.h b/lang/runtime/engram_geometry.h index 0811b96..94fc2e8 100644 --- a/lang/runtime/engram_geometry.h +++ b/lang/runtime/engram_geometry.h @@ -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