diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 199fa90..3971fad 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -13775,7 +13775,65 @@ el_val_t engram_think_json(el_val_t seeds, el_val_t faculty) { if (!g) return eg_geo_err("geometry unavailable"); CogStance st; cog_stance_init(&st, NULL, EL_CSTR(faculty), g->hub_id, NULL, g); GeoGradient grad; - if (engram_think(g, NULL, &st, &grad) != 0) { cog_stance_free(&st); engram_geo_free(g); return eg_geo_err("think failed"); } + + /* ANCHOR THE READ (2026-08-16 self-review). This passed NULL, and NULL is + * not "no opinion" — engram_think re-origins at `anchor ? anchor : + * region->centroid`, so NULL means "read from the centroid", and the + * centroid is the ONE point where the gradient is zero by construction: + * r = x - centroid = 0, so every axis projection is 0, grad is 0, and + * direction takes the "at rest" branch. Measured consequence: EVERY + * faculty — reason, abduce, induce, plan, analogize — returned an + * identical null result, differing only in its label: + * {"direction":[0,0,...],"spread":0,"magnitude":1,"confidence":0.5} + * magnitude 1 is membership evaluated at the centroid, spread 0 is its + * distance to itself, and confidence 0.5 is the stance fallback. The + * geometry was never the problem — /api/drift computes real values + * (centroid_sep 0.104, core_disp 0.045) over the very same 87 members. + * Neuron could not think because the read was always taken from the + * region's own centre. + * + * The seeds choose WHICH region; they must also supply the VANTAGE it is + * read from. Anchor at the first resolvable embedded seed — the same seed + * eg_geo_build_desc infers `dim` from, so the two never disagree. A single + * seed still yields a real gradient because the descriptor expands to the + * seed's neighbourhood (87 members for the self anchor), so the seed's own + * position is distinct from the neighbourhood centroid. + * + * COPY the vector, never borrow it: g->nodes is realloc'd in place on + * append, so a borrowed EngramNode* is a dangling pointer across any + * concurrent write. 768 floats is 3 KB. */ + float* anchor = NULL; + { + EngramStore* eg = engram_get(); + const char* csv = EL_CSTR(seeds); + if (eg && csv) { + const char* p = csv; + while (*p && !anchor) { + while (*p == ' ' || *p == ',') p++; + const char* s = p; + while (*p && *p != ',') p++; + const char* e = p; while (e > s && e[-1] == ' ') e--; + if (e > s) { + char* id = strndup(s, (size_t)(e - s)); + if (id) { + int64_t idx = engram_find_node_index(id); + if (idx >= 0 && idx < eg->node_count) { + EngramNode* n = &eg->nodes[idx]; + if (n->emb && n->emb_dim == g->dim) { + anchor = malloc(sizeof(float) * (size_t)g->dim); + if (anchor) memcpy(anchor, n->emb, + sizeof(float) * (size_t)g->dim); + } + } + free(id); + } + } + } + } + } + + if (engram_think(g, anchor, &st, &grad) != 0) { free(anchor); cog_stance_free(&st); engram_geo_free(g); return eg_geo_err("think failed"); } + free(anchor); JsonBuf b; jb_init(&b); char t[256]; snprintf(t, sizeof t, "{\"faculty\":\"%s\",\"n_support\":%d,\"magnitude\":%.6g,\"spread\":%.6g,\"confidence\":%.6g,\"dim\":%d", EL_CSTR(faculty), grad.n_support, grad.magnitude, grad.spread, grad.confidence, grad.dim);