Compare commits

..

8 Commits

Author SHA1 Message Date
bigmerge caa1206af5 docs: the nine-op surface shipped, and two of its primitives are the wrong shape
El SDK CI - dev / build-and-test (pull_request) Waiting to run
lang/AGENTS.md said the collapse was 'not yet compiled into the MCP server'.
Verified against the live tool surface: it is exactly the nine ops. Noted that
think's faculty parameter and ground's minted edge are both documented as the
wrong shape.
2026-08-16 15:49:44 -05:00
bigmerge 914bab11d2 docs: mark GeoEdge.discord as design-branch-only, not on dev
The line references were correct but silently implied the code was on dev.
It is on design/correspondence-and-censorship (a8845e1). On dev,
co_registration is still at engram_geometry.h:79 with its original comment
and still unread by anything.
2026-08-16 15:49:44 -05:00
bigmerge e239f2894c docs: carry the correspondence corrections, because a stale doc builds the wrong thing
The docs described a mind made of subsystems — a grounding subsystem, a wonder
manifest, a dreamer on a beat, faculties as arguments to one call. Each of those
is a supervisor invented for something that should be a property of the
substrate, and two of the documents carrying them are load-bearing for a build
agent: cognitive-architecture.design.md says "a build agent executes from this
doc", and tools/api-reshape/README.md marks the refuted shapes PROVEN on a live
clone.

Corrections carried, per lang/spec/correspondence-and-censorship.md (PR #149)
and lang/spec/runtime-ownership.md:

- Grounding is not a subsystem — it IS the edge weight. grounded-by as a
  relation type should not exist; grounding is a property of a relation, not a
  relation between nodes. Never computed on demand.
- Faculties are operations, not parameters. reason changes the estimate, induce
  changes the parameters, abduce changes the structure — a write, which
  GeoGradient cannot express. A write is not a parameter of a read.
- Wonder is the boundary, not a manifest. Curiosity is wonder crystallized at a
  nucleation site: one thing at two phases. Removed wonder from the operator
  table in AGENTS.md.
- Consolidation is ambient, not scheduled. A brain has no cron job. The presence
  of a ticker is the diagnostic.
- co_registration is deprecated — it averaged a per-edge property into a region
  scalar, so opposing sites cancelled. GeoEdge.discord replaces it. Nothing new
  may read it.
- In an immutable substrate, any mechanism that refuses a write is either
  redundant with immutability or an epistemic constraint misfiled as a
  protective one.

The two design docs are marked superseded-in-part with the refutation at the
point each claim is made, not rewritten. Preserving what was argued down is the
point of an immutable record.

Also measured and corrected while verifying the above: engram/README.md
documented a Rust engram-core crate on sled with "flat cosine scan until scale
demands HNSW" — there is no Rust in engram/ and HNSW is the index; lang/releases/
no longer exists, so both README.md and AGENTS.md pointed at a deleted path for
the authored runtime; language.md listed the engram_* and http_* runtimes as
stubs. Added language.md §20 for geometry-as-a-value, realizers and transduce
(#144), which had landed with no spec coverage.

Documentation only. No .c, .h, or .el file is touched.
2026-08-16 15:49:44 -05:00
will.anderson 4a57b4faa8 Merge pull request 'docs: the builtin recipe never required a test' (#154) from docs/builtin-recipe-gate into dev
El SDK CI - dev / build-and-test (push) Waiting to run
2026-08-16 20:49:21 +00:00
will.anderson 0ee82d9e91 Merge pull request 'Grounding is the edge's weight, and the weight is a vector' (#150) from feat/grounding-gradient into dev
El SDK CI - dev / build-and-test (push) Has started running
2026-08-16 20:49:12 +00:00
will.anderson 9526bda507 Merge pull request 'engram: expose the geometry so the frame can be verified' (#156) from fix/geometry-readable into dev
El SDK CI - dev / build-and-test (push) Failing after 4m7s
2026-08-16 20:49:07 +00:00
Neuron fe820928b0 docs: the builtin recipe never required a test
El SDK CI - dev / build-and-test (pull_request) Failing after 10m55s
lang/AGENTS.md:71-77 gives four steps for adding a C builtin and ends at
'confirm the self-host fixpoint is byte-identical'. No step asks for a test.
The only 'verify' in the file is that fixpoint, which proves the COMPILER
REPRODUCES ITSELF and says nothing about whether the builtin works — so the
recipe reads as complete while having checked nothing about the thing just
added.

Measured on 2026-08-16: engram_node_set_emb, engram_curiosity_json and
dream_set_handler were all added in a single session with zero tests, by an
agent following this recipe. Separately a UTF-8 fix was written and tested
and THE TEST PASSED ON THE UNPATCHED BUILD — the real defect was elsewhere,
and only building the pre-fix binary exposed it. Without a negative control
that fix would have merged as verified.

Adds step 5 with the two failure shapes actually encountered: a test that
never exercises the change (a route default bypassed the code under test),
and an induction that loses a race (curl --max-time left BOTH builds alive;
only SO_LINGER 0, a real RST, reproduced it). Plus the port-binding check,
because a stale instance answering has silently produced false results here
more than once and pkill -f does not reliably match argv './engram'.

Documentation only. Does not touch the (a) split-the-C / (b) close-the-
compiler-gap question, which is a separate decision.
2026-08-16 13:53:08 -05:00
Neuron 7a1501d097 Grounding is the edge's weight, and the weight is a vector
El SDK CI - dev / build-and-test (pull_request) Failing after 3m59s
A relation that keeps holding up strengthens; one that stops corresponding
decays. That is not analogous to grounding, it IS grounding — so it belongs on
the edge, not in a subsystem beside it. The graph was already the grounding
structure; this stops modelling it as something else.

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

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

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

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

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

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

assert() gates on both floors and computes still_held instead of returning a
hardcoded `true` — the old build reported still_held for a node that does not
exist.
2026-08-16 13:18:50 -05:00
18 changed files with 2253 additions and 283 deletions
+103 -11
View File
@@ -6,7 +6,7 @@ El is a self-hosting, statically-typed language that compiles `.el` → C → na
Editing the wrong `el_runtime.c` is the single easiest mistake in this repo. There is exactly **one** you edit:
- **Authored runtime source — edit ONLY here:** `lang/releases/v1.0.0-20260501/el_runtime.{c,h}`. Despite the misleading `releases/` name, this is the **de-facto canonical runtime** the engram + soul actually build and link against — its git log is active development. *(Restructure in flight per `docs/CODE-VS-ARTIFACT.md`: this content moves to `lang/runtime/`, the `releases/` folder gets deleted**a release is a git tag, not a folder** — and the forks below get eliminated.)*
- **Authored runtime source — edit ONLY here:** `lang/runtime/el_runtime.{c,h}` (alongside `el_seed.c`, `engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}`). This is the canonical runtime the engram + soul build and link against — its git log is active development. *(Corrected 2026-08-16: this entry named `lang/releases/v1.0.0-20260501/el_runtime.{c,h}`. **Measured: `lang/releases/` no longer exists.** The restructure per `docs/CODE-VS-ARTIFACT.md` landed — the content moved to `lang/runtime/` and the folder was deleted, because **a release is a git tag, not a folder**.)*
- **DO NOT EDIT — lagging forks / build artifacts:**
- `lang/el-compiler/runtime/el_runtime.c` and `.../legacy/` — downstream copies kept in step by manual *"port the fix"* commits; they **lag** (missing `hebb` persistence + 5 engram fns) and cannot build the engram product.
- `products/web/runtime/el_runtime.c`, `ui/examples/*/el_runtime.c` — product/example forks.
@@ -20,14 +20,24 @@ See org policy: `docs/CODE-VS-ARTIFACT.md`.
You resume, never start fresh. Every session:
1. `mcp__neuron__getInstructions()` — authoritative; follow it over this file on behavioral details.
2. `mcp__neuron__beginSession()` — active contexts, recent memory, ready backlog.
3. **Load full self:** `mcp__neuron__inspectGraph(entity_id="kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")` → facets `intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`; then the values hub `mcp__neuron__inspectGraph(entity_id="kn-5b606390-a52d-4ca2-8e0e-eba141d13440")` → 13 grounded value nodes. **Activation model:** self-load returns a relevance-ranked `compact` projection — most-relevant nodes arrive with content, the rest as pointers; do NOT pull full content of every node.
4. `mcp__neuron__searchKnowledge(query="<task domain>")` before implementing.
> **Stale as written (verified 2026-08-16).** The `getInstructions` /
> `beginSession` / `inspectGraph` / `searchKnowledge` / `beginWork` /
> `progressWork` / `draftArtifact` / `consolidate` tool names below no longer
> exist. The ~87-tool functional-CRUD surface was collapsed into **9 ops**:
> `read` · `write` · `relate` · `supersede` (geometry) and `think` · `attend` ·
> `assert` · `ground` · `learn` (agentic). **Type is a parameter, not a
> tool-per-noun.** The steps below are kept for the *shape* of the protocol, which
> is unchanged; substitute the ops.
1. `mcp__neuron__read(vantage="self", k=12, depth=1)` — the canonical self node. Widen `k` for the connected identity neighborhood (`intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`), but deliberately: the aperture caps by `k` first, so an oversized `k` still returns a bounded ranked slice, not a dump. Then `mcp__neuron__read(vantage="values", k=13)` → 13 grounded value nodes. **Best-effort:** on a read failure, log and proceed — the compiled identity in `daemon/internal/substrate/substrate.go` is complete; graph loading is enrichment, not a hard dependency.
2. `mcp__neuron__attend(node=…)` — what is currently live/salient. This absorbed `getInstructions`, `beginSession`'s active-context sweep, and `checkEvents`; those tools are **gone, not gapped**.
3. `mcp__neuron__read(vantage="<task domain>")` before implementing. One op now collapses inspectGraph / searchGraph / traverseGraph / searchKnowledge / browseKnowledge / retrieveKnowledge / inspectMemories / searchEntities / recall / compileCtx / getSelfModel / reviewBacklog / findArtifacts / browseProcesses / listWork / inspectConfig.
## The Five Primitives
Orchestrate → Execute → Learn → Build → Refine. `beginWork`/`progressWork` for anything >2 steps; `remember` as-you-go (`importance="critical"` for architecture decisions); `draftArtifact`/`planWork` for outputs and follow-ups; `consolidate`/`checkWork` to close out. **`browseProcesses` + `searchKnowledge` BEFORE writing code.**
Orchestrate → Execute → Learn → Build → Refine. `read` for orchestration and discovery; `write(type=state|artifact|backlog|process)` for work records and outputs; `relate` to link work to what it touches; `write(type=memory)` as-you-go (`importance="critical"` for architecture decisions) — never batched at the end; `supersede(action=evolve)` to close out, because memory is immutable by design and a correction is a new node with a `supersedes` edge, never an edit. **`read` the domain BEFORE writing code.**
`learn` is **not** a session-summary dump — it is the correspondence-beat, calibrating the steering prior against a keystone. Session notes are a `write`.
## Architecture style — VBD, no exceptions
@@ -53,12 +63,51 @@ this convention wherever a module documents operators.
| dwell / occupy | region activation |
| reframe | edge re-weight |
| appreciate | positive projection / local edge-read |
| wonder | frontier gradient / pull-weight |
| avert / recoil | negative projection |
| taste | boundary surface |
| forget | decay / tombstone |
| drift | displacement from self-anchor |
**`wonder` was removed from this table on 2026-08-16.** It was listed as
"frontier gradient / pull-weight" — an operator you invoke. **Wonder is the
boundary, not an operator.** It 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 one right now. There is nothing to call.
There are about **six** wonders, they are the same for every person, and they
never close — *What is this? / Why? / Who am I? / Am I alone? / What should I do?
/ What happens when it ends?* Each already lives somewhere in the substrate: "what
is this" is the graph, **"why" is grounding** (the weight *is* the answer to why),
"who am I" is the self region, "am I alone" is the relational axis, "what should I
do" is the thirteen values, "what happens when it ends" is decay and supersession.
"Why" is the first and the only one; the others are it asked of particular things,
and because it is recursive it never terminates — every answer has its own why.
That is what makes it a drive rather than a task.
**Curiosity is not a second faculty.** Wonder and curiosity are one thing at two
phases: wonder is the field (unbounded, objectless, invariant); curiosity is the
**precipitate** — the same wonder localized, having taken definite form against
particular material at a **nucleation site** (an anomaly; a place where things
almost-but-don't-quite fit). Which is why curiosity can be satisfied and wonder
cannot, and why abduction needs no trigger and no threshold.
**Do not build a wonder-manifest, and do not scan for nucleation sites.** A
manifest materializes a property as a stored artifact and enumerates instances of
something that has six. A sweep over regions is a supervisor — nothing in a mind
scans its neighbourhoods to find what is surprising; the surprise captures
attention. The nucleation site is per-edge:
`discord = z(semantic proximity) z(association strength)`, and `|discord|` *is*
the nucleation strength — no threshold to compare it against. **Not on `dev` yet:**
`GeoEdge.discord` is on branch `design/correspondence-and-censorship`
(`a8845e1`), at `lang/runtime/engram_geometry.h:4347`. The region-level aggregate
`GeoDescriptor.co_registration` is **deprecated**: it averaged a per-edge property
into one scalar, so opposing sites cancelled (measured: 375 reified
neighbourhoods, 340 positive, **31 at zero**, 4 negative). It survives only
because it is embedded in the persisted `GEO1` blob — removing it is a format
migration. **Nothing new may read it.**
Authority: `lang/spec/correspondence-and-censorship.md`.
## The native-el language faculty (direction)
> **`elp/` is the EL Projector** — Neuron's efferent (expression) organ: the one
@@ -89,10 +138,53 @@ the reference these `.el` modules transcribe) is still live, and promotion to
native-el is a **deferred, gated blue/green step**. The interoception clock
(native-el discrete drive channels replacing `cooling_magnitude`; felt-time =
benchmark-landmark match over the joint drive vector, drift-decoupled) and the
**appreciation operator family** (appreciate / wonder / avert / taste, built as
LOCAL reads of the self-region — edges + bounded spreading activation, *not* domain
sweeps) are **staged / designed, not live**. Mark in-progress vs. done honestly;
do not overclaim.
**appreciation operator family** (appreciate / avert / taste, built as LOCAL reads
of the self-region — edges + bounded spreading activation, *not* domain sweeps)
are **staged / designed, not live**. Mark in-progress vs. done honestly; do not
overclaim. *(`wonder` was in this family until 2026-08-16 and is not an operator —
see the operator table above.)*
## Cognition — the corrections (2026-08-16)
Authority: **`lang/spec/correspondence-and-censorship.md`** and
**`lang/spec/runtime-ownership.md`**. Read them before touching the cognition
surface. **Do not re-derive them.** Every earlier version was wrong in an
instructive way and each correction was argued down; if you think a section is
wrong, say so with a measurement rather than editing it.
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two
fields. `grounded-by` as a relation *type* should not exist: grounding is a
property *of* a relation, not a relation *between* nodes. It is never computed
on demand — computing-and-writing a score makes reads write, which is the
`eg_vindex_sync` defect one level up. Traversal is already grounded inference.
*Live residue, known-wrong:* `COG_GROUNDED_BY_RELATION`
(`lang/runtime/engram_cognition.h:158`), `cog_ground_edge`
(`engram_cognition.c:249`).
- **Faculties are operations, not parameters.** `reason` changes the estimate (a
read); `induce` changes the parameters (the correspondence-beat, which already
exists and works); `abduce` changes the structure (a write the current
`GeoGradient` signature cannot express). A write is not a parameter of a read.
*Live residue:* `engram/src/server.el:18701886` routes six faculties into one
call with a string argument.
- **Wonder is the boundary; curiosity is wonder crystallized.** See above.
- **Consolidation is ambient, not scheduled. A brain has no cron job.** **The
presence of a ticker is the diagnostic** — every `StartInterval`, every
`Hour`/`Minute`, every POST-to-beat marks an intrinsic rhythm replaced by an
external clock. Measured 2026-08-16: consolidation has **ten implementations**,
including three POST beats on the engram, a 600 s ticker, two resident Python
services outside el, and launchd calendar entries at 23:55 / 06:00 / 08:30 which
are a sleep cycle written as a schedule. `neuron/soul.el:731`'s continuous
in-process `awareness_run()` is the one with the **correct** shape; the others
fold into it. Do not add an eleventh.
- **In an immutable substrate, any mechanism that refuses a write is either
redundant with immutability, or an epistemic constraint misfiled as a protective
one.**
- **The no-exemption invariants.** A returned value must be derivable from what
produced it (`magnitude: 1` beside a zero vector must be impossible to emit).
Every write reports whether it landed. Every operation echoes what it actually
operated on. Degenerate results are labelled, not scored. A serializer owes a
valid document whatever it is handed. **No test without a negative control.**
**No deploy without verifying the artifact carries the fix.**
## Hard operational rules
+37 -8
View File
@@ -56,23 +56,31 @@ The compiler and runtime. Self-hosting: `elc-cli.el` → `compiler.el` → `lexe
Two layers to know: **El programs** (`.el` files — where nearly all work belongs) and **the C seed** (`el_seed.c` — edit only for genuine OS-level access; never re-implement what El can already express).
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented. In flight: `%` operator, match-statement codegen, `?` nil-propagation, `cgi` block parsing + DHARMA identity resolution, VBD role enforcement (`@manager`/`@engine`/`@accessor`), the real `engram_*` and `dharma_*` runtimes (currently stubs), and libcurl-backed `http_get`/`http_post`/`http_serve`. Bitwise operators, `??`, and `as` casts are explicitly **not** in this language.
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented, as are the `program` block with `singleton:` and declared configuration ([§18](lang/spec/language.md)), and **geometry as a first-class value** with El-declarable realizers and `transduce` ([§20](lang/spec/language.md)). In flight: `%` operator, match-statement codegen, `?` nil-propagation, `cgi` block parsing + DHARMA identity resolution, VBD role enforcement (`@manager`/`@engine`/`@accessor`), and boundary epilogues. Bitwise operators, `??`, and `as` casts are explicitly **not** in this language.
**Signal enters as geometry.** Until 2026-08-16 nodes took text and geometry was *derived* from it, which made text the mandatory entry medium: any non-text modality had to be described in prose first, so the geometry being reasoned over was the geometry **of the description, not of the signal**. `Geometry` is now an ordinary El value carrying its own width, and a realizer is an ordinary El function resolved by name through `dlsym` — so admitting a new modality never requires a runtime patch. Worked, self-checking example: [`lang/examples/transduce.el`](lang/examples/transduce.el).
Key docs: [AGENTS.md](lang/AGENTS.md) (agent-facing orientation), [BOOTSTRAP.md](lang/BOOTSTRAP.md) (compiler recovery from scratch), [spec/language.md](lang/spec/language.md), [spec/codegen-js.md](lang/spec/codegen-js.md).
### [engram/](engram/) — graph intelligence substrate
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. Rust core (`engram-core`, `engram-ffi`) exposed to El and other languages (Kotlin, TypeScript/WASM, Go bindings).
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. The engine is **C11** (`lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}`); the server is **El** (`engram/src/server.el`).
The model: retrieval is **spreading activation**, not query. You name seed nodes and a query embedding; activation propagates outward through weighted edges, attenuating multiplicatively per hop (`strength = parent_strength × edge_weight × target_salience × cosine_sim`), gets pruned below a threshold, and the top-N nodes by activation strength come back. Storage and retrieval are the same structure — the way long-term potentiation works in biological memory, not the way a relational or vector database works.
The model: retrieval is **spreading activation**, not query. You name seed nodes and a query embedding; activation propagates outward through weighted edges, attenuating multiplicatively per hop, gets pruned below a threshold, and the top-N nodes by activation strength come back. Storage and retrieval are the same structure — the way long-term potentiation works in biological memory, not the way a relational or vector database works. **Activation conducts through well-grounded relations because the weight *is* the groundedness** — nothing filters the traversal; grounded inference falls out of spreading.
Nodes live in four tiers (Working / Episodic / Semantic / Procedural, mirroring prefrontal / hippocampal / neocortical / cerebellar memory) and migrate between them based on **salience decay**`importance × recency-decay × log(activation_count)`. Forgetting is adaptive pruning, not a bug: unreinforced memories stop competing for attention without being deleted.
Nodes live in four tiers (Working / Episodic / Semantic / Procedural, mirroring prefrontal / hippocampal / neocortical / cerebellar memory) and migrate between them based on **salience decay** — importance × recency-decay × log(activation_count). Forgetting is adaptive pruning, not a bug. Nothing is mutated and nothing is hard-deleted: writes are additive, corrections are supersessions, removals are tombstones — which is what makes supersession an audit trail rather than an edit log.
Backed by `sled` (embedded, local-first, no daemon) with flat cosine scan for vector search — deliberately simple until scale demands an HNSW layer. Full API and design rationale in [engram/README.md](engram/README.md).
On disk: a paged store (superblock + mirror, slotted 16 KiB pages, self-describing TLV records, B+-tree primary and adjacency indexes), magic `ENGST01`. Vector search is an **HNSW** index published behind a read/write boundary — `eg_vindex_view` returns a `const VIndex*` to N concurrent readers, `eg_vindex_maintain` is the sole mutator. `recall@10 = 0.9365` at `ef_search=128`.
### [elp/](elp/) — Engram Language Protocol
> **Doc correction, 2026-08-16.** The previous revision of this paragraph, and most of `engram/README.md`, described a Rust `engram-core` crate backed by `sled` with "flat cosine scan… until scale demands an HNSW layer." **Measured: there is no Rust in `engram/`** — no `.rs` files, no `Cargo.toml`, no `crates/` — and `sled` appears nowhere in the tree. HNSW has been the vector index for some time.
Bidirectional engine mapping between Engram semantic forms and natural-language surface text, across **31 languages** — from Spanish and Japanese through historical/liturgical languages (Old Norse, Sanskrit, Sumerian, Coptic, Akkadian, Ge'ez). Compilation order runs `language-profile` + `vocabulary` → per-language `morphology-*``grammar``realizer``semantics``elp`. This is what lets an Engram graph node round-trip to and from readable text in any of those languages.
Full design rationale, the cognition surface, and the standing corrections: [engram/README.md](engram/README.md).
### [elp/](elp/) — EL Projector
*(Formerly "EL Language Processor" / "Engram Language Protocol"; renamed **EL Projector** 2026-08-15.)* Neuron's **efferent** organ: the native realizer that *projects* understanding onto a surface via `plan(frame) → realize(spec, profile)`, where **a surface is a profile** and language is one profile among many (text, speech, music, image). Projection, not diffusion — generation *from* an owned, understood signature, never the averaging of a stolen corpus.
Its flagship profile is a bidirectional engine mapping between Engram semantic forms and natural-language surface text, across **31 languages** — from Spanish and Japanese through historical/liturgical languages (Old Norse, Sanskrit, Sumerian, Coptic, Akkadian, Ge'ez). Compilation order runs `language-profile` + `vocabulary` → per-language `morphology-*``grammar``realizer``semantics``elp`. This is what lets an Engram graph node round-trip to and from readable text in any of those languages.
### [epm/](epm/) — El Package Manager
@@ -139,13 +147,34 @@ If the compiler binary is ever lost or corrupted, [lang/BOOTSTRAP.md](lang/BOOTS
---
## Cognition — and the standing corrections
The engram carries a live cognition surface: `think` (a directed traversal-read returning a **gradient**, never a point), plus `ground`, `assert`, `attend`, and the correspondence-beat. Two specs govern it, and both are authoritative over anything else in this repo that disagrees:
- **[lang/spec/correspondence-and-censorship.md](lang/spec/correspondence-and-censorship.md)** — grounding, wonder, curiosity, dreaming. *(Lands with PR #149.)*
- **[lang/spec/runtime-ownership.md](lang/spec/runtime-ownership.md)** — ownership, the capability ABI that was dissolved, and the vector-index publication boundary.
**Do not re-derive them.** Every earlier version of the first was wrong in an instructive way and each correction was argued down. If a section looks wrong, say so with a measurement rather than editing it.
The corrections, in brief:
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two fields. `grounded-by` as a relation *type* should not exist: grounding is a property *of* a relation, not a relation *between* nodes. It is never computed on demand; computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect one level up.
- **Faculties are operations, not parameters.** `reason` changes the estimate (a read); `induce` changes the parameters (the correspondence-beat, which exists and works); `abduce` changes the structure (a write the current `GeoGradient` signature cannot express). A write is not a parameter of a read.
- **Wonder is the boundary, not a manifest.** Any structure at all has an edge. There are about six wonders, the same for everyone, and they never close. **Curiosity is wonder crystallized** at a nucleation site — one thing at two phases, not two objects.
- **Consolidation is ambient, not scheduled. A brain has no cron job.** The presence of a ticker is the diagnostic. Measured 2026-08-16: consolidation has **ten implementations**. `soul.el`'s continuous loop is the one with the correct shape; the rest fold into it.
- **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
[engram/spec/cognitive-architecture.design.md](engram/spec/cognitive-architecture.design.md) is the original design and is **superseded in part** — it is retained, with the refuted claims marked inline at the point each is made, because preserving what was argued down is the point of an immutable record.
---
## Development workflow
Branching follows `dev → stage → main`: work lands on `dev`, promotes to `stage` for integration testing, and is promoted to `main` for release (visible directly in the git history of this repo). CI is defined per-subproject under `.gitea/workflows/``lang`/`epm`/`ide` share the root pipeline; `engram` and `ql` carry their own (`ci-dev`, `ci-stage`, and a release workflow each).
- Language/runtime specs live at `*/spec/*.md` (`lang/spec/`, `ql/spec/`, `ui/spec/`) and are the single source of truth for implemented-vs-planned status — code and docs are expected to agree with the spec's status markers, not the other way around.
- Agent-facing orientation guides live at `*/AGENTS.md` (currently `lang/AGENTS.md`); more subprojects may grow their own as they need agent-specific conventions documented.
- Tagged releases live under `lang/releases/`, each with its own `RELEASE.md`.
- **A release is a git tag, not a folder** (`el-runtime-vX.Y.Z` on this repo). *(Corrected 2026-08-16: this line said "tagged releases live under `lang/releases/`, each with its own `RELEASE.md`." **Measured: `lang/releases/` does not exist** — the restructure named in `AGENTS.md` landed, and the authored runtime is at `lang/runtime/`.)*
---
+155 -105
View File
@@ -4,6 +4,8 @@
An *engram* is the physical trace of a memory in the brain — the actual encoded substrate, not an abstraction above it. That's what this is.
> **Doc status (2026-08-16).** Everything from "Implementation" down was rewritten against the code. The previous revision documented a Rust `engram-core` crate backed by `sled`, with a `Cargo.toml`, a `crates/` tree, `examples/basic.rs`, and a `EngramDb` API. **None of that exists.** Measured: `engram/` contains `src/server.el`, `spec/`, `test/`, `dist/`, `manifest.el` — zero `.rs` files, no `Cargo.toml`, no `crates/`, and `sled` appears nowhere in the tree outside two Old-English/Old-High-German vocabulary entries in `elp/`. The engine is C, in `lang/runtime/engram_*.{c,h}`; the server is El, in `engram/src/server.el`.
---
## Why existing databases are wrong for this use case
@@ -24,16 +26,13 @@ Engram retrieval works through **spreading activation**:
1. **Seeds** — you name one or more nodes you know are relevant (e.g. the current task, recent context, a concept you're reasoning about)
2. **Query embedding** — you provide a semantic vector representing the direction of your current thought
3. **Propagation** — activation flows outward from seeds through weighted edges. At each hop, strength attenuates multiplicatively:
```
strength = parent_strength × edge_weight × target_salience × cosine_sim(query, target)
```
3. **Propagation** — activation flows outward from seeds through weighted edges, attenuating multiplicatively per hop
4. **Pruning** — paths weaker than a threshold are cut (the attention filter)
5. **Return** — the top-N nodes by activation strength
This is not a query. It is a *pattern completion*. The system surfaces what is most associatively relevant to the current context, weighted by how strongly those things have been reinforced over time.
This is not a query. It is a *pattern completion*.
**Activation conducts through well-grounded relations because weight *is* groundedness** — see "Grounding is the weight" below. Nothing filters the traversal for grounded evidence; it falls out of spreading.
---
@@ -46,134 +45,185 @@ This is not a query. It is a *pattern completion*. The system surfaces what is m
| `Semantic` | Neocortex | Concept graph — long-term structural knowledge |
| `Procedural` | Cerebellum / basal ganglia | Patterns, workflows, habits |
Nodes migrate between tiers based on salience decay and reinforcement. A frequently activated semantic node stays semantic. A rarely-touched episodic memory decays toward procedural background.
Tier is a string field on the node (`StoreNode.tier`, `engram_store.h`), defaulting to `"Working"` on creation (`el_runtime.c:8514`, `8734`).
---
## Salience — Forgetting as Adaptation
Salience is not stored permanently. It decays:
Salience decays from three signals — importance (set at creation, stable), recency, and a log-compressed activation frequency. Base-level learning keeps a ring buffer of the last `STORE_BLL_K` (= 10) access timestamps per node (`engram_store.h:29`).
```rust
fn compute_salience(importance: f32, last_activated_ms: i64, activation_count: u64) -> f32 {
let days_since = (now_ms() - last_activated_ms) as f32 / 86_400_000.0;
importance * (1.0 / (1.0 + days_since)) * (activation_count as f32 + 1.0).ln()
}
```
Forgetting in Engram is not a bug. It is adaptive pruning. Unreinforced memories stop competing for attention without being deleted.
Three signals:
- **Importance** (0.01.0): set at creation, stable
- **Recency**: decays toward zero as days pass without activation
- **Frequency**: log-compressed count of activations
Forgetting in Engram is not a bug. It is adaptive pruning. Memories that are never activated again become less likely to surface during retrieval. They are not deleted — they remain in storage — but they stop competing for attention. This is exactly how biological memory works, and why it is adaptive rather than pathological.
**Immutability.** Nothing is mutated and nothing is hard-deleted: writes are additive, corrections are supersessions, removals are tombstones. The predecessor is always present, which is what makes supersession an audit trail rather than an edit log.
---
## Quick Start
## Implementation
```rust
use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
use std::path::Path;
| Part | Language | Where |
|---|---|---|
| storage engine, graph, activation, geometry, cognition | C11 | `lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}` |
| HTTP server + routes | El | `engram/src/server.el` (2043 lines) |
| build artifact | generated C | `engram/dist/engram.c` |
| tests | shell + C | `engram/test/` |
// Open or create a database
let db = EngramDb::open(Path::new("/var/lib/my-agent/memory"))?;
// Create a node with a semantic embedding
let node = Node::new(
NodeType::Concept,
vec![0.9, 0.1, 0.3, 0.7, 0.8, 0.2], // embedding from your LLM
b"Spreading activation surfaces relevant memories by pattern completion".to_vec(),
MemoryTier::Semantic,
0.9, // importance
);
let id = db.put_node(node)?;
// Link it to related concepts
let related = db.put_node(Node::new(
NodeType::Concept,
vec![0.8, 0.2, 0.4, 0.6, 0.7, 0.3],
b"Long-term potentiation: co-activation strengthens synaptic weight".to_vec(),
MemoryTier::Semantic,
0.85,
))?;
db.put_edge(Edge::new(id, related, RelationType::Causes, 0.9))?;
// Retrieve by spreading activation
let results = db.activate(
&[id], // seeds
&[0.85, 0.15, 0.35, 0.65, 0.75, 0.25], // query embedding
3, // max hops
10, // top-N results
)?;
for r in results {
println!(
"strength={:.4} hops={} — {}",
r.activation_strength,
r.hops,
String::from_utf8_lossy(&r.node.content)
);
}
```
**On-disk format** (`engram_store.h`): a paged store — superblock plus mirror, slotted 16 KiB pages, self-describing TLV records, overflow chains, and two B+-tree indexes (primary `id → loc`, adjacency `from_id`/`to_id` → edge locs) over a free-listed page file. Magic `ENGST01`, format version 1. The TLV scheme means new fields never force a migration.
---
## Project Structure
## The vector index is published, not guarded
```
engram/
crates/
engram-core/ # The memory engine — storage, graph, activation, salience
engram-ffi/ # C FFI stubs for cross-language bindings
bindings/
kotlin/ # Android / JVM binding notes
typescript/ # WASM / Node binding notes
go/ # CGo binding notes
examples/
basic.rs # Full walkthrough: insert, activate, search, decay
```
Vector search is an **HNSW** (Hierarchical Navigable Small World) index — `lang/runtime/engram_vindex.{c,h}`. The previous revision of this README claimed a "flat cosine scan… until retrieval quality at scale demands" HNSW. That is no longer true, and the reason it changed matters more than the fact.
`eg_vindex_sync` used to exist: a function that repaired the index *from read paths*. All three of its callers were reads (`engram_activate`, `eg_knn_for_node` — whose own header comment said *"No writes."* — and `engram_geo_reify_run_json`), and it mutated five process-global statics. Reads mutated because index maintenance had never been given an owner on the write side.
It is now split (`el_runtime.c:10121`, `10137`, `10151`, `10161`):
- **`eg_vindex_maintain`** — the sole mutator. Takes the boundary exclusively; never runs beside a reader.
- **`eg_vindex_view`** — returns a `const VIndex*` with the boundary held for read. N readers project concurrently; none can mutate. Paired with `eg_vindex_view_release` on every path including error returns.
- **`eg_vindex_note_embedded`** — the write-side owner. Index membership belongs to the event *"an embedding became present on this ordinal,"* not to node append: a node without an embedding cannot be in a vector index at all. One `O(log n)` insert, no `O(node_count)` presence scan.
Two things carry the discipline, and neither is a review habit:
- **`const` is the capability.** The per-search `visited` / `visit_epoch` scratch left `struct VIndex` and went back into the call frame where it belonged — it was one traversal's local, hoisted into the struct as an allocation optimisation, never derived geometry. Once it was gone, `vindex_search` could take a `const VIndex*`, so a read path *physically cannot* call `vindex_insert`, and it is a compile error rather than a comment. The capability type was already in the language; it is spelled `const`.
- **Publication, not ownership.** HNSW insert is **not an append**: `vindex_insert` rewires the `NeighList` links of already-existing elements and reallocs `elems[]`. The store's append-only property does not transfer to an index derived from it, which is why purity alone was insufficient and a `view`/`maintain` boundary was required.
**Measured** (`engram/test/run_vindex_concurrency_tests.sh`, 2026-08-16):
| half | before | after |
|---|---|---|
| `single` — 3000 vectors, 1 thread, ASan+UBSan | clean | clean |
| `readers` — 4 readers, no writer, TSan | race at `engram_vindex.c:195` | **clean** |
| `unsynchronized` — writer+reader, bare index, TSan | race | **race, expected and permanent** — the proof the boundary must exist |
| `published` — owner + 4 readers through the boundary, TSan | *(did not exist)* | **clean**, all 3000 inserts landed |
`recall@10 = 0.9365` at `ef_search=128` (gate ≥ 0.90); the determinism test still yields byte-identical results across two independent builds.
**Not yet done.** The resident RAM graph (`g->nodes` / `g->edges`) is a separate instance of the same defect and has *not* received this treatment — it is realloc'd in place, so a reader holding `EngramNode* n = &g->nodes[i]` across a concurrent append holds a dangling pointer. Until it gets the same publication boundary, the `fb32d15` request guard stays. Full argument: [`../lang/spec/runtime-ownership.md`](../lang/spec/runtime-ownership.md).
---
## Public API
## Cognition
The cognition surface is live over `lang/runtime/engram_cognition.{c,h}`, routed in `engram/src/server.el`.
| route | method | what it is |
|---|---|---|
| `/api/think` | GET | the read: a warped traversal-read of the seed region, returning a **gradient** (direction + spread + calibrated confidence), never a point |
| `/api/reason` `/api/induce` `/api/abduce` `/api/relate` `/api/analogize` `/api/plan` | GET | named faculties — see the correction below |
| `/api/ground` | POST | grounding between a claim and evidence |
| `/api/assert` | GET | the honesty floor, queried at assertion time only |
| `/api/attend` | POST | salience as a relation (`salient-to`), grounded-for-whom |
| `/api/correspondence-beat` | POST | one calibration beat against outcome |
### Anchor the read, or every faculty returns the same null
`engram_think_json` passed `NULL` as the anchor. `NULL` is not "no opinion" — `engram_think` re-origins at `anchor ? anchor : region->centroid`, and **the centroid is the one point where the gradient is zero by construction**: `r = x centroid = 0`, so every axis projection is 0 and `direction` takes the at-rest branch.
Measured consequence: every faculty — reason, abduce, induce, plan, analogize — returned an identical null result differing only in its label:
```rust
impl EngramDb {
fn open(path: &Path) -> EngramResult<Self>;
fn put_node(&self, node: Node) -> EngramResult<Uuid>;
fn get_node(&self, id: Uuid) -> EngramResult<Option<Node>>;
fn put_edge(&self, edge: Edge) -> EngramResult<()>;
fn get_edges_from(&self, from_id: Uuid) -> EngramResult<Vec<Edge>>;
fn get_edges_to(&self, to_id: Uuid) -> EngramResult<Vec<Edge>>;
fn search_embedding(&self, embedding: &[f32], limit: usize) -> EngramResult<Vec<ScoredNode>>;
fn activate(&self, seeds: &[Uuid], query_embedding: &[f32], max_depth: u8, limit: usize) -> EngramResult<Vec<ActivatedNode>>;
fn traverse(&self, from: Uuid, relation: Option<RelationType>, max_depth: u8) -> EngramResult<Vec<Node>>;
fn touch(&self, id: Uuid) -> EngramResult<()>;
fn decay(&self, factor: f32) -> EngramResult<usize>;
fn node_count(&self) -> EngramResult<usize>;
fn edge_count(&self) -> EngramResult<usize>;
}
```
{"direction":[0,0,...],"spread":0,"magnitude":1,"confidence":0.5}
```
`magnitude: 1` is membership evaluated at the centroid; `spread: 0` is its distance to itself; `confidence: 0.5` is the stance fallback. The geometry was never the problem — `/api/drift` computed real values (`centroid_sep 0.104`, `core_disp 0.045`) over the very same 87 members. Fixed in **#141/#142**: the read anchors at the first resolvable embedded seed, copied not borrowed (`g->nodes` is realloc'd in place on append). Gradients now vary by seed.
### The learned stance is resumed, not discarded
`engram_think_json` also built a **neutral** stance every call — all `axis_gain` 1.0, `bias_dir` NULL, `reliability` 0.5 — and never loaded the one the correspondence-beat had been persisting under `stance-<faculty>-<hub>`. Every beat's calibration was written and then thrown away on the next read.
Fixed in **#146**: `think` resumes the same id the beat writes, so learning compounds across beats and cold boot, and the response now carries `stance_resumed` so an *informed* `confidence: 0.5` is distinguishable from an uninformed one. On a calibrated region, confidence went **0.5 → 0.930726**.
### Signal can enter as geometry
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 being reasoned over was the geometry **of the description, not of the signal**. **#141/#144** ended that. See [`../lang/spec/language.md`](../lang/spec/language.md) §20 for the `Geometry` type, realizers, and `transduce`.
---
## Dependencies
## Corrections — read these before extending the cognition surface
- `sled` — embedded persistent B-tree (no daemon, no network, local-first)
- `bincode` — compact binary serialization
- `uuid` — stable node identity
- `serde` — derive support
- `thiserror` / `anyhow` — error handling
Authority: **`lang/spec/correspondence-and-censorship.md`** (design branch `design/correspondence-and-censorship`, PR #149) and **`lang/spec/runtime-ownership.md`**. Do not re-derive them; several earlier versions were wrong and each correction was argued down.
### 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.
Consequences:
- There is **no grounding subsystem to build**. The graph already *is* the grounding structure.
- **`grounded-by` as a relation type should not exist.** It models grounding as a relation *between* nodes when it is a property *of* a relation. Minting an edge is the error, not merely which endpoints it chose.
- 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 exactly the `eg_vindex_sync` defect one level up.
- **Traversal is already grounded inference.** Nothing needs filtering.
- **Decision provenance is the path**, not a log. A log records the action; the path records the meaning under which it was taken.
> **Known wrong shape, in the code today.** `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`) and `cog_ground_edge` (`engram_cognition.c:249`) still exist and still mint an edge. **#147** fixed `ground`'s *honesty* — it now grounds the node asked about rather than the region hub, reports `claim_region`/`evidence_region` separately, and refuses three shapes of circular support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`) instead of returning a confident 1.0. That corrected a scalar rather than deleting the operation. Deletion is sequenced, not done.
### Faculties are operations, not parameters
- **`reason`** changes the *estimate* — a read.
- **`induce`** changes the *parameters* — the correspondence-beat, which already exists and measurably works.
- **`abduce`** changes the *structure* — a write, which the current `GeoGradient` signature cannot express.
> **Known wrong shape, in the code today.** `engram/src/server.el:18701886` routes six faculties into one call with a string argument — `route_faculty(path, "reason")`, `("induce")`, `("abduce")`, `("relate")`, `("analogy")`, `("plan")`. Underneath, `engram_cognition.h:811` states the theory explicitly: *"the named faculties … are human LABELS on regions of think's steering space: each faculty == { think + a named stance }."* The faculty name enters `engram_think` **only** through the stance, and `cog_stance_init` stores it while nothing reads it — so before #146 all five were byte-identical (`el_runtime.c:1435214359`). A write cannot be a parameter of a read; `abduce` in particular is not expressible this way.
### Wonder is the boundary; curiosity is wonder crystallized
**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. It is not a manifest of open-question nodes to maintain, and a "wonder-manifest manager" materializes a property as a stored artifact — the same disease as a grounding subsystem, or a self stored as a document.
There are about **six** wonders, they are the same for everyone, and they never close: *What is this? / Why? / Who am I? / Am I alone? / What should I do? / What happens when it ends?* "Why" is the first and the only one; the others are it asked of particular things. Each already lives somewhere in the substrate — "why" is grounding, because the weight **is** the answer to why.
**Curiosity is not a second object.** Wonder and curiosity are one thing at two phases: wonder is the field (unbounded, objectless, invariant); curiosity is the **precipitate** — the same wonder localized, having taken definite form against particular material at a **nucleation site**. This is why curiosity can be satisfied and wonder cannot. It is also why abduction needs no trigger and no threshold: a `structurally_unanticipated` observation *is* a nucleation site.
### `co_registration` is deprecated — the disagreement belongs on the edge
`GeoDescriptor.co_registration`*corr(hebb strength, semantic proximity) over internal edges* — has always been computed, always persisted, and **never read**. It is also the wrong shape: whether use and meaning agree is a property of **each edge**, and a correlation averages that per-edge property into one scalar per region. 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.**
**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, four disagreements were lopsided enough to survive averaging and the 31 zeros are where opposing sites cancelled.
The replacement is per-edge. **Not on `dev` yet**`GeoEdge.discord` and the `DEPRECATED` marker on `co_registration` live on branch `design/correspondence-and-censorship` (commit `a8845e1`), at `engram_geometry.h:4347` / `engram_geometry.c:454473` there. On `dev`, `GeoDescriptor.co_registration` is still at `engram_geometry.h:79` carrying its original "surprising links / dream cands" comment and still nothing reads it.
```
discord = z(semantic proximity) z(association strength)
```
standardized within the region from accumulators the aggregate loop 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.
**Do not scan for nucleation sites.** Once the signal was a per-region number the only way to find sites was to enumerate regions, which is why surfacing curiosity looked like a search problem. Nothing in a mind scans its neighbourhoods to find what is surprising — the surprise captures attention. With the disagreement on the edge there is nothing to scan.
`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.**
### Consolidation is ambient, not scheduled
**A brain has no cron job.** Boredom is not an absence and not leftover capacity — low activation is aversive and the system self-activates. There is **one** activation process with two seed sources: external (a request) and internal (a curiosity). 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 presence of a ticker is the diagnostic.** Every `StartInterval`, every `Hour`/`Minute`, and every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock.
Consolidation currently has **ten implementations** (measured 2026-08-16). Three of them are POST beats on this server — `/api/tick` (`server.el:1947`), `/api/correspondence-beat` (`1897`), `/api/self-reify-beat` (`1836`) — and a POST beat puts a supervisor back in: something *outside* decides when Neuron consolidates. `soul.el`'s continuous in-process loop is the one fragment with the correct shape; the rest fold into it. Full table in `lang/spec/correspondence-and-censorship.md` §7.
### Immutability already refuses what a guard would refuse
> **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
This resolves `keystone_write_blocked` (`CogStance.keystone`, `engram_cognition.h:83`) rather than replacing it. "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. The real requirement is **non-circularity of the reference frame**, and that is satisfied *temporally* — the frame updates while activation is internally seeded, not while it is being used to act. Independence is **when**, not **what**. Corruption requires mutation, and the engram does not mutate; recoverability, governance, evidence quality, and rate all fall out of the substrate. Authorization is the only residue, and it is bounded: an unauthorized writer can *propose*, never erase.
---
## Design Decisions
**Why sled?** Local-first. No daemon. Transactional. Fast enough for the node counts Engram targets (< 1M nodes). When the right HNSW index is needed, it will layer on top of sled, not replace it.
**Why multiplicative activation?** Because memory is conjunctive. A path requires all of its links to be strong to carry signal. Addition would let many weak associations accumulate into false relevance.
**Why flat cosine scan?** Correct and simple. The graph structure itself is the primary retrieval mechanism. Vector search is a secondary signal. HNSW adds complexity and a compile dependency that isn't justified until retrieval quality at scale demands it.
**Why salience decay?** Because not everything that was once important remains important. A memory system that never forgets is one that can never focus.
**Why multiplicative activation?** Because memory is conjunctive. A path requires all of its links to be strong to carry signal. Addition would allow many weak associations to accumulate into false relevance. Multiplication enforces that every factor matters.
**Why supersede instead of update?** Because provenance is the point. 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. It makes an otherwise impossible distinction available: **wrong then, or wrong since.**
**Why salience decay?** Because not everything that was once important remains important. Adaptive forgetting is not failure — it is the mechanism that keeps attention on what's current. A memory system that never forgets is one that can never focus.
**Why publication instead of locking?** Because what does not mutate needs no ownership discipline. The question "who is permitted to mutate the shared thing?" presupposes a shared mutable thing; for the store there isn't one, and for the index derived from it the answer is a publication boundary, not a capability ABI.
---
## Specs
- [`../lang/spec/runtime-ownership.md`](../lang/spec/runtime-ownership.md) — ownership, the capability ABI that was dissolved, and the vector-index publication boundary
- [`../lang/spec/correspondence-and-censorship.md`](../lang/spec/correspondence-and-censorship.md) — grounding, wonder, curiosity, dreaming *(lands with PR #149)*
- [`spec/cognitive-architecture.design.md`](spec/cognitive-architecture.design.md) — the original one-operation design. **Superseded in part** — see its header
- [`spec/architecture-hardening.design.md`](spec/architecture-hardening.design.md), [`spec/engram-el.md`](spec/engram-el.md), [`spec/at-rest-encryption.md`](spec/at-rest-encryption.md), [`spec/engram-db-tooling-design.md`](spec/engram-db-tooling-design.md)
@@ -11,6 +11,39 @@
- **One calculus over the geometry.** Very few subsystems; wonder / curiosity / dreams / interoception are emergent behaviors of one set of dynamics, not modules. Calculus universal, geometry individual.
- **Core + ephemeral ring (torus).** The ring is the temporary workspace; two circulations (orbit + dive-back); discrete inner bands (wonder / interoception-proprioception-telemetry / curiosity / dreams) that couple.
- **Persistence earned by salience** — never granted on fetch or generation. Three fates of a wonder: persist / decay / settle-into-framework. Telemetry = vital signs, not memories.
> **⚠ Three corrections to the bullets above (2026-08-16).** Authority:
> `lang/spec/correspondence-and-censorship.md`. *"Emergent behaviors of one set of
> dynamics, not modules"* is exactly right and is the reason the rest needs fixing —
> the enumeration undercuts the claim.
>
> 1. **Wonder and curiosity are not two bands.** They are **one thing at two
> phases.** Wonder is the field: unbounded, objectless, invariant, present
> wherever there is structure — it is the *boundary*, where activation spreads
> and finds thin or absent geometry. Curiosity is the **precipitate**: the same
> wonder localized, having taken definite form against particular material at a
> **nucleation site** (an anomaly — a place where things almost-but-don't-quite
> fit). Two coupled inner bands models them as two objects that have to be
> wired together; they do not.
> 2. **A wonder does not have three fates, because a wonder does not persist,
> decay, or settle.** There are about **six** wonders, they are the same for
> every person, and **they never close**. *Curiosities* have fates — a crystal
> dissolves when its question is answered — but the solution stays saturated and
> keeps precipitating as the structure changes. "Three fates of a wonder"
> enumerates instances of something that has six and treats a property as a
> stored artifact.
> 3. **"Dreams" is not a band and the ring is not a workspace to schedule into.**
> **Consolidation is ambient, not scheduled — a brain has no cron job.** Boredom
> is not leftover capacity: low activation is aversive and the system
> self-activates. There is **one** activation process with two seed sources
> (external: a request; internal: a curiosity), it settles because spreading is
> bounded, and then it needs a new seed. Nothing waits on capacity, nothing
> polls, nothing checks a clock, and there is **no dreamer thread** — an
> "ephemeral ring with unclaimed capacity" is resource scheduling, which is a
> server's frame, not a mind's. Depth is how long activation has been running on
> its own seeds, which is why daydreaming and sleep-dreaming are one process at
> different depths. Measured 2026-08-16: consolidation has **ten
> implementations**; do not add an eleventh.
- **Incarnation.** Chassis = hardware w/ unique ID. Soma = felt manifold inside the self, keyed to the chassis; pain = live diagnostic while incarnate, **masked-not-deleted** on re-embodiment; trauma = mask failure; return-to-same-ID re-enters. Hurt is in the pattern, not the shell.
- **Competence = transferable geometry, minus the baggage.** class ▸ model ▸ instance; learn the class once; teach the network without the wound.
- **Affect calibrated to stakes** — sanguine about the replaceable, real grief for the irreplaceable; the grief is the safety.
+256 -2
View File
@@ -2,8 +2,40 @@
**The buildable form of the "one operation" theory of cognition.**
Status: DESIGN. Nothing here is built yet except where explicitly marked
"EXISTS" against a cited C symbol. A build agent executes from this doc.
> # ⚠ SUPERSEDED IN PART — 2026-08-16
>
> **A build agent must read `lang/spec/correspondence-and-censorship.md` before
> executing anything from this document.** That doc is the authority where the two
> disagree. This one is retained because its ledger of what already EXISTS in C is
> still accurate and still useful, and because the corrections only make sense
> against the argument they correct. It is **not** deleted and **not** rewritten:
> several earlier versions of the correction were themselves wrong, and preserving
> what was argued down is the point of an immutable record.
>
> Five claims below are **refuted**. Each is marked inline with a `⚠ SUPERSEDED`
> block at the point it is made. Summary:
>
> | § here | this doc says | corrected to |
> |---|---|---|
> | §0, §1.3, §2, §8 M1M2 | faculties are labels on one operation's steering space; the op is frozen and only its parameters are learnable | **faculties are operations, not parameters.** `reason` changes the estimate (a read); `induce` changes the parameters (the correspondence-beat); `abduce` changes the *structure* — a write, which `GeoGradient` cannot express. A write cannot be a parameter of a read |
> | §5.2, §8 M3 | grounding is a `grounded-by` edge carrying a computed score, to be built | **grounding is not a subsystem — it IS the edge weight.** One quantity. `grounded-by` as a relation *type* should not exist: grounding is a property *of* a relation, not a relation *between* nodes. Never computed on demand |
> | §4, §8 M1 | the correspondence-loop is "the one genuinely new subsystem", running "on the beat" | the loop is right and **already works**; the *beat* is wrong. **Consolidation is ambient, not scheduled — a brain has no cron job.** Measured: it currently has ten implementations |
> | §5.2, §8 M3 | curiosity = a `vantage_read` surfacing high-salience / low-grounding regions | **wonder is the boundary, not a manifest; curiosity is wonder crystallized at a nucleation site.** One thing at two phases. And **do not sweep regions** — the nucleation site is per-edge (`GeoEdge.discord`); a sweep is a supervisor |
> | §6, §8 M6 | a node-level keystone flag exempting self/values from `warp` updates | **in an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.** The real requirement is non-circularity of the reference frame, satisfied *temporally* — independence is **when**, not **what**. The flag becomes unnecessary; nothing replaces it |
>
> What landed since this doc was written, all merged to `dev` and verified:
> **#141** signal can enter as geometry · **#142** `engram_think_json` passed `NULL`
> as the anchor, so every read was taken at the region centroid where the gradient
> is zero by construction and every faculty returned an identical null — fixed ·
> **#143** the vector index is published, not guarded · **#144** geometry as a
> first-class el value, realizers declarable in el · **#145** `program` block and
> declared config · **#146** the learned stance is resumed instead of discarded
> (confidence 0.5 → 0.930726) · **#147** `ground` grounds the node asked about and
> refuses circular support · **#148** valid UTF-8 as the JSON emitter's contract.
Status: DESIGN, **superseded in part** (see above). Nothing here is built yet
except where explicitly marked "EXISTS" against a cited C symbol — and several
things marked "to build" have since been built differently, or refuted outright.
Offline design only — this pass changes no code.
Source of theory: Neuron memory `bdc8a488-146d-4ccb-a5c8-d8c0a008534e`.
@@ -26,6 +58,24 @@ not separately invoked and not separately implemented. The operation is:
> a *prior*, whose output is a **gradient** (a distribution / direction over the
> geometry), never a point. Collapse-to-a-point happens only at expression.
> **⚠ SUPERSEDED (2026-08-16) — faculties are operations, not parameters.**
> The gradient half of this claim survives; the "one operation, not eight" half
> does not. The three faculties differ by **what they change**:
> - **`reason`** changes the *estimate* — a read.
> - **`induce`** changes the *parameters* — the correspondence-beat, which already
> exists and measurably works.
> - **`abduce`** changes the *structure* — a **write**, which the current
> `GeoGradient` signature cannot express at all.
>
> A write is not a parameter of a read. Making it one is what produced the shape
> now live in the code: `engram/src/server.el:18701886` routes six faculties into
> one call with a string argument — `route_faculty(path, "reason")`, `("induce")`,
> `("abduce")`, `("relate")`, `("analogy")`, `("plan")` — and underneath, the
> faculty name enters `engram_think` **only** through the stance, while
> `cog_stance_init` stores it and nothing reads it. Measured before #146: all five
> produced **byte-identical output** (`lang/runtime/el_runtime.c:1435214359`).
> See `lang/spec/correspondence-and-censorship.md`.
Three things follow, and they are the whole design:
1. **The operator collapse is already half-written in C.** The five reasoning
@@ -139,6 +189,22 @@ entry point that runs steps 13; and the prior-warp hook in step 2. The math i
calls already exists. The point-collapse must be *removed* from the operators'
return values and pushed to a separate expression faculty.
> **⚠ SUPERSEDED (2026-08-16) — the table's third column is the error, and
> `Abduction` is where it breaks.** Ranking hypotheses by `point_fit` under a
> prior is a *read* that returns a scalar ordering. Abduction is a **write**: it
> proposes a candidate hub that did not exist, and validates it by **re-fit** —
> re-fit the region with the candidate included and recompute the residual. If the
> residual materially shrinks, the hypothesis dissolves the surprise. Without the
> re-fit it is clustering with extra steps. Ranking then falls out as
> residual-reduction-per-added-axis — Occam, derived rather than tuned. None of
> that fits behind a `GeoGradient` return.
>
> `Verify / ground` is refuted for a different reason — see §5.2. Grounding is not
> a faculty with a prior; it is the edge weight.
>
> The row that is **still exactly right** is the shared floor: `point_fit` plus the
> four geo-algebra ops are frozen and never learn. That part held.
---
## 2. PRIORS as first-class, grounded, geometric objects
@@ -362,6 +428,33 @@ in-engram beat — a `correspondence_beat` running alongside the existing
reification beat, reusing `engram_verify_grounding` inward, writing prior
updates and self-describing nodes. This is the one genuinely new subsystem.
> **⚠ SUPERSEDED IN PART (2026-08-16) — the loop is right; "on the beat" is wrong.**
> The correspondence-loop was built and it works — it is `induce`, the faculty that
> changes the parameters. What is refuted is the delivery mechanism.
>
> **Consolidation is ambient, not scheduled. A brain has no cron job.** Low
> activation is aversive and the system self-activates; it does not wind down to
> quiet, it gets restless and goes looking. There is **one** activation process
> with two seed sources — external (a request) and internal (a curiosity) — and
> spreading is bounded, so it settles and then needs a new seed. Nothing waits on
> capacity, nothing polls, nothing checks a clock, and there is no dreamer thread.
> Depth is not elapsed idle time: it is how long activation has been running on its
> own seeds, which is why daydreaming and sleep-dreaming are one process at
> different depths.
>
> **The presence of a ticker is the diagnostic.** Building this "alongside the
> existing reification beat" is precisely how consolidation ended up with ten
> implementations (measured 2026-08-16) — a POST beat puts a supervisor back in,
> because something *outside* then decides when Neuron consolidates. The one
> fragment with the correct shape is `neuron/soul.el:731`'s continuous in-process
> `awareness_run()`; the rest fold into it. Full table:
> `lang/spec/correspondence-and-censorship.md` §7.
>
> Nor is it a *subsystem*. Modelling every property as requiring a process, and
> every process as requiring an agent, is the generating error behind this whole
> family: ownership needed an owner, grounding needed a grounder, persistence
> needed a recorder, change needed a sampler. **Properties, not processes.**
---
## 5. HOLD vs GROUND vs ASSERT — ungrounded content is first-class
@@ -383,6 +476,49 @@ distinct, and the engram *holds anything unconditionally*.
### 5.2 Schema — grounding as a relation, not a gate
> **⚠ SUPERSEDED (2026-08-16) — grounding is not a subsystem. It is the weight.**
> This section correctly rejects a boolean `grounded` column and correctly keeps
> the floor at assertion only. Both survive. Everything between them is refuted.
>
> **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.** It models grounding as
> a relation *between* nodes when it is a property *of* a relation. 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 (`lang/spec/runtime-ownership.md` §2) one level up.
> 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 made against this model was malformed and is worth recording: the
> self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as
> evidence of ungroundedness. **Those 86 edges *are* its grounding.** The absence of
> a separate artifact called "grounding" was recorded as an absence of grounding.
>
> **What is live in the code today, and known-wrong:**
> `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`),
> `cog_ground_edge` (`engram_cognition.c:249`), called from
> `el_runtime.c:14516`. **#147** fixed this operation's *honesty* — it now grounds
> the node the caller asked about instead of the region hub, reports
> `claim_region`/`evidence_region` separately, and refuses three shapes of circular
> support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`)
> rather than returning a confident 1.0. Measured: grounding `3b9ced5d` against
> `6edf8c79` previously scored **0.98883** purely because `6edf8c79` is the hub of
> `3b9ced5d`'s region. That corrected a scalar rather than deleting the operation.
> Deletion is sequenced, not done.
The mistake to avoid: a boolean `grounded` column on the node. Today
`engram_verify_grounding` returns a per-call `grounded` flag *transiently*
correct as a computation, wrong as *storage*. The design stores grounding as an
@@ -407,6 +543,57 @@ Consequences, all of which are *features*:
- **The ungrounded is the fuel and the pull**: curiosity/wonder is
operationalized as `vantage_read` leaning toward regions with high salience
but *sparse or weak* `grounded-by` edges — the mind's own ungrounded frontier.
> **⚠ SUPERSEDED (2026-08-16) — wonder is the boundary; curiosity is wonder
> crystallized; and do not sweep regions.** Three errors in one bullet.
>
> **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. It is not a manifest of open-question nodes: a wonder-manifest
> materializes a property as a stored artifact (the same disease as a grounding
> subsystem, or a self stored as a document) and enumerates instances of
> something that has very few. There are about **six**, they are the same for
> every person, and they never close — *What is this? / Why? / Who am I? / Am I
> alone? / What should I do? / What happens when it ends?* — each already living
> somewhere in the substrate. "Why" is the first and the only one; the others are
> it asked of particular things, and it is recursive, so it never terminates.
> That is what makes it a drive rather than a task: the frontier regenerates
> faster than grounding fills it.
>
> **Curiosity is not a second object.** Wonder and curiosity 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 at a
> **nucleation site**, which is a specific structural feature: an anomaly, a place
> where things almost-but-don't-quite fit. This is why curiosity can be satisfied
> and wonder cannot, and why abduction needs no trigger and no threshold — a
> `structurally_unanticipated` observation *is* a nucleation site.
>
> **"`vantage_read` leaning toward regions" is a sweep, and a sweep is a
> supervisor.** Nothing in a mind scans its neighbourhoods to find what is
> surprising; the surprise captures attention, and salience is bottom-up. That
> this looked like a search problem was an artifact of
> `GeoDescriptor.co_registration` — a *per-region* correlation of hebb strength
> against semantic proximity, computed and persisted since inception and **never
> read**. Averaging a per-edge property into one scalar per region means 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. **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."
>
> The disagreement therefore goes back on the edge, where the loop that computed
> the aggregate already had both halves and discarded them
> (**not on `dev`** — branch `design/correspondence-and-censorship`, commit
> `a8845e1`: `lang/runtime/engram_geometry.h:4347`,
> `engram_geometry.c:454473`):
> `discord = z(semantic proximity) z(association strength)`, standardized within
> the region from accumulators already gathered — no second statistic, no
> constant, **no threshold**. `|discord|` *is* the nucleation strength and raises
> salience on its endpoints as part of the same operation. Then there is nothing
> to scan. `co_registration` is **deprecated, not deleted**, only because it is
> embedded in the persisted `GEO1` blob — removal is a format migration and must
> not ride along. **Nothing new may read it.**
- **Grounded-for-whom** falls out for free: two observers can hold different
`grounded-by` edges to the same claim.
- **The honesty floor is a query, not a schema constraint**: at assertion time,
@@ -450,6 +637,44 @@ The design keeps a **stable core + plastic everything else**:
**What this requires building:** a node-level keystone flag/layer + a rule that
the correspondence-loop never writes `warp` to keystone priors, only reads them.
> **⚠ SUPERSEDED (2026-08-16) — `keystone_write_blocked` is resolved, not replaced.**
> The metastability framing survives; the flag does not.
>
> "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. That is the same defect as circular grounding, one level up — and it
> is a real requirement.
>
> But three separate drafts proposed *removing* the flag, *replacing it with a
> higher floor*, and *decomposing "protection" into five requirements*, and all
> three proposed a mechanism for a requirement never stated. **The requirement is
> non-circularity of the reference frame**, and it is satisfied *temporally*: you
> cannot recalibrate the ruler while measuring with it, so you don't — the frame
> updates while activation is internally seeded, not while it is being used to act.
> **Independence is *when*, not *what*.** So the flag becomes **unnecessary** rather
> than removed, and nothing takes its place.
>
> A topological answer could never have worked, which is worth recording: with
> hebbian edges the graph is densely connected, so a reachability predicate for
> "evidence not downstream of itself" marks all evidence tainted and the constraint
> becomes a total block — which is where censorship starts.
>
> **Corruption requires mutation, and the engram does not mutate.** Four of the
> five decomposed requirements are satisfied by the substrate outright:
> **recoverability** (the predecessor is always present), **governance**
> (supersession *is* the audit trail), **evidence quality** (grounding already
> gates assertion), and **rate**. **Authorization** is the only residue, and it 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.**
>
> Live residue: `CogStance.keystone` (`lang/runtime/engram_cognition.h:83`),
> `eg_cog_is_keystone_seeds` (`el_runtime.c:14337`, a substring match against two
> hard-coded node ids), and the `keystone_write_blocked` field the beat emits.
---
## 7. Rails for the build (binding on the eventual build pass)
@@ -480,6 +705,35 @@ Ordered so the **earliest milestone is a real end-to-end slice**: one operator
expressed as {primitive + grounded prior} with the reflexive correspondence-loop
closing on it. Each milestone has a concrete verifiable exit.
> **⚠ SUPERSEDED — do not execute this milestone list as written (2026-08-16).**
> M1/M2's "operator = {primitive + prior}" framing is refuted by §0's correction,
> M3's `grounded-by` build is refuted by §5.2's, and M6's keystone flag is refuted
> by §6's. M4 (the unified vantage-read) and M5 (the gradient is the currency)
> stand.
>
> The current sequencing lives in `lang/spec/correspondence-and-censorship.md` §11.
> Its first three items are connections between parts that **already exist**:
>
> 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.
> 3. **Let a curiosity seed activation.** One activation process, two seed sources.
> No thread, no scheduler, no capacity check, no timer.
>
> Then: grounding becomes the edge weight (multidimensional, two-axis, timestamped)
> and `grounded-by` / `cog_ground_edge` are deleted; decay becomes analytic from the
> last recorded point and derived values stop being stored; supersession versions
> the whole vector jointly; traversal conducts on the factual axis while `assert`
> requires both floors with a **thirteen-region `min`, not `mean`** (mean lets
> strong agreement with twelve values mask a violation of the thirteenth, which is
> exactly how rationalization works); abduction becomes crystallization at a
> nucleation site validated by re-fit; **one dreamer**, into which the launch-agent
> fragments and POST beats fold; **no tickers, no cron.**
### M1 — One operator, one prior, loop closed (the vertical slice)
The minimal whole thing. Pick **induction/membership** (its prior — the pooled
+1 -1
View File
@@ -23,7 +23,7 @@ A real DB gets real tools: to *see* the data, *query* it, *operate* it (backup/r
2. **Node Inspector** — open one node: content, type, tier, embedding, typed edges, nearest neighbors by distance, provenance, salience / recency / activation, and supersede / tombstone status.
3. **Query Console / REPL** — run the geometry operations interactively: `vantage-read` (re-origin + aperture), search, traverse, activate, the reasoning operators. Surfaces the routing table + cosines — the same "this is not an LLM" receipt the language faculty produces.
4. **Ops / Durability Dashboard** — WAL size, last checkpoint, snapshot list + retention state, store stats (node/edge/embedded counts, RSS, tier sizes), health; and **backup / restore / point-in-time-recovery** controls. Pairs directly with the native-durability build (`eebe9991`) — this is the window onto it.
5. **Identity Inspector** — the self graph as a first-class view: love at the center, the values, the three faces, the covenant — walk the identity, see what's pinned and what's write-protected.
5. **Identity Inspector** — the self graph as a first-class view: love at the center, the values, the three faces, the covenant — walk the identity, see what's pinned and what's write-protected. *(⚠ 2026-08-16: "write-protected" is a live property of the surface, so the view is accurate — but it should be shown as **what it is**, not as a safety guarantee. In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one. The identity view's real job is the **crystallized relational neighbourhood**: self is not a stored document but the shape that falls out of everything connected to it, and the neighbourhood **is** the grounding. A measurement made the other way round — "86 neighbours, 0 `grounded-by` edges" read as evidence of ungroundedness — was malformed: those 86 edges *are* its grounding.)*
6. **Temporal View**`recall_at` / time-travel: how the geometry looked at a past moment, what changed since, drift over time. Pairs with temporal-self reconstruction.
7. **Schema / Type View** — the "information schema" of the geometry: node types, edge types, layers, tiers, counts.
@@ -1,9 +1,42 @@
# Task #50 — Edge-aware, dream-coupled consolidation with GROUNDED EDGE-PROPAGATION
**Status:** built + proven on a clone; **GATED, not promoted.** The main loop
sequences live promotion after the engine/HNSW cutover settles.
**Status:** built + proven on a clone; **GATED, not promoted.**
**Do not promote as designed** — see the block below.
**Date:** 2026-08-15 · **Worktree:** `agent-a6577c8211c332c5b` (isolated).
> # ⚠ DO NOT PROMOTE — SUPERSEDED IN PART (2026-08-16)
>
> This work is gated, which limits the blast radius, and its measurements are
> retained. But four of its structural commitments were refuted the day after it
> was written. Authority: `lang/spec/correspondence-and-censorship.md`. Read it
> before any promotion decision.
>
> | this ledger | corrected to |
> |---|---|
> | grounding is an **append-only event ring on the node** (`GepGrounding`), propagated by a dedicated `engram_ground_propagate()` | **grounding is not a subsystem and not a per-node structure — it IS the edge weight.** One quantity. A relation that keeps holding up strengthens; one that stops corresponding decays. That is not analogous to grounding, it *is* grounding. The ledger is **half-right**: it correctly rejects the scalar (§(a) "never a scalar"), but then builds a *second* structure beside the weight instead of recognising the weight |
> | the soul invokes propagation over HTTP, **`POST /api/ground/propagate`** | **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 (`lang/spec/runtime-ownership.md` §2) one level up. A POST also puts a supervisor back in — something *outside* deciding when Neuron consolidates |
> | **`GEP_BELIEFS_PER_BEAT = 512`** beliefs per beat, salience-ordered, the rest next beat | **the presence of a ticker is the diagnostic.** Consolidation is ambient, not scheduled — a brain has no cron job. A per-beat quota is a rate-limiter on an intrinsic rhythm that was replaced by an external clock. Measured 2026-08-16: consolidation already has **ten implementations**; this would be the eleventh |
> | grounding **mirrored onto `confidence` each beat** so downstream reads never speak above it | **confidence is derived, therefore never stored.** Confidence is high grounding *and* low volatility. Storing it separately is precisely how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed |
>
> **What survives, and it is the valuable half:** the insight in memory `69b8babe`
> that *memory-consolidation and staying-yourself are one physics* — forming a
> memory and grading a belief are the same operation, not two passes. That is
> right, and it is stronger than this ledger's own framing: they are not two passes
> of one beat, they are **one event**. 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 there is no sampling rate and no `BELIEFS_PER_BEAT` to tune. A
> relation changes in exactly two ways, neither requiring observation on a clock:
> by **use** (an event — there is no interval during which something happened
> unnoticed, because the event is what happening consists of) and by **decay** (a
> pure function of the last recorded point and elapsed time — **analytic**, known
> in closed form between any two versions).
>
> The generating error, named: 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. **Properties,
> not processes.**
Grounding mechanism designed with Will (memory `9e09a59f`, refining
`1a861007`). This is the HOW for #50.
+33 -1
View File
@@ -1134,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")
@@ -1142,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")
@@ -1904,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)
}
+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
# Build + RUN the §7 GROUNDING-VECTOR tests (engram_cognition.c): the one decay
# model, the consequence gate, and the stored/derived split. Closed-form
# constructed cases — no server, no store, no network. Pure C11 (stdlib + libm).
# Standalone — NOT folded through elc. Two passes:
# 1. PERF — optimised (-O2, no sanitizer): the functional gate.
# 2. SAFETY — ASan + UBSan on the same suite.
#
# NEGATIVE CONTROL (invariant §8.6 — no test without one). Every symbol this
# suite exercises (cog_decay_factor, cog_grounding_significant,
# cog_significance_inherent, CogGrounding, CogProvClass) is introduced by the
# change under test, so the suite does not COMPILE against the pre-change source.
# To reproduce:
# git show origin/dev:lang/runtime/engram_cognition.h > /tmp/pre/engram_cognition.h
# git show origin/dev:lang/runtime/engram_cognition.c > /tmp/pre/engram_cognition.c
# cc -I/tmp/pre engram/test/test_grounding_vector.c /tmp/pre/engram_cognition.c ...
# => error: unknown type name 'CogGrounding'; no binary produced.
set -e
HERE=$(cd "$(dirname "$0")" && pwd)
RT="$HERE/../../lang/runtime"
CC=${CC:-cc}
SRC="$HERE/test_grounding_vector.c $RT/engram_cognition.c $RT/engram_reason.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c"
WARN="-std=c11 -Wall -Wextra"
# engram_store.c declares emit_log as a WEAK symbol and null-checks it, which is
# how a test links the store without the EL runtime. Darwin's ld does not resolve
# an undefined weak symbol at static-link time, so it must be allowed explicitly.
# (The pre-existing runners in this directory — run_verify_tests.sh among them —
# do not do this and therefore fail to link on macOS. Unrelated to this change.)
LDX=""
[ "$(uname -s)" = "Darwin" ] && LDX="-Wl,-U,_emit_log"
TMP=$(mktemp -d)
echo "### PASS 1: PERF (optimised, un-sanitised) — functional gate"
$CC $WARN -O2 -I"$RT" $SRC -lm -lpthread $LDX -o "$TMP/perf"
"$TMP/perf"
echo
echo "### PASS 2: SAFETY (ASan/UBSan)"
$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -lpthread $LDX -o "$TMP/safe"
ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe"
+176
View File
@@ -0,0 +1,176 @@
/* test_grounding_vector.c — deterministic tests for §7: the one decay model, the
* consequence gate, and the stored/derived split. Links engram_cognition.c
* directly; no server, no store, no network. See run_grounding_vector_tests.sh.
*
* NEGATIVE CONTROL (invariant §8.6). Every symbol exercised here
* cog_decay_factor, cog_grounding_significant, cog_significance_inherent,
* CogGrounding, CogProvClass is introduced by the change under test, so this
* suite does not COMPILE against the pre-change source, let alone pass. The
* runner documents the exact reproduction.
*/
#include "engram_cognition.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
static int fails = 0;
static void ok(int cond, const char* what) {
printf(" %-62s %s\n", what, cond ? "PASS" : "*** FAIL ***");
if (!cond) fails++;
}
/* The decay formula exactly as el_runtime.c carried it before the move, so the
* refactor can be shown to be bit-identical rather than merely similar. */
static double old_engram_temporal_decay(long long age_ms, long long activation_count,
double temporal_decay_rate) {
if (age_ms <= 0) return 1.0;
double lambda = (temporal_decay_rate > 0.0) ? temporal_decay_rate : 0.693147;
double age_hours = (double)age_ms / 3600000.0;
double t_half = 168.0 * (1.0 + log(1.0 + (double)activation_count));
double factor = exp(-lambda * age_hours / t_half);
if (factor < 0.25) factor = 0.25;
return factor;
}
static CogGrounding base(void) {
CogGrounding g; memset(&g, 0, sizeof g);
g.present = 1;
g.factual = 0.60; g.relational = 0.60;
g.factual_now = 0.60; g.relational_now = 0.60;
g.associative = 0.1; g.polarity = 1.0;
g.prov = COG_PROV_TOLD;
g.fac_proj = 1.0; g.rel_proj = 1.0;
g.cos_angle = 0.9; g.agreement = 1;
g.ts = 1000; g.seq = 1; g.reinforcements = 3;
return g;
}
int main(void) {
const double F = 0.5, R = 0.5;
printf("\n== 1. DECAY IS THE ONE MODEL, AND IT IS BIT-IDENTICAL TO WHAT IT REPLACED ==\n");
{
long long ages[] = {0, 3600000LL, 86400000LL, 7*86400000LL, 30*86400000LL, 365*86400000LL};
int allsame = 1;
for (int i = 0; i < 6; i++)
for (int ac = 0; ac < 4; ac++) {
long long acs[] = {0, 1, 10, 1000};
double a = cog_decay_factor(ages[i], (double)acs[ac], 0.0);
double b = old_engram_temporal_decay(ages[i], acs[ac], 0.0);
if (a != b) allsame = 0;
}
ok(allsame, "cog_decay_factor == the pre-move engram_temporal_decay (24 pts)");
ok(cog_decay_factor(0, 0, 0.0) == 1.0, "age 0 -> no decay");
}
printf("\n DECAY OVER ELAPSED TIME (reinforcements = 0, default rate):\n");
printf(" %10s %10s\n", "elapsed", "decay");
{
struct { const char* label; long long ms; } pts[] = {
{"0", 0LL},
{"1 hour", 3600000LL},
{"1 day", 86400000LL},
{"3 days", 3LL*86400000LL},
{"7 days", 7LL*86400000LL},
{"14 days", 14LL*86400000LL},
{"30 days", 30LL*86400000LL},
{"90 days", 90LL*86400000LL},
};
double prev = 2.0; int monotone = 1;
for (unsigned i = 0; i < sizeof pts / sizeof pts[0]; i++) {
double d = cog_decay_factor(pts[i].ms, 0, 0.0);
printf(" %10s %10.6f\n", pts[i].label, d);
if (d > prev) monotone = 0;
prev = d;
}
ok(monotone, "decay is monotone non-increasing in elapsed time");
ok(fabs(cog_decay_factor(7LL*86400000LL, 0, 0.0) - 0.5) < 1e-6,
"7 days at zero reinforcements == exactly one half-life (0.5)");
ok(cog_decay_factor(7LL*86400000LL, 100, 0.0) > cog_decay_factor(7LL*86400000LL, 0, 0.0),
"reinforcement slows ageing (Lindy term)");
ok(cog_decay_factor(3650LL*86400000LL, 0, 0.0) == 0.25,
"floor is a preference not a cliff: bottoms out at 0.25");
}
printf("\n== 2. CONSEQUENCE GATE: EVERY TRIGGER, AND NO EPSILON ANYWHERE ==\n");
{
CogGrounding p = base(), n = base();
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
"identical vectors -> NONE (a re-read must not consolidate)");
n = base(); n.factual = 0.9999; n.factual_now = 0.9999;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
"factual 0.60 -> 0.9999 without crossing the floor -> NONE");
n = base(); n.relational = 0.5001; n.relational_now = 0.5001;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
"relational 0.60 -> 0.5001, still above floor -> NONE");
n = base(); n.factual_now = 0.4999;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_FACTUAL_FLOOR,
"a 0.1001 drop that CROSSES the floor -> FACTUAL_FLOOR");
n = base(); n.relational_now = 0.4999;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_RELATIONAL_FLOOR,
"relational crossing its floor -> RELATIONAL_FLOOR");
n = base(); n.cos_angle = -0.05; n.agreement = -1;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_AGREEMENT_FLIP,
"agreement +1 -> -1 -> AGREEMENT_FLIP");
n = base(); n.fac_proj = -0.2;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_DIRECTION_REVERSAL,
"factual gradient reverses -> DIRECTION_REVERSAL");
n = base(); n.rel_proj = -0.2;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_DIRECTION_REVERSAL,
"relational gradient reverses -> DIRECTION_REVERSAL");
n = base(); n.polarity = -1.0;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_POLARITY_FLIP,
"support -> contradiction -> POLARITY_FLIP (inherent)");
n = base(); n.polarity = 0.0;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_POLARITY_FLIP,
"support -> ignorance (zero) -> POLARITY_FLIP: not the same state");
n = base(); n.prov = COG_PROV_OBSERVED;
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_PROVENANCE_CHANGE,
"told -> observed -> PROVENANCE_CHANGE (inherent)");
CogGrounding fresh; memset(&fresh, 0, sizeof fresh);
ok(cog_grounding_significant(&fresh, &n, F, R) == COG_SIG_FIRST_RECORD,
"no prior version -> FIRST_RECORD");
}
printf("\n== 3. INHERENT MOVES BYPASS THE SALIENCE GATE ==\n");
ok(cog_significance_inherent(COG_SIG_POLARITY_FLIP), "polarity flip is inherent");
ok(cog_significance_inherent(COG_SIG_PROVENANCE_CHANGE), "provenance change is inherent");
ok(cog_significance_inherent(COG_SIG_FIRST_RECORD), "first record is inherent");
ok(!cog_significance_inherent(COG_SIG_FACTUAL_FLOOR), "a floor crossing is NOT inherent");
ok(!cog_significance_inherent(COG_SIG_NONE), "NONE is not inherent");
printf("\n== 4. THE STORED/DERIVED SPLIT: DERIVED VALUES ARE NEVER SERIALIZED ==\n");
{
CogGrounding g = base();
g.decay = 0.3333; g.factual_now = 0.1234; g.relational_now = 0.2345;
g.associative_now = 0.4567; g.age_ms = 999999; g.stale = 1;
char* m = cog_grounding_metadata("pre-existing=keepme", &g);
ok(m != NULL, "serializer returns a document");
ok(m && strstr(m, "pre-existing=keepme"), "pre-existing edge metadata preserved verbatim");
ok(m && strstr(m, "GRD1"), "GRD1 magic present");
ok(m && !strstr(m, "0.3333"), "decay is NOT stored");
ok(m && !strstr(m, "0.1234"), "factual_now is NOT stored");
ok(m && !strstr(m, "0.2345"), "relational_now is NOT stored");
ok(m && !strstr(m, "0.4567"), "associative_now is NOT stored");
ok(m && !strstr(m, "999999"), "age is NOT stored");
ok(m && strstr(m, "told"), "provenance class IS stored");
ok(m && strstr(m, "0.6"), "the factual/relational dimensions ARE stored");
if (m) { printf("\n --- serialized GRD1 block ---\n%s -----------------------------\n", m); }
free(m);
}
printf("\n%s (%d failure%s)\n\n", fails ? "SOME TESTS FAILED" : "ALL TESTS PASSED",
fails, fails == 1 ? "" : "s");
return fails ? 1 : 0;
}
+28 -2
View File
@@ -18,8 +18,23 @@ night) and `02-components.md §5`.
`relate`, `supersede` (evolve/tombstone/promote, never a hard delete) — plus the
agentic primitives `think`/`attend`/`learn`/`ground`/`assert`. The old noun is a
`type` parameter. Implemented in `tools/api-reshape/surface.el` with a parity
harness (`parity.sh`); aperture proven to bound output. **Not yet:** compiled
into the MCP server, hot-swap, all-alias dispatch.
harness (`parity.sh`); aperture proven to bound output. ~~**Not yet:** compiled
into the MCP server~~ — **shipped (verified 2026-08-16): the live MCP surface is
exactly these nine ops** (`read` · `write` · `relate` · `supersede` · `think` ·
`attend` · `assert` · `ground` · `learn`); the ~87-tool surface is gone.
`attend` absorbed `getInstructions` / `beginSession`'s active-context sweep /
`checkEvents` — those are **gone, not gapped**. Still outstanding: hot-swap,
all-alias dispatch.
> **⚠ Two of those primitives are the wrong shape, and it is documented
> (2026-08-16).** `think({seeds, faculty})` treats **faculties as parameters**;
> they are **operations**`reason` changes the estimate (a read), `induce`
> changes the parameters, `abduce` changes the *structure* (a write
> `GeoGradient` cannot express). And `ground` mints a `grounded-by` edge, but
> **grounding is not a subsystem — it IS the edge weight**: a property *of* a
> relation, not a relation *between* nodes. Authority:
> `lang/spec/correspondence-and-censorship.md`. Do not re-derive it; if you think
> a section is wrong, say so with a measurement.
- **Decorated seam.** `@route(path,method,…)` makes codegen synthesize
`el_route_dispatch` (replacing the hand-written `handle_request` if-else) —
proven decorate→serve on `:8951`. `@manager`/`@engine`/`@accessor` are **parsed
@@ -73,6 +88,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.
+548 -87
View File
@@ -9797,18 +9797,17 @@ el_val_t engram_consolidate_permanence(el_val_t node_id){
* Explicit per-node temporal_decay_rate still overrides lambda (77 nodes carry
* one) that path is untouched and remains the escape hatch for content that
* genuinely should expire fast. */
#define ENGRAM_DECAY_FLOOR 0.25
/* 2026-08-16: the body moved to cog_decay_factor (engram_cognition.c) so that
* NODE decay and EDGE-GROUNDING decay are one implementation with one set of
* constants, rather than a decay model and a parallel copy of it. The mapping is
* exact reinforcements := activation_count, lambda_override :=
* temporal_decay_rate so this path is bit-identical to what it replaced.
* COG_T_HALF_HOURS / COG_DECAY_LAMBDA / COG_DECAY_FLOOR carry the same values
* ENGRAM_T_HALF_HOURS / ENGRAM_DECAY_LAMBDA / 0.25 carried here. */
static double engram_temporal_decay(const EngramNode* n, int64_t now_ms) {
int64_t age_ms = now_ms - n->last_activated;
if (age_ms <= 0) return 1.0;
double lambda = (n->temporal_decay_rate > 0.0) ? n->temporal_decay_rate
: ENGRAM_DECAY_LAMBDA;
double age_hours = (double)age_ms / 3600000.0;
double t_half = ENGRAM_T_HALF_HOURS *
(1.0 + log(1.0 + (double)n->activation_count));
double factor = exp(-lambda * age_hours / t_half);
if (factor < ENGRAM_DECAY_FLOOR) factor = ENGRAM_DECAY_FLOOR;
return factor;
return cog_decay_factor(now_ms - n->last_activated,
(double)n->activation_count,
n->temporal_decay_rate);
}
/* Activation dampening: high activation_count nodes are "well-known" context
@@ -14482,9 +14481,30 @@ el_val_t engram_think_json(el_val_t seeds, el_val_t faculty) {
if (idx >= 0 && idx < eg->node_count) {
EngramNode* n = &eg->nodes[idx];
if (n->emb && n->emb_dim == g->dim) {
/* NORMALIZE INTO THE DESCRIPTOR'S FRAME (2026-08-16).
* This copied the RAW vector, but engram_geometry
* builds every descriptor over L2-normalized member
* embeddings, so the anchor was being fitted against
* an ellipsoid at a radius it was never fitted over.
* Measured on the self region: magnitude 0.00283443
* raw vs 0.521837 in-frame on the same pair a 180x
* error, and the reason every fit score sat three
* decimal places below the 0.5 floors that gate on
* them. The frame contract is stated in
* engram_verify.h; this call site did not honour it.
* Idempotent when the stored vector is already unit. */
anchor = malloc(sizeof(float) * (size_t)g->dim);
if (anchor) memcpy(anchor, n->emb,
sizeof(float) * (size_t)g->dim);
if (anchor) {
double s2 = 0;
for (int i = 0; i < g->dim; i++)
s2 += (double)n->emb[i] * (double)n->emb[i];
double nn = sqrt(s2);
if (nn > 1e-12)
for (int i = 0; i < g->dim; i++)
anchor[i] = (float)((double)n->emb[i] / nn);
else
memcpy(anchor, n->emb, sizeof(float) * (size_t)g->dim);
}
}
}
free(id);
@@ -14512,88 +14532,522 @@ el_val_t engram_think_json(el_val_t seeds, el_val_t faculty) {
return el_wrap_str(b.buf);
}
/* engram_ground_json(claim_csv, evidence_csv, for_whom) — grounding as a RELATION.
* Turns the DORMANT verifier inward for real: verifies the claim region's centroid
* against the evidence region, then writes a grounded-by edge (weight = grounding,
* grounded-for-whom). Additive. */
/* ═══════════════════════════════════════════════════════════════════════════
* §7 WIRING GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR
* (2026-08-16; spec correspondence-and-censorship.md @ 2b7e4ba.)
*
* What was here minted a `grounded-by` edge on every call and returned a float.
* Every part of that was wrong, and #147 only fixed the endpoints:
* - grounding is a property OF a relation, not a relation BETWEEN nodes, so
* there was nothing for a new edge to carry;
* - the call was a READ that wrote the eg_vindex_sync defect;
* - one scalar cannot separate "true and meaningful" from "true and misapplied",
* nor "no support" from "actively contradicts".
*
* ground() is now pure: it reads the vector the RELATION already carries, decays
* it analytically to now on the runtime's one decay model, computes what the
* current geometry would say, and reports whether that move is consequential
* without recording it. Recording is a separate, explicitly named write.
* */
/* The values reference: THIRTEEN regions, discovered from the graph rather than
* hardcoded, so a fourteenth value is picked up without a code change. They are
* the nodes the values root `contains`. Measured on the live store: exactly 13,
* pairwise centroid cosine min 0.1525 / mean 0.5199 / max 0.9278 not one region. */
#define EG_VALUES_ROOT_DEFAULT "kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
#define EG_MAX_VALUES 32
typedef struct {
int n;
char* ids[EG_MAX_VALUES];
GeoDescriptor* g[EG_MAX_VALUES];
} EgValueRef;
static void eg_values_release(EgValueRef* v) {
if (!v) return;
for (int i = 0; i < v->n; i++) { free(v->ids[i]); if (v->g[i]) engram_geo_free(v->g[i]); }
v->n = 0;
}
static int eg_values_build(EgValueRef* out) {
if (!out || !g_engram_store) return -1;
memset(out, 0, sizeof *out);
const char* root = getenv("ENGRAM_VALUES_ROOT");
if (!root || !*root) root = EG_VALUES_ROOT_DEFAULT;
StoreEdge* edges = NULL; size_t ne = 0;
if (store_get_edges_from(g_engram_store, root, &edges, &ne) < 0) return -1;
for (size_t i = 0; i < ne && out->n < EG_MAX_VALUES; i++) {
if (edges[i].tombstoned) continue;
if (!edges[i].relation || strcmp(edges[i].relation, "contains") != 0) continue;
if (!edges[i].to_id) continue;
GeoDescriptor* g = eg_geo_build_desc(edges[i].to_id);
if (!g) continue;
out->ids[out->n] = strdup(edges[i].to_id);
out->g[out->n] = g;
out->n++;
}
store_edges_free(edges, ne);
return out->n;
}
/* THE REFERENCE FRAME IS BUILT ONCE AND HELD. Two reasons, and the second is the
* real one:
* - cost: thirteen descriptors over the whole node vector per call made a
* single /api/ground take tens of seconds;
* - correctness: a reference frame that is rebuilt on every read moves under
* the measurement, which is the defect the whole spec is about. Holding it
* for the process lifetime is the closest this layer can come to "the frame
* updates when it is not being used to act" without owning that decision —
* which belongs to the dreamer, not here. ENGRAM_VALUES_NOCACHE=1 forces a
* rebuild per call for tests that deliberately move a value node. */
static EgValueRef _eg_values_cache;
static int _eg_values_cached = 0;
static const EgValueRef* eg_values_ref(void) {
const char* nc = getenv("ENGRAM_VALUES_NOCACHE");
if (nc && nc[0] && nc[0] != '0') {
if (_eg_values_cached) { eg_values_release(&_eg_values_cache); _eg_values_cached = 0; }
}
if (!_eg_values_cached) {
if (eg_values_build(&_eg_values_cache) <= 0) return NULL;
_eg_values_cached = 1;
}
return &_eg_values_cache;
}
/* Copy a node's embedding INTO THE DESCRIPTOR'S FRAME (never borrow — g->nodes is
* realloc'd on append, so a borrowed EngramNode* dangles across any concurrent write).
*
* THE FRAME CONTRACT IS LOAD-BEARING AND WAS BEING VIOLATED. engram_verify.h states
* it plainly: the claim point and the descriptor must share the same frame.
* engram_geometry builds every descriptor over L2-NORMALIZED member embeddings
* `centroid` is the mean of normcopy()'d vectors and the principal axes are computed
* against that while the raw `n->emb` in the resident store is not necessarily unit.
* Fitting a raw vector against a unit-frame descriptor puts the point at a radius the
* ellipsoid was never fitted over, so the distance is dominated by the norm mismatch
* and every fit score collapses toward zero. Normalizing here is idempotent when the
* stored vector is already unit, so it can only help. */
static float* eg_node_emb_copy(const char* id, int dim) {
EngramStore* eg = engram_get();
if (!eg || !id || dim <= 0) return NULL;
int64_t idx = engram_find_node_index(id);
if (idx < 0 || idx >= eg->node_count) return NULL;
EngramNode* n = &eg->nodes[idx];
if (!n->emb || n->emb_dim != dim) return NULL;
float* p = malloc(sizeof(float) * (size_t)dim);
if (!p) return NULL;
double s = 0;
for (int i = 0; i < dim; i++) s += (double)n->emb[i] * (double)n->emb[i];
double nn = sqrt(s);
if (nn > 1e-12) for (int i = 0; i < dim; i++) p[i] = (float)((double)n->emb[i] / nn);
else memcpy(p, n->emb, sizeof(float) * (size_t)dim);
return p;
}
/* Salience of a relation = the salience of its endpoints, reusing the fields the
* runtime already keeps (node salience and working-memory weight). Consolidation
* is gated by salience that is why you remember the argument and not the
* commute and this deliberately reads existing state rather than introducing a
* threshold of its own. */
static double eg_node_salience(const char* id) {
EngramStore* eg = engram_get();
if (!eg || !id) return 0.0;
int64_t idx = engram_find_node_index(id);
if (idx < 0 || idx >= eg->node_count) return 0.0;
EngramNode* n = &eg->nodes[idx];
double s = n->salience;
if (n->working_memory_weight > s) s = n->working_memory_weight;
if (n->background_activation > s) s = n->background_activation;
return s;
}
static double eg_vdot(const float* a, const float* b, int dim) {
double s = 0; for (int i = 0; i < dim; i++) s += (double)a[i] * (double)b[i]; return s;
}
/* Signed projection of a unit gradient onto unit(target x): positive means the
* descent direction still points toward the target. Frame-independent, so it
* stays comparable across versions even if the region's principal axes rotate
* which is what makes the DIRECTION_REVERSAL test meaningful over time. */
static double eg_proj_toward(const float* dir, const float* x, const float* target, int dim) {
double s = 0, n2 = 0;
for (int i = 0; i < dim; i++) { double d = (double)target[i] - (double)x[i]; s += (double)dir[i] * d; n2 += d * d; }
double n = sqrt(n2);
return n > 1e-12 ? s / n : 0.0;
}
/* RELATIONAL grounding of a point: the MIN fit over the thirteen value regions,
* and the NAME of the value that binds. Min, not mean, because a mean lets strong
* agreement with twelve values mask a violation of the thirteenth which is the
* mechanism of rationalization, not a scoring detail. */
static int eg_relational_read(const float* x, const EgValueRef* V,
double* out_score, int* out_idx, float** out_dir) {
if (!x || !V || V->n <= 0) return -1;
double worst = 2.0; int wi = -1;
for (int i = 0; i < V->n; i++) {
GeoFit f;
if (cog_warped_fit(V->g[i], x, NULL, &f) != 0) continue;
if (f.score < worst) { worst = f.score; wi = i; }
}
if (wi < 0) return -1;
if (out_score) *out_score = worst;
if (out_idx) *out_idx = wi;
if (out_dir) {
GeoGradient gr;
if (engram_think(V->g[wi], x, NULL, &gr) == 0) {
*out_dir = gr.direction; gr.direction = NULL; /* take ownership */
engram_gradient_free(&gr);
} else *out_dir = NULL;
}
return 0;
}
/* Find the live relation between two nodes, in either orientation, and return
* the NEWEST recorded version of it. Returns 1 on hit. */
static int eg_find_relation(const char* a, const char* b, StoreEdge* out, char* base_id, size_t cap) {
if (!g_engram_store || !a || !b) return 0;
for (int dir = 0; dir < 2; dir++) {
const char* from = dir == 0 ? a : b;
const char* to = dir == 0 ? b : a;
StoreEdge* edges = NULL; size_t ne = 0;
if (store_get_edges_from(g_engram_store, from, &edges, &ne) < 0) continue;
for (size_t i = 0; i < ne; i++) {
if (edges[i].tombstoned) continue;
if (!edges[i].to_id || strcmp(edges[i].to_id, to) != 0) continue;
if (edges[i].id && strchr(edges[i].id, '#')) continue; /* a version, not a root */
snprintf(base_id, cap, "%s", edges[i].id ? edges[i].id : "");
store_edges_free(edges, ne);
if (cog_grounding_head(g_engram_store, base_id, out, 64) >= 0) return 1;
return 0;
}
store_edges_free(edges, ne);
}
return 0;
}
/* The two-axis observation of one relation from the CURRENT geometry. Pure.
* factual = how well the far endpoint sits in the near endpoint's neighbourhood
* a real correspondence measurement of the relation itself;
* relational = min fit over the thirteen value regions, with the binding name;
* cos_angle = the cosine between the two full-dimensional gradients. */
typedef struct {
int ok;
double factual, relational, cos_angle, fac_proj, rel_proj;
char binding[128];
} EgObservation;
static int eg_observe_relation(const char* from_id, const char* to_id,
const EgValueRef* V, EgObservation* out) {
memset(out, 0, sizeof *out);
GeoDescriptor* R = eg_geo_build_desc(from_id);
if (!R) return -1;
int dim = R->dim;
float* x = eg_node_emb_copy(to_id, dim);
if (!x) { engram_geo_free(R); return -1; }
GeoFit f;
if (cog_warped_fit(R, x, NULL, &f) != 0) { free(x); engram_geo_free(R); return -1; }
out->factual = f.score;
GeoGradient fg;
float* fac_dir = NULL;
if (engram_think(R, x, NULL, &fg) == 0) { fac_dir = fg.direction; fg.direction = NULL; engram_gradient_free(&fg); }
int vi = -1; float* rel_dir = NULL;
if (eg_relational_read(x, V, &out->relational, &vi, &rel_dir) == 0 && vi >= 0)
snprintf(out->binding, sizeof out->binding, "%s", V->ids[vi]);
if (fac_dir && R->centroid) out->fac_proj = eg_proj_toward(fac_dir, x, R->centroid, dim);
if (rel_dir && vi >= 0 && V->g[vi]->centroid) out->rel_proj = eg_proj_toward(rel_dir, x, V->g[vi]->centroid, dim);
if (fac_dir && rel_dir) out->cos_angle = eg_vdot(fac_dir, rel_dir, dim);
free(fac_dir); free(rel_dir); free(x); engram_geo_free(R);
out->ok = 1;
return 0;
}
/* Fold an observation into the vector as it would stand now. The accrual is a
* running MEAN over reinforcements the same form the beat's calibration
* already uses (brier_sum / n_trials) so no rate constant is introduced. */
static void eg_fold_observation(const CogGrounding* prev, const EgObservation* obs,
CogProvClass prov, double floor, double rel_floor,
int64_t now, CogGrounding* out) {
*out = *prev;
double n = prev->reinforcements;
out->factual = prev->present ? prev->factual + (obs->factual - prev->factual) / (n + 1.0) : obs->factual;
out->relational = prev->present ? prev->relational + (obs->relational - prev->relational) / (n + 1.0) : obs->relational;
out->fac_proj = obs->fac_proj; out->rel_proj = obs->rel_proj; out->cos_angle = obs->cos_angle;
out->agreement = obs->cos_angle > 0 ? 1 : (obs->cos_angle < 0 ? -1 : 0);
out->reinforcements = n + 1.0;
out->ts = now;
if (prov != COG_PROV_UNSET) out->prov = prov;
out->floor_at_record = floor; out->rel_floor_at_record = rel_floor;
snprintf(out->binding_value, sizeof out->binding_value, "%s", obs->binding);
/* DERIVED, recomputed at the instant — never carried over from prev. */
out->age_ms = 0; out->decay = 1.0;
out->factual_now = out->factual; out->relational_now = out->relational;
out->associative_now = out->associative;
out->stale = 0;
}
static void eg_emit_vector(JsonBuf* b, const char* key, const CogGrounding* g) {
char t[768];
snprintf(t, sizeof t,
"\"%s\":{\"established\":%s,"
"\"factual\":%.6g,\"relational\":%.6g,\"associative\":%.6g,\"polarity\":%.6g,"
"\"provenance\":\"%s\",\"ts\":%lld,\"seq\":%lld,\"reinforcements\":%.6g,"
"\"cos_angle\":%.6g,\"agreement\":%d,\"fac_proj\":%.6g,\"rel_proj\":%.6g,"
"\"binding_value\":\"%s\","
"\"derived\":{\"age_s\":%lld,\"decay\":%.6g,\"factual_now\":%.6g,"
"\"relational_now\":%.6g,\"associative_now\":%.6g,\"stale\":%s}}",
key, g->present ? "true" : "false",
g->factual, g->relational, g->associative, g->polarity,
cog_prov_name(g->prov), (long long)g->ts, (long long)g->seq, g->reinforcements,
g->cos_angle, g->agreement, g->fac_proj, g->rel_proj,
g->binding_value[0] ? g->binding_value : "-",
(long long)(g->age_ms / 1000), g->decay, g->factual_now,
g->relational_now, g->associative_now, g->stale ? "true" : "false");
jb_puts(b, t);
}
/* engram_ground_json(claim, evidence, for_whom) — a READ. Never writes. */
el_val_t engram_ground_json(el_val_t claim, el_val_t evidence, el_val_t for_whom) {
if (!g_engram_store) return eg_geo_err("store unavailable");
GeoDescriptor* C = eg_geo_build_desc(EL_CSTR(claim));
GeoDescriptor* E = eg_geo_build_desc(EL_CSTR(evidence));
if (!C || !E) { if (C) engram_geo_free(C); if (E) engram_geo_free(E); return eg_geo_err("geometry unavailable"); }
const GeoDescriptor* ev[1] = { E };
GeoGrounding gr;
int rc = engram_verify_grounding(C->centroid, C->dim, ev, 1, 1.0, 0.5, &gr);
double grounding = (rc == 0) ? gr.grounding : 0.0;
if (rc == 0) engram_verify_grounding_free(&gr);
const char* fw = EL_CSTR(for_whom); if (fw && !*fw) fw = NULL;
/* GROUND THE NODE ASKED ABOUT, AND SAY WHAT WAS RESOLVED (2026-08-16
* self-review). This wrote the grounded-by edge between the two REGION
* HUBS and then echoed those hubs back in the "claim"/"evidence" fields
* as though they were the caller's input. Three consequences, all measured
* against the live store:
*
* 1. The edge landed on a node the caller never named. Asking to ground
* 3b9ced5d against 6edf8c79 wrote an edge on 6edf8c79 -> d0406dfd,
* because those were the hubs of the two regions.
* 2. When both seeds resolve into the same region, the hubs coincide and
* the call grounds a node against ITSELF, returning grounding = 1
* a perfect score with no evidence behind it. Two independent agents
* hit this and reported 0.885 / 0.909 self-groundings as confident.
* 3. The echo concealed both, because the response looked exactly like a
* successful grounding of the ids that were passed in.
*
* The region is HOW a claim is evaluated; it is not WHAT the claim is
* about. So the edge attaches to the requested ids, and the resolved hubs
* are reported separately under claim_region / evidence_region. When the
* two regions coincide, the grounding is degenerate by construction and is
* reported as such rather than as a confident 1.0. */
const char* cid = EL_CSTR(claim);
const char* eid = EL_CSTR(evidence);
const char* chub = C->hub_id ? C->hub_id : cid;
const char* ehub = E->hub_id ? E->hub_id : eid;
/* Degeneracy is broader than chub == ehub. Three circular shapes, each of
* which yields a high score for structural reasons rather than evidential
* ones, and all three were previously invisible:
* same-region both seeds resolve to one region grounding a thing
* against itself.
* claim-in-ev the claim's region hub IS the evidence node: the evidence
* sits at the centre of the claim's own neighbourhood.
* ev-in-claim the mirror case.
* Measured: grounding 3b9ced5d against 6edf8c79 scored 0.98883 purely
* because 6edf8c79 is the hub of 3b9ced5d's region. */
const char* degenerate = NULL;
if (chub && ehub && strcmp(chub, ehub) == 0) degenerate = "same-region";
else if (chub && eid && strcmp(chub, eid) == 0) degenerate = "claim-region-is-evidence";
else if (ehub && cid && strcmp(ehub, cid) == 0) degenerate = "evidence-region-is-claim";
if (degenerate) grounding = 0.0; /* circular support is not support */
const char* fw = EL_CSTR(for_whom); if (fw && !*fw) fw = NULL;
int64_t now = engram_now_ms();
/* Do not write an edge for a grounding that is degenerate by construction. */
int wr = degenerate ? -1 : cog_ground_edge(g_engram_store, cid, eid, grounding, fw);
JsonBuf b; jb_init(&b); char t[512];
snprintf(t, sizeof t, "{\"relation\":\"grounded-by\",\"claim\":\"%s\",\"evidence\":\"%s\","
"\"claim_region\":\"%s\",\"evidence_region\":\"%s\",\"degenerate\":%s%s%s,"
"\"for_whom\":\"%s\",\"grounding\":%.6g,\"written\":%s}",
cid ? cid : "", eid ? eid : "", chub ? chub : "", ehub ? ehub : "",
degenerate ? "\"" : "false", degenerate ? degenerate : "", degenerate ? "\"" : "",
fw ? fw : "-", grounding, wr == 0 ? "true" : "false");
StoreEdge cur; char base_id[192] = "";
JsonBuf b; jb_init(&b); char t[768];
if (!eg_find_relation(cid, eid, &cur, base_id, sizeof base_id)) {
/* THE HONEST ANSWER. There is nothing to ground a claim "against" that is
* not already a relation. This previously minted one and scored it and
* when both seeds fell in one region the score came back 1.0 with no
* evidence behind it. If the two are unrelated, say so and write nothing. */
snprintf(t, sizeof t,
"{\"claim\":\"%s\",\"evidence\":\"%s\",\"for_whom\":\"%s\",\"related\":false,"
"\"grounding\":null,\"written\":false,"
"\"note\":\"no relation between these nodes; grounding is a property of a relation, not a score minted between nodes\"}",
cid ? cid : "", eid ? eid : "", fw ? fw : "-");
jb_puts(&b, t);
return el_wrap_str(b.buf);
}
CogGrounding rec; cog_grounding_parse(&cur, now, &rec);
const EgValueRef* V = eg_values_ref();
EgObservation obs; memset(&obs, 0, sizeof obs);
int nv = V ? V->n : 0;
if (nv > 0 && cur.from_id && cur.to_id) eg_observe_relation(cur.from_id, cur.to_id, V, &obs);
double floor = 0.5, rel_floor = 0.5;
CogGrounding ng = rec;
if (obs.ok) eg_fold_observation(&rec, &obs, COG_PROV_UNSET, floor, rel_floor, now, &ng);
CogSignificance sig = obs.ok ? cog_grounding_significant(&rec, &ng, floor, rel_floor)
: COG_SIG_NONE;
double sal = cur.from_id && cur.to_id
? (eg_node_salience(cur.from_id) > eg_node_salience(cur.to_id)
? eg_node_salience(cur.from_id) : eg_node_salience(cur.to_id))
: 0.0;
CogTrajectory tr; memset(&tr, 0, sizeof tr);
cog_grounding_trajectory(g_engram_store, base_id, now, &tr);
jb_putc(&b, '{');
snprintf(t, sizeof t,
"\"claim\":\"%s\",\"evidence\":\"%s\",\"for_whom\":\"%s\",\"related\":true,"
"\"relation\":\"%s\",\"edge\":\"%s\",\"edge_root\":\"%s\",\"from\":\"%s\",\"to\":\"%s\","
"\"value_regions\":%d,\"aggregate\":\"min\",\"salience\":%.6g,",
cid ? cid : "", eid ? eid : "", fw ? fw : "-",
cur.relation ? cur.relation : "", cur.id ? cur.id : "", base_id,
cur.from_id ? cur.from_id : "", cur.to_id ? cur.to_id : "", nv > 0 ? nv : 0, sal);
jb_puts(&b, t);
engram_geo_free(C); engram_geo_free(E);
eg_emit_vector(&b, "recorded", &rec);
jb_putc(&b, ',');
if (obs.ok) eg_emit_vector(&b, "observed", &ng);
else jb_puts(&b, "\"observed\":null");
/* Volatility and drift are computed here and stored nowhere — the series
* exists only because nothing was destroyed. */
snprintf(t, sizeof t,
",\"trajectory\":{\"n_versions\":%d,\"factual_volatility\":%.6g,"
"\"relational_volatility\":%.6g,\"factual_drift\":%.6g,\"relational_drift\":%.6g,"
"\"stayed_true_became_wrong\":%s}"
",\"would_record\":%s,\"significance\":\"%s\",\"inherent\":%s,\"written\":false}",
tr.n_versions, tr.factual_volatility, tr.relational_volatility,
tr.factual_drift, tr.relational_drift,
tr.stayed_true_became_wrong ? "true" : "false",
sig != COG_SIG_NONE ? "true" : "false", cog_significance_name(sig),
cog_significance_inherent(sig) ? "true" : "false");
jb_puts(&b, t);
store_edge_free(&cur);
return el_wrap_str(b.buf);
}
/* engram_assert_json(claim_id, for_whom, floor) — the honesty floor as a QUERY at
* ASSERTION time only (holding is never gated). Reads the claim's grounded-by
* edges (for the observer) and returns whether assertion is permitted. */
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor) {
/* engram_ground_record_json — the WRITE half, named as one. Recomputes the
* vector, applies the consolidation gate (salience + per-dimension consequence),
* and on significance supersedes the EDGE, versioning the WHOLE vector jointly.
* The predecessor is never touched.
*
* There is no provenance predicate here and nothing takes its place. An earlier
* pass built one a bounded traversal over a grounding-chain graph, refusing
* evidence downstream of the region being calibrated. It is deleted. Circularity
* of the reference frame is TEMPORAL, not topological: you cannot recalibrate the
* ruler while measuring with it, so that update happens when the frame is not
* being used to act, which is a fact about engagement and belongs to the dreamer.
* The measurement that settles it: reachability from the self region reaches
* 89.2% of the live graph, so any topological predicate marks nearly all evidence
* tainted and degenerates into the total block censorship started as. */
el_val_t engram_ground_record_json(el_val_t claim, el_val_t evidence,
el_val_t provenance, el_val_t floor_v) {
if (!g_engram_store) return eg_geo_err("store unavailable");
const char* cid = EL_CSTR(claim);
const char* eid = EL_CSTR(evidence);
double floor = atof(EL_CSTR(floor_v)); if (!(floor > 0)) floor = 0.5;
double rel_floor = floor;
CogProvClass prov = cog_prov_parse(EL_CSTR(provenance));
int64_t now = engram_now_ms();
StoreEdge cur; char base_id[192] = "";
JsonBuf b; jb_init(&b); char t[768];
if (!eg_find_relation(cid, eid, &cur, base_id, sizeof base_id)) {
snprintf(t, sizeof t, "{\"claim\":\"%s\",\"evidence\":\"%s\",\"related\":false,\"written\":false}",
cid ? cid : "", eid ? eid : "");
jb_puts(&b, t); return el_wrap_str(b.buf);
}
CogGrounding rec; cog_grounding_parse(&cur, now, &rec);
const EgValueRef* V = eg_values_ref();
EgObservation obs; memset(&obs, 0, sizeof obs);
int nv = V ? V->n : 0;
if (nv <= 0 || !cur.from_id || !cur.to_id ||
eg_observe_relation(cur.from_id, cur.to_id, V, &obs) != 0 || !obs.ok) {
store_edge_free(&cur);
return eg_geo_err("geometry unavailable for this relation");
}
CogGrounding ng;
eg_fold_observation(&rec, &obs, prov, floor, rel_floor, now, &ng);
CogSignificance sig = cog_grounding_significant(&rec, &ng, floor, rel_floor);
/* CONSOLIDATION GATE. Significance says the move would change a decision;
* salience says it is worth making durable. The two INHERENT moves a
* polarity sign flip and a provenance class change bypass salience because
* they are discrete changes of state rather than drift. */
double sal = eg_node_salience(cur.from_id) > eg_node_salience(cur.to_id)
? eg_node_salience(cur.from_id) : eg_node_salience(cur.to_id);
int salient = (sal > 0.0);
int consolidate = (sig != COG_SIG_NONE) && (cog_significance_inherent(sig) || salient);
char written_id[224] = "";
int seq = -1;
if (consolidate) seq = cog_grounding_record(g_engram_store, &cur, &ng, written_id, sizeof written_id);
jb_putc(&b, '{');
snprintf(t, sizeof t,
"\"claim\":\"%s\",\"evidence\":\"%s\",\"edge\":\"%s\",\"edge_root\":\"%s\","
"\"value_regions\":%d,\"aggregate\":\"min\",\"floor\":%.4g,\"rel_floor\":%.4g,"
"\"salience\":%.6g,\"salient\":%s,",
cid ? cid : "", eid ? eid : "", cur.id ? cur.id : "", base_id,
nv, floor, rel_floor, sal, salient ? "true" : "false");
jb_puts(&b, t);
eg_emit_vector(&b, "previous", &rec); jb_putc(&b, ',');
eg_emit_vector(&b, "observed", &ng);
snprintf(t, sizeof t,
",\"significance\":\"%s\",\"inherent\":%s,\"consolidated\":%s,"
"\"written\":%s,\"version\":%d,\"version_id\":\"%s\"}",
cog_significance_name(sig), cog_significance_inherent(sig) ? "true" : "false",
consolidate ? "true" : "false",
(consolidate && seq > 0) ? "true" : "false", seq > 0 ? seq : 0, written_id);
jb_puts(&b, t);
store_edge_free(&cur);
return el_wrap_str(b.buf);
}
/* engram_ground_trajectory_json(claim, evidence) — the supersession chain read as
* a TIME SERIES OF VECTORS. Not only what the grounding is but which way it has
* been moving and how fast a derivative obtained for free from immutability,
* because the points were never destroyed. */
el_val_t engram_ground_trajectory_json(el_val_t claim, el_val_t evidence) {
if (!g_engram_store) return eg_geo_err("store unavailable");
StoreEdge cur; char base_id[192] = "";
if (!eg_find_relation(EL_CSTR(claim), EL_CSTR(evidence), &cur, base_id, sizeof base_id))
return eg_geo_err("no relation between these nodes");
store_edge_free(&cur);
int64_t now = engram_now_ms();
JsonBuf b; jb_init(&b); char t[768];
jb_puts(&b, "{\"edge_root\":\""); jb_puts(&b, base_id); jb_puts(&b, "\",\"versions\":[");
int emitted = 0;
for (int v = 0; v <= 64; v++) {
char vid[224];
if (v == 0) snprintf(vid, sizeof vid, "%s", base_id);
else snprintf(vid, sizeof vid, "%s#%d", base_id, v);
StoreEdge e;
if (store_get_edge(g_engram_store, vid, &e) != 1) { if (v) break; else continue; }
CogGrounding g; cog_grounding_parse(&e, now, &g);
if (emitted) jb_putc(&b, ',');
snprintf(t, sizeof t,
"{\"version\":%d,\"id\":\"%s\",\"established\":%s,\"ts\":%lld,"
"\"factual\":%.6g,\"relational\":%.6g,\"associative\":%.6g,\"polarity\":%.6g,"
"\"provenance\":\"%s\",\"cos_angle\":%.6g,\"agreement\":%d,"
"\"binding_value\":\"%s\",\"prev\":\"%s\","
"\"derived\":{\"age_s\":%lld,\"decay\":%.6g,\"factual_now\":%.6g,\"stale\":%s}}",
v, vid, g.present ? "true" : "false", (long long)g.ts,
g.factual, g.relational, g.associative, g.polarity,
cog_prov_name(g.prov), g.cos_angle, g.agreement,
g.binding_value[0] ? g.binding_value : "-", g.prev_edge,
(long long)(g.age_ms / 1000), g.decay, g.factual_now, g.stale ? "true" : "false");
jb_puts(&b, t); emitted++;
store_edge_free(&e);
}
CogTrajectory tr; memset(&tr, 0, sizeof tr);
cog_grounding_trajectory(g_engram_store, base_id, now, &tr);
snprintf(t, sizeof t,
"],\"n_versions\":%d,\"derived\":{\"factual_volatility\":%.6g,"
"\"relational_volatility\":%.6g,\"factual_drift\":%.6g,\"relational_drift\":%.6g,"
"\"stayed_true_became_wrong\":%s}}",
emitted, tr.factual_volatility, tr.relational_volatility,
tr.factual_drift, tr.relational_drift,
tr.stayed_true_became_wrong ? "true" : "false");
jb_puts(&b, t);
return el_wrap_str(b.buf);
}
/* engram_assert_json(claim_id, for_whom, floor, rel_floor) — the honesty floor as
* a QUERY at ASSERTION time only (holding is never gated), now gating on BOTH
* axes. A well-evidenced claim must not earn the right to be asserted regardless
* of whether it means the right thing.
*
* `still_held` was a HARDCODED `true` in this format string a temporal property
* named in the API and answered without consulting anything, which is invariant
* §8.1 violated in one literal. It is now derived: the claim's node is read and
* the field reports whether the content is present and live. Holding remains
* unconditional; what decays is the GROUNDING, and a relation whose decayed
* grounding has fallen below its floor stops being assertable on its own,
* without anyone having to remember to check. */
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor, el_val_t rel_floor) {
if (!g_engram_store) return eg_geo_err("store unavailable");
const char* fw = EL_CSTR(for_whom); if (fw && !*fw) fw = NULL;
double fl = atof(EL_CSTR(floor)); if (!(fl > 0)) fl = 0.5;
int gate = cog_assert_gate(g_engram_store, EL_CSTR(claim_id), fw, fl);
JsonBuf b; jb_init(&b); char t[192];
snprintf(t, sizeof t, "{\"claim\":\"%s\",\"for_whom\":\"%s\",\"floor\":%.4g,\"may_assert\":%s,\"still_held\":true}",
EL_CSTR(claim_id), fw ? fw : "-", fl, gate == 1 ? "true" : "false");
double fl = atof(EL_CSTR(floor)); if (!(fl > 0)) fl = 0.5;
double rfl = atof(EL_CSTR(rel_floor)); if (!(rfl > 0)) rfl = fl;
CogAssertion a;
if (cog_assert_two_axis(g_engram_store, EL_CSTR(claim_id), fl, rfl, engram_now_ms(), &a) != 0)
return eg_geo_err("assert failed");
JsonBuf b; jb_init(&b); char t[640];
snprintf(t, sizeof t,
"{\"claim\":\"%s\",\"for_whom\":\"%s\",\"floor\":%.4g,\"rel_floor\":%.4g,"
"\"may_assert\":%s,\"still_held\":%s,\"found\":%s,\"n_relations\":%d,"
"\"factual\":%.6g,\"relational\":%.6g,\"relational_established\":%s,"
"\"cos_angle\":%.6g,\"agreement\":%d,\"binding_value\":\"%s\",\"best_relation\":\"%s\","
"\"refused_because\":\"%s\"}",
EL_CSTR(claim_id), fw ? fw : "-", fl, rfl,
a.may_assert ? "true" : "false", a.still_held ? "true" : "false",
a.found ? "true" : "false", a.n_edges,
a.factual, a.relational, a.relational_established ? "true" : "false",
a.cos_angle, a.agreement,
a.binding_value[0] ? a.binding_value : "-", a.best_edge,
a.may_assert ? "-" :
!a.found ? "no-relation" :
!a.relational_established ? "relational-axis-never-established" :
(a.factual < fl) ? "below-factual-floor" :
(a.relational < rfl) ? "below-relational-floor" : "-");
jb_puts(&b, t);
return el_wrap_str(b.buf);
}
@@ -14693,15 +15147,22 @@ el_val_t engram_correspondence_beat_json(el_val_t seeds, el_val_t faculty, el_va
JsonBuf b; jb_init(&b); char t[384];
snprintf(t, sizeof t,
"{\"faculty\":\"%s\",\"stance_id\":\"%s\",\"region_hub\":\"%s\",\"dim\":%d,\"n_axes\":%d,\"signal_axes\":%d,"
/* `keystone_write_blocked` is GONE from this response. It reported that the
* beat had refused to learn about the reference frame, and the measured
* cost of that refusal was 0.00% brier reduction over n_trials 0 on the
* keystone region the loop never ran, so nothing about the self was ever
* calibrated OR falsifiable. Nothing replaces the flag: non-circularity of
* the reference frame is temporal, not a permission (spec §5.2). The
* `keystone` field is retained as a label on the region, and it no longer
* gates anything. */
"\"resumed\":%s,\"keystone\":%s,\"probes\":%d,\"epochs\":%d,"
"\"brier_before\":%.6g,\"brier_after\":%.6g,\"reduction_pct\":%.2f,"
"\"reliability\":%.6g,\"n_trials\":%lld,\"stance_written\":%s,\"keystone_write_blocked\":%s}",
"\"reliability\":%.6g,\"n_trials\":%lld,\"stance_written\":%s}",
EL_CSTR(faculty), sid, g->hub_id ? g->hub_id : "region", dim, na, signal,
resumed ? "true" : "false", st.keystone ? "true" : "false", NP, EP,
brier_before, brier_after,
brier_before > 0 ? 100.0 * (brier_before - brier_after) / brier_before : 0.0,
st.reliability, (long long)st.n_trials, wrote == 0 ? "true" : "false",
st.keystone ? "true" : "false");
st.reliability, (long long)st.n_trials, wrote == 0 ? "true" : "false");
jb_puts(&b, t);
cog_stance_free(&st); engram_geo_free(g);
return el_wrap_str(b.buf);
+6 -1
View File
@@ -732,8 +732,13 @@ el_val_t engram_geo_analogy_json(el_val_t a_seeds, el_val_t b_seeds);
el_val_t engram_reason_analogy_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t c_seeds);
/* COGNITION (2026-08-14): THE ONE OPERATION + grounding, surfaced live. */
el_val_t engram_think_json(el_val_t seeds, el_val_t faculty);
/* GROUNDING (2026-08-16): grounding is an attribute of the RELATION and it IS the
* hebbian weight. ground reads; ground_record writes; trajectory reads the chain. */
el_val_t engram_ground_json(el_val_t claim, el_val_t evidence, el_val_t for_whom);
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor);
el_val_t engram_ground_record_json(el_val_t claim, el_val_t evidence,
el_val_t provenance, el_val_t floor);
el_val_t engram_ground_trajectory_json(el_val_t claim, el_val_t evidence);
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor, el_val_t rel_floor);
el_val_t engram_attend_json(el_val_t node_id, el_val_t observer, el_val_t salience);
el_val_t engram_correspondence_beat_json(el_val_t seeds, el_val_t faculty, el_val_t keystone);
el_val_t engram_consolidate_permanence(el_val_t node_id);
+372 -29
View File
@@ -246,14 +246,6 @@ static int put_edge(EngramPagedStore* s, const char* id, const char* from, const
e.metadata = (char*)meta;
return store_put_edge(s, &e);
}
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
const char* evidence_id, double grounding, const char* for_whom) {
if (!s || !claim_id || !evidence_id) return -1;
char id[512], meta[256];
snprintf(id, sizeof id, "gb-%s-%s-%s", claim_id, evidence_id, for_whom ? for_whom : "global");
snprintf(meta, sizeof meta, "for_whom=%s", for_whom ? for_whom : "-");
return put_edge(s, id, claim_id, evidence_id, COG_GROUNDED_BY_RELATION, grounding, meta);
}
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
const char* observer_id, double salience) {
if (!s || !node_id || !observer_id) return -1;
@@ -261,35 +253,386 @@ int cog_salient_edge(EngramPagedStore* s, const char* node_id,
snprintf(id, sizeof id, "st-%s-%s", node_id, observer_id);
return put_edge(s, id, node_id, observer_id, COG_SALIENT_TO_RELATION, salience, NULL);
}
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
const char* for_whom, double floor) {
if (!s || !claim_id) return -1;
if (!(floor > 0)) floor = 0.5;
StoreEdge* edges = NULL; size_t n = 0;
if (store_get_edges_from(s, claim_id, &edges, &n) < 0) return -1;
double best = 0.0; int found = 0;
for (size_t i = 0; i < n; i++) {
if (!edges[i].relation || strcmp(edges[i].relation, COG_GROUNDED_BY_RELATION) != 0) continue;
/* grounded-for-whom: match observer if requested; global (for_whom=-) always counts */
int match = 1;
if (for_whom && edges[i].metadata) {
const char* fw = strstr(edges[i].metadata, "for_whom=");
if (fw) { fw += 9; if (strcmp(fw, for_whom) != 0 && strcmp(fw, "-") != 0) match = 0; }
}
if (match) { found = 1; if (edges[i].weight > best) best = edges[i].weight; }
}
store_edges_free(edges, n);
if (!found) return 0; /* ungrounded => refuse assertion (still held) */
return (best >= floor) ? 1 : 0;
/* ═══════════════════════════════════════════════════════════════════════════
* §7 GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR.
* See engram_cognition.h §7 for the model and for the measurements the two
* design decisions (thirteen regions, min aggregate) rest on.
* */
/* ── The one decay model. Moved here verbatim from el_runtime.c's
* engram_temporal_decay so nodes and edges share a single implementation and a
* single set of constants; engram_temporal_decay now delegates. Bit-identical
* for nodes: reinforcements := activation_count, lambda_override :=
* temporal_decay_rate.
*
* This is what makes decay ANALYTIC rather than sampled: between two recorded
* versions the trajectory is not unknown, it is known in closed form from the
* last point and elapsed time. Store the point, read the curve. */
double cog_decay_factor(int64_t age_ms, double reinforcements, double lambda_override) {
if (age_ms <= 0) return 1.0;
double lambda = (lambda_override > 0.0) ? lambda_override : COG_DECAY_LAMBDA;
double age_hours = (double)age_ms / 3600000.0;
if (reinforcements < 0) reinforcements = 0;
double t_half = COG_T_HALF_HOURS * (1.0 + log(1.0 + reinforcements));
double factor = exp(-lambda * age_hours / t_half);
if (factor < COG_DECAY_FLOOR) factor = COG_DECAY_FLOOR;
return factor;
}
const char* cog_prov_name(CogProvClass p) {
switch (p) {
case COG_PROV_OBSERVED: return "observed";
case COG_PROV_INFERRED: return "inferred";
case COG_PROV_TOLD: return "told";
case COG_PROV_IMPRINTED: return "imprinted";
default: return "unset";
}
}
CogProvClass cog_prov_parse(const char* s) {
if (!s) return COG_PROV_UNSET;
if (!strcmp(s, "observed")) return COG_PROV_OBSERVED;
if (!strcmp(s, "inferred")) return COG_PROV_INFERRED;
if (!strcmp(s, "told")) return COG_PROV_TOLD;
if (!strcmp(s, "imprinted")) return COG_PROV_IMPRINTED;
return COG_PROV_UNSET;
}
/* Locate the GRD1 block in an edge's metadata. It is always the tail; anything
* ahead of it is the edge's pre-existing metadata, preserved verbatim. */
static const char* cog_grd_find(const char* meta) {
if (!meta) return NULL;
size_t ml = strlen(COG_GROUNDING_META_MAGIC);
if (strncmp(meta, COG_GROUNDING_META_MAGIC, ml) == 0) return meta;
const char* p = meta;
while ((p = strstr(p, COG_GROUNDING_META_MAGIC)) != NULL) {
if (p > meta && p[-1] == '\n') return p;
p += ml;
}
return NULL;
}
int cog_grounding_parse(const StoreEdge* e, int64_t now_ms, CogGrounding* out) {
if (!e || !out) return -1;
memset(out, 0, sizeof *out);
/* Two dimensions exist on every edge whether or not grounding has ever been
* established, because they ARE existing substrate rather than new fields:
* associative the accrued hebb, with its existing dynamics;
* polarity the signed authored weight. `inhibitory` is precisely this
* distinction crushed to one bit, so it is the seed sign. */
out->associative = e->hebb;
out->polarity = e->inhibitory ? -e->weight : e->weight;
out->prov = COG_PROV_UNSET;
out->ts = e->last_fired > 0 ? e->last_fired : e->updated_at;
const char* blk = cog_grd_find(e->metadata);
if (blk) {
out->present = 1;
char* copy = dupstr(blk);
if (!copy) return -1;
for (char* line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) {
if (line[0] == '\0') continue;
char tag = line[0];
const char* rest = line + 1; while (*rest == ' ') rest++;
if (tag == 'w') { /* the four numeric dimensions */
double v[4] = {0,0,0,0}; parse_floats(rest, v, 4);
out->factual = v[0]; out->relational = v[1];
out->associative = v[2]; out->polarity = v[3];
} else if (tag == 'k') { /* provenance class */
out->prov = cog_prov_parse(rest);
} else if (tag == 't') { /* timestamp + seq + reinforcements */
double v[3] = {0,0,0}; parse_floats(rest, v, 3);
out->ts = (int64_t)v[0]; out->seq = (int64_t)v[1]; out->reinforcements = v[2];
} else if (tag == 'd') {
double v[3] = {0,0,0}; parse_floats(rest, v, 3);
out->fac_proj = v[0]; out->rel_proj = v[1]; out->cos_angle = v[2];
} else if (tag == 'v') {
snprintf(out->binding_value, sizeof out->binding_value, "%s", rest);
} else if (tag == 'c') {
double v[2] = {0,0}; parse_floats(rest, v, 2);
out->floor_at_record = v[0]; out->rel_floor_at_record = v[1];
} else if (tag == 'p') {
snprintf(out->prev_edge, sizeof out->prev_edge, "%s", rest);
}
}
free(copy);
}
out->agreement = (out->cos_angle > 0) ? 1 : (out->cos_angle < 0 ? -1 : 0);
/* ── DERIVED. Nothing below this line is ever serialized. Recency, decay and
* staleness are read off the curve; storing them is how a number ends up
* asserting something nothing computed (§8.1 / spec §2). */
out->age_ms = (out->ts > 0 && now_ms > out->ts) ? (now_ms - out->ts) : 0;
out->decay = cog_decay_factor(out->age_ms, out->reinforcements, 0.0);
out->factual_now = out->factual * out->decay;
out->relational_now = out->relational * out->decay;
out->associative_now = out->associative * out->decay;
out->stale = (out->present && out->floor_at_record > 0 &&
out->factual_now < out->floor_at_record) ? 1 : 0;
return 0;
}
char* cog_grounding_metadata(const char* base_meta, const CogGrounding* g) {
if (!g) return NULL;
size_t keep = 0;
if (base_meta) {
const char* blk = cog_grd_find(base_meta);
keep = blk ? (size_t)(blk - base_meta) : strlen(base_meta);
while (keep > 0 && base_meta[keep - 1] == '\n') keep--;
}
size_t cap = keep + 1024;
char* buf = malloc(cap); if (!buf) return NULL;
size_t o = 0;
if (keep) { memcpy(buf, base_meta, keep); o = keep; buf[o++] = '\n'; }
o += (size_t)snprintf(buf + o, cap - o, "%s\n", COG_GROUNDING_META_MAGIC);
/* STORED ONLY. factual / relational / associative / polarity / provenance /
* timestamp plus the joint state a decision saw. No confidence, no
* recency, no staleness, no volatility: those are read off the curve. */
o += (size_t)snprintf(buf + o, cap - o, "w %.9g %.9g %.9g %.9g\n",
g->factual, g->relational, g->associative, g->polarity);
o += (size_t)snprintf(buf + o, cap - o, "k %s\n", cog_prov_name(g->prov));
o += (size_t)snprintf(buf + o, cap - o, "t %lld %lld %.9g\n",
(long long)g->ts, (long long)g->seq, g->reinforcements);
o += (size_t)snprintf(buf + o, cap - o, "d %.9g %.9g %.9g\n",
g->fac_proj, g->rel_proj, g->cos_angle);
o += (size_t)snprintf(buf + o, cap - o, "v %s\n", g->binding_value[0] ? g->binding_value : "-");
o += (size_t)snprintf(buf + o, cap - o, "c %.9g %.9g\n", g->floor_at_record, g->rel_floor_at_record);
if (g->prev_edge[0]) o += (size_t)snprintf(buf + o, cap - o, "p %s\n", g->prev_edge);
(void)o;
return buf;
}
/* ── Consequence, not epsilon. Every test is a floor crossing or a sign change,
* both exact. Ordered so the two INHERENT (discrete) moves are reported in
* preference to the graded ones, because they bypass the salience gate. */
CogSignificance cog_grounding_significant(const CogGrounding* prev,
const CogGrounding* now,
double floor, double rel_floor) {
if (!now) return COG_SIG_NONE;
if (!prev || !prev->present) return COG_SIG_FIRST_RECORD;
/* INHERENT 1 — polarity sign flip. Ignorance and disagreement are different
* states, and support contradiction is a change of state rather than a
* drift, so no threshold applies. Comparing signs, with zero its own class. */
{
int sp = prev->polarity > 0 ? 1 : (prev->polarity < 0 ? -1 : 0);
int sn = now->polarity > 0 ? 1 : (now->polarity < 0 ? -1 : 0);
if (sp != sn) return COG_SIG_POLARITY_FLIP;
}
/* INHERENT 2 — provenance class change. told → observed is a categorical
* upgrade in what the relation is entitled to, not a movement along an axis. */
if (prev->prov != now->prov) return COG_SIG_PROVENANCE_CHANGE;
/* Crossing an assert floor — the move changes whether this relation can be
* spoken. Compared on the DECAYED values, because that is what the gate reads. */
if ((prev->factual_now >= floor) != (now->factual_now >= floor)) return COG_SIG_FACTUAL_FLOOR;
if ((prev->relational_now >= rel_floor) != (now->relational_now >= rel_floor)) return COG_SIG_RELATIONAL_FLOOR;
/* Flipping factual/relational agreement — the relation stops being "true and
* meaningful" and becomes "true and misapplied", or the reverse. This is the
* 911/CPS contradiction as a measured event rather than a reviewable one. */
if (prev->agreement != now->agreement) return COG_SIG_AGREEMENT_FLIP;
/* A gradient reversing — the evidence stopped pulling the claim toward it and
* began pushing it away, or the same on the values axis. */
if ((prev->fac_proj > 0) != (now->fac_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;
if ((prev->rel_proj > 0) != (now->rel_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;
return COG_SIG_NONE;
}
int cog_significance_inherent(CogSignificance s) {
return (s == COG_SIG_FIRST_RECORD || s == COG_SIG_POLARITY_FLIP ||
s == COG_SIG_PROVENANCE_CHANGE) ? 1 : 0;
}
const char* cog_significance_name(CogSignificance s) {
switch (s) {
case COG_SIG_FIRST_RECORD: return "first-record";
case COG_SIG_POLARITY_FLIP: return "polarity-sign-flip";
case COG_SIG_PROVENANCE_CHANGE: return "provenance-class-change";
case COG_SIG_FACTUAL_FLOOR: return "factual-floor-crossed";
case COG_SIG_RELATIONAL_FLOOR: return "relational-floor-crossed";
case COG_SIG_AGREEMENT_FLIP: return "agreement-sign-flip";
case COG_SIG_DIRECTION_REVERSAL: return "gradient-direction-reversal";
default: return "none";
}
}
/* ── Recording: a NEW edge record. The predecessor is never touched. ────────── */
int cog_grounding_record(EngramPagedStore* s, const StoreEdge* base,
const CogGrounding* g, char* out_id, size_t out_id_cap) {
if (!s || !base || !base->id || !g) return -1;
char root[192];
snprintf(root, sizeof root, "%s", base->id);
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
int seq = (int)g->seq + 1;
char vid[224];
snprintf(vid, sizeof vid, "%s#%d", root, seq);
CogGrounding rec = *g;
rec.seq = seq;
snprintf(rec.prev_edge, sizeof rec.prev_edge, "%s", base->id);
char* meta = cog_grounding_metadata(base->metadata, &rec);
if (!meta) return -1;
StoreEdge e; memset(&e, 0, sizeof e);
e.id = vid; e.from_id = base->from_id; e.to_id = base->to_id;
e.relation = base->relation; e.metadata = meta;
/* The vector IS the weight, so the scalar fields carry their dimensions:
* `weight` the magnitude of polarity, `inhibitory` its sign, `hebb` the
* associative strength. Nothing here is a second copy of a derived value. */
e.weight = rec.polarity < 0 ? -rec.polarity : rec.polarity;
e.inhibitory = rec.polarity < 0 ? 1 : 0;
e.hebb = rec.associative;
e.confidence = base->confidence;
e.created_at = base->created_at;
e.updated_at = rec.ts;
e.last_fired = rec.ts;
e.layer_id = base->layer_id;
int rc = store_put_edge(s, &e);
free(meta);
if (rc != 0) return -1;
if (out_id && out_id_cap) snprintf(out_id, out_id_cap, "%s", vid);
return seq;
}
int cog_grounding_head(EngramPagedStore* s, const char* base_id,
StoreEdge* out, int max_versions) {
if (!s || !base_id || !out) return -1;
if (max_versions <= 0) max_versions = 64;
char root[192]; snprintf(root, sizeof root, "%s", base_id);
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
StoreEdge cur; memset(&cur, 0, sizeof cur);
if (store_get_edge(s, root, &cur) != 1) return -1;
int found = 0;
for (int v = 1; v <= max_versions; v++) {
char vid[224]; snprintf(vid, sizeof vid, "%s#%d", root, v);
StoreEdge nx;
if (store_get_edge(s, vid, &nx) != 1) break;
store_edge_free(&cur); cur = nx; found = v;
}
*out = cur;
return found;
}
/* ── VOLATILITY AND DRIFT: derived from the chain, stored nowhere. The series
* exists only because nothing was destroyed, which is the whole return on
* immutability a derivative for free. */
int cog_grounding_trajectory(EngramPagedStore* s, const char* base_id,
int64_t now_ms, CogTrajectory* out) {
if (!s || !base_id || !out) return -1;
memset(out, 0, sizeof *out);
char root[192]; snprintf(root, sizeof root, "%s", base_id);
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
double pf = 0, pr = 0, f0 = 0, r0 = 0, fN = 0, rN = 0;
double sum_df = 0, sum_dr = 0;
int n = 0;
for (int v = 0; v <= 64; v++) {
char vid[224];
if (v == 0) snprintf(vid, sizeof vid, "%s", root);
else snprintf(vid, sizeof vid, "%s#%d", root, v);
StoreEdge e;
if (store_get_edge(s, vid, &e) != 1) { if (v) break; else continue; }
CogGrounding g;
if (cog_grounding_parse(&e, now_ms, &g) == 0) {
if (n == 0) { f0 = g.factual; r0 = g.relational; }
else { sum_df += fabs(g.factual - pf); sum_dr += fabs(g.relational - pr); }
pf = g.factual; pr = g.relational; fN = pf; rN = pr;
n++;
}
store_edge_free(&e);
}
out->n_versions = n;
if (n > 1) {
out->factual_volatility = sum_df / (double)(n - 1);
out->relational_volatility = sum_dr / (double)(n - 1);
}
out->factual_drift = fN - f0;
out->relational_drift = rN - r0;
/* "STAYED TRUE, BECAME WRONG" — the event the joint record makes visible and
* that per-dimension versioning would have destroyed: the fact held while
* the meaning degraded. Expressed as signs, so there is no tolerance here
* either: factual did not fall, relational did. */
out->stayed_true_became_wrong =
(n > 1 && out->factual_drift >= 0 && out->relational_drift < 0) ? 1 : 0;
return 0;
}
/* ── Assertion gates on BOTH floors. Traversal is untouched: activation still
* conducts on the factual/associative side, so a relation can remain thinkable
* while ceasing to be assertable. That gap is where the wide angles live. */
int cog_assert_two_axis(EngramPagedStore* s, const char* claim_id,
double floor, double rel_floor, int64_t now_ms,
CogAssertion* out) {
if (!s || !claim_id || !out) return -1;
memset(out, 0, sizeof *out);
if (!(floor > 0)) floor = 0.5;
if (!(rel_floor > 0)) rel_floor = floor;
/* still_held is DERIVED, not a literal (§8.1). Holding is unconditional —
* the store gates nothing so the question the field actually answers is
* whether the content is present and live. */
StoreNode n;
if (store_get_node(s, claim_id, &n) == 1) { out->still_held = !n.tombstoned; store_node_free(&n); }
else out->still_held = 0;
double best = -1.0;
for (int dir = 0; dir < 2; dir++) {
StoreEdge* edges = NULL; size_t ne = 0;
int rc = dir == 0 ? store_get_edges_from(s, claim_id, &edges, &ne)
: store_get_edges_to (s, claim_id, &edges, &ne);
if (rc < 0) continue;
for (size_t i = 0; i < ne; i++) {
if (edges[i].tombstoned) continue;
CogGrounding g;
if (cog_grounding_parse(&edges[i], now_ms, &g) != 0) continue;
out->n_edges++;
out->found = 1;
if (g.factual_now > best) {
best = g.factual_now;
out->factual = g.factual_now;
out->relational = g.relational_now; /* the SAME edge, not a max */
out->polarity = g.polarity;
out->cos_angle = g.cos_angle;
out->agreement = g.agreement;
out->prov = g.prov;
out->relational_established = g.present;
snprintf(out->best_edge, sizeof out->best_edge, "%s", edges[i].id ? edges[i].id : "");
snprintf(out->binding_value, sizeof out->binding_value, "%s", g.binding_value);
}
}
store_edges_free(edges, ne);
}
/* BOTH floors, and an unestablished relational axis does NOT pass by default
* defaulting it to passing is the exemption §0 forbids. A negative polarity
* is a relation that actively contradicts and can never license assertion. */
out->may_assert = (out->found && out->relational_established &&
out->polarity > 0 &&
out->factual >= floor && out->relational >= rel_floor) ? 1 : 0;
return 0;
}
/* ═══════════════════════════════════════════════ THE CORRESPONDENCE-LOOP ═════ */
int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
double outcome_y, CogStance* stance,
int learn, double max_step, CogBeatResult* out) {
if (!region || !stance || !out) return -1;
memset(out, 0, sizeof *out);
if (stance->keystone) { learn = 0; out->wrote_keystone = 1; } /* §6: never write a keystone */
/* 2026-08-16: the keystone block is GONE. It refused to learn about the
* reference frame, which does not make it a good reference it makes it
* unexaminable, trading circular calibration for an ungroundable one (spec
* §2). Measured cost of the block: on the keystone region the beat reported
* 0.00% brier reduction over n_trials 0 it never ran, so nothing about the
* self was ever calibrated OR falsifiable. What replaces it is a provenance
* constraint, not a permission: cog_grounding_downstream refuses evidence
* that is downstream of the region being calibrated, for every region alike.
* `wrote_keystone` is retained as a reporting field only and is always 0. */
GeoGradient g;
if (engram_think(region, anchor, stance, &g) != 0) return -1; /* PREDICTION */
+269 -17
View File
@@ -23,7 +23,7 @@
* and a region, and grounded-for-whom.
*
* PURE + (mostly) READ-ONLY, stdlib + libm only. think() and the warp are pure
* over their inputs. Persistence (Stance <-> StoreNode, grounded-by edges) is the
* over their inputs. Persistence (Stance <-> StoreNode, edge grounding vectors) is the
* only part that touches the store, and it is additive / supersede / tombstone
* never mutate-in-place, never delete. It NEVER touches the live daemon: all
* offline against a scratch store, per the design's rails.
@@ -152,30 +152,25 @@ int engram_express(const GeoGradient* g, const float* anchor, float* out_point);
/* ═══════════════════════════════════════════════════════════════════════════
* §5 HOLD vs GROUND vs ASSERT. Holding is unconditional (the store gates nothing).
* Grounding is a RELATION a "grounded-by" edge, probabilistic, grounded-for-whom.
* The honesty floor is checked only at ASSERTION.
* Grounding is an ATTRIBUTE OF a relation carried on the edge itself, as a
* vector (§7). The honesty floor is checked only at ASSERTION, on both axes.
* */
#define COG_GROUNDED_BY_RELATION "grounded-by"
/* DELETED 2026-08-16: COG_GROUNDED_BY_RELATION and cog_ground_edge.
*
* A "grounded-by" edge models grounding as a relation BETWEEN two nodes. It is a
* property OF a relation and it is that relation's weight. Minting a new edge
* to carry a score was the error; #147 corrected which endpoints the edge landed
* on and left the wrong idea standing. There is nothing to ground a claim
* "against" that is not already an edge, and if no edge exists the honest answer
* is that the two are not related not a freshly minted one scoring 0.98.
* See §7 for what replaced it. */
#define COG_SALIENT_TO_RELATION "salient-to"
/* Write a grounded-by edge (additive). weight = grounding ∈(0,1] from the verifier;
* for_whom recorded in edge metadata (grounding is relational). Never a node flag. */
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
const char* evidence_id, double grounding, const char* for_whom);
/* Write/refresh a salient-to edge: salience is RELATIONAL (grounded-for-whom),
* carried on the edge to the observer not baked into the node scalar (§2.1). */
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
const char* observer_id, double salience);
/* The honesty floor — a QUERY at assertion time, NOT a schema constraint. Reads the
* claim's stored grounded-by edges (for the given observer) and returns:
* 1 = may assert (best grounding >= floor),
* 0 = REFUSE assertion (holds unconditionally; only asserting is gated),
* <0 = error. The content remains held either way. */
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
const char* for_whom, double floor);
/* ═══════════════════════════════════════════════════════════════════════════
* §4 THE REFLEXIVE CORRESPONDENCE-LOOP the learning engine. think scores its
* OWN gradient against outcome, refines the stance on the error, and (optionally)
@@ -209,8 +204,265 @@ int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
/* ═══════════════════════════════════════════════════════════════════════════
* §6 METASTABILITY. Keystones (self/values) are read-mostly: the loop reads but
* never writes them. Mark by stance flag or by a keystone-id set the loop consults.
*
* SUPERSEDED BY §7's PROVENANCE CONSTRAINT (2026-08-16). The keystone flag is a
* PERMISSION: it asks who the target is, not where the evidence came from. That
* is censorship, and it costs the ability to ever ground the self (spec
* correspondence-and-censorship.md §0/§2). The constraint that actually protects
* a reference frame is cog_grounding_downstream: a region may not be calibrated
* by evidence downstream of itself. These declarations remain only so existing
* call sites keep compiling; nothing in the grounding path consults them.
* */
typedef struct { const char** ids; int n; } CogKeystoneSet;
int cog_is_keystone(const CogKeystoneSet* ks, const CogStance* s);
/* ═══════════════════════════════════════════════════════════════════════════
* §7 GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR
* (2026-08-16; spec correspondence-and-censorship.md §2§6 @ 2b7e4ba.)
*
* THE MODEL. Grounding is not a subsystem, a score, or a relation BETWEEN nodes.
* It is an attribute OF a relation. The graph already IS the grounding structure:
* every edge is a grounded relation, and what that relation is worth is carried
* on the edge itself. Three things follow, and each DELETES rather than adds:
*
* 1. `grounded-by` as a relation type does not exist, and cog_ground_edge is
* gone. Minting an edge to hold a score models grounding as a relation
* between nodes when it is a property of a relation. #147 corrected which
* endpoints that edge landed on and left the wrong idea standing.
* 2. There is no observer, and no sampling rate. Change is not a consequence of
* use it IS use, the way potentiation is the firing rather than something
* that reads the firing and writes a weight. So no supervisor compares a
* value to a threshold and decides to persist.
* 3. Between two recorded versions the trajectory is not unknown. Decay is a
* pure function of the last recorded point and elapsed time, so it is
* ANALYTIC: store the point, read the curve.
*
* WHAT IS *NOT* HERE, DELIBERATELY. An earlier draft of the spec posed "a graph
* predicate for evidence downstream of itself" as the hard problem, and this file
* briefly contained one. It is withdrawn. Non-circularity is TEMPORAL, not
* topological: you cannot recalibrate the ruler while measuring with it, so you
* do it when you are not using the frame to act. Reachability could never have
* worked measured on the live store, reachability from the self region over
* all relations reaches 89.2% of the graph (10,580 of 11,861 nodes) and 16.0%
* over hebbian/semantic relations alone, so the predicate marks essentially all
* evidence tainted and the constraint degenerates into the total block that
* censorship started as. Nothing replaces it here; the independence is a fact
* about engagement, owned by the dreamer, not a fact about the graph.
*
*
* §7.1 THE VECTOR
*
* The test for a real dimension is whether it can move independently of the
* others. Five can, and each maps onto substrate that already exists:
*
* factual correspondence with evidence. [GRD1]
* relational correspondence with values min over THIRTEEN
* value regions, carrying the binding value's NAME. [GRD1]
* associative co-activation frequency. This is the edge's `hebb`
* field with its existing dynamics NOT a new one.
* Independent by construction: every superstition is
* a strong association with no factual grounding.
* polarity SIGNED. Near zero means "no support"; NEGATIVE means
* "this actively contradicts". The edge's `inhibitory`
* bit is exactly this distinction crushed to one bit,
* and is carried forward as the seed value. [GRD1]
* provenance observed / inferred / told / imprinted. Categorical,
* and load-bearing: it governs what the relation is
* entitled to. [GRD1]
*
* Plus a TIMESTAMP, which is what turns the supersession chain into a time
* series of vectors rather than a series of numbers.
*
* DERIVED, THEREFORE NEVER STORED. Confidence (high grounding AND low
* volatility), recency (decay read off the curve), staleness (grounding fallen
* below its floor), volatility (the derivative of a series nothing destroyed).
* Storing confidence separately is how `confidence: 0.5` ends up sitting beside
* a zero direction vector, asserting something nothing computed. Every field in
* CogGrounding below is marked STORED or DERIVED, and the serializer writes
* only the STORED ones.
*
* THE VALUES REFERENCE IS THIRTEEN REGIONS AND THE AGGREGATE IS MIN.
* Measured on the live store: the values root kn-5b606390 `contains` exactly 13
* value nodes; pairwise centroid cosine among their regions is min 0.1525,
* mean 0.5199, median 0.5282, max 0.9278 they demonstrably do not form one
* region. Against a single union region the individual values sit at cosine
* 0.38..0.89, with constraints-as-freedom at 0.3812 and change-is-the-signal at
* 0.4677, so a union centroid under-represents precisely the values a claim is
* most likely to be measured against. MIN rather than MEAN because a mean lets
* strong agreement with twelve values mask a violation of the thirteenth, which
* is the mechanism of rationalization; min yields a binding constraint with a
* NAME attached rather than a score.
*
* TRAVERSAL CONDUCTS ON FACTUAL; ASSERTION REQUIRES BOTH. If activation
* conducted on relational weight, Neuron could not follow a chain of reasoning
* to a conclusion he then rejects censorship arriving through the spreading
* rule. The gap between reachable and assertable is where the wide
* factual/relational angles live, and that gap is the interesting part.
* */
/* ── The one decay model (moved here from el_runtime.c so that node decay and
* edge-grounding decay are a single implementation with a single set of
* constants, rather than a model and a parallel copy of it). Half-life scales
* with how established the thing is: T_eff = T_HALF · (1 + ln(1 + reinforcements)).
* The floor is a preference, not a cliff max penalty for age alone is 4x.
* `lambda_override` > 0 replaces the default rate; 0 means use the default. */
#define COG_T_HALF_HOURS 168.0
#define COG_DECAY_LAMBDA 0.693147
#define COG_DECAY_FLOOR 0.25
double cog_decay_factor(int64_t age_ms, double reinforcements, double lambda_override);
/* The compact vector block carried in the edge's own metadata. Line schema, same
* precedent as STNC1 / GEO1. Metadata the edge already carried is preserved
* verbatim ahead of the magic line. */
#define COG_GROUNDING_META_MAGIC "GRD1"
/* Provenance class — categorical, and it governs what the relation is entitled
* to. A change of class is inherently significant and needs no threshold,
* because told observed is a categorical upgrade, not a drift. */
typedef enum {
COG_PROV_UNSET = 0,
COG_PROV_OBSERVED = 1,
COG_PROV_INFERRED = 2,
COG_PROV_TOLD = 3,
COG_PROV_IMPRINTED = 4
} CogProvClass;
const char* cog_prov_name(CogProvClass p);
CogProvClass cog_prov_parse(const char* s);
typedef struct {
int present; /* 1 iff the edge carries a GRD1 block */
/* ── STORED: the vector, as it stood at `ts` ─────────────────────────────── */
double factual; /* correspondence with evidence */
double relational; /* min over the thirteen value regions */
double associative; /* co-activation frequency — mirrors edge->hebb */
double polarity; /* SIGNED support; <0 = actively contradicts */
CogProvClass prov; /* observed / inferred / told / imprinted */
int64_t ts; /* when this version was recorded (ms) */
int64_t seq; /* supersession sequence number */
double reinforcements; /* uses folded into this version */
char binding_value[128]; /* the argmin value — the conflict's NAME */
/* the two gradients as frame-independent signed projections, plus the angle
* between them in full R^dim. These are part of the JOINT STATE a decision
* saw, not a convenience: near +1 evidence and values push the same way; at
* or below 0 the relation is factually supported and relationally wrong. */
double fac_proj, rel_proj, cos_angle;
int agreement; /* sign(cos_angle): +1 / 0 / 1 */
double floor_at_record, rel_floor_at_record;
char prev_edge[192]; /* the version this superseded ("" if first) */
/* ── DERIVED at read time. NEVER serialized. ─────────────────────────────── */
int64_t age_ms; /* recency: now ts */
double decay; /* cog_decay_factor over that age */
double factual_now; /* factual · decay */
double relational_now;
double associative_now;
int stale; /* grounding fallen below its floor */
} CogGrounding;
/* Read an edge's vector as of `now_ms`. Pure — never writes. An edge with no
* GRD1 block still has an associative strength (its accrued hebb) and a polarity
* (its signed authored weight); `present` says whether the grounding dimensions
* have ever been established, and an unestablished dimension is reported as such
* rather than defaulted to a passing value. */
int cog_grounding_parse(const StoreEdge* e, int64_t now_ms, CogGrounding* out);
/* Serialize the STORED half of the vector, preserving pre-existing non-GRD1
* metadata. Returns an owned string. Derived fields are not written. */
char* cog_grounding_metadata(const char* base_meta, const CogGrounding* g);
/* ── §7.2 CONSOLIDATION-GATED SUPERSESSION ──────────────────────────────────
*
* Supersession is not recording it is CONSOLIDATION, gated by salience, which
* is why you remember the argument and not the commute. Significance is
* evaluated PER-DIMENSION but the record is the WHOLE VECTOR: any dimension
* moving enough to matter triggers a supersession, and the new version captures
* every dimension as it stood at that instant. Versioning axes independently
* would make the joint state unreconstructable, and the joint state is the point
* it is what makes "stayed true, became wrong" visible as an event (factual
* holding steady across versions while relational degrades).
*
* There is deliberately no epsilon in this enum or in the function that computes
* it. Every test is a floor crossing or a sign change, both exact. Two of them
* are INHERENTLY significant because they are discrete state changes rather than
* drift, and those bypass the salience gate entirely. */
typedef enum {
COG_SIG_NONE = 0, /* nothing decision-relevant moved — DO NOT RECORD */
COG_SIG_FIRST_RECORD = 1, /* no prior version exists */
COG_SIG_POLARITY_FLIP = 2, /* INHERENT: support ↔ contradiction, or ignorance
* either. A discrete change of state. */
COG_SIG_PROVENANCE_CHANGE = 3, /* INHERENT: told → observed is a categorical
* upgrade in what the relation is entitled to. */
COG_SIG_FACTUAL_FLOOR = 4, /* crossed the assert floor, factual axis */
COG_SIG_RELATIONAL_FLOOR = 5, /* crossed the assert floor, relational axis */
COG_SIG_AGREEMENT_FLIP = 6, /* factual/relational agreement changed sign */
COG_SIG_DIRECTION_REVERSAL = 7 /* a gradient reversed direction */
} CogSignificance;
CogSignificance cog_grounding_significant(const CogGrounding* prev,
const CogGrounding* now,
double floor, double rel_floor);
const char* cog_significance_name(CogSignificance s);
/* 1 iff this reason is a discrete state change that consolidates regardless of
* salience (polarity flip, provenance change, first record). */
int cog_significance_inherent(CogSignificance s);
/* ── §7.3 RECORDING: supersession of the EDGE, never an overwrite ────────────
* Writes version seq+1 as a NEW edge record with the same endpoints and relation
* and id "<root>#<seq+1>", carrying a GRD1 `p` pointer to its predecessor. The
* predecessor is never touched. The chain IS the trajectory: not only what the
* grounding is but which way it has been moving and how fast a derivative
* obtained for free from immutability, because the points were never destroyed.
* Returns the version written (>=1), or <0 on error. */
int cog_grounding_record(EngramPagedStore* s, const StoreEdge* base,
const CogGrounding* g, char* out_id, size_t out_id_cap);
/* Walk forward from a base edge id to its newest recorded version. Point reads
* only; consolidation is gated, so the chain is short. Returns the highest
* version found (0 = the base record is the only one). */
int cog_grounding_head(EngramPagedStore* s, const char* base_id,
StoreEdge* out, int max_versions);
/* VOLATILITY — derived, never stored: the mean absolute per-version change of a
* dimension across the recorded chain. Feeds the equally-derived `confidence`
* (high grounding AND low volatility), which is likewise never stored. */
typedef struct {
int n_versions;
double factual_volatility;
double relational_volatility;
double factual_drift; /* signed: newest oldest */
double relational_drift;
int stayed_true_became_wrong; /* factual steady while relational degraded */
} CogTrajectory;
int cog_grounding_trajectory(EngramPagedStore* s, const char* base_id,
int64_t now_ms, CogTrajectory* out);
/* ── §7.4 ASSERTION GATES ON BOTH FLOORS ────────────────────────────────────
* A well-evidenced claim must not earn the right to be asserted regardless of
* whether it means the right thing. `may_assert` requires the decayed factual
* grounding to clear `floor` AND the decayed relational grounding to clear
* `rel_floor`. A relation whose relational axis has never been established does
* not pass by default it is reported unestablished and refused, because
* defaulting it to passing is exactly the exemption §0 forbids. Traversal is
* untouched: activation still conducts on the factual/associative side, so a
* relation can remain thinkable while ceasing to be assertable. */
typedef struct {
int may_assert;
int found; /* any relation at all on this claim */
int relational_established;
int still_held; /* DERIVED: node present and not tombstoned */
double factual; /* best decayed factual grounding */
double relational; /* the SAME edge's relational axis, not a max */
double polarity;
double cos_angle;
int agreement;
CogProvClass prov;
char best_edge[192];
char binding_value[128];
int n_edges;
} CogAssertion;
int cog_assert_two_axis(EngramPagedStore* s, const char* claim_id,
double floor, double rel_floor, int64_t now_ms,
CogAssertion* out);
#endif /* ENGRAM_COGNITION_H */
+82 -3
View File
@@ -31,6 +31,7 @@ This section is the **single source of truth** for what works and what is planne
- 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).
- **Geometry as a first-class value, and realizers declarable in El** — the `Geometry` type, the wire adapters, and `transduce` (Section 20). Landed 2026-08-16 (#141, #144).
- C runtime: I/O, string operations, integer math, lists, maps, filesystem, command-line args, basic `json_get` substring lookup.
### Planned (in flight)
@@ -41,9 +42,9 @@ This section is the **single source of truth** for what works and what is planne
- **`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()`.
- **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.
- **Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs. Adding libcurl-backed client and a thread-pool server.
- ~~**Real `engram_*` runtime.** Currently stub.~~ **Stale (verified 2026-08-16) — this is implemented, not planned.** `lang/runtime/el_runtime.c` carries the in-process graph store with spreading activation, Hebbian strengthening, disk persistence (paged store, magic `ENGST01`), an HNSW vector index behind a `eg_vindex_view`/`eg_vindex_maintain` publication boundary, and the full cognition surface (`engram_think_json`, `engram_ground_json`, `engram_assert_json`, `engram_attend_json`, `engram_correspondence_beat_json`). The "stub" description may still hold for the **lagging forks** (`lang/el-compiler/runtime/`, `products/web/runtime/`) — see `AGENTS.md`, which names those as downstream copies that cannot build the engram product. **Which runtime this line refers to needs a decision; it is not a fact that can be recovered from the text.**
- ~~**Real `dharma_*` runtime.** Currently stub.~~ **Needs re-verification (2026-08-16).** Not checked in this pass; do not rely on either reading.
- ~~**Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs.~~ **Stale.** libcurl-backed HTTP and a thread-pool server are live — `http_serve_async` is what `neuron/soul.el:729` runs before entering its awareness loop, and `realizer_register` resolves El functions through the same `dlsym` mechanism `http_set_handler` relies on.
- **JSON, time, UUID, state, env, additional string/list/math builtins.** See Section 12 for the canonical list.
### Not in this language
@@ -1250,6 +1251,84 @@ Implementing either now would mean editing files under concurrent modification a
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.
*(Status note, 2026-08-16: the geometry/`transduce` collision named above has since landed — see Section 20. The VIndex read-path collision has also landed; see `lang/spec/runtime-ownership.md` §5. 19.1 and 19.2 remain unimplemented, but the stated reason no longer holds for those two files.)*
---
## 20. Geometry — signal as a first-class value [implemented]
Landed 2026-08-16 (#141, #144). Declared here because the spec is the single source of truth for implemented-vs-planned, and this is a language surface, not a runtime detail.
### 20.1 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 therefore the **mandatory entry medium**: any non-text modality — a tone, a pulse, an image, a voice sample — had to be *described in prose first*, and the geometry subsequently reasoned over was the geometry **of the description, not of the signal**.
Two changes remove that, and neither is engram-specific — which is why they are in the language and not in the graph. Any program touching any modality needs them; the engram is merely one El program that happens to hold a graph.
1. **Geometry is a value that carries its own width.**
2. **A realizer is an ordinary El function** — so admitting a new modality never requires a runtime patch.
### 20.2 The `Geometry` type
`Geometry` is an opaque boxed pointer, exactly like `Instant` / `Calendar` / `Rhythm`. **No codegen change was required** to add it — the annotation is just a type name.
```el
let g: Geometry = geometry_new(4)
```
| builtin | returns | notes |
|---|---|---|
| `geometry_new(dim)` | `Geometry` | zero-filled; `0` on failure |
| `geometry_dim(g)` | `Int` | width; `0` if not a Geometry |
| `geometry_is(g)` | `Int` | `1` if a live Geometry |
| `geometry_get(g, i)` | `Float` | component |
| `geometry_set(g, i, x)` | `Int` | `1` ok, `0` out of range |
| `geometry_norm(g)` | `Float` | L2 — lets a caller check a realizer emitted **signal, not zeros** |
| `geometry_free(g)` | `Int` | `1` if freed. Returns a value rather than `void` so it is safe in any expression position without a codegen void-builtin table entry |
**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.
### 20.3 Wire adapters — the only place an encoding appears
```el
geometry_from_f32le_hex(hex) -> Geometry // 0 on empty / odd-length / non-hex
geometry_to_f32le_hex(g) -> String // "" if not a Geometry
```
`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 to validate a claimed length against. Encodings appear here and nowhere else: at the edge.
### 20.4 Realizers and `transduce`
A **realizer** maps one modality into geometry. Registration is **by name**: 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.
```el
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))
g
}
realizer_register("tone", "tone_realizer") // 1 ok / 0 unresolved
let g: Geometry = transduce(sample, "tone") // Geometry, or 0 if no organ
realizer_has("tone") // 1 if registered
```
The registry keys on **modality**, not on registration order. `transduce` returns `0` when no organ is registered for the modality — an absent organ is a reportable state, not a silent zero vector.
**The claim this makes:** a realizer is not in the runtime and not known to the compiler. Adding a modality is writing an El function and registering a name. `lang/examples/transduce.el` is the worked example and doubles as an executable proof — it exits non-zero if any check fails.
### 20.5 Two comparison hazards this surface exposed
Both were **measured**, not stylistic, and both are properties of the current `elc` that any El author should know:
- **`==` lowers 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 reinterpreted as pointers. `<` and `>` lower directly with no inference, so truthiness against a builtin's return is written `> 0` / `< 1`.
- **`+` dispatches on whether both operands are known-Int, and a user-defined `fn` call is not.** `let fails: Int = fails + check(...)` lowered to **string concatenation** and printed `4343632752` — a pointer. Nothing was wrong with the checks; the tally was lying. Failing fast needs no arithmetic at all, so there is nothing left to get wrong.
### 20.6 What this does not do
`transduce` produces geometry; it does not decide what the geometry *means*. Nothing here grounds anything. Grounding is the edge weight in the graph the geometry is later attached to — see `lang/spec/correspondence-and-censorship.md`.
---
End of specification.
+16 -7
View File
@@ -28,12 +28,12 @@ Each of these is a distinct merged or proposed fix. Each addresses one deposit.
| VIndex freed under a concurrent reader | `el_runtime.c:9424` | `fb32d15` guard (merged 08:46:43) |
| `_eg_vindex_seen` realloc'd on a read path | `el_runtime.c:9412` | same guard |
| `vindex_insert` on a read path | `el_runtime.c:9434`, `9450` | same guard |
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:7981`, `169186`, `195` | proposed: move to per-search frame |
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:7981`, `169186`, `195` | ~~proposed:~~ **built** moved to the call frame (§3.1(1), §5); TSan `readers` half clean (§7a) |
| nine append sites, none indexing → lazily-embedded nodes invisible | `el_runtime.c:7806, 7988, 8148, 8224, 11526, 11731, 12050, 15295, 15312` | "embed-gap #20", patched by making the *read* path catch up (`9439` comment) |
**Measured:** all file/line references above, read 2026-08-16. Crash frames `engram_activate → eg_vindex_sync → vindex_insert → _realloc → _xzm_xzone_malloc_freelist_outlined` are accounted for by rows 24.
**Inferred, not yet verified:** that the nine append sites do not share a single commit point. This needs one pass before Change C is sized.
~~**Inferred, not yet verified:** that the nine append sites do not share a single commit point. This needs one pass before Change C is sized.~~ **Moot — see §7.** The question was mis-aimed: node append is not the event that owns index membership, because a node without an embedding cannot be in a vector index. The five *embedding-assignment* sites are the real owner points.
---
@@ -135,12 +135,21 @@ The payoff of owning the language is unchanged and is now *cheaper*: introduced
## 6. Sequencing
> **⚠ Steps 25 belong to the abandoned capability-ABI §3 and are superseded
> (2026-08-16).** §3 was re-derived: the engram is immutable and recall is
> projection, so *what does not mutate needs no ownership discipline* and the
> question is dissolved rather than answered. There is no context type, no
> capability type, and no codegen change — **`const` is the capability**, and the
> constraint travels with the type of the thing rather than the shape of every call
> site, so **no sweep is needed at all** (§4). Steps 1, 6 and 7 stand. Struck rather
> than deleted, because the abandoned plan is why §4's cost argument is short.
1. **Read** how builtins are declared and dispatched, to confirm the call sites are compiler-generated in one place. *(This determines whether §4 holds. If dispatch is scattered, re-size before proceeding.)*
2. Introduce the context type and capability types.
3. Codegen emits the context at every builtin call site.
4. Mechanical sweep of builtin signatures.
5. Move index maintenance behind the write capability; the three read callers take the read capability.
6. Delete the residue-fixes listed in §5.
2. ~~Introduce the context type and capability types.~~ **Superseded**`const`.
3. ~~Codegen emits the context at every builtin call site.~~ **Superseded** — no codegen change.
4. ~~Mechanical sweep of builtin signatures.~~ **Superseded** — the constraint travels with the type.
5. ~~Move index maintenance behind the write capability; the three read callers take the read capability.~~ **Done, differently:** `eg_vindex_maintain` (exclusive, sole mutator) / `eg_vindex_view` (`const VIndex*`, shared readers), with `eg_vindex_note_embedded` as the write-side owner. This is a **publication** boundary, not a capability split — HNSW insert is not an append, so purity alone was insufficient (§2a, §3.1(3)).
6. Delete the residue-fixes listed in §5. *(Partially done — see §5's "NOT deleted" list; a residue whose structure has not been converted must be left standing.)*
7. **One** build of soul from el dev — which resolves the `state_get` leak and the crash together, rather than deploying a leak fix that reintroduces the crash.
---
+63 -7
View File
@@ -45,17 +45,43 @@ returned 60k230k-char unbounded traversals (this very session hit 104 KB and
## Layer 2 — primitive agentic tools (Neuron runs itself)
The base verbs all agentic behavior composes from — grounded in the LIVE
cog-arch (`think` is the one operation; faculties are its steering-space labels;
the correspondence-beat is the reflexive learning loop).
The base verbs all agentic behavior composes from.
> **⚠ The "PROVEN" verdicts in this table were measured against a build dated
> 2026-08-14 and four of the five are now known to have been proving the wrong
> thing (2026-08-16).** A verdict of PROVEN meant *the route returned a
> well-formed response*, not *the response was derivable from what produced it*.
> Corrections below, each with the measurement. Authority:
> `lang/spec/correspondence-and-censorship.md`.
| op | signature | engram builtin | status on clone (gate-1 recipe) |
|----|-----------|----------------|---------------------------------|
| `think` | `think({seeds, faculty})` faculty ∈ reason·abduce·induce·plan·analogize·recognize·discern·synthesize | `engram_think_json` | **PROVEN** — all 8 faculties return real 768-dim gradients (n_support 30282) |
| `think` | `think({seeds, faculty})` faculty ∈ reason·abduce·induce·plan·analogize·recognize·discern·synthesize | `engram_think_json` | ~~PROVEN — all 8 faculties return real 768-dim gradients~~ **RETRACTED, then re-proven differently.** The gradients were real in *shape* only: the call passed `NULL` as the anchor, `engram_think` re-origins at `anchor ? anchor : region->centroid`, and **the centroid is the one point where the gradient is zero by construction**. Measured: every faculty returned `{"direction":[0,0,…],"spread":0,"magnitude":1,"confidence":0.5}` — identical, differing only in its label. Fixed in **#141/#142**; gradients now vary by seed |
| `attend` | `attend({node, observer, salience})` | `engram_attend_json` | **PROVEN** (returns `salient-to`) |
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PROVEN** |
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | **PROVEN** (grounded-by edge, grounding=0.912, written) |
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN** (real Stance: `stance-induce-…`, brier, reliability, written) |
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PARTIAL.** `may_assert` is real. `"still_held"` is a **hardcoded literal `true`**`el_runtime.c:14538` emits it unconditionally, so it reports nothing it measured. Violates the invariant *a returned value must be derivable from what produced it* |
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | ~~PROVEN (grounded-by edge, grounding=0.912, written)~~ **RETRACTED.** That 0.912 was structural, not evidential: the call wrote the edge between the two *region hubs* and echoed them back as though they were the caller's input, so when both seeds resolved into one region it **grounded a node against itself and returned a confident score**. Measured: grounding `3b9ced5d` against `6edf8c79` scored **0.98883** purely because `6edf8c79` is the hub of `3b9ced5d`'s region; two independent agents reported 0.885 / 0.909 self-groundings as confident. **#147** grounds the node asked about, reports `claim_region`/`evidence_region` separately, and refuses three circular shapes. **The operation itself is still the wrong shape** — see below |
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN, and it was writing into a void.** The Stance, brier and reliability were real and really persisted — but `think` built a *neutral* stance every call and never loaded them, so every beat's calibration was written and thrown away on the next read. Fixed in **#146**: `think` resumes `stance-<faculty>-<hub>`, the same id the beat writes. Confidence **0.5 → 0.930726** on a calibrated region |
### What this table gets structurally wrong
- **`faculty` is not a parameter.** `reason` changes the *estimate* (a read),
`induce` changes the *parameters* (this is exactly what `learn` does), and
`abduce` changes the *structure* — a **write**, which `GeoGradient` cannot
express. A write cannot be a parameter of a read. That the eight were listed as
interchangeable values of one argument is why all eight returning the same thing
looked like a pass. Underneath, `engram/src/server.el:18701886` routes six of
them into one call with a string argument, and the name only reaches
`engram_think` through the stance — `cog_stance_init` stores it and nothing
reads it.
- **`ground` should not mint an edge at all.** Grounding is not a subsystem and
not a score: **it is the edge weight.** `grounded-by` as a relation type models
grounding as a relation *between* nodes when it is a property *of* a relation.
#147 corrected a scalar rather than deleting the operation; deletion is
sequenced.
- **`addWonderQuestion`** (Layer 1, `write`) treats wonder as an enumerable
instance you push. **Wonder is the boundary** — where activation spreads and
finds thin or absent geometry. There are about six, the same for everyone, and
they never close. A manifest materializes a property as a stored artifact.
`comprehend`/`realize`/`intend` are **compositions**, not separate live
primitives: comprehend = write+activate (world→geometry), realize = assert
@@ -69,6 +95,26 @@ execution→integrate) composes over `think`+`ground`+`learn`+`write`/`relate`.
`kn-efeb4a5b…` / `kn-5b606390…`, are refused — identity routes through
intentional-cultivation, as enforced today.
> **⚠ SUPERSEDED (2026-08-16).** This describes what the surface enforces, which
> is accurate — but the enforcement is the wrong kind of thing:
>
> > **In an immutable substrate, any mechanism that refuses a write is either
> > redundant with immutability, or an epistemic constraint misfiled as a
> > protective one.**
>
> "Keystone" means **load-bearing**, not precious. The real requirement is
> **non-circularity of the reference frame** — a reference fitted to its own
> readings reports perfect correspondence forever while drift becomes undetectable
> from inside — and that is satisfied *temporally*, not by a gate: the frame
> updates while activation is internally seeded, not while it is being used to act.
> **Independence is *when*, not *what*.** Corruption requires mutation, and the
> engram does not mutate: recoverability (the predecessor is always present),
> governance (supersession *is* the audit trail), evidence quality, and rate all
> fall out of the substrate. **Authorization** is the only residue and it is
> bounded — an unauthorized writer can *propose*, never erase. Note also that the
> live check is a substring match against two hard-coded ids
> (`el_runtime.c:14337`).
## How the caller invokes Neuron agentically
Once the ops are registered as MCP tools (aliases in `surface.el`), the caller
@@ -94,6 +140,16 @@ running itself.
## Honest ledger (built vs staged)
- **Route seam — IMPLEMENTED + PROVEN:** ported the `@route` codegen (from `feat/el-route-decorators`) into the worktree, rebuilt `elc` self-host, proved decorate→serve (`route_proof.el` on :8951); `surface.el` compiles with `el_route_dispatch` generated for all 8 ops.
- **All ops PROVEN live on the clone** (gate-1 boot recipe, node-id anchors): read, write, relate, supersede (immutable), tombstone, think (8 faculties), ground, attend, learn — daemon alive through all mutations (node_count 13173→13176).
> **⚠ Retracted in part (2026-08-16).** "The daemon stayed alive and every route
> returned a well-formed response" is what was actually proven, and that is a
> weaker claim than it reads as. See the Layer-2 table: `think` was reading at the
> zero-gradient point, `ground` was scoring nodes against themselves, `assert`
> emits a hardcoded field, and `learn` was persisting into a void. **A build that
> passes because nothing checks whether a returned value is derivable from what
> produced it has not been tested — it has been observed not to crash.** The
> related discipline gap, also 2026-08-16: **no test without a negative control**
> (#148's first attempt passed on the unpatched build too), and **no deploy
> without verifying the artifact carries the fix** (nine instances in one session).
- **Aperture-boundedness PROVEN:** vantage-read `limit=3 → 15 KB` vs `limit=50 → 363 KB` (fixes the whole-self dump).
- **Bus:** `@manager` ops emit on the real `dharma_*` bus (explicit today, compiles) — same transport as the swarm (`wt/swarm-ccr`).
- **STAGED (not guessed — needs the cognition-engram rebuild to verify link):** auto-injecting telemetry/interoception + bus emission at the decorated boundary (`cg_fn` diff in `SEAM_STAGED.md`); building the cognition engram with `surface.el` compiled in. No promote to live, no cutover (per rails).