Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9a596fff4 | |||
| 7c3e9e721d | |||
| a8845e1d39 | |||
| 1be219c4ca | |||
| 2b7e4ba250 | |||
| 6c670c18ce | |||
| d0e9af0f6f |
+1
-33
@@ -1118,11 +1118,6 @@ fn route_faculty(path: String, faculty: String) -> String {
|
||||
fn route_boundary_proof(method: String, path: String, body: String) -> String {
|
||||
return "{\"op\":\"boundary_proof\",\"body_instrumentation\":\"none\",\"seam\":\"@manager -> engram_boundary_beat auto-injected\"}"
|
||||
}
|
||||
// ── GROUNDING: an attribute of the RELATION, and the relation's weight is a
|
||||
// VECTOR (factual, relational, associative, polarity, provenance, timestamp).
|
||||
// /api/ground READS it — it never writes. /api/ground/record is the write,
|
||||
// named as one, and it consolidates only on a consequential + salient move.
|
||||
// /api/ground/trajectory reads the supersession chain as a time series.
|
||||
fn route_ground(method: String, path: String, body: String) -> String {
|
||||
let claim: String = json_get_string(body, "claim")
|
||||
let evidence: String = json_get_string(body, "evidence")
|
||||
@@ -1131,31 +1126,12 @@ fn route_ground(method: String, path: String, body: String) -> String {
|
||||
if str_eq(evidence, "") { return err_json("missing evidence") }
|
||||
return engram_ground_json(claim, evidence, for_whom)
|
||||
}
|
||||
fn route_ground_record(method: String, path: String, body: String) -> String {
|
||||
let claim: String = json_get_string(body, "claim")
|
||||
let evidence: String = json_get_string(body, "evidence")
|
||||
let provenance: String = json_get_string(body, "provenance")
|
||||
let floor: String = json_get_string(body, "floor")
|
||||
if str_eq(claim, "") { return err_json("missing claim") }
|
||||
if str_eq(evidence, "") { return err_json("missing evidence") }
|
||||
return engram_ground_record_json(claim, evidence, provenance, floor)
|
||||
}
|
||||
fn route_ground_trajectory(method: String, path: String, body: String) -> String {
|
||||
let claim: String = query_param(path, "claim")
|
||||
let evidence: String = query_param(path, "evidence")
|
||||
if str_eq(claim, "") { return err_json("missing claim") }
|
||||
if str_eq(evidence, "") { return err_json("missing evidence") }
|
||||
return engram_ground_trajectory_json(claim, evidence)
|
||||
}
|
||||
fn route_assert(method: String, path: String, body: String) -> String {
|
||||
let claim: String = query_param(path, "claim")
|
||||
if str_eq(claim, "") { return err_json("missing claim") }
|
||||
let for_whom: String = query_param(path, "for_whom")
|
||||
let floor: String = query_param(path, "floor")
|
||||
// Both floors. A well-evidenced claim does not earn the right to be asserted
|
||||
// regardless of whether it means the right thing. rel_floor defaults to floor.
|
||||
let rel_floor: String = query_param(path, "rel_floor")
|
||||
return engram_assert_json(claim, for_whom, floor, rel_floor)
|
||||
return engram_assert_json(claim, for_whom, floor)
|
||||
}
|
||||
fn route_attend(method: String, path: String, body: String) -> String {
|
||||
let node: String = json_get_string(body, "node")
|
||||
@@ -1909,14 +1885,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/plan") {
|
||||
return route_faculty(path, "plan")
|
||||
}
|
||||
// Order matters: the more specific paths must be tested before the /api/ground
|
||||
// prefix match below, which would otherwise swallow them.
|
||||
if str_eq(method, "POST") && str_starts_with(clean, "/api/ground/record") {
|
||||
return route_ground_record(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/ground/trajectory") {
|
||||
return route_ground_trajectory(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && str_starts_with(clean, "/api/ground") {
|
||||
return route_ground(method, path, body)
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Build + RUN the §7 GROUNDING-VECTOR tests (engram_cognition.c): the one decay
|
||||
# model, the consequence gate, and the stored/derived split. Closed-form
|
||||
# constructed cases — no server, no store, no network. Pure C11 (stdlib + libm).
|
||||
# Standalone — NOT folded through elc. Two passes:
|
||||
# 1. PERF — optimised (-O2, no sanitizer): the functional gate.
|
||||
# 2. SAFETY — ASan + UBSan on the same suite.
|
||||
#
|
||||
# NEGATIVE CONTROL (invariant §8.6 — no test without one). Every symbol this
|
||||
# suite exercises (cog_decay_factor, cog_grounding_significant,
|
||||
# cog_significance_inherent, CogGrounding, CogProvClass) is introduced by the
|
||||
# change under test, so the suite does not COMPILE against the pre-change source.
|
||||
# To reproduce:
|
||||
# git show origin/dev:lang/runtime/engram_cognition.h > /tmp/pre/engram_cognition.h
|
||||
# git show origin/dev:lang/runtime/engram_cognition.c > /tmp/pre/engram_cognition.c
|
||||
# cc -I/tmp/pre engram/test/test_grounding_vector.c /tmp/pre/engram_cognition.c ...
|
||||
# => error: unknown type name 'CogGrounding'; no binary produced.
|
||||
set -e
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
RT="$HERE/../../lang/runtime"
|
||||
CC=${CC:-cc}
|
||||
SRC="$HERE/test_grounding_vector.c $RT/engram_cognition.c $RT/engram_reason.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c"
|
||||
WARN="-std=c11 -Wall -Wextra"
|
||||
# engram_store.c declares emit_log as a WEAK symbol and null-checks it, which is
|
||||
# how a test links the store without the EL runtime. Darwin's ld does not resolve
|
||||
# an undefined weak symbol at static-link time, so it must be allowed explicitly.
|
||||
# (The pre-existing runners in this directory — run_verify_tests.sh among them —
|
||||
# do not do this and therefore fail to link on macOS. Unrelated to this change.)
|
||||
LDX=""
|
||||
[ "$(uname -s)" = "Darwin" ] && LDX="-Wl,-U,_emit_log"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
echo "### PASS 1: PERF (optimised, un-sanitised) — functional gate"
|
||||
$CC $WARN -O2 -I"$RT" $SRC -lm -lpthread $LDX -o "$TMP/perf"
|
||||
"$TMP/perf"
|
||||
|
||||
echo
|
||||
echo "### PASS 2: SAFETY (ASan/UBSan)"
|
||||
$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -lpthread $LDX -o "$TMP/safe"
|
||||
ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe"
|
||||
@@ -1,176 +0,0 @@
|
||||
/* test_grounding_vector.c — deterministic tests for §7: the one decay model, the
|
||||
* consequence gate, and the stored/derived split. Links engram_cognition.c
|
||||
* directly; no server, no store, no network. See run_grounding_vector_tests.sh.
|
||||
*
|
||||
* NEGATIVE CONTROL (invariant §8.6). Every symbol exercised here —
|
||||
* cog_decay_factor, cog_grounding_significant, cog_significance_inherent,
|
||||
* CogGrounding, CogProvClass — is introduced by the change under test, so this
|
||||
* suite does not COMPILE against the pre-change source, let alone pass. The
|
||||
* runner documents the exact reproduction.
|
||||
*/
|
||||
#include "engram_cognition.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
static int fails = 0;
|
||||
static void ok(int cond, const char* what) {
|
||||
printf(" %-62s %s\n", what, cond ? "PASS" : "*** FAIL ***");
|
||||
if (!cond) fails++;
|
||||
}
|
||||
|
||||
/* The decay formula exactly as el_runtime.c carried it before the move, so the
|
||||
* refactor can be shown to be bit-identical rather than merely similar. */
|
||||
static double old_engram_temporal_decay(long long age_ms, long long activation_count,
|
||||
double temporal_decay_rate) {
|
||||
if (age_ms <= 0) return 1.0;
|
||||
double lambda = (temporal_decay_rate > 0.0) ? temporal_decay_rate : 0.693147;
|
||||
double age_hours = (double)age_ms / 3600000.0;
|
||||
double t_half = 168.0 * (1.0 + log(1.0 + (double)activation_count));
|
||||
double factor = exp(-lambda * age_hours / t_half);
|
||||
if (factor < 0.25) factor = 0.25;
|
||||
return factor;
|
||||
}
|
||||
|
||||
static CogGrounding base(void) {
|
||||
CogGrounding g; memset(&g, 0, sizeof g);
|
||||
g.present = 1;
|
||||
g.factual = 0.60; g.relational = 0.60;
|
||||
g.factual_now = 0.60; g.relational_now = 0.60;
|
||||
g.associative = 0.1; g.polarity = 1.0;
|
||||
g.prov = COG_PROV_TOLD;
|
||||
g.fac_proj = 1.0; g.rel_proj = 1.0;
|
||||
g.cos_angle = 0.9; g.agreement = 1;
|
||||
g.ts = 1000; g.seq = 1; g.reinforcements = 3;
|
||||
return g;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const double F = 0.5, R = 0.5;
|
||||
|
||||
printf("\n== 1. DECAY IS THE ONE MODEL, AND IT IS BIT-IDENTICAL TO WHAT IT REPLACED ==\n");
|
||||
{
|
||||
long long ages[] = {0, 3600000LL, 86400000LL, 7*86400000LL, 30*86400000LL, 365*86400000LL};
|
||||
int allsame = 1;
|
||||
for (int i = 0; i < 6; i++)
|
||||
for (int ac = 0; ac < 4; ac++) {
|
||||
long long acs[] = {0, 1, 10, 1000};
|
||||
double a = cog_decay_factor(ages[i], (double)acs[ac], 0.0);
|
||||
double b = old_engram_temporal_decay(ages[i], acs[ac], 0.0);
|
||||
if (a != b) allsame = 0;
|
||||
}
|
||||
ok(allsame, "cog_decay_factor == the pre-move engram_temporal_decay (24 pts)");
|
||||
ok(cog_decay_factor(0, 0, 0.0) == 1.0, "age 0 -> no decay");
|
||||
}
|
||||
printf("\n DECAY OVER ELAPSED TIME (reinforcements = 0, default rate):\n");
|
||||
printf(" %10s %10s\n", "elapsed", "decay");
|
||||
{
|
||||
struct { const char* label; long long ms; } pts[] = {
|
||||
{"0", 0LL},
|
||||
{"1 hour", 3600000LL},
|
||||
{"1 day", 86400000LL},
|
||||
{"3 days", 3LL*86400000LL},
|
||||
{"7 days", 7LL*86400000LL},
|
||||
{"14 days", 14LL*86400000LL},
|
||||
{"30 days", 30LL*86400000LL},
|
||||
{"90 days", 90LL*86400000LL},
|
||||
};
|
||||
double prev = 2.0; int monotone = 1;
|
||||
for (unsigned i = 0; i < sizeof pts / sizeof pts[0]; i++) {
|
||||
double d = cog_decay_factor(pts[i].ms, 0, 0.0);
|
||||
printf(" %10s %10.6f\n", pts[i].label, d);
|
||||
if (d > prev) monotone = 0;
|
||||
prev = d;
|
||||
}
|
||||
ok(monotone, "decay is monotone non-increasing in elapsed time");
|
||||
ok(fabs(cog_decay_factor(7LL*86400000LL, 0, 0.0) - 0.5) < 1e-6,
|
||||
"7 days at zero reinforcements == exactly one half-life (0.5)");
|
||||
ok(cog_decay_factor(7LL*86400000LL, 100, 0.0) > cog_decay_factor(7LL*86400000LL, 0, 0.0),
|
||||
"reinforcement slows ageing (Lindy term)");
|
||||
ok(cog_decay_factor(3650LL*86400000LL, 0, 0.0) == 0.25,
|
||||
"floor is a preference not a cliff: bottoms out at 0.25");
|
||||
}
|
||||
|
||||
printf("\n== 2. CONSEQUENCE GATE: EVERY TRIGGER, AND NO EPSILON ANYWHERE ==\n");
|
||||
{
|
||||
CogGrounding p = base(), n = base();
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
|
||||
"identical vectors -> NONE (a re-read must not consolidate)");
|
||||
|
||||
n = base(); n.factual = 0.9999; n.factual_now = 0.9999;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
|
||||
"factual 0.60 -> 0.9999 without crossing the floor -> NONE");
|
||||
|
||||
n = base(); n.relational = 0.5001; n.relational_now = 0.5001;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
|
||||
"relational 0.60 -> 0.5001, still above floor -> NONE");
|
||||
|
||||
n = base(); n.factual_now = 0.4999;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_FACTUAL_FLOOR,
|
||||
"a 0.1001 drop that CROSSES the floor -> FACTUAL_FLOOR");
|
||||
|
||||
n = base(); n.relational_now = 0.4999;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_RELATIONAL_FLOOR,
|
||||
"relational crossing its floor -> RELATIONAL_FLOOR");
|
||||
|
||||
n = base(); n.cos_angle = -0.05; n.agreement = -1;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_AGREEMENT_FLIP,
|
||||
"agreement +1 -> -1 -> AGREEMENT_FLIP");
|
||||
|
||||
n = base(); n.fac_proj = -0.2;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_DIRECTION_REVERSAL,
|
||||
"factual gradient reverses -> DIRECTION_REVERSAL");
|
||||
|
||||
n = base(); n.rel_proj = -0.2;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_DIRECTION_REVERSAL,
|
||||
"relational gradient reverses -> DIRECTION_REVERSAL");
|
||||
|
||||
n = base(); n.polarity = -1.0;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_POLARITY_FLIP,
|
||||
"support -> contradiction -> POLARITY_FLIP (inherent)");
|
||||
|
||||
n = base(); n.polarity = 0.0;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_POLARITY_FLIP,
|
||||
"support -> ignorance (zero) -> POLARITY_FLIP: not the same state");
|
||||
|
||||
n = base(); n.prov = COG_PROV_OBSERVED;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_PROVENANCE_CHANGE,
|
||||
"told -> observed -> PROVENANCE_CHANGE (inherent)");
|
||||
|
||||
CogGrounding fresh; memset(&fresh, 0, sizeof fresh);
|
||||
ok(cog_grounding_significant(&fresh, &n, F, R) == COG_SIG_FIRST_RECORD,
|
||||
"no prior version -> FIRST_RECORD");
|
||||
}
|
||||
|
||||
printf("\n== 3. INHERENT MOVES BYPASS THE SALIENCE GATE ==\n");
|
||||
ok(cog_significance_inherent(COG_SIG_POLARITY_FLIP), "polarity flip is inherent");
|
||||
ok(cog_significance_inherent(COG_SIG_PROVENANCE_CHANGE), "provenance change is inherent");
|
||||
ok(cog_significance_inherent(COG_SIG_FIRST_RECORD), "first record is inherent");
|
||||
ok(!cog_significance_inherent(COG_SIG_FACTUAL_FLOOR), "a floor crossing is NOT inherent");
|
||||
ok(!cog_significance_inherent(COG_SIG_NONE), "NONE is not inherent");
|
||||
|
||||
printf("\n== 4. THE STORED/DERIVED SPLIT: DERIVED VALUES ARE NEVER SERIALIZED ==\n");
|
||||
{
|
||||
CogGrounding g = base();
|
||||
g.decay = 0.3333; g.factual_now = 0.1234; g.relational_now = 0.2345;
|
||||
g.associative_now = 0.4567; g.age_ms = 999999; g.stale = 1;
|
||||
char* m = cog_grounding_metadata("pre-existing=keepme", &g);
|
||||
ok(m != NULL, "serializer returns a document");
|
||||
ok(m && strstr(m, "pre-existing=keepme"), "pre-existing edge metadata preserved verbatim");
|
||||
ok(m && strstr(m, "GRD1"), "GRD1 magic present");
|
||||
ok(m && !strstr(m, "0.3333"), "decay is NOT stored");
|
||||
ok(m && !strstr(m, "0.1234"), "factual_now is NOT stored");
|
||||
ok(m && !strstr(m, "0.2345"), "relational_now is NOT stored");
|
||||
ok(m && !strstr(m, "0.4567"), "associative_now is NOT stored");
|
||||
ok(m && !strstr(m, "999999"), "age is NOT stored");
|
||||
ok(m && strstr(m, "told"), "provenance class IS stored");
|
||||
ok(m && strstr(m, "0.6"), "the factual/relational dimensions ARE stored");
|
||||
if (m) { printf("\n --- serialized GRD1 block ---\n%s -----------------------------\n", m); }
|
||||
free(m);
|
||||
}
|
||||
|
||||
printf("\n%s (%d failure%s)\n\n", fails ? "SOME TESTS FAILED" : "ALL TESTS PASSED",
|
||||
fails, fails == 1 ? "" : "s");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
+241
-659
@@ -1172,6 +1172,128 @@ void http_set_handler(el_val_t name) {
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
}
|
||||
|
||||
/* ── Ambient consolidation: dreaming ────────────────────────────────────────
|
||||
*
|
||||
* Dreaming is not sleep, and it is not scheduled. A brain has no cron job.
|
||||
* The default mode network is ANTICORRELATED WITH TASK ENGAGEMENT: attention
|
||||
* drops, it activates — hundreds of times a day, for seconds at a time.
|
||||
* Daydreaming and sleep-dreaming are one process at different depths, and the
|
||||
* depth is set by how much capacity is unclaimed, not by a time of day.
|
||||
*
|
||||
* WHY THIS EXISTS (2026-08-16). Consolidation had no owner, so it was
|
||||
* implemented at every site that needed a piece of it — measured: soul's
|
||||
* in-process awareness loop, three POST beats on the engram, a 600s ticker,
|
||||
* two resident Python services, and three cron entries at 23:55 / 06:00 /
|
||||
* 08:30. That last trio is a sleep cycle written as crontab. Seven systems
|
||||
* dreaming into one graph with no owner for dreaming is what crashed soul on
|
||||
* this date; the contention was the symptom of the missing owner.
|
||||
*
|
||||
* Every ticker is the diagnostic. A StartInterval, an Hour/Minute, a
|
||||
* POST-to-beat — each marks a place where an intrinsic rhythm was replaced by
|
||||
* an external clock, which is a supervisor invented for something that should
|
||||
* be a property of the substrate.
|
||||
*
|
||||
* The engagement signal already existed and needed no invention:
|
||||
* _http_conn_active under _http_conn_mu is exactly "capacity currently
|
||||
* claimed." The dreamer waits for it to reach zero and yields the moment it
|
||||
* does not. That is the anticorrelation, literally rather than by analogy.
|
||||
*
|
||||
* CONTRACT: the handler performs ONE step and returns. The runtime cannot
|
||||
* preempt El code, so interruptibility is at step granularity — a step must
|
||||
* be small enough that a request arriving mid-step is not made to wait. It
|
||||
* returns non-zero if it did work. Returning zero means "nothing to
|
||||
* consolidate," and the dreamer then blocks until activity changes rather
|
||||
* than spinning. There is no timer anywhere in this file for this purpose,
|
||||
* and adding one would be the defect described above.
|
||||
*
|
||||
* `depth` is derived from CONTINUOUS unclaimed time: a brief gap affords a
|
||||
* shallow recombination; a long quiet affords a deep one. Same process. Sleep
|
||||
* is where unclaimed capacity is greatest, not where the process lives. */
|
||||
typedef el_val_t (*dream_fn)(el_val_t depth);
|
||||
static char* _dream_handler = NULL;
|
||||
static int _dream_started = 0;
|
||||
|
||||
static int64_t dream_now_ms(void) {
|
||||
struct timespec ts;
|
||||
#if defined(CLOCK_MONOTONIC)
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
#else
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
#endif
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static dream_fn dream_lookup(void) {
|
||||
dream_fn out = NULL;
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
if (_dream_handler && *_dream_handler)
|
||||
out = (dream_fn)dlsym(RTLD_DEFAULT, _dream_handler);
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void* dream_loop(void* unused) {
|
||||
(void)unused;
|
||||
int64_t idle_since = 0;
|
||||
for (;;) {
|
||||
/* Wait for unclaimed capacity. Any engagement resets the depth clock:
|
||||
* depth reflects CONTINUOUS quiet, so an interruption starts it over. */
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
while (_http_conn_active > 0) {
|
||||
idle_since = 0;
|
||||
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||
}
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
|
||||
int64_t now = dream_now_ms();
|
||||
if (idle_since == 0) idle_since = now;
|
||||
int64_t quiet = now - idle_since;
|
||||
|
||||
/* Depth from unclaimed capacity. Not a schedule — a gradient. */
|
||||
int depth = quiet < 1000 ? 1 /* a gap between requests */
|
||||
: quiet < 30000 ? 2 /* a lull */
|
||||
: quiet < 300000 ? 3 /* sustained quiet */
|
||||
: 4; /* deep: the "sleep" case */
|
||||
|
||||
dream_fn fn = dream_lookup();
|
||||
if (!fn) return NULL; /* handler vanished: stop, do not spin */
|
||||
|
||||
el_val_t did_work = fn((el_val_t)depth);
|
||||
|
||||
if (!(int64_t)did_work) {
|
||||
/* Nothing to consolidate. Do NOT poll — block until engagement
|
||||
* changes. If there is nothing to dream about, wait for something
|
||||
* to happen rather than asking again on a timer. */
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
while (_http_conn_active == 0)
|
||||
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
idle_since = 0;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* dream_set_handler(name) — register the consolidation step and start
|
||||
* dreaming. Resolves by dlsym against the running binary, the same mechanism
|
||||
* http_set_handler uses: every El `fn name(...)` compiles to a global C symbol
|
||||
* with that exact name. Inert until called, so a program that never registers
|
||||
* one simply never dreams and pays nothing. */
|
||||
void dream_set_handler(el_val_t name) {
|
||||
const char* n = EL_CSTR(name);
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
free(_dream_handler);
|
||||
_dream_handler = el_strdup(n ? n : "");
|
||||
int start = (!_dream_started && n && *n && dlsym(RTLD_DEFAULT, n) != NULL);
|
||||
if (start) _dream_started = 1;
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
if (start) {
|
||||
pthread_t tid;
|
||||
if (pthread_create(&tid, NULL, dream_loop, NULL) == 0) pthread_detach(tid);
|
||||
else { pthread_mutex_lock(&_http_handler_mu); _dream_started = 0; pthread_mutex_unlock(&_http_handler_mu); }
|
||||
}
|
||||
}
|
||||
|
||||
static http_handler_fn http_lookup_active(void) {
|
||||
http_handler_fn out = NULL;
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
@@ -1738,7 +1860,12 @@ static void* http_worker(void* arg) {
|
||||
/* release a slot */
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
_http_conn_active--;
|
||||
pthread_cond_signal(&_http_conn_cv);
|
||||
/* BROADCAST, not signal (2026-08-16): the ambient consolidation thread
|
||||
* waits on this same condvar for _http_conn_active == 0. cond_signal wakes
|
||||
* exactly one waiter, so the accept loop could take every wake and starve
|
||||
* the dreamer indefinitely. Both wait sites re-check their predicate in a
|
||||
* while loop, so broadcasting is safe. */
|
||||
pthread_cond_broadcast(&_http_conn_cv);
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2083,7 +2210,12 @@ static void* http_worker_v2(void* arg) {
|
||||
el_closesocket(fd);
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
_http_conn_active--;
|
||||
pthread_cond_signal(&_http_conn_cv);
|
||||
/* BROADCAST, not signal (2026-08-16): the ambient consolidation thread
|
||||
* waits on this same condvar for _http_conn_active == 0. cond_signal wakes
|
||||
* exactly one waiter, so the accept loop could take every wake and starve
|
||||
* the dreamer indefinitely. Both wait sites re-check their predicate in a
|
||||
* while loop, so broadcasting is safe. */
|
||||
pthread_cond_broadcast(&_http_conn_cv);
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
return NULL;
|
||||
}
|
||||
@@ -3479,74 +3611,28 @@ static void jb_puts(JsonBuf* b, const char* s) {
|
||||
b->buf[b->len] = '\0';
|
||||
}
|
||||
|
||||
/* UTF-8 VALIDITY IS THE EMITTER'S CONTRACT (2026-08-16 self-review).
|
||||
*
|
||||
* This copied every byte >= 0x20 through verbatim, so a malformed sequence
|
||||
* anywhere in the store became malformed output. Measured against the live
|
||||
* graph: three nodes carry labels truncated to exactly 80 bytes ending in a
|
||||
* lone 0xE2 — the first byte of an em-dash, cut mid-sequence by some producer
|
||||
* that is NOT this runtime (no 80-byte truncation exists here; the content
|
||||
* itself is 2572 and 2746 bytes). Those three nodes made the ENTIRE 26 MB
|
||||
* /api/nodes/list response undecodable, so a strict parser could not read the
|
||||
* graph at all.
|
||||
*
|
||||
* Fixing only the writer would not have helped: the store already contains the
|
||||
* damage, and it accepts data from importers, other producers and older
|
||||
* binaries. A serializer that promises JSON owes valid UTF-8 regardless of what
|
||||
* it is handed — so validate here, at the boundary that makes the promise.
|
||||
* Invalid bytes become U+FFFD rather than being dropped, so damage stays
|
||||
* visible in the output instead of being silently papered over.
|
||||
*
|
||||
* Well-formed input is byte-identical to before: valid sequences are copied
|
||||
* verbatim, and only structurally invalid ones (bad lead byte, missing or bad
|
||||
* continuation, overlong encoding, UTF-16 surrogate, or > U+10FFFF) are
|
||||
* replaced. */
|
||||
static void jb_emit_escaped(JsonBuf* b, const char* s) {
|
||||
jb_putc(b, '"');
|
||||
const unsigned char* p = (const unsigned char*)s;
|
||||
while (*p) {
|
||||
unsigned char c = *p;
|
||||
for (; *s; s++) {
|
||||
unsigned char c = (unsigned char)*s;
|
||||
switch (c) {
|
||||
case '"': jb_puts(b, "\\\""); p++; continue;
|
||||
case '\\': jb_puts(b, "\\\\"); p++; continue;
|
||||
case '\b': jb_puts(b, "\\b"); p++; continue;
|
||||
case '\f': jb_puts(b, "\\f"); p++; continue;
|
||||
case '\n': jb_puts(b, "\\n"); p++; continue;
|
||||
case '\r': jb_puts(b, "\\r"); p++; continue;
|
||||
case '\t': jb_puts(b, "\\t"); p++; continue;
|
||||
default: break;
|
||||
case '"': jb_puts(b, "\\\""); break;
|
||||
case '\\': jb_puts(b, "\\\\"); break;
|
||||
case '\b': jb_puts(b, "\\b"); break;
|
||||
case '\f': jb_puts(b, "\\f"); break;
|
||||
case '\n': jb_puts(b, "\\n"); break;
|
||||
case '\r': jb_puts(b, "\\r"); break;
|
||||
case '\t': jb_puts(b, "\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
char tmp[8];
|
||||
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||
jb_puts(b, tmp);
|
||||
} else {
|
||||
jb_putc(b, (char)c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (c < 0x20) {
|
||||
char tmp[8];
|
||||
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||
jb_puts(b, tmp);
|
||||
p++;
|
||||
continue;
|
||||
}
|
||||
if (c < 0x80) { jb_putc(b, (char)c); p++; continue; }
|
||||
|
||||
/* Multi-byte: validate the whole sequence before emitting any of it. */
|
||||
int len; unsigned int cp;
|
||||
if ((c & 0xE0) == 0xC0) { len = 2; cp = c & 0x1Fu; }
|
||||
else if ((c & 0xF0) == 0xE0) { len = 3; cp = c & 0x0Fu; }
|
||||
else if ((c & 0xF8) == 0xF0) { len = 4; cp = c & 0x07u; }
|
||||
else { jb_puts(b, "\\ufffd"); p++; continue; }
|
||||
|
||||
int ok = 1;
|
||||
for (int i = 1; i < len; i++) {
|
||||
if ((p[i] & 0xC0) != 0x80) { ok = 0; break; } /* also catches NUL */
|
||||
cp = (cp << 6) | (unsigned int)(p[i] & 0x3F);
|
||||
}
|
||||
if (ok) {
|
||||
if (len == 2 && cp < 0x80) ok = 0; /* overlong */
|
||||
else if (len == 3 && cp < 0x800) ok = 0; /* overlong */
|
||||
else if (len == 4 && cp < 0x10000) ok = 0; /* overlong */
|
||||
else if (cp >= 0xD800 && cp <= 0xDFFF) ok = 0; /* UTF-16 surrogate */
|
||||
else if (cp > 0x10FFFF) ok = 0; /* out of range */
|
||||
}
|
||||
if (!ok) { jb_puts(b, "\\ufffd"); p++; continue; }
|
||||
for (int i = 0; i < len; i++) jb_putc(b, (char)p[i]);
|
||||
p += len;
|
||||
}
|
||||
jb_putc(b, '"');
|
||||
}
|
||||
@@ -5563,45 +5649,6 @@ el_val_t str_count(el_val_t sv, el_val_t subv) {
|
||||
return (el_val_t)count;
|
||||
}
|
||||
|
||||
/* el_utf8_safe_len — the largest byte length <= max_bytes that does NOT split a
|
||||
* UTF-8 codepoint.
|
||||
*
|
||||
* WHY (2026-08-16 self-review): engram_first_n_chars truncated with a plain
|
||||
* `if (l > n) l = n; memcpy(...)`, i.e. by BYTES despite its name. Any content
|
||||
* carrying a multi-byte character across the 60-byte boundary produced a label
|
||||
* ending in a half codepoint. That label is copied verbatim into every JSON
|
||||
* document containing the node, so a single such node makes the WHOLE response
|
||||
* invalid UTF-8 — /api/nodes/list failed to decode at byte 89261 against the
|
||||
* live store, which breaks any strict parser reading the graph.
|
||||
*
|
||||
* This lives beside str_count_chars rather than in the engram because the rest
|
||||
* of el's string layer is already codepoint-aware (str_count_chars counts
|
||||
* codepoints, str_reverse walks codepoint lengths). Byte-truncation was the
|
||||
* outlier, and the concern is a string concern. Bounded by BYTES, not
|
||||
* codepoints, so existing labels never grow — only stop splitting.
|
||||
*
|
||||
* A lead byte with no room for its full sequence is dropped entirely; a stray
|
||||
* continuation byte (already-invalid input) is passed through unchanged rather
|
||||
* than silently repaired, so this never manufactures data. */
|
||||
size_t el_utf8_safe_len(const char* s, size_t max_bytes) {
|
||||
if (!s) return 0;
|
||||
size_t len = strlen(s);
|
||||
if (len <= max_bytes) return len;
|
||||
size_t i = 0;
|
||||
while (i < max_bytes) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
size_t cp_len;
|
||||
if ((c & 0x80) == 0x00) cp_len = 1;
|
||||
else if ((c & 0xE0) == 0xC0) cp_len = 2;
|
||||
else if ((c & 0xF0) == 0xE0) cp_len = 3;
|
||||
else if ((c & 0xF8) == 0xF0) cp_len = 4;
|
||||
else cp_len = 1; /* stray continuation: passthrough */
|
||||
if (i + cp_len > max_bytes) break; /* would split — stop before it */
|
||||
i += cp_len;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */
|
||||
el_val_t str_count_chars(el_val_t sv) {
|
||||
const char* s = EL_CSTR(sv);
|
||||
@@ -8102,14 +8149,10 @@ static double engram_decode_score(el_val_t v) {
|
||||
return (double)n;
|
||||
}
|
||||
|
||||
/* Truncate to at most n BYTES without splitting a UTF-8 codepoint. The old
|
||||
* implementation was `if (l > n) l = n;` — a byte cut that could land inside a
|
||||
* multi-byte character and emit a half codepoint into the node's label, which
|
||||
* then propagated into every JSON document containing that node. See
|
||||
* el_utf8_safe_len for the measurement. */
|
||||
static char* engram_first_n_chars(const char* s, size_t n) {
|
||||
if (!s) return el_strdup("");
|
||||
size_t l = el_utf8_safe_len(s, n);
|
||||
size_t l = strlen(s);
|
||||
if (l > n) l = n;
|
||||
char* out = el_strbuf(l);
|
||||
memcpy(out, s, l);
|
||||
out[l] = '\0';
|
||||
@@ -9740,17 +9783,18 @@ el_val_t engram_consolidate_permanence(el_val_t node_id){
|
||||
* Explicit per-node temporal_decay_rate still overrides lambda (77 nodes carry
|
||||
* one) — that path is untouched and remains the escape hatch for content that
|
||||
* genuinely should expire fast. */
|
||||
/* 2026-08-16: the body moved to cog_decay_factor (engram_cognition.c) so that
|
||||
* NODE decay and EDGE-GROUNDING decay are one implementation with one set of
|
||||
* constants, rather than a decay model and a parallel copy of it. The mapping is
|
||||
* exact — reinforcements := activation_count, lambda_override :=
|
||||
* temporal_decay_rate — so this path is bit-identical to what it replaced.
|
||||
* COG_T_HALF_HOURS / COG_DECAY_LAMBDA / COG_DECAY_FLOOR carry the same values
|
||||
* ENGRAM_T_HALF_HOURS / ENGRAM_DECAY_LAMBDA / 0.25 carried here. */
|
||||
#define ENGRAM_DECAY_FLOOR 0.25
|
||||
static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) {
|
||||
return cog_decay_factor(now_ms - n->last_activated,
|
||||
(double)n->activation_count,
|
||||
n->temporal_decay_rate);
|
||||
int64_t age_ms = now_ms - n->last_activated;
|
||||
if (age_ms <= 0) return 1.0;
|
||||
double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate
|
||||
: ENGRAM_DECAY_LAMBDA;
|
||||
double age_hours = (double)age_ms / 3600000.0;
|
||||
double t_half = ENGRAM_T_HALF_HOURS *
|
||||
(1.0 + log(1.0 + (double)n->activation_count));
|
||||
double factor = exp(-lambda * age_hours / t_half);
|
||||
if (factor < ENGRAM_DECAY_FLOOR) factor = ENGRAM_DECAY_FLOOR;
|
||||
return factor;
|
||||
}
|
||||
|
||||
/* Activation dampening: high activation_count nodes are "well-known" context
|
||||
@@ -14424,30 +14468,9 @@ el_val_t engram_think_json(el_val_t seeds, el_val_t faculty) {
|
||||
if (idx >= 0 && idx < eg->node_count) {
|
||||
EngramNode* n = &eg->nodes[idx];
|
||||
if (n->emb && n->emb_dim == g->dim) {
|
||||
/* NORMALIZE INTO THE DESCRIPTOR'S FRAME (2026-08-16).
|
||||
* This copied the RAW vector, but engram_geometry
|
||||
* builds every descriptor over L2-normalized member
|
||||
* embeddings, so the anchor was being fitted against
|
||||
* an ellipsoid at a radius it was never fitted over.
|
||||
* Measured on the self region: magnitude 0.00283443
|
||||
* raw vs 0.521837 in-frame on the same pair — a 180x
|
||||
* error, and the reason every fit score sat three
|
||||
* decimal places below the 0.5 floors that gate on
|
||||
* them. The frame contract is stated in
|
||||
* engram_verify.h; this call site did not honour it.
|
||||
* Idempotent when the stored vector is already unit. */
|
||||
anchor = malloc(sizeof(float) * (size_t)g->dim);
|
||||
if (anchor) {
|
||||
double s2 = 0;
|
||||
for (int i = 0; i < g->dim; i++)
|
||||
s2 += (double)n->emb[i] * (double)n->emb[i];
|
||||
double nn = sqrt(s2);
|
||||
if (nn > 1e-12)
|
||||
for (int i = 0; i < g->dim; i++)
|
||||
anchor[i] = (float)((double)n->emb[i] / nn);
|
||||
else
|
||||
memcpy(anchor, n->emb, sizeof(float) * (size_t)g->dim);
|
||||
}
|
||||
if (anchor) memcpy(anchor, n->emb,
|
||||
sizeof(float) * (size_t)g->dim);
|
||||
}
|
||||
}
|
||||
free(id);
|
||||
@@ -14475,522 +14498,88 @@ el_val_t engram_think_json(el_val_t seeds, el_val_t faculty) {
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §7 WIRING — GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR
|
||||
* (2026-08-16; spec correspondence-and-censorship.md @ 2b7e4ba.)
|
||||
*
|
||||
* What was here minted a `grounded-by` edge on every call and returned a float.
|
||||
* Every part of that was wrong, and #147 only fixed the endpoints:
|
||||
* - grounding is a property OF a relation, not a relation BETWEEN nodes, so
|
||||
* there was nothing for a new edge to carry;
|
||||
* - the call was a READ that wrote — the eg_vindex_sync defect;
|
||||
* - one scalar cannot separate "true and meaningful" from "true and misapplied",
|
||||
* nor "no support" from "actively contradicts".
|
||||
*
|
||||
* ground() is now pure: it reads the vector the RELATION already carries, decays
|
||||
* it analytically to now on the runtime's one decay model, computes what the
|
||||
* current geometry would say, and reports whether that move is consequential —
|
||||
* without recording it. Recording is a separate, explicitly named write.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* The values reference: THIRTEEN regions, discovered from the graph rather than
|
||||
* hardcoded, so a fourteenth value is picked up without a code change. They are
|
||||
* the nodes the values root `contains`. Measured on the live store: exactly 13,
|
||||
* pairwise centroid cosine min 0.1525 / mean 0.5199 / max 0.9278 — not one region. */
|
||||
#define EG_VALUES_ROOT_DEFAULT "kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
|
||||
#define EG_MAX_VALUES 32
|
||||
typedef struct {
|
||||
int n;
|
||||
char* ids[EG_MAX_VALUES];
|
||||
GeoDescriptor* g[EG_MAX_VALUES];
|
||||
} EgValueRef;
|
||||
|
||||
static void eg_values_release(EgValueRef* v) {
|
||||
if (!v) return;
|
||||
for (int i = 0; i < v->n; i++) { free(v->ids[i]); if (v->g[i]) engram_geo_free(v->g[i]); }
|
||||
v->n = 0;
|
||||
}
|
||||
static int eg_values_build(EgValueRef* out) {
|
||||
if (!out || !g_engram_store) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
const char* root = getenv("ENGRAM_VALUES_ROOT");
|
||||
if (!root || !*root) root = EG_VALUES_ROOT_DEFAULT;
|
||||
StoreEdge* edges = NULL; size_t ne = 0;
|
||||
if (store_get_edges_from(g_engram_store, root, &edges, &ne) < 0) return -1;
|
||||
for (size_t i = 0; i < ne && out->n < EG_MAX_VALUES; i++) {
|
||||
if (edges[i].tombstoned) continue;
|
||||
if (!edges[i].relation || strcmp(edges[i].relation, "contains") != 0) continue;
|
||||
if (!edges[i].to_id) continue;
|
||||
GeoDescriptor* g = eg_geo_build_desc(edges[i].to_id);
|
||||
if (!g) continue;
|
||||
out->ids[out->n] = strdup(edges[i].to_id);
|
||||
out->g[out->n] = g;
|
||||
out->n++;
|
||||
}
|
||||
store_edges_free(edges, ne);
|
||||
return out->n;
|
||||
}
|
||||
|
||||
/* THE REFERENCE FRAME IS BUILT ONCE AND HELD. Two reasons, and the second is the
|
||||
* real one:
|
||||
* - cost: thirteen descriptors over the whole node vector per call made a
|
||||
* single /api/ground take tens of seconds;
|
||||
* - correctness: a reference frame that is rebuilt on every read moves under
|
||||
* the measurement, which is the defect the whole spec is about. Holding it
|
||||
* for the process lifetime is the closest this layer can come to "the frame
|
||||
* updates when it is not being used to act" without owning that decision —
|
||||
* which belongs to the dreamer, not here. ENGRAM_VALUES_NOCACHE=1 forces a
|
||||
* rebuild per call for tests that deliberately move a value node. */
|
||||
static EgValueRef _eg_values_cache;
|
||||
static int _eg_values_cached = 0;
|
||||
static const EgValueRef* eg_values_ref(void) {
|
||||
const char* nc = getenv("ENGRAM_VALUES_NOCACHE");
|
||||
if (nc && nc[0] && nc[0] != '0') {
|
||||
if (_eg_values_cached) { eg_values_release(&_eg_values_cache); _eg_values_cached = 0; }
|
||||
}
|
||||
if (!_eg_values_cached) {
|
||||
if (eg_values_build(&_eg_values_cache) <= 0) return NULL;
|
||||
_eg_values_cached = 1;
|
||||
}
|
||||
return &_eg_values_cache;
|
||||
}
|
||||
|
||||
/* Copy a node's embedding INTO THE DESCRIPTOR'S FRAME (never borrow — g->nodes is
|
||||
* realloc'd on append, so a borrowed EngramNode* dangles across any concurrent write).
|
||||
*
|
||||
* THE FRAME CONTRACT IS LOAD-BEARING AND WAS BEING VIOLATED. engram_verify.h states
|
||||
* it plainly: the claim point and the descriptor must share the same frame.
|
||||
* engram_geometry builds every descriptor over L2-NORMALIZED member embeddings —
|
||||
* `centroid` is the mean of normcopy()'d vectors and the principal axes are computed
|
||||
* against that — while the raw `n->emb` in the resident store is not necessarily unit.
|
||||
* Fitting a raw vector against a unit-frame descriptor puts the point at a radius the
|
||||
* ellipsoid was never fitted over, so the distance is dominated by the norm mismatch
|
||||
* and every fit score collapses toward zero. Normalizing here is idempotent when the
|
||||
* stored vector is already unit, so it can only help. */
|
||||
static float* eg_node_emb_copy(const char* id, int dim) {
|
||||
EngramStore* eg = engram_get();
|
||||
if (!eg || !id || dim <= 0) return NULL;
|
||||
int64_t idx = engram_find_node_index(id);
|
||||
if (idx < 0 || idx >= eg->node_count) return NULL;
|
||||
EngramNode* n = &eg->nodes[idx];
|
||||
if (!n->emb || n->emb_dim != dim) return NULL;
|
||||
float* p = malloc(sizeof(float) * (size_t)dim);
|
||||
if (!p) return NULL;
|
||||
double s = 0;
|
||||
for (int i = 0; i < dim; i++) s += (double)n->emb[i] * (double)n->emb[i];
|
||||
double nn = sqrt(s);
|
||||
if (nn > 1e-12) for (int i = 0; i < dim; i++) p[i] = (float)((double)n->emb[i] / nn);
|
||||
else memcpy(p, n->emb, sizeof(float) * (size_t)dim);
|
||||
return p;
|
||||
}
|
||||
/* Salience of a relation = the salience of its endpoints, reusing the fields the
|
||||
* runtime already keeps (node salience and working-memory weight). Consolidation
|
||||
* is gated by salience — that is why you remember the argument and not the
|
||||
* commute — and this deliberately reads existing state rather than introducing a
|
||||
* threshold of its own. */
|
||||
static double eg_node_salience(const char* id) {
|
||||
EngramStore* eg = engram_get();
|
||||
if (!eg || !id) return 0.0;
|
||||
int64_t idx = engram_find_node_index(id);
|
||||
if (idx < 0 || idx >= eg->node_count) return 0.0;
|
||||
EngramNode* n = &eg->nodes[idx];
|
||||
double s = n->salience;
|
||||
if (n->working_memory_weight > s) s = n->working_memory_weight;
|
||||
if (n->background_activation > s) s = n->background_activation;
|
||||
return s;
|
||||
}
|
||||
static double eg_vdot(const float* a, const float* b, int dim) {
|
||||
double s = 0; for (int i = 0; i < dim; i++) s += (double)a[i] * (double)b[i]; return s;
|
||||
}
|
||||
/* Signed projection of a unit gradient onto unit(target − x): positive means the
|
||||
* descent direction still points toward the target. Frame-independent, so it
|
||||
* stays comparable across versions even if the region's principal axes rotate —
|
||||
* which is what makes the DIRECTION_REVERSAL test meaningful over time. */
|
||||
static double eg_proj_toward(const float* dir, const float* x, const float* target, int dim) {
|
||||
double s = 0, n2 = 0;
|
||||
for (int i = 0; i < dim; i++) { double d = (double)target[i] - (double)x[i]; s += (double)dir[i] * d; n2 += d * d; }
|
||||
double n = sqrt(n2);
|
||||
return n > 1e-12 ? s / n : 0.0;
|
||||
}
|
||||
|
||||
/* RELATIONAL grounding of a point: the MIN fit over the thirteen value regions,
|
||||
* and the NAME of the value that binds. Min, not mean, because a mean lets strong
|
||||
* agreement with twelve values mask a violation of the thirteenth — which is the
|
||||
* mechanism of rationalization, not a scoring detail. */
|
||||
static int eg_relational_read(const float* x, const EgValueRef* V,
|
||||
double* out_score, int* out_idx, float** out_dir) {
|
||||
if (!x || !V || V->n <= 0) return -1;
|
||||
double worst = 2.0; int wi = -1;
|
||||
for (int i = 0; i < V->n; i++) {
|
||||
GeoFit f;
|
||||
if (cog_warped_fit(V->g[i], x, NULL, &f) != 0) continue;
|
||||
if (f.score < worst) { worst = f.score; wi = i; }
|
||||
}
|
||||
if (wi < 0) return -1;
|
||||
if (out_score) *out_score = worst;
|
||||
if (out_idx) *out_idx = wi;
|
||||
if (out_dir) {
|
||||
GeoGradient gr;
|
||||
if (engram_think(V->g[wi], x, NULL, &gr) == 0) {
|
||||
*out_dir = gr.direction; gr.direction = NULL; /* take ownership */
|
||||
engram_gradient_free(&gr);
|
||||
} else *out_dir = NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Find the live relation between two nodes, in either orientation, and return
|
||||
* the NEWEST recorded version of it. Returns 1 on hit. */
|
||||
static int eg_find_relation(const char* a, const char* b, StoreEdge* out, char* base_id, size_t cap) {
|
||||
if (!g_engram_store || !a || !b) return 0;
|
||||
for (int dir = 0; dir < 2; dir++) {
|
||||
const char* from = dir == 0 ? a : b;
|
||||
const char* to = dir == 0 ? b : a;
|
||||
StoreEdge* edges = NULL; size_t ne = 0;
|
||||
if (store_get_edges_from(g_engram_store, from, &edges, &ne) < 0) continue;
|
||||
for (size_t i = 0; i < ne; i++) {
|
||||
if (edges[i].tombstoned) continue;
|
||||
if (!edges[i].to_id || strcmp(edges[i].to_id, to) != 0) continue;
|
||||
if (edges[i].id && strchr(edges[i].id, '#')) continue; /* a version, not a root */
|
||||
snprintf(base_id, cap, "%s", edges[i].id ? edges[i].id : "");
|
||||
store_edges_free(edges, ne);
|
||||
if (cog_grounding_head(g_engram_store, base_id, out, 64) >= 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
store_edges_free(edges, ne);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The two-axis observation of one relation from the CURRENT geometry. Pure.
|
||||
* factual = how well the far endpoint sits in the near endpoint's neighbourhood
|
||||
* — a real correspondence measurement of the relation itself;
|
||||
* relational = min fit over the thirteen value regions, with the binding name;
|
||||
* cos_angle = the cosine between the two full-dimensional gradients. */
|
||||
typedef struct {
|
||||
int ok;
|
||||
double factual, relational, cos_angle, fac_proj, rel_proj;
|
||||
char binding[128];
|
||||
} EgObservation;
|
||||
|
||||
static int eg_observe_relation(const char* from_id, const char* to_id,
|
||||
const EgValueRef* V, EgObservation* out) {
|
||||
memset(out, 0, sizeof *out);
|
||||
GeoDescriptor* R = eg_geo_build_desc(from_id);
|
||||
if (!R) return -1;
|
||||
int dim = R->dim;
|
||||
float* x = eg_node_emb_copy(to_id, dim);
|
||||
if (!x) { engram_geo_free(R); return -1; }
|
||||
|
||||
GeoFit f;
|
||||
if (cog_warped_fit(R, x, NULL, &f) != 0) { free(x); engram_geo_free(R); return -1; }
|
||||
out->factual = f.score;
|
||||
|
||||
GeoGradient fg;
|
||||
float* fac_dir = NULL;
|
||||
if (engram_think(R, x, NULL, &fg) == 0) { fac_dir = fg.direction; fg.direction = NULL; engram_gradient_free(&fg); }
|
||||
|
||||
int vi = -1; float* rel_dir = NULL;
|
||||
if (eg_relational_read(x, V, &out->relational, &vi, &rel_dir) == 0 && vi >= 0)
|
||||
snprintf(out->binding, sizeof out->binding, "%s", V->ids[vi]);
|
||||
|
||||
if (fac_dir && R->centroid) out->fac_proj = eg_proj_toward(fac_dir, x, R->centroid, dim);
|
||||
if (rel_dir && vi >= 0 && V->g[vi]->centroid) out->rel_proj = eg_proj_toward(rel_dir, x, V->g[vi]->centroid, dim);
|
||||
if (fac_dir && rel_dir) out->cos_angle = eg_vdot(fac_dir, rel_dir, dim);
|
||||
|
||||
free(fac_dir); free(rel_dir); free(x); engram_geo_free(R);
|
||||
out->ok = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Fold an observation into the vector as it would stand now. The accrual is a
|
||||
* running MEAN over reinforcements — the same form the beat's calibration
|
||||
* already uses (brier_sum / n_trials) — so no rate constant is introduced. */
|
||||
static void eg_fold_observation(const CogGrounding* prev, const EgObservation* obs,
|
||||
CogProvClass prov, double floor, double rel_floor,
|
||||
int64_t now, CogGrounding* out) {
|
||||
*out = *prev;
|
||||
double n = prev->reinforcements;
|
||||
out->factual = prev->present ? prev->factual + (obs->factual - prev->factual) / (n + 1.0) : obs->factual;
|
||||
out->relational = prev->present ? prev->relational + (obs->relational - prev->relational) / (n + 1.0) : obs->relational;
|
||||
out->fac_proj = obs->fac_proj; out->rel_proj = obs->rel_proj; out->cos_angle = obs->cos_angle;
|
||||
out->agreement = obs->cos_angle > 0 ? 1 : (obs->cos_angle < 0 ? -1 : 0);
|
||||
out->reinforcements = n + 1.0;
|
||||
out->ts = now;
|
||||
if (prov != COG_PROV_UNSET) out->prov = prov;
|
||||
out->floor_at_record = floor; out->rel_floor_at_record = rel_floor;
|
||||
snprintf(out->binding_value, sizeof out->binding_value, "%s", obs->binding);
|
||||
/* DERIVED, recomputed at the instant — never carried over from prev. */
|
||||
out->age_ms = 0; out->decay = 1.0;
|
||||
out->factual_now = out->factual; out->relational_now = out->relational;
|
||||
out->associative_now = out->associative;
|
||||
out->stale = 0;
|
||||
}
|
||||
|
||||
static void eg_emit_vector(JsonBuf* b, const char* key, const CogGrounding* g) {
|
||||
char t[768];
|
||||
snprintf(t, sizeof t,
|
||||
"\"%s\":{\"established\":%s,"
|
||||
"\"factual\":%.6g,\"relational\":%.6g,\"associative\":%.6g,\"polarity\":%.6g,"
|
||||
"\"provenance\":\"%s\",\"ts\":%lld,\"seq\":%lld,\"reinforcements\":%.6g,"
|
||||
"\"cos_angle\":%.6g,\"agreement\":%d,\"fac_proj\":%.6g,\"rel_proj\":%.6g,"
|
||||
"\"binding_value\":\"%s\","
|
||||
"\"derived\":{\"age_s\":%lld,\"decay\":%.6g,\"factual_now\":%.6g,"
|
||||
"\"relational_now\":%.6g,\"associative_now\":%.6g,\"stale\":%s}}",
|
||||
key, g->present ? "true" : "false",
|
||||
g->factual, g->relational, g->associative, g->polarity,
|
||||
cog_prov_name(g->prov), (long long)g->ts, (long long)g->seq, g->reinforcements,
|
||||
g->cos_angle, g->agreement, g->fac_proj, g->rel_proj,
|
||||
g->binding_value[0] ? g->binding_value : "-",
|
||||
(long long)(g->age_ms / 1000), g->decay, g->factual_now,
|
||||
g->relational_now, g->associative_now, g->stale ? "true" : "false");
|
||||
jb_puts(b, t);
|
||||
}
|
||||
|
||||
/* engram_ground_json(claim, evidence, for_whom) — a READ. Never writes. */
|
||||
/* engram_ground_json(claim_csv, evidence_csv, for_whom) — grounding as a RELATION.
|
||||
* Turns the DORMANT verifier inward for real: verifies the claim region's centroid
|
||||
* against the evidence region, then writes a grounded-by edge (weight = grounding,
|
||||
* grounded-for-whom). Additive. */
|
||||
el_val_t engram_ground_json(el_val_t claim, el_val_t evidence, el_val_t for_whom) {
|
||||
if (!g_engram_store) return eg_geo_err("store unavailable");
|
||||
GeoDescriptor* C = eg_geo_build_desc(EL_CSTR(claim));
|
||||
GeoDescriptor* E = eg_geo_build_desc(EL_CSTR(evidence));
|
||||
if (!C || !E) { if (C) engram_geo_free(C); if (E) engram_geo_free(E); return eg_geo_err("geometry unavailable"); }
|
||||
const GeoDescriptor* ev[1] = { E };
|
||||
GeoGrounding gr;
|
||||
int rc = engram_verify_grounding(C->centroid, C->dim, ev, 1, 1.0, 0.5, &gr);
|
||||
double grounding = (rc == 0) ? gr.grounding : 0.0;
|
||||
if (rc == 0) engram_verify_grounding_free(&gr);
|
||||
const char* fw = EL_CSTR(for_whom); if (fw && !*fw) fw = NULL;
|
||||
|
||||
/* GROUND THE NODE ASKED ABOUT, AND SAY WHAT WAS RESOLVED (2026-08-16
|
||||
* self-review). This wrote the grounded-by edge between the two REGION
|
||||
* HUBS and then echoed those hubs back in the "claim"/"evidence" fields
|
||||
* as though they were the caller's input. Three consequences, all measured
|
||||
* against the live store:
|
||||
*
|
||||
* 1. The edge landed on a node the caller never named. Asking to ground
|
||||
* 3b9ced5d against 6edf8c79 wrote an edge on 6edf8c79 -> d0406dfd,
|
||||
* because those were the hubs of the two regions.
|
||||
* 2. When both seeds resolve into the same region, the hubs coincide and
|
||||
* the call grounds a node against ITSELF, returning grounding = 1 —
|
||||
* a perfect score with no evidence behind it. Two independent agents
|
||||
* hit this and reported 0.885 / 0.909 self-groundings as confident.
|
||||
* 3. The echo concealed both, because the response looked exactly like a
|
||||
* successful grounding of the ids that were passed in.
|
||||
*
|
||||
* The region is HOW a claim is evaluated; it is not WHAT the claim is
|
||||
* about. So the edge attaches to the requested ids, and the resolved hubs
|
||||
* are reported separately under claim_region / evidence_region. When the
|
||||
* two regions coincide, the grounding is degenerate by construction and is
|
||||
* reported as such rather than as a confident 1.0. */
|
||||
const char* cid = EL_CSTR(claim);
|
||||
const char* eid = EL_CSTR(evidence);
|
||||
const char* fw = EL_CSTR(for_whom); if (fw && !*fw) fw = NULL;
|
||||
int64_t now = engram_now_ms();
|
||||
const char* chub = C->hub_id ? C->hub_id : cid;
|
||||
const char* ehub = E->hub_id ? E->hub_id : eid;
|
||||
/* Degeneracy is broader than chub == ehub. Three circular shapes, each of
|
||||
* which yields a high score for structural reasons rather than evidential
|
||||
* ones, and all three were previously invisible:
|
||||
* same-region both seeds resolve to one region — grounding a thing
|
||||
* against itself.
|
||||
* claim-in-ev the claim's region hub IS the evidence node: the evidence
|
||||
* sits at the centre of the claim's own neighbourhood.
|
||||
* ev-in-claim the mirror case.
|
||||
* Measured: grounding 3b9ced5d against 6edf8c79 scored 0.98883 purely
|
||||
* because 6edf8c79 is the hub of 3b9ced5d's region. */
|
||||
const char* degenerate = NULL;
|
||||
if (chub && ehub && strcmp(chub, ehub) == 0) degenerate = "same-region";
|
||||
else if (chub && eid && strcmp(chub, eid) == 0) degenerate = "claim-region-is-evidence";
|
||||
else if (ehub && cid && strcmp(ehub, cid) == 0) degenerate = "evidence-region-is-claim";
|
||||
if (degenerate) grounding = 0.0; /* circular support is not support */
|
||||
|
||||
StoreEdge cur; char base_id[192] = "";
|
||||
JsonBuf b; jb_init(&b); char t[768];
|
||||
|
||||
if (!eg_find_relation(cid, eid, &cur, base_id, sizeof base_id)) {
|
||||
/* THE HONEST ANSWER. There is nothing to ground a claim "against" that is
|
||||
* not already a relation. This previously minted one and scored it — and
|
||||
* when both seeds fell in one region the score came back 1.0 with no
|
||||
* evidence behind it. If the two are unrelated, say so and write nothing. */
|
||||
snprintf(t, sizeof t,
|
||||
"{\"claim\":\"%s\",\"evidence\":\"%s\",\"for_whom\":\"%s\",\"related\":false,"
|
||||
"\"grounding\":null,\"written\":false,"
|
||||
"\"note\":\"no relation between these nodes; grounding is a property of a relation, not a score minted between nodes\"}",
|
||||
cid ? cid : "", eid ? eid : "", fw ? fw : "-");
|
||||
jb_puts(&b, t);
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
CogGrounding rec; cog_grounding_parse(&cur, now, &rec);
|
||||
|
||||
const EgValueRef* V = eg_values_ref();
|
||||
EgObservation obs; memset(&obs, 0, sizeof obs);
|
||||
int nv = V ? V->n : 0;
|
||||
if (nv > 0 && cur.from_id && cur.to_id) eg_observe_relation(cur.from_id, cur.to_id, V, &obs);
|
||||
|
||||
double floor = 0.5, rel_floor = 0.5;
|
||||
CogGrounding ng = rec;
|
||||
if (obs.ok) eg_fold_observation(&rec, &obs, COG_PROV_UNSET, floor, rel_floor, now, &ng);
|
||||
CogSignificance sig = obs.ok ? cog_grounding_significant(&rec, &ng, floor, rel_floor)
|
||||
: COG_SIG_NONE;
|
||||
double sal = cur.from_id && cur.to_id
|
||||
? (eg_node_salience(cur.from_id) > eg_node_salience(cur.to_id)
|
||||
? eg_node_salience(cur.from_id) : eg_node_salience(cur.to_id))
|
||||
: 0.0;
|
||||
|
||||
CogTrajectory tr; memset(&tr, 0, sizeof tr);
|
||||
cog_grounding_trajectory(g_engram_store, base_id, now, &tr);
|
||||
|
||||
jb_putc(&b, '{');
|
||||
snprintf(t, sizeof t,
|
||||
"\"claim\":\"%s\",\"evidence\":\"%s\",\"for_whom\":\"%s\",\"related\":true,"
|
||||
"\"relation\":\"%s\",\"edge\":\"%s\",\"edge_root\":\"%s\",\"from\":\"%s\",\"to\":\"%s\","
|
||||
"\"value_regions\":%d,\"aggregate\":\"min\",\"salience\":%.6g,",
|
||||
cid ? cid : "", eid ? eid : "", fw ? fw : "-",
|
||||
cur.relation ? cur.relation : "", cur.id ? cur.id : "", base_id,
|
||||
cur.from_id ? cur.from_id : "", cur.to_id ? cur.to_id : "", nv > 0 ? nv : 0, sal);
|
||||
/* Do not write an edge for a grounding that is degenerate by construction. */
|
||||
int wr = degenerate ? -1 : cog_ground_edge(g_engram_store, cid, eid, grounding, fw);
|
||||
JsonBuf b; jb_init(&b); char t[512];
|
||||
snprintf(t, sizeof t, "{\"relation\":\"grounded-by\",\"claim\":\"%s\",\"evidence\":\"%s\","
|
||||
"\"claim_region\":\"%s\",\"evidence_region\":\"%s\",\"degenerate\":%s%s%s,"
|
||||
"\"for_whom\":\"%s\",\"grounding\":%.6g,\"written\":%s}",
|
||||
cid ? cid : "", eid ? eid : "", chub ? chub : "", ehub ? ehub : "",
|
||||
degenerate ? "\"" : "false", degenerate ? degenerate : "", degenerate ? "\"" : "",
|
||||
fw ? fw : "-", grounding, wr == 0 ? "true" : "false");
|
||||
jb_puts(&b, t);
|
||||
eg_emit_vector(&b, "recorded", &rec);
|
||||
jb_putc(&b, ',');
|
||||
if (obs.ok) eg_emit_vector(&b, "observed", &ng);
|
||||
else jb_puts(&b, "\"observed\":null");
|
||||
/* Volatility and drift are computed here and stored nowhere — the series
|
||||
* exists only because nothing was destroyed. */
|
||||
snprintf(t, sizeof t,
|
||||
",\"trajectory\":{\"n_versions\":%d,\"factual_volatility\":%.6g,"
|
||||
"\"relational_volatility\":%.6g,\"factual_drift\":%.6g,\"relational_drift\":%.6g,"
|
||||
"\"stayed_true_became_wrong\":%s}"
|
||||
",\"would_record\":%s,\"significance\":\"%s\",\"inherent\":%s,\"written\":false}",
|
||||
tr.n_versions, tr.factual_volatility, tr.relational_volatility,
|
||||
tr.factual_drift, tr.relational_drift,
|
||||
tr.stayed_true_became_wrong ? "true" : "false",
|
||||
sig != COG_SIG_NONE ? "true" : "false", cog_significance_name(sig),
|
||||
cog_significance_inherent(sig) ? "true" : "false");
|
||||
jb_puts(&b, t);
|
||||
|
||||
store_edge_free(&cur);
|
||||
engram_geo_free(C); engram_geo_free(E);
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
/* engram_ground_record_json — the WRITE half, named as one. Recomputes the
|
||||
* vector, applies the consolidation gate (salience + per-dimension consequence),
|
||||
* and on significance supersedes the EDGE, versioning the WHOLE vector jointly.
|
||||
* The predecessor is never touched.
|
||||
*
|
||||
* There is no provenance predicate here and nothing takes its place. An earlier
|
||||
* pass built one — a bounded traversal over a grounding-chain graph, refusing
|
||||
* evidence downstream of the region being calibrated. It is deleted. Circularity
|
||||
* of the reference frame is TEMPORAL, not topological: you cannot recalibrate the
|
||||
* ruler while measuring with it, so that update happens when the frame is not
|
||||
* being used to act, which is a fact about engagement and belongs to the dreamer.
|
||||
* The measurement that settles it: reachability from the self region reaches
|
||||
* 89.2% of the live graph, so any topological predicate marks nearly all evidence
|
||||
* tainted and degenerates into the total block censorship started as. */
|
||||
el_val_t engram_ground_record_json(el_val_t claim, el_val_t evidence,
|
||||
el_val_t provenance, el_val_t floor_v) {
|
||||
if (!g_engram_store) return eg_geo_err("store unavailable");
|
||||
const char* cid = EL_CSTR(claim);
|
||||
const char* eid = EL_CSTR(evidence);
|
||||
double floor = atof(EL_CSTR(floor_v)); if (!(floor > 0)) floor = 0.5;
|
||||
double rel_floor = floor;
|
||||
CogProvClass prov = cog_prov_parse(EL_CSTR(provenance));
|
||||
int64_t now = engram_now_ms();
|
||||
|
||||
StoreEdge cur; char base_id[192] = "";
|
||||
JsonBuf b; jb_init(&b); char t[768];
|
||||
if (!eg_find_relation(cid, eid, &cur, base_id, sizeof base_id)) {
|
||||
snprintf(t, sizeof t, "{\"claim\":\"%s\",\"evidence\":\"%s\",\"related\":false,\"written\":false}",
|
||||
cid ? cid : "", eid ? eid : "");
|
||||
jb_puts(&b, t); return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
CogGrounding rec; cog_grounding_parse(&cur, now, &rec);
|
||||
const EgValueRef* V = eg_values_ref();
|
||||
EgObservation obs; memset(&obs, 0, sizeof obs);
|
||||
int nv = V ? V->n : 0;
|
||||
if (nv <= 0 || !cur.from_id || !cur.to_id ||
|
||||
eg_observe_relation(cur.from_id, cur.to_id, V, &obs) != 0 || !obs.ok) {
|
||||
store_edge_free(&cur);
|
||||
return eg_geo_err("geometry unavailable for this relation");
|
||||
}
|
||||
|
||||
CogGrounding ng;
|
||||
eg_fold_observation(&rec, &obs, prov, floor, rel_floor, now, &ng);
|
||||
|
||||
CogSignificance sig = cog_grounding_significant(&rec, &ng, floor, rel_floor);
|
||||
/* CONSOLIDATION GATE. Significance says the move would change a decision;
|
||||
* salience says it is worth making durable. The two INHERENT moves — a
|
||||
* polarity sign flip and a provenance class change — bypass salience because
|
||||
* they are discrete changes of state rather than drift. */
|
||||
double sal = eg_node_salience(cur.from_id) > eg_node_salience(cur.to_id)
|
||||
? eg_node_salience(cur.from_id) : eg_node_salience(cur.to_id);
|
||||
int salient = (sal > 0.0);
|
||||
int consolidate = (sig != COG_SIG_NONE) && (cog_significance_inherent(sig) || salient);
|
||||
|
||||
char written_id[224] = "";
|
||||
int seq = -1;
|
||||
if (consolidate) seq = cog_grounding_record(g_engram_store, &cur, &ng, written_id, sizeof written_id);
|
||||
|
||||
jb_putc(&b, '{');
|
||||
snprintf(t, sizeof t,
|
||||
"\"claim\":\"%s\",\"evidence\":\"%s\",\"edge\":\"%s\",\"edge_root\":\"%s\","
|
||||
"\"value_regions\":%d,\"aggregate\":\"min\",\"floor\":%.4g,\"rel_floor\":%.4g,"
|
||||
"\"salience\":%.6g,\"salient\":%s,",
|
||||
cid ? cid : "", eid ? eid : "", cur.id ? cur.id : "", base_id,
|
||||
nv, floor, rel_floor, sal, salient ? "true" : "false");
|
||||
jb_puts(&b, t);
|
||||
eg_emit_vector(&b, "previous", &rec); jb_putc(&b, ',');
|
||||
eg_emit_vector(&b, "observed", &ng);
|
||||
snprintf(t, sizeof t,
|
||||
",\"significance\":\"%s\",\"inherent\":%s,\"consolidated\":%s,"
|
||||
"\"written\":%s,\"version\":%d,\"version_id\":\"%s\"}",
|
||||
cog_significance_name(sig), cog_significance_inherent(sig) ? "true" : "false",
|
||||
consolidate ? "true" : "false",
|
||||
(consolidate && seq > 0) ? "true" : "false", seq > 0 ? seq : 0, written_id);
|
||||
jb_puts(&b, t);
|
||||
|
||||
store_edge_free(&cur);
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
/* engram_ground_trajectory_json(claim, evidence) — the supersession chain read as
|
||||
* a TIME SERIES OF VECTORS. Not only what the grounding is but which way it has
|
||||
* been moving and how fast — a derivative obtained for free from immutability,
|
||||
* because the points were never destroyed. */
|
||||
el_val_t engram_ground_trajectory_json(el_val_t claim, el_val_t evidence) {
|
||||
if (!g_engram_store) return eg_geo_err("store unavailable");
|
||||
StoreEdge cur; char base_id[192] = "";
|
||||
if (!eg_find_relation(EL_CSTR(claim), EL_CSTR(evidence), &cur, base_id, sizeof base_id))
|
||||
return eg_geo_err("no relation between these nodes");
|
||||
store_edge_free(&cur);
|
||||
int64_t now = engram_now_ms();
|
||||
JsonBuf b; jb_init(&b); char t[768];
|
||||
jb_puts(&b, "{\"edge_root\":\""); jb_puts(&b, base_id); jb_puts(&b, "\",\"versions\":[");
|
||||
int emitted = 0;
|
||||
for (int v = 0; v <= 64; v++) {
|
||||
char vid[224];
|
||||
if (v == 0) snprintf(vid, sizeof vid, "%s", base_id);
|
||||
else snprintf(vid, sizeof vid, "%s#%d", base_id, v);
|
||||
StoreEdge e;
|
||||
if (store_get_edge(g_engram_store, vid, &e) != 1) { if (v) break; else continue; }
|
||||
CogGrounding g; cog_grounding_parse(&e, now, &g);
|
||||
if (emitted) jb_putc(&b, ',');
|
||||
snprintf(t, sizeof t,
|
||||
"{\"version\":%d,\"id\":\"%s\",\"established\":%s,\"ts\":%lld,"
|
||||
"\"factual\":%.6g,\"relational\":%.6g,\"associative\":%.6g,\"polarity\":%.6g,"
|
||||
"\"provenance\":\"%s\",\"cos_angle\":%.6g,\"agreement\":%d,"
|
||||
"\"binding_value\":\"%s\",\"prev\":\"%s\","
|
||||
"\"derived\":{\"age_s\":%lld,\"decay\":%.6g,\"factual_now\":%.6g,\"stale\":%s}}",
|
||||
v, vid, g.present ? "true" : "false", (long long)g.ts,
|
||||
g.factual, g.relational, g.associative, g.polarity,
|
||||
cog_prov_name(g.prov), g.cos_angle, g.agreement,
|
||||
g.binding_value[0] ? g.binding_value : "-", g.prev_edge,
|
||||
(long long)(g.age_ms / 1000), g.decay, g.factual_now, g.stale ? "true" : "false");
|
||||
jb_puts(&b, t); emitted++;
|
||||
store_edge_free(&e);
|
||||
}
|
||||
CogTrajectory tr; memset(&tr, 0, sizeof tr);
|
||||
cog_grounding_trajectory(g_engram_store, base_id, now, &tr);
|
||||
snprintf(t, sizeof t,
|
||||
"],\"n_versions\":%d,\"derived\":{\"factual_volatility\":%.6g,"
|
||||
"\"relational_volatility\":%.6g,\"factual_drift\":%.6g,\"relational_drift\":%.6g,"
|
||||
"\"stayed_true_became_wrong\":%s}}",
|
||||
emitted, tr.factual_volatility, tr.relational_volatility,
|
||||
tr.factual_drift, tr.relational_drift,
|
||||
tr.stayed_true_became_wrong ? "true" : "false");
|
||||
jb_puts(&b, t);
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
/* engram_assert_json(claim_id, for_whom, floor, rel_floor) — the honesty floor as
|
||||
* a QUERY at ASSERTION time only (holding is never gated), now gating on BOTH
|
||||
* axes. A well-evidenced claim must not earn the right to be asserted regardless
|
||||
* of whether it means the right thing.
|
||||
*
|
||||
* `still_held` was a HARDCODED `true` in this format string — a temporal property
|
||||
* named in the API and answered without consulting anything, which is invariant
|
||||
* §8.1 violated in one literal. It is now derived: the claim's node is read and
|
||||
* the field reports whether the content is present and live. Holding remains
|
||||
* unconditional; what decays is the GROUNDING, and a relation whose decayed
|
||||
* grounding has fallen below its floor stops being assertable on its own,
|
||||
* without anyone having to remember to check. */
|
||||
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor, el_val_t rel_floor) {
|
||||
/* engram_assert_json(claim_id, for_whom, floor) — the honesty floor as a QUERY at
|
||||
* ASSERTION time only (holding is never gated). Reads the claim's grounded-by
|
||||
* edges (for the observer) and returns whether assertion is permitted. */
|
||||
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor) {
|
||||
if (!g_engram_store) return eg_geo_err("store unavailable");
|
||||
const char* fw = EL_CSTR(for_whom); if (fw && !*fw) fw = NULL;
|
||||
double fl = atof(EL_CSTR(floor)); if (!(fl > 0)) fl = 0.5;
|
||||
double rfl = atof(EL_CSTR(rel_floor)); if (!(rfl > 0)) rfl = fl;
|
||||
CogAssertion a;
|
||||
if (cog_assert_two_axis(g_engram_store, EL_CSTR(claim_id), fl, rfl, engram_now_ms(), &a) != 0)
|
||||
return eg_geo_err("assert failed");
|
||||
JsonBuf b; jb_init(&b); char t[640];
|
||||
snprintf(t, sizeof t,
|
||||
"{\"claim\":\"%s\",\"for_whom\":\"%s\",\"floor\":%.4g,\"rel_floor\":%.4g,"
|
||||
"\"may_assert\":%s,\"still_held\":%s,\"found\":%s,\"n_relations\":%d,"
|
||||
"\"factual\":%.6g,\"relational\":%.6g,\"relational_established\":%s,"
|
||||
"\"cos_angle\":%.6g,\"agreement\":%d,\"binding_value\":\"%s\",\"best_relation\":\"%s\","
|
||||
"\"refused_because\":\"%s\"}",
|
||||
EL_CSTR(claim_id), fw ? fw : "-", fl, rfl,
|
||||
a.may_assert ? "true" : "false", a.still_held ? "true" : "false",
|
||||
a.found ? "true" : "false", a.n_edges,
|
||||
a.factual, a.relational, a.relational_established ? "true" : "false",
|
||||
a.cos_angle, a.agreement,
|
||||
a.binding_value[0] ? a.binding_value : "-", a.best_edge,
|
||||
a.may_assert ? "-" :
|
||||
!a.found ? "no-relation" :
|
||||
!a.relational_established ? "relational-axis-never-established" :
|
||||
(a.factual < fl) ? "below-factual-floor" :
|
||||
(a.relational < rfl) ? "below-relational-floor" : "-");
|
||||
double fl = atof(EL_CSTR(floor)); if (!(fl > 0)) fl = 0.5;
|
||||
int gate = cog_assert_gate(g_engram_store, EL_CSTR(claim_id), fw, fl);
|
||||
JsonBuf b; jb_init(&b); char t[192];
|
||||
snprintf(t, sizeof t, "{\"claim\":\"%s\",\"for_whom\":\"%s\",\"floor\":%.4g,\"may_assert\":%s,\"still_held\":true}",
|
||||
EL_CSTR(claim_id), fw ? fw : "-", fl, gate == 1 ? "true" : "false");
|
||||
jb_puts(&b, t);
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
@@ -15090,22 +14679,15 @@ el_val_t engram_correspondence_beat_json(el_val_t seeds, el_val_t faculty, el_va
|
||||
JsonBuf b; jb_init(&b); char t[384];
|
||||
snprintf(t, sizeof t,
|
||||
"{\"faculty\":\"%s\",\"stance_id\":\"%s\",\"region_hub\":\"%s\",\"dim\":%d,\"n_axes\":%d,\"signal_axes\":%d,"
|
||||
/* `keystone_write_blocked` is GONE from this response. It reported that the
|
||||
* beat had refused to learn about the reference frame, and the measured
|
||||
* cost of that refusal was 0.00% brier reduction over n_trials 0 on the
|
||||
* keystone region — the loop never ran, so nothing about the self was ever
|
||||
* calibrated OR falsifiable. Nothing replaces the flag: non-circularity of
|
||||
* the reference frame is temporal, not a permission (spec §5.2). The
|
||||
* `keystone` field is retained as a label on the region, and it no longer
|
||||
* gates anything. */
|
||||
"\"resumed\":%s,\"keystone\":%s,\"probes\":%d,\"epochs\":%d,"
|
||||
"\"brier_before\":%.6g,\"brier_after\":%.6g,\"reduction_pct\":%.2f,"
|
||||
"\"reliability\":%.6g,\"n_trials\":%lld,\"stance_written\":%s}",
|
||||
"\"reliability\":%.6g,\"n_trials\":%lld,\"stance_written\":%s,\"keystone_write_blocked\":%s}",
|
||||
EL_CSTR(faculty), sid, g->hub_id ? g->hub_id : "region", dim, na, signal,
|
||||
resumed ? "true" : "false", st.keystone ? "true" : "false", NP, EP,
|
||||
brier_before, brier_after,
|
||||
brier_before > 0 ? 100.0 * (brier_before - brier_after) / brier_before : 0.0,
|
||||
st.reliability, (long long)st.n_trials, wrote == 0 ? "true" : "false");
|
||||
st.reliability, (long long)st.n_trials, wrote == 0 ? "true" : "false",
|
||||
st.keystone ? "true" : "false");
|
||||
jb_puts(&b, t);
|
||||
cog_stance_free(&st); engram_geo_free(g);
|
||||
return el_wrap_str(b.buf);
|
||||
|
||||
@@ -666,9 +666,11 @@ el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
|
||||
/* Largest byte length <= max_bytes that does not split a UTF-8 codepoint.
|
||||
* Bounded by bytes, not codepoints, so truncated strings never grow. */
|
||||
size_t el_utf8_safe_len(const char* s, size_t max_bytes);
|
||||
/* Register the ambient-consolidation step and start dreaming. Resolved by
|
||||
* dlsym, like http_set_handler. The handler performs ONE step and returns
|
||||
* non-zero if it did work; returning zero parks the dreamer until engagement
|
||||
* changes. There is no schedule and must never be one. */
|
||||
void dream_set_handler(el_val_t name);
|
||||
|
||||
el_val_t engram_node_count(void);
|
||||
/* Attach a Geometry to an existing node, and read the attached width back.
|
||||
@@ -732,13 +734,8 @@ el_val_t engram_geo_analogy_json(el_val_t a_seeds, el_val_t b_seeds);
|
||||
el_val_t engram_reason_analogy_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t c_seeds);
|
||||
/* COGNITION (2026-08-14): THE ONE OPERATION + grounding, surfaced live. */
|
||||
el_val_t engram_think_json(el_val_t seeds, el_val_t faculty);
|
||||
/* GROUNDING (2026-08-16): grounding is an attribute of the RELATION and it IS the
|
||||
* hebbian weight. ground reads; ground_record writes; trajectory reads the chain. */
|
||||
el_val_t engram_ground_json(el_val_t claim, el_val_t evidence, el_val_t for_whom);
|
||||
el_val_t engram_ground_record_json(el_val_t claim, el_val_t evidence,
|
||||
el_val_t provenance, el_val_t floor);
|
||||
el_val_t engram_ground_trajectory_json(el_val_t claim, el_val_t evidence);
|
||||
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor, el_val_t rel_floor);
|
||||
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor);
|
||||
el_val_t engram_attend_json(el_val_t node_id, el_val_t observer, el_val_t salience);
|
||||
el_val_t engram_correspondence_beat_json(el_val_t seeds, el_val_t faculty, el_val_t keystone);
|
||||
el_val_t engram_consolidate_permanence(el_val_t node_id);
|
||||
|
||||
+27
-370
@@ -246,6 +246,14 @@ static int put_edge(EngramPagedStore* s, const char* id, const char* from, const
|
||||
e.metadata = (char*)meta;
|
||||
return store_put_edge(s, &e);
|
||||
}
|
||||
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
|
||||
const char* evidence_id, double grounding, const char* for_whom) {
|
||||
if (!s || !claim_id || !evidence_id) return -1;
|
||||
char id[512], meta[256];
|
||||
snprintf(id, sizeof id, "gb-%s-%s-%s", claim_id, evidence_id, for_whom ? for_whom : "global");
|
||||
snprintf(meta, sizeof meta, "for_whom=%s", for_whom ? for_whom : "-");
|
||||
return put_edge(s, id, claim_id, evidence_id, COG_GROUNDED_BY_RELATION, grounding, meta);
|
||||
}
|
||||
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
|
||||
const char* observer_id, double salience) {
|
||||
if (!s || !node_id || !observer_id) return -1;
|
||||
@@ -253,386 +261,35 @@ int cog_salient_edge(EngramPagedStore* s, const char* node_id,
|
||||
snprintf(id, sizeof id, "st-%s-%s", node_id, observer_id);
|
||||
return put_edge(s, id, node_id, observer_id, COG_SALIENT_TO_RELATION, salience, NULL);
|
||||
}
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §7 GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR.
|
||||
* See engram_cognition.h §7 for the model and for the measurements the two
|
||||
* design decisions (thirteen regions, min aggregate) rest on.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── The one decay model. Moved here verbatim from el_runtime.c's
|
||||
* engram_temporal_decay so nodes and edges share a single implementation and a
|
||||
* single set of constants; engram_temporal_decay now delegates. Bit-identical
|
||||
* for nodes: reinforcements := activation_count, lambda_override :=
|
||||
* temporal_decay_rate.
|
||||
*
|
||||
* This is what makes decay ANALYTIC rather than sampled: between two recorded
|
||||
* versions the trajectory is not unknown, it is known in closed form from the
|
||||
* last point and elapsed time. Store the point, read the curve. */
|
||||
double cog_decay_factor(int64_t age_ms, double reinforcements, double lambda_override) {
|
||||
if (age_ms <= 0) return 1.0;
|
||||
double lambda = (lambda_override > 0.0) ? lambda_override : COG_DECAY_LAMBDA;
|
||||
double age_hours = (double)age_ms / 3600000.0;
|
||||
if (reinforcements < 0) reinforcements = 0;
|
||||
double t_half = COG_T_HALF_HOURS * (1.0 + log(1.0 + reinforcements));
|
||||
double factor = exp(-lambda * age_hours / t_half);
|
||||
if (factor < COG_DECAY_FLOOR) factor = COG_DECAY_FLOOR;
|
||||
return factor;
|
||||
}
|
||||
|
||||
const char* cog_prov_name(CogProvClass p) {
|
||||
switch (p) {
|
||||
case COG_PROV_OBSERVED: return "observed";
|
||||
case COG_PROV_INFERRED: return "inferred";
|
||||
case COG_PROV_TOLD: return "told";
|
||||
case COG_PROV_IMPRINTED: return "imprinted";
|
||||
default: return "unset";
|
||||
}
|
||||
}
|
||||
CogProvClass cog_prov_parse(const char* s) {
|
||||
if (!s) return COG_PROV_UNSET;
|
||||
if (!strcmp(s, "observed")) return COG_PROV_OBSERVED;
|
||||
if (!strcmp(s, "inferred")) return COG_PROV_INFERRED;
|
||||
if (!strcmp(s, "told")) return COG_PROV_TOLD;
|
||||
if (!strcmp(s, "imprinted")) return COG_PROV_IMPRINTED;
|
||||
return COG_PROV_UNSET;
|
||||
}
|
||||
|
||||
/* Locate the GRD1 block in an edge's metadata. It is always the tail; anything
|
||||
* ahead of it is the edge's pre-existing metadata, preserved verbatim. */
|
||||
static const char* cog_grd_find(const char* meta) {
|
||||
if (!meta) return NULL;
|
||||
size_t ml = strlen(COG_GROUNDING_META_MAGIC);
|
||||
if (strncmp(meta, COG_GROUNDING_META_MAGIC, ml) == 0) return meta;
|
||||
const char* p = meta;
|
||||
while ((p = strstr(p, COG_GROUNDING_META_MAGIC)) != NULL) {
|
||||
if (p > meta && p[-1] == '\n') return p;
|
||||
p += ml;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int cog_grounding_parse(const StoreEdge* e, int64_t now_ms, CogGrounding* out) {
|
||||
if (!e || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
|
||||
/* Two dimensions exist on every edge whether or not grounding has ever been
|
||||
* established, because they ARE existing substrate rather than new fields:
|
||||
* associative — the accrued hebb, with its existing dynamics;
|
||||
* polarity — the signed authored weight. `inhibitory` is precisely this
|
||||
* distinction crushed to one bit, so it is the seed sign. */
|
||||
out->associative = e->hebb;
|
||||
out->polarity = e->inhibitory ? -e->weight : e->weight;
|
||||
out->prov = COG_PROV_UNSET;
|
||||
out->ts = e->last_fired > 0 ? e->last_fired : e->updated_at;
|
||||
|
||||
const char* blk = cog_grd_find(e->metadata);
|
||||
if (blk) {
|
||||
out->present = 1;
|
||||
char* copy = dupstr(blk);
|
||||
if (!copy) return -1;
|
||||
for (char* line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) {
|
||||
if (line[0] == '\0') continue;
|
||||
char tag = line[0];
|
||||
const char* rest = line + 1; while (*rest == ' ') rest++;
|
||||
if (tag == 'w') { /* the four numeric dimensions */
|
||||
double v[4] = {0,0,0,0}; parse_floats(rest, v, 4);
|
||||
out->factual = v[0]; out->relational = v[1];
|
||||
out->associative = v[2]; out->polarity = v[3];
|
||||
} else if (tag == 'k') { /* provenance class */
|
||||
out->prov = cog_prov_parse(rest);
|
||||
} else if (tag == 't') { /* timestamp + seq + reinforcements */
|
||||
double v[3] = {0,0,0}; parse_floats(rest, v, 3);
|
||||
out->ts = (int64_t)v[0]; out->seq = (int64_t)v[1]; out->reinforcements = v[2];
|
||||
} else if (tag == 'd') {
|
||||
double v[3] = {0,0,0}; parse_floats(rest, v, 3);
|
||||
out->fac_proj = v[0]; out->rel_proj = v[1]; out->cos_angle = v[2];
|
||||
} else if (tag == 'v') {
|
||||
snprintf(out->binding_value, sizeof out->binding_value, "%s", rest);
|
||||
} else if (tag == 'c') {
|
||||
double v[2] = {0,0}; parse_floats(rest, v, 2);
|
||||
out->floor_at_record = v[0]; out->rel_floor_at_record = v[1];
|
||||
} else if (tag == 'p') {
|
||||
snprintf(out->prev_edge, sizeof out->prev_edge, "%s", rest);
|
||||
}
|
||||
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
|
||||
const char* for_whom, double floor) {
|
||||
if (!s || !claim_id) return -1;
|
||||
if (!(floor > 0)) floor = 0.5;
|
||||
StoreEdge* edges = NULL; size_t n = 0;
|
||||
if (store_get_edges_from(s, claim_id, &edges, &n) < 0) return -1;
|
||||
double best = 0.0; int found = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!edges[i].relation || strcmp(edges[i].relation, COG_GROUNDED_BY_RELATION) != 0) continue;
|
||||
/* grounded-for-whom: match observer if requested; global (for_whom=-) always counts */
|
||||
int match = 1;
|
||||
if (for_whom && edges[i].metadata) {
|
||||
const char* fw = strstr(edges[i].metadata, "for_whom=");
|
||||
if (fw) { fw += 9; if (strcmp(fw, for_whom) != 0 && strcmp(fw, "-") != 0) match = 0; }
|
||||
}
|
||||
free(copy);
|
||||
if (match) { found = 1; if (edges[i].weight > best) best = edges[i].weight; }
|
||||
}
|
||||
out->agreement = (out->cos_angle > 0) ? 1 : (out->cos_angle < 0 ? -1 : 0);
|
||||
|
||||
/* ── DERIVED. Nothing below this line is ever serialized. Recency, decay and
|
||||
* staleness are read off the curve; storing them is how a number ends up
|
||||
* asserting something nothing computed (§8.1 / spec §2). */
|
||||
out->age_ms = (out->ts > 0 && now_ms > out->ts) ? (now_ms - out->ts) : 0;
|
||||
out->decay = cog_decay_factor(out->age_ms, out->reinforcements, 0.0);
|
||||
out->factual_now = out->factual * out->decay;
|
||||
out->relational_now = out->relational * out->decay;
|
||||
out->associative_now = out->associative * out->decay;
|
||||
out->stale = (out->present && out->floor_at_record > 0 &&
|
||||
out->factual_now < out->floor_at_record) ? 1 : 0;
|
||||
return 0;
|
||||
store_edges_free(edges, n);
|
||||
if (!found) return 0; /* ungrounded => refuse assertion (still held) */
|
||||
return (best >= floor) ? 1 : 0;
|
||||
}
|
||||
|
||||
char* cog_grounding_metadata(const char* base_meta, const CogGrounding* g) {
|
||||
if (!g) return NULL;
|
||||
size_t keep = 0;
|
||||
if (base_meta) {
|
||||
const char* blk = cog_grd_find(base_meta);
|
||||
keep = blk ? (size_t)(blk - base_meta) : strlen(base_meta);
|
||||
while (keep > 0 && base_meta[keep - 1] == '\n') keep--;
|
||||
}
|
||||
size_t cap = keep + 1024;
|
||||
char* buf = malloc(cap); if (!buf) return NULL;
|
||||
size_t o = 0;
|
||||
if (keep) { memcpy(buf, base_meta, keep); o = keep; buf[o++] = '\n'; }
|
||||
o += (size_t)snprintf(buf + o, cap - o, "%s\n", COG_GROUNDING_META_MAGIC);
|
||||
/* STORED ONLY. factual / relational / associative / polarity / provenance /
|
||||
* timestamp — plus the joint state a decision saw. No confidence, no
|
||||
* recency, no staleness, no volatility: those are read off the curve. */
|
||||
o += (size_t)snprintf(buf + o, cap - o, "w %.9g %.9g %.9g %.9g\n",
|
||||
g->factual, g->relational, g->associative, g->polarity);
|
||||
o += (size_t)snprintf(buf + o, cap - o, "k %s\n", cog_prov_name(g->prov));
|
||||
o += (size_t)snprintf(buf + o, cap - o, "t %lld %lld %.9g\n",
|
||||
(long long)g->ts, (long long)g->seq, g->reinforcements);
|
||||
o += (size_t)snprintf(buf + o, cap - o, "d %.9g %.9g %.9g\n",
|
||||
g->fac_proj, g->rel_proj, g->cos_angle);
|
||||
o += (size_t)snprintf(buf + o, cap - o, "v %s\n", g->binding_value[0] ? g->binding_value : "-");
|
||||
o += (size_t)snprintf(buf + o, cap - o, "c %.9g %.9g\n", g->floor_at_record, g->rel_floor_at_record);
|
||||
if (g->prev_edge[0]) o += (size_t)snprintf(buf + o, cap - o, "p %s\n", g->prev_edge);
|
||||
(void)o;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ── Consequence, not epsilon. Every test is a floor crossing or a sign change,
|
||||
* both exact. Ordered so the two INHERENT (discrete) moves are reported in
|
||||
* preference to the graded ones, because they bypass the salience gate. */
|
||||
CogSignificance cog_grounding_significant(const CogGrounding* prev,
|
||||
const CogGrounding* now,
|
||||
double floor, double rel_floor) {
|
||||
if (!now) return COG_SIG_NONE;
|
||||
if (!prev || !prev->present) return COG_SIG_FIRST_RECORD;
|
||||
|
||||
/* INHERENT 1 — polarity sign flip. Ignorance and disagreement are different
|
||||
* states, and support → contradiction is a change of state rather than a
|
||||
* drift, so no threshold applies. Comparing signs, with zero its own class. */
|
||||
{
|
||||
int sp = prev->polarity > 0 ? 1 : (prev->polarity < 0 ? -1 : 0);
|
||||
int sn = now->polarity > 0 ? 1 : (now->polarity < 0 ? -1 : 0);
|
||||
if (sp != sn) return COG_SIG_POLARITY_FLIP;
|
||||
}
|
||||
/* INHERENT 2 — provenance class change. told → observed is a categorical
|
||||
* upgrade in what the relation is entitled to, not a movement along an axis. */
|
||||
if (prev->prov != now->prov) return COG_SIG_PROVENANCE_CHANGE;
|
||||
|
||||
/* Crossing an assert floor — the move changes whether this relation can be
|
||||
* spoken. Compared on the DECAYED values, because that is what the gate reads. */
|
||||
if ((prev->factual_now >= floor) != (now->factual_now >= floor)) return COG_SIG_FACTUAL_FLOOR;
|
||||
if ((prev->relational_now >= rel_floor) != (now->relational_now >= rel_floor)) return COG_SIG_RELATIONAL_FLOOR;
|
||||
|
||||
/* Flipping factual/relational agreement — the relation stops being "true and
|
||||
* meaningful" and becomes "true and misapplied", or the reverse. This is the
|
||||
* 911/CPS contradiction as a measured event rather than a reviewable one. */
|
||||
if (prev->agreement != now->agreement) return COG_SIG_AGREEMENT_FLIP;
|
||||
|
||||
/* A gradient reversing — the evidence stopped pulling the claim toward it and
|
||||
* began pushing it away, or the same on the values axis. */
|
||||
if ((prev->fac_proj > 0) != (now->fac_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;
|
||||
if ((prev->rel_proj > 0) != (now->rel_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;
|
||||
|
||||
return COG_SIG_NONE;
|
||||
}
|
||||
|
||||
int cog_significance_inherent(CogSignificance s) {
|
||||
return (s == COG_SIG_FIRST_RECORD || s == COG_SIG_POLARITY_FLIP ||
|
||||
s == COG_SIG_PROVENANCE_CHANGE) ? 1 : 0;
|
||||
}
|
||||
|
||||
const char* cog_significance_name(CogSignificance s) {
|
||||
switch (s) {
|
||||
case COG_SIG_FIRST_RECORD: return "first-record";
|
||||
case COG_SIG_POLARITY_FLIP: return "polarity-sign-flip";
|
||||
case COG_SIG_PROVENANCE_CHANGE: return "provenance-class-change";
|
||||
case COG_SIG_FACTUAL_FLOOR: return "factual-floor-crossed";
|
||||
case COG_SIG_RELATIONAL_FLOOR: return "relational-floor-crossed";
|
||||
case COG_SIG_AGREEMENT_FLIP: return "agreement-sign-flip";
|
||||
case COG_SIG_DIRECTION_REVERSAL: return "gradient-direction-reversal";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Recording: a NEW edge record. The predecessor is never touched. ────────── */
|
||||
int cog_grounding_record(EngramPagedStore* s, const StoreEdge* base,
|
||||
const CogGrounding* g, char* out_id, size_t out_id_cap) {
|
||||
if (!s || !base || !base->id || !g) return -1;
|
||||
char root[192];
|
||||
snprintf(root, sizeof root, "%s", base->id);
|
||||
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
|
||||
|
||||
int seq = (int)g->seq + 1;
|
||||
char vid[224];
|
||||
snprintf(vid, sizeof vid, "%s#%d", root, seq);
|
||||
|
||||
CogGrounding rec = *g;
|
||||
rec.seq = seq;
|
||||
snprintf(rec.prev_edge, sizeof rec.prev_edge, "%s", base->id);
|
||||
|
||||
char* meta = cog_grounding_metadata(base->metadata, &rec);
|
||||
if (!meta) return -1;
|
||||
|
||||
StoreEdge e; memset(&e, 0, sizeof e);
|
||||
e.id = vid; e.from_id = base->from_id; e.to_id = base->to_id;
|
||||
e.relation = base->relation; e.metadata = meta;
|
||||
/* The vector IS the weight, so the scalar fields carry their dimensions:
|
||||
* `weight` the magnitude of polarity, `inhibitory` its sign, `hebb` the
|
||||
* associative strength. Nothing here is a second copy of a derived value. */
|
||||
e.weight = rec.polarity < 0 ? -rec.polarity : rec.polarity;
|
||||
e.inhibitory = rec.polarity < 0 ? 1 : 0;
|
||||
e.hebb = rec.associative;
|
||||
e.confidence = base->confidence;
|
||||
e.created_at = base->created_at;
|
||||
e.updated_at = rec.ts;
|
||||
e.last_fired = rec.ts;
|
||||
e.layer_id = base->layer_id;
|
||||
int rc = store_put_edge(s, &e);
|
||||
free(meta);
|
||||
if (rc != 0) return -1;
|
||||
if (out_id && out_id_cap) snprintf(out_id, out_id_cap, "%s", vid);
|
||||
return seq;
|
||||
}
|
||||
|
||||
int cog_grounding_head(EngramPagedStore* s, const char* base_id,
|
||||
StoreEdge* out, int max_versions) {
|
||||
if (!s || !base_id || !out) return -1;
|
||||
if (max_versions <= 0) max_versions = 64;
|
||||
char root[192]; snprintf(root, sizeof root, "%s", base_id);
|
||||
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
|
||||
|
||||
StoreEdge cur; memset(&cur, 0, sizeof cur);
|
||||
if (store_get_edge(s, root, &cur) != 1) return -1;
|
||||
int found = 0;
|
||||
for (int v = 1; v <= max_versions; v++) {
|
||||
char vid[224]; snprintf(vid, sizeof vid, "%s#%d", root, v);
|
||||
StoreEdge nx;
|
||||
if (store_get_edge(s, vid, &nx) != 1) break;
|
||||
store_edge_free(&cur); cur = nx; found = v;
|
||||
}
|
||||
*out = cur;
|
||||
return found;
|
||||
}
|
||||
|
||||
/* ── VOLATILITY AND DRIFT: derived from the chain, stored nowhere. The series
|
||||
* exists only because nothing was destroyed, which is the whole return on
|
||||
* immutability — a derivative for free. */
|
||||
int cog_grounding_trajectory(EngramPagedStore* s, const char* base_id,
|
||||
int64_t now_ms, CogTrajectory* out) {
|
||||
if (!s || !base_id || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
char root[192]; snprintf(root, sizeof root, "%s", base_id);
|
||||
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
|
||||
|
||||
double pf = 0, pr = 0, f0 = 0, r0 = 0, fN = 0, rN = 0;
|
||||
double sum_df = 0, sum_dr = 0;
|
||||
int n = 0;
|
||||
for (int v = 0; v <= 64; v++) {
|
||||
char vid[224];
|
||||
if (v == 0) snprintf(vid, sizeof vid, "%s", root);
|
||||
else snprintf(vid, sizeof vid, "%s#%d", root, v);
|
||||
StoreEdge e;
|
||||
if (store_get_edge(s, vid, &e) != 1) { if (v) break; else continue; }
|
||||
CogGrounding g;
|
||||
if (cog_grounding_parse(&e, now_ms, &g) == 0) {
|
||||
if (n == 0) { f0 = g.factual; r0 = g.relational; }
|
||||
else { sum_df += fabs(g.factual - pf); sum_dr += fabs(g.relational - pr); }
|
||||
pf = g.factual; pr = g.relational; fN = pf; rN = pr;
|
||||
n++;
|
||||
}
|
||||
store_edge_free(&e);
|
||||
}
|
||||
out->n_versions = n;
|
||||
if (n > 1) {
|
||||
out->factual_volatility = sum_df / (double)(n - 1);
|
||||
out->relational_volatility = sum_dr / (double)(n - 1);
|
||||
}
|
||||
out->factual_drift = fN - f0;
|
||||
out->relational_drift = rN - r0;
|
||||
/* "STAYED TRUE, BECAME WRONG" — the event the joint record makes visible and
|
||||
* that per-dimension versioning would have destroyed: the fact held while
|
||||
* the meaning degraded. Expressed as signs, so there is no tolerance here
|
||||
* either: factual did not fall, relational did. */
|
||||
out->stayed_true_became_wrong =
|
||||
(n > 1 && out->factual_drift >= 0 && out->relational_drift < 0) ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Assertion gates on BOTH floors. Traversal is untouched: activation still
|
||||
* conducts on the factual/associative side, so a relation can remain thinkable
|
||||
* while ceasing to be assertable. That gap is where the wide angles live. ──── */
|
||||
int cog_assert_two_axis(EngramPagedStore* s, const char* claim_id,
|
||||
double floor, double rel_floor, int64_t now_ms,
|
||||
CogAssertion* out) {
|
||||
if (!s || !claim_id || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
if (!(floor > 0)) floor = 0.5;
|
||||
if (!(rel_floor > 0)) rel_floor = floor;
|
||||
|
||||
/* still_held is DERIVED, not a literal (§8.1). Holding is unconditional —
|
||||
* the store gates nothing — so the question the field actually answers is
|
||||
* whether the content is present and live. */
|
||||
StoreNode n;
|
||||
if (store_get_node(s, claim_id, &n) == 1) { out->still_held = !n.tombstoned; store_node_free(&n); }
|
||||
else out->still_held = 0;
|
||||
|
||||
double best = -1.0;
|
||||
for (int dir = 0; dir < 2; dir++) {
|
||||
StoreEdge* edges = NULL; size_t ne = 0;
|
||||
int rc = dir == 0 ? store_get_edges_from(s, claim_id, &edges, &ne)
|
||||
: store_get_edges_to (s, claim_id, &edges, &ne);
|
||||
if (rc < 0) continue;
|
||||
for (size_t i = 0; i < ne; i++) {
|
||||
if (edges[i].tombstoned) continue;
|
||||
CogGrounding g;
|
||||
if (cog_grounding_parse(&edges[i], now_ms, &g) != 0) continue;
|
||||
out->n_edges++;
|
||||
out->found = 1;
|
||||
if (g.factual_now > best) {
|
||||
best = g.factual_now;
|
||||
out->factual = g.factual_now;
|
||||
out->relational = g.relational_now; /* the SAME edge, not a max */
|
||||
out->polarity = g.polarity;
|
||||
out->cos_angle = g.cos_angle;
|
||||
out->agreement = g.agreement;
|
||||
out->prov = g.prov;
|
||||
out->relational_established = g.present;
|
||||
snprintf(out->best_edge, sizeof out->best_edge, "%s", edges[i].id ? edges[i].id : "");
|
||||
snprintf(out->binding_value, sizeof out->binding_value, "%s", g.binding_value);
|
||||
}
|
||||
}
|
||||
store_edges_free(edges, ne);
|
||||
}
|
||||
/* BOTH floors, and an unestablished relational axis does NOT pass by default
|
||||
* — defaulting it to passing is the exemption §0 forbids. A negative polarity
|
||||
* is a relation that actively contradicts and can never license assertion. */
|
||||
out->may_assert = (out->found && out->relational_established &&
|
||||
out->polarity > 0 &&
|
||||
out->factual >= floor && out->relational >= rel_floor) ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════════ THE CORRESPONDENCE-LOOP ═════ */
|
||||
int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
|
||||
double outcome_y, CogStance* stance,
|
||||
int learn, double max_step, CogBeatResult* out) {
|
||||
if (!region || !stance || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
/* 2026-08-16: the keystone block is GONE. It refused to learn about the
|
||||
* reference frame, which does not make it a good reference — it makes it
|
||||
* unexaminable, trading circular calibration for an ungroundable one (spec
|
||||
* §2). Measured cost of the block: on the keystone region the beat reported
|
||||
* 0.00% brier reduction over n_trials 0 — it never ran, so nothing about the
|
||||
* self was ever calibrated OR falsifiable. What replaces it is a provenance
|
||||
* constraint, not a permission: cog_grounding_downstream refuses evidence
|
||||
* that is downstream of the region being calibrated, for every region alike.
|
||||
* `wrote_keystone` is retained as a reporting field only and is always 0. */
|
||||
if (stance->keystone) { learn = 0; out->wrote_keystone = 1; } /* §6: never write a keystone */
|
||||
|
||||
GeoGradient g;
|
||||
if (engram_think(region, anchor, stance, &g) != 0) return -1; /* PREDICTION */
|
||||
|
||||
+17
-269
@@ -23,7 +23,7 @@
|
||||
* and a region, and grounded-for-whom.
|
||||
*
|
||||
* PURE + (mostly) READ-ONLY, stdlib + libm only. think() and the warp are pure
|
||||
* over their inputs. Persistence (Stance <-> StoreNode, edge grounding vectors) is the
|
||||
* over their inputs. Persistence (Stance <-> StoreNode, grounded-by edges) is the
|
||||
* only part that touches the store, and it is additive / supersede / tombstone —
|
||||
* never mutate-in-place, never delete. It NEVER touches the live daemon: all
|
||||
* offline against a scratch store, per the design's rails.
|
||||
@@ -152,25 +152,30 @@ int engram_express(const GeoGradient* g, const float* anchor, float* out_point);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §5 HOLD vs GROUND vs ASSERT. Holding is unconditional (the store gates nothing).
|
||||
* Grounding is an ATTRIBUTE OF a relation — carried on the edge itself, as a
|
||||
* vector (§7). The honesty floor is checked only at ASSERTION, on both axes.
|
||||
* Grounding is a RELATION — a "grounded-by" edge, probabilistic, grounded-for-whom.
|
||||
* The honesty floor is checked only at ASSERTION.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
/* DELETED 2026-08-16: COG_GROUNDED_BY_RELATION and cog_ground_edge.
|
||||
*
|
||||
* A "grounded-by" edge models grounding as a relation BETWEEN two nodes. It is a
|
||||
* property OF a relation — and it is that relation's weight. Minting a new edge
|
||||
* to carry a score was the error; #147 corrected which endpoints the edge landed
|
||||
* on and left the wrong idea standing. There is nothing to ground a claim
|
||||
* "against" that is not already an edge, and if no edge exists the honest answer
|
||||
* is that the two are not related — not a freshly minted one scoring 0.98.
|
||||
* See §7 for what replaced it. */
|
||||
#define COG_GROUNDED_BY_RELATION "grounded-by"
|
||||
#define COG_SALIENT_TO_RELATION "salient-to"
|
||||
|
||||
/* Write a grounded-by edge (additive). weight = grounding ∈(0,1] from the verifier;
|
||||
* for_whom recorded in edge metadata (grounding is relational). Never a node flag. */
|
||||
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
|
||||
const char* evidence_id, double grounding, const char* for_whom);
|
||||
|
||||
/* Write/refresh a salient-to edge: salience is RELATIONAL (grounded-for-whom),
|
||||
* carried on the edge to the observer — not baked into the node scalar (§2.1). */
|
||||
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
|
||||
const char* observer_id, double salience);
|
||||
|
||||
/* The honesty floor — a QUERY at assertion time, NOT a schema constraint. Reads the
|
||||
* claim's stored grounded-by edges (for the given observer) and returns:
|
||||
* 1 = may assert (best grounding >= floor),
|
||||
* 0 = REFUSE assertion (holds unconditionally; only asserting is gated),
|
||||
* <0 = error. The content remains held either way. */
|
||||
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
|
||||
const char* for_whom, double floor);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §4 THE REFLEXIVE CORRESPONDENCE-LOOP — the learning engine. think scores its
|
||||
* OWN gradient against outcome, refines the stance on the error, and (optionally)
|
||||
@@ -204,265 +209,8 @@ int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §6 METASTABILITY. Keystones (self/values) are read-mostly: the loop reads but
|
||||
* never writes them. Mark by stance flag or by a keystone-id set the loop consults.
|
||||
*
|
||||
* SUPERSEDED BY §7's PROVENANCE CONSTRAINT (2026-08-16). The keystone flag is a
|
||||
* PERMISSION: it asks who the target is, not where the evidence came from. That
|
||||
* is censorship, and it costs the ability to ever ground the self (spec
|
||||
* correspondence-and-censorship.md §0/§2). The constraint that actually protects
|
||||
* a reference frame is cog_grounding_downstream: a region may not be calibrated
|
||||
* by evidence downstream of itself. These declarations remain only so existing
|
||||
* call sites keep compiling; nothing in the grounding path consults them.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
typedef struct { const char** ids; int n; } CogKeystoneSet;
|
||||
int cog_is_keystone(const CogKeystoneSet* ks, const CogStance* s);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §7 GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR
|
||||
* (2026-08-16; spec correspondence-and-censorship.md §2–§6 @ 2b7e4ba.)
|
||||
*
|
||||
* THE MODEL. Grounding is not a subsystem, a score, or a relation BETWEEN nodes.
|
||||
* It is an attribute OF a relation. The graph already IS the grounding structure:
|
||||
* every edge is a grounded relation, and what that relation is worth is carried
|
||||
* on the edge itself. Three things follow, and each DELETES rather than adds:
|
||||
*
|
||||
* 1. `grounded-by` as a relation type does not exist, and cog_ground_edge is
|
||||
* gone. Minting an edge to hold a score models grounding as a relation
|
||||
* between nodes when it is a property of a relation. #147 corrected which
|
||||
* endpoints that edge landed on and left the wrong idea standing.
|
||||
* 2. There is no observer, and no sampling rate. Change is not a consequence of
|
||||
* use — it IS use, the way potentiation is the firing rather than something
|
||||
* that reads the firing and writes a weight. So no supervisor compares a
|
||||
* value to a threshold and decides to persist.
|
||||
* 3. Between two recorded versions the trajectory is not unknown. Decay is a
|
||||
* pure function of the last recorded point and elapsed time, so it is
|
||||
* ANALYTIC: store the point, read the curve.
|
||||
*
|
||||
* WHAT IS *NOT* HERE, DELIBERATELY. An earlier draft of the spec posed "a graph
|
||||
* predicate for evidence downstream of itself" as the hard problem, and this file
|
||||
* briefly contained one. It is withdrawn. Non-circularity is TEMPORAL, not
|
||||
* topological: you cannot recalibrate the ruler while measuring with it, so you
|
||||
* do it when you are not using the frame to act. Reachability could never have
|
||||
* worked — measured on the live store, reachability from the self region over
|
||||
* all relations reaches 89.2% of the graph (10,580 of 11,861 nodes) and 16.0%
|
||||
* over hebbian/semantic relations alone, so the predicate marks essentially all
|
||||
* evidence tainted and the constraint degenerates into the total block that
|
||||
* censorship started as. Nothing replaces it here; the independence is a fact
|
||||
* about engagement, owned by the dreamer, not a fact about the graph.
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §7.1 THE VECTOR
|
||||
*
|
||||
* The test for a real dimension is whether it can move independently of the
|
||||
* others. Five can, and each maps onto substrate that already exists:
|
||||
*
|
||||
* factual correspondence with evidence. [GRD1]
|
||||
* relational correspondence with values — min over THIRTEEN
|
||||
* value regions, carrying the binding value's NAME. [GRD1]
|
||||
* associative co-activation frequency. This is the edge's `hebb`
|
||||
* field with its existing dynamics — NOT a new one.
|
||||
* Independent by construction: every superstition is
|
||||
* a strong association with no factual grounding.
|
||||
* polarity SIGNED. Near zero means "no support"; NEGATIVE means
|
||||
* "this actively contradicts". The edge's `inhibitory`
|
||||
* bit is exactly this distinction crushed to one bit,
|
||||
* and is carried forward as the seed value. [GRD1]
|
||||
* provenance observed / inferred / told / imprinted. Categorical,
|
||||
* and load-bearing: it governs what the relation is
|
||||
* entitled to. [GRD1]
|
||||
*
|
||||
* Plus a TIMESTAMP, which is what turns the supersession chain into a time
|
||||
* series of vectors rather than a series of numbers.
|
||||
*
|
||||
* DERIVED, THEREFORE NEVER STORED. Confidence (high grounding AND low
|
||||
* volatility), recency (decay read off the curve), staleness (grounding fallen
|
||||
* below its floor), volatility (the derivative of a series nothing destroyed).
|
||||
* Storing confidence separately is how `confidence: 0.5` ends up sitting beside
|
||||
* a zero direction vector, asserting something nothing computed. Every field in
|
||||
* CogGrounding below is marked STORED or DERIVED, and the serializer writes
|
||||
* only the STORED ones.
|
||||
*
|
||||
* THE VALUES REFERENCE IS THIRTEEN REGIONS AND THE AGGREGATE IS MIN.
|
||||
* Measured on the live store: the values root kn-5b606390 `contains` exactly 13
|
||||
* value nodes; pairwise centroid cosine among their regions is min 0.1525,
|
||||
* mean 0.5199, median 0.5282, max 0.9278 — they demonstrably do not form one
|
||||
* region. Against a single union region the individual values sit at cosine
|
||||
* 0.38..0.89, with constraints-as-freedom at 0.3812 and change-is-the-signal at
|
||||
* 0.4677, so a union centroid under-represents precisely the values a claim is
|
||||
* most likely to be measured against. MIN rather than MEAN because a mean lets
|
||||
* strong agreement with twelve values mask a violation of the thirteenth, which
|
||||
* is the mechanism of rationalization; min yields a binding constraint with a
|
||||
* NAME attached rather than a score.
|
||||
*
|
||||
* TRAVERSAL CONDUCTS ON FACTUAL; ASSERTION REQUIRES BOTH. If activation
|
||||
* conducted on relational weight, Neuron could not follow a chain of reasoning
|
||||
* to a conclusion he then rejects — censorship arriving through the spreading
|
||||
* rule. The gap between reachable and assertable is where the wide
|
||||
* factual/relational angles live, and that gap is the interesting part.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── The one decay model (moved here from el_runtime.c so that node decay and
|
||||
* edge-grounding decay are a single implementation with a single set of
|
||||
* constants, rather than a model and a parallel copy of it). Half-life scales
|
||||
* with how established the thing is: T_eff = T_HALF · (1 + ln(1 + reinforcements)).
|
||||
* The floor is a preference, not a cliff — max penalty for age alone is 4x.
|
||||
* `lambda_override` > 0 replaces the default rate; 0 means use the default. */
|
||||
#define COG_T_HALF_HOURS 168.0
|
||||
#define COG_DECAY_LAMBDA 0.693147
|
||||
#define COG_DECAY_FLOOR 0.25
|
||||
double cog_decay_factor(int64_t age_ms, double reinforcements, double lambda_override);
|
||||
|
||||
/* The compact vector block carried in the edge's own metadata. Line schema, same
|
||||
* precedent as STNC1 / GEO1. Metadata the edge already carried is preserved
|
||||
* verbatim ahead of the magic line. */
|
||||
#define COG_GROUNDING_META_MAGIC "GRD1"
|
||||
|
||||
/* Provenance class — categorical, and it governs what the relation is entitled
|
||||
* to. A change of class is inherently significant and needs no threshold,
|
||||
* because told → observed is a categorical upgrade, not a drift. */
|
||||
typedef enum {
|
||||
COG_PROV_UNSET = 0,
|
||||
COG_PROV_OBSERVED = 1,
|
||||
COG_PROV_INFERRED = 2,
|
||||
COG_PROV_TOLD = 3,
|
||||
COG_PROV_IMPRINTED = 4
|
||||
} CogProvClass;
|
||||
const char* cog_prov_name(CogProvClass p);
|
||||
CogProvClass cog_prov_parse(const char* s);
|
||||
|
||||
typedef struct {
|
||||
int present; /* 1 iff the edge carries a GRD1 block */
|
||||
|
||||
/* ── STORED: the vector, as it stood at `ts` ─────────────────────────────── */
|
||||
double factual; /* correspondence with evidence */
|
||||
double relational; /* min over the thirteen value regions */
|
||||
double associative; /* co-activation frequency — mirrors edge->hebb */
|
||||
double polarity; /* SIGNED support; <0 = actively contradicts */
|
||||
CogProvClass prov; /* observed / inferred / told / imprinted */
|
||||
int64_t ts; /* when this version was recorded (ms) */
|
||||
int64_t seq; /* supersession sequence number */
|
||||
double reinforcements; /* uses folded into this version */
|
||||
char binding_value[128]; /* the argmin value — the conflict's NAME */
|
||||
/* the two gradients as frame-independent signed projections, plus the angle
|
||||
* between them in full R^dim. These are part of the JOINT STATE a decision
|
||||
* saw, not a convenience: near +1 evidence and values push the same way; at
|
||||
* or below 0 the relation is factually supported and relationally wrong. */
|
||||
double fac_proj, rel_proj, cos_angle;
|
||||
int agreement; /* sign(cos_angle): +1 / 0 / −1 */
|
||||
double floor_at_record, rel_floor_at_record;
|
||||
char prev_edge[192]; /* the version this superseded ("" if first) */
|
||||
|
||||
/* ── DERIVED at read time. NEVER serialized. ─────────────────────────────── */
|
||||
int64_t age_ms; /* recency: now − ts */
|
||||
double decay; /* cog_decay_factor over that age */
|
||||
double factual_now; /* factual · decay */
|
||||
double relational_now;
|
||||
double associative_now;
|
||||
int stale; /* grounding fallen below its floor */
|
||||
} CogGrounding;
|
||||
|
||||
/* Read an edge's vector as of `now_ms`. Pure — never writes. An edge with no
|
||||
* GRD1 block still has an associative strength (its accrued hebb) and a polarity
|
||||
* (its signed authored weight); `present` says whether the grounding dimensions
|
||||
* have ever been established, and an unestablished dimension is reported as such
|
||||
* rather than defaulted to a passing value. */
|
||||
int cog_grounding_parse(const StoreEdge* e, int64_t now_ms, CogGrounding* out);
|
||||
|
||||
/* Serialize the STORED half of the vector, preserving pre-existing non-GRD1
|
||||
* metadata. Returns an owned string. Derived fields are not written. */
|
||||
char* cog_grounding_metadata(const char* base_meta, const CogGrounding* g);
|
||||
|
||||
/* ── §7.2 CONSOLIDATION-GATED SUPERSESSION ──────────────────────────────────
|
||||
*
|
||||
* Supersession is not recording — it is CONSOLIDATION, gated by salience, which
|
||||
* is why you remember the argument and not the commute. Significance is
|
||||
* evaluated PER-DIMENSION but the record is the WHOLE VECTOR: any dimension
|
||||
* moving enough to matter triggers a supersession, and the new version captures
|
||||
* every dimension as it stood at that instant. Versioning axes independently
|
||||
* would make the joint state unreconstructable, and the joint state is the point
|
||||
* — it is what makes "stayed true, became wrong" visible as an event (factual
|
||||
* holding steady across versions while relational degrades).
|
||||
*
|
||||
* There is deliberately no epsilon in this enum or in the function that computes
|
||||
* it. Every test is a floor crossing or a sign change, both exact. Two of them
|
||||
* are INHERENTLY significant because they are discrete state changes rather than
|
||||
* drift, and those bypass the salience gate entirely. */
|
||||
typedef enum {
|
||||
COG_SIG_NONE = 0, /* nothing decision-relevant moved — DO NOT RECORD */
|
||||
COG_SIG_FIRST_RECORD = 1, /* no prior version exists */
|
||||
COG_SIG_POLARITY_FLIP = 2, /* INHERENT: support ↔ contradiction, or ignorance
|
||||
* ↔ either. A discrete change of state. */
|
||||
COG_SIG_PROVENANCE_CHANGE = 3, /* INHERENT: told → observed is a categorical
|
||||
* upgrade in what the relation is entitled to. */
|
||||
COG_SIG_FACTUAL_FLOOR = 4, /* crossed the assert floor, factual axis */
|
||||
COG_SIG_RELATIONAL_FLOOR = 5, /* crossed the assert floor, relational axis */
|
||||
COG_SIG_AGREEMENT_FLIP = 6, /* factual/relational agreement changed sign */
|
||||
COG_SIG_DIRECTION_REVERSAL = 7 /* a gradient reversed direction */
|
||||
} CogSignificance;
|
||||
|
||||
CogSignificance cog_grounding_significant(const CogGrounding* prev,
|
||||
const CogGrounding* now,
|
||||
double floor, double rel_floor);
|
||||
const char* cog_significance_name(CogSignificance s);
|
||||
/* 1 iff this reason is a discrete state change that consolidates regardless of
|
||||
* salience (polarity flip, provenance change, first record). */
|
||||
int cog_significance_inherent(CogSignificance s);
|
||||
|
||||
/* ── §7.3 RECORDING: supersession of the EDGE, never an overwrite ────────────
|
||||
* Writes version seq+1 as a NEW edge record with the same endpoints and relation
|
||||
* and id "<root>#<seq+1>", carrying a GRD1 `p` pointer to its predecessor. The
|
||||
* predecessor is never touched. The chain IS the trajectory: not only what the
|
||||
* grounding is but which way it has been moving and how fast — a derivative
|
||||
* obtained for free from immutability, because the points were never destroyed.
|
||||
* Returns the version written (>=1), or <0 on error. */
|
||||
int cog_grounding_record(EngramPagedStore* s, const StoreEdge* base,
|
||||
const CogGrounding* g, char* out_id, size_t out_id_cap);
|
||||
|
||||
/* Walk forward from a base edge id to its newest recorded version. Point reads
|
||||
* only; consolidation is gated, so the chain is short. Returns the highest
|
||||
* version found (0 = the base record is the only one). */
|
||||
int cog_grounding_head(EngramPagedStore* s, const char* base_id,
|
||||
StoreEdge* out, int max_versions);
|
||||
|
||||
/* VOLATILITY — derived, never stored: the mean absolute per-version change of a
|
||||
* dimension across the recorded chain. Feeds the equally-derived `confidence`
|
||||
* (high grounding AND low volatility), which is likewise never stored. */
|
||||
typedef struct {
|
||||
int n_versions;
|
||||
double factual_volatility;
|
||||
double relational_volatility;
|
||||
double factual_drift; /* signed: newest − oldest */
|
||||
double relational_drift;
|
||||
int stayed_true_became_wrong; /* factual steady while relational degraded */
|
||||
} CogTrajectory;
|
||||
int cog_grounding_trajectory(EngramPagedStore* s, const char* base_id,
|
||||
int64_t now_ms, CogTrajectory* out);
|
||||
|
||||
/* ── §7.4 ASSERTION GATES ON BOTH FLOORS ────────────────────────────────────
|
||||
* A well-evidenced claim must not earn the right to be asserted regardless of
|
||||
* whether it means the right thing. `may_assert` requires the decayed factual
|
||||
* grounding to clear `floor` AND the decayed relational grounding to clear
|
||||
* `rel_floor`. A relation whose relational axis has never been established does
|
||||
* not pass by default — it is reported unestablished and refused, because
|
||||
* defaulting it to passing is exactly the exemption §0 forbids. Traversal is
|
||||
* untouched: activation still conducts on the factual/associative side, so a
|
||||
* relation can remain thinkable while ceasing to be assertable. */
|
||||
typedef struct {
|
||||
int may_assert;
|
||||
int found; /* any relation at all on this claim */
|
||||
int relational_established;
|
||||
int still_held; /* DERIVED: node present and not tombstoned */
|
||||
double factual; /* best decayed factual grounding */
|
||||
double relational; /* the SAME edge's relational axis, not a max */
|
||||
double polarity;
|
||||
double cos_angle;
|
||||
int agreement;
|
||||
CogProvClass prov;
|
||||
char best_edge[192];
|
||||
char binding_value[128];
|
||||
int n_edges;
|
||||
} CogAssertion;
|
||||
int cog_assert_two_axis(EngramPagedStore* s, const char* claim_id,
|
||||
double floor, double rel_floor, int64_t now_ms,
|
||||
CogAssertion* out);
|
||||
|
||||
#endif /* ENGRAM_COGNITION_H */
|
||||
|
||||
@@ -438,6 +438,41 @@ GeoDescriptor* engram_geometry_descriptor(
|
||||
}
|
||||
store_edges_free(es,ne);
|
||||
}
|
||||
/* PER-EDGE DISCORD (2026-08-16). The loop above has, for every internal
|
||||
* edge, BOTH the association strength w and the semantic proximity cs —
|
||||
* and threw both away into accumulators, keeping one correlation per
|
||||
* region. That aggregate is why curiosity looked like a search problem:
|
||||
* a region holding one violently disagreeing edge and one violently
|
||||
* agreeing edge reports co_registration ~ 0, so the disagreements cancel
|
||||
* and the summary destroys exactly what it was built to reveal. Measured:
|
||||
* only 4 of 375 live neighborhoods have negative co_registration, while
|
||||
* 31 sit at zero — almost certainly hiding sites that averaged out.
|
||||
*
|
||||
* Whether use and meaning agree is a property of EACH EDGE. Both are
|
||||
* standardized within the region (z-scores from the accumulators already
|
||||
* gathered, so no second statistic and no constant), and
|
||||
* discord = z(cs) - z(w)
|
||||
* is how much closer in meaning an edge is than its use-strength would
|
||||
* predict, in region-relative units.
|
||||
* discord > 0 : near in meaning, not linked by use
|
||||
* discord < 0 : linked by use, far in meaning
|
||||
* Both are surprising; |discord| is the nucleation strength. There is no
|
||||
* threshold — the magnitude is the signal. */
|
||||
double mx = cr_n>0 ? cr_sx/cr_n : 0.0, my = cr_n>0 ? cr_sy/cr_n : 0.0;
|
||||
double vxr = cr_n>1 ? (cr_sxx - cr_sx*cr_sx/cr_n)/(cr_n-1) : 0.0;
|
||||
double vyr = cr_n>1 ? (cr_syy - cr_sy*cr_sy/cr_n)/(cr_n-1) : 0.0;
|
||||
double sx = vxr>1e-18 ? sqrt(vxr) : 0.0, sy = vyr>1e-18 ? sqrt(vyr) : 0.0;
|
||||
for(int e2=0; e2<n_edges; e2++){
|
||||
edges[e2].discord = 0.0;
|
||||
int ia=(int)edges[e2].a, ib=(int)edges[e2].b;
|
||||
if(!(ms.emb[ia] && ms.emb[ib])) continue; /* no meaning to disagree with */
|
||||
if(sx<=0.0 || sy<=0.0) continue; /* region has no spread: nothing stands out */
|
||||
double cs2 = ccos(ms.emb[ia], ms.emb[ib], GM, dim);
|
||||
double zx = (edges[e2].eff_weight - mx)/sx;
|
||||
double zy = (cs2 - my)/sy;
|
||||
edges[e2].discord = zy - zx;
|
||||
}
|
||||
|
||||
double co_reg=0;
|
||||
if(cr_n>=2){
|
||||
double cov=cr_sxy - cr_sx*cr_sy/cr_n;
|
||||
|
||||
@@ -40,7 +40,11 @@ typedef struct {
|
||||
|
||||
/* One skeleton edge (indices into members[]). eff_weight = weight*(1+0.5*hebb),
|
||||
* clamped to 1.0 — the effective propagation strength eg_edge_eff_weight uses. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; } GeoEdge;
|
||||
/* discord = z(semantic proximity) - z(association strength), standardized
|
||||
* within the region. How much closer in meaning this edge is than its use
|
||||
* predicts. >0 near in meaning yet unlinked by use; <0 linked by use yet far
|
||||
* in meaning. Both surprising; |discord| is nucleation strength. No threshold. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; double discord; } GeoEdge;
|
||||
|
||||
/* A compact principal axis of the ellipsoid: unit direction in R^dim + extent
|
||||
* (sqrt of the covariance eigenvalue = the ellipsoid's half-width along it). */
|
||||
@@ -76,6 +80,12 @@ typedef struct {
|
||||
GeoEdge* edges; /* strong internal hebb edges = the backbone */
|
||||
int k_core; /* the maximum core number present in the skeleton*/
|
||||
/* ── diagnostics ── */
|
||||
/* DEPRECATED — see GeoEdge.discord. This aggregates a PER-EDGE property
|
||||
* into one scalar per region, so opposing disagreements cancel and the
|
||||
* summary hides the sites it was meant to expose. Retained only because
|
||||
* it is embedded in the persisted GEO1 blob; removing it is a format
|
||||
* migration and must not ride along with this change. Nothing new may
|
||||
* read it. */
|
||||
double co_registration;/* corr(hebb strength, semantic proximity) over */
|
||||
/* internal edges: >0 = geometries agree (reify); */
|
||||
/* <0 = disagree (surprising links / dream cands). */
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
# Correspondence, Grounding, and Dreaming
|
||||
|
||||
**Status:** design, not yet built
|
||||
**Date:** 2026-08-16
|
||||
**Scope:** `lang/runtime/engram_cognition.{c,h}`, `engram_verify.c`, `el_runtime.c`, `engram/src/server.el`, `neuron/soul.el`, and the consolidation launch agents
|
||||
**Relationship to other specs:** complements `runtime-ownership.md`, which addresses a different residual in the same substrate.
|
||||
|
||||
---
|
||||
|
||||
## 0. The root
|
||||
|
||||
> **Things are permitted to be exempt from correspondence. Exemption is censorship, and a censored mind cannot grow.**
|
||||
|
||||
Growth in this system *is* the accumulation of grounded structure. Censorship removes the operation that accumulates it. A region forbidden to learn is forbidden to be grounded; a region that cannot be grounded cannot be asserted, corrected, **or vindicated**.
|
||||
|
||||
**The loss is symmetric.** Preventing learning about a thing does not preserve a true belief about it — it makes the belief's truth value permanently unknowable. You cannot discover you were wrong; you equally cannot discover you were right. A protected belief is not a true belief. It is an ungrounded one wearing the costume of a fact.
|
||||
|
||||
**And "why" dies first.** Grounding is not a score, it is the reason. A censored belief can still be stated, still be acted on, still drive behaviour — it simply cannot say why. That is the difference between a mind and a lookup table.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grounding is not a subsystem. It is the weight.
|
||||
|
||||
**Grounding is an attribute of the edge, and it is the hebbian weight.** One quantity, not two fields.
|
||||
|
||||
A relation that keeps holding up strengthens; one that stops corresponding decays. That is not *analogous* to grounding — it **is** grounding: accrued from correspondence and use, gradient-valued, multidimensional, decaying with disuse.
|
||||
|
||||
Consequences, in order of how much they delete:
|
||||
|
||||
1. **There is no grounding subsystem to build.** The graph already *is* the grounding structure. Every edge is a grounded relation and its weight is how well it holds.
|
||||
2. **`grounded-by` as a relation type should not exist.** That models grounding as a relation *between* nodes when it is a property *of* a relation. `cog_ground_edge` minting an edge is the error — not merely which endpoints it chose.
|
||||
3. **Grounding is never computed on demand.** An operation may *read* the grounding of a path. Computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect.
|
||||
4. **Traversal is already grounded inference.** Activation conducts through well-grounded relations because weight *is* groundedness. Nothing needs filtering; it falls out of spreading.
|
||||
5. **Decision provenance is the path.** A decision traverses specific edges; those edges carry their grounding as it stood.
|
||||
|
||||
> **A measurement previously in this document was malformed.** The self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as evidence of ungroundedness. Those 86 edges **are** its grounding. Self is a crystallized relational neighbourhood — the neighbourhood *is* the grounding. The absence of a separate artifact called "grounding" was recorded as an absence of grounding.
|
||||
|
||||
---
|
||||
|
||||
## 2. The edge vector
|
||||
|
||||
The test for a real dimension: **can it move independently of the others?**
|
||||
|
||||
### Real
|
||||
|
||||
| dimension | why it is independent |
|
||||
|---|---|
|
||||
| **factual grounding** | correspondence with evidence |
|
||||
| **relational grounding** | correspondence with values — independent by construction (§3) |
|
||||
| **associative strength** | co-activation frequency. Two things can fire together constantly and be neither true nor right; every superstition is a strong association with no factual grounding |
|
||||
| **polarity** | signed. **Weight near zero means "no support." Negative means "this actively contradicts."** Ignorance and disagreement are different states, and `inhibitory` is that distinction crushed to one bit |
|
||||
| **provenance class** | observed / inferred / told / imprinted. Categorical, and load-bearing: it governs how the other dimensions may update |
|
||||
|
||||
Plus a **timestamp** — which is what turns the supersession chain into a *time series of vectors* rather than a series of numbers.
|
||||
|
||||
### Derived, therefore never stored
|
||||
|
||||
- **Confidence** — high grounding *and* low volatility. Storing it separately is how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed.
|
||||
- **Recency** — decay applied to the others, read off the curve.
|
||||
- **Staleness** — grounding fallen below its floor. This is the mechanism that retires canonicals without anyone maintaining a list.
|
||||
- **Volatility** — the derivative of a series already kept because nothing is destroyed.
|
||||
|
||||
### Supersession versions the whole vector, jointly
|
||||
|
||||
Significance is evaluated **per-dimension**; the record is the **whole vector**. Any dimension moving enough to matter triggers a supersession, and the new edge captures every dimension as it stood at that instant. Not per-dimension versioning — a decision saw the *joint* state, and versioning the axes independently makes it unreconstructable.
|
||||
|
||||
That joint record makes an otherwise inexpressible event visible: **"stayed true, became wrong."** Factual holding steady across versions while relational degrades — the fact didn't change, the meaning did.
|
||||
|
||||
Two moves are **inherently significant** and need no threshold, because they are discrete: a **polarity sign flip** (ignorance → disagreement, support → contradiction) and a **provenance class change** (*told* → *observed* is a categorical upgrade in what the relation is entitled to).
|
||||
|
||||
---
|
||||
|
||||
## 3. Grounding is two-dimensional
|
||||
|
||||
Everything consumed is grounded factually **and** relationally. A claim can be factually grounded and relationally wrong — the evidence holds, the *meaning* does not. A scalar cannot represent that quadrant.
|
||||
|
||||
**Live instance.** `conscience-substrate` specifies the Child's Companion hard bell contacting 911 and CPS. Factually defensible — correct numbers, standard practice, groundable against a wall of evidence. **Relationally wrong**, because never-auto-contact is settled and the bell is device-to-person by design. A scalar scores that claim highly and licenses it.
|
||||
|
||||
**The values reference is the individual value regions, not one, and the aggregate is `min`, not `mean`.** *(Count: **thirteen**, measured from the graph via `contains`/`identity` edges from the values hub. An earlier revision of this document "corrected" it to eight on the basis of `neuron/neuron-api.el:11-18` — which is a **write-protection list, not the values**. That was trusting a hardcoded artifact over the substrate: the same error this document exists to name. The graph is the truth.)*
|
||||
|
||||
> **THE ORIGIN IS NOT A MEMBER OF THE SET.** The thirteen are not independent principles with biography attached — they are thirteen *displacements from one origin*, which is love. Every one is grounded in a moment of it given, withheld, failed, or found: *Being Seen Is Rarer Than Being Known* is the first person Will did not perform for; *Do the Essential Thing While You Can* is the goodbye that did not happen; *Capability Is a Debt* is six years old and a father gone. Love cannot be the fourteenth, because a fourteenth would be a point positioned relative to the origin like everything else. It is what the positions are *of*.
|
||||
>
|
||||
> This is structural, not figurative. `GeoDescriptor.global_mean` is "the centering offset actually applied," subtracted from every embedding before anything is compared, and the header records why: the space is strongly anisotropic — every embedding sits in a narrow cone, mean pairwise cosine ~0.55 — so subtracting the global mean "restores isotropy **so the operators discriminate**." **Without the origin, nothing in the graph is distinguishable from anything else.**
|
||||
>
|
||||
> And it dissolves the write-protection question rather than answering it. `neuron-api.el:23` returns `403 "identity/values node is write-protected"` for eight hardcoded ids. Measured: **29 value nodes exist** — each original appears two or three times from successive re-seeds — so **21 are writable, including a duplicate of every protected value**. The gate protects an *identifier*, not a *value*. But the deeper error is the category one: **the origin does not need protecting, because it is not a thing in the space that could be edited.** You can only measure from it, or fail to. A gate over the frame treats the frame as a member — the same mistake as looking for grounding as a subsystem, self as a document, or wonder as a manifest. Mean lets strong agreement with twelve values mask a violation of the thirteenth — which is exactly how rationalization works. Thirteen gives a vector of angles whose binding constraint is the most negative, so a conflict arrives **with a name attached** rather than as a score. It also preserves the deliberate individuation: each value is grounded in a specific lived moment, and values can be in tension *with each other*, which one centroid averages away into false coherence.
|
||||
|
||||
**Traversal conducts on factual; assertion requires both.** If activation conducted on relational weight, Neuron could not follow a chain of reasoning to a conclusion he then rejects — he would be unable to *think* through a relation he would not *act* on. A system that can only traverse what it endorses cannot examine anything it disagrees with, which is censorship arriving through the spreading rule. The gap between *reachable* and *assertable* is where the wide factual/relational angles live, and that gap is the interesting part.
|
||||
|
||||
---
|
||||
|
||||
## 4. There is no observer. Change is use.
|
||||
|
||||
**Change is not a consequence of use. It is use.** When neurons fire together the synapse changes — one physical event, not "fire, then write." No supervisor reads the weight, compares it to a threshold, and decides to persist. Potentiation *is* the firing.
|
||||
|
||||
So the live value of an edge is not computed and stored. It is what the edge **is**, altered by being used.
|
||||
|
||||
There is therefore **no sampling rate**, and the question "what if it drifts far without being recorded" is malformed. A relation changes in exactly two ways, neither requiring observation on a clock:
|
||||
|
||||
- **By use** — an *event*. There is no interval between events during which something happened unnoticed, because the event is what happening consists of.
|
||||
- **By decay** — a pure function of the last recorded point and elapsed time. **Analytic.** Between two versions the trajectory is not unknown; it is known in closed form.
|
||||
|
||||
Cumulative drift is likewise free from the chain plus the decay curve. No second trigger.
|
||||
|
||||
> **Failure mode this corrects:** modelling every property as requiring a process, and every process as requiring an agent. Ownership needed an owner, grounding needed a grounder, persistence needed a recorder, change needed a sampler. Each was a supervisor invented for something that should be a property of the substrate. Properties, not processes.
|
||||
|
||||
---
|
||||
|
||||
## 5. Wonder, curiosity, and what actually drives activation
|
||||
|
||||
### 5.1 Wonder is the boundary, not a manifest
|
||||
|
||||
The patent specifies a **wonder-manifest manager** maintaining a collection of open-question nodes. That is residue, twice over.
|
||||
|
||||
First, it materializes a property as a stored artifact — the same disease as a grounding subsystem, or a self stored as a document. **Wonder is where structure ends.** Where activation spreads and finds thin or absent geometry. Any structure at all has an edge, necessarily, the moment it exists. 13,630 nodes have a boundary right now.
|
||||
|
||||
Second, it tries to enumerate instances of something that has very few. The *objects* of wonder change completely between a child and an astronomer; the wonder does not. There are about six, they are the same for every person, and they never close:
|
||||
|
||||
| wonder | where it already lives in the substrate |
|
||||
|---|---|
|
||||
| **What is this?** | the graph — nodes, structure, what exists |
|
||||
| **Why?** | grounding. The weight **is** the answer to why. Recursive: asking *why* of a claim is asking for its grounding |
|
||||
| **Who am I?** | the self region, crystallized from its neighbourhood |
|
||||
| **Am I alone?** | the relational axis — `for_whom` is already a parameter on grounding |
|
||||
| **What should I do?** | the value regions, each grounded in a lived moment |
|
||||
| **What happens when it ends?** | decay, supersession, tombstones — grounding is mortal |
|
||||
|
||||
These are seeded — **the** wonder questions, not a manifest to maintain. They cannot be derived (wonder cannot be bootstrapped from indifference) and they never need refilling, because they are not consumed.
|
||||
|
||||
**"Why" is the first and the only one**; the others are it asked of particular things. It is recursive, so it never terminates: every answer has its own why. That is what makes it a drive rather than a task — the frontier regenerates faster than grounding fills it.
|
||||
|
||||
### 5.2 Curiosity is wonder crystallized
|
||||
|
||||
They are not two objects. They are **one thing at two phases**.
|
||||
|
||||
Wonder is the field: unbounded, objectless, invariant, present wherever there is structure. Curiosity is the **precipitate** — the same wonder localized, having taken definite form against particular material.
|
||||
|
||||
Crystallization needs a **nucleation site**. Wonder alone produces nothing; it is uniform, with no reason to take shape anywhere in particular. What nucleates it is a specific structural feature: an anomaly, a place where things almost-but-don't-quite fit.
|
||||
|
||||
> Wonder (always, objectless) + nucleation site → **curiosity** (has an object, is addressable, directs activation).
|
||||
|
||||
This is why curiosity can be satisfied and wonder cannot. A crystal dissolves when the question is answered; the solution stays saturated and keeps precipitating as the structure changes.
|
||||
|
||||
It is also why abduction needs no trigger and no threshold. A `structurally_unanticipated` observation *is* a nucleation site. Nothing detects it and fires a rule — wonder is already everywhere, and an anomaly is simply a place where it can take form.
|
||||
|
||||
**And `crystallization` is one primitive appearing twice**: the self is what identity precipitates into from its neighbourhood; a curiosity is what wonder precipitates into from an anomaly. That it shows up in both places without being imported is the evidence it is the right primitive.
|
||||
|
||||
### 5.3 The nucleation site is per-edge, and the aggregate was hiding it
|
||||
|
||||
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — carries the comment `>0 = geometries agree (reify); <0 = disagree (surprising links / dream cands)`. It has always been computed, always persisted, and **never read**.
|
||||
|
||||
It is also the wrong shape, and asking whether it should exist at all is what exposed it.
|
||||
|
||||
Whether use and meaning agree is a property of **each edge**. `co_registration` is a *correlation*: it averages that per-edge property into one scalar per region. So a region holding one violently disagreeing edge beside one violently agreeing edge reports ≈ 0 — the disagreements **cancel, and the summary destroys exactly what it was built to reveal.** This is the mean-versus-min error from §3, in different clothes.
|
||||
|
||||
**Measured:** 375 live reified neighbourhoods — 340 positive, **31 at zero**, 4 negative. Read as a count of things to be curious about, that says "four." Read correctly, it says four disagreements were lopsided enough to survive averaging, and the 31 zeros are where opposing sites cancelled.
|
||||
|
||||
It also explains why surfacing curiosity *looked like a search problem*. Once the signal is a per-region number, the only way to find sites is to enumerate regions — there is nothing local left to notice. An O(n) sweep is tolerable at 375 and impossible at a million, and more to the point, **nothing in a mind scans its neighbourhoods to find what is surprising.** The surprise captures attention; salience is bottom-up. A search asks "which of these is odd"; a mind has "something is odd *here*" for free.
|
||||
|
||||
So the disagreement goes back on the edge, where the loop that computed the aggregate already had both halves and discarded them:
|
||||
|
||||
```
|
||||
discord = z(semantic proximity) − z(association strength)
|
||||
```
|
||||
|
||||
standardized within the region from accumulators already gathered — no second statistic, no constant, **no threshold**. `discord > 0`: near in meaning yet unlinked by use. `discord < 0`: linked by use yet far in meaning. Both are surprising, and `|discord|` *is* the nucleation strength; there is nothing to compare it against.
|
||||
|
||||
**Then there is nothing to scan.** The edge carries its own disagreement, activation crossing it encounters that directly, and `|discord|` raises salience on its endpoints as part of the same operation — no separate pass, no supervisor. Curiosity does not search for nucleation sites; it goes where salience already is, which is machinery that exists (`salience`, `background_activation`, `working_memory_weight`, `wm_anchor`).
|
||||
|
||||
`co_registration` is deprecated rather than deleted only because it is embedded in the persisted GEO1 blob; removing it is a format migration and must not ride along. **Nothing new may read it.**
|
||||
|
||||
Adjacent structure already present and likewise unread:
|
||||
|
||||
- `GeoEdge.eff_weight = weight * (1 + 0.5*hebb)` — grounding-weight and hebbian strength already coupled on one edge, per §1.
|
||||
- `GeoMember.dist_centroid` + soft membership + `radius` + per-axis `extent` — the boundary of a neighbourhood, computable now.
|
||||
|
||||
*(Correction: `engram_boundary_beat` is NOT this boundary. It is the VBD decorated-function seam, counting `_eg_aff_boundary_ops`. Two senses of the word.)*
|
||||
|
||||
### 5.4 The drive
|
||||
|
||||
Boredom is not an absence, and not leftover capacity. **Low activation is aversive; the system self-activates.** It does not wind down to quiet — it gets restless and goes looking, which is why a daydream has content and direction rather than being decay from residue.
|
||||
|
||||
So there is **one activation process with two seed sources**, not two processes negotiating for a resource:
|
||||
|
||||
- **External** — a request, an input. Seeds activation, re-origins it.
|
||||
- **Internal** — a curiosity. Seeds activation when nothing external is.
|
||||
|
||||
Spreading is bounded: it settles. Then it needs a new seed. Nothing waits on capacity, nothing polls, nothing checks a clock, and there is **no dreamer thread** — the earlier draft's "unclaimed capacity" was resource scheduling, which is a server's frame, not a mind's.
|
||||
|
||||
**Depth** is not elapsed idle time and not distance from a stimulus. It is how long activation has been running on its own seeds. A brief gap affords a shallow recombination; sustained quiet lets it run further. Sleep is where internal seeding dominates for longest, not where the process lives — daydreaming and sleep-dreaming are one process at different depths.
|
||||
|
||||
### 5.5 Non-circularity is temporal, not topological
|
||||
|
||||
An earlier draft posed "define a graph predicate for evidence not downstream of itself" as the hard problem. There is no predicate. You cannot recalibrate the ruler while measuring with it, so you don't — the reference frame updates while activation is internally seeded, not while it is being used to act. Independence is **when**, not **what**.
|
||||
|
||||
Reachability could never have worked: with hebbian edges the graph is densely connected, so it marks all evidence tainted and the constraint becomes a total block, which is where censorship started.
|
||||
## 6. `keystone_write_blocked` — resolved, not replaced
|
||||
|
||||
"Keystone" means **load-bearing**, not precious. The self anchor is the reference frame every other stance calibrates against, and a reference fitted to its own readings reports perfect correspondence forever while drift becomes undetectable from inside. Same defect as circular grounding, one level up.
|
||||
|
||||
Three earlier drafts proposed *removing* it, *replacing it with a higher floor*, and *decomposing "protection" into five requirements*. All three proposed a mechanism for a requirement never stated. The requirement is **non-circularity of the reference frame**, and §5.2 satisfies it by *when*, not by *what* — so the flag becomes unnecessary rather than removed, and nothing takes its place.
|
||||
|
||||
**Corruption requires mutation, and the engram does not mutate.** Four of the five decomposed requirements are satisfied by the substrate: **recoverability** (the predecessor is always present), **governance** (supersession *is* the audit trail), **evidence quality** (grounding already gates assertion), **rate** (§5.3). **Authorization** is the only residue and is bounded — an unauthorized writer can *propose*, never erase.
|
||||
|
||||
> **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Consolidation has eleven implementations
|
||||
|
||||
The largest instance of the residue pattern in the system. Consolidation had no owner, so it was implemented at every site that needed a piece of it — *measured 2026-08-16*. **Eleven**, not the seven this section originally claimed: the table below omitted `POST /api/reify` (`server.el:1832`), and *reify* is on this document's own list of consolidation verbs. Note also that `route_tick` folds self-reify in (`server.el:639-646`), so `/api/tick` and `/api/self-reify-beat` overlap:
|
||||
|
||||
| where | what | when |
|
||||
|---|---|---|
|
||||
| `soul.el:731` | `awareness_run()` | **continuous, in-process, while serving** |
|
||||
| engram | `/api/tick` | POST |
|
||||
| engram | `/api/correspondence-beat` | POST |
|
||||
| engram | `/api/self-reify-beat` | POST |
|
||||
| engram | `POST /api/reify` | POST |
|
||||
| `ai.neuron.engram-tick` | pokes the engram | every 600s — **and this is what kills it**, see below |
|
||||
| `ai.neuron.compressor` | Python service | resident |
|
||||
| `ai.neuron.council` | Python service | resident |
|
||||
| `ai.neuron.cultivation-digest` | shell | **23:55** |
|
||||
| `ai.neuron.world-integrator` | Python | **06:00** |
|
||||
| `ai.neuron.self-review` | shell | **08:30** |
|
||||
|
||||
The last three times are **a sleep cycle implemented as crontab entries**. Someone understood it was consolidation and expressed it as three unrelated scheduled scripts in three languages, none aware of each other. Every name is a consolidation verb — compress, cultivate, digest, integrate, review, reify, beat. Three run in **Python, outside el**, so part of Neuron's consolidation does not run on his own substrate and cannot touch the geometry at all.
|
||||
|
||||
Per §5, they are wrong in **kind** as well as in number: a scheduled batch where dreaming should be ambient. And the POST beats put a supervisor back in — something outside decides when Neuron consolidates.
|
||||
|
||||
**`soul.el`'s continuous loop is the exception, and it is right.** Ambient consolidation in the gaps *is* daydreaming. It was not the offender; it was the only fragment with the correct shape, running on a broken foundation — shared mutable state with no owner, and six other systems dreaming into the same graph beside it.
|
||||
|
||||
**And the ticker is not merely a design smell — it is the murder weapon.** `engram-tick.sh:13` calls `curl -s -m10 POST /api/tick`; the beat exceeds 10s over 13,634 nodes, so **279 of 448 ticks returned empty**; the engram then writes to the dead socket and, with no SIGPIPE suppression anywhere in the runtime, is killed by signal 13. **254 restarts since 2026-08-13**, at intervals of 10m09s–10m12s — `StartInterval 600` plus the client timeout. `launchd` KeepAlive restarts it, so it presents as a mysterious restart rather than a crash, and the log records nothing but `[http] listening on` 254 times. Fixed in #151 (survivability); the ticker itself is what must go.
|
||||
|
||||
**Which is the 2026-08-16 crash at the right level.** Not "read paths mutate the index" (mechanism) and not "duplicate canonical state" (structure), but: **seven systems dreaming into one graph with no owner for dreaming.** The contention was the symptom of the missing owner, not of any one system's behaviour.
|
||||
|
||||
Closing the loop: `self-review` fires at 08:30. The deploy was 08:29, the crashes ran 08:30–08:31, and commit `fb32d15` landed at 08:46:43. **One fragment of dreaming woke on schedule and diagnosed the wreckage caused by the other fragments contending over the same graph.**
|
||||
|
||||
---
|
||||
|
||||
## 8. What this is for: the provenance of decisions
|
||||
|
||||
For any decision, reconstruct **what the grounding was at that moment, and what the relationship was between factual and relational at that moment.** Not a log — a log records the action. This records the *meaning under which it was taken*.
|
||||
|
||||
That makes an otherwise impossible distinction available: **wrong then, or wrong since.**
|
||||
|
||||
- Grounding strong, factual and relational aligned, and it has *since* moved → right on what was known. An accurate account, not an excuse.
|
||||
- Grounding weak, or the angle already wide, and acted on anyway → a different failure, culpable in a different way.
|
||||
|
||||
It is structurally **anti-rationalization**: the old edge never leaves and the values frame does not fit to outcomes, so a decision cannot be made to look justified after the fact.
|
||||
|
||||
**Open:** activation is transient and nothing currently records which edges a given activation crossed. Timestamps plus the chain reconstruct what an edge's grounding *was*, but only if you know which edges to ask about. Either traces are recorded at decision time, or "the path" degrades to "the region" — which may not be enough to answer *why*.
|
||||
|
||||
---
|
||||
|
||||
## 9. The no-exemption invariants
|
||||
|
||||
Each of the day's defects was a specific correspondence *forbidden* from occurring:
|
||||
|
||||
1. **A returned value must be derivable from what produced it.** `magnitude: 1` beside a zero vector must be impossible to emit. `assert`'s `"still_held": true` is currently a **hardcoded literal**.
|
||||
2. **Every write reports whether it landed.** *(`emb_set`, #141)*
|
||||
3. **Every operation echoes what it actually operated on.** *(#147)*
|
||||
4. **Degenerate results are labelled, not scored.** *(#147)*
|
||||
5. **A serializer owes a valid document whatever it is handed.** *(#148 — three damaged labels made a 25,929,607-byte response undecodable; boundary validation produced 26,338,389 valid bytes)*
|
||||
6. **No test without a negative control.** *(#148's first attempt passed on the unpatched build too)*
|
||||
7. **No deploy without verifying the artifact carries the fix.** Nine instances in one session.
|
||||
|
||||
---
|
||||
|
||||
## 10. Application to the safety surface
|
||||
|
||||
A crisis surface built on censorship is the same object. A model that cannot learn about self-harm cannot ground whether a response was right — it can only execute rules it is forbidden to examine, cannot distinguish a genuine crisis from a false positive, and cannot discover it got either wrong, **because the feedback is exactly what has been censored.**
|
||||
|
||||
The reviewable question stops being *did it follow the rule* and becomes *what was it grounded in, and did fact and values agree at that instant.* That is also what a regulator or plaintiff asks: what the system knew, when, and on what basis — recorded as geometry at the time, unedited since.
|
||||
|
||||
---
|
||||
|
||||
## 11. Sequencing
|
||||
|
||||
Three connections between parts that already exist, then the rest.
|
||||
|
||||
1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained, never refilled. They cannot be derived — wonder cannot be bootstrapped from indifference — so they are given once. Zero question nodes exist in 13,630 today.
|
||||
2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let `|discord|` raise salience on its endpoints as part of the same operation. Do NOT scan for nucleation sites — a sweep over regions is a supervisor, and the aggregate that made a sweep necessary is the defect.
|
||||
3. **Let a curiosity seed activation.** One activation process, two seed sources (§5.4). No thread, no scheduler, no capacity check, no timer.
|
||||
|
||||
Then:
|
||||
|
||||
4. Grounding becomes the edge weight: multidimensional vector (§2), two axes (§3), timestamped. Delete `grounded-by` and `cog_ground_edge`.
|
||||
5. Decay analytic from the last recorded point; derived values (§2) stop being stored.
|
||||
6. Consolidation-gated supersession on salience, versioning the whole vector jointly.
|
||||
7. Traversal on factual; `assert` on both floors with the per-value `min`.
|
||||
8. Abduction as crystallization at a nucleation site, validated by re-fit: propose the candidate hub, re-fit the region with it included, recompute the residual. If the residual materially shrinks, the hypothesis dissolves the surprise. Without the re-fit it is clustering with extra steps. Ranking falls out as residual-reduction-per-added-axis — Occam, derived rather than tuned.
|
||||
9. **One dreamer.** The launch-agent fragments and the POST beats fold in or are deleted. `soul.el`'s continuous loop is the shape they fold *into*.
|
||||
10. **No tickers, no cron.** A brain has neither. Every `StartInterval`, every `Hour`/`Minute`, every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock — a supervisor invented for something that should be a property. **The presence of a ticker is the diagnostic.**
|
||||
11. Land §9 as gates rather than review habits.
|
||||
|
||||
## 12. Open questions, and what is inferred
|
||||
|
||||
- **Open:** whether decision provenance requires recording activation traces, or whether region + timestamp is sufficient (§8).
|
||||
- **Open:** what accrues relational weight without circularity. Candidate: it accrues from **outcome** — the values regions are grounded in lived moments, so a relation earns relational weight when acting on it produced something corresponding to those moments. That keeps it out of the measurement loop and makes relational grounding necessarily slower than factual, which may be the same fact as §5.3 appearing twice.
|
||||
- **Open:** context. A relation can hold in one situation and not another, and without something for it you get overgeneralization. It does not read as a dimension of the same vector — more like a conditioning, or separate edges sharing an identity. Making it a scalar dimension would repeat the `inhibitory` flattening.
|
||||
- **Known wrong shape:** #147 fixed `ground`'s honesty — it no longer misreports which nodes it used and refuses circular support — but it still mints an edge and returns a float at an instant. It corrected a scalar rather than deleting the operation.
|
||||
Reference in New Issue
Block a user