Compare commits

...

3 Commits

Author SHA1 Message Date
Neuron 7a1501d097 Grounding is the edge's weight, and the weight is a vector
El SDK CI - dev / build-and-test (pull_request) Failing after 3m59s
A relation that keeps holding up strengthens; one that stops corresponding
decays. That is not analogous to grounding, it IS grounding — so it belongs on
the edge, not in a subsystem beside it. The graph was already the grounding
structure; this stops modelling it as something else.

Deleted, not refactored:
  - cog_ground_edge and the `grounded-by` relation type. A grounded-by edge
    models grounding as a relation BETWEEN nodes when it is a property OF a
    relation. #147 fixed which endpoints that edge landed on and left the wrong
    idea intact. Measured on the live store: the old path scored two nodes with
    ZERO edges between them at 0.925237 and wrote an edge for it.
  - ground() writing. It was a read that wrote — the eg_vindex_sync defect.
    Three identical calls produced three writes to the same edge id.
  - keystone_write_blocked. Its measured cost was 0.00% brier reduction over
    n_trials 0 on the keystone: the loop never ran, so the self was never
    calibrated and never falsifiable. Nothing replaces it — non-circularity of
    the reference frame is temporal, not a permission.
  - a graph predicate for "evidence downstream of itself", built and then
    withdrawn. Reachability from the self region covers 89.2% of the live graph
    (10,580 of 11,861 nodes), so any topological predicate marks nearly all
    evidence tainted and degenerates into the total block censorship began as.

The vector, carried in a GRD1 block on the edge's own metadata:
factual, relational, associative (the existing hebb), polarity (SIGNED — near
zero is "no support", negative is "actively contradicts"; `inhibitory` is that
distinction crushed to one bit), provenance class, and a timestamp. Confidence,
recency, staleness and volatility are DERIVED at read and never serialized.

Decay is one model, not two: cog_decay_factor is the single implementation and
engram_temporal_decay now delegates to it — proven bit-identical over 24
(age, reinforcement) points.

Values reference: thirteen regions, aggregate MIN, binding value named. Measured
— the 13 have pairwise centroid cosine min 0.1525 / mean 0.5199 / max 0.9278, so
they demonstrably are not one region, and a mean would let agreement with twelve
mask a violation of the thirteenth.

Supersession versions the whole vector jointly, gated by consequence and
salience with no epsilon anywhere: floor crossings and sign changes only.
Polarity flips and provenance-class changes are inherently significant and
bypass the salience gate.

Also fixed: the frame contract. Descriptors are built over L2-normalized member
embeddings; think() and the grounding path were fitting RAW vectors against them.
Measured on the self region, same data, same 106 members:
  magnitude 0.00283443 -> 0.536134, spread 18.7565 -> 0.930163.
Every fit score sat three decimal places below the 0.5 floors that gate on them.

assert() gates on both floors and computes still_held instead of returning a
hardcoded `true` — the old build reported still_held for a node that does not
exist.
2026-08-16 13:18:50 -05:00
will.anderson d41645388a runtime: make valid UTF-8 the JSON emitter's contract (#148)
El SDK CI - dev / build-and-test (push) Failing after 4m12s
El SDK CI - dev / build-and-test (pull_request) Failing after 4m37s
2026-08-16 17:03:44 +00:00
Neuron 8a307dfd42 runtime: make valid UTF-8 the JSON emitter's contract
El SDK CI - dev / build-and-test (pull_request) Failing after 10m36s
Three nodes in the live graph carry labels truncated to exactly 80 bytes
ending in a lone 0xE2 — the first byte of an em-dash, cut mid-sequence.
jb_emit_escaped copied every byte >= 0x20 through verbatim, so those three
nodes made the ENTIRE /api/nodes/list response undecodable and no strict
parser could read the graph at all.

  production binary   25,929,607 bytes   INVALID at byte 89260
  this build          26,338,389 bytes   VALID, parses to 13,630 nodes

The damage was NOT written by this runtime. No 80-byte truncation exists
here (the only label truncation is engram_first_n_chars at 60), and the
content of those nodes is 2572 and 2746 bytes. Some other producer wrote
them. That is exactly why fixing a writer could not have fixed this: the
store already holds the damage, and it accepts data from importers, other
producers and older binaries.

So the fix goes where the promise is made. A serializer that emits JSON
owes valid UTF-8 whatever it is handed. jb_emit_escaped now validates each
multi-byte sequence before emitting any of it and substitutes U+FFFD for a
bad lead byte, a missing or malformed continuation, an overlong encoding, a
UTF-16 surrogate, or a codepoint above U+10FFFF. Invalid bytes are REPLACED
rather than dropped, so the damage stays visible in the output instead of
being silently papered over. Well-formed input is byte-identical to before.

Second, preventive and explicitly NOT the cause of the above:
engram_first_n_chars truncated by BYTES despite its name, so content with a
multi-byte character crossing byte 60 would produce a half codepoint in the
label. It now uses el_utf8_safe_len, which returns the largest byte length
<= max that does not split a codepoint. Bounded by bytes, not codepoints,
so existing labels never grow — they only stop splitting.

el_utf8_safe_len 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.

Note on the investigation: I first "fixed" the truncator and wrote a test
that passed on the UNPATCHED build too, because route_create_node passes
label = content when no label is supplied, so engram_first_n_chars is never
reached over HTTP. The test proved nothing. The real cause was only found
by decoding the actual failing bytes out of the live response.
2026-08-16 12:03:03 -05:00
7 changed files with 1557 additions and 155 deletions
+33 -1
View File
@@ -1118,6 +1118,11 @@ 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")
@@ -1126,12 +1131,31 @@ 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")
return engram_assert_json(claim, for_whom, 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)
}
fn route_attend(method: String, path: String, body: String) -> String {
let node: String = json_get_string(body, "node")
@@ -1885,6 +1909,14 @@ 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)
}
+40
View File
@@ -0,0 +1,40 @@
#!/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"
+176
View File
@@ -0,0 +1,176 @@
/* 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;
}
+657 -107
View File
@@ -3479,28 +3479,74 @@ 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, '"');
for (; *s; s++) {
unsigned char c = (unsigned char)*s;
const unsigned char* p = (const unsigned char*)s;
while (*p) {
unsigned char c = *p;
switch (c) {
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;
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;
}
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, '"');
}
@@ -5517,6 +5563,45 @@ 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);
@@ -8017,10 +8102,14 @@ 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 = strlen(s);
if (l > n) l = n;
size_t l = el_utf8_safe_len(s, n);
char* out = el_strbuf(l);
memcpy(out, s, l);
out[l] = '\0';
@@ -9651,18 +9740,17 @@ 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. */
#define ENGRAM_DECAY_FLOOR 0.25
/* 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. */
static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) {
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;
return cog_decay_factor(now_ms - n->last_activated,
(double)n->activation_count,
n->temporal_decay_rate);
}
/* Activation dampening: high activation_count nodes are "well-known" context
@@ -14336,9 +14424,30 @@ 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) memcpy(anchor, n->emb,
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);
}
}
}
free(id);
@@ -14366,88 +14475,522 @@ el_val_t engram_think_json(el_val_t seeds, el_val_t faculty) {
return el_wrap_str(b.buf);
}
/* 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. */
/* ═══════════════════════════════════════════════════════════════════════════
* §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. */
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* 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 */
const char* fw = EL_CSTR(for_whom); if (fw && !*fw) fw = NULL;
int64_t now = engram_now_ms();
/* 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");
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);
jb_puts(&b, t);
engram_geo_free(C); engram_geo_free(E);
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);
return el_wrap_str(b.buf);
}
/* 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) {
/* 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) {
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;
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");
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" : "-");
jb_puts(&b, t);
return el_wrap_str(b.buf);
}
@@ -14547,15 +15090,22 @@ 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,\"keystone_write_blocked\":%s}",
"\"reliability\":%.6g,\"n_trials\":%lld,\"stance_written\":%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.keystone ? "true" : "false");
st.reliability, (long long)st.n_trials, wrote == 0 ? "true" : "false");
jb_puts(&b, t);
cog_stance_free(&st); engram_geo_free(g);
return el_wrap_str(b.buf);
+10 -1
View File
@@ -666,6 +666,10 @@ 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);
el_val_t engram_node_count(void);
/* Attach a Geometry to an existing node, and read the attached width back.
* Named for the operation, not the store: a node acquires geometry. This is
@@ -728,8 +732,13 @@ 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_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor);
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_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);
+372 -29
View File
@@ -246,14 +246,6 @@ 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;
@@ -261,35 +253,386 @@ 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);
}
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; }
}
if (match) { found = 1; if (edges[i].weight > best) best = edges[i].weight; }
}
store_edges_free(edges, n);
if (!found) return 0; /* ungrounded => refuse assertion (still held) */
return (best >= floor) ? 1 : 0;
/* ═══════════════════════════════════════════════════════════════════════════
* §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);
}
}
free(copy);
}
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;
}
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);
if (stance->keystone) { learn = 0; out->wrote_keystone = 1; } /* §6: never write a keystone */
/* 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. */
GeoGradient g;
if (engram_think(region, anchor, stance, &g) != 0) return -1; /* PREDICTION */
+269 -17
View File
@@ -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, grounded-by edges) is the
* over their inputs. Persistence (Stance <-> StoreNode, edge grounding vectors) 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,30 +152,25 @@ 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 a RELATION a "grounded-by" edge, probabilistic, grounded-for-whom.
* The honesty floor is checked only at ASSERTION.
* 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.
* */
#define COG_GROUNDED_BY_RELATION "grounded-by"
/* 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_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)
@@ -209,8 +204,265 @@ 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 */