Files
el/engram/test/run_m35_hebb_persist.sh
T
will.anderson 9a0266cbf9 engram tiered storage M3.5: persist activation field updates (pre-flip gate)
Flag-on checkpoint now full-walks the resident graph: store_put_node (WM weight,
activation_count, last_activated, wm_anchor) + store_put_edge (hebb, last_fired)
for every node/edge, then engram_checkpoint. Uses store_put_edge (idempotent
upsert) not store_hebb_batch, because activation FORMS new hebbian-associate edges
that bypass the create hook and delta-only hebb_batch can't create them. Store-on
boot now applies the same WM-halving + floor + cap transforms as engram_load.

This is the hebb-survives-restart fix. Gate: reboot from neuron.egm with
snapshot.json deleted -> edge hebb + activation_count survive unchanged, WM weight
survives with identical boot transform; negative control proves persist is
load-bearing (hebb->0 without it). M1 33/33 + M2 36/36 + M3 parity PASS, ASan/UBSan
clean, flag-off untouched. Engine unchanged (boundary held).
2026-08-11 23:37:52 -05:00

159 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# M3.5 PRE-FLIP GATE. Pure C harness (NOT elb/elc): links the real el_runtime.c
# native engram builtins + engram_store.c and proves activation-time field
# mutations (edge hebb, node activation_count, WM weight) persist through a
# checkpoint and survive a reboot from neuron.egm with snapshot.json DELETED.
# Writes ONLY under a throwaway /tmp dir with a throwaway HOME.
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
RT="$HERE/../../lang/runtime/el_runtime.c"
ST="$HERE/../../lang/runtime/engram_store.c"
INC="$HERE/../../lang/runtime"
WORK="$(mktemp -d /tmp/engram-m35-XXXXXX)"
BIN="$WORK/m35"
export HOME="$WORK/home"; mkdir -p "$HOME" # never touch real ~/.neuron
export ENGRAM_WAL_SYNC=always
unset ENGRAM_STORE
fail=0
echo "== compiling harness (gcc: el_runtime.c + engram_store.c + test_m35_hebb_persist.c) =="
gcc -O1 -std=c11 -I "$INC" "$HERE/test_m35_hebb_persist.c" "$RT" "$ST" -lcurl -o "$BIN" 2>"$WORK/cc.log"
if [ $? -ne 0 ]; then echo "COMPILE FAILED:"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; fi
echo
echo "== 0) flag-OFF: seed+activate+checkpoint must NOT touch the store =="
DOFF="$WORK/off"; mkdir -p "$DOFF"
( unset ENGRAM_STORE; "$BIN" offcheck "$DOFF" )
[ $? -ne 0 ] && { echo "FAIL: offcheck"; fail=1; }
[ -e "$DOFF/neuron.egm" ] && { echo "FAIL: neuron.egm created while flag OFF"; fail=1; } \
|| echo " ok: no neuron.egm created with flag OFF"
echo
echo "== 1) POSITIVE: ENGRAM_STORE=1 seed -> activate -> checkpoint(field-persist) -> close =="
DPOS="$WORK/pos"; mkdir -p "$DPOS"
ENGRAM_STORE=1 "$BIN" pos_seed "$DPOS" || { echo "FAIL: pos_seed"; fail=1; }
[ -e "$DPOS/neuron.egm" ] && echo " ok: neuron.egm created" || { echo "FAIL: neuron.egm missing"; fail=1; }
echo
echo "== 2) reboot from neuron.egm with snapshot.json DELETED (must never read JSON) =="
rm -f "$DPOS/snapshot.json"
ENGRAM_STORE=1 "$BIN" pos_reboot "$DPOS" || { echo "FAIL: pos_reboot"; fail=1; }
echo
echo "== 3) NEGATIVE CONTROL: seed -> activate -> close WITHOUT the field-persist checkpoint =="
DNEG="$WORK/neg"; mkdir -p "$DNEG"
ENGRAM_STORE=1 "$BIN" neg_seed "$DNEG" || { echo "FAIL: neg_seed"; fail=1; }
rm -f "$DNEG/snapshot.json"
ENGRAM_STORE=1 "$BIN" neg_reboot "$DNEG" || { echo "FAIL: neg_reboot"; fail=1; }
echo
echo "== 4) assertions (python over the JSON exports) =="
python3 - "$DPOS" "$DNEG" <<'PY'
import json, sys, os
WM_FLOOR = 0.05
HEBB_MIN = 1e-6
def load(d, name):
with open(os.path.join(d, name)) as f: return json.load(f)
def node_by_label(g, label):
for n in g["nodes"]:
if n.get("label") == label: return n
return None
def edge_between(g, a_id, b_id):
for e in g["edges"]:
if e.get("from_id") == a_id and e.get("to_id") == b_id:
return e
return None
rc = 0
def check(cond, msg):
global rc
if cond: print(f" PASS: {msg}")
else: print(f" FAIL: {msg}"); rc = 1
dpos, dneg = sys.argv[1], sys.argv[2]
pre = load(dpos, "pre_reboot.json")
rebt = load(dpos, "reboot.json")
pa, pb = node_by_label(pre, "hebb-a"), node_by_label(pre, "hebb-b")
ra = node_by_label(rebt, "hebb-a")
assert pa and pb and ra, "target nodes missing"
pe = edge_between(pre, pa["id"], pb["id"])
re = edge_between(rebt, pa["id"], pb["id"])
assert pe and re, "target edge missing"
pre_hebb = pe.get("hebb", 0.0)
rebt_hebb = re.get("hebb", 0.0)
pre_ac = pa.get("activation_count", 0)
rebt_ac = ra.get("activation_count", 0)
pre_wm = pa.get("working_memory_weight", 0.0)
rebt_wm = ra.get("working_memory_weight", 0.0)
print(f" edge hebb-a->hebb-b : pre={pre_hebb!r} reboot={rebt_hebb!r}")
print(f" node hebb-a act_cnt : pre={pre_ac!r} reboot={rebt_ac!r}")
print(f" node hebb-a wm : pre={pre_wm!r} reboot={rebt_wm!r} (halved+floored expected)")
# --- learning actually happened this run (else the test proves nothing) ---
check(pre_hebb > HEBB_MIN, f"activation raised edge hebb above 0 (pre={pre_hebb})")
check(pre_ac >= 1, f"activation reinforced node activation_count (pre={pre_ac})")
check(pre_wm > 0.0, f"activation promoted node to working memory (pre_wm={pre_wm})")
# --- the load-bearing survival assertions after a real delete-JSON reboot ---
check(abs(rebt_hebb - pre_hebb) < 1e-12,
f"edge hebb SURVIVED reboot unchanged ({rebt_hebb} == {pre_hebb})")
check(rebt_ac == pre_ac,
f"node activation_count SURVIVED reboot unchanged ({rebt_ac} == {pre_ac})")
# --- WM weight: must equal the JSON path's boot transform exactly (halve+floor) ---
expected_wm = pre_wm * 0.5
if expected_wm < WM_FLOOR: expected_wm = 0.0
check(abs(rebt_wm - expected_wm) < 1e-9,
f"node WM weight SURVIVED with the SAME boot transform as JSON path "
f"(reboot={rebt_wm} == halve+floor(pre)={expected_wm})")
check(expected_wm > 0.0,
f"WM survival is observable (halved weight stays above floor: {expected_wm} > {WM_FLOOR})")
# --- NEGATIVE CONTROL: without the field-persist step the learning is LOST ---
npre = load(dneg, "neg_pre.json")
nrebt = load(dneg, "neg_reboot.json")
na_pre = node_by_label(npre, "hebb-a")
na_rebt = node_by_label(nrebt, "hebb-a")
ne_pre = edge_between(npre, na_pre["id"], node_by_label(npre, "hebb-b")["id"])
ne_rebt = edge_between(nrebt, na_rebt["id"], node_by_label(nrebt, "hebb-b")["id"])
print(f" [neg] edge hebb : pre={ne_pre.get('hebb',0.0)!r} reboot={ne_rebt.get('hebb',0.0)!r}")
print(f" [neg] node act_cnt : pre={na_pre.get('activation_count',0)!r} reboot={na_rebt.get('activation_count',0)!r}")
check(ne_pre.get("hebb", 0.0) > HEBB_MIN,
f"[neg] activation DID raise hebb in RAM (pre={ne_pre.get('hebb',0.0)})")
check(ne_rebt.get("hebb", 0.0) == 0.0,
"[neg] WITHOUT checkpoint field-persist, edge hebb is LOST on reboot (==0) — fix is load-bearing")
check(na_rebt.get("activation_count", 0) == 0,
"[neg] WITHOUT checkpoint field-persist, activation_count is LOST on reboot (==0)")
sys.exit(rc)
PY
[ $? -ne 0 ] && fail=1
echo
echo "== 5) ASan+UBSan build, exercise the full persist+reboot flow (leaks off — harness intentionally leaks el_strdup) =="
SANBIN="$WORK/m35.san"
gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \
-I "$INC" "$HERE/test_m35_hebb_persist.c" "$RT" "$ST" -lcurl -o "$SANBIN" 2>"$WORK/san_cc.log"
if [ $? -ne 0 ]; then echo " SAN COMPILE FAILED:"; tail -20 "$WORK/san_cc.log"; fail=1; else
export ASAN_OPTIONS=detect_leaks=0
DSAN="$WORK/san"; mkdir -p "$DSAN"
ENGRAM_STORE=1 "$SANBIN" pos_seed "$DSAN" >/dev/null 2>"$WORK/san_run.log" && \
{ rm -f "$DSAN/snapshot.json"; ENGRAM_STORE=1 "$SANBIN" pos_reboot "$DSAN" >/dev/null 2>>"$WORK/san_run.log"; }
if grep -qiE 'runtime error|AddressSanitizer|UndefinedBehavior|ERROR: ' "$WORK/san_run.log"; then
echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san_run.log" | head; fail=1
else
echo " ok: ASan+UBSan clean across pos_seed/checkpoint/reboot (field-persist, boot laundering)"
fi
fi
echo
if [ "$fail" -eq 0 ]; then echo "================ M3.5 HEBB-PERSIST GATE: PASS ================"; else echo "================ M3.5 HEBB-PERSIST GATE: FAIL ================"; fi
rm -rf "$WORK"
exit $fail