9a0266cbf9
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).
131 lines
6.7 KiB
C
131 lines
6.7 KiB
C
/* test_m35_hebb_persist.c — M3.5 PRE-FLIP GATE.
|
|
*
|
|
* Proves that in-place field mutations made during spreading activation — edge
|
|
* `hebb` (+ last_fired), node `activation_count`, node working-memory weight —
|
|
* PERSIST to the paged store and survive a restart from neuron.egm with
|
|
* snapshot.json deleted. This is the "hebb-survives-restart" fix that gates the
|
|
* live cutover.
|
|
*
|
|
* Same style as test_m3_parity.c: a REAL el-level harness linking the actual
|
|
* el_runtime.c native engram builtins + engram_store.c, driving engram_node_full
|
|
* / engram_connect / engram_activate_json / engram_save / engram_store_boot /
|
|
* engram_store_checkpoint / engram_store_close directly from C. No EL interpreter.
|
|
*
|
|
* Modes (argv[1]), data dir (argv[2]):
|
|
* pos_seed — ENGRAM_STORE=1: fresh store, seed a graph tuned so activation
|
|
* co-activates a connected pair (edge hebb 0 -> ETA) and reinforces
|
|
* nodes (activation_count 0 -> >=1, WM weight -> >0). Export the
|
|
* post-activation resident graph to pre_reboot.json, then CHECKPOINT
|
|
* (the M3.5 field-persist), then close.
|
|
* pos_reboot— ENGRAM_STORE=1, snapshot.json deleted by runner: boot from
|
|
* neuron.egm (WAL replay), export reboot.json, close. The values in
|
|
* reboot.json are what actually survived the round-trip.
|
|
* neg_seed — identical to pos_seed but WITHOUT the checkpoint field-persist
|
|
* (negative control): activation mutations never reach the store.
|
|
* neg_reboot— boot from neuron.egm, export neg_reboot.json, close.
|
|
* offcheck — ENGRAM_STORE unset: seed+activate+checkpoint must NOT touch the
|
|
* store (no neuron.egm, checkpoint returns 0).
|
|
*
|
|
* The pass/fail assertions live in run_m35_hebb_persist.sh (python over the JSON
|
|
* exports): reboot.json must carry the learned hebb / activation_count and the
|
|
* JSON-identical halved WM weight; neg_reboot.json must have LOST them.
|
|
*/
|
|
#include "el_runtime.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
extern int engram_store_enabled(void);
|
|
extern el_val_t engram_store_boot(el_val_t data_dir);
|
|
extern el_val_t engram_store_checkpoint(void);
|
|
extern el_val_t engram_store_close(void);
|
|
|
|
static el_val_t S(const char* s){ return EL_STR(s); }
|
|
static el_val_t F(double d){ return el_from_float(d); }
|
|
|
|
/* Two nodes with DISTINCT content (so the redundancy-suppression pass cannot
|
|
* dedup one of them away) that both match the query strongly, wired by one
|
|
* "associate" edge. A handful of weakly-related distractors make it a real
|
|
* graph. On activation both A and B promote to working memory and co-activate,
|
|
* so their edge's hebb rises from 0 to ENGRAM_HEBB_ETA. */
|
|
static void build_seed(void){
|
|
el_val_t a = engram_node_full(S("hebbian potentiation strengthens co-active memory links"),
|
|
S("Concept"), S("hebb-a"), F(0.9), F(0.85), F(1.0), S("Semantic"),
|
|
S("hebbian,memory,activation"));
|
|
el_val_t b = engram_node_full(S("co-active memory links accrue hebbian associative weight"),
|
|
S("Concept"), S("hebb-b"), F(0.9), F(0.85), F(1.0), S("Semantic"),
|
|
S("hebbian,memory,weight"));
|
|
el_val_t c = engram_node_full(S("unrelated culinary recipe for sourdough bread"),
|
|
S("Fact"), S("distractor-1"), F(0.4), F(0.4), F(1.0), S("Semantic"),
|
|
S("food"));
|
|
el_val_t d = engram_node_full(S("the weather forecast predicts rain tomorrow afternoon"),
|
|
S("Fact"), S("distractor-2"), F(0.4), F(0.4), F(1.0), S("Semantic"),
|
|
S("weather"));
|
|
engram_connect(a, b, F(0.8), S("associate")); /* the edge under test */
|
|
engram_connect(a, c, F(0.3), S("associate"));
|
|
engram_connect(b, d, F(0.3), S("associate"));
|
|
}
|
|
|
|
static const char* QUERY =
|
|
"hebbian potentiation co-active memory links associative weight";
|
|
|
|
static void export_graph(const char* dir, const char* name){
|
|
char p[1024];
|
|
snprintf(p, sizeof p, "%s/%s", dir, name);
|
|
if (!engram_save(S(p))){ fprintf(stderr, "save %s failed\n", name); exit(2); }
|
|
}
|
|
|
|
int main(int argc, char** argv){
|
|
if (argc < 3){
|
|
fprintf(stderr, "usage: %s <pos_seed|pos_reboot|neg_seed|neg_reboot|offcheck> <dir>\n", argv[0]);
|
|
return 2;
|
|
}
|
|
const char* mode = argv[1];
|
|
const char* dir = argv[2];
|
|
|
|
if (!strcmp(mode, "pos_seed") || !strcmp(mode, "neg_seed")){
|
|
int persist = !strcmp(mode, "pos_seed");
|
|
if (!engram_store_enabled()){ fprintf(stderr, "%s requires ENGRAM_STORE=1\n", mode); return 2; }
|
|
if (!engram_store_boot(S(dir))){ fprintf(stderr, "store boot failed\n"); return 2; }
|
|
build_seed();
|
|
el_val_t act = engram_activate_json(S(QUERY), (el_val_t)3);
|
|
(void)act;
|
|
/* Capture the post-activation resident state BEFORE persisting/closing. */
|
|
export_graph(dir, persist ? "pre_reboot.json" : "neg_pre.json");
|
|
printf("[%s] nodes=%lld edges=%lld\n", mode,
|
|
(long long)(int64_t)engram_node_count(),
|
|
(long long)(int64_t)engram_edge_count());
|
|
if (persist){
|
|
if (!engram_store_checkpoint()){ fprintf(stderr, "checkpoint failed\n"); return 2; }
|
|
}
|
|
/* neg mode: NO field-persist checkpoint. engram_store_close still flushes
|
|
* pages, but no store_put_* ran post-creation, so the store keeps the
|
|
* pristine creation-time field values (hebb=0, activation_count=0). */
|
|
engram_store_close();
|
|
return 0;
|
|
}
|
|
if (!strcmp(mode, "pos_reboot") || !strcmp(mode, "neg_reboot")){
|
|
if (!engram_store_enabled()){ fprintf(stderr, "%s requires ENGRAM_STORE=1\n", mode); return 2; }
|
|
/* snapshot.json deleted by the runner — boot MUST come from neuron.egm. */
|
|
if (!engram_store_boot(S(dir))){ fprintf(stderr, "reboot boot failed\n"); return 2; }
|
|
export_graph(dir, !strcmp(mode, "pos_reboot") ? "reboot.json" : "neg_reboot.json");
|
|
printf("[%s] nodes=%lld edges=%lld\n", mode,
|
|
(long long)(int64_t)engram_node_count(),
|
|
(long long)(int64_t)engram_edge_count());
|
|
engram_store_close();
|
|
return 0;
|
|
}
|
|
if (!strcmp(mode, "offcheck")){
|
|
int en = engram_store_enabled();
|
|
el_val_t boot = engram_store_boot(S(dir)); /* no-op with flag off */
|
|
build_seed();
|
|
engram_activate_json(S(QUERY), (el_val_t)3);
|
|
el_val_t ck = engram_store_checkpoint(); /* must be a no-op */
|
|
printf("[offcheck] enabled=%d boot=%lld checkpoint=%lld\n",
|
|
en, (long long)(int64_t)boot, (long long)(int64_t)ck);
|
|
return (en == 0 && (int64_t)boot == 0 && (int64_t)ck == 0) ? 0 : 1;
|
|
}
|
|
fprintf(stderr, "unknown mode %s\n", mode);
|
|
return 2;
|
|
}
|