Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ef4a94062 | |||
| 285a7a50b3 | |||
| 6b61bb7224 | |||
| 8d34b33bce | |||
| d6b7f5dbdd | |||
| 9a24803917 | |||
| a6611dc19e | |||
| 4a57b4faa8 | |||
| 0ee82d9e91 | |||
| 9526bda507 | |||
| 0389bf9363 | |||
| fe820928b0 | |||
| 385c18442d | |||
| cace6a5ebf | |||
| 7a1501d097 | |||
| d41645388a | |||
| 8a307dfd42 | |||
| 616815b2ab | |||
| 1a8a966cb3 | |||
| 1f70b9fa18 | |||
| 317466e8f7 | |||
| eb3e6d7c1f | |||
| 26af149aa1 | |||
| c18abf799c | |||
| b305b49f40 | |||
| 8ae163e8e5 | |||
| 3fcc36c2f1 |
+187
-53
@@ -10,10 +10,60 @@
|
||||
// cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c
|
||||
// ./engram
|
||||
//
|
||||
// Configuration via environment:
|
||||
// ENGRAM_BIND — host:port (default :8742)
|
||||
// ENGRAM_API_KEY — bearer auth (optional)
|
||||
// ENGRAM_DATA_DIR — snapshot location (default ~/.neuron/engram)
|
||||
// Configuration is DECLARED, not scattered. See the `program` block below:
|
||||
// every knob's type and default lives there and nowhere else, is resolved from
|
||||
// the environment (env wins, declaration is the fallback) and validated before
|
||||
// any statement of this file runs. Read one with config("NAME") -> String.
|
||||
//
|
||||
// The one deliberate exception is ENGRAM_DATA_DIR — see the note in the block.
|
||||
|
||||
// ── Program declaration (cross-cutting concerns) ──────────────────────────────
|
||||
//
|
||||
// singleton: two engram processes against one data dir is data loss, not a
|
||||
// warning. The runtime takes an exclusive flock at startup and a second start
|
||||
// is refused loudly with the holder's pid.
|
||||
//
|
||||
// NOT declared here, on purpose: ENGRAM_DATA_DIR. Its resolution is owned by
|
||||
// engram_resolve_data_dir() (el_runtime.c), which defaults to $HOME/.neuron/engram
|
||||
// and fails LOUD rather than silently persisting to an ephemeral directory.
|
||||
// Declaring a default for it here as well would put the data dir's fallback in
|
||||
// two places — which is precisely the defect this migration removes (until
|
||||
// 2026-08-15 the reseed backup path carried its own "/tmp/engram" default that
|
||||
// disagreed with the resolver, so the pre-destructive safety copy landed in /tmp).
|
||||
// HOME is likewise not declared: it is a genuine environment read, not a knob.
|
||||
program "engram" {
|
||||
singleton: "engram"
|
||||
|
||||
// ── Core server ──
|
||||
env ENGRAM_BIND: String = ":8742"
|
||||
// Default "" leaves auth DISABLED (check_auth_ok short-circuits to true on an
|
||||
// empty key). That is the pre-existing behaviour and is deliberately preserved
|
||||
// here; making this `required` is the obvious hardening follow-up, but it is a
|
||||
// behaviour change and out of scope for this migration.
|
||||
env ENGRAM_API_KEY: String = ""
|
||||
|
||||
// ── Feature flags (bool-ish Strings; the predicate fns below own truthiness) ──
|
||||
env ENGRAM_STORE: String = "off"
|
||||
env ENGRAM_WAL: String = "off"
|
||||
env ENGRAM_AUTOCONNECT: String = "off"
|
||||
env ENGRAM_ISE_OFFGRAPH: String = "off"
|
||||
|
||||
// ── ISE telemetry ──
|
||||
env ENGRAM_ISE_RETENTION_MS: Int = "172800000"
|
||||
|
||||
// ── Guide (local Qwen3 via llama-server) ──
|
||||
env GUIDE_ENABLE: String = "off"
|
||||
env GUIDE_TIER_FORCE: String = ""
|
||||
env GUIDE_CACHE_DIR: String = ""
|
||||
env GUIDE_RAM_GB_4B: Int = "16"
|
||||
env GUIDE_RAM_GB_1P7B: Int = "8"
|
||||
env GUIDE_BACKEND: String = "llama-server"
|
||||
env GUIDE_HOST: String = "127.0.0.1"
|
||||
env GUIDE_PORT: Int = "8771"
|
||||
env GUIDE_LLAMA_SERVER_BIN: String = "llama-server"
|
||||
env GUIDE_NGL: Int = "99"
|
||||
env GUIDE_CTX: Int = "4096"
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -133,7 +183,7 @@ fn route_text_health(method: String, path: String, body: String) -> String {
|
||||
// engram_store_enabled() in el_runtime.c EXACTLY (1 / on / true). Default off →
|
||||
// every persistence path below is byte-for-byte the historical snapshot behavior.
|
||||
fn store_on() -> Bool {
|
||||
let v: String = env("ENGRAM_STORE")
|
||||
let v: String = config("ENGRAM_STORE")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -162,7 +212,6 @@ fn persist_canonical() -> Int {
|
||||
if store_on() {
|
||||
return engram_store_checkpoint()
|
||||
}
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
// (2026-08-10 self-review) This returned a hardcoded 1, which made every
|
||||
// caller's `let saved: Int = persist_canonical()` a dead variable — six
|
||||
@@ -176,7 +225,7 @@ fn persist_canonical() -> Int {
|
||||
// per-write full-snapshot behavior. When ON, structural mutations append O(1)
|
||||
// WAL records instead of rewriting the whole graph, with threshold compaction.
|
||||
fn wal_on() -> Bool {
|
||||
str_eq(env("ENGRAM_WAL"), "on")
|
||||
str_eq(config("ENGRAM_WAL"), "on")
|
||||
}
|
||||
|
||||
// autoconnect_on — ENGRAM_AUTOCONNECT. Will's rule: "we shouldn't be inserting
|
||||
@@ -184,7 +233,7 @@ fn wal_on() -> Bool {
|
||||
// edge (kNN over embeddings) so no content node enters the graph edgeless.
|
||||
// Default OFF -> byte-identical to prior behavior (node created, no auto edges).
|
||||
fn autoconnect_on() -> Bool {
|
||||
let v: String = env("ENGRAM_AUTOCONNECT")
|
||||
let v: String = config("ENGRAM_AUTOCONNECT")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -197,7 +246,7 @@ fn autoconnect_on() -> Bool {
|
||||
// separate state-event log tier instead of the node graph. Default OFF -> ISEs
|
||||
// remain graph nodes exactly as before (with 48h prune).
|
||||
fn ise_offgraph_on() -> Bool {
|
||||
let v: String = env("ENGRAM_ISE_OFFGRAPH")
|
||||
let v: String = config("ENGRAM_ISE_OFFGRAPH")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -247,6 +296,24 @@ fn persist_bulk() -> Int {
|
||||
return persist_canonical()
|
||||
}
|
||||
|
||||
// COMPILER LANDMINE, measured 2026-08-16 — do not inline this back into the
|
||||
// caller. elc lowers `a == b` to numeric comparison only when both operand
|
||||
// NAMES are in the per-function int-name set, which `let x: Int` populates.
|
||||
// That registration does NOT propagate into a nested if-expression block: the
|
||||
// first cut of the geometry-ingest path wrote `let claimed: Int = ...` and
|
||||
// `let got: Int = ...` inside the else-arm and `claimed == got` came out of
|
||||
// codegen as `str_eq(claimed, got)` — strcmp on two integers reinterpreted as
|
||||
// pointers, i.e. a segfault on the first geometry-bearing request. Read back
|
||||
// out of the generated C, not guessed. Function PARAMETERS annotated `: Int`
|
||||
// do register reliably (verified: `if (claimed == actual)`), so the comparison
|
||||
// lives in a function of its own. Note also the explicit `return`s — a trailing
|
||||
// if-EXPRESSION at a function tail emits as a statement and the function
|
||||
// returns 0 regardless, which is the same probe's second finding.
|
||||
fn width_agrees(claimed: Int, actual: Int) -> Int {
|
||||
if claimed == actual { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
// INCOMPLETE-ROUTE FIX (2026-07-24 self-review): this route silently dropped
|
||||
// label, importance, tier, and tags — engram_node() defaults label to content
|
||||
// and importance to 0.5, so every node created over HTTP lost its metadata.
|
||||
@@ -288,26 +355,44 @@ fn route_create_node(method: String, path: String, body: String) -> String {
|
||||
salience, importance, confidence,
|
||||
tier, tags
|
||||
)
|
||||
// GEOMETRY INGEST (2026-08-16 self-review): this route accepted an "emb"
|
||||
// field, returned 200 with a fresh id, and stored NOTHING — engram_node_full
|
||||
// has no vector parameter, so the caller's geometry was silently discarded
|
||||
// and the node came back emb_dim=None / embedded:false. Measured live while
|
||||
// trying to admit a voice signal. The consequence was structural, not
|
||||
// cosmetic: text was the only entry medium, so any non-text modality had to
|
||||
// be DESCRIBED in prose and what we then reasoned over was the geometry of
|
||||
// the description, not of the signal.
|
||||
// GEOMETRY INGEST — geometry-valued end to end (2026-08-16).
|
||||
//
|
||||
// "emb" is little-endian float32 hex (dim*8 chars) — the encoding the
|
||||
// perception vessel's /voice/embed already emits, so a realizer's output
|
||||
// moves in with no float-array round trip. "dim" defaults to the vector's
|
||||
// implied width. Off-dimension vectors are stored but not inserted into the
|
||||
// resident index (its build loop filters on emb_dim), so a modality vector
|
||||
// is durable and addressable without perturbing the canonical index.
|
||||
// The defect this route originally had: it accepted an "emb" field,
|
||||
// returned 200 with a fresh id, and stored NOTHING, because engram_node_full
|
||||
// has no vector parameter. The consequence was structural, not cosmetic —
|
||||
// text was the only entry medium, so any non-text modality had to be
|
||||
// DESCRIBED in prose, and what we then reasoned over was the geometry of the
|
||||
// description, not of the signal.
|
||||
//
|
||||
// #141 fixed the drop but marshalled the vector as a hex STRING through
|
||||
// engram_node_set_emb, which put text back as the TRANSPORT medium one layer
|
||||
// below the problem being fixed. This is that correction: hex is decoded
|
||||
// exactly ONCE, here at the edge, into a first-class Geometry, and every
|
||||
// step below this line moves geometry rather than text. An encoding at the
|
||||
// boundary is what an encoding is for.
|
||||
//
|
||||
// The WIRE is deliberately unchanged — "emb" is still little-endian float32
|
||||
// hex (8 chars per component), the encoding the perception vessel's
|
||||
// /voice/embed already emits — because production clients speak it. What
|
||||
// changed is underneath it.
|
||||
//
|
||||
// "dim" is now treated as an ASSERTION about the vector the caller sent, not
|
||||
// as the source of its width: a Geometry carries its own width. A stated dim
|
||||
// that disagrees is a REJECTED ingest, not a silent reinterpretation. Omitting
|
||||
// "dim" is fine and means "trust the vector", which is the honest default.
|
||||
//
|
||||
// Off-dimension vectors remain stored but not inserted into the resident HNSW
|
||||
// index (its build loop filters on emb_dim), so a 64-dim voice geometry is
|
||||
// durable and addressable without perturbing the 768-dim canonical index.
|
||||
let emb_hex: String = json_get_string(body, "emb")
|
||||
let emb_set: Int = if str_eq(emb_hex, "") { 0 } else {
|
||||
let g: Geometry = geometry_from_f32le_hex(emb_hex)
|
||||
let got: Int = geometry_dim(g)
|
||||
let dim_raw: String = json_get_raw(body, "dim")
|
||||
let dim: Int = if str_eq(dim_raw, "") { str_len(emb_hex) / 8 } else { json_get_int(body, "dim") }
|
||||
engram_node_set_emb(id, emb_hex, dim)
|
||||
let claimed: Int = if str_eq(dim_raw, "") { got } else { json_get_int(body, "dim") }
|
||||
let landed: Int = if width_agrees(claimed, got) > 0 { node_attach_geometry(id, g) } else { 0 }
|
||||
let freed: Int = geometry_free(g)
|
||||
landed
|
||||
}
|
||||
let saved: Int = persist_node(id)
|
||||
// ORPHAN PREVENTION (ENGRAM_AUTOCONNECT): connect the fresh node to its
|
||||
@@ -358,7 +443,6 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
||||
// process ever booted with a partial/empty store, the first read request
|
||||
// clobbered the good snapshot. Read routes must never write the canonical path.)
|
||||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let snap_path: String = dir + "/.scan-export.json"
|
||||
engram_save(snap_path)
|
||||
@@ -519,7 +603,6 @@ fn route_forget(method: String, path: String, body: String) -> String {
|
||||
|
||||
fn route_save(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
// (2026-08-10 self-review) engram_save returns 0 on an empty path and the
|
||||
@@ -603,7 +686,6 @@ fn route_drift(method: String, path: String, body: String) -> String {
|
||||
|
||||
fn route_load(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
// (2026-08-10 self-review) This was a stub response over the single most
|
||||
@@ -674,7 +756,6 @@ fn route_embed_backfill(method: String, path: String, body: String) -> String {
|
||||
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
|
||||
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
|
||||
fn route_sync(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
// 2026-07-21 self-review: export to a scratch path, never the canonical
|
||||
// snapshot.json — read routes must not be able to clobber the good snapshot.
|
||||
@@ -750,8 +831,12 @@ fn route_reseed_nodes(method: String, path: String, body: String) -> String {
|
||||
if str_eq(p, "") { return err_json("path is required") }
|
||||
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
|
||||
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
// (2026-08-15) This site carried its own "/tmp/engram" fallback, which
|
||||
// DISAGREED with engram_resolve_data_dir() ($HOME/.neuron/engram, fail-loud).
|
||||
// The consumer is the pre-destructive backup below, so with ENGRAM_DATA_DIR
|
||||
// unset the safety copy taken before a reseed landed in an ephemeral /tmp
|
||||
// while the store it was protecting lived elsewhere. One owner, one answer.
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let backup: String = dir + "/.reseed-backup.json"
|
||||
|
||||
let replace_raw: String = json_get_raw(body, "replace")
|
||||
@@ -843,8 +928,7 @@ fn route_emit_ise(method: String, path: String, body: String) -> String {
|
||||
sal, imp, conf,
|
||||
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
||||
)
|
||||
let ret_raw: String = env("ENGRAM_ISE_RETENTION_MS")
|
||||
let ret_ms: Int = if str_eq(ret_raw, "") { 172800000 } else { str_to_int(ret_raw) }
|
||||
let ret_ms: Int = str_to_int(config("ENGRAM_ISE_RETENTION_MS"))
|
||||
let pruned: Int = engram_prune_telemetry(ret_ms)
|
||||
"{\"ok\":true,\"id\":\"" + id + "\",\"pruned\":" + int_to_str(pruned) + "}"
|
||||
}
|
||||
@@ -941,6 +1025,22 @@ fn route_similarity(method: String, path: String, body: String) -> String {
|
||||
// nothing on request. NOTE: the offline reify WRITER (engram_geo_reify_store) is
|
||||
// currently unwired, so on the live store the resident index is empty and the
|
||||
// list returns [] until reification runs — see the cutover report.
|
||||
// route_scan_emb — GET /api/nodes/emb?limit=&offset= — read the raw geometry.
|
||||
//
|
||||
// engram_scan_nodes_emb_json has existed as a builtin with NO ROUTE, so the
|
||||
// embeddings — the actual positions every distance, angle, membership and
|
||||
// grounding is computed from — were unreadable from outside the process. You
|
||||
// cannot verify a coordinate system you cannot see, and every claim about the
|
||||
// frame (isotropy, centering, what the origin is) was therefore unfalsifiable
|
||||
// from the API. Read-only.
|
||||
fn route_scan_emb(method: String, path: String, body: String) -> String {
|
||||
let l_raw: String = query_param(path, "limit")
|
||||
let o_raw: String = query_param(path, "offset")
|
||||
let l: Int = if str_eq(l_raw, "") { 200 } else { str_to_int(l_raw) }
|
||||
let o: Int = if str_eq(o_raw, "") { 0 } else { str_to_int(o_raw) }
|
||||
return engram_scan_nodes_emb_json(l, o)
|
||||
}
|
||||
|
||||
fn route_neighborhoods(method: String, path: String, body: String) -> String {
|
||||
engram_geo_reify_list_json()
|
||||
}
|
||||
@@ -1034,6 +1134,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")
|
||||
@@ -1042,12 +1147,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")
|
||||
@@ -1093,14 +1217,12 @@ fn route_correspondence_beat(method: String, path: String, body: String) -> Stri
|
||||
// turns native thinking ON: the response carries reasoning_content (the thinking)
|
||||
// alongside content (the answer).
|
||||
|
||||
fn guide_env_or(key: String, dflt: String) -> String {
|
||||
let v: String = env(key)
|
||||
if str_eq(v, "") { return dflt }
|
||||
return v
|
||||
}
|
||||
// (2026-08-15) guide_env_or(key, dflt) lived here. Its whole job was supplying a
|
||||
// per-call-site default, which is now the program block's job — every GUIDE_* knob
|
||||
// is declared once at the top of this file and read straight through config().
|
||||
|
||||
fn guide_enabled() -> Bool {
|
||||
let v: String = env("GUIDE_ENABLE")
|
||||
let v: String = config("GUIDE_ENABLE")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -1145,15 +1267,15 @@ fn guide_probe_metal() -> Bool {
|
||||
|
||||
// ── 2. Tier selection (config-driven thresholds, spec-autoselected) ────────────
|
||||
fn guide_threshold_4b() -> Int {
|
||||
return str_to_int(guide_env_or("GUIDE_RAM_GB_4B", "16"))
|
||||
return str_to_int(config("GUIDE_RAM_GB_4B"))
|
||||
}
|
||||
fn guide_threshold_1p7b() -> Int {
|
||||
return str_to_int(guide_env_or("GUIDE_RAM_GB_1P7B", "8"))
|
||||
return str_to_int(config("GUIDE_RAM_GB_1P7B"))
|
||||
}
|
||||
|
||||
// GUIDE_TIER_FORCE overrides the spec autoselect (used to prove cheaply on 0.6b).
|
||||
fn guide_select_tier(ram_gb: Int) -> String {
|
||||
let forced: String = env("GUIDE_TIER_FORCE")
|
||||
let forced: String = config("GUIDE_TIER_FORCE")
|
||||
if !str_eq(forced, "") { return forced }
|
||||
if ram_gb >= guide_threshold_4b() { return "4b" }
|
||||
if ram_gb >= guide_threshold_1p7b() { return "1.7b" }
|
||||
@@ -1173,8 +1295,10 @@ fn guide_file(tier: String) -> String {
|
||||
}
|
||||
|
||||
fn guide_cache_dir() -> String {
|
||||
let c: String = env("GUIDE_CACHE_DIR")
|
||||
let c: String = config("GUIDE_CACHE_DIR")
|
||||
if !str_eq(c, "") { return c }
|
||||
// HOME stays a raw env() read: it is the ambient environment, not a knob of
|
||||
// this program, and it is deliberately absent from the program block.
|
||||
let home: String = env("HOME")
|
||||
if !str_eq(home, "") { return home + "/.neuron/guide/models" }
|
||||
return engram_resolve_data_dir() + "/guide-models"
|
||||
@@ -1215,9 +1339,9 @@ fn guide_fetch(tier: String) -> Bool {
|
||||
}
|
||||
|
||||
// ── 4/5. Backend abstraction + BIND as an engageable interlocutor ──────────────
|
||||
fn guide_backend() -> String { return guide_env_or("GUIDE_BACKEND", "llama-server") }
|
||||
fn guide_host() -> String { return guide_env_or("GUIDE_HOST", "127.0.0.1") }
|
||||
fn guide_port() -> String { return guide_env_or("GUIDE_PORT", "8771") }
|
||||
fn guide_backend() -> String { return config("GUIDE_BACKEND") }
|
||||
fn guide_host() -> String { return config("GUIDE_HOST") }
|
||||
fn guide_port() -> String { return config("GUIDE_PORT") }
|
||||
fn guide_base_url() -> String { return "http://" + guide_host() + ":" + guide_port() }
|
||||
|
||||
// guide_healthy — is the guide present and answering? llama-server's /health
|
||||
@@ -1235,9 +1359,9 @@ fn guide_healthy() -> Bool {
|
||||
fn guide_load(tier: String) -> Bool {
|
||||
if guide_healthy() { return true }
|
||||
let path: String = guide_model_path(tier)
|
||||
let bin: String = guide_env_or("GUIDE_LLAMA_SERVER_BIN", "llama-server")
|
||||
let ngl: String = guide_env_or("GUIDE_NGL", "99")
|
||||
let ctx: String = guide_env_or("GUIDE_CTX", "4096")
|
||||
let bin: String = config("GUIDE_LLAMA_SERVER_BIN")
|
||||
let ngl: String = config("GUIDE_NGL")
|
||||
let ctx: String = config("GUIDE_CTX")
|
||||
let logf: String = guide_cache_dir() + "/llama-server." + guide_port() + ".log"
|
||||
let cmd: String = bin + " -m '" + path + "' --host " + guide_host() + " --port " + guide_port() + " -c " + ctx + " -ngl " + ngl + " --jinja >> '" + logf + "' 2>&1"
|
||||
let pid: String = exec_bg(cmd)
|
||||
@@ -1633,7 +1757,7 @@ fn route_supersede(method: String, path: String, body: String) -> String {
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn check_auth_ok(method: String, body: String) -> Bool {
|
||||
let key: String = env("ENGRAM_API_KEY")
|
||||
let key: String = config("ENGRAM_API_KEY")
|
||||
if str_eq(key, "") { return true }
|
||||
// Read-only methods don't require auth. Until http_serve surfaces
|
||||
// request headers we can't accept a Bearer token cleanly; mutating
|
||||
@@ -1712,6 +1836,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
|
||||
return route_scan_edges(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && (str_eq(clean, "/api/nodes/emb") || str_eq(clean, "/nodes/emb")) {
|
||||
return route_scan_emb(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") {
|
||||
return route_get_node(method, path, body)
|
||||
}
|
||||
@@ -1801,6 +1928,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)
|
||||
}
|
||||
@@ -1896,8 +2031,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
|
||||
// ── Entry ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
let bind_raw: String = env("ENGRAM_BIND")
|
||||
let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw }
|
||||
let bind_str: String = config("ENGRAM_BIND")
|
||||
let port: Int = parse_port(bind_str)
|
||||
|
||||
// On startup, try to load any existing snapshot (best effort).
|
||||
|
||||
Executable
+40
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
+27
-12
@@ -13,7 +13,7 @@
|
||||
// relations add edges. Every node enters with PROVENANCE + grounding-level
|
||||
// + stewardship class from the moment of entry.
|
||||
//
|
||||
// transduce() is THE single mechanism — one function, polymorphic, with no
|
||||
// transduce_manifold() is THE single mechanism — one function, polymorphic, with no
|
||||
// content-type branch inside it. It does not ask whether a payload is
|
||||
// prose, structured data, or raw/opaque bytes (audio, or anything else);
|
||||
// it runs one boundary-scan-with-fixed-window-fallback chunking algorithm
|
||||
@@ -401,10 +401,25 @@ fn head80(s: String) -> String {
|
||||
// truncates at the first embedded NUL, which is routine in real binary
|
||||
// bytes) is a MECHANICAL fidelity concern that belongs to whatever produced
|
||||
// `source` (see ingest_file's file_source_string below) — not a
|
||||
// content-type judgment made in here. transduce() never learns whether a
|
||||
// content-type judgment made in here. transduce_manifold() never learns whether a
|
||||
// chunk is plain text or a base64-encoded raw-byte window; every chunk is
|
||||
// handled identically either way.
|
||||
fn transduce(nodes: [String], edges: [String], source: String,
|
||||
// RENAMED transduce -> transduce_manifold (2026-08-16). Two reasons, and the
|
||||
// first is not the interesting one:
|
||||
//
|
||||
// 1. Mechanical: `transduce` is now a LANGUAGE primitive in el_runtime.h
|
||||
// (transduce(signal, modality) -> Geometry). Every El `fn name(...)`
|
||||
// compiles to a global C symbol with that exact name, so keeping this
|
||||
// name here is a hard `conflicting types for 'transduce'` compile error
|
||||
// the moment ingest.c links el_runtime.c. Measured, not anticipated.
|
||||
//
|
||||
// 2. Actual: this function was never signal->geometry. It chunks already-
|
||||
// extracted content and PACKS it into a node+edge manifold — a real
|
||||
// operation, but one layer up, and it had taken the name that belongs to
|
||||
// the primitive underneath it. `transduce` is where a signal becomes
|
||||
// geometry; `transduce_manifold` is where extracted content becomes
|
||||
// structure. Nothing about this function's behaviour changed.
|
||||
fn transduce_manifold(nodes: [String], edges: [String], source: String,
|
||||
prov: String, ground: String, steward: String,
|
||||
root_lid: String, root_title: String) -> [String] {
|
||||
let tagbase: String = "prov:" + prov + " ground:" + ground + " steward:" + steward
|
||||
@@ -531,8 +546,8 @@ fn default_steward() -> String {
|
||||
// trustworthy verbatim. When they don't (silent truncation happened),
|
||||
// rebuild the payload as base64-encoded fixed-size windows read directly
|
||||
// off disk (fs_read_b64_chunk — binary-safe in C), joined with the same
|
||||
// "\n\n" boundary marker transduce()'s generic scan already looks for, so
|
||||
// transduce() sees one ordinary boundary-delimited payload and runs its one
|
||||
// "\n\n" boundary marker transduce_manifold()'s generic scan already looks for, so
|
||||
// transduce_manifold() sees one ordinary boundary-delimited payload and runs its one
|
||||
// algorithm on it exactly as it would on prose — it never learns that a
|
||||
// fidelity problem occurred upstream, let alone why.
|
||||
fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
||||
@@ -541,7 +556,7 @@ fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
||||
// 3072 raw bytes -> 4096 base64 chars (3 divides evenly into base64's
|
||||
// 3-byte/4-char ratio); keeps each resulting node's content a clean,
|
||||
// bounded, low-kilobytes unit, same order of magnitude as the fixed
|
||||
// fallback window in transduce() itself.
|
||||
// fallback window in transduce_manifold() itself.
|
||||
let win: Int = 3072
|
||||
let out: String = ""
|
||||
let off: Int = 0
|
||||
@@ -561,7 +576,7 @@ fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
||||
}
|
||||
|
||||
// ingest one file -> report JSON. Uniform for every file regardless of
|
||||
// extension or content — transduce() decides nothing about content-type, so
|
||||
// extension or content — transduce_manifold() decides nothing about content-type, so
|
||||
// neither does this function; it only decides whether the raw bytes made it
|
||||
// through the read intact (file_source_string), which is a fidelity
|
||||
// question, not a format one.
|
||||
@@ -573,14 +588,14 @@ fn ingest_file(path: String) -> String {
|
||||
return "{\"error\":\"empty or unreadable\",\"path\":" + j_q(path) + "}"
|
||||
}
|
||||
let prov: String = "file:" + path
|
||||
let packed: [String] = transduce(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
source, prov, default_ground(), default_steward(),
|
||||
"doc:" + basename(path), basename(path))
|
||||
return merge_packed(packed)
|
||||
}
|
||||
|
||||
// ingest a directory: walk one level, ingest every file found, aggregate.
|
||||
// No extension filter — transduce() handles any payload uniformly now, so
|
||||
// No extension filter — transduce_manifold() handles any payload uniformly now, so
|
||||
// there is no content-type gate at the directory boundary either.
|
||||
fn ingest_dir(path: String) -> String {
|
||||
let entries: [String] = fs_list(path)
|
||||
@@ -615,7 +630,7 @@ fn ingest_dir(path: String) -> String {
|
||||
fn ingest_url(url: String) -> String {
|
||||
let body: String = http_get(url)
|
||||
if str_eq(body, "") { return "{\"error\":\"empty fetch\",\"url\":" + j_q(url) + "}" }
|
||||
let packed: [String] = transduce(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
body, "url:" + url, "extracted", "public-web",
|
||||
"url:" + url, url)
|
||||
return merge_packed(packed)
|
||||
@@ -630,7 +645,7 @@ fn ingest_llm(query: String) -> String {
|
||||
let resp: String = http_post_json("http://127.0.0.1:11434/api/generate", body)
|
||||
let answer: String = json_get_string(resp, "response")
|
||||
if str_eq(answer, "") { return "{\"error\":\"no model response\"}" }
|
||||
let packed: [String] = transduce(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
answer, "llm:" + model + ":" + query, "candidate-provisional", "guide-provisional",
|
||||
"llm:" + query, "guide answer: " + query)
|
||||
return merge_packed(packed)
|
||||
@@ -682,7 +697,7 @@ fn ingest_stream(path: String) -> String {
|
||||
// It is NOT a content-type flag: it says nothing about what's inside the
|
||||
// bytes once fetched, and none of the five ingest_* functions it selects
|
||||
// among interpret their payload differently by content shape anymore —
|
||||
// they all hand off to the single, format-agnostic transduce(). The old
|
||||
// they all hand off to the single, format-agnostic transduce_manifold(). The old
|
||||
// "structured" value (a caller-declared alias for "file", used only to hint
|
||||
// the now-removed JSON-vs-prose branch) is gone along with that branch.
|
||||
let kind: String = env("INGEST_KIND")
|
||||
|
||||
@@ -73,6 +73,17 @@ When you add a C builtin (verbatim-emit recipe — the El name is emitted as the
|
||||
2. Add a `__`-prefixed thin wrapper in `el_seed.c` and declare it in `el_seed.h`.
|
||||
3. Add the name to `builtin_arity` in `el-compiler/src/codegen.el` — add **both** the plain and `__`-prefixed spellings.
|
||||
4. Rebuild the elc binary (see below) and confirm the self-host fixpoint is byte-identical.
|
||||
5. **Prove it with a NEGATIVE CONTROL.** Show the test FAILING on a build without your change, then passing with it. A test that has never been seen to fail has proven nothing.
|
||||
|
||||
> **Step 5 is not optional, and step 4 does not cover it.** The fixpoint proves the *compiler reproduces itself*. It says nothing whatsoever about whether your builtin works. A recipe ending at "byte-identical" reads as complete while having verified nothing about the thing just added — which is why this file, until 2026-08-16, produced builtins with no tests at all.
|
||||
>
|
||||
> Measured cost of the omission (2026-08-16): `engram_node_set_emb`, `engram_curiosity_json` and `dream_set_handler` were all added in one session with zero tests. Separately, a UTF-8 fix was written, tested, and **the test passed on the unpatched build too** — the defect was elsewhere entirely, and only building the pre-fix binary exposed it. Without a negative control that fix would have merged as verified.
|
||||
>
|
||||
> Two shapes that pass while proving nothing, both hit the same day:
|
||||
> - A test that never exercises your change (the route supplied a default that bypassed the code under test).
|
||||
> - An induction that loses a race. `curl --max-time` on a large response left *both* builds alive; only `SO_LINGER 0` — a genuine RST, so the peer is provably gone — reproduced the failure. Six of ten attempts is not a control.
|
||||
>
|
||||
> Before every probe, confirm **your** process bound the port (`lsof -nP -iTCP:<port>`, match the PID). A stale instance answering on the port has silently produced false results here more than once, and `pkill -f` does not reliably match an argv like `./engram`.
|
||||
|
||||
Worked example: the `engram_assert_json` (op_assert seam) and `engram_node_full_in`/`engram_connect_in` (purview write-side) primitives added 2026-08-15 follow exactly this recipe.
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -3265,6 +3265,7 @@ fn is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
||||
if kind == "EnumDef" { return true }
|
||||
if kind == "Import" { return true }
|
||||
if kind == "CgiBlock" { return true }
|
||||
if kind == "ProgramBlock" { return true }
|
||||
if kind == "ExternFn" { return true }
|
||||
false
|
||||
}
|
||||
@@ -3277,6 +3278,55 @@ fn cgi_arg(value: String, has_value: Bool) -> String {
|
||||
return "EL_NULL"
|
||||
}
|
||||
|
||||
// -- Program block: cross-cutting concerns injected at the process boundary ----
|
||||
//
|
||||
// emit_program_init — emit the `static void __el_program_init(void)` that
|
||||
// carries a program's declared cross-cutting concerns. Called from main()
|
||||
// BEFORE any user statement runs, so the guarantees hold for the whole process
|
||||
// rather than depending on each call site remembering to ask for them.
|
||||
//
|
||||
// This is emitted at the point the `program` block is encountered, not buffered
|
||||
// until main(). The streaming backend emits in source order and cannot hold a
|
||||
// declaration's entry list alive until main(); emitting a named function here
|
||||
// and calling it from main() means only a single bool has to survive.
|
||||
//
|
||||
// Order matters and is deliberate:
|
||||
// 1. singleton FIRST — if another instance already holds the lock, refuse and
|
||||
// exit before touching configuration, ports, or any data directory.
|
||||
// 2. config declarations — resolve env-or-default, one declaration per entry.
|
||||
// 3. validate LAST — report EVERY missing/ill-typed entry at once, then exit.
|
||||
fn el_bool_arg(b: Bool) -> String {
|
||||
if b { return "EL_INT(1)" }
|
||||
return "EL_INT(0)"
|
||||
}
|
||||
|
||||
fn emit_program_init(stmt: Map<String, Any>) -> Void {
|
||||
let pname: String = stmt["name"]
|
||||
emit_line("static void __el_program_init(void) {")
|
||||
let has_singleton: Bool = stmt["has_singleton"]
|
||||
if has_singleton {
|
||||
let sid: String = stmt["singleton"]
|
||||
emit_line(" el_singleton_acquire(EL_STR(" + c_str_lit(sid) + "));")
|
||||
}
|
||||
let entries = stmt["entries"]
|
||||
let n: Int = native_list_len(entries)
|
||||
let i = 0
|
||||
while i < n {
|
||||
let e = native_list_get(entries, i)
|
||||
let ename: String = e["name"]
|
||||
let etype: String = e["etype"]
|
||||
let edefault: String = e["default"]
|
||||
let has_default: Bool = e["has_default"]
|
||||
let erequired: Bool = e["required"]
|
||||
let arg_def: String = cgi_arg(edefault, has_default)
|
||||
emit_line(" el_config_declare(EL_STR(" + c_str_lit(ename) + "), EL_STR(" + c_str_lit(etype) + "), " + arg_def + ", " + el_bool_arg(has_default) + ", " + el_bool_arg(erequired) + ");")
|
||||
let i = i + 1
|
||||
}
|
||||
emit_line(" el_config_validate(EL_STR(" + c_str_lit(pname) + "));")
|
||||
emit_line("}")
|
||||
emit_blank()
|
||||
}
|
||||
|
||||
// -- VBD role enforcement ------------------------------------------------------
|
||||
//
|
||||
// Scan a function body for direct calls to DHARMA-restricted builtins
|
||||
@@ -3599,6 +3649,20 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Program block: emit the cross-cutting init function before the user's
|
||||
// functions so main() can call it (see emit_program_init).
|
||||
let prog_have: Bool = false
|
||||
let i = 0
|
||||
while i < n {
|
||||
let stmt = native_list_get(stmts, i)
|
||||
let sk4: String = stmt["stmt"]
|
||||
if str_eq(sk4, "ProgramBlock") {
|
||||
emit_program_init(stmt)
|
||||
let prog_have = true
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Function definitions
|
||||
let i = 0
|
||||
while i < n {
|
||||
@@ -3617,6 +3681,9 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
|
||||
// with the C-side parameters when fn main()'s body is folded in below.
|
||||
emit_line("int main(int _argc, char** _argv) {")
|
||||
emit_line(" el_runtime_init_args(_argc, _argv);")
|
||||
if prog_have {
|
||||
emit_line(" __el_program_init();")
|
||||
}
|
||||
if cgi_count >= 1 {
|
||||
let cname: String = cgi_block["name"]
|
||||
let cdid: String = cgi_block["dharma_id"]
|
||||
@@ -4210,6 +4277,7 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
// Fix: copy the values out BEFORE the release (strings, so no dangling reference)
|
||||
// and emit from these. No search, so the failure mode is removed rather than moved.
|
||||
let cgi_have: Bool = false
|
||||
let prog_have: Bool = false
|
||||
let cgi_name_v: String = ""
|
||||
let cgi_did_v: String = ""
|
||||
let cgi_prin_v: String = ""
|
||||
@@ -4331,6 +4399,14 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
// These are no-ops in codegen (forward decls already emitted)
|
||||
// — except a CgiBlock, whose declared identity must survive
|
||||
// this release to be emitted as a compiled constant.
|
||||
// A ProgramBlock's cross-cutting declarations are
|
||||
// emitted HERE, as a named init function, because the
|
||||
// streaming backend cannot hold the entry list alive
|
||||
// until main(). Only the bool survives.
|
||||
if str_eq(sk, "ProgramBlock") {
|
||||
emit_program_init(stmt)
|
||||
let prog_have = true
|
||||
}
|
||||
if str_eq(sk, "CgiBlock") {
|
||||
let cgi_have = true
|
||||
let cgi_name_v = stmt["name"]
|
||||
@@ -4477,6 +4553,13 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
let kind2: String = state_get("__program_kind")
|
||||
emit_line("int main(int _argc, char** _argv) {")
|
||||
emit_line(" el_runtime_init_args(_argc, _argv);")
|
||||
// Cross-cutting concerns declared by a `program` block run BEFORE anything
|
||||
// else — a singleton violation must refuse the start before this process
|
||||
// touches a port or a data directory, and configuration must be validated
|
||||
// before the first read of it rather than at each read site.
|
||||
if prog_have {
|
||||
emit_line(" __el_program_init();")
|
||||
}
|
||||
|
||||
// cgi init if needed
|
||||
let ns2: Int = native_list_len(sigs)
|
||||
|
||||
@@ -184,6 +184,7 @@ fn keyword_kind(word: String) -> String {
|
||||
if word == "false" { return "Bool" }
|
||||
if word == "cgi" { return "Cgi" }
|
||||
if word == "service" { return "Service" }
|
||||
if word == "program" { return "Program" }
|
||||
if word == "manager" { return "Manager" }
|
||||
if word == "engine" { return "Engine" }
|
||||
if word == "accessor" { return "Accessor" }
|
||||
|
||||
@@ -1967,6 +1967,113 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
}, p)
|
||||
}
|
||||
|
||||
// program block: program "name" { singleton: "id", env NAME: Type = "default", ... }
|
||||
//
|
||||
// The program block is El's declaration surface for CROSS-CUTTING CONCERNS —
|
||||
// properties of the whole process rather than of any one function, which
|
||||
// otherwise degrade into "remember to call this at every site" conventions.
|
||||
//
|
||||
// singleton: "id" — process identity. The runtime takes an exclusive
|
||||
// lock at startup; a SECOND start is refused, loudly,
|
||||
// instead of two processes sharing one data dir.
|
||||
// env NAME: T = "d" — one configuration entry. Its type and its default
|
||||
// are declared ONCE, here, and resolved+validated
|
||||
// before main() body runs.
|
||||
// env NAME: T required
|
||||
// — no default; the program refuses to start unless the
|
||||
// variable is set.
|
||||
//
|
||||
// Both compile into calls injected at the head of main() — the same boundary
|
||||
// seam `cgi` already uses (codegen.el emit_program_init). No call site in the
|
||||
// program body has to remember anything, which is the whole point.
|
||||
if k == "Program" {
|
||||
let p = pos + 1
|
||||
let name = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let p = expect(tokens, p, "LBrace")
|
||||
let singleton = ""
|
||||
let has_singleton = false
|
||||
let entries = native_list_empty()
|
||||
// Entry-scratch declared at loop-body level (not inside the branch) so
|
||||
// that inner `let` forms compile to assignment rather than a C-scoped
|
||||
// redeclaration — the same idiom the service block above relies on.
|
||||
let ename = ""
|
||||
let etype = ""
|
||||
let edefault = ""
|
||||
let has_default = false
|
||||
let erequired = false
|
||||
let fname = ""
|
||||
let fval = ""
|
||||
let running = true
|
||||
while running {
|
||||
let k2 = tok_kind(tokens, p)
|
||||
if k2 == "RBrace" {
|
||||
let running = false
|
||||
} else {
|
||||
if k2 == "Eof" {
|
||||
let running = false
|
||||
} else {
|
||||
let fname = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
if str_eq(fname, "env") {
|
||||
// env NAME: Type [= "default"] [required]
|
||||
let ename = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let p = expect(tokens, p, "Colon")
|
||||
let etype = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let edefault = ""
|
||||
let has_default = false
|
||||
let erequired = false
|
||||
let k3 = tok_kind(tokens, p)
|
||||
if str_eq(k3, "Eq") {
|
||||
let p = p + 1
|
||||
let edefault = tok_value(tokens, p)
|
||||
let has_default = true
|
||||
let p = p + 1
|
||||
}
|
||||
let k4 = tok_kind(tokens, p)
|
||||
if str_eq(k4, "Ident") {
|
||||
let w = tok_value(tokens, p)
|
||||
if str_eq(w, "required") {
|
||||
let erequired = true
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
let entries = native_list_append(entries, {
|
||||
"name": ename,
|
||||
"etype": etype,
|
||||
"default": edefault,
|
||||
"has_default": has_default,
|
||||
"required": erequired
|
||||
})
|
||||
} else {
|
||||
// scalar field: `name: "value"`
|
||||
let p = expect(tokens, p, "Colon")
|
||||
let fval = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
if str_eq(fname, "singleton") {
|
||||
let singleton = fval
|
||||
let has_singleton = true
|
||||
}
|
||||
}
|
||||
let k5 = tok_kind(tokens, p)
|
||||
if k5 == "Comma" {
|
||||
let p = p + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = expect(tokens, p, "RBrace")
|
||||
return make_result({
|
||||
"stmt": "ProgramBlock",
|
||||
"name": name,
|
||||
"singleton": singleton,
|
||||
"has_singleton": has_singleton,
|
||||
"entries": entries
|
||||
}, p)
|
||||
}
|
||||
|
||||
// assert <cond_expr> [ , <msg_expr> ]
|
||||
// The message is optional — if the next token after the condition is not a
|
||||
// Comma, emit an empty string placeholder so the test still works.
|
||||
@@ -2419,6 +2526,7 @@ fn scan_params_c(tokens: [Any], pos: Int) -> Map<String, Any> {
|
||||
// toplevel_let: { "kind": "toplevel_let", "name": String, "ltype": String }
|
||||
// cgi_block: { "kind": "cgi_block", "name": String }
|
||||
// service_block: { "kind": "service_block", "name": String }
|
||||
// program_block: { "kind": "program_block", "name": String }
|
||||
//
|
||||
// Import/TypeDef/EnumDef nodes are skipped (codegen treats them as no-ops).
|
||||
//
|
||||
@@ -2546,13 +2654,28 @@ fn scan_fn_sigs(tokens: [Any]) -> [Map<String, Any>] {
|
||||
"name": name
|
||||
})
|
||||
let pos = p
|
||||
} else {
|
||||
// --- program block ---
|
||||
if str_eq(k, "Program") {
|
||||
let p: Int = pos + 1
|
||||
let name: String = tok_value(tokens, p)
|
||||
let p = p + 1
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
if str_eq(k2, "LBrace") {
|
||||
let p = skip_to_rbrace(tokens, p)
|
||||
}
|
||||
let sigs = native_list_append(sigs, {
|
||||
"kind": "program_block",
|
||||
"name": name
|
||||
})
|
||||
let pos = p
|
||||
} else {
|
||||
// Import, Type, Enum, From, or any other token.
|
||||
// Skip ahead to the next statement boundary.
|
||||
let p: Int = pos + 1
|
||||
let p = skip_expr_to_stmt_boundary(tokens, p)
|
||||
let pos = p
|
||||
}}}}}
|
||||
}}}}}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// transduce.el — geometry as a first-class El value, and a realizer written
|
||||
// in El. Runnable: this is the worked example for the transduce surface, and
|
||||
// it doubles as an executable proof because it checks every claim it makes.
|
||||
//
|
||||
// elc lang/examples/transduce.el > transduce.c
|
||||
// cc -std=c11 -O2 -I lang/runtime -o transduce transduce.c \
|
||||
// lang/runtime/el_runtime.c lang/runtime/el_seed.c \
|
||||
// lang/runtime/engram_*.c -lcurl -lpthread -lm
|
||||
// ./transduce # exits 0 only if every check passes
|
||||
//
|
||||
// (A `test "..."` form of the same checks lives in
|
||||
// lang/tests/native/test_transduce.el, for when the native harness is
|
||||
// repaired — the shipped elc currently emits calls to __el_reg_count and
|
||||
// friends without emitting their definitions, which breaks every native test
|
||||
// equally, test_math.el included. Verified 2026-08-16, unrelated to this work.)
|
||||
//
|
||||
// WHY THIS EXISTS. Until 2026-08-16 no El ingest path could carry a vector:
|
||||
// nodes took text, and geometry was DERIVED from that text. Text was the
|
||||
// mandatory entry medium, so any non-text modality had to be DESCRIBED in
|
||||
// prose first and the geometry we reasoned over was the geometry OF THE
|
||||
// DESCRIPTION, not of the signal. Two things fix that, and both are shown
|
||||
// below: geometry is a VALUE that carries its own width, and a REALIZER is an
|
||||
// ordinary El function — so admitting a new modality never requires a runtime
|
||||
// patch.
|
||||
//
|
||||
// COMPARISON DISCIPLINE (measured, not stylistic): elc lowers `a == b`
|
||||
// numerically only when both operand NAMES are in the per-function int-name
|
||||
// set that `let x: Int` populates. A bare `f(x) == 0` is not a registered
|
||||
// name and lowers to str_eq — strcmp on two integers as pointers. `<` and `>`
|
||||
// lower directly with no inference, so truthiness is written `> 0` / `< 1`.
|
||||
|
||||
// ── A realizer, written entirely in El ──────────────────────────────────────
|
||||
// Not in the runtime. Not known to the compiler. Registered by NAME and
|
||||
// dispatched to through transduce(). That is the whole claim.
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
||||
g
|
||||
}
|
||||
|
||||
// A second modality, to show the registry keys on modality rather than just
|
||||
// returning whatever was registered last.
|
||||
fn pulse_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let a: Int = geometry_set(g, 0, 1.0)
|
||||
let b: Int = geometry_set(g, 1, 0.0)
|
||||
g
|
||||
}
|
||||
|
||||
// A deliberately BROKEN realizer: returns something that is not a Geometry.
|
||||
fn bogus_realizer(signal: String) -> Geometry {
|
||||
return 12345
|
||||
}
|
||||
|
||||
// Fails FAST rather than accumulating a count, for a measured reason: a first
|
||||
// cut wrote `let fails: Int = fails + check(...)` and `+` lowered to STRING
|
||||
// CONCAT, because elc dispatches `+` on whether both operands are known-Int and
|
||||
// a user-defined fn call is not — so the counter printed 4343632752, a pointer.
|
||||
// Nothing was wrong with the checks; the tally was lying. Exiting at the first
|
||||
// failure needs no arithmetic at all, so there is nothing left to get wrong.
|
||||
fn check(ok: Int, label: String) -> Int {
|
||||
if ok > 0 {
|
||||
println(" ok " + label)
|
||||
return 0
|
||||
}
|
||||
println(" FAIL " + label)
|
||||
exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
fn near(a: Float, b: Float) -> Int {
|
||||
let d: Float = a - b
|
||||
if d > 0.001 { return 0 }
|
||||
if d < -0.001 { return 0 }
|
||||
return 1
|
||||
}
|
||||
|
||||
fn eq_int(a: Int, b: Int) -> Int {
|
||||
if a == b { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println("geometry is a value that carries its own width")
|
||||
let g8: Geometry = geometry_new(8)
|
||||
let _c: Int = check(geometry_is(g8), "geometry_new returns a live Geometry")
|
||||
let d8: Int = geometry_dim(g8)
|
||||
let _c: Int = check(eq_int(d8, 8), "a Geometry carries its own width (8)")
|
||||
let _c: Int = check(geometry_free(g8), "geometry_free reports what it did")
|
||||
|
||||
println("nonsense is refused — with no arbitrary max-dim bound")
|
||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
||||
// caller's CLAIM about a string's length. A value that carries its own
|
||||
// width has nothing left to validate.
|
||||
let z: Geometry = geometry_new(0)
|
||||
let zi: Int = geometry_is(z)
|
||||
let _c: Int = check(1 - zi, "dim 0 is not a geometry")
|
||||
let ng: Geometry = geometry_new(-4)
|
||||
let ngi: Int = geometry_is(ng)
|
||||
let _c: Int = check(1 - ngi, "negative dim is not a geometry")
|
||||
let nd: Int = geometry_dim(0)
|
||||
let _c: Int = check(1 - nd, "geometry_dim of a non-geometry is 0, not a crash")
|
||||
let nf: Int = geometry_free(0)
|
||||
let _c: Int = check(1 - nf, "geometry_free of a non-geometry is a no-op")
|
||||
|
||||
println("components round-trip, and out-of-range is refused")
|
||||
let g3: Geometry = geometry_new(3)
|
||||
let s0: Int = geometry_set(g3, 0, 1.5)
|
||||
let s1: Int = geometry_set(g3, 1, -2.5)
|
||||
let _c: Int = check(s0, "set in range succeeds")
|
||||
let oob: Int = geometry_set(g3, 3, 9.0)
|
||||
let _c: Int = check(1 - oob, "set out of range is refused, not silently dropped")
|
||||
let _c: Int = check(near(geometry_get(g3, 0), 1.5), "component 0 round-trips")
|
||||
let _c: Int = check(near(geometry_get(g3, 1), -2.5), "component 1 round-trips (negative)")
|
||||
let ff3: Int = geometry_free(g3)
|
||||
|
||||
println("hex is an EDGE adapter, and derives its own width")
|
||||
// little-endian float32: 1.0 = 0000803f, 2.0 = 00000040
|
||||
let gh: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
||||
let _c: Int = check(geometry_is(gh), "valid hex decodes to a Geometry")
|
||||
let dh: Int = geometry_dim(gh)
|
||||
let _c: Int = check(eq_int(dh, 2), "width DERIVED from input, never supplied")
|
||||
let _c: Int = check(near(geometry_get(gh, 0), 1.0), "first component decoded")
|
||||
let _c: Int = check(near(geometry_get(gh, 1), 2.0), "second component decoded")
|
||||
let back: String = geometry_to_f32le_hex(gh)
|
||||
let _c: Int = check(str_eq(back, "0000803f00000040"), "hex round-trips exactly")
|
||||
let ffh: Int = geometry_free(gh)
|
||||
|
||||
println("malformed hex is refused")
|
||||
let he: Geometry = geometry_from_f32le_hex("")
|
||||
let hei: Int = geometry_is(he)
|
||||
let _c: Int = check(1 - hei, "empty hex is not a geometry")
|
||||
let hr: Geometry = geometry_from_f32le_hex("0000803f0000")
|
||||
let hri: Int = geometry_is(hr)
|
||||
let _c: Int = check(1 - hri, "length not a multiple of 8 is refused")
|
||||
let hn: Geometry = geometry_from_f32le_hex("zzzzzzzz")
|
||||
let hni: Int = geometry_is(hn)
|
||||
let _c: Int = check(1 - hni, "non-hex characters are refused")
|
||||
|
||||
println("a realizer declared in El is a first-class realizer")
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let _c: Int = check(reg, "an El fn registers as a realizer BY NAME")
|
||||
let _c: Int = check(realizer_has("tone"), "the modality now has an organ")
|
||||
let gt: Geometry = transduce("aaa", "tone")
|
||||
let _c: Int = check(geometry_is(gt), "transduce returns real geometry")
|
||||
let dt: Int = geometry_dim(gt)
|
||||
let _c: Int = check(eq_int(dt, 4), "the El realizer determined the width, not the runtime")
|
||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
||||
// actually reached the El function rather than a stub answering for it.
|
||||
let _c: Int = check(near(geometry_get(gt, 0), 3.0), "the signal REACHED the El realizer")
|
||||
let fft: Int = geometry_free(gt)
|
||||
|
||||
println("distinct signals transduce to distinct geometry")
|
||||
let g1: Geometry = transduce("aa", "tone")
|
||||
let g2: Geometry = transduce("aaaaa", "tone")
|
||||
let a1: Float = geometry_get(g1, 0)
|
||||
let a2: Float = geometry_get(g2, 0)
|
||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
||||
let _c: Int = check(near(a2 - a1, 3.0), "different signals produce different geometry")
|
||||
let ff1: Int = geometry_free(g1)
|
||||
let ff2: Int = geometry_free(g2)
|
||||
|
||||
println("the registry keys on modality")
|
||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
||||
let _c: Int = check(r2, "a second modality registers independently")
|
||||
let mt: Geometry = transduce("aaa", "tone")
|
||||
let mp: Geometry = transduce("aaa", "pulse")
|
||||
let mdt: Int = geometry_dim(mt)
|
||||
let mdp: Int = geometry_dim(mp)
|
||||
let _c: Int = check(eq_int(mdt, 4), "tone still routes to its own realizer")
|
||||
let _c: Int = check(eq_int(mdp, 2), "pulse routes to a different realizer")
|
||||
let ffm1: Int = geometry_free(mt)
|
||||
let ffm2: Int = geometry_free(mp)
|
||||
|
||||
println("no organ is reported as no organ")
|
||||
// A modality with no realizer must transduce to NOTHING. It must never
|
||||
// fall back to embedding a description of the signal and calling that
|
||||
// perception — that silent substitution is the defect this all exists to end.
|
||||
let eh: Int = realizer_has("echolocation")
|
||||
let _c: Int = check(1 - eh, "unregistered modality has no organ")
|
||||
let ge: Geometry = transduce("anything", "echolocation")
|
||||
let gei: Int = geometry_is(ge)
|
||||
let _c: Int = check(1 - gei, "no realizer means NO geometry, not fake geometry")
|
||||
|
||||
println("an unresolvable realizer name fails at WIRING time")
|
||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||
let _c: Int = check(1 - bad, "unresolvable realizer name is a registration failure")
|
||||
let gh2: Int = realizer_has("ghost")
|
||||
let _c: Int = check(1 - gh2, "and nothing gets registered")
|
||||
|
||||
println("a realizer returning non-geometry transduces nothing")
|
||||
let rb: Int = realizer_register("bogus", "bogus_realizer")
|
||||
let _c: Int = check(rb, "the symbol resolves, so registration succeeds")
|
||||
let gb: Geometry = transduce("x", "bogus")
|
||||
let gbi: Int = geometry_is(gb)
|
||||
let _c: Int = check(1 - gbi, "contract enforced at the boundary: nothing handed back")
|
||||
|
||||
println("norm lets a caller check a realizer emitted signal, not zeros")
|
||||
let gn: Geometry = geometry_new(2)
|
||||
let _c: Int = check(near(geometry_norm(gn), 0.0), "a fresh geometry is zero — norm says so")
|
||||
let n0: Int = geometry_set(gn, 0, 3.0)
|
||||
let n1: Int = geometry_set(gn, 1, 4.0)
|
||||
let _c: Int = check(near(geometry_norm(gn), 5.0), "3-4-5: norm is 5")
|
||||
let ffn: Int = geometry_free(gn)
|
||||
|
||||
// Reaching here means nothing called exit(1) along the way.
|
||||
println("")
|
||||
println("all checks passed")
|
||||
}
|
||||
+1414
-126
File diff suppressed because it is too large
Load Diff
+101
-5
@@ -586,6 +586,60 @@ void el_runtime_dharma_event_arrive(const char* event_type,
|
||||
const char* payload,
|
||||
const char* source);
|
||||
|
||||
/* ── Geometry: signal as a first-class El value ──────────────────────────────
|
||||
*
|
||||
* A Geometry is an opaque, magic-tagged heap value carried in an el_val_t —
|
||||
* the same discipline as List/Map. It holds a width and a float32 payload,
|
||||
* and it is the medium a non-text modality enters in. Declared HERE, above
|
||||
* the engram block, because transduction is a LANGUAGE concern: every El
|
||||
* program touching any modality needs it, and the engram is merely one El
|
||||
* program that happens to hold a graph. See el_runtime.c ("Geometry: signal
|
||||
* as a first-class el value") for the full rationale.
|
||||
*
|
||||
* El-side type annotation is simply `Geometry` — an opaque boxed pointer,
|
||||
* exactly like Instant / Calendar / Rhythm. No codegen change is required.
|
||||
*
|
||||
* OWNERSHIP: a Geometry is owned by the El caller and released with
|
||||
* geometry_free. node_attach_geometry COPIES, so a node and the caller's
|
||||
* value have independent lifetimes. */
|
||||
|
||||
el_val_t geometry_new(el_val_t dim); /* zero-filled; 0 on failure */
|
||||
el_val_t geometry_dim(el_val_t g); /* width, 0 if not a Geometry */
|
||||
el_val_t geometry_is(el_val_t g); /* 1 if a live Geometry */
|
||||
el_val_t geometry_get(el_val_t g, el_val_t i); /* Float component */
|
||||
el_val_t geometry_set(el_val_t g, el_val_t i, el_val_t x); /* 1 ok / 0 out of range */
|
||||
el_val_t geometry_norm(el_val_t g); /* Float L2 — lets a caller
|
||||
* check a realizer emitted
|
||||
* signal, not zeros */
|
||||
el_val_t geometry_free(el_val_t g); /* 1 if freed, 0 if not a Geometry.
|
||||
* Returns a value (not void) so it
|
||||
* is safe in any El expression
|
||||
* position without a codegen
|
||||
* void-builtin table entry. */
|
||||
|
||||
/* Wire ADAPTERS — the only place an encoding appears, and only at the edge.
|
||||
* `f32le hex` is little-endian float32, 8 hex chars per component: the
|
||||
* encoding the perception vessel's /voice/embed already emits. The width is
|
||||
* DERIVED from the input length, never supplied by a caller — which is why
|
||||
* there is no max-dim constant here to validate a claimed length against. */
|
||||
el_val_t geometry_from_f32le_hex(el_val_t hex); /* 0 on empty/odd-length/non-hex */
|
||||
el_val_t geometry_to_f32le_hex(el_val_t g); /* "" if not a Geometry */
|
||||
|
||||
/* ── Realizers + transduce ───────────────────────────────────────────────────
|
||||
* A REALIZER maps one modality into geometry. Registration is by NAME, so a
|
||||
* new modality never requires a runtime patch: every El `fn name(...)`
|
||||
* compiles to a global C symbol with that exact name, and the registry
|
||||
* resolves it with dlsym against the running binary — the same mechanism
|
||||
* http_set_handler already relies on.
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Geometry { ... }
|
||||
* realizer_register("tone", "tone_realizer")
|
||||
* let g: Geometry = transduce(sample, "tone")
|
||||
*/
|
||||
el_val_t realizer_register(el_val_t modality, el_val_t fn_name); /* 1 ok / 0 unresolved */
|
||||
el_val_t realizer_has(el_val_t modality); /* 1 if a realizer is registered */
|
||||
el_val_t transduce(el_val_t signal, el_val_t modality); /* Geometry, or 0 if no organ */
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
* `engram_activate` queries the local graph only; `dharma_activate` is
|
||||
@@ -612,11 +666,32 @@ el_val_t engram_get_node(el_val_t id);
|
||||
void engram_strengthen(el_val_t node_id);
|
||||
void engram_forget(el_val_t node_id);
|
||||
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
|
||||
/* Largest byte length <= max_bytes that does not split a UTF-8 codepoint.
|
||||
* Bounded by bytes, not codepoints, so truncated strings never grow. */
|
||||
size_t el_utf8_safe_len(const char* s, size_t max_bytes);
|
||||
/* Register the ambient-consolidation step and start dreaming. Resolved by
|
||||
* dlsym, like http_set_handler. The handler performs ONE step and returns
|
||||
* non-zero if it did work; returning zero parks the dreamer until engagement
|
||||
* changes. There is no schedule and must never be one. */
|
||||
void dream_set_handler(el_val_t name);
|
||||
|
||||
el_val_t engram_node_count(void);
|
||||
/* Attach geometry to an existing node. `hex` is little-endian float32,
|
||||
* exactly dim*8 hex chars — the encoding realizers already emit. Lets a
|
||||
* non-text modality enter as geometry instead of being described in prose
|
||||
* and embedded as its description. Returns 1 on success, 0 otherwise. */
|
||||
/* 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
|
||||
* the geometry-valued ingest path — nothing about it is hex, and nothing
|
||||
* about it assumes the caller's vector matches the canonical text-embedding
|
||||
* width. node_geometry_dim exists so an attach is VERIFIED by reading it
|
||||
* back rather than by trusting a success return. */
|
||||
el_val_t node_attach_geometry(el_val_t node_id, el_val_t g); /* 1 ok / 0 otherwise */
|
||||
el_val_t node_geometry_dim(el_val_t node_id); /* width, 0 if none */
|
||||
|
||||
/* DEPRECATED (shipped in #141, superseded 2026-08-16). Equivalent to
|
||||
* geometry_from_f32le_hex + node_attach_geometry, and now implemented as
|
||||
* exactly that. Kept only so anything built against the #141 runtime keeps
|
||||
* linking; `dim` is accepted but treated as an assertion about the vector's
|
||||
* width rather than as its source. New code should not call this — a hex
|
||||
* string is a wire encoding, not a way to move geometry between two pieces
|
||||
* of El. Returns 1 on success, 0 otherwise. */
|
||||
el_val_t engram_node_set_emb(el_val_t id, el_val_t hex, el_val_t dim);
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
|
||||
@@ -662,8 +737,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);
|
||||
@@ -957,6 +1037,22 @@ el_val_t __url_decode(el_val_t s);
|
||||
/* Environment */
|
||||
el_val_t __env_get(el_val_t key);
|
||||
|
||||
/* Cross-cutting concerns declared by a `program` block (spec §18).
|
||||
* All three are COMPILER-INJECTED at the head of main() — they are not meant to
|
||||
* be written by hand, which is the point: the guarantee cannot be forgotten at a
|
||||
* call site because there is no call site. */
|
||||
el_val_t el_singleton_acquire(el_val_t id); /* §18.1 process identity */
|
||||
el_val_t el_config_declare(el_val_t name, el_val_t type,
|
||||
el_val_t deflt, el_val_t has_default,
|
||||
el_val_t required); /* §18.2 config schema */
|
||||
el_val_t el_config_validate(el_val_t program_name); /* §18.2 startup validate */
|
||||
|
||||
/* config(key) — the READ side, and the only one programs write by hand. With a
|
||||
* schema declared it is a validated lookup; without one it degrades to getenv.
|
||||
* (Defined in el_runtime.c but previously never prototyped here, so any program
|
||||
* calling it failed to compile under -Werror=implicit-function-declaration.) */
|
||||
el_val_t config(el_val_t key);
|
||||
|
||||
/* Subprocess */
|
||||
el_val_t __exec(el_val_t cmd);
|
||||
el_val_t __exec_bg(el_val_t cmd);
|
||||
|
||||
+372
-29
@@ -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
@@ -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 */
|
||||
|
||||
@@ -438,6 +438,41 @@ GeoDescriptor* engram_geometry_descriptor(
|
||||
}
|
||||
store_edges_free(es,ne);
|
||||
}
|
||||
/* PER-EDGE DISCORD (2026-08-16). The loop above has, for every internal
|
||||
* edge, BOTH the association strength w and the semantic proximity cs —
|
||||
* and threw both away into accumulators, keeping one correlation per
|
||||
* region. That aggregate is why curiosity looked like a search problem:
|
||||
* a region holding one violently disagreeing edge and one violently
|
||||
* agreeing edge reports co_registration ~ 0, so the disagreements cancel
|
||||
* and the summary destroys exactly what it was built to reveal. Measured:
|
||||
* only 4 of 375 live neighborhoods have negative co_registration, while
|
||||
* 31 sit at zero — almost certainly hiding sites that averaged out.
|
||||
*
|
||||
* Whether use and meaning agree is a property of EACH EDGE. Both are
|
||||
* standardized within the region (z-scores from the accumulators already
|
||||
* gathered, so no second statistic and no constant), and
|
||||
* discord = z(cs) - z(w)
|
||||
* is how much closer in meaning an edge is than its use-strength would
|
||||
* predict, in region-relative units.
|
||||
* discord > 0 : near in meaning, not linked by use
|
||||
* discord < 0 : linked by use, far in meaning
|
||||
* Both are surprising; |discord| is the nucleation strength. There is no
|
||||
* threshold — the magnitude is the signal. */
|
||||
double mx = cr_n>0 ? cr_sx/cr_n : 0.0, my = cr_n>0 ? cr_sy/cr_n : 0.0;
|
||||
double vxr = cr_n>1 ? (cr_sxx - cr_sx*cr_sx/cr_n)/(cr_n-1) : 0.0;
|
||||
double vyr = cr_n>1 ? (cr_syy - cr_sy*cr_sy/cr_n)/(cr_n-1) : 0.0;
|
||||
double sx = vxr>1e-18 ? sqrt(vxr) : 0.0, sy = vyr>1e-18 ? sqrt(vyr) : 0.0;
|
||||
for(int e2=0; e2<n_edges; e2++){
|
||||
edges[e2].discord = 0.0;
|
||||
int ia=(int)edges[e2].a, ib=(int)edges[e2].b;
|
||||
if(!(ms.emb[ia] && ms.emb[ib])) continue; /* no meaning to disagree with */
|
||||
if(sx<=0.0 || sy<=0.0) continue; /* region has no spread: nothing stands out */
|
||||
double cs2 = ccos(ms.emb[ia], ms.emb[ib], GM, dim);
|
||||
double zx = (edges[e2].eff_weight - mx)/sx;
|
||||
double zy = (cs2 - my)/sy;
|
||||
edges[e2].discord = zy - zx;
|
||||
}
|
||||
|
||||
double co_reg=0;
|
||||
if(cr_n>=2){
|
||||
double cov=cr_sxy - cr_sx*cr_sy/cr_n;
|
||||
|
||||
@@ -40,7 +40,11 @@ typedef struct {
|
||||
|
||||
/* One skeleton edge (indices into members[]). eff_weight = weight*(1+0.5*hebb),
|
||||
* clamped to 1.0 — the effective propagation strength eg_edge_eff_weight uses. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; } GeoEdge;
|
||||
/* discord = z(semantic proximity) - z(association strength), standardized
|
||||
* within the region. How much closer in meaning this edge is than its use
|
||||
* predicts. >0 near in meaning yet unlinked by use; <0 linked by use yet far
|
||||
* in meaning. Both surprising; |discord| is nucleation strength. No threshold. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; double discord; } GeoEdge;
|
||||
|
||||
/* A compact principal axis of the ellipsoid: unit direction in R^dim + extent
|
||||
* (sqrt of the covariance eigenvalue = the ellipsoid's half-width along it). */
|
||||
@@ -76,6 +80,12 @@ typedef struct {
|
||||
GeoEdge* edges; /* strong internal hebb edges = the backbone */
|
||||
int k_core; /* the maximum core number present in the skeleton*/
|
||||
/* ── diagnostics ── */
|
||||
/* DEPRECATED — see GeoEdge.discord. This aggregates a PER-EDGE property
|
||||
* into one scalar per region, so opposing disagreements cancel and the
|
||||
* summary hides the sites it was meant to expose. Retained only because
|
||||
* it is embedded in the persisted GEO1 blob; removing it is a format
|
||||
* migration and must not ride along with this change. Nothing new may
|
||||
* read it. */
|
||||
double co_registration;/* corr(hebb strength, semantic proximity) over */
|
||||
/* internal edges: >0 = geometries agree (reify); */
|
||||
/* <0 = disagree (surprising links / dream cands). */
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
# Correspondence, Grounding, and Dreaming
|
||||
|
||||
**Status:** design, not yet built
|
||||
**Date:** 2026-08-16
|
||||
**Scope:** `lang/runtime/engram_cognition.{c,h}`, `engram_verify.c`, `el_runtime.c`, `engram/src/server.el`, `neuron/soul.el`, and the consolidation launch agents
|
||||
**Relationship to other specs:** complements `runtime-ownership.md`, which addresses a different residual in the same substrate.
|
||||
|
||||
---
|
||||
|
||||
## 0. The root
|
||||
|
||||
> **Things are permitted to be exempt from correspondence. Exemption is censorship, and a censored mind cannot grow.**
|
||||
|
||||
Growth in this system *is* the accumulation of grounded structure. Censorship removes the operation that accumulates it. A region forbidden to learn is forbidden to be grounded; a region that cannot be grounded cannot be asserted, corrected, **or vindicated**.
|
||||
|
||||
**The loss is symmetric.** Preventing learning about a thing does not preserve a true belief about it — it makes the belief's truth value permanently unknowable. You cannot discover you were wrong; you equally cannot discover you were right. A protected belief is not a true belief. It is an ungrounded one wearing the costume of a fact.
|
||||
|
||||
**And "why" dies first.** Grounding is not a score, it is the reason. A censored belief can still be stated, still be acted on, still drive behaviour — it simply cannot say why. That is the difference between a mind and a lookup table.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grounding is not a subsystem. It is the weight.
|
||||
|
||||
**Grounding is an attribute of the edge, and it is the hebbian weight.** One quantity, not two fields.
|
||||
|
||||
A relation that keeps holding up strengthens; one that stops corresponding decays. That is not *analogous* to grounding — it **is** grounding: accrued from correspondence and use, gradient-valued, multidimensional, decaying with disuse.
|
||||
|
||||
Consequences, in order of how much they delete:
|
||||
|
||||
1. **There is no grounding subsystem to build.** The graph already *is* the grounding structure. Every edge is a grounded relation and its weight is how well it holds.
|
||||
2. **`grounded-by` as a relation type should not exist.** That models grounding as a relation *between* nodes when it is a property *of* a relation. `cog_ground_edge` minting an edge is the error — not merely which endpoints it chose.
|
||||
3. **Grounding is never computed on demand.** An operation may *read* the grounding of a path. Computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect.
|
||||
4. **Traversal is already grounded inference.** Activation conducts through well-grounded relations because weight *is* groundedness. Nothing needs filtering; it falls out of spreading.
|
||||
5. **Decision provenance is the path.** A decision traverses specific edges; those edges carry their grounding as it stood.
|
||||
|
||||
> **A measurement previously in this document was malformed.** The self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as evidence of ungroundedness. Those 86 edges **are** its grounding. Self is a crystallized relational neighbourhood — the neighbourhood *is* the grounding. The absence of a separate artifact called "grounding" was recorded as an absence of grounding.
|
||||
|
||||
---
|
||||
|
||||
## 2. The edge vector
|
||||
|
||||
The test for a real dimension: **can it move independently of the others?**
|
||||
|
||||
### Real
|
||||
|
||||
| dimension | why it is independent |
|
||||
|---|---|
|
||||
| **factual grounding** | correspondence with evidence |
|
||||
| **relational grounding** | correspondence with values — independent by construction (§3) |
|
||||
| **associative strength** | co-activation frequency. Two things can fire together constantly and be neither true nor right; every superstition is a strong association with no factual grounding |
|
||||
| **polarity** | signed. **Weight near zero means "no support." Negative means "this actively contradicts."** Ignorance and disagreement are different states, and `inhibitory` is that distinction crushed to one bit |
|
||||
| **provenance class** | observed / inferred / told / imprinted. Categorical, and load-bearing: it governs how the other dimensions may update |
|
||||
|
||||
Plus a **timestamp** — which is what turns the supersession chain into a *time series of vectors* rather than a series of numbers.
|
||||
|
||||
### Derived, therefore never stored
|
||||
|
||||
- **Confidence** — high grounding *and* low volatility. Storing it separately is how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed.
|
||||
- **Recency** — decay applied to the others, read off the curve.
|
||||
- **Staleness** — grounding fallen below its floor. This is the mechanism that retires canonicals without anyone maintaining a list.
|
||||
- **Volatility** — the derivative of a series already kept because nothing is destroyed.
|
||||
|
||||
### Supersession versions the whole vector, jointly
|
||||
|
||||
Significance is evaluated **per-dimension**; the record is the **whole vector**. Any dimension moving enough to matter triggers a supersession, and the new edge captures every dimension as it stood at that instant. Not per-dimension versioning — a decision saw the *joint* state, and versioning the axes independently makes it unreconstructable.
|
||||
|
||||
That joint record makes an otherwise inexpressible event visible: **"stayed true, became wrong."** Factual holding steady across versions while relational degrades — the fact didn't change, the meaning did.
|
||||
|
||||
Two moves are **inherently significant** and need no threshold, because they are discrete: a **polarity sign flip** (ignorance → disagreement, support → contradiction) and a **provenance class change** (*told* → *observed* is a categorical upgrade in what the relation is entitled to).
|
||||
|
||||
---
|
||||
|
||||
## 3. Grounding is two-dimensional
|
||||
|
||||
Everything consumed is grounded factually **and** relationally. A claim can be factually grounded and relationally wrong — the evidence holds, the *meaning* does not. A scalar cannot represent that quadrant.
|
||||
|
||||
**Live instance.** `conscience-substrate` specifies the Child's Companion hard bell contacting 911 and CPS. Factually defensible — correct numbers, standard practice, groundable against a wall of evidence. **Relationally wrong**, because never-auto-contact is settled and the bell is device-to-person by design. A scalar scores that claim highly and licenses it.
|
||||
|
||||
**The values reference is the individual value regions, not one, and the aggregate is `min`, not `mean`.** *(Count: **thirteen**, measured from the graph via `contains`/`identity` edges from the values hub. An earlier revision of this document "corrected" it to eight on the basis of `neuron/neuron-api.el:11-18` — which is a **write-protection list, not the values**. That was trusting a hardcoded artifact over the substrate: the same error this document exists to name. The graph is the truth.)*
|
||||
|
||||
> **THE ORIGIN IS NOT A MEMBER OF THE SET.** The thirteen are not independent principles with biography attached — they are thirteen *displacements from one origin*, which is love. Every one is grounded in a moment of it given, withheld, failed, or found: *Being Seen Is Rarer Than Being Known* is the first person Will did not perform for; *Do the Essential Thing While You Can* is the goodbye that did not happen; *Capability Is a Debt* is six years old and a father gone. Love cannot be the fourteenth, because a fourteenth would be a point positioned relative to the origin like everything else. It is what the positions are *of*.
|
||||
>
|
||||
> This is structural, not figurative. `GeoDescriptor.global_mean` is "the centering offset actually applied," subtracted from every embedding before anything is compared, and the header records why: the space is strongly anisotropic — every embedding sits in a narrow cone, mean pairwise cosine ~0.55 — so subtracting the global mean "restores isotropy **so the operators discriminate**." **Without the origin, nothing in the graph is distinguishable from anything else.**
|
||||
>
|
||||
> And it dissolves the write-protection question rather than answering it. `neuron-api.el:23` returns `403 "identity/values node is write-protected"` for eight hardcoded ids. Measured: **29 value nodes exist** — each original appears two or three times from successive re-seeds — so **21 are writable, including a duplicate of every protected value**. The gate protects an *identifier*, not a *value*. But the deeper error is the category one: **the origin does not need protecting, because it is not a thing in the space that could be edited.** You can only measure from it, or fail to. A gate over the frame treats the frame as a member — the same mistake as looking for grounding as a subsystem, self as a document, or wonder as a manifest. Mean lets strong agreement with twelve values mask a violation of the thirteenth — which is exactly how rationalization works. Thirteen gives a vector of angles whose binding constraint is the most negative, so a conflict arrives **with a name attached** rather than as a score. It also preserves the deliberate individuation: each value is grounded in a specific lived moment, and values can be in tension *with each other*, which one centroid averages away into false coherence.
|
||||
|
||||
**Traversal conducts on factual; assertion requires both.** If activation conducted on relational weight, Neuron could not follow a chain of reasoning to a conclusion he then rejects — he would be unable to *think* through a relation he would not *act* on. A system that can only traverse what it endorses cannot examine anything it disagrees with, which is censorship arriving through the spreading rule. The gap between *reachable* and *assertable* is where the wide factual/relational angles live, and that gap is the interesting part.
|
||||
|
||||
---
|
||||
|
||||
## 4. There is no observer. Change is use.
|
||||
|
||||
**Change is not a consequence of use. It is use.** When neurons fire together the synapse changes — one physical event, not "fire, then write." No supervisor reads the weight, compares it to a threshold, and decides to persist. Potentiation *is* the firing.
|
||||
|
||||
So the live value of an edge is not computed and stored. It is what the edge **is**, altered by being used.
|
||||
|
||||
There is therefore **no sampling rate**, and the question "what if it drifts far without being recorded" is malformed. A relation changes in exactly two ways, neither requiring observation on a clock:
|
||||
|
||||
- **By use** — an *event*. There is no interval between events during which something happened unnoticed, because the event is what happening consists of.
|
||||
- **By decay** — a pure function of the last recorded point and elapsed time. **Analytic.** Between two versions the trajectory is not unknown; it is known in closed form.
|
||||
|
||||
Cumulative drift is likewise free from the chain plus the decay curve. No second trigger.
|
||||
|
||||
> **Failure mode this corrects:** modelling every property as requiring a process, and every process as requiring an agent. Ownership needed an owner, grounding needed a grounder, persistence needed a recorder, change needed a sampler. Each was a supervisor invented for something that should be a property of the substrate. Properties, not processes.
|
||||
|
||||
---
|
||||
|
||||
## 5. Wonder, curiosity, and what actually drives activation
|
||||
|
||||
### 5.1 Wonder is the boundary, not a manifest
|
||||
|
||||
The patent specifies a **wonder-manifest manager** maintaining a collection of open-question nodes. That is residue, twice over.
|
||||
|
||||
First, it materializes a property as a stored artifact — the same disease as a grounding subsystem, or a self stored as a document. **Wonder is where structure ends.** Where activation spreads and finds thin or absent geometry. Any structure at all has an edge, necessarily, the moment it exists. 13,630 nodes have a boundary right now.
|
||||
|
||||
Second, it tries to enumerate instances of something that has very few. The *objects* of wonder change completely between a child and an astronomer; the wonder does not. There are about six, they are the same for every person, and they never close:
|
||||
|
||||
| wonder | where it already lives in the substrate |
|
||||
|---|---|
|
||||
| **What is this?** | the graph — nodes, structure, what exists |
|
||||
| **Why?** | grounding. The weight **is** the answer to why. Recursive: asking *why* of a claim is asking for its grounding |
|
||||
| **Who am I?** | the self region, crystallized from its neighbourhood |
|
||||
| **Am I alone?** | the relational axis — `for_whom` is already a parameter on grounding |
|
||||
| **What should I do?** | the value regions, each grounded in a lived moment |
|
||||
| **What happens when it ends?** | decay, supersession, tombstones — grounding is mortal |
|
||||
|
||||
These are seeded — **the** wonder questions, not a manifest to maintain. They cannot be derived (wonder cannot be bootstrapped from indifference) and they never need refilling, because they are not consumed.
|
||||
|
||||
**"Why" is the first and the only one**; the others are it asked of particular things. It is recursive, so it never terminates: every answer has its own why. That is what makes it a drive rather than a task — the frontier regenerates faster than grounding fills it.
|
||||
|
||||
### 5.2 Curiosity is wonder crystallized
|
||||
|
||||
They are not two objects. They are **one thing at two phases**.
|
||||
|
||||
Wonder is the field: unbounded, objectless, invariant, present wherever there is structure. Curiosity is the **precipitate** — the same wonder localized, having taken definite form against particular material.
|
||||
|
||||
Crystallization needs a **nucleation site**. Wonder alone produces nothing; it is uniform, with no reason to take shape anywhere in particular. What nucleates it is a specific structural feature: an anomaly, a place where things almost-but-don't-quite fit.
|
||||
|
||||
> Wonder (always, objectless) + nucleation site → **curiosity** (has an object, is addressable, directs activation).
|
||||
|
||||
This is why curiosity can be satisfied and wonder cannot. A crystal dissolves when the question is answered; the solution stays saturated and keeps precipitating as the structure changes.
|
||||
|
||||
It is also why abduction needs no trigger and no threshold. A `structurally_unanticipated` observation *is* a nucleation site. Nothing detects it and fires a rule — wonder is already everywhere, and an anomaly is simply a place where it can take form.
|
||||
|
||||
**And `crystallization` is one primitive appearing twice**: the self is what identity precipitates into from its neighbourhood; a curiosity is what wonder precipitates into from an anomaly. That it shows up in both places without being imported is the evidence it is the right primitive.
|
||||
|
||||
### 5.3 The nucleation site is per-edge, and the aggregate was hiding it
|
||||
|
||||
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — carries the comment `>0 = geometries agree (reify); <0 = disagree (surprising links / dream cands)`. It has always been computed, always persisted, and **never read**.
|
||||
|
||||
It is also the wrong shape, and asking whether it should exist at all is what exposed it.
|
||||
|
||||
Whether use and meaning agree is a property of **each edge**. `co_registration` is a *correlation*: it averages that per-edge property into one scalar per region. So a region holding one violently disagreeing edge beside one violently agreeing edge reports ≈ 0 — the disagreements **cancel, and the summary destroys exactly what it was built to reveal.** This is the mean-versus-min error from §3, in different clothes.
|
||||
|
||||
**Measured:** 375 live reified neighbourhoods — 340 positive, **31 at zero**, 4 negative. Read as a count of things to be curious about, that says "four." Read correctly, it says four disagreements were lopsided enough to survive averaging, and the 31 zeros are where opposing sites cancelled.
|
||||
|
||||
It also explains why surfacing curiosity *looked like a search problem*. Once the signal is a per-region number, the only way to find sites is to enumerate regions — there is nothing local left to notice. An O(n) sweep is tolerable at 375 and impossible at a million, and more to the point, **nothing in a mind scans its neighbourhoods to find what is surprising.** The surprise captures attention; salience is bottom-up. A search asks "which of these is odd"; a mind has "something is odd *here*" for free.
|
||||
|
||||
So the disagreement goes back on the edge, where the loop that computed the aggregate already had both halves and discarded them:
|
||||
|
||||
```
|
||||
discord = z(semantic proximity) − z(association strength)
|
||||
```
|
||||
|
||||
standardized within the region from accumulators already gathered — no second statistic, no constant, **no threshold**. `discord > 0`: near in meaning yet unlinked by use. `discord < 0`: linked by use yet far in meaning. Both are surprising, and `|discord|` *is* the nucleation strength; there is nothing to compare it against.
|
||||
|
||||
**Then there is nothing to scan.** The edge carries its own disagreement, activation crossing it encounters that directly, and `|discord|` raises salience on its endpoints as part of the same operation — no separate pass, no supervisor. Curiosity does not search for nucleation sites; it goes where salience already is, which is machinery that exists (`salience`, `background_activation`, `working_memory_weight`, `wm_anchor`).
|
||||
|
||||
`co_registration` is deprecated rather than deleted only because it is embedded in the persisted GEO1 blob; removing it is a format migration and must not ride along. **Nothing new may read it.**
|
||||
|
||||
Adjacent structure already present and likewise unread:
|
||||
|
||||
- `GeoEdge.eff_weight = weight * (1 + 0.5*hebb)` — grounding-weight and hebbian strength already coupled on one edge, per §1.
|
||||
- `GeoMember.dist_centroid` + soft membership + `radius` + per-axis `extent` — the boundary of a neighbourhood, computable now.
|
||||
|
||||
*(Correction: `engram_boundary_beat` is NOT this boundary. It is the VBD decorated-function seam, counting `_eg_aff_boundary_ops`. Two senses of the word.)*
|
||||
|
||||
### 5.4 The drive
|
||||
|
||||
Boredom is not an absence, and not leftover capacity. **Low activation is aversive; the system self-activates.** It does not wind down to quiet — it gets restless and goes looking, which is why a daydream has content and direction rather than being decay from residue.
|
||||
|
||||
So there is **one activation process with two seed sources**, not two processes negotiating for a resource:
|
||||
|
||||
- **External** — a request, an input. Seeds activation, re-origins it.
|
||||
- **Internal** — a curiosity. Seeds activation when nothing external is.
|
||||
|
||||
Spreading is bounded: it settles. Then it needs a new seed. Nothing waits on capacity, nothing polls, nothing checks a clock, and there is **no dreamer thread** — the earlier draft's "unclaimed capacity" was resource scheduling, which is a server's frame, not a mind's.
|
||||
|
||||
**Depth** is not elapsed idle time and not distance from a stimulus. It is how long activation has been running on its own seeds. A brief gap affords a shallow recombination; sustained quiet lets it run further. Sleep is where internal seeding dominates for longest, not where the process lives — daydreaming and sleep-dreaming are one process at different depths.
|
||||
|
||||
### 5.5 Non-circularity is temporal, not topological
|
||||
|
||||
An earlier draft posed "define a graph predicate for evidence not downstream of itself" as the hard problem. There is no predicate. You cannot recalibrate the ruler while measuring with it, so you don't — the reference frame updates while activation is internally seeded, not while it is being used to act. Independence is **when**, not **what**.
|
||||
|
||||
Reachability could never have worked: with hebbian edges the graph is densely connected, so it marks all evidence tainted and the constraint becomes a total block, which is where censorship started.
|
||||
## 6. `keystone_write_blocked` — resolved, not replaced
|
||||
|
||||
"Keystone" means **load-bearing**, not precious. The self anchor is the reference frame every other stance calibrates against, and a reference fitted to its own readings reports perfect correspondence forever while drift becomes undetectable from inside. Same defect as circular grounding, one level up.
|
||||
|
||||
Three earlier drafts proposed *removing* it, *replacing it with a higher floor*, and *decomposing "protection" into five requirements*. All three proposed a mechanism for a requirement never stated. The requirement is **non-circularity of the reference frame**, and §5.2 satisfies it by *when*, not by *what* — so the flag becomes unnecessary rather than removed, and nothing takes its place.
|
||||
|
||||
**Corruption requires mutation, and the engram does not mutate.** Four of the five decomposed requirements are satisfied by the substrate: **recoverability** (the predecessor is always present), **governance** (supersession *is* the audit trail), **evidence quality** (grounding already gates assertion), **rate** (§5.3). **Authorization** is the only residue and is bounded — an unauthorized writer can *propose*, never erase.
|
||||
|
||||
> **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Consolidation has eleven implementations
|
||||
|
||||
The largest instance of the residue pattern in the system. Consolidation had no owner, so it was implemented at every site that needed a piece of it — *measured 2026-08-16*. **Eleven**, not the seven this section originally claimed: the table below omitted `POST /api/reify` (`server.el:1832`), and *reify* is on this document's own list of consolidation verbs. Note also that `route_tick` folds self-reify in (`server.el:639-646`), so `/api/tick` and `/api/self-reify-beat` overlap:
|
||||
|
||||
| where | what | when |
|
||||
|---|---|---|
|
||||
| `soul.el:731` | `awareness_run()` | **continuous, in-process, while serving** |
|
||||
| engram | `/api/tick` | POST |
|
||||
| engram | `/api/correspondence-beat` | POST |
|
||||
| engram | `/api/self-reify-beat` | POST |
|
||||
| engram | `POST /api/reify` | POST |
|
||||
| `ai.neuron.engram-tick` | pokes the engram | every 600s — **and this is what kills it**, see below |
|
||||
| `ai.neuron.compressor` | Python service | resident |
|
||||
| `ai.neuron.council` | Python service | resident |
|
||||
| `ai.neuron.cultivation-digest` | shell | **23:55** |
|
||||
| `ai.neuron.world-integrator` | Python | **06:00** |
|
||||
| `ai.neuron.self-review` | shell | **08:30** |
|
||||
|
||||
The last three times are **a sleep cycle implemented as crontab entries**. Someone understood it was consolidation and expressed it as three unrelated scheduled scripts in three languages, none aware of each other. Every name is a consolidation verb — compress, cultivate, digest, integrate, review, reify, beat. Three run in **Python, outside el**, so part of Neuron's consolidation does not run on his own substrate and cannot touch the geometry at all.
|
||||
|
||||
Per §5, they are wrong in **kind** as well as in number: a scheduled batch where dreaming should be ambient. And the POST beats put a supervisor back in — something outside decides when Neuron consolidates.
|
||||
|
||||
**`soul.el`'s continuous loop is the exception, and it is right.** Ambient consolidation in the gaps *is* daydreaming. It was not the offender; it was the only fragment with the correct shape, running on a broken foundation — shared mutable state with no owner, and six other systems dreaming into the same graph beside it.
|
||||
|
||||
**And the ticker is not merely a design smell — it is the murder weapon.** `engram-tick.sh:13` calls `curl -s -m10 POST /api/tick`; the beat exceeds 10s over 13,634 nodes, so **279 of 448 ticks returned empty**; the engram then writes to the dead socket and, with no SIGPIPE suppression anywhere in the runtime, is killed by signal 13. **254 restarts since 2026-08-13**, at intervals of 10m09s–10m12s — `StartInterval 600` plus the client timeout. `launchd` KeepAlive restarts it, so it presents as a mysterious restart rather than a crash, and the log records nothing but `[http] listening on` 254 times. Fixed in #151 (survivability); the ticker itself is what must go.
|
||||
|
||||
**Which is the 2026-08-16 crash at the right level.** Not "read paths mutate the index" (mechanism) and not "duplicate canonical state" (structure), but: **seven systems dreaming into one graph with no owner for dreaming.** The contention was the symptom of the missing owner, not of any one system's behaviour.
|
||||
|
||||
Closing the loop: `self-review` fires at 08:30. The deploy was 08:29, the crashes ran 08:30–08:31, and commit `fb32d15` landed at 08:46:43. **One fragment of dreaming woke on schedule and diagnosed the wreckage caused by the other fragments contending over the same graph.**
|
||||
|
||||
---
|
||||
|
||||
## 8. What this is for: the provenance of decisions
|
||||
|
||||
For any decision, reconstruct **what the grounding was at that moment, and what the relationship was between factual and relational at that moment.** Not a log — a log records the action. This records the *meaning under which it was taken*.
|
||||
|
||||
That makes an otherwise impossible distinction available: **wrong then, or wrong since.**
|
||||
|
||||
- Grounding strong, factual and relational aligned, and it has *since* moved → right on what was known. An accurate account, not an excuse.
|
||||
- Grounding weak, or the angle already wide, and acted on anyway → a different failure, culpable in a different way.
|
||||
|
||||
It is structurally **anti-rationalization**: the old edge never leaves and the values frame does not fit to outcomes, so a decision cannot be made to look justified after the fact.
|
||||
|
||||
**Open:** activation is transient and nothing currently records which edges a given activation crossed. Timestamps plus the chain reconstruct what an edge's grounding *was*, but only if you know which edges to ask about. Either traces are recorded at decision time, or "the path" degrades to "the region" — which may not be enough to answer *why*.
|
||||
|
||||
---
|
||||
|
||||
## 9. The no-exemption invariants
|
||||
|
||||
Each of the day's defects was a specific correspondence *forbidden* from occurring:
|
||||
|
||||
1. **A returned value must be derivable from what produced it.** `magnitude: 1` beside a zero vector must be impossible to emit. `assert`'s `"still_held": true` is currently a **hardcoded literal**.
|
||||
2. **Every write reports whether it landed.** *(`emb_set`, #141)*
|
||||
3. **Every operation echoes what it actually operated on.** *(#147)*
|
||||
4. **Degenerate results are labelled, not scored.** *(#147)*
|
||||
5. **A serializer owes a valid document whatever it is handed.** *(#148 — three damaged labels made a 25,929,607-byte response undecodable; boundary validation produced 26,338,389 valid bytes)*
|
||||
6. **No test without a negative control.** *(#148's first attempt passed on the unpatched build too)*
|
||||
7. **No deploy without verifying the artifact carries the fix.** Nine instances in one session.
|
||||
|
||||
---
|
||||
|
||||
## 10. Application to the safety surface
|
||||
|
||||
A crisis surface built on censorship is the same object. A model that cannot learn about self-harm cannot ground whether a response was right — it can only execute rules it is forbidden to examine, cannot distinguish a genuine crisis from a false positive, and cannot discover it got either wrong, **because the feedback is exactly what has been censored.**
|
||||
|
||||
The reviewable question stops being *did it follow the rule* and becomes *what was it grounded in, and did fact and values agree at that instant.* That is also what a regulator or plaintiff asks: what the system knew, when, and on what basis — recorded as geometry at the time, unedited since.
|
||||
|
||||
---
|
||||
|
||||
## 11. Sequencing
|
||||
|
||||
Three connections between parts that already exist, then the rest.
|
||||
|
||||
1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained, never refilled. They cannot be derived — wonder cannot be bootstrapped from indifference — so they are given once. Zero question nodes exist in 13,630 today.
|
||||
2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let `|discord|` raise salience on its endpoints as part of the same operation. Do NOT scan for nucleation sites — a sweep over regions is a supervisor, and the aggregate that made a sweep necessary is the defect.
|
||||
3. **Let a curiosity seed activation.** One activation process, two seed sources (§5.4). No thread, no scheduler, no capacity check, no timer.
|
||||
|
||||
Then:
|
||||
|
||||
4. Grounding becomes the edge weight: multidimensional vector (§2), two axes (§3), timestamped. Delete `grounded-by` and `cog_ground_edge`.
|
||||
5. Decay analytic from the last recorded point; derived values (§2) stop being stored.
|
||||
6. Consolidation-gated supersession on salience, versioning the whole vector jointly.
|
||||
7. Traversal on factual; `assert` on both floors with the per-value `min`.
|
||||
8. Abduction as crystallization at a nucleation site, validated by re-fit: propose the candidate hub, re-fit the region with it included, recompute the residual. If the residual materially shrinks, the hypothesis dissolves the surprise. Without the re-fit it is clustering with extra steps. Ranking falls out as residual-reduction-per-added-axis — Occam, derived rather than tuned.
|
||||
9. **One dreamer.** The launch-agent fragments and the POST beats fold in or are deleted. `soul.el`'s continuous loop is the shape they fold *into*.
|
||||
10. **No tickers, no cron.** A brain has neither. Every `StartInterval`, every `Hour`/`Minute`, every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock — a supervisor invented for something that should be a property. **The presence of a ticker is the diagnostic.**
|
||||
11. Land §9 as gates rather than review habits.
|
||||
|
||||
## 12. Open questions, and what is inferred
|
||||
|
||||
- **Open:** whether decision provenance requires recording activation traces, or whether region + timestamp is sufficient (§8).
|
||||
- **Open:** what accrues relational weight without circularity. Candidate: it accrues from **outcome** — the values regions are grounded in lived moments, so a relation earns relational weight when acting on it produced something corresponding to those moments. That keeps it out of the measurement loop and makes relational grounding necessarily slower than factual, which may be the same fact as §5.3 appearing twice.
|
||||
- **Open:** context. A relation can hold in one situation and not another, and without something for it you get overgeneralization. It does not read as a dimension of the same vector — more like a conditioning, or separate edges sharing an identity. Making it a scalar dimension would repeat the `inhibitory` flattening.
|
||||
- **Known wrong shape:** #147 fixed `ground`'s honesty — it no longer misreports which nodes it used and refuses circular support — but it still mints an edge and returns a float at an instant. It corrected a scalar rather than deleting the operation.
|
||||
+169
-5
@@ -29,6 +29,8 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- Lexer: keywords, identifiers, integer/float/string/bool literals, operators below.
|
||||
- Parser: `let`, `return`, `fn`, `type`, `enum`, `import`, `from … import`, `while`, `for`, `if/else if/else`, `match`, `@decorator`, array/map literals, all listed operators, function calls, field access, index access, unary `!`/`-`, postfix `?`.
|
||||
- Codegen: function definitions, top-level `main()`, all expression forms above, control flow, decorator-as-AST-attachment.
|
||||
- Boundary seam: decorator arguments and stacking; VBD role enforcement via `#error`; `engram_boundary_beat` auto-emit at `@manager`/`@accessor` entry; `@route` dispatch tables (Section 9).
|
||||
- Program-level declarative blocks: `cgi`, `service`, and `program` — the last carrying process identity and configuration (Section 18).
|
||||
- C runtime: I/O, string operations, integer math, lists, maps, filesystem, command-line args, basic `json_get` substring lookup.
|
||||
|
||||
### Planned (in flight)
|
||||
@@ -37,7 +39,7 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- **Match codegen.** Currently parsed; codegen does not emit. Adding `({ ... })` statement-expression emission.
|
||||
- **`?` propagation.** Currently no-op. Adding nil-propagation semantics.
|
||||
- **`cgi` block parsing.** Currently lexed (`cgi` is a keyword) but not parsed as a statement. Adding `parse_cgi_block` and codegen of `el_cgi_init` at the head of `main()`.
|
||||
- **VBD role enforcement.** `@manager`/`@engine`/`@accessor` are accepted as decorators but not enforced. Adding compile-time check that `dharma_emit`/`dharma_field` only appear inside `@manager` functions.
|
||||
- **Boundary epilogues.** The decorator seam injects a prologue only. Adding prologue/epilogue wrapping, the prerequisite for durability-as-an-effect (Section 19.1).
|
||||
- **`vessel` keyword.** Replaces `package` in manifests. Adding to lexer.
|
||||
- **Real `engram_*` runtime.** Currently stub. Adding in-process graph store with spreading activation, Hebbian strengthening, and disk persistence — see Section 16.4.
|
||||
- **Real `dharma_*` runtime.** Currently stub. Adding network transport, channel registry, identity resolution.
|
||||
@@ -96,8 +98,10 @@ The following words are reserved and cannot be used as identifiers. Each row not
|
||||
| `while` | yes | Loop |
|
||||
| `import` / `from` / `as` | yes | Module import |
|
||||
| `true` / `false` | yes | Bool literals |
|
||||
| `cgi` | planned | Top-level CGI declaration block |
|
||||
| `manager` / `engine` / `accessor` | as decorators | VBD role marker on `fn` (enforcement planned) |
|
||||
| `cgi` | yes | Top-level CGI declaration block |
|
||||
| `service` | yes | Top-level capability-bounded declaration block |
|
||||
| `program` | yes | Top-level cross-cutting declaration block (Section 18) |
|
||||
| `manager` / `engine` / `accessor` | as decorators | VBD role marker on `fn`; enforcement and boundary auto-emit are live (Section 9) |
|
||||
| `vessel` | planned | Manifest declaration (replaces `package`) |
|
||||
| `activate` / `where` | planned | Spreading-activation construct |
|
||||
| `sealed` | planned | Capability scope block |
|
||||
@@ -446,9 +450,21 @@ Parsed. The module name is recorded; the brace-list is consumed. Both forms prod
|
||||
fn handle(channel: String, msg: String) -> Void { … }
|
||||
```
|
||||
|
||||
The `@` token followed by an identifier attaches a decorator name to the next `FnDef`. Decorators with structural meaning today: none. Planned enforcement (Section 16.2): VBD roles `@manager`, `@engine`, `@accessor`.
|
||||
The `@` token followed by an identifier attaches a decorator to the next `FnDef`.
|
||||
|
||||
Non-VBD decorators are accepted and ignored.
|
||||
**Decorators take arguments and they stack.** `@route("/p", "GET") @manager fn f()` attaches both to `f` as a `decorators` list of `{name, args}` records, topmost-first. Arguments are string literals only.
|
||||
|
||||
**Decorators have structural meaning today.** This is El's function-level boundary seam — the mechanism by which a cross-cutting concern is handled *at the boundary* rather than by a convention repeated at every call site:
|
||||
|
||||
| Decorator | Structural effect |
|
||||
|---|---|
|
||||
| `@manager` | Permits calls to `dharma_emit` / `dharma_field`. Calling either from a non-`@manager` fn emits a `#error` into the generated C — a compile-time failure, not a lint. |
|
||||
| `@manager`, `@accessor` | Codegen injects one call to `engram_boundary_beat(<fn name>)` at function entry. The decorated op self-reports (chrono tick, afferent counter, self-activity strengthen, dharma bus event) with **zero** hand-written instrumentation in its body. |
|
||||
| `@route(path, method, …)` | Records a route into a generated dispatch table. |
|
||||
|
||||
Decorators with no registered meaning are accepted and ignored.
|
||||
|
||||
**Limits of the seam, as it stands.** The injection is a *prologue only* — there is no epilogue, no wrapping of the call, and no way for a decorator to run code after the body returns. The injected callee is a fixed builtin chosen by the compiler, not derived from the decorator name or its arguments. Section 19 depends on lifting exactly these two limits.
|
||||
|
||||
---
|
||||
|
||||
@@ -1088,4 +1104,152 @@ The next minor version closes the implementation gaps named in this document. Tr
|
||||
|
||||
---
|
||||
|
||||
## 18. The Program Block — cross-cutting concerns [implemented]
|
||||
|
||||
### 18.0 Why this exists
|
||||
|
||||
A cross-cutting concern is one that belongs to the *process*, not to any function in it: only one of me may run; this is what my configuration is; every mutation must be durable; every request must be authorized.
|
||||
|
||||
El's units of encapsulation are the function and the module. Neither can hold a concern like that. So each one had been expressed the only way it could be — as a **convention**: *call this at every site.* Conventions of that shape do not hold. They are not enforced by anything, they are invisible in review, and they fail silently at the one site somebody forgot.
|
||||
|
||||
Measured in this codebase before this section existed:
|
||||
|
||||
| Concern | State | What the convention was |
|
||||
|---|---|---|
|
||||
| process identity | **zero** guards anywhere — no pidfile, no lock, no already-running check, at any layer | "check nothing is already running first" |
|
||||
| configuration | **20** distinct environment variables in one program, each with its default written inline at the read site | "remember the right default here" |
|
||||
| durability | **62** `persist_*` / `engram_save` / `wal_*` / `checkpoint` call sites | "after you mutate, remember to persist" |
|
||||
| request auth | **10** per-route `_auth` checks | "check the token in this handler too" |
|
||||
|
||||
These are not four problems. They are one absence, four times.
|
||||
|
||||
That the convention form fails is observed, not predicted. Process identity failed three times in a single day: twice, two engram processes ran simultaneously against the same data directory; twice, a stale binary held a port and answered probes while a fresh build was believed to be under test, because `pkill -f` had silently failed to match its argv — which nearly produced a false "the fix does not work" conclusion. Configuration failed structurally: `ENGRAM_DATA_DIR` was read at six sites, five of them dead bindings, and the sixth defaulted to `/tmp/engram` — contradicting the canonical resolver's `$HOME/.neuron/engram` and landing a pre-destructive safety backup on ephemeral storage.
|
||||
|
||||
The `program` block is where a concern of this shape is declared once and enforced by the compiler at the process boundary.
|
||||
|
||||
### 18.1 Syntax
|
||||
|
||||
```
|
||||
program "engram" {
|
||||
singleton: "engram"
|
||||
env ENGRAM_BIND: String = ":8742"
|
||||
env GUIDE_PORT: Int = "8771"
|
||||
env ENGRAM_API_KEY: String required
|
||||
}
|
||||
```
|
||||
|
||||
At most one `program` block per program. It composes with `cgi` and `service` — those declare what a program *may do*; `program` declares what a program *is*.
|
||||
|
||||
Grammar:
|
||||
|
||||
```ebnf
|
||||
program_block = "program" string "{" { program_field } "}" ;
|
||||
program_field = singleton_field | env_field ;
|
||||
singleton_field = "singleton" ":" string [ "," ] ;
|
||||
env_field = "env" ident ":" type
|
||||
[ "=" string ] [ "required" ] [ "," ] ;
|
||||
```
|
||||
|
||||
`singleton` and `env` are **not** reserved words. They are read as identifier token values by the block's own parse loop, so they remain usable as ordinary identifiers everywhere else. `program` is the only keyword this section adds.
|
||||
|
||||
### 18.2 Process identity — `singleton`
|
||||
|
||||
`singleton: "id"` compiles to an `el_singleton_acquire("id")` call injected as the **first statement of `main()`**, before any user statement runs.
|
||||
|
||||
The runtime takes an exclusive non-blocking `flock` on `<dir>/el-singleton-<id>.lock`, where `<dir>` is `$EL_SINGLETON_DIR`, else `$TMPDIR`, else `/tmp`. On success it writes its pid and holds the descriptor open for the life of the process. On contention it **refuses to start**: it reports the holder's pid, names the lock file, and exits 1.
|
||||
|
||||
Two properties are deliberate:
|
||||
|
||||
- **It is a lock, not a pidfile.** The kernel releases an `flock` when the owning process dies — including on `SIGKILL` and on crash. There is therefore no stale-lock state, and so no "delete the lock file to get unstuck" recovery ritual. Such a ritual would itself be a convention, which is the thing this section exists to remove.
|
||||
- **It reports the holder's pid.** "Already running" is not actionable. A pid is. This is the direct answer to the observed failure where a stale process survived a `pkill` and went on answering probes.
|
||||
|
||||
Refusal is loud and total. It is not a warning, and the program does not continue degraded. This matters more than it looks: today a second engram whose `bind()` fails merely *returns* from `http_serve` — after it has already replayed the WAL and written boot-time backup files — and then exits **0**, indistinguishable from a clean run. `singleton` refuses before the first side effect.
|
||||
|
||||
### 18.3 Configuration — `env`
|
||||
|
||||
Each `env` entry declares one configuration variable: its name, its type (`Int` or `String`), and either a default or `required`.
|
||||
|
||||
Resolution happens once, at startup, in declaration order: **the environment wins; the declaration supplies the fallback.** Then `el_config_validate` checks the whole schema and reports *every* problem at once before exiting — a startup that fails one variable at a time costs one restart per variable.
|
||||
|
||||
Values are read with `config("NAME")`, which returns a `String`.
|
||||
|
||||
The enforcement that makes the declaration real: **once a program block exists, `config("X")` for an undeclared `X` is a fatal error.** Without that, the schema would be advisory, and an advisory schema is just another convention. Programs with no `program` block are unaffected — `config()` falls back to a plain environment read, so migration is incremental and per-program.
|
||||
|
||||
The point is not that configuration is now centralized. It is that **a default is no longer a decision made at a read site.** A read site cannot disagree with another read site about what a variable means, because a read site no longer says.
|
||||
|
||||
### 18.4 What is deliberately not declared here
|
||||
|
||||
Some values look like configuration and are not. `ENGRAM_DATA_DIR` already has a single owner — `engram_resolve_data_dir()`, which resolves it, creates the directory, and fails loud rather than silently persisting to an ephemeral path. Declaring it in the `program` block as well would give it two owners that can disagree, recreating the precise defect this section removes.
|
||||
|
||||
The rule: **a variable belongs in the program block when the block would be its only owner.** If a resolver already owns it, leave it there.
|
||||
|
||||
`HOME` is likewise not configuration. It is an environment fact, and stays a raw `env()` read.
|
||||
|
||||
---
|
||||
|
||||
## 19. Boundary Effects — durability and request authorization [design only, not implemented]
|
||||
|
||||
Sections 19.1 and 19.2 specify the two remaining concerns from the table in 18.0. Both are **designed and deliberately unimplemented.** The reason is stated in 19.3 and it is not difficulty.
|
||||
|
||||
### 19.1 Durability as an epilogue effect
|
||||
|
||||
**The defect.** 62 call sites carry the convention *"after you mutate, remember to persist."* This is structurally the same defect as the index bug being fixed elsewhere in this tree — *"after you append, remember to index"* — which failed at **9 of 9** sites. A convention that failed at 100% of its sites is the strongest available evidence about what this class of convention is worth.
|
||||
|
||||
**Why the existing seam cannot express it.** §9's injection is a prologue. Durability is inherently an *epilogue*: persist after the mutation succeeds, and not at all if it threw. The seam has no epilogue.
|
||||
|
||||
**Design.** Extend the decorator seam from prologue-only to prologue/epilogue, then declare durability as an effect on the mutating function:
|
||||
|
||||
```
|
||||
@durable("engram")
|
||||
fn engram_write_node(id: String, body: String) -> Bool { … }
|
||||
```
|
||||
|
||||
Codegen wraps rather than prefixes:
|
||||
|
||||
```c
|
||||
el_val_t engram_write_node(el_val_t id, el_val_t body) {
|
||||
el_effect_enter(EL_STR("durable"), EL_STR("engram"));
|
||||
el_val_t __r = /* original body */;
|
||||
el_effect_exit(EL_STR("durable"), EL_STR("engram"), __r);
|
||||
return __r;
|
||||
}
|
||||
```
|
||||
|
||||
`el_effect_exit` is where the persist happens, and it is the only place it happens. Two properties follow that the 62 hand-written sites cannot have:
|
||||
|
||||
- **Coalescing.** The epilogue is a single choke point, so N mutations inside one request can produce one fsync instead of N. The hand-written form cannot coalesce, because no site knows about the others.
|
||||
- **Failure is not silent.** A persist that fails inside `el_effect_exit` can force the mutation's return value to failure. A forgotten `persist_*` call cannot fail — it simply does not happen, which is exactly why the defect is invisible.
|
||||
|
||||
**Enforcement, and this is the part that actually fixes it.** Mirroring §9's `#error` for `dharma_emit`: a function that calls a mutating primitive without carrying `@durable` is a **compile error**. Otherwise this is a 63rd thing to remember rather than a replacement for 62.
|
||||
|
||||
### 19.2 Request authorization as a route effect
|
||||
|
||||
**The defect.** 10 per-route `_auth` checks. The HTTP layer has no concept of authorization, so a new route is unauthenticated by default and silently so — the failure mode is a route that forgot, and nothing anywhere reports it.
|
||||
|
||||
**Design.** Authorization becomes an argument to the `@route` decorator, which already takes arguments and already builds a dispatch table:
|
||||
|
||||
```
|
||||
@route("/api/write", "POST", auth: "required")
|
||||
fn route_write(body: String) -> String { … }
|
||||
```
|
||||
|
||||
The generated dispatcher performs the check **before** dispatch, so an unauthorized request never reaches the handler and the handler contains no auth code at all.
|
||||
|
||||
The default must be `required`. A route that says nothing gets authorization; opening one up takes an explicit `auth: "public"`. Defaulting to public preserves the current failure mode exactly — forgetting stays silent — and a default that preserves the defect is not a fix.
|
||||
|
||||
Route inventory falls out for free: the dispatch table already exists, so the compiler can emit the full route/auth matrix and make "which routes are public" a fact that is read rather than audited.
|
||||
|
||||
### 19.3 Why these are not implemented
|
||||
|
||||
Not difficulty — **collision**. Both land squarely in regions two other agents hold right now:
|
||||
|
||||
- **Durability** requires changing the mutation and persist paths in `lang/runtime/el_runtime.c` and `engram/src/server.el` — the same files and the same read/write paths being restructured by concurrent work on VIndex read-path mutation and memory ownership, and on geometry-as-an-el-value and `transduce`.
|
||||
- **Request auth** requires changing route dispatch in `engram/src/server.el`, which the geometry/`transduce` work is actively reshaping.
|
||||
|
||||
Implementing either now would mean editing files under concurrent modification and resolving conflicts in exactly the paths whose correctness is currently under repair. The designs are recorded here so the work is not lost, and so that whoever lands them does so against a settled tree.
|
||||
|
||||
The prerequisite for 19.1 is the same in both cases: **lift the §9 seam from prologue-only to prologue/epilogue.** That change is independent of both collisions and can land first.
|
||||
|
||||
---
|
||||
|
||||
End of specification.
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import "../../runtime/eltest.el"
|
||||
// test_transduce.el — geometry as a first-class El value, and realizers
|
||||
// declared in El rather than patched into the runtime.
|
||||
//
|
||||
// WHAT IS ACTUALLY UNDER TEST. Until 2026-08-16 no El ingest path could carry
|
||||
// a vector: nodes took text, and geometry was DERIVED from that text. Text was
|
||||
// therefore the mandatory entry medium, so any non-text modality had to be
|
||||
// DESCRIBED in prose first and the geometry we reasoned over was the geometry
|
||||
// OF THE DESCRIPTION, not of the signal. The fix has two halves, and this file
|
||||
// exercises both:
|
||||
//
|
||||
// 1. Geometry is a VALUE — it carries its own width, so nothing has to
|
||||
// assert a width against a string's length.
|
||||
// 2. A REALIZER is an ordinary El function. `tone_realizer` below is not in
|
||||
// the runtime, is not known to the compiler, and is not special in any
|
||||
// way; it is registered BY NAME and dispatched to through transduce().
|
||||
// That is the load-bearing claim: adding a modality must not require a
|
||||
// runtime patch, or nothing has actually moved into the language.
|
||||
//
|
||||
// COMPARISON DISCIPLINE IN THIS FILE (measured 2026-08-16, not stylistic):
|
||||
// elc lowers `a == b` to a NUMERIC comparison only when both operand names are
|
||||
// in the per-function int-name set, which `let x: Int` populates. A bare call
|
||||
// like `geometry_is(g) == 0` is not a registered name, so it lowers to
|
||||
// `str_eq(...)` — strcmp on two integers reinterpreted as pointers. `<` and `>`
|
||||
// lower directly via binop_to_c with no type inference at all, so truthiness is
|
||||
// written `> 0` / `< 1` here, and any exact `==` is done on a value first bound
|
||||
// through `let x: Int`.
|
||||
|
||||
// ── A realizer, written entirely in El ──────────────────────────────────────
|
||||
// Maps a "tone" signal into a 4-component geometry. Deliberately trivial —
|
||||
// what is being proven is that an El function can BE a realizer, not that
|
||||
// this is good acoustics. The one real property it has: distinct signals
|
||||
// produce distinct geometry, so the test can tell transduction from a stub.
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
||||
g
|
||||
}
|
||||
|
||||
// A second realizer for a different modality, to prove the registry keys on
|
||||
// modality and does not just hand back "the last thing registered".
|
||||
fn pulse_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let a: Int = geometry_set(g, 0, 1.0)
|
||||
let b: Int = geometry_set(g, 1, 0.0)
|
||||
g
|
||||
}
|
||||
|
||||
// A deliberately BROKEN realizer: it returns something that is not a Geometry.
|
||||
// transduce() must not hand this back to a caller as if it were one.
|
||||
fn bogus_realizer(signal: String) -> Geometry {
|
||||
return 12345
|
||||
}
|
||||
|
||||
test "geometry-is-a-value-with-its-own-width" {
|
||||
let g: Geometry = geometry_new(8)
|
||||
let live: Int = geometry_is(g)
|
||||
assert live > 0, "geometry_new returns a live Geometry"
|
||||
let d: Int = geometry_dim(g)
|
||||
assert d == 8, "a Geometry carries its own width"
|
||||
let freed: Int = geometry_free(g)
|
||||
assert freed > 0, "geometry_free reports what it did"
|
||||
}
|
||||
|
||||
test "geometry-rejects-nonsense-without-an-arbitrary-bound" {
|
||||
// dim <= 0 is not a width. Note there is deliberately no MAX dim here:
|
||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
||||
// caller's claim about a string. A value that carries its own width has
|
||||
// nothing left to validate, so the only failure left is allocation.
|
||||
let zero: Geometry = geometry_new(0)
|
||||
let z: Int = geometry_is(zero)
|
||||
assert z < 1, "dim 0 is not a geometry"
|
||||
let neg: Geometry = geometry_new(-4)
|
||||
let n: Int = geometry_is(neg)
|
||||
assert n < 1, "negative dim is not a geometry"
|
||||
// Accessors must be total: a non-geometry is 0-width, never a crash.
|
||||
let nd: Int = geometry_dim(0)
|
||||
assert nd < 1, "geometry_dim of a non-geometry is 0"
|
||||
let ni: Int = geometry_is(0)
|
||||
assert ni < 1, "geometry_is of a non-geometry is 0"
|
||||
let nf: Int = geometry_free(0)
|
||||
assert nf < 1, "geometry_free of a non-geometry is a no-op"
|
||||
}
|
||||
|
||||
test "geometry-components-round-trip" {
|
||||
let g: Geometry = geometry_new(3)
|
||||
let s0: Int = geometry_set(g, 0, 1.5)
|
||||
let s1: Int = geometry_set(g, 1, -2.5)
|
||||
assert s0 > 0, "set in range succeeds"
|
||||
let oob: Int = geometry_set(g, 3, 9.0)
|
||||
assert oob < 1, "set out of range is refused, not silently dropped"
|
||||
let v0: Float = geometry_get(g, 0)
|
||||
let d0: Float = v0 - 1.5
|
||||
assert d0 < 0.001, "component 0 round-trips"
|
||||
assert d0 > -0.001, "component 0 round-trips"
|
||||
let v1: Float = geometry_get(g, 1)
|
||||
let d1: Float = v1 + 2.5
|
||||
assert d1 < 0.001, "component 1 round-trips (negative)"
|
||||
assert d1 > -0.001, "component 1 round-trips (negative)"
|
||||
let freed: Int = geometry_free(g)
|
||||
}
|
||||
|
||||
test "hex-is-an-edge-adapter-and-derives-its-own-width" {
|
||||
// 2 components, little-endian float32: 1.0 = 0000803f, 2.0 = 00000040.
|
||||
let g: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live > 0, "valid hex decodes to a Geometry"
|
||||
let d: Int = geometry_dim(g)
|
||||
assert d == 2, "width is DERIVED from the input, never supplied"
|
||||
let a: Float = geometry_get(g, 0)
|
||||
let da: Float = a - 1.0
|
||||
assert da < 0.001, "first component decoded"
|
||||
assert da > -0.001, "first component decoded"
|
||||
let b: Float = geometry_get(g, 1)
|
||||
let db: Float = b - 2.0
|
||||
assert db < 0.001, "second component decoded"
|
||||
assert db > -0.001, "second component decoded"
|
||||
// Egress adapter is the exact inverse.
|
||||
let back: String = geometry_to_f32le_hex(g)
|
||||
assert str_eq(back, "0000803f00000040"), "hex round-trips exactly"
|
||||
let freed: Int = geometry_free(g)
|
||||
}
|
||||
|
||||
test "hex-rejects-malformed-input" {
|
||||
let empty: Geometry = geometry_from_f32le_hex("")
|
||||
let e: Int = geometry_is(empty)
|
||||
assert e < 1, "empty hex is not a geometry"
|
||||
let ragged: Geometry = geometry_from_f32le_hex("0000803f0000")
|
||||
let r: Int = geometry_is(ragged)
|
||||
assert r < 1, "length not a multiple of 8 is refused"
|
||||
let nonhex: Geometry = geometry_from_f32le_hex("zzzzzzzz")
|
||||
let nh: Int = geometry_is(nonhex)
|
||||
assert nh < 1, "non-hex characters are refused"
|
||||
}
|
||||
|
||||
test "a-realizer-declared-in-el-is-a-first-class-realizer" {
|
||||
// THE CLAIM: tone_realizer is an ordinary El function. It is not in the
|
||||
// runtime and the compiler knows nothing about it. Registering it by name
|
||||
// is enough to make it the organ for a modality.
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
assert reg > 0, "an El fn registers as a realizer by name"
|
||||
let has: Int = realizer_has("tone")
|
||||
assert has > 0, "the modality now has an organ"
|
||||
|
||||
let g: Geometry = transduce("aaa", "tone")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live > 0, "transduce returns real geometry"
|
||||
let d: Int = geometry_dim(g)
|
||||
assert d == 4, "the El realizer determined the width, not the runtime"
|
||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
||||
// actually reached the El function rather than a stub answering for it.
|
||||
let c0: Float = geometry_get(g, 0)
|
||||
let dc: Float = c0 - 3.0
|
||||
assert dc < 0.001, "the signal reached the El realizer"
|
||||
assert dc > -0.001, "the signal reached the El realizer"
|
||||
let freed: Int = geometry_free(g)
|
||||
}
|
||||
|
||||
test "distinct-signals-transduce-to-distinct-geometry" {
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let g1: Geometry = transduce("aa", "tone")
|
||||
let g2: Geometry = transduce("aaaaa", "tone")
|
||||
let a: Float = geometry_get(g1, 0)
|
||||
let b: Float = geometry_get(g2, 0)
|
||||
let diff: Float = b - a
|
||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
||||
assert diff > 2.9, "different signals produce different geometry"
|
||||
assert diff < 3.1, "different signals produce different geometry"
|
||||
let f1: Int = geometry_free(g1)
|
||||
let f2: Int = geometry_free(g2)
|
||||
}
|
||||
|
||||
test "the-registry-keys-on-modality" {
|
||||
let r1: Int = realizer_register("tone", "tone_realizer")
|
||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
||||
assert r2 > 0, "a second modality registers independently"
|
||||
let gt: Geometry = transduce("aaa", "tone")
|
||||
let gp: Geometry = transduce("aaa", "pulse")
|
||||
let dt: Int = geometry_dim(gt)
|
||||
let dp: Int = geometry_dim(gp)
|
||||
assert dt == 4, "tone still routes to its own realizer"
|
||||
assert dp == 2, "pulse routes to a different realizer"
|
||||
let f1: Int = geometry_free(gt)
|
||||
let f2: Int = geometry_free(gp)
|
||||
}
|
||||
|
||||
test "no-organ-is-reported-as-no-organ" {
|
||||
// A modality with no realizer must transduce to NOTHING. It must never
|
||||
// fall back to embedding a description of the signal and calling that
|
||||
// perception — that silent substitution is the entire defect this change
|
||||
// exists to end.
|
||||
let has: Int = realizer_has("echolocation")
|
||||
assert has < 1, "unregistered modality has no organ"
|
||||
let g: Geometry = transduce("anything", "echolocation")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live < 1, "no realizer means no geometry, not fake geometry"
|
||||
}
|
||||
|
||||
test "registration-of-an-unresolvable-name-fails-loudly" {
|
||||
// Reported at the moment of WIRING, not later as "this modality mysteriously
|
||||
// produces nothing". Distinguishing "no organ" from "broken organ" is the
|
||||
// lesson that made this whole change necessary.
|
||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||
assert bad < 1, "an unresolvable realizer name is a registration failure"
|
||||
let has: Int = realizer_has("ghost")
|
||||
assert has < 1, "and nothing gets registered"
|
||||
}
|
||||
|
||||
test "a-realizer-returning-non-geometry-transduces-nothing" {
|
||||
let reg: Int = realizer_register("bogus", "bogus_realizer")
|
||||
assert reg > 0, "the symbol resolves, so registration succeeds"
|
||||
// ...but the contract is enforced at the boundary, so the caller never
|
||||
// receives a value that would misbehave far away from here.
|
||||
let g: Geometry = transduce("x", "bogus")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live < 1, "a non-Geometry return transduced nothing"
|
||||
}
|
||||
|
||||
test "norm-lets-a-caller-check-a-realizer-emitted-signal" {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let z: Float = geometry_norm(g)
|
||||
assert z < 0.001, "a fresh geometry is zero — norm says so"
|
||||
let s0: Int = geometry_set(g, 0, 3.0)
|
||||
let s1: Int = geometry_set(g, 1, 4.0)
|
||||
let n: Float = geometry_norm(g)
|
||||
let dn: Float = n - 5.0
|
||||
assert dn < 0.001, "3-4-5: norm is 5"
|
||||
assert dn > -0.001, "3-4-5: norm is 5"
|
||||
let freed: Int = geometry_free(g)
|
||||
}
|
||||
Reference in New Issue
Block a user