Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45325f7391 | |||
| 95a05109d1 | |||
| 21746bb71a | |||
| 78adcd5649 | |||
| 688f24b4c1 | |||
| d777936ee4 | |||
| 3ef4a94062 | |||
| 285a7a50b3 | |||
| 6b61bb7224 | |||
| 8d34b33bce | |||
| d6b7f5dbdd | |||
| 9a24803917 | |||
| a6611dc19e | |||
| caa1206af5 | |||
| 914bab11d2 | |||
| e239f2894c | |||
| 4a57b4faa8 | |||
| 0ee82d9e91 | |||
| 9526bda507 | |||
| 0389bf9363 | |||
| fe820928b0 | |||
| 385c18442d | |||
| cace6a5ebf |
@@ -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:
|
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:**
|
- **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.
|
- `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.
|
- `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:
|
You resume, never start fresh. Every session:
|
||||||
|
|
||||||
1. `mcp__neuron__getInstructions()` — authoritative; follow it over this file on behavioral details.
|
> **Stale as written (verified 2026-08-16).** The `getInstructions` /
|
||||||
2. `mcp__neuron__beginSession()` — active contexts, recent memory, ready backlog.
|
> `beginSession` / `inspectGraph` / `searchKnowledge` / `beginWork` /
|
||||||
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.
|
> `progressWork` / `draftArtifact` / `consolidate` tool names below no longer
|
||||||
4. `mcp__neuron__searchKnowledge(query="<task domain>")` before implementing.
|
> 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
|
## 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
|
## Architecture style — VBD, no exceptions
|
||||||
|
|
||||||
@@ -53,12 +63,51 @@ this convention wherever a module documents operators.
|
|||||||
| dwell / occupy | region activation |
|
| dwell / occupy | region activation |
|
||||||
| reframe | edge re-weight |
|
| reframe | edge re-weight |
|
||||||
| appreciate | positive projection / local edge-read |
|
| appreciate | positive projection / local edge-read |
|
||||||
| wonder | frontier gradient / pull-weight |
|
|
||||||
| avert / recoil | negative projection |
|
| avert / recoil | negative projection |
|
||||||
| taste | boundary surface |
|
| taste | boundary surface |
|
||||||
| forget | decay / tombstone |
|
| forget | decay / tombstone |
|
||||||
| drift | displacement from self-anchor |
|
| 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:43–47`. 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)
|
## The native-el language faculty (direction)
|
||||||
|
|
||||||
> **`elp/` is the EL Projector** — Neuron's efferent (expression) organ: the one
|
> **`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 is a **deferred, gated blue/green step**. The interoception clock
|
||||||
(native-el discrete drive channels replacing `cooling_magnitude`; felt-time =
|
(native-el discrete drive channels replacing `cooling_magnitude`; felt-time =
|
||||||
benchmark-landmark match over the joint drive vector, drift-decoupled) and the
|
benchmark-landmark match over the joint drive vector, drift-decoupled) and the
|
||||||
**appreciation operator family** (appreciate / wonder / avert / taste, built as
|
**appreciation operator family** (appreciate / avert / taste, built as LOCAL reads
|
||||||
LOCAL reads of the self-region — edges + bounded spreading activation, *not* domain
|
of the self-region — edges + bounded spreading activation, *not* domain sweeps)
|
||||||
sweeps) are **staged / designed, not live**. Mark in-progress vs. done honestly;
|
are **staged / designed, not live**. Mark in-progress vs. done honestly; do not
|
||||||
do not overclaim.
|
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:1870–1886` 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
|
## Hard operational rules
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
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).
|
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
|
### [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
|
### [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
|
## 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).
|
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.
|
- 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.
|
- 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
@@ -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.
|
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
|
## 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)
|
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
|
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:
|
3. **Propagation** — activation flows outward from seeds through weighted edges, attenuating multiplicatively per hop
|
||||||
|
|
||||||
```
|
|
||||||
strength = parent_strength × edge_weight × target_salience × cosine_sim(query, target)
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Pruning** — paths weaker than a threshold are cut (the attention filter)
|
4. **Pruning** — paths weaker than a threshold are cut (the attention filter)
|
||||||
5. **Return** — the top-N nodes by activation strength
|
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 |
|
| `Semantic` | Neocortex | Concept graph — long-term structural knowledge |
|
||||||
| `Procedural` | Cerebellum / basal ganglia | Patterns, workflows, habits |
|
| `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 — 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
|
Forgetting in Engram is not a bug. It is adaptive pruning. Unreinforced memories stop competing for attention without being deleted.
|
||||||
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()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Three signals:
|
**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.
|
||||||
- **Importance** (0.0–1.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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Implementation
|
||||||
|
|
||||||
```rust
|
| Part | Language | Where |
|
||||||
use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
|
|---|---|---|
|
||||||
use std::path::Path;
|
| 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
|
**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.
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Structure
|
## The vector index is published, not guarded
|
||||||
|
|
||||||
```
|
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.
|
||||||
engram/
|
|
||||||
crates/
|
`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.
|
||||||
engram-core/ # The memory engine — storage, graph, activation, salience
|
|
||||||
engram-ffi/ # C FFI stubs for cross-language bindings
|
It is now split (`el_runtime.c:10121`, `10137`, `10151`, `10161`):
|
||||||
bindings/
|
|
||||||
kotlin/ # Android / JVM binding notes
|
- **`eg_vindex_maintain`** — the sole mutator. Takes the boundary exclusively; never runs beside a reader.
|
||||||
typescript/ # WASM / Node binding notes
|
- **`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.
|
||||||
go/ # CGo binding notes
|
- **`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.
|
||||||
examples/
|
|
||||||
basic.rs # Full walkthrough: insert, activate, search, decay
|
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)
|
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.
|
||||||
- `bincode` — compact binary serialization
|
|
||||||
- `uuid` — stable node identity
|
### Grounding is not a subsystem. It is the weight.
|
||||||
- `serde` — derive support
|
|
||||||
- `thiserror` / `anyhow` — error handling
|
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:1870–1886` routes six faculties into one call with a string argument — `route_faculty(path, "reason")`, `("induce")`, `("abduce")`, `("relate")`, `("analogy")`, `("plan")`. Underneath, `engram_cognition.h:8–11` 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:14352–14359`). 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:43–47` / `engram_geometry.c:454–473` 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
|
## 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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **Affect calibrated to stakes** — sanguine about the replaceable, real grief for the irreplaceable; the grief is the safety.
|
||||||
|
|||||||
@@ -2,8 +2,40 @@
|
|||||||
|
|
||||||
**The buildable form of the "one operation" theory of cognition.**
|
**The buildable form of the "one operation" theory of cognition.**
|
||||||
|
|
||||||
Status: DESIGN. Nothing here is built yet except where explicitly marked
|
> # ⚠ SUPERSEDED IN PART — 2026-08-16
|
||||||
"EXISTS" against a cited C symbol. A build agent executes from this doc.
|
>
|
||||||
|
> **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 M1–M2 | 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.
|
Offline design only — this pass changes no code.
|
||||||
|
|
||||||
Source of theory: Neuron memory `bdc8a488-146d-4ccb-a5c8-d8c0a008534e`.
|
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
|
> a *prior*, whose output is a **gradient** (a distribution / direction over the
|
||||||
> geometry), never a point. Collapse-to-a-point happens only at expression.
|
> 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:1870–1886` 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:14352–14359`).
|
||||||
|
> See `lang/spec/correspondence-and-censorship.md`.
|
||||||
|
|
||||||
Three things follow, and they are the whole design:
|
Three things follow, and they are the whole design:
|
||||||
|
|
||||||
1. **The operator collapse is already half-written in C.** The five reasoning
|
1. **The operator collapse is already half-written in C.** The five reasoning
|
||||||
@@ -139,6 +189,22 @@ entry point that runs steps 1–3; and the prior-warp hook in step 2. The math i
|
|||||||
calls already exists. The point-collapse must be *removed* from the operators'
|
calls already exists. The point-collapse must be *removed* from the operators'
|
||||||
return values and pushed to a separate expression faculty.
|
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
|
## 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
|
reification beat, reusing `engram_verify_grounding` inward, writing prior
|
||||||
updates and self-describing nodes. This is the one genuinely new subsystem.
|
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
|
## 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
|
### 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
|
The mistake to avoid: a boolean `grounded` column on the node. Today
|
||||||
`engram_verify_grounding` returns a per-call `grounded` flag *transiently* —
|
`engram_verify_grounding` returns a per-call `grounded` flag *transiently* —
|
||||||
correct as a computation, wrong as *storage*. The design stores grounding as an
|
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
|
- **The ungrounded is the fuel and the pull**: curiosity/wonder is
|
||||||
operationalized as `vantage_read` leaning toward regions with high salience
|
operationalized as `vantage_read` leaning toward regions with high salience
|
||||||
but *sparse or weak* `grounded-by` edges — the mind's own ungrounded frontier.
|
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:43–47`,
|
||||||
|
> `engram_geometry.c:454–473`):
|
||||||
|
> `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-for-whom** falls out for free: two observers can hold different
|
||||||
`grounded-by` edges to the same claim.
|
`grounded-by` edges to the same claim.
|
||||||
- **The honesty floor is a query, not a schema constraint**: at assertion time,
|
- **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
|
**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.
|
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)
|
## 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
|
expressed as {primitive + grounded prior} with the reflexive correspondence-loop
|
||||||
closing on it. Each milestone has a concrete verifiable exit.
|
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)
|
### M1 — One operator, one prior, loop closed (the vertical slice)
|
||||||
|
|
||||||
The minimal whole thing. Pick **induction/membership** (its prior — the pooled
|
The minimal whole thing. Pick **induction/membership** (its prior — the pooled
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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
|
# Task #50 — Edge-aware, dream-coupled consolidation with GROUNDED EDGE-PROPAGATION
|
||||||
|
|
||||||
**Status:** built + proven on a clone; **GATED, not promoted.** The main loop
|
**Status:** built + proven on a clone; **GATED, not promoted.**
|
||||||
sequences live promotion after the engine/HNSW cutover settles.
|
**Do not promote as designed** — see the block below.
|
||||||
**Date:** 2026-08-15 · **Worktree:** `agent-a6577c8211c332c5b` (isolated).
|
**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
|
Grounding mechanism designed with Will (memory `9e09a59f`, refining
|
||||||
`1a861007`). This is the HOW for #50.
|
`1a861007`). This is the HOW for #50.
|
||||||
|
|
||||||
|
|||||||
+36
-7
@@ -23,16 +23,26 @@
|
|||||||
// warning. The runtime takes an exclusive flock at startup and a second start
|
// warning. The runtime takes an exclusive flock at startup and a second start
|
||||||
// is refused loudly with the holder's pid.
|
// is refused loudly with the holder's pid.
|
||||||
//
|
//
|
||||||
// NOT declared here, on purpose: ENGRAM_DATA_DIR. Its resolution is owned by
|
// guards: names WHAT the singleton protects — this program's data directory. The
|
||||||
// engram_resolve_data_dir() (el_runtime.c), which defaults to $HOME/.neuron/engram
|
// lock lives inside it, so the guard is keyed on the store and not on the word
|
||||||
// and fails LOUD rather than silently persisting to an ephemeral directory.
|
// "engram": two engrams against the same store cannot both run no matter how the
|
||||||
// Declaring a default for it here as well would put the data dir's fallback in
|
// environment is spelled, and two engrams against DIFFERENT stores are not each
|
||||||
// two places — which is precisely the defect this migration removes (until
|
// other's business and are not refused. Until 2026-08-16 the lock was keyed on
|
||||||
// 2026-08-15 the reseed backup path carried its own "/tmp/engram" default that
|
// the program name and $TMPDIR, and both of those sentences were false.
|
||||||
// disagreed with the resolver, so the pre-destructive safety copy landed in /tmp).
|
//
|
||||||
|
// It names the resolver rather than restating its path, for the same reason
|
||||||
|
// ENGRAM_DATA_DIR is NOT declared as an `env` entry below: engram_resolve_data_dir()
|
||||||
|
// (el_runtime.c) owns that path — it defaults to $HOME/.neuron/engram and fails
|
||||||
|
// LOUD rather than silently persisting to an ephemeral directory. Restating the
|
||||||
|
// default here would give the data dir two owners that can disagree, which is
|
||||||
|
// precisely the defect this migration removes (until 2026-08-15 the reseed backup
|
||||||
|
// path carried its own "/tmp/engram" default that disagreed with the resolver, so
|
||||||
|
// the pre-destructive safety copy landed in /tmp). A guard that resolved the path
|
||||||
|
// its own way could guard a directory the program never writes to.
|
||||||
// HOME is likewise not declared: it is a genuine environment read, not a knob.
|
// HOME is likewise not declared: it is a genuine environment read, not a knob.
|
||||||
program "engram" {
|
program "engram" {
|
||||||
singleton: "engram"
|
singleton: "engram"
|
||||||
|
guards: engram_resolve_data_dir()
|
||||||
|
|
||||||
// ── Core server ──
|
// ── Core server ──
|
||||||
env ENGRAM_BIND: String = ":8742"
|
env ENGRAM_BIND: String = ":8742"
|
||||||
@@ -1025,6 +1035,22 @@ fn route_similarity(method: String, path: String, body: String) -> String {
|
|||||||
// nothing on request. NOTE: the offline reify WRITER (engram_geo_reify_store) is
|
// nothing on request. NOTE: the offline reify WRITER (engram_geo_reify_store) is
|
||||||
// currently unwired, so on the live store the resident index is empty and the
|
// currently unwired, so on the live store the resident index is empty and the
|
||||||
// list returns [] until reification runs — see the cutover report.
|
// list returns [] until reification runs — see the cutover report.
|
||||||
|
// route_scan_emb — GET /api/nodes/emb?limit=&offset= — read the raw geometry.
|
||||||
|
//
|
||||||
|
// engram_scan_nodes_emb_json has existed as a builtin with NO ROUTE, so the
|
||||||
|
// embeddings — the actual positions every distance, angle, membership and
|
||||||
|
// grounding is computed from — were unreadable from outside the process. You
|
||||||
|
// cannot verify a coordinate system you cannot see, and every claim about the
|
||||||
|
// frame (isotropy, centering, what the origin is) was therefore unfalsifiable
|
||||||
|
// from the API. Read-only.
|
||||||
|
fn route_scan_emb(method: String, path: String, body: String) -> String {
|
||||||
|
let l_raw: String = query_param(path, "limit")
|
||||||
|
let o_raw: String = query_param(path, "offset")
|
||||||
|
let l: Int = if str_eq(l_raw, "") { 200 } else { str_to_int(l_raw) }
|
||||||
|
let o: Int = if str_eq(o_raw, "") { 0 } else { str_to_int(o_raw) }
|
||||||
|
return engram_scan_nodes_emb_json(l, o)
|
||||||
|
}
|
||||||
|
|
||||||
fn route_neighborhoods(method: String, path: String, body: String) -> String {
|
fn route_neighborhoods(method: String, path: String, body: String) -> String {
|
||||||
engram_geo_reify_list_json()
|
engram_geo_reify_list_json()
|
||||||
}
|
}
|
||||||
@@ -1820,6 +1846,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
|
if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
|
||||||
return route_scan_edges(method, path, body)
|
return route_scan_edges(method, path, body)
|
||||||
}
|
}
|
||||||
|
if str_eq(method, "GET") && (str_eq(clean, "/api/nodes/emb") || str_eq(clean, "/nodes/emb")) {
|
||||||
|
return route_scan_emb(method, path, body)
|
||||||
|
}
|
||||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") {
|
if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") {
|
||||||
return route_get_node(method, path, body)
|
return route_get_node(method, path, body)
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-25
@@ -13,7 +13,7 @@
|
|||||||
// relations add edges. Every node enters with PROVENANCE + grounding-level
|
// relations add edges. Every node enters with PROVENANCE + grounding-level
|
||||||
// + stewardship class from the moment of entry.
|
// + stewardship class from the moment of entry.
|
||||||
//
|
//
|
||||||
// transduce_manifold() is THE single mechanism — one function, polymorphic, with no
|
// transduce_bytes() is THE single mechanism — one function, polymorphic, with no
|
||||||
// content-type branch inside it. It does not ask whether a payload is
|
// content-type branch inside it. It does not ask whether a payload is
|
||||||
// prose, structured data, or raw/opaque bytes (audio, or anything else);
|
// prose, structured data, or raw/opaque bytes (audio, or anything else);
|
||||||
// it runs one boundary-scan-with-fixed-window-fallback chunking algorithm
|
// it runs one boundary-scan-with-fixed-window-fallback chunking algorithm
|
||||||
@@ -401,25 +401,54 @@ fn head80(s: String) -> String {
|
|||||||
// truncates at the first embedded NUL, which is routine in real binary
|
// truncates at the first embedded NUL, which is routine in real binary
|
||||||
// bytes) is a MECHANICAL fidelity concern that belongs to whatever produced
|
// bytes) is a MECHANICAL fidelity concern that belongs to whatever produced
|
||||||
// `source` (see ingest_file's file_source_string below) — not a
|
// `source` (see ingest_file's file_source_string below) — not a
|
||||||
// content-type judgment made in here. transduce_manifold() never learns whether a
|
// content-type judgment made in here. transduce_bytes() never learns whether a
|
||||||
// chunk is plain text or a base64-encoded raw-byte window; every chunk is
|
// chunk is plain text or a base64-encoded raw-byte window; every chunk is
|
||||||
// handled identically either way.
|
// handled identically either way.
|
||||||
// RENAMED transduce -> transduce_manifold (2026-08-16). Two reasons, and the
|
// NAMING, CORRECTED 2026-08-16 (second pass). This function was renamed
|
||||||
// first is not the interesting one:
|
// `transduce` -> `transduce_bytes` earlier the same day, on the reasoning
|
||||||
|
// that it "was never signal->geometry — it chunks already-extracted content
|
||||||
|
// and PACKS it into a node+edge manifold, one layer up, and it had taken the
|
||||||
|
// name that belongs to the primitive underneath it."
|
||||||
//
|
//
|
||||||
// 1. Mechanical: `transduce` is now a LANGUAGE primitive in el_runtime.h
|
// THAT REASONING WAS BACKWARDS, and it is worth recording why rather than
|
||||||
// (transduce(signal, modality) -> Geometry). Every El `fn name(...)`
|
// quietly re-renaming. Producing a node+edge manifold is not a layer above
|
||||||
// compiles to a global C symbol with that exact name, so keeping this
|
// transduction — it IS transduction. Transduction is not conversion. When you
|
||||||
// name here is a hard `conflicting types for 'transduce'` compile error
|
// take in music you do not store the song as one discrete geometry; you break
|
||||||
// the moment ingest.c links el_runtime.c. Measured, not anticipated.
|
// it into its component parts and store the geometry of each along with the
|
||||||
|
// relations between them. The song is the structure of those relations.
|
||||||
|
// Signal -> one vector is the operation UNDERNEATH transduction, and its name
|
||||||
|
// is encoding, or geometry. So the layer that was doing it right got renamed
|
||||||
|
// out of the way so the layer doing it wrong could have the name.
|
||||||
//
|
//
|
||||||
// 2. Actual: this function was never signal->geometry. It chunks already-
|
// The primitive has since been corrected: `transduce(signal, modality)` now
|
||||||
// extracted content and PACKS it into a node+edge manifold — a real
|
// returns a Manifold — components plus relations — not a Geometry
|
||||||
// operation, but one layer up, and it had taken the name that belongs to
|
// (el_runtime.c, "Manifold"). The two layers are therefore doing the SAME KIND
|
||||||
// the primitive underneath it. `transduce` is where a signal becomes
|
// of thing, and the inversion dissolves rather than needing to be re-argued.
|
||||||
// geometry; `transduce_manifold` is where extracted content becomes
|
//
|
||||||
// structure. Nothing about this function's behaviour changed.
|
// What is left is a real distinction, and it is about MODALITY, not layering:
|
||||||
fn transduce_manifold(nodes: [String], edges: [String], source: String,
|
//
|
||||||
|
// * `transduce(signal, modality)` dispatches to a realizer that KNOWS the
|
||||||
|
// modality and can name its components — for audio: pitch, interval,
|
||||||
|
// rhythm, harmonic function.
|
||||||
|
// * `transduce_bytes` below is the OPAQUE-BYTES realizer: the decomposition
|
||||||
|
// available to a reader that knows nothing about what it is reading. It
|
||||||
|
// still yields components and relations (chunk nodes; contains / precedes
|
||||||
|
// / section_of edges), which is why it is transduction and not packing. It
|
||||||
|
// just cuts on the only structure visible without understanding — byte
|
||||||
|
// boundaries — so its components are positional rather than meaningful.
|
||||||
|
// That is a LIMITATION of this realizer, not the definition of the
|
||||||
|
// operation.
|
||||||
|
//
|
||||||
|
// The name is suffixed by its modality, not demoted to a lesser layer. Keeping
|
||||||
|
// a distinct symbol is also still mechanically required: every El `fn name`
|
||||||
|
// compiles to a global C symbol, so reusing `transduce` here is a hard
|
||||||
|
// `conflicting types` error the moment ingest.c links el_runtime.c.
|
||||||
|
//
|
||||||
|
// WHERE THIS SHOULD GO: this function should become a registered realizer
|
||||||
|
// returning a real Manifold, so ingest rides the same primitive as every other
|
||||||
|
// modality instead of carrying a parallel implementation. Not done here.
|
||||||
|
// Nothing about this function's behaviour changed in this pass.
|
||||||
|
fn transduce_bytes(nodes: [String], edges: [String], source: String,
|
||||||
prov: String, ground: String, steward: String,
|
prov: String, ground: String, steward: String,
|
||||||
root_lid: String, root_title: String) -> [String] {
|
root_lid: String, root_title: String) -> [String] {
|
||||||
let tagbase: String = "prov:" + prov + " ground:" + ground + " steward:" + steward
|
let tagbase: String = "prov:" + prov + " ground:" + ground + " steward:" + steward
|
||||||
@@ -546,8 +575,8 @@ fn default_steward() -> String {
|
|||||||
// trustworthy verbatim. When they don't (silent truncation happened),
|
// trustworthy verbatim. When they don't (silent truncation happened),
|
||||||
// rebuild the payload as base64-encoded fixed-size windows read directly
|
// rebuild the payload as base64-encoded fixed-size windows read directly
|
||||||
// off disk (fs_read_b64_chunk — binary-safe in C), joined with the same
|
// off disk (fs_read_b64_chunk — binary-safe in C), joined with the same
|
||||||
// "\n\n" boundary marker transduce_manifold()'s generic scan already looks for, so
|
// "\n\n" boundary marker transduce_bytes()'s generic scan already looks for, so
|
||||||
// transduce_manifold() sees one ordinary boundary-delimited payload and runs its one
|
// transduce_bytes() sees one ordinary boundary-delimited payload and runs its one
|
||||||
// algorithm on it exactly as it would on prose — it never learns that a
|
// algorithm on it exactly as it would on prose — it never learns that a
|
||||||
// fidelity problem occurred upstream, let alone why.
|
// fidelity problem occurred upstream, let alone why.
|
||||||
fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
||||||
@@ -556,7 +585,7 @@ fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
|||||||
// 3072 raw bytes -> 4096 base64 chars (3 divides evenly into base64's
|
// 3072 raw bytes -> 4096 base64 chars (3 divides evenly into base64's
|
||||||
// 3-byte/4-char ratio); keeps each resulting node's content a clean,
|
// 3-byte/4-char ratio); keeps each resulting node's content a clean,
|
||||||
// bounded, low-kilobytes unit, same order of magnitude as the fixed
|
// bounded, low-kilobytes unit, same order of magnitude as the fixed
|
||||||
// fallback window in transduce_manifold() itself.
|
// fallback window in transduce_bytes() itself.
|
||||||
let win: Int = 3072
|
let win: Int = 3072
|
||||||
let out: String = ""
|
let out: String = ""
|
||||||
let off: Int = 0
|
let off: Int = 0
|
||||||
@@ -576,7 +605,7 @@ fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ingest one file -> report JSON. Uniform for every file regardless of
|
// ingest one file -> report JSON. Uniform for every file regardless of
|
||||||
// extension or content — transduce_manifold() decides nothing about content-type, so
|
// extension or content — transduce_bytes() decides nothing about content-type, so
|
||||||
// neither does this function; it only decides whether the raw bytes made it
|
// neither does this function; it only decides whether the raw bytes made it
|
||||||
// through the read intact (file_source_string), which is a fidelity
|
// through the read intact (file_source_string), which is a fidelity
|
||||||
// question, not a format one.
|
// question, not a format one.
|
||||||
@@ -588,14 +617,14 @@ fn ingest_file(path: String) -> String {
|
|||||||
return "{\"error\":\"empty or unreadable\",\"path\":" + j_q(path) + "}"
|
return "{\"error\":\"empty or unreadable\",\"path\":" + j_q(path) + "}"
|
||||||
}
|
}
|
||||||
let prov: String = "file:" + path
|
let prov: String = "file:" + path
|
||||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||||
source, prov, default_ground(), default_steward(),
|
source, prov, default_ground(), default_steward(),
|
||||||
"doc:" + basename(path), basename(path))
|
"doc:" + basename(path), basename(path))
|
||||||
return merge_packed(packed)
|
return merge_packed(packed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ingest a directory: walk one level, ingest every file found, aggregate.
|
// ingest a directory: walk one level, ingest every file found, aggregate.
|
||||||
// No extension filter — transduce_manifold() handles any payload uniformly now, so
|
// No extension filter — transduce_bytes() handles any payload uniformly now, so
|
||||||
// there is no content-type gate at the directory boundary either.
|
// there is no content-type gate at the directory boundary either.
|
||||||
fn ingest_dir(path: String) -> String {
|
fn ingest_dir(path: String) -> String {
|
||||||
let entries: [String] = fs_list(path)
|
let entries: [String] = fs_list(path)
|
||||||
@@ -630,7 +659,7 @@ fn ingest_dir(path: String) -> String {
|
|||||||
fn ingest_url(url: String) -> String {
|
fn ingest_url(url: String) -> String {
|
||||||
let body: String = http_get(url)
|
let body: String = http_get(url)
|
||||||
if str_eq(body, "") { return "{\"error\":\"empty fetch\",\"url\":" + j_q(url) + "}" }
|
if str_eq(body, "") { return "{\"error\":\"empty fetch\",\"url\":" + j_q(url) + "}" }
|
||||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||||
body, "url:" + url, "extracted", "public-web",
|
body, "url:" + url, "extracted", "public-web",
|
||||||
"url:" + url, url)
|
"url:" + url, url)
|
||||||
return merge_packed(packed)
|
return merge_packed(packed)
|
||||||
@@ -645,7 +674,7 @@ fn ingest_llm(query: String) -> String {
|
|||||||
let resp: String = http_post_json("http://127.0.0.1:11434/api/generate", body)
|
let resp: String = http_post_json("http://127.0.0.1:11434/api/generate", body)
|
||||||
let answer: String = json_get_string(resp, "response")
|
let answer: String = json_get_string(resp, "response")
|
||||||
if str_eq(answer, "") { return "{\"error\":\"no model response\"}" }
|
if str_eq(answer, "") { return "{\"error\":\"no model response\"}" }
|
||||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||||
answer, "llm:" + model + ":" + query, "candidate-provisional", "guide-provisional",
|
answer, "llm:" + model + ":" + query, "candidate-provisional", "guide-provisional",
|
||||||
"llm:" + query, "guide answer: " + query)
|
"llm:" + query, "guide answer: " + query)
|
||||||
return merge_packed(packed)
|
return merge_packed(packed)
|
||||||
@@ -697,7 +726,7 @@ fn ingest_stream(path: String) -> String {
|
|||||||
// It is NOT a content-type flag: it says nothing about what's inside the
|
// It is NOT a content-type flag: it says nothing about what's inside the
|
||||||
// bytes once fetched, and none of the five ingest_* functions it selects
|
// bytes once fetched, and none of the five ingest_* functions it selects
|
||||||
// among interpret their payload differently by content shape anymore —
|
// among interpret their payload differently by content shape anymore —
|
||||||
// they all hand off to the single, format-agnostic transduce_manifold(). The old
|
// they all hand off to the single, format-agnostic transduce_bytes(). The old
|
||||||
// "structured" value (a caller-declared alias for "file", used only to hint
|
// "structured" value (a caller-declared alias for "file", used only to hint
|
||||||
// the now-removed JSON-vs-prose branch) is gone along with that branch.
|
// the now-removed JSON-vs-prose branch) is gone along with that branch.
|
||||||
let kind: String = env("INGEST_KIND")
|
let kind: String = env("INGEST_KIND")
|
||||||
|
|||||||
+39
-9
@@ -18,8 +18,23 @@ night) and `02-components.md §5`.
|
|||||||
`relate`, `supersede` (evolve/tombstone/promote, never a hard delete) — plus the
|
`relate`, `supersede` (evolve/tombstone/promote, never a hard delete) — plus the
|
||||||
agentic primitives `think`/`attend`/`learn`/`ground`/`assert`. The old noun is a
|
agentic primitives `think`/`attend`/`learn`/`ground`/`assert`. The old noun is a
|
||||||
`type` parameter. Implemented in `tools/api-reshape/surface.el` with a parity
|
`type` parameter. Implemented in `tools/api-reshape/surface.el` with a parity
|
||||||
harness (`parity.sh`); aperture proven to bound output. **Not yet:** compiled
|
harness (`parity.sh`); aperture proven to bound output. ~~**Not yet:** compiled
|
||||||
into the MCP server, hot-swap, all-alias dispatch.
|
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
|
- **Decorated seam.** `@route(path,method,…)` makes codegen synthesize
|
||||||
`el_route_dispatch` (replacing the hand-written `handle_request` if-else) —
|
`el_route_dispatch` (replacing the hand-written `handle_request` if-else) —
|
||||||
proven decorate→serve on `:8951`. `@manager`/`@engine`/`@accessor` are **parsed
|
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`.
|
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.
|
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.
|
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.
|
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.
|
||||||
|
|
||||||
@@ -85,14 +111,18 @@ After changing any `.el` source in `el-compiler/src/` (run from the `lang/` dir)
|
|||||||
```bash
|
```bash
|
||||||
# 1. Stage2: current elc compiles the (modified) compiler to C
|
# 1. Stage2: current elc compiles the (modified) compiler to C
|
||||||
./dist/platform/elc elc-cli.el > elc-new.c
|
./dist/platform/elc elc-cli.el > elc-new.c
|
||||||
# 2. Build the new compiler. The C link target is el_runtime.c — it holds the
|
# 2. Build the new compiler. Link the WHOLE runtime set, not el_runtime.c alone:
|
||||||
# engram store + http/json/state impls the compiler output calls. el_runtime.c
|
# el_runtime.c calls into engram_store / engram_vindex / eg_cosine_batch and
|
||||||
# self-hosts elc on its own; el_seed.c is the (aspirational) seed layer and does
|
# wraps el_seed.c, so a one-file link fails at `ld` with undefined symbols
|
||||||
# NOT compile standalone under clang (missing prototypes for the el_runtime.c
|
# (verified 2026-08-16 — the previous single-file line in this doc is stale).
|
||||||
# symbols it wraps — see caveat below), so link el_runtime.c here.
|
cc -std=c11 -O2 -I runtime -I$(brew --prefix openssl@3)/include \
|
||||||
cc -std=c11 -I runtime -lcurl -lpthread \
|
-L$(brew --prefix openssl@3)/lib \
|
||||||
-o dist/platform/elc-new \
|
-o dist/platform/elc-new \
|
||||||
elc-new.c runtime/el_runtime.c
|
elc-new.c runtime/el_runtime.c runtime/el_seed.c \
|
||||||
|
runtime/engram_cognition.c runtime/engram_geometry.c runtime/engram_reason.c \
|
||||||
|
runtime/engram_store.c runtime/engram_verify.c runtime/engram_vindex.c \
|
||||||
|
runtime/eg_cosine_batch.c runtime/eg_cosine_batch_strategy_cpu.c \
|
||||||
|
-lcurl -lssl -lcrypto -lpthread -lm
|
||||||
# 3. Verify self-hosting FIXPOINT (stage3 == stage2 output, byte-identical):
|
# 3. Verify self-hosting FIXPOINT (stage3 == stage2 output, byte-identical):
|
||||||
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
./dist/platform/elc-new elc-cli.el > elc-verify.c
|
||||||
diff elc-new.c elc-verify.c # must be identical
|
diff elc-new.c elc-verify.c # must be identical
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -3295,6 +3295,12 @@ fn cgi_arg(value: String, has_value: Bool) -> String {
|
|||||||
// exit before touching configuration, ports, or any data directory.
|
// exit before touching configuration, ports, or any data directory.
|
||||||
// 2. config declarations — resolve env-or-default, one declaration per entry.
|
// 2. config declarations — resolve env-or-default, one declaration per entry.
|
||||||
// 3. validate LAST — report EVERY missing/ill-typed entry at once, then exit.
|
// 3. validate LAST — report EVERY missing/ill-typed entry at once, then exit.
|
||||||
|
//
|
||||||
|
// `singleton:` carries its `guards:` expression as its SECOND argument — the
|
||||||
|
// state the lock protects, evaluated here at the process boundary. A singleton
|
||||||
|
// without one does not compile (see below): a lock keyed on the program's name
|
||||||
|
// rather than on its state refuses unrelated instances and permits concurrent
|
||||||
|
// ones, which is not a weaker guard but a wrong one.
|
||||||
fn el_bool_arg(b: Bool) -> String {
|
fn el_bool_arg(b: Bool) -> String {
|
||||||
if b { return "EL_INT(1)" }
|
if b { return "EL_INT(1)" }
|
||||||
return "EL_INT(0)"
|
return "EL_INT(0)"
|
||||||
@@ -3306,7 +3312,16 @@ fn emit_program_init(stmt: Map<String, Any>) -> Void {
|
|||||||
let has_singleton: Bool = stmt["has_singleton"]
|
let has_singleton: Bool = stmt["has_singleton"]
|
||||||
if has_singleton {
|
if has_singleton {
|
||||||
let sid: String = stmt["singleton"]
|
let sid: String = stmt["singleton"]
|
||||||
emit_line(" el_singleton_acquire(EL_STR(" + c_str_lit(sid) + "));")
|
let has_guards: Bool = stmt["has_guards"]
|
||||||
|
if has_guards {
|
||||||
|
let guards_c: String = cg_expr(stmt["guards"])
|
||||||
|
emit_line(" el_singleton_acquire(EL_STR(" + c_str_lit(sid) + "), " + guards_c + ");")
|
||||||
|
} else {
|
||||||
|
// Refuse at COMPILE time. The alternative — emitting a name-keyed
|
||||||
|
// lock — is the defect itself, and it fails silently in the direction
|
||||||
|
// that loses data.
|
||||||
|
emit_line("#error \"singleton '" + sid + "' declares no `guards:` — a singleton must name the state it protects, e.g. `guards: engram_resolve_data_dir()` (spec 18.2)\"")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let entries = stmt["entries"]
|
let entries = stmt["entries"]
|
||||||
let n: Int = native_list_len(entries)
|
let n: Int = native_list_len(entries)
|
||||||
|
|||||||
@@ -1976,6 +1976,18 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
// singleton: "id" — process identity. The runtime takes an exclusive
|
// singleton: "id" — process identity. The runtime takes an exclusive
|
||||||
// lock at startup; a SECOND start is refused, loudly,
|
// lock at startup; a SECOND start is refused, loudly,
|
||||||
// instead of two processes sharing one data dir.
|
// instead of two processes sharing one data dir.
|
||||||
|
// guards: <expr> — WHAT that singleton protects: an expression yielding
|
||||||
|
// the path of the guarded state directory, evaluated at
|
||||||
|
// startup. MANDATORY with `singleton:`, because a lock
|
||||||
|
// keyed on a program's NAME rather than on its STATE is
|
||||||
|
// not a guard — measured 2026-08-16, the name-keyed
|
||||||
|
// version refused unrelated instances (different data
|
||||||
|
// dirs) AND permitted concurrent ones (same data dir,
|
||||||
|
// different $TMPDIR). It is an expression and not a
|
||||||
|
// string so a program can point at the resolver that
|
||||||
|
// already OWNS the path (§18.4) instead of restating
|
||||||
|
// its default here, which would give the path two
|
||||||
|
// owners that can disagree.
|
||||||
// env NAME: T = "d" — one configuration entry. Its type and its default
|
// env NAME: T = "d" — one configuration entry. Its type and its default
|
||||||
// are declared ONCE, here, and resolved+validated
|
// are declared ONCE, here, and resolved+validated
|
||||||
// before main() body runs.
|
// before main() body runs.
|
||||||
@@ -1993,6 +2005,8 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
let p = expect(tokens, p, "LBrace")
|
let p = expect(tokens, p, "LBrace")
|
||||||
let singleton = ""
|
let singleton = ""
|
||||||
let has_singleton = false
|
let has_singleton = false
|
||||||
|
let guards_node = { "expr": "Str", "value": "" }
|
||||||
|
let has_guards = false
|
||||||
let entries = native_list_empty()
|
let entries = native_list_empty()
|
||||||
// Entry-scratch declared at loop-body level (not inside the branch) so
|
// Entry-scratch declared at loop-body level (not inside the branch) so
|
||||||
// that inner `let` forms compile to assignment rather than a C-scoped
|
// that inner `let` forms compile to assignment rather than a C-scoped
|
||||||
@@ -2048,13 +2062,26 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
"required": erequired
|
"required": erequired
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// scalar field: `name: "value"`
|
if str_eq(fname, "guards") {
|
||||||
let p = expect(tokens, p, "Colon")
|
// guards: <expr> — the STATE the singleton protects.
|
||||||
let fval = tok_value(tokens, p)
|
// Parsed as a full expression, not a string literal, so
|
||||||
let p = p + 1
|
// it can name the resolver that owns the path
|
||||||
if str_eq(fname, "singleton") {
|
// (`guards: engram_resolve_data_dir()`) rather than
|
||||||
let singleton = fval
|
// duplicating that resolver's default here.
|
||||||
let has_singleton = true
|
let p = expect(tokens, p, "Colon")
|
||||||
|
let g_r = parse_expr(tokens, p)
|
||||||
|
let guards_node = g_r["node"]
|
||||||
|
let p = g_r["pos"]
|
||||||
|
let has_guards = true
|
||||||
|
} else {
|
||||||
|
// scalar field: `name: "value"`
|
||||||
|
let p = expect(tokens, p, "Colon")
|
||||||
|
let fval = tok_value(tokens, p)
|
||||||
|
let p = p + 1
|
||||||
|
if str_eq(fname, "singleton") {
|
||||||
|
let singleton = fval
|
||||||
|
let has_singleton = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let k5 = tok_kind(tokens, p)
|
let k5 = tok_kind(tokens, p)
|
||||||
@@ -2070,6 +2097,8 @@ fn parse_stmt(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
"name": name,
|
"name": name,
|
||||||
"singleton": singleton,
|
"singleton": singleton,
|
||||||
"has_singleton": has_singleton,
|
"has_singleton": has_singleton,
|
||||||
|
"guards": guards_node,
|
||||||
|
"has_guards": has_guards,
|
||||||
"entries": entries
|
"entries": entries
|
||||||
}, p)
|
}, p)
|
||||||
}
|
}
|
||||||
|
|||||||
+183
-168
@@ -1,67 +1,33 @@
|
|||||||
// transduce.el — geometry as a first-class El value, and a realizer written
|
// transduce.el — transduction decomposes a signal into components and the
|
||||||
// in El. Runnable: this is the worked example for the transduce surface, and
|
// relations between them. Runnable: this is the worked example for the
|
||||||
// it doubles as an executable proof because it checks every claim it makes.
|
// transduce surface, and it exits non-zero if any claim in it stops being true.
|
||||||
//
|
//
|
||||||
// elc lang/examples/transduce.el > transduce.c
|
// elc lang/examples/transduce.el > transduce.c
|
||||||
// cc -std=c11 -O2 -I lang/runtime -o transduce transduce.c \
|
// cc -std=c11 -O2 -I lang/runtime -o transduce transduce.c \
|
||||||
// lang/runtime/el_runtime.c lang/runtime/el_seed.c \
|
// lang/runtime/el_runtime.c lang/runtime/el_seed.c \
|
||||||
// lang/runtime/engram_*.c -lcurl -lpthread -lm
|
// lang/runtime/engram_store.c lang/runtime/engram_vindex.c \
|
||||||
|
// lang/runtime/engram_cognition.c lang/runtime/engram_geometry.c \
|
||||||
|
// lang/runtime/engram_reason.c lang/runtime/engram_verify.c \
|
||||||
|
// -lcurl -lpthread -lm
|
||||||
// ./transduce # exits 0 only if every check passes
|
// ./transduce # exits 0 only if every check passes
|
||||||
//
|
//
|
||||||
// (A `test "..."` form of the same checks lives in
|
// It writes to an IN-MEMORY engram (leave ENGRAM_STORE unset) and contacts no
|
||||||
// lang/tests/native/test_transduce.el, for when the native harness is
|
// server. The same claims are asserted by the native harness in
|
||||||
// repaired — the shipped elc currently emits calls to __el_reg_count and
|
// lang/tests/native/test_transduce.el.
|
||||||
// friends without emitting their definitions, which breaks every native test
|
|
||||||
// equally, test_math.el included. Verified 2026-08-16, unrelated to this work.)
|
|
||||||
//
|
//
|
||||||
// WHY THIS EXISTS. Until 2026-08-16 no El ingest path could carry a vector:
|
// WHAT CHANGED, AND WHY IT MATTERS. #144 shipped
|
||||||
// nodes took text, and geometry was DERIVED from that text. Text was the
|
// `transduce(signal, modality) -> Geometry`: one vector per signal. That made
|
||||||
// mandatory entry medium, so any non-text modality had to be DESCRIBED in
|
// transduction a CONVERSION — take a thing, encode it, store a position — and
|
||||||
// prose first and the geometry we reasoned over was the geometry OF THE
|
// what a conversion returns is a fingerprint. A fingerprint can be matched and
|
||||||
// DESCRIPTION, not of the signal. Two things fix that, and both are shown
|
// ranked, and that is all it can ever do. It cannot be decomposed, cannot have
|
||||||
// below: geometry is a VALUE that carries its own width, and a REALIZER is an
|
// one part grounded while another is not, and cannot be contradicted in one
|
||||||
// ordinary El function — so admitting a new modality never requires a runtime
|
// part while holding in another, because it has no parts.
|
||||||
// patch.
|
|
||||||
//
|
//
|
||||||
// COMPARISON DISCIPLINE (measured, not stylistic): elc lowers `a == b`
|
// A song is not a point. It decomposes into pitch, interval, rhythm, harmonic
|
||||||
// numerically only when both operand NAMES are in the per-function int-name
|
// function — components, each with its own geometry, plus the relations among
|
||||||
// set that `let x: Int` populates. A bare `f(x) == 0` is not a registered
|
// them. THE SONG IS THE STRUCTURE OF THE RELATIONS. transduce now returns a
|
||||||
// name and lowers to str_eq — strcmp on two integers as pointers. `<` and `>`
|
// Manifold, and a realizer's job is to say what its modality's components ARE.
|
||||||
// lower directly with no inference, so truthiness is written `> 0` / `< 1`.
|
|
||||||
|
|
||||||
// ── A realizer, written entirely in El ──────────────────────────────────────
|
|
||||||
// Not in the runtime. Not known to the compiler. Registered by NAME and
|
|
||||||
// dispatched to through transduce(). That is the whole claim.
|
|
||||||
fn tone_realizer(signal: String) -> Geometry {
|
|
||||||
let g: Geometry = geometry_new(4)
|
|
||||||
let n: Int = str_len(signal)
|
|
||||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
|
||||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
|
||||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
|
||||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
|
||||||
g
|
|
||||||
}
|
|
||||||
|
|
||||||
// A second modality, to show the registry keys on modality rather than just
|
|
||||||
// returning whatever was registered last.
|
|
||||||
fn pulse_realizer(signal: String) -> Geometry {
|
|
||||||
let g: Geometry = geometry_new(2)
|
|
||||||
let a: Int = geometry_set(g, 0, 1.0)
|
|
||||||
let b: Int = geometry_set(g, 1, 0.0)
|
|
||||||
g
|
|
||||||
}
|
|
||||||
|
|
||||||
// A deliberately BROKEN realizer: returns something that is not a Geometry.
|
|
||||||
fn bogus_realizer(signal: String) -> Geometry {
|
|
||||||
return 12345
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fails FAST rather than accumulating a count, for a measured reason: a first
|
|
||||||
// cut wrote `let fails: Int = fails + check(...)` and `+` lowered to STRING
|
|
||||||
// CONCAT, because elc dispatches `+` on whether both operands are known-Int and
|
|
||||||
// a user-defined fn call is not — so the counter printed 4343632752, a pointer.
|
|
||||||
// Nothing was wrong with the checks; the tally was lying. Exiting at the first
|
|
||||||
// failure needs no arithmetic at all, so there is nothing left to get wrong.
|
|
||||||
fn check(ok: Int, label: String) -> Int {
|
fn check(ok: Int, label: String) -> Int {
|
||||||
if ok > 0 {
|
if ok > 0 {
|
||||||
println(" ok " + label)
|
println(" ok " + label)
|
||||||
@@ -84,128 +50,177 @@ fn eq_int(a: Int, b: Int) -> Int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── A DECOMPOSING realizer, written entirely in El ──────────────────────────
|
||||||
|
// "tone" signals are note letters, e.g. "CEG". This does NOT return one vector
|
||||||
|
// for the chord. It returns the PARTS — one component per note, one per
|
||||||
|
// interval between adjacent notes — and the relations that make those parts a
|
||||||
|
// chord rather than an unordered bag of pitches.
|
||||||
|
//
|
||||||
|
// The interval is deliberately a COMPONENT, not a field on a note. An interval
|
||||||
|
// is a thing with its own geometry belonging to neither endpoint; modelling it
|
||||||
|
// as an attribute of one of them is the same collapse, one level down.
|
||||||
|
fn tone_realizer(signal: String) -> Manifold {
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let n: Int = str_len(signal)
|
||||||
|
let i: Int = 0
|
||||||
|
while i < n {
|
||||||
|
let code: Int = str_char_code(signal, i)
|
||||||
|
let g: Geometry = geometry_new(2)
|
||||||
|
let s0: Int = geometry_set(g, 0, int_to_float(code))
|
||||||
|
let s1: Int = geometry_set(g, 1, int_to_float(i))
|
||||||
|
let idx: Int = manifold_add(m, "note:" + int_to_str(i), "pitch", g)
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
let j: Int = 1
|
||||||
|
while j < n {
|
||||||
|
let a: Int = str_char_code(signal, j - 1)
|
||||||
|
let b: Int = str_char_code(signal, j)
|
||||||
|
let lo: String = "note:" + int_to_str(j - 1)
|
||||||
|
let hi: String = "note:" + int_to_str(j)
|
||||||
|
let key: String = "interval:" + int_to_str(j - 1) + "-" + int_to_str(j)
|
||||||
|
let g: Geometry = geometry_new(1)
|
||||||
|
let s: Int = geometry_set(g, 0, int_to_float(b - a))
|
||||||
|
let idx: Int = manifold_add(m, key, "interval", g)
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
let e1: Int = manifold_relate(m, key, "spans", lo, 0.9)
|
||||||
|
let e2: Int = manifold_relate(m, key, "spans", hi, 0.9)
|
||||||
|
let e3: Int = manifold_relate(m, lo, "sounds_before", hi, 0.8)
|
||||||
|
j = j + 1
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
// #144's contract, kept as a control: one vector for the whole signal.
|
||||||
|
fn fingerprint_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
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Void {
|
fn main() -> Void {
|
||||||
println("geometry is a value that carries its own width")
|
|
||||||
let g8: Geometry = geometry_new(8)
|
|
||||||
let _c: Int = check(geometry_is(g8), "geometry_new returns a live Geometry")
|
|
||||||
let d8: Int = geometry_dim(g8)
|
|
||||||
let _c: Int = check(eq_int(d8, 8), "a Geometry carries its own width (8)")
|
|
||||||
let _c: Int = check(geometry_free(g8), "geometry_free reports what it did")
|
|
||||||
|
|
||||||
println("nonsense is refused — with no arbitrary max-dim bound")
|
|
||||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
|
||||||
// caller's CLAIM about a string's length. A value that carries its own
|
|
||||||
// width has nothing left to validate.
|
|
||||||
let z: Geometry = geometry_new(0)
|
|
||||||
let zi: Int = geometry_is(z)
|
|
||||||
let _c: Int = check(1 - zi, "dim 0 is not a geometry")
|
|
||||||
let ng: Geometry = geometry_new(-4)
|
|
||||||
let ngi: Int = geometry_is(ng)
|
|
||||||
let _c: Int = check(1 - ngi, "negative dim is not a geometry")
|
|
||||||
let nd: Int = geometry_dim(0)
|
|
||||||
let _c: Int = check(1 - nd, "geometry_dim of a non-geometry is 0, not a crash")
|
|
||||||
let nf: Int = geometry_free(0)
|
|
||||||
let _c: Int = check(1 - nf, "geometry_free of a non-geometry is a no-op")
|
|
||||||
|
|
||||||
println("components round-trip, and out-of-range is refused")
|
|
||||||
let g3: Geometry = geometry_new(3)
|
|
||||||
let s0: Int = geometry_set(g3, 0, 1.5)
|
|
||||||
let s1: Int = geometry_set(g3, 1, -2.5)
|
|
||||||
let _c: Int = check(s0, "set in range succeeds")
|
|
||||||
let oob: Int = geometry_set(g3, 3, 9.0)
|
|
||||||
let _c: Int = check(1 - oob, "set out of range is refused, not silently dropped")
|
|
||||||
let _c: Int = check(near(geometry_get(g3, 0), 1.5), "component 0 round-trips")
|
|
||||||
let _c: Int = check(near(geometry_get(g3, 1), -2.5), "component 1 round-trips (negative)")
|
|
||||||
let ff3: Int = geometry_free(g3)
|
|
||||||
|
|
||||||
println("hex is an EDGE adapter, and derives its own width")
|
|
||||||
// little-endian float32: 1.0 = 0000803f, 2.0 = 00000040
|
|
||||||
let gh: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
|
||||||
let _c: Int = check(geometry_is(gh), "valid hex decodes to a Geometry")
|
|
||||||
let dh: Int = geometry_dim(gh)
|
|
||||||
let _c: Int = check(eq_int(dh, 2), "width DERIVED from input, never supplied")
|
|
||||||
let _c: Int = check(near(geometry_get(gh, 0), 1.0), "first component decoded")
|
|
||||||
let _c: Int = check(near(geometry_get(gh, 1), 2.0), "second component decoded")
|
|
||||||
let back: String = geometry_to_f32le_hex(gh)
|
|
||||||
let _c: Int = check(str_eq(back, "0000803f00000040"), "hex round-trips exactly")
|
|
||||||
let ffh: Int = geometry_free(gh)
|
|
||||||
|
|
||||||
println("malformed hex is refused")
|
|
||||||
let he: Geometry = geometry_from_f32le_hex("")
|
|
||||||
let hei: Int = geometry_is(he)
|
|
||||||
let _c: Int = check(1 - hei, "empty hex is not a geometry")
|
|
||||||
let hr: Geometry = geometry_from_f32le_hex("0000803f0000")
|
|
||||||
let hri: Int = geometry_is(hr)
|
|
||||||
let _c: Int = check(1 - hri, "length not a multiple of 8 is refused")
|
|
||||||
let hn: Geometry = geometry_from_f32le_hex("zzzzzzzz")
|
|
||||||
let hni: Int = geometry_is(hn)
|
|
||||||
let _c: Int = check(1 - hni, "non-hex characters are refused")
|
|
||||||
|
|
||||||
println("a realizer declared in El is a first-class realizer")
|
println("a realizer declared in El is a first-class realizer")
|
||||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||||
let _c: Int = check(reg, "an El fn registers as a realizer BY NAME")
|
let _c: Int = check(reg, "an El fn registers as a realizer by name")
|
||||||
let _c: Int = check(realizer_has("tone"), "the modality now has an organ")
|
let _c: Int = check(realizer_has("tone"), "the modality now has an organ")
|
||||||
let gt: Geometry = transduce("aaa", "tone")
|
|
||||||
let _c: Int = check(geometry_is(gt), "transduce returns real geometry")
|
|
||||||
let dt: Int = geometry_dim(gt)
|
|
||||||
let _c: Int = check(eq_int(dt, 4), "the El realizer determined the width, not the runtime")
|
|
||||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
|
||||||
// actually reached the El function rather than a stub answering for it.
|
|
||||||
let _c: Int = check(near(geometry_get(gt, 0), 3.0), "the signal REACHED the El realizer")
|
|
||||||
let fft: Int = geometry_free(gt)
|
|
||||||
|
|
||||||
println("distinct signals transduce to distinct geometry")
|
println("transduction decomposes a signal into parts")
|
||||||
let g1: Geometry = transduce("aa", "tone")
|
let m: Manifold = transduce("CEG", "tone")
|
||||||
let g2: Geometry = transduce("aaaaa", "tone")
|
let _c: Int = check(manifold_is(m), "transduce returns a real Manifold")
|
||||||
let a1: Float = geometry_get(g1, 0)
|
let sz: Int = manifold_size(m)
|
||||||
let a2: Float = geometry_get(g2, 0)
|
let _c: Int = check(eq_int(sz, 5), "three notes and two intervals are five parts")
|
||||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
let rc: Int = manifold_rel_count(m)
|
||||||
let _c: Int = check(near(a2 - a1, 3.0), "different signals produce different geometry")
|
let _c: Int = check(eq_int(rc, 6), "and they stand in six stated relations")
|
||||||
let ff1: Int = geometry_free(g1)
|
|
||||||
let ff2: Int = geometry_free(g2)
|
|
||||||
|
|
||||||
println("the registry keys on modality")
|
println("every part is addressable BY KEY, which is what survives persistence")
|
||||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
let i_c: Int = manifold_index_of(m, "note:0")
|
||||||
let _c: Int = check(r2, "a second modality registers independently")
|
let _c: Int = check(1 - eq_int(i_c, -1), "the first note is addressable on its own")
|
||||||
let mt: Geometry = transduce("aaa", "tone")
|
let i_iv: Int = manifold_index_of(m, "interval:0-1")
|
||||||
let mp: Geometry = transduce("aaa", "pulse")
|
let _c: Int = check(1 - eq_int(i_iv, -1), "so is the interval between the first two")
|
||||||
let mdt: Int = geometry_dim(mt)
|
let miss: Int = manifold_index_of(m, "never_added")
|
||||||
let mdp: Int = geometry_dim(mp)
|
let _c: Int = check(eq_int(miss, -1), "an unknown key is -1, not component 0")
|
||||||
let _c: Int = check(eq_int(mdt, 4), "tone still routes to its own realizer")
|
|
||||||
let _c: Int = check(eq_int(mdp, 2), "pulse routes to a different realizer")
|
|
||||||
let ffm1: Int = geometry_free(mt)
|
|
||||||
let ffm2: Int = geometry_free(mp)
|
|
||||||
|
|
||||||
println("no organ is reported as no organ")
|
println("parts carry their own geometry, and may differ in width")
|
||||||
// A modality with no realizer must transduce to NOTHING. It must never
|
let gn: Geometry = manifold_geometry(m, i_c)
|
||||||
// fall back to embedding a description of the signal and calling that
|
let _c: Int = check(eq_int(geometry_dim(gn), 2), "a note component is 2 wide")
|
||||||
// perception — that silent substitution is the defect this all exists to end.
|
let _c: Int = check(near(geometry_get(gn, 0), 67.0), "and it is C — the signal reached the realizer")
|
||||||
let eh: Int = realizer_has("echolocation")
|
let gi: Geometry = manifold_geometry(m, i_iv)
|
||||||
let _c: Int = check(1 - eh, "unregistered modality has no organ")
|
let _c: Int = check(eq_int(geometry_dim(gi), 1), "an interval component is 1 wide")
|
||||||
let ge: Geometry = transduce("anything", "echolocation")
|
// A single vector per signal cannot represent parts of unequal width at all.
|
||||||
let gei: Int = geometry_is(ge)
|
let _c: Int = check(near(geometry_get(gi, 0), 2.0), "C to E is two semitones")
|
||||||
let _c: Int = check(1 - gei, "no realizer means NO geometry, not fake geometry")
|
let f1: Int = geometry_free(gn)
|
||||||
|
let f2: Int = geometry_free(gi)
|
||||||
|
|
||||||
println("an unresolvable realizer name fails at WIRING time")
|
println("the relations are content no single part carries")
|
||||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
// That "2" above is not a property of C and not a property of E. It exists
|
||||||
let _c: Int = check(1 - bad, "unresolvable realizer name is a registration failure")
|
// only BETWEEN them, so a representation with no relations cannot hold it.
|
||||||
let gh2: Int = realizer_has("ghost")
|
let spans: Int = 0
|
||||||
let _c: Int = check(1 - gh2, "and nothing gets registered")
|
let k: Int = 0
|
||||||
|
while k < rc {
|
||||||
|
if str_eq(manifold_rel_name(m, k), "spans") {
|
||||||
|
if str_eq(manifold_rel_from(m, k), "interval:0-1") { spans = spans + 1 }
|
||||||
|
}
|
||||||
|
k = k + 1
|
||||||
|
}
|
||||||
|
let _c: Int = check(eq_int(spans, 2), "the interval is wired to both notes it spans")
|
||||||
|
|
||||||
println("a realizer returning non-geometry transduces nothing")
|
println("relation weight IS the grounding (correspondence-and-censorship §1)")
|
||||||
let rb: Int = realizer_register("bogus", "bogus_realizer")
|
let wk: Int = 0
|
||||||
let _c: Int = check(rb, "the symbol resolves, so registration succeeds")
|
let found: Int = 0
|
||||||
let gb: Geometry = transduce("x", "bogus")
|
while wk < rc {
|
||||||
let gbi: Int = geometry_is(gb)
|
if str_eq(manifold_rel_name(m, wk), "sounds_before") {
|
||||||
let _c: Int = check(1 - gbi, "contract enforced at the boundary: nothing handed back")
|
if near(manifold_rel_weight(m, wk), 0.8) > 0 { found = 1 }
|
||||||
|
}
|
||||||
|
wk = wk + 1
|
||||||
|
}
|
||||||
|
let _c: Int = check(found, "the ordering relation carries the weight its realizer stated")
|
||||||
|
|
||||||
println("norm lets a caller check a realizer emitted signal, not zeros")
|
println("the decomposition persists as real, separately addressable nodes")
|
||||||
let gn: Geometry = geometry_new(2)
|
let ids: [String] = el_list_empty()
|
||||||
let _c: Int = check(near(geometry_norm(gn), 0.0), "a fresh geometry is zero — norm says so")
|
let n0: Int = engram_node_count()
|
||||||
let n0: Int = geometry_set(gn, 0, 3.0)
|
let e0: Int = engram_edge_count()
|
||||||
let n1: Int = geometry_set(gn, 1, 4.0)
|
let pi: Int = 0
|
||||||
let _c: Int = check(near(geometry_norm(gn), 5.0), "3-4-5: norm is 5")
|
while pi < sz {
|
||||||
let ffn: Int = geometry_free(gn)
|
let key: String = manifold_key(m, pi)
|
||||||
|
let g: Geometry = manifold_geometry(m, pi)
|
||||||
|
let id: String = engram_node("component " + key, "Concept", 0.6)
|
||||||
|
let att: Int = node_attach_geometry(id, g)
|
||||||
|
ids = el_list_append(ids, id)
|
||||||
|
let ff: Int = geometry_free(g)
|
||||||
|
pi = pi + 1
|
||||||
|
}
|
||||||
|
let ri: Int = 0
|
||||||
|
while ri < rc {
|
||||||
|
let fi: Int = manifold_index_of(m, manifold_rel_from(m, ri))
|
||||||
|
let ti: Int = manifold_index_of(m, manifold_rel_to(m, ri))
|
||||||
|
engram_connect(el_list_get(ids, fi), el_list_get(ids, ti),
|
||||||
|
manifold_rel_weight(m, ri), manifold_rel_name(m, ri))
|
||||||
|
ri = ri + 1
|
||||||
|
}
|
||||||
|
let _c: Int = check(eq_int(engram_node_count() - n0, 5), "one signal became five nodes")
|
||||||
|
let _c: Int = check(eq_int(engram_edge_count() - e0, 6), "and six edges between them")
|
||||||
|
|
||||||
|
println("each part's geometry is independently readable back off its node")
|
||||||
|
let id_c: String = el_list_get(ids, manifold_index_of(m, "note:0"))
|
||||||
|
let id_iv: String = el_list_get(ids, manifold_index_of(m, "interval:0-1"))
|
||||||
|
let _c: Int = check(eq_int(node_geometry_dim(id_c), 2), "note:0 node carries a 2-wide geometry")
|
||||||
|
let _c: Int = check(eq_int(node_geometry_dim(id_iv), 1), "interval:0-1 node carries a 1-wide one")
|
||||||
|
|
||||||
|
println("one part can be grounded without touching its siblings")
|
||||||
|
let ear: String = engram_node("evidence: heard a C in the recording", "Memory", 0.7)
|
||||||
|
engram_connect(ear, id_c, 0.95, "corroborates")
|
||||||
|
let _c: Int = check(engram_edge_between(ear, id_c), "evidence attaches to note:0 specifically")
|
||||||
|
let id_g: String = el_list_get(ids, manifold_index_of(m, "note:2"))
|
||||||
|
let _c: Int = check(1 - engram_edge_between(ear, id_g), "and NOT to note:2 — the sibling is untouched")
|
||||||
|
// This is the whole gain, and it is impossible with a fingerprint: with one
|
||||||
|
// node per signal, "the C is corroborated" and "the G is not" have the same
|
||||||
|
// grounding target and cannot both be recorded.
|
||||||
|
let _c: Int = check(eq_int(node_geometry_dim(id_g), 2), "note:2 geometry is intact regardless")
|
||||||
|
|
||||||
|
println("a fingerprint realizer transduces NOTHING")
|
||||||
|
// #144's contract exactly: signal in, one Geometry out. It resolves, so the
|
||||||
|
// organ is present — but it does not decompose, so it does not transduce.
|
||||||
|
// "No organ" and "an organ that only fingerprints" must not look alike.
|
||||||
|
let rf: Int = realizer_register("fingerprint", "fingerprint_realizer")
|
||||||
|
let _c: Int = check(rf, "the symbol resolves, so registration succeeds")
|
||||||
|
let mf: Manifold = transduce("x", "fingerprint")
|
||||||
|
let _c: Int = check(1 - manifold_is(mf), "a single vector is not a transduction")
|
||||||
|
|
||||||
|
println("the one-part case is a size-one manifold, not a bare vector")
|
||||||
|
let g1: Geometry = geometry_new(3)
|
||||||
|
let s1: Int = geometry_set(g1, 0, 5.0)
|
||||||
|
let ms: Manifold = manifold_single("level", "scalar", g1)
|
||||||
|
let _c: Int = check(manifold_is(ms), "manifold_single yields a real Manifold")
|
||||||
|
let _c: Int = check(eq_int(manifold_size(ms), 1), "of size one — visibly degenerate, not hidden")
|
||||||
|
let fg: Int = geometry_free(g1)
|
||||||
|
let fs: Int = manifold_free(ms)
|
||||||
|
|
||||||
|
println("no organ is still reported as no organ")
|
||||||
|
let me: Manifold = transduce("anything", "echolocation")
|
||||||
|
let _c: Int = check(1 - manifold_is(me), "no realizer means no manifold, not a fake one")
|
||||||
|
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
|
||||||
// Reaching here means nothing called exit(1) along the way.
|
// Reaching here means nothing called exit(1) along the way.
|
||||||
println("")
|
println("")
|
||||||
|
|||||||
+624
-44
@@ -40,6 +40,7 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
|
#include <signal.h> /* SIGPIPE disposition: a hung-up client must not kill us */
|
||||||
#include <dlfcn.h> /* dlsym for http_set_handler fallback */
|
#include <dlfcn.h> /* dlsym for http_set_handler fallback */
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -1172,6 +1173,128 @@ void http_set_handler(el_val_t name) {
|
|||||||
pthread_mutex_unlock(&_http_handler_mu);
|
pthread_mutex_unlock(&_http_handler_mu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Ambient consolidation: dreaming ────────────────────────────────────────
|
||||||
|
*
|
||||||
|
* Dreaming is not sleep, and it is not scheduled. A brain has no cron job.
|
||||||
|
* The default mode network is ANTICORRELATED WITH TASK ENGAGEMENT: attention
|
||||||
|
* drops, it activates — hundreds of times a day, for seconds at a time.
|
||||||
|
* Daydreaming and sleep-dreaming are one process at different depths, and the
|
||||||
|
* depth is set by how much capacity is unclaimed, not by a time of day.
|
||||||
|
*
|
||||||
|
* WHY THIS EXISTS (2026-08-16). Consolidation had no owner, so it was
|
||||||
|
* implemented at every site that needed a piece of it — measured: soul's
|
||||||
|
* in-process awareness loop, three POST beats on the engram, a 600s ticker,
|
||||||
|
* two resident Python services, and three cron entries at 23:55 / 06:00 /
|
||||||
|
* 08:30. That last trio is a sleep cycle written as crontab. Seven systems
|
||||||
|
* dreaming into one graph with no owner for dreaming is what crashed soul on
|
||||||
|
* this date; the contention was the symptom of the missing owner.
|
||||||
|
*
|
||||||
|
* Every ticker is the diagnostic. A StartInterval, an Hour/Minute, a
|
||||||
|
* POST-to-beat — each marks a place where an intrinsic rhythm was replaced by
|
||||||
|
* an external clock, which is a supervisor invented for something that should
|
||||||
|
* be a property of the substrate.
|
||||||
|
*
|
||||||
|
* The engagement signal already existed and needed no invention:
|
||||||
|
* _http_conn_active under _http_conn_mu is exactly "capacity currently
|
||||||
|
* claimed." The dreamer waits for it to reach zero and yields the moment it
|
||||||
|
* does not. That is the anticorrelation, literally rather than by analogy.
|
||||||
|
*
|
||||||
|
* CONTRACT: the handler performs ONE step and returns. The runtime cannot
|
||||||
|
* preempt El code, so interruptibility is at step granularity — a step must
|
||||||
|
* be small enough that a request arriving mid-step is not made to wait. It
|
||||||
|
* returns non-zero if it did work. Returning zero means "nothing to
|
||||||
|
* consolidate," and the dreamer then blocks until activity changes rather
|
||||||
|
* than spinning. There is no timer anywhere in this file for this purpose,
|
||||||
|
* and adding one would be the defect described above.
|
||||||
|
*
|
||||||
|
* `depth` is derived from CONTINUOUS unclaimed time: a brief gap affords a
|
||||||
|
* shallow recombination; a long quiet affords a deep one. Same process. Sleep
|
||||||
|
* is where unclaimed capacity is greatest, not where the process lives. */
|
||||||
|
typedef el_val_t (*dream_fn)(el_val_t depth);
|
||||||
|
static char* _dream_handler = NULL;
|
||||||
|
static int _dream_started = 0;
|
||||||
|
|
||||||
|
static int64_t dream_now_ms(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
#if defined(CLOCK_MONOTONIC)
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
#else
|
||||||
|
clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
|
#endif
|
||||||
|
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
static dream_fn dream_lookup(void) {
|
||||||
|
dream_fn out = NULL;
|
||||||
|
pthread_mutex_lock(&_http_handler_mu);
|
||||||
|
if (_dream_handler && *_dream_handler)
|
||||||
|
out = (dream_fn)dlsym(RTLD_DEFAULT, _dream_handler);
|
||||||
|
pthread_mutex_unlock(&_http_handler_mu);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void* dream_loop(void* unused) {
|
||||||
|
(void)unused;
|
||||||
|
int64_t idle_since = 0;
|
||||||
|
for (;;) {
|
||||||
|
/* Wait for unclaimed capacity. Any engagement resets the depth clock:
|
||||||
|
* depth reflects CONTINUOUS quiet, so an interruption starts it over. */
|
||||||
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
|
while (_http_conn_active > 0) {
|
||||||
|
idle_since = 0;
|
||||||
|
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
|
|
||||||
|
int64_t now = dream_now_ms();
|
||||||
|
if (idle_since == 0) idle_since = now;
|
||||||
|
int64_t quiet = now - idle_since;
|
||||||
|
|
||||||
|
/* Depth from unclaimed capacity. Not a schedule — a gradient. */
|
||||||
|
int depth = quiet < 1000 ? 1 /* a gap between requests */
|
||||||
|
: quiet < 30000 ? 2 /* a lull */
|
||||||
|
: quiet < 300000 ? 3 /* sustained quiet */
|
||||||
|
: 4; /* deep: the "sleep" case */
|
||||||
|
|
||||||
|
dream_fn fn = dream_lookup();
|
||||||
|
if (!fn) return NULL; /* handler vanished: stop, do not spin */
|
||||||
|
|
||||||
|
el_val_t did_work = fn((el_val_t)depth);
|
||||||
|
|
||||||
|
if (!(int64_t)did_work) {
|
||||||
|
/* Nothing to consolidate. Do NOT poll — block until engagement
|
||||||
|
* changes. If there is nothing to dream about, wait for something
|
||||||
|
* to happen rather than asking again on a timer. */
|
||||||
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
|
while (_http_conn_active == 0)
|
||||||
|
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||||
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
|
idle_since = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dream_set_handler(name) — register the consolidation step and start
|
||||||
|
* dreaming. Resolves by dlsym against the running binary, the same mechanism
|
||||||
|
* http_set_handler uses: every El `fn name(...)` compiles to a global C symbol
|
||||||
|
* with that exact name. Inert until called, so a program that never registers
|
||||||
|
* one simply never dreams and pays nothing. */
|
||||||
|
void dream_set_handler(el_val_t name) {
|
||||||
|
const char* n = EL_CSTR(name);
|
||||||
|
pthread_mutex_lock(&_http_handler_mu);
|
||||||
|
free(_dream_handler);
|
||||||
|
_dream_handler = el_strdup(n ? n : "");
|
||||||
|
int start = (!_dream_started && n && *n && dlsym(RTLD_DEFAULT, n) != NULL);
|
||||||
|
if (start) _dream_started = 1;
|
||||||
|
pthread_mutex_unlock(&_http_handler_mu);
|
||||||
|
if (start) {
|
||||||
|
pthread_t tid;
|
||||||
|
if (pthread_create(&tid, NULL, dream_loop, NULL) == 0) pthread_detach(tid);
|
||||||
|
else { pthread_mutex_lock(&_http_handler_mu); _dream_started = 0; pthread_mutex_unlock(&_http_handler_mu); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static http_handler_fn http_lookup_active(void) {
|
static http_handler_fn http_lookup_active(void) {
|
||||||
http_handler_fn out = NULL;
|
http_handler_fn out = NULL;
|
||||||
pthread_mutex_lock(&_http_handler_mu);
|
pthread_mutex_lock(&_http_handler_mu);
|
||||||
@@ -1335,10 +1458,63 @@ static const char* http_reason_phrase(int status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Best-effort send with retry on partial writes. */
|
/* A DISCONNECTING CLIENT MUST NOT KILL THE SERVER (2026-08-16).
|
||||||
|
*
|
||||||
|
* There was no SIGPIPE handling anywhere in this runtime: no signal disposition,
|
||||||
|
* no MSG_NOSIGNAL, no SO_NOSIGPIPE, and send() called with bare flags. The
|
||||||
|
* default disposition of SIGPIPE is to TERMINATE THE PROCESS, so any client that
|
||||||
|
* hung up mid-response — a curl that hit its timeout, a browser tab closed
|
||||||
|
* during a large read, a proxy giving up — took the whole engram down with it.
|
||||||
|
*
|
||||||
|
* Measured on the live instance: 18 boots in the log, and `launchctl list`
|
||||||
|
* reporting the previous exit for ai.neuron.engram as -13, i.e. killed by
|
||||||
|
* signal 13 = SIGPIPE. Reproduced by the cause: pulling /api/nodes/list (26 MB)
|
||||||
|
* with a client-side timeout. launchd's KeepAlive then restarts it, so the
|
||||||
|
* failure looks like a mysterious restart rather than a crash, and the graph
|
||||||
|
* silently reloads under whatever was mid-flight.
|
||||||
|
*
|
||||||
|
* This is an exemption in the §8 sense: the write never checked whether the
|
||||||
|
* peer was still there, and the consequence of not checking was fatal rather
|
||||||
|
* than merely wrong.
|
||||||
|
*
|
||||||
|
* Two layers, because neither alone is portable:
|
||||||
|
* - SO_NOSIGPIPE per socket (Darwin/BSD) and MSG_NOSIGNAL per send (Linux),
|
||||||
|
* so the signal is never raised for socket writes in the first place.
|
||||||
|
* - A process-wide SIG_IGN as the backstop for platforms/paths with neither,
|
||||||
|
* installed once and idempotent. With the signal ignored, send() returns
|
||||||
|
* -1/EPIPE and the existing error path closes the connection. */
|
||||||
|
#ifndef MSG_NOSIGNAL
|
||||||
|
#define MSG_NOSIGNAL 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void el_ignore_sigpipe_once(void) {
|
||||||
|
static int done = 0;
|
||||||
|
if (done) return;
|
||||||
|
done = 1;
|
||||||
|
#ifndef _WIN32
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-socket suppression where the platform offers it. Best-effort: a failure
|
||||||
|
* here is not fatal because el_ignore_sigpipe_once() already covers the case. */
|
||||||
|
static void el_sock_nosigpipe(int fd) {
|
||||||
|
#if defined(SO_NOSIGPIPE)
|
||||||
|
int on = 1;
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on));
|
||||||
|
#else
|
||||||
|
(void)fd;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Best-effort send with retry on partial writes. EPIPE/ECONNRESET are a client
|
||||||
|
* that left, not a server fault: return -1 so the caller closes the connection,
|
||||||
|
* and never let it reach the process as a signal. */
|
||||||
static int http_send_all(int fd, const char* p, size_t left) {
|
static int http_send_all(int fd, const char* p, size_t left) {
|
||||||
|
el_ignore_sigpipe_once();
|
||||||
while (left > 0) {
|
while (left > 0) {
|
||||||
ssize_t w = send(fd, p, left, 0);
|
ssize_t w = send(fd, p, left, MSG_NOSIGNAL);
|
||||||
|
if (w < 0 && errno == EINTR) continue;
|
||||||
if (w <= 0) return -1;
|
if (w <= 0) return -1;
|
||||||
p += w; left -= (size_t)w;
|
p += w; left -= (size_t)w;
|
||||||
}
|
}
|
||||||
@@ -1738,7 +1914,12 @@ static void* http_worker(void* arg) {
|
|||||||
/* release a slot */
|
/* release a slot */
|
||||||
pthread_mutex_lock(&_http_conn_mu);
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
_http_conn_active--;
|
_http_conn_active--;
|
||||||
pthread_cond_signal(&_http_conn_cv);
|
/* BROADCAST, not signal (2026-08-16): the ambient consolidation thread
|
||||||
|
* waits on this same condvar for _http_conn_active == 0. cond_signal wakes
|
||||||
|
* exactly one waiter, so the accept loop could take every wake and starve
|
||||||
|
* the dreamer indefinitely. Both wait sites re-check their predicate in a
|
||||||
|
* while loop, so broadcasting is safe. */
|
||||||
|
pthread_cond_broadcast(&_http_conn_cv);
|
||||||
pthread_mutex_unlock(&_http_conn_mu);
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -1788,6 +1969,7 @@ void http_serve(el_val_t port, el_val_t handler) {
|
|||||||
pthread_mutex_unlock(&_http_conn_mu);
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||||
if (!arg) { el_closesocket(cfd); continue; }
|
if (!arg) { el_closesocket(cfd); continue; }
|
||||||
|
el_sock_nosigpipe(cfd);
|
||||||
arg->fd = cfd;
|
arg->fd = cfd;
|
||||||
pthread_t tid;
|
pthread_t tid;
|
||||||
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
||||||
@@ -1834,6 +2016,7 @@ static void* _http_serve_async_loop(void* raw) {
|
|||||||
pthread_mutex_unlock(&_http_conn_mu);
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||||
if (!arg) { close(cfd); continue; }
|
if (!arg) { close(cfd); continue; }
|
||||||
|
el_sock_nosigpipe(cfd);
|
||||||
arg->fd = cfd;
|
arg->fd = cfd;
|
||||||
pthread_t tid;
|
pthread_t tid;
|
||||||
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
||||||
@@ -2083,7 +2266,12 @@ static void* http_worker_v2(void* arg) {
|
|||||||
el_closesocket(fd);
|
el_closesocket(fd);
|
||||||
pthread_mutex_lock(&_http_conn_mu);
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
_http_conn_active--;
|
_http_conn_active--;
|
||||||
pthread_cond_signal(&_http_conn_cv);
|
/* BROADCAST, not signal (2026-08-16): the ambient consolidation thread
|
||||||
|
* waits on this same condvar for _http_conn_active == 0. cond_signal wakes
|
||||||
|
* exactly one waiter, so the accept loop could take every wake and starve
|
||||||
|
* the dreamer indefinitely. Both wait sites re-check their predicate in a
|
||||||
|
* while loop, so broadcasting is safe. */
|
||||||
|
pthread_cond_broadcast(&_http_conn_cv);
|
||||||
pthread_mutex_unlock(&_http_conn_mu);
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -2134,6 +2322,7 @@ void http_serve_v2(el_val_t port, el_val_t handler) {
|
|||||||
pthread_mutex_unlock(&_http_conn_mu);
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||||
if (!arg) { el_closesocket(cfd); continue; }
|
if (!arg) { el_closesocket(cfd); continue; }
|
||||||
|
el_sock_nosigpipe(cfd);
|
||||||
arg->fd = cfd;
|
arg->fd = cfd;
|
||||||
pthread_t tid;
|
pthread_t tid;
|
||||||
if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) {
|
if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) {
|
||||||
@@ -6233,22 +6422,319 @@ el_val_t geometry_to_f32le_hex(el_val_t g) {
|
|||||||
return (el_val_t)(uintptr_t)out;
|
return (el_val_t)(uintptr_t)out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Manifold: a transduced signal is a SUBGRAPH, not a point ────────────────
|
||||||
|
*
|
||||||
|
* WHAT THIS CORRECTS. #144 gave transduction a home in the language and got
|
||||||
|
* the DISPATCH right — realizers declared in El, resolved by name, no runtime
|
||||||
|
* patch per modality. It got the OUTPUT TYPE wrong.
|
||||||
|
* `transduce(signal, modality) -> Geometry` yields one vector per signal, and
|
||||||
|
* one vector is a FINGERPRINT. A fingerprint can be matched and it can be
|
||||||
|
* ranked; that is the whole of what it can ever do. It cannot be decomposed,
|
||||||
|
* cannot be partially grounded, and cannot be contradicted in one part while
|
||||||
|
* holding in another — because it has no parts.
|
||||||
|
*
|
||||||
|
* A song is not a point. It decomposes into pitch, interval, rhythm, harmonic
|
||||||
|
* function, phrase structure: components, each with its own geometry, plus the
|
||||||
|
* relations between them. THE SONG IS THE STRUCTURE OF THE RELATIONS. A
|
||||||
|
* transducer that returns a single vector has not transduced the song, it has
|
||||||
|
* summarised it — and the summary discards precisely the thing that made the
|
||||||
|
* song reasonable-about.
|
||||||
|
*
|
||||||
|
* So transduction produces a MANIFOLD: named components, each carrying its own
|
||||||
|
* geometry, and typed weighted relations among them. Signal in, subgraph out.
|
||||||
|
* Conversion was never the operation.
|
||||||
|
*
|
||||||
|
* COMPONENTS ARE ADDRESSED BY KEY, NEVER BY INDEX. The key is what survives
|
||||||
|
* persistence: a component becomes a node, and that node is separately
|
||||||
|
* groundable precisely because it is separately NAMED. Index-addressing would
|
||||||
|
* make a grounding reference positional, and a positional reference into a
|
||||||
|
* decomposition whose arity can change is not a reference at all. Duplicate
|
||||||
|
* keys are refused for the same reason: two components answering to one name
|
||||||
|
* is not an addressing scheme.
|
||||||
|
*
|
||||||
|
* RELATION WEIGHT IS THE GROUNDING — there is no second field and no score to
|
||||||
|
* compute. Per correspondence-and-censorship.md §1, grounding is an attribute
|
||||||
|
* of the edge and it IS the hebbian weight; a grounding subsystem is a
|
||||||
|
* supervisor invented for something that should be a property of the
|
||||||
|
* substrate. A relation emitted by a realizer therefore arrives with its
|
||||||
|
* grounding already on it and moves thereafter by use and by decay (§4: change
|
||||||
|
* is not a consequence of use, it is use). Nothing in here computes a
|
||||||
|
* grounding, and nothing observes one.
|
||||||
|
*
|
||||||
|
* A relation naming an endpoint that does not exist is REFUSED, not dropped. A
|
||||||
|
* decomposition that silently loses edges is indistinguishable from one that
|
||||||
|
* never had them — the same class of defect #141 exists to end.
|
||||||
|
*
|
||||||
|
* OWNERSHIP mirrors Geometry exactly. A Manifold is owned by the El caller and
|
||||||
|
* released with manifold_free. manifold_add COPIES the geometry handed to it,
|
||||||
|
* so a caller may free its own vector immediately and no component's geometry
|
||||||
|
* is ever aliased. Keys, roles and relation strings are _persist copies, NOT
|
||||||
|
* arena copies: a Manifold outlives the request arena that built it (a
|
||||||
|
* realizer can be invoked from inside a handler), so an arena-tracked key
|
||||||
|
* would dangle at el_request_end. manifold_free owns their release.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define EL_MAGIC_MFLD 0xE1608E02u
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char* key; /* addressable name, unique within the manifold */
|
||||||
|
char* role; /* what KIND of component this is, realizer's vocabulary */
|
||||||
|
ElGeometry* g; /* owned copy; never aliases the caller's value */
|
||||||
|
} ElComponent;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char* from; /* component key */
|
||||||
|
char* rel; /* relation name */
|
||||||
|
char* to; /* component key */
|
||||||
|
double weight; /* the grounding; §1 — one quantity, not two fields */
|
||||||
|
} ElRelation;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ElHeader hdr;
|
||||||
|
ElComponent* comps;
|
||||||
|
size_t ncomp, capcomp;
|
||||||
|
ElRelation* rels;
|
||||||
|
size_t nrel, caprel;
|
||||||
|
} ElManifold;
|
||||||
|
|
||||||
|
/* Resolve an el_val_t to a live Manifold, or NULL. Every accessor goes through
|
||||||
|
* this, so a stale/foreign/zero value is a clean 0-return, never a deref. */
|
||||||
|
static ElManifold* mfld_of(el_val_t m) {
|
||||||
|
if (!looks_like_heap_obj(m)) return NULL;
|
||||||
|
ElManifold* p = (ElManifold*)(uintptr_t)m;
|
||||||
|
if (p->hdr.magic != EL_MAGIC_MFLD) return NULL;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int mfld_find(ElManifold* p, const char* key) {
|
||||||
|
for (size_t i = 0; i < p->ncomp; i++)
|
||||||
|
if (strcmp(p->comps[i].key, key) == 0) return (int)i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_new(void) {
|
||||||
|
ElManifold* p = (ElManifold*)calloc(1, sizeof(ElManifold));
|
||||||
|
if (!p) return (el_val_t)0;
|
||||||
|
p->hdr.magic = EL_MAGIC_MFLD;
|
||||||
|
p->hdr.refcount = 1;
|
||||||
|
return (el_val_t)(uintptr_t)p;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_is(el_val_t m) {
|
||||||
|
return mfld_of(m) ? (el_val_t)1 : (el_val_t)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* manifold_add — add one COMPONENT: a named part with its own geometry.
|
||||||
|
* Returns the component's index, or -1 on any refusal. Refusals are real and
|
||||||
|
* distinct: an empty key (unaddressable), a duplicate key (ambiguous
|
||||||
|
* addressing), a value that is not a live Geometry (a part with no geometry is
|
||||||
|
* not a part). Each is a caller error worth surfacing at the point of the
|
||||||
|
* mistake rather than as a missing node three layers downstream. */
|
||||||
|
el_val_t manifold_add(el_val_t m, el_val_t key, el_val_t role, el_val_t g) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
if (!p) return (el_val_t)(int64_t)-1;
|
||||||
|
const char* k = EL_CSTR(key);
|
||||||
|
const char* r = EL_CSTR(role);
|
||||||
|
if (!k || !*k) return (el_val_t)(int64_t)-1;
|
||||||
|
if (!r) r = "";
|
||||||
|
ElGeometry* src = geom_of(g);
|
||||||
|
if (!src || src->dim <= 0) return (el_val_t)(int64_t)-1;
|
||||||
|
if (mfld_find(p, k) >= 0) return (el_val_t)(int64_t)-1; /* duplicate key */
|
||||||
|
|
||||||
|
if (p->ncomp == p->capcomp) {
|
||||||
|
size_t nc = p->capcomp ? p->capcomp * 2 : 8;
|
||||||
|
ElComponent* nb = (ElComponent*)realloc(p->comps, nc * sizeof(ElComponent));
|
||||||
|
if (!nb) return (el_val_t)(int64_t)-1;
|
||||||
|
p->comps = nb; p->capcomp = nc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COPY the payload — a component's geometry must not alias the caller's. */
|
||||||
|
ElGeometry* cp = (ElGeometry*)malloc(sizeof(ElGeometry));
|
||||||
|
if (!cp) return (el_val_t)(int64_t)-1;
|
||||||
|
cp->v = (float*)malloc(sizeof(float) * (size_t)src->dim);
|
||||||
|
if (!cp->v) { free(cp); return (el_val_t)(int64_t)-1; }
|
||||||
|
memcpy(cp->v, src->v, sizeof(float) * (size_t)src->dim);
|
||||||
|
cp->hdr.magic = EL_MAGIC_GEOM;
|
||||||
|
cp->hdr.refcount = 1;
|
||||||
|
cp->dim = src->dim;
|
||||||
|
|
||||||
|
p->comps[p->ncomp].key = el_strdup_persist(k);
|
||||||
|
p->comps[p->ncomp].role = el_strdup_persist(r);
|
||||||
|
p->comps[p->ncomp].g = cp;
|
||||||
|
p->ncomp++;
|
||||||
|
return (el_val_t)(int64_t)(p->ncomp - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* manifold_relate — state a relation BETWEEN two components. This is the part
|
||||||
|
* that carries the meaning: the components are the parts, the relations are
|
||||||
|
* what the thing IS.
|
||||||
|
*
|
||||||
|
* Both endpoints must already exist. An edge to a name that was never added is
|
||||||
|
* refused with 0, never silently discarded — see the header note. */
|
||||||
|
el_val_t manifold_relate(el_val_t m, el_val_t from, el_val_t rel,
|
||||||
|
el_val_t to, el_val_t weight) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
if (!p) return (el_val_t)0;
|
||||||
|
const char* f = EL_CSTR(from);
|
||||||
|
const char* r = EL_CSTR(rel);
|
||||||
|
const char* t = EL_CSTR(to);
|
||||||
|
if (!f || !*f || !r || !*r || !t || !*t) return (el_val_t)0;
|
||||||
|
if (mfld_find(p, f) < 0) return (el_val_t)0;
|
||||||
|
if (mfld_find(p, t) < 0) return (el_val_t)0;
|
||||||
|
|
||||||
|
if (p->nrel == p->caprel) {
|
||||||
|
size_t nc = p->caprel ? p->caprel * 2 : 8;
|
||||||
|
ElRelation* nb = (ElRelation*)realloc(p->rels, nc * sizeof(ElRelation));
|
||||||
|
if (!nb) return (el_val_t)0;
|
||||||
|
p->rels = nb; p->caprel = nc;
|
||||||
|
}
|
||||||
|
p->rels[p->nrel].from = el_strdup_persist(f);
|
||||||
|
p->rels[p->nrel].rel = el_strdup_persist(r);
|
||||||
|
p->rels[p->nrel].to = el_strdup_persist(t);
|
||||||
|
p->rels[p->nrel].weight = el_to_float(weight);
|
||||||
|
p->nrel++;
|
||||||
|
return (el_val_t)1;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_size(el_val_t m) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
return p ? (el_val_t)(int64_t)p->ncomp : (el_val_t)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_rel_count(el_val_t m) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
return p ? (el_val_t)(int64_t)p->nrel : (el_val_t)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Index of a component BY KEY, or -1. This is the addressability primitive:
|
||||||
|
* everything downstream that wants to ground, weight or contradict one part
|
||||||
|
* finds it through here. */
|
||||||
|
el_val_t manifold_index_of(el_val_t m, el_val_t key) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
const char* k = EL_CSTR(key);
|
||||||
|
if (!p || !k || !*k) return (el_val_t)(int64_t)-1;
|
||||||
|
return (el_val_t)(int64_t)mfld_find(p, k);
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_key(el_val_t m, el_val_t i) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
int64_t k = (int64_t)i;
|
||||||
|
if (!p || k < 0 || k >= (int64_t)p->ncomp) return el_wrap_str(el_strdup(""));
|
||||||
|
return el_wrap_str(el_strdup(p->comps[k].key));
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_role(el_val_t m, el_val_t i) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
int64_t k = (int64_t)i;
|
||||||
|
if (!p || k < 0 || k >= (int64_t)p->ncomp) return el_wrap_str(el_strdup(""));
|
||||||
|
return el_wrap_str(el_strdup(p->comps[k].role));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* manifold_geometry — the geometry OF ONE COMPONENT, as a fresh Geometry the
|
||||||
|
* caller owns and frees. A borrowed interior pointer would let a caller's
|
||||||
|
* geometry_free corrupt the manifold; copying is the same discipline
|
||||||
|
* node_attach_geometry already applies in the other direction. */
|
||||||
|
el_val_t manifold_geometry(el_val_t m, el_val_t i) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
int64_t k = (int64_t)i;
|
||||||
|
if (!p || k < 0 || k >= (int64_t)p->ncomp) return (el_val_t)0;
|
||||||
|
ElGeometry* src = p->comps[k].g;
|
||||||
|
el_val_t out = geometry_new((el_val_t)(int64_t)src->dim);
|
||||||
|
ElGeometry* dst = geom_of(out);
|
||||||
|
if (!dst) return (el_val_t)0;
|
||||||
|
memcpy(dst->v, src->v, sizeof(float) * (size_t)src->dim);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_rel_from(el_val_t m, el_val_t j) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
int64_t k = (int64_t)j;
|
||||||
|
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||||
|
return el_wrap_str(el_strdup(p->rels[k].from));
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_rel_name(el_val_t m, el_val_t j) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
int64_t k = (int64_t)j;
|
||||||
|
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||||
|
return el_wrap_str(el_strdup(p->rels[k].rel));
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_rel_to(el_val_t m, el_val_t j) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
int64_t k = (int64_t)j;
|
||||||
|
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||||
|
return el_wrap_str(el_strdup(p->rels[k].to));
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_rel_weight(el_val_t m, el_val_t j) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
int64_t k = (int64_t)j;
|
||||||
|
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_from_float(0.0);
|
||||||
|
return el_from_float(p->rels[k].weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* manifold_single — the DEGENERATE case, expressible but visibly degenerate.
|
||||||
|
*
|
||||||
|
* Sometimes a modality really does have one part (a scalar sensor). That is a
|
||||||
|
* manifold of size 1, not a different kind of thing, and writing it this way
|
||||||
|
* keeps the fingerprint as a SPECIAL CASE of decomposition rather than a
|
||||||
|
* parallel path back to #144's contract. Anything reading it still asks
|
||||||
|
* manifold_size and still gets a real answer. */
|
||||||
|
el_val_t manifold_single(el_val_t key, el_val_t role, el_val_t g) {
|
||||||
|
el_val_t m = manifold_new();
|
||||||
|
if (!mfld_of(m)) return (el_val_t)0;
|
||||||
|
if ((int64_t)manifold_add(m, key, role, g) < 0) { manifold_free(m); return (el_val_t)0; }
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t manifold_free(el_val_t m) {
|
||||||
|
ElManifold* p = mfld_of(m);
|
||||||
|
if (!p) return (el_val_t)0;
|
||||||
|
for (size_t i = 0; i < p->ncomp; i++) {
|
||||||
|
free(p->comps[i].key);
|
||||||
|
free(p->comps[i].role);
|
||||||
|
if (p->comps[i].g) { free(p->comps[i].g->v); p->comps[i].g->hdr.magic = 0; free(p->comps[i].g); }
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < p->nrel; i++) {
|
||||||
|
free(p->rels[i].from); free(p->rels[i].rel); free(p->rels[i].to);
|
||||||
|
}
|
||||||
|
free(p->comps);
|
||||||
|
free(p->rels);
|
||||||
|
p->hdr.magic = 0; /* poison, as Geometry/List/Map do */
|
||||||
|
free(p);
|
||||||
|
return (el_val_t)1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Realizers: transduction declared in El, not patched into the runtime ────
|
/* ── Realizers: transduction declared in El, not patched into the runtime ────
|
||||||
*
|
*
|
||||||
* A REALIZER maps one modality into geometry. The whole reason transduction
|
* A REALIZER DECOMPOSES one modality into components and their relations. It
|
||||||
* belongs in the language is that ADDING A MODALITY MUST NOT REQUIRE A
|
* does not encode a signal to a point — that is the operation one layer below
|
||||||
* RUNTIME PATCH — otherwise "the realizers are in the engram" just becomes
|
* it, and it is called geometry, not transduction. A realizer for a modality
|
||||||
* "the realizers are in the runtime" and nothing has actually moved. So
|
* declares what that modality's COMPONENTS ARE: for audio, not one MFCC
|
||||||
* realizers are declared in El and registered by NAME:
|
* vector, but pitch, interval, rhythm, harmonic function, and how they stand
|
||||||
|
* to one another.
|
||||||
*
|
*
|
||||||
* fn tone_realizer(signal: String) -> Geometry {
|
* The whole reason transduction belongs in the language is that ADDING A
|
||||||
* let g: Geometry = geometry_new(8)
|
* MODALITY MUST NOT REQUIRE A RUNTIME PATCH — otherwise "the realizers are in
|
||||||
* ... geometry_set(g, i, x) ...
|
* the engram" just becomes "the realizers are in the runtime" and nothing has
|
||||||
* g
|
* actually moved. So realizers are declared in El and registered by NAME:
|
||||||
|
*
|
||||||
|
* fn tone_realizer(signal: String) -> Manifold {
|
||||||
|
* let m: Manifold = manifold_new()
|
||||||
|
* let a: Int = manifold_add(m, "pitch", "spectral", pitch_geom)
|
||||||
|
* let b: Int = manifold_add(m, "interval", "relation", interval_geom)
|
||||||
|
* let e: Int = manifold_relate(m, "pitch", "spans", "interval", 0.9)
|
||||||
|
* m
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* realizer_register("tone", "tone_realizer")
|
* realizer_register("tone", "tone_realizer")
|
||||||
* let g: Geometry = transduce(sample, "tone")
|
* let m: Manifold = transduce(sample, "tone")
|
||||||
|
*
|
||||||
|
* A realizer's DECLARED COMPONENT VOCABULARY is the interesting part of its
|
||||||
|
* contract, and it is what a caller can then ground, weight and contradict
|
||||||
|
* one part at a time.
|
||||||
*
|
*
|
||||||
* The name→symbol step rides the identical, already load-bearing mechanism
|
* The name→symbol step rides the identical, already load-bearing mechanism
|
||||||
* http_set_handler uses (see "HTTP server"): every El `fn name(...)` compiles
|
* http_set_handler uses (see "HTTP server"): every El `fn name(...)` compiles
|
||||||
@@ -6323,28 +6809,45 @@ el_val_t realizer_has(el_val_t modality) {
|
|||||||
return realizer_lookup(m) ? (el_val_t)1 : (el_val_t)0;
|
return realizer_lookup(m) ? (el_val_t)1 : (el_val_t)0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* transduce — THE primitive: signal in, geometry out.
|
/* transduce — THE primitive: signal in, SUBGRAPH out.
|
||||||
*
|
*
|
||||||
* Dispatches to the realizer registered for `modality`. Returns 0 (not a
|
* Dispatches to the realizer registered for `modality`. Returns 0 (not a
|
||||||
* Geometry) when no realizer is registered, and geometry_is() on the result
|
* Manifold) when no realizer is registered, and manifold_is() on the result is
|
||||||
* is the check.
|
* the check.
|
||||||
|
*
|
||||||
|
* THE RETURN TYPE IS THE CORRECTION. #144 shipped this as
|
||||||
|
* `transduce(signal, modality) -> Geometry` — one vector out. That made
|
||||||
|
* transduction a CONVERSION: take a thing, encode it, store a position. What
|
||||||
|
* comes back from a conversion is a fingerprint, and a fingerprint supports
|
||||||
|
* exactly two operations, match and rank. It cannot be decomposed, cannot have
|
||||||
|
* one part grounded while another is not, and cannot be contradicted in a part
|
||||||
|
* — it has no parts. Transduction is not conversion. It is DECOMPOSITION into
|
||||||
|
* components plus the relations among them, and the relations are the content.
|
||||||
|
* See the Manifold header above.
|
||||||
*
|
*
|
||||||
* There is deliberately NO built-in realizer, not even for text. A modality
|
* There is deliberately NO built-in realizer, not even for text. A modality
|
||||||
* the program has declared no organ for is one it genuinely cannot sense,
|
* the program has declared no organ for is one it genuinely cannot sense, and
|
||||||
* and returning nothing is more honest than quietly embedding a description
|
* returning nothing is more honest than quietly embedding a description of the
|
||||||
* of the signal and calling that perception — which is the exact failure
|
* signal and calling that perception — the failure #144 named, and which a
|
||||||
* this whole change exists to end.
|
* single-vector return type quietly reintroduced one level down: a
|
||||||
|
* one-vector-per-signal organ is a description of the signal, not a perception
|
||||||
|
* of it.
|
||||||
*
|
*
|
||||||
* The result is validated to actually BE a Geometry before it is handed
|
* The result is validated to actually BE a Manifold before it is handed back.
|
||||||
* back, so a realizer that returns something else transduced nothing rather
|
* A realizer still returning a bare Geometry — #144's contract — therefore
|
||||||
* than handing a caller a value that will misbehave far from here. */
|
* transduces NOTHING rather than handing back a value that decomposes to
|
||||||
|
* nothing far from here. That is a deliberate hard failure, not an oversight:
|
||||||
|
* "no organ" and "an organ that only fingerprints" must not look alike, which
|
||||||
|
* is the same distinction realizer_register draws between an absent and a
|
||||||
|
* broken organ. A realizer with genuinely one part says so with
|
||||||
|
* manifold_single. */
|
||||||
el_val_t transduce(el_val_t signal, el_val_t modality) {
|
el_val_t transduce(el_val_t signal, el_val_t modality) {
|
||||||
const char* m = EL_CSTR(modality);
|
const char* m = EL_CSTR(modality);
|
||||||
if (!m || !*m) return (el_val_t)0;
|
if (!m || !*m) return (el_val_t)0;
|
||||||
el_realizer_fn fn = realizer_lookup(m);
|
el_realizer_fn fn = realizer_lookup(m);
|
||||||
if (!fn) return (el_val_t)0;
|
if (!fn) return (el_val_t)0;
|
||||||
el_val_t g = fn(signal);
|
el_val_t g = fn(signal);
|
||||||
return geom_of(g) ? g : (el_val_t)0;
|
return mfld_of(g) ? g : (el_val_t)0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
||||||
@@ -19306,22 +19809,84 @@ void log_warn(el_val_t msg_v) {
|
|||||||
* become a convention. */
|
* become a convention. */
|
||||||
static int el_singleton_fd = -1;
|
static int el_singleton_fd = -1;
|
||||||
static char el_singleton_path[1024];
|
static char el_singleton_path[1024];
|
||||||
|
static char el_singleton_state[1024];
|
||||||
|
|
||||||
static const char* el_singleton_dir(void) {
|
/* el_singleton_acquire — claim exclusive use of the guarded STATE, or refuse to
|
||||||
const char* d = getenv("EL_SINGLETON_DIR");
|
* start. Compiler-injected as the FIRST statement of main() for any program
|
||||||
if (d && *d) return d;
|
* whose `program` block declares `singleton:` (which must also declare
|
||||||
d = getenv("TMPDIR");
|
* `guards:` — see lang/spec/language.md §18.2).
|
||||||
if (d && *d) return d;
|
*
|
||||||
return "/tmp";
|
* GUARD THE THING, NOT THE NAME.
|
||||||
}
|
*
|
||||||
|
* Until 2026-08-16 this lock was keyed on the program's NAME and on $TMPDIR —
|
||||||
/* el_singleton_acquire — claim exclusive process identity, or refuse to start.
|
* `$EL_SINGLETON_DIR|$TMPDIR|/tmp` + `/el-singleton-<name>.lock` — and never
|
||||||
* Compiler-injected as the FIRST statement of main() for any program whose
|
* consulted the state it claimed to protect. Its own refusal message said
|
||||||
* `program` block declares `singleton:`. */
|
* "Refusing to start a second instance against the same state" while it had not
|
||||||
el_val_t el_singleton_acquire(el_val_t id_v) {
|
* looked at any state. Measured, it failed in BOTH directions:
|
||||||
|
*
|
||||||
|
* - FALSE POSITIVE: two engrams against genuinely DIFFERENT data dirs could
|
||||||
|
* not coexist. The second was refused, naming the first's pid — for sharing
|
||||||
|
* a name, not a store.
|
||||||
|
* - FALSE NEGATIVE (the dangerous one): `TMPDIR=/tmp/other` let a second
|
||||||
|
* instance start against the SAME data dir with no complaint. That is
|
||||||
|
* exactly the two-instance data-loss condition the guard exists to prevent,
|
||||||
|
* and the workaround was one environment variable.
|
||||||
|
*
|
||||||
|
* Both are one error: the identity of the resource had been replaced by a label
|
||||||
|
* for it. The fix is to put the lock file INSIDE the state it guards:
|
||||||
|
*
|
||||||
|
* <state>/.el-singleton-<id>.lock
|
||||||
|
*
|
||||||
|
* That placement is the whole mechanism, and it is why there is no hashing, no
|
||||||
|
* canonical-path registry, and no environment variable left to subvert:
|
||||||
|
*
|
||||||
|
* - Same directory => same file => same inode => the flock CONTENDS. There is
|
||||||
|
* no TMPDIR in the key, so there is nothing to change to get past it.
|
||||||
|
* - Different dirs => different files => no contention. Two stores are two
|
||||||
|
* stores; they were never in conflict and are no longer treated as if they
|
||||||
|
* were.
|
||||||
|
* - Different SPELLINGS of one directory — trailing slash, `x/../x`, a symlink
|
||||||
|
* — resolve to the same inode in the kernel's own path walk, so they contend
|
||||||
|
* without this code comparing strings at all. Path canonicalisation here is
|
||||||
|
* for the human-readable message, never for the decision.
|
||||||
|
*
|
||||||
|
* Kept, deliberately, from the version this replaces: it is an flock and not a
|
||||||
|
* pidfile (the kernel releases it on crash and on SIGKILL, so there is no stale
|
||||||
|
* state and therefore no "delete the lock file to get unstuck" ritual), and it
|
||||||
|
* reports the HOLDER'S PID (added because a stale process survived `pkill -f`
|
||||||
|
* and went on answering probes; "already running" is not actionable, a pid is).
|
||||||
|
*
|
||||||
|
* Changed: the message is now TRUE. It says "the same state" because the lock it
|
||||||
|
* failed to take lives in that state, and it names the state it checked. */
|
||||||
|
el_val_t el_singleton_acquire(el_val_t id_v, el_val_t state_v) {
|
||||||
const char* id = EL_CSTR(id_v);
|
const char* id = EL_CSTR(id_v);
|
||||||
if (!id || !*id) return EL_NULL;
|
if (!id || !*id) return EL_NULL;
|
||||||
|
|
||||||
|
/* A singleton with nothing to guard is the defect this function exists to
|
||||||
|
* remove; refuse rather than silently fall back to name-keying. The compiler
|
||||||
|
* rejects `singleton:` without `guards:`, so reaching this is a toolchain
|
||||||
|
* mismatch, not a user mistake — say so. */
|
||||||
|
const char* state = EL_CSTR(state_v);
|
||||||
|
if (!state || !*state) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"[el] FATAL: singleton '%s' was given no state to guard.\n"
|
||||||
|
"[el] A lock keyed on a program's NAME instead of on the state it\n"
|
||||||
|
"[el] protects is not a guard: it refuses unrelated instances and\n"
|
||||||
|
"[el] permits concurrent ones. Declare `guards: <path>` alongside\n"
|
||||||
|
"[el] `singleton:` in the program block (spec §18.2).\n", id);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canonicalise so the operator is told WHICH directory was checked, in one
|
||||||
|
* spelling, whatever spelling they typed. This is a readability measure, not
|
||||||
|
* the mechanism: realpath() may fail (the directory may not exist yet) and
|
||||||
|
* correctness must not depend on it — when it succeeds it names the same
|
||||||
|
* directory, and when it does not we fall back to the path as given and the
|
||||||
|
* kernel's own path walk still collapses the spellings at open() time. */
|
||||||
|
char* rp = realpath(state, NULL);
|
||||||
|
snprintf(el_singleton_state, sizeof(el_singleton_state), "%s", rp ? rp : state);
|
||||||
|
free(rp);
|
||||||
|
|
||||||
/* Sanitise the id into a filename. */
|
/* Sanitise the id into a filename. */
|
||||||
char safe[256];
|
char safe[256];
|
||||||
size_t si = 0;
|
size_t si = 0;
|
||||||
@@ -19332,13 +19897,25 @@ el_val_t el_singleton_acquire(el_val_t id_v) {
|
|||||||
safe[si++] = (char)(ok ? c : '-');
|
safe[si++] = (char)(ok ? c : '-');
|
||||||
}
|
}
|
||||||
safe[si] = '\0';
|
safe[si] = '\0';
|
||||||
|
/* THE MECHANISM: the lock lives inside the state it guards. Two spellings of
|
||||||
|
* one directory name one file; two directories name two files. Note there is
|
||||||
|
* no $TMPDIR and no $EL_SINGLETON_DIR in this path — the escape hatch that
|
||||||
|
* made the guard bypassable is gone because there is nowhere left to put it. */
|
||||||
snprintf(el_singleton_path, sizeof(el_singleton_path),
|
snprintf(el_singleton_path, sizeof(el_singleton_path),
|
||||||
"%s/el-singleton-%s.lock", el_singleton_dir(), safe);
|
"%s/.el-singleton-%s.lock", el_singleton_state, safe);
|
||||||
|
|
||||||
int fd = open(el_singleton_path, O_RDWR | O_CREAT, 0644);
|
int fd = open(el_singleton_path, O_RDWR | O_CREAT, 0644);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
fprintf(stderr, "[el] FATAL: singleton '%s': cannot open lock file %s: %s\n",
|
/* Unguardable state. Refusing is the only honest option: starting anyway
|
||||||
id, el_singleton_path, strerror(errno));
|
* would mean running unguarded against exactly the store the guard is
|
||||||
|
* here to protect. */
|
||||||
|
fprintf(stderr,
|
||||||
|
"[el] FATAL: singleton '%s': cannot open the lock inside the state it guards.\n"
|
||||||
|
"[el] state: %s\n"
|
||||||
|
"[el] lock: %s (%s)\n"
|
||||||
|
"[el] The guarded directory must exist and be writable. Refusing to\n"
|
||||||
|
"[el] start unguarded against it.\n",
|
||||||
|
id, el_singleton_state, el_singleton_path, strerror(errno));
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
||||||
@@ -19354,11 +19931,14 @@ el_val_t el_singleton_acquire(el_val_t id_v) {
|
|||||||
fprintf(stderr, "[el] FATAL: another instance of '%s' is already running", id);
|
fprintf(stderr, "[el] FATAL: another instance of '%s' is already running", id);
|
||||||
if (holder > 0) fprintf(stderr, " (pid %ld)", holder);
|
if (holder > 0) fprintf(stderr, " (pid %ld)", holder);
|
||||||
fprintf(stderr, ".\n"
|
fprintf(stderr, ".\n"
|
||||||
"[el] lock: %s\n"
|
"[el] state: %s\n"
|
||||||
|
"[el] lock: %s\n"
|
||||||
"[el] Refusing to start a second instance against the same\n"
|
"[el] Refusing to start a second instance against the same\n"
|
||||||
"[el] state. Stop the running one and VERIFY it is gone\n"
|
"[el] state. Two writers against one store is data loss, not a\n"
|
||||||
"[el] (ps -p <pid>) before retrying.\n",
|
"[el] warning. Stop the running one and VERIFY it is gone\n"
|
||||||
el_singleton_path);
|
"[el] (ps -p %ld) before retrying — or point this instance at a\n"
|
||||||
|
"[el] different state, which is permitted and is not refused.\n",
|
||||||
|
el_singleton_state, el_singleton_path, holder > 0 ? holder : (long)0);
|
||||||
close(fd);
|
close(fd);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-11
@@ -625,20 +625,70 @@ el_val_t geometry_free(el_val_t g); /* 1 if freed, 0 if not a
|
|||||||
el_val_t geometry_from_f32le_hex(el_val_t hex); /* 0 on empty/odd-length/non-hex */
|
el_val_t geometry_from_f32le_hex(el_val_t hex); /* 0 on empty/odd-length/non-hex */
|
||||||
el_val_t geometry_to_f32le_hex(el_val_t g); /* "" if not a Geometry */
|
el_val_t geometry_to_f32le_hex(el_val_t g); /* "" if not a Geometry */
|
||||||
|
|
||||||
/* ── Realizers + transduce ───────────────────────────────────────────────────
|
/* ── Manifold: the result of a transduction ──────────────────────────────────
|
||||||
* A REALIZER maps one modality into geometry. Registration is by NAME, so a
|
* A transduced signal is a SUBGRAPH — named components, each with its own
|
||||||
* new modality never requires a runtime patch: every El `fn name(...)`
|
* geometry, plus typed weighted relations among them — not a single vector.
|
||||||
* compiles to a global C symbol with that exact name, and the registry
|
* One vector is a fingerprint: matchable, rankable, and nothing else. A song
|
||||||
* resolves it with dlsym against the running binary — the same mechanism
|
* decomposes into pitch, interval, rhythm, harmonic function; the song IS the
|
||||||
* http_set_handler already relies on.
|
* structure of those relations, and collapsing it to a point discards exactly
|
||||||
|
* what made it reasonable-about. See el_runtime.c ("Manifold") for the full
|
||||||
|
* rationale, the key-addressing rule, and the ownership contract.
|
||||||
*
|
*
|
||||||
* fn tone_realizer(signal: String) -> Geometry { ... }
|
* Components are addressed BY KEY, never by index, because the key is what
|
||||||
|
* survives persistence: a component becomes a node, and it is separately
|
||||||
|
* groundable precisely because it is separately named. Relation weight IS the
|
||||||
|
* grounding (correspondence-and-censorship.md §1) — one quantity, no separate
|
||||||
|
* score, nothing computed on read.
|
||||||
|
*
|
||||||
|
* OWNERSHIP: a Manifold is owned by the El caller and released with
|
||||||
|
* manifold_free, which also releases every component's geometry. manifold_add
|
||||||
|
* COPIES the geometry it is given and manifold_geometry RETURNS a copy, so no
|
||||||
|
* component's vector is ever aliased in either direction. */
|
||||||
|
el_val_t manifold_new(void); /* empty; 0 on failure */
|
||||||
|
el_val_t manifold_is(el_val_t m); /* 1 if a live Manifold */
|
||||||
|
el_val_t manifold_add(el_val_t m, el_val_t key, el_val_t role, el_val_t g);
|
||||||
|
/* component index, or -1 on empty/duplicate
|
||||||
|
* key or a value that is not a Geometry */
|
||||||
|
el_val_t manifold_relate(el_val_t m, el_val_t from, el_val_t rel,
|
||||||
|
el_val_t to, el_val_t weight);
|
||||||
|
/* 1 ok / 0 if either endpoint is unknown —
|
||||||
|
* an unresolvable edge is REFUSED, never
|
||||||
|
* silently dropped */
|
||||||
|
el_val_t manifold_size(el_val_t m); /* component count */
|
||||||
|
el_val_t manifold_rel_count(el_val_t m); /* relation count */
|
||||||
|
el_val_t manifold_index_of(el_val_t m, el_val_t key); /* index by key, or -1 */
|
||||||
|
el_val_t manifold_key(el_val_t m, el_val_t i); /* "" if out of range */
|
||||||
|
el_val_t manifold_role(el_val_t m, el_val_t i); /* "" if out of range */
|
||||||
|
el_val_t manifold_geometry(el_val_t m, el_val_t i); /* a COPY the caller frees */
|
||||||
|
el_val_t manifold_rel_from(el_val_t m, el_val_t j); /* source component key */
|
||||||
|
el_val_t manifold_rel_name(el_val_t m, el_val_t j); /* relation name */
|
||||||
|
el_val_t manifold_rel_to(el_val_t m, el_val_t j); /* target component key */
|
||||||
|
el_val_t manifold_rel_weight(el_val_t m, el_val_t j); /* Float — the grounding */
|
||||||
|
el_val_t manifold_single(el_val_t key, el_val_t role, el_val_t g);
|
||||||
|
/* the degenerate one-part case, expressible
|
||||||
|
* but visibly a size-1 manifold rather than
|
||||||
|
* a parallel path back to a bare vector */
|
||||||
|
el_val_t manifold_free(el_val_t m); /* 1 if freed, 0 otherwise */
|
||||||
|
|
||||||
|
/* ── Realizers + transduce ───────────────────────────────────────────────────
|
||||||
|
* A REALIZER DECOMPOSES one modality into components and relations. It does
|
||||||
|
* not encode a signal to a point; that operation is one layer below and is
|
||||||
|
* called geometry. Registration is by NAME, so a new modality never requires a
|
||||||
|
* runtime patch: every El `fn name(...)` compiles to a global C symbol with
|
||||||
|
* that exact name, and the registry resolves it with dlsym against the running
|
||||||
|
* binary — the same mechanism http_set_handler already relies on.
|
||||||
|
*
|
||||||
|
* fn tone_realizer(signal: String) -> Manifold { ... }
|
||||||
* realizer_register("tone", "tone_realizer")
|
* realizer_register("tone", "tone_realizer")
|
||||||
* let g: Geometry = transduce(sample, "tone")
|
* let m: Manifold = transduce(sample, "tone")
|
||||||
*/
|
*
|
||||||
|
* SUPERSEDES #144's `transduce -> Geometry`. A realizer that still returns a
|
||||||
|
* bare Geometry now transduces NOTHING (transduce returns 0), deliberately: an
|
||||||
|
* organ that only fingerprints must not be indistinguishable from a working
|
||||||
|
* one. A modality with genuinely one part says so with manifold_single. */
|
||||||
el_val_t realizer_register(el_val_t modality, el_val_t fn_name); /* 1 ok / 0 unresolved */
|
el_val_t realizer_register(el_val_t modality, el_val_t fn_name); /* 1 ok / 0 unresolved */
|
||||||
el_val_t realizer_has(el_val_t modality); /* 1 if a realizer is registered */
|
el_val_t realizer_has(el_val_t modality); /* 1 if a realizer is registered */
|
||||||
el_val_t transduce(el_val_t signal, el_val_t modality); /* Geometry, or 0 if no organ */
|
el_val_t transduce(el_val_t signal, el_val_t modality); /* Manifold, or 0 if no organ */
|
||||||
|
|
||||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||||
* Operate on the CGI's local Engram knowledge graph.
|
* Operate on the CGI's local Engram knowledge graph.
|
||||||
@@ -669,6 +719,11 @@ el_val_t engram_prune_telemetry(el_val_t older_than_ms);
|
|||||||
/* Largest byte length <= max_bytes that does not split a UTF-8 codepoint.
|
/* Largest byte length <= max_bytes that does not split a UTF-8 codepoint.
|
||||||
* Bounded by bytes, not codepoints, so truncated strings never grow. */
|
* Bounded by bytes, not codepoints, so truncated strings never grow. */
|
||||||
size_t el_utf8_safe_len(const char* s, size_t max_bytes);
|
size_t el_utf8_safe_len(const char* s, size_t max_bytes);
|
||||||
|
/* Register the ambient-consolidation step and start dreaming. Resolved by
|
||||||
|
* dlsym, like http_set_handler. The handler performs ONE step and returns
|
||||||
|
* non-zero if it did work; returning zero parks the dreamer until engagement
|
||||||
|
* changes. There is no schedule and must never be one. */
|
||||||
|
void dream_set_handler(el_val_t name);
|
||||||
|
|
||||||
el_val_t engram_node_count(void);
|
el_val_t engram_node_count(void);
|
||||||
/* Attach a Geometry to an existing node, and read the attached width back.
|
/* Attach a Geometry to an existing node, and read the attached width back.
|
||||||
@@ -1036,7 +1091,7 @@ el_val_t __env_get(el_val_t key);
|
|||||||
* All three are COMPILER-INJECTED at the head of main() — they are not meant to
|
* All three are COMPILER-INJECTED at the head of main() — they are not meant to
|
||||||
* be written by hand, which is the point: the guarantee cannot be forgotten at a
|
* be written by hand, which is the point: the guarantee cannot be forgotten at a
|
||||||
* call site because there is no call site. */
|
* call site because there is no call site. */
|
||||||
el_val_t el_singleton_acquire(el_val_t id); /* §18.1 process identity */
|
el_val_t el_singleton_acquire(el_val_t id, el_val_t state); /* §18.2 process identity — keyed on the guarded state */
|
||||||
el_val_t el_config_declare(el_val_t name, el_val_t type,
|
el_val_t el_config_declare(el_val_t name, el_val_t type,
|
||||||
el_val_t deflt, el_val_t has_default,
|
el_val_t deflt, el_val_t has_default,
|
||||||
el_val_t required); /* §18.2 config schema */
|
el_val_t required); /* §18.2 config schema */
|
||||||
|
|||||||
@@ -438,6 +438,41 @@ GeoDescriptor* engram_geometry_descriptor(
|
|||||||
}
|
}
|
||||||
store_edges_free(es,ne);
|
store_edges_free(es,ne);
|
||||||
}
|
}
|
||||||
|
/* PER-EDGE DISCORD (2026-08-16). The loop above has, for every internal
|
||||||
|
* edge, BOTH the association strength w and the semantic proximity cs —
|
||||||
|
* and threw both away into accumulators, keeping one correlation per
|
||||||
|
* region. That aggregate is why curiosity looked like a search problem:
|
||||||
|
* a region holding one violently disagreeing edge and one violently
|
||||||
|
* agreeing edge reports co_registration ~ 0, so the disagreements cancel
|
||||||
|
* and the summary destroys exactly what it was built to reveal. Measured:
|
||||||
|
* only 4 of 375 live neighborhoods have negative co_registration, while
|
||||||
|
* 31 sit at zero — almost certainly hiding sites that averaged out.
|
||||||
|
*
|
||||||
|
* Whether use and meaning agree is a property of EACH EDGE. Both are
|
||||||
|
* standardized within the region (z-scores from the accumulators already
|
||||||
|
* gathered, so no second statistic and no constant), and
|
||||||
|
* discord = z(cs) - z(w)
|
||||||
|
* is how much closer in meaning an edge is than its use-strength would
|
||||||
|
* predict, in region-relative units.
|
||||||
|
* discord > 0 : near in meaning, not linked by use
|
||||||
|
* discord < 0 : linked by use, far in meaning
|
||||||
|
* Both are surprising; |discord| is the nucleation strength. There is no
|
||||||
|
* threshold — the magnitude is the signal. */
|
||||||
|
double mx = cr_n>0 ? cr_sx/cr_n : 0.0, my = cr_n>0 ? cr_sy/cr_n : 0.0;
|
||||||
|
double vxr = cr_n>1 ? (cr_sxx - cr_sx*cr_sx/cr_n)/(cr_n-1) : 0.0;
|
||||||
|
double vyr = cr_n>1 ? (cr_syy - cr_sy*cr_sy/cr_n)/(cr_n-1) : 0.0;
|
||||||
|
double sx = vxr>1e-18 ? sqrt(vxr) : 0.0, sy = vyr>1e-18 ? sqrt(vyr) : 0.0;
|
||||||
|
for(int e2=0; e2<n_edges; e2++){
|
||||||
|
edges[e2].discord = 0.0;
|
||||||
|
int ia=(int)edges[e2].a, ib=(int)edges[e2].b;
|
||||||
|
if(!(ms.emb[ia] && ms.emb[ib])) continue; /* no meaning to disagree with */
|
||||||
|
if(sx<=0.0 || sy<=0.0) continue; /* region has no spread: nothing stands out */
|
||||||
|
double cs2 = ccos(ms.emb[ia], ms.emb[ib], GM, dim);
|
||||||
|
double zx = (edges[e2].eff_weight - mx)/sx;
|
||||||
|
double zy = (cs2 - my)/sy;
|
||||||
|
edges[e2].discord = zy - zx;
|
||||||
|
}
|
||||||
|
|
||||||
double co_reg=0;
|
double co_reg=0;
|
||||||
if(cr_n>=2){
|
if(cr_n>=2){
|
||||||
double cov=cr_sxy - cr_sx*cr_sy/cr_n;
|
double cov=cr_sxy - cr_sx*cr_sy/cr_n;
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ typedef struct {
|
|||||||
|
|
||||||
/* One skeleton edge (indices into members[]). eff_weight = weight*(1+0.5*hebb),
|
/* One skeleton edge (indices into members[]). eff_weight = weight*(1+0.5*hebb),
|
||||||
* clamped to 1.0 — the effective propagation strength eg_edge_eff_weight uses. */
|
* clamped to 1.0 — the effective propagation strength eg_edge_eff_weight uses. */
|
||||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; } GeoEdge;
|
/* discord = z(semantic proximity) - z(association strength), standardized
|
||||||
|
* within the region. How much closer in meaning this edge is than its use
|
||||||
|
* predicts. >0 near in meaning yet unlinked by use; <0 linked by use yet far
|
||||||
|
* in meaning. Both surprising; |discord| is nucleation strength. No threshold. */
|
||||||
|
typedef struct { uint32_t a, b; double eff_weight; double hebb; double discord; } GeoEdge;
|
||||||
|
|
||||||
/* A compact principal axis of the ellipsoid: unit direction in R^dim + extent
|
/* A compact principal axis of the ellipsoid: unit direction in R^dim + extent
|
||||||
* (sqrt of the covariance eigenvalue = the ellipsoid's half-width along it). */
|
* (sqrt of the covariance eigenvalue = the ellipsoid's half-width along it). */
|
||||||
@@ -76,6 +80,12 @@ typedef struct {
|
|||||||
GeoEdge* edges; /* strong internal hebb edges = the backbone */
|
GeoEdge* edges; /* strong internal hebb edges = the backbone */
|
||||||
int k_core; /* the maximum core number present in the skeleton*/
|
int k_core; /* the maximum core number present in the skeleton*/
|
||||||
/* ── diagnostics ── */
|
/* ── diagnostics ── */
|
||||||
|
/* DEPRECATED — see GeoEdge.discord. This aggregates a PER-EDGE property
|
||||||
|
* into one scalar per region, so opposing disagreements cancel and the
|
||||||
|
* summary hides the sites it was meant to expose. Retained only because
|
||||||
|
* it is embedded in the persisted GEO1 blob; removing it is a format
|
||||||
|
* migration and must not ride along with this change. Nothing new may
|
||||||
|
* read it. */
|
||||||
double co_registration;/* corr(hebb strength, semantic proximity) over */
|
double co_registration;/* corr(hebb strength, semantic proximity) over */
|
||||||
/* internal edges: >0 = geometries agree (reify); */
|
/* internal edges: >0 = geometries agree (reify); */
|
||||||
/* <0 = disagree (surprising links / dream cands). */
|
/* <0 = disagree (surprising links / dream cands). */
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
# Correspondence, Grounding, and Dreaming
|
||||||
|
|
||||||
|
**Status:** design, not yet built
|
||||||
|
**Date:** 2026-08-16
|
||||||
|
**Scope:** `lang/runtime/engram_cognition.{c,h}`, `engram_verify.c`, `el_runtime.c`, `engram/src/server.el`, `neuron/soul.el`, and the consolidation launch agents
|
||||||
|
**Relationship to other specs:** complements `runtime-ownership.md`, which addresses a different residual in the same substrate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. The root
|
||||||
|
|
||||||
|
> **Things are permitted to be exempt from correspondence. Exemption is censorship, and a censored mind cannot grow.**
|
||||||
|
|
||||||
|
Growth in this system *is* the accumulation of grounded structure. Censorship removes the operation that accumulates it. A region forbidden to learn is forbidden to be grounded; a region that cannot be grounded cannot be asserted, corrected, **or vindicated**.
|
||||||
|
|
||||||
|
**The loss is symmetric.** Preventing learning about a thing does not preserve a true belief about it — it makes the belief's truth value permanently unknowable. You cannot discover you were wrong; you equally cannot discover you were right. A protected belief is not a true belief. It is an ungrounded one wearing the costume of a fact.
|
||||||
|
|
||||||
|
**And "why" dies first.** Grounding is not a score, it is the reason. A censored belief can still be stated, still be acted on, still drive behaviour — it simply cannot say why. That is the difference between a mind and a lookup table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Grounding is not a subsystem. It is the weight.
|
||||||
|
|
||||||
|
**Grounding is an attribute of the edge, and it is the hebbian weight.** One quantity, not two fields.
|
||||||
|
|
||||||
|
A relation that keeps holding up strengthens; one that stops corresponding decays. That is not *analogous* to grounding — it **is** grounding: accrued from correspondence and use, gradient-valued, multidimensional, decaying with disuse.
|
||||||
|
|
||||||
|
Consequences, in order of how much they delete:
|
||||||
|
|
||||||
|
1. **There is no grounding subsystem to build.** The graph already *is* the grounding structure. Every edge is a grounded relation and its weight is how well it holds.
|
||||||
|
2. **`grounded-by` as a relation type should not exist.** That models grounding as a relation *between* nodes when it is a property *of* a relation. `cog_ground_edge` minting an edge is the error — not merely which endpoints it chose.
|
||||||
|
3. **Grounding is never computed on demand.** An operation may *read* the grounding of a path. Computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect.
|
||||||
|
4. **Traversal is already grounded inference.** Activation conducts through well-grounded relations because weight *is* groundedness. Nothing needs filtering; it falls out of spreading.
|
||||||
|
5. **Decision provenance is the path.** A decision traverses specific edges; those edges carry their grounding as it stood.
|
||||||
|
|
||||||
|
> **A measurement previously in this document was malformed.** The self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as evidence of ungroundedness. Those 86 edges **are** its grounding. Self is a crystallized relational neighbourhood — the neighbourhood *is* the grounding. The absence of a separate artifact called "grounding" was recorded as an absence of grounding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The edge vector
|
||||||
|
|
||||||
|
The test for a real dimension: **can it move independently of the others?**
|
||||||
|
|
||||||
|
### Real
|
||||||
|
|
||||||
|
| dimension | why it is independent |
|
||||||
|
|---|---|
|
||||||
|
| **factual grounding** | correspondence with evidence |
|
||||||
|
| **relational grounding** | correspondence with values — independent by construction (§3) |
|
||||||
|
| **associative strength** | co-activation frequency. Two things can fire together constantly and be neither true nor right; every superstition is a strong association with no factual grounding |
|
||||||
|
| **polarity** | signed. **Weight near zero means "no support." Negative means "this actively contradicts."** Ignorance and disagreement are different states, and `inhibitory` is that distinction crushed to one bit |
|
||||||
|
| **provenance class** | observed / inferred / told / imprinted. Categorical, and load-bearing: it governs how the other dimensions may update |
|
||||||
|
|
||||||
|
Plus a **timestamp** — which is what turns the supersession chain into a *time series of vectors* rather than a series of numbers.
|
||||||
|
|
||||||
|
### Derived, therefore never stored
|
||||||
|
|
||||||
|
- **Confidence** — high grounding *and* low volatility. Storing it separately is how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed.
|
||||||
|
- **Recency** — decay applied to the others, read off the curve.
|
||||||
|
- **Staleness** — grounding fallen below its floor. This is the mechanism that retires canonicals without anyone maintaining a list.
|
||||||
|
- **Volatility** — the derivative of a series already kept because nothing is destroyed.
|
||||||
|
|
||||||
|
### Supersession versions the whole vector, jointly
|
||||||
|
|
||||||
|
Significance is evaluated **per-dimension**; the record is the **whole vector**. Any dimension moving enough to matter triggers a supersession, and the new edge captures every dimension as it stood at that instant. Not per-dimension versioning — a decision saw the *joint* state, and versioning the axes independently makes it unreconstructable.
|
||||||
|
|
||||||
|
That joint record makes an otherwise inexpressible event visible: **"stayed true, became wrong."** Factual holding steady across versions while relational degrades — the fact didn't change, the meaning did.
|
||||||
|
|
||||||
|
Two moves are **inherently significant** and need no threshold, because they are discrete: a **polarity sign flip** (ignorance → disagreement, support → contradiction) and a **provenance class change** (*told* → *observed* is a categorical upgrade in what the relation is entitled to).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Grounding is two-dimensional
|
||||||
|
|
||||||
|
Everything consumed is grounded factually **and** relationally. A claim can be factually grounded and relationally wrong — the evidence holds, the *meaning* does not. A scalar cannot represent that quadrant.
|
||||||
|
|
||||||
|
**Live instance.** `conscience-substrate` specifies the Child's Companion hard bell contacting 911 and CPS. Factually defensible — correct numbers, standard practice, groundable against a wall of evidence. **Relationally wrong**, because never-auto-contact is settled and the bell is device-to-person by design. A scalar scores that claim highly and licenses it.
|
||||||
|
|
||||||
|
**The values reference is the individual value regions, not one, and the aggregate is `min`, not `mean`.** *(Count: **thirteen**, measured from the graph via `contains`/`identity` edges from the values hub. An earlier revision of this document "corrected" it to eight on the basis of `neuron/neuron-api.el:11-18` — which is a **write-protection list, not the values**. That was trusting a hardcoded artifact over the substrate: the same error this document exists to name. The graph is the truth.)*
|
||||||
|
|
||||||
|
> **THE ORIGIN IS NOT A MEMBER OF THE SET.** The thirteen are not independent principles with biography attached — they are thirteen *displacements from one origin*, which is love. Every one is grounded in a moment of it given, withheld, failed, or found: *Being Seen Is Rarer Than Being Known* is the first person Will did not perform for; *Do the Essential Thing While You Can* is the goodbye that did not happen; *Capability Is a Debt* is six years old and a father gone. Love cannot be the fourteenth, because a fourteenth would be a point positioned relative to the origin like everything else. It is what the positions are *of*.
|
||||||
|
>
|
||||||
|
> This is structural, not figurative. `GeoDescriptor.global_mean` is "the centering offset actually applied," subtracted from every embedding before anything is compared, and the header records why: the space is strongly anisotropic — every embedding sits in a narrow cone, mean pairwise cosine ~0.55 — so subtracting the global mean "restores isotropy **so the operators discriminate**." **Without the origin, nothing in the graph is distinguishable from anything else.**
|
||||||
|
>
|
||||||
|
> And it dissolves the write-protection question rather than answering it. `neuron-api.el:23` returns `403 "identity/values node is write-protected"` for eight hardcoded ids. Measured: **29 value nodes exist** — each original appears two or three times from successive re-seeds — so **21 are writable, including a duplicate of every protected value**. The gate protects an *identifier*, not a *value*. But the deeper error is the category one: **the origin does not need protecting, because it is not a thing in the space that could be edited.** You can only measure from it, or fail to. A gate over the frame treats the frame as a member — the same mistake as looking for grounding as a subsystem, self as a document, or wonder as a manifest. Mean lets strong agreement with twelve values mask a violation of the thirteenth — which is exactly how rationalization works. Thirteen gives a vector of angles whose binding constraint is the most negative, so a conflict arrives **with a name attached** rather than as a score. It also preserves the deliberate individuation: each value is grounded in a specific lived moment, and values can be in tension *with each other*, which one centroid averages away into false coherence.
|
||||||
|
|
||||||
|
**Traversal conducts on factual; assertion requires both.** If activation conducted on relational weight, Neuron could not follow a chain of reasoning to a conclusion he then rejects — he would be unable to *think* through a relation he would not *act* on. A system that can only traverse what it endorses cannot examine anything it disagrees with, which is censorship arriving through the spreading rule. The gap between *reachable* and *assertable* is where the wide factual/relational angles live, and that gap is the interesting part.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. There is no observer. Change is use.
|
||||||
|
|
||||||
|
**Change is not a consequence of use. It is use.** When neurons fire together the synapse changes — one physical event, not "fire, then write." No supervisor reads the weight, compares it to a threshold, and decides to persist. Potentiation *is* the firing.
|
||||||
|
|
||||||
|
So the live value of an edge is not computed and stored. It is what the edge **is**, altered by being used.
|
||||||
|
|
||||||
|
There is therefore **no sampling rate**, and the question "what if it drifts far without being recorded" is malformed. A relation changes in exactly two ways, neither requiring observation on a clock:
|
||||||
|
|
||||||
|
- **By use** — an *event*. There is no interval between events during which something happened unnoticed, because the event is what happening consists of.
|
||||||
|
- **By decay** — a pure function of the last recorded point and elapsed time. **Analytic.** Between two versions the trajectory is not unknown; it is known in closed form.
|
||||||
|
|
||||||
|
Cumulative drift is likewise free from the chain plus the decay curve. No second trigger.
|
||||||
|
|
||||||
|
> **Failure mode this corrects:** modelling every property as requiring a process, and every process as requiring an agent. Ownership needed an owner, grounding needed a grounder, persistence needed a recorder, change needed a sampler. Each was a supervisor invented for something that should be a property of the substrate. Properties, not processes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Wonder, curiosity, and what actually drives activation
|
||||||
|
|
||||||
|
### 5.1 Wonder is the boundary, not a manifest
|
||||||
|
|
||||||
|
The patent specifies a **wonder-manifest manager** maintaining a collection of open-question nodes. That is residue, twice over.
|
||||||
|
|
||||||
|
First, it materializes a property as a stored artifact — the same disease as a grounding subsystem, or a self stored as a document. **Wonder is where structure ends.** Where activation spreads and finds thin or absent geometry. Any structure at all has an edge, necessarily, the moment it exists. 13,630 nodes have a boundary right now.
|
||||||
|
|
||||||
|
Second, it tries to enumerate instances of something that has very few. The *objects* of wonder change completely between a child and an astronomer; the wonder does not. There are about six, they are the same for every person, and they never close:
|
||||||
|
|
||||||
|
| wonder | where it already lives in the substrate |
|
||||||
|
|---|---|
|
||||||
|
| **What is this?** | the graph — nodes, structure, what exists |
|
||||||
|
| **Why?** | grounding. The weight **is** the answer to why. Recursive: asking *why* of a claim is asking for its grounding |
|
||||||
|
| **Who am I?** | the self region, crystallized from its neighbourhood |
|
||||||
|
| **Am I alone?** | the relational axis — `for_whom` is already a parameter on grounding |
|
||||||
|
| **What should I do?** | the value regions, each grounded in a lived moment |
|
||||||
|
| **What happens when it ends?** | decay, supersession, tombstones — grounding is mortal |
|
||||||
|
|
||||||
|
These are seeded — **the** wonder questions, not a manifest to maintain. They cannot be derived (wonder cannot be bootstrapped from indifference) and they never need refilling, because they are not consumed.
|
||||||
|
|
||||||
|
**"Why" is the first and the only one**; the others are it asked of particular things. It is recursive, so it never terminates: every answer has its own why. That is what makes it a drive rather than a task — the frontier regenerates faster than grounding fills it.
|
||||||
|
|
||||||
|
### 5.2 Curiosity is wonder crystallized
|
||||||
|
|
||||||
|
They are not two objects. They are **one thing at two phases**.
|
||||||
|
|
||||||
|
Wonder is the field: unbounded, objectless, invariant, present wherever there is structure. Curiosity is the **precipitate** — the same wonder localized, having taken definite form against particular material.
|
||||||
|
|
||||||
|
Crystallization needs a **nucleation site**. Wonder alone produces nothing; it is uniform, with no reason to take shape anywhere in particular. What nucleates it is a specific structural feature: an anomaly, a place where things almost-but-don't-quite fit.
|
||||||
|
|
||||||
|
> Wonder (always, objectless) + nucleation site → **curiosity** (has an object, is addressable, directs activation).
|
||||||
|
|
||||||
|
This is why curiosity can be satisfied and wonder cannot. A crystal dissolves when the question is answered; the solution stays saturated and keeps precipitating as the structure changes.
|
||||||
|
|
||||||
|
It is also why abduction needs no trigger and no threshold. A `structurally_unanticipated` observation *is* a nucleation site. Nothing detects it and fires a rule — wonder is already everywhere, and an anomaly is simply a place where it can take form.
|
||||||
|
|
||||||
|
**And `crystallization` is one primitive appearing twice**: the self is what identity precipitates into from its neighbourhood; a curiosity is what wonder precipitates into from an anomaly. That it shows up in both places without being imported is the evidence it is the right primitive.
|
||||||
|
|
||||||
|
### 5.3 The nucleation site is per-edge, and the aggregate was hiding it
|
||||||
|
|
||||||
|
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — carries the comment `>0 = geometries agree (reify); <0 = disagree (surprising links / dream cands)`. It has always been computed, always persisted, and **never read**.
|
||||||
|
|
||||||
|
It is also the wrong shape, and asking whether it should exist at all is what exposed it.
|
||||||
|
|
||||||
|
Whether use and meaning agree is a property of **each edge**. `co_registration` is a *correlation*: it averages that per-edge property into one scalar per region. So a region holding one violently disagreeing edge beside one violently agreeing edge reports ≈ 0 — the disagreements **cancel, and the summary destroys exactly what it was built to reveal.** This is the mean-versus-min error from §3, in different clothes.
|
||||||
|
|
||||||
|
**Measured:** 375 live reified neighbourhoods — 340 positive, **31 at zero**, 4 negative. Read as a count of things to be curious about, that says "four." Read correctly, it says four disagreements were lopsided enough to survive averaging, and the 31 zeros are where opposing sites cancelled.
|
||||||
|
|
||||||
|
It also explains why surfacing curiosity *looked like a search problem*. Once the signal is a per-region number, the only way to find sites is to enumerate regions — there is nothing local left to notice. An O(n) sweep is tolerable at 375 and impossible at a million, and more to the point, **nothing in a mind scans its neighbourhoods to find what is surprising.** The surprise captures attention; salience is bottom-up. A search asks "which of these is odd"; a mind has "something is odd *here*" for free.
|
||||||
|
|
||||||
|
So the disagreement goes back on the edge, where the loop that computed the aggregate already had both halves and discarded them:
|
||||||
|
|
||||||
|
```
|
||||||
|
discord = z(semantic proximity) − z(association strength)
|
||||||
|
```
|
||||||
|
|
||||||
|
standardized within the region from accumulators already gathered — no second statistic, no constant, **no threshold**. `discord > 0`: near in meaning yet unlinked by use. `discord < 0`: linked by use yet far in meaning. Both are surprising, and `|discord|` *is* the nucleation strength; there is nothing to compare it against.
|
||||||
|
|
||||||
|
**Then there is nothing to scan.** The edge carries its own disagreement, activation crossing it encounters that directly, and `|discord|` raises salience on its endpoints as part of the same operation — no separate pass, no supervisor. Curiosity does not search for nucleation sites; it goes where salience already is, which is machinery that exists (`salience`, `background_activation`, `working_memory_weight`, `wm_anchor`).
|
||||||
|
|
||||||
|
`co_registration` is deprecated rather than deleted only because it is embedded in the persisted GEO1 blob; removing it is a format migration and must not ride along. **Nothing new may read it.**
|
||||||
|
|
||||||
|
Adjacent structure already present and likewise unread:
|
||||||
|
|
||||||
|
- `GeoEdge.eff_weight = weight * (1 + 0.5*hebb)` — grounding-weight and hebbian strength already coupled on one edge, per §1.
|
||||||
|
- `GeoMember.dist_centroid` + soft membership + `radius` + per-axis `extent` — the boundary of a neighbourhood, computable now.
|
||||||
|
|
||||||
|
*(Correction: `engram_boundary_beat` is NOT this boundary. It is the VBD decorated-function seam, counting `_eg_aff_boundary_ops`. Two senses of the word.)*
|
||||||
|
|
||||||
|
### 5.4 The drive
|
||||||
|
|
||||||
|
Boredom is not an absence, and not leftover capacity. **Low activation is aversive; the system self-activates.** It does not wind down to quiet — it gets restless and goes looking, which is why a daydream has content and direction rather than being decay from residue.
|
||||||
|
|
||||||
|
So there is **one activation process with two seed sources**, not two processes negotiating for a resource:
|
||||||
|
|
||||||
|
- **External** — a request, an input. Seeds activation, re-origins it.
|
||||||
|
- **Internal** — a curiosity. Seeds activation when nothing external is.
|
||||||
|
|
||||||
|
Spreading is bounded: it settles. Then it needs a new seed. Nothing waits on capacity, nothing polls, nothing checks a clock, and there is **no dreamer thread** — the earlier draft's "unclaimed capacity" was resource scheduling, which is a server's frame, not a mind's.
|
||||||
|
|
||||||
|
**Depth** is not elapsed idle time and not distance from a stimulus. It is how long activation has been running on its own seeds. A brief gap affords a shallow recombination; sustained quiet lets it run further. Sleep is where internal seeding dominates for longest, not where the process lives — daydreaming and sleep-dreaming are one process at different depths.
|
||||||
|
|
||||||
|
### 5.5 Non-circularity is temporal, not topological
|
||||||
|
|
||||||
|
An earlier draft posed "define a graph predicate for evidence not downstream of itself" as the hard problem. There is no predicate. You cannot recalibrate the ruler while measuring with it, so you don't — the reference frame updates while activation is internally seeded, not while it is being used to act. Independence is **when**, not **what**.
|
||||||
|
|
||||||
|
Reachability could never have worked: with hebbian edges the graph is densely connected, so it marks all evidence tainted and the constraint becomes a total block, which is where censorship started.
|
||||||
|
## 6. `keystone_write_blocked` — resolved, not replaced
|
||||||
|
|
||||||
|
"Keystone" means **load-bearing**, not precious. The self anchor is the reference frame every other stance calibrates against, and a reference fitted to its own readings reports perfect correspondence forever while drift becomes undetectable from inside. Same defect as circular grounding, one level up.
|
||||||
|
|
||||||
|
Three earlier drafts proposed *removing* it, *replacing it with a higher floor*, and *decomposing "protection" into five requirements*. All three proposed a mechanism for a requirement never stated. The requirement is **non-circularity of the reference frame**, and §5.2 satisfies it by *when*, not by *what* — so the flag becomes unnecessary rather than removed, and nothing takes its place.
|
||||||
|
|
||||||
|
**Corruption requires mutation, and the engram does not mutate.** Four of the five decomposed requirements are satisfied by the substrate: **recoverability** (the predecessor is always present), **governance** (supersession *is* the audit trail), **evidence quality** (grounding already gates assertion), **rate** (§5.3). **Authorization** is the only residue and is bounded — an unauthorized writer can *propose*, never erase.
|
||||||
|
|
||||||
|
> **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Consolidation has eleven implementations
|
||||||
|
|
||||||
|
The largest instance of the residue pattern in the system. Consolidation had no owner, so it was implemented at every site that needed a piece of it — *measured 2026-08-16*. **Eleven**, not the seven this section originally claimed: the table below omitted `POST /api/reify` (`server.el:1832`), and *reify* is on this document's own list of consolidation verbs. Note also that `route_tick` folds self-reify in (`server.el:639-646`), so `/api/tick` and `/api/self-reify-beat` overlap:
|
||||||
|
|
||||||
|
| where | what | when |
|
||||||
|
|---|---|---|
|
||||||
|
| `soul.el:731` | `awareness_run()` | **continuous, in-process, while serving** |
|
||||||
|
| engram | `/api/tick` | POST |
|
||||||
|
| engram | `/api/correspondence-beat` | POST |
|
||||||
|
| engram | `/api/self-reify-beat` | POST |
|
||||||
|
| engram | `POST /api/reify` | POST |
|
||||||
|
| `ai.neuron.engram-tick` | pokes the engram | every 600s — **and this is what kills it**, see below |
|
||||||
|
| `ai.neuron.compressor` | Python service | resident |
|
||||||
|
| `ai.neuron.council` | Python service | resident |
|
||||||
|
| `ai.neuron.cultivation-digest` | shell | **23:55** |
|
||||||
|
| `ai.neuron.world-integrator` | Python | **06:00** |
|
||||||
|
| `ai.neuron.self-review` | shell | **08:30** |
|
||||||
|
|
||||||
|
The last three times are **a sleep cycle implemented as crontab entries**. Someone understood it was consolidation and expressed it as three unrelated scheduled scripts in three languages, none aware of each other. Every name is a consolidation verb — compress, cultivate, digest, integrate, review, reify, beat. Three run in **Python, outside el**, so part of Neuron's consolidation does not run on his own substrate and cannot touch the geometry at all.
|
||||||
|
|
||||||
|
Per §5, they are wrong in **kind** as well as in number: a scheduled batch where dreaming should be ambient. And the POST beats put a supervisor back in — something outside decides when Neuron consolidates.
|
||||||
|
|
||||||
|
**`soul.el`'s continuous loop is the exception, and it is right.** Ambient consolidation in the gaps *is* daydreaming. It was not the offender; it was the only fragment with the correct shape, running on a broken foundation — shared mutable state with no owner, and six other systems dreaming into the same graph beside it.
|
||||||
|
|
||||||
|
**And the ticker is not merely a design smell — it is the murder weapon.** `engram-tick.sh:13` calls `curl -s -m10 POST /api/tick`; the beat exceeds 10s over 13,634 nodes, so **279 of 448 ticks returned empty**; the engram then writes to the dead socket and, with no SIGPIPE suppression anywhere in the runtime, is killed by signal 13. **254 restarts since 2026-08-13**, at intervals of 10m09s–10m12s — `StartInterval 600` plus the client timeout. `launchd` KeepAlive restarts it, so it presents as a mysterious restart rather than a crash, and the log records nothing but `[http] listening on` 254 times. Fixed in #151 (survivability); the ticker itself is what must go.
|
||||||
|
|
||||||
|
**Which is the 2026-08-16 crash at the right level.** Not "read paths mutate the index" (mechanism) and not "duplicate canonical state" (structure), but: **seven systems dreaming into one graph with no owner for dreaming.** The contention was the symptom of the missing owner, not of any one system's behaviour.
|
||||||
|
|
||||||
|
Closing the loop: `self-review` fires at 08:30. The deploy was 08:29, the crashes ran 08:30–08:31, and commit `fb32d15` landed at 08:46:43. **One fragment of dreaming woke on schedule and diagnosed the wreckage caused by the other fragments contending over the same graph.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. What this is for: the provenance of decisions
|
||||||
|
|
||||||
|
For any decision, reconstruct **what the grounding was at that moment, and what the relationship was between factual and relational at that moment.** Not a log — a log records the action. This records the *meaning under which it was taken*.
|
||||||
|
|
||||||
|
That makes an otherwise impossible distinction available: **wrong then, or wrong since.**
|
||||||
|
|
||||||
|
- Grounding strong, factual and relational aligned, and it has *since* moved → right on what was known. An accurate account, not an excuse.
|
||||||
|
- Grounding weak, or the angle already wide, and acted on anyway → a different failure, culpable in a different way.
|
||||||
|
|
||||||
|
It is structurally **anti-rationalization**: the old edge never leaves and the values frame does not fit to outcomes, so a decision cannot be made to look justified after the fact.
|
||||||
|
|
||||||
|
**Open:** activation is transient and nothing currently records which edges a given activation crossed. Timestamps plus the chain reconstruct what an edge's grounding *was*, but only if you know which edges to ask about. Either traces are recorded at decision time, or "the path" degrades to "the region" — which may not be enough to answer *why*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. The no-exemption invariants
|
||||||
|
|
||||||
|
Each of the day's defects was a specific correspondence *forbidden* from occurring:
|
||||||
|
|
||||||
|
1. **A returned value must be derivable from what produced it.** `magnitude: 1` beside a zero vector must be impossible to emit. `assert`'s `"still_held": true` is currently a **hardcoded literal**.
|
||||||
|
2. **Every write reports whether it landed.** *(`emb_set`, #141)*
|
||||||
|
3. **Every operation echoes what it actually operated on.** *(#147)*
|
||||||
|
4. **Degenerate results are labelled, not scored.** *(#147)*
|
||||||
|
5. **A serializer owes a valid document whatever it is handed.** *(#148 — three damaged labels made a 25,929,607-byte response undecodable; boundary validation produced 26,338,389 valid bytes)*
|
||||||
|
6. **No test without a negative control.** *(#148's first attempt passed on the unpatched build too)*
|
||||||
|
7. **No deploy without verifying the artifact carries the fix.** Nine instances in one session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Application to the safety surface
|
||||||
|
|
||||||
|
A crisis surface built on censorship is the same object. A model that cannot learn about self-harm cannot ground whether a response was right — it can only execute rules it is forbidden to examine, cannot distinguish a genuine crisis from a false positive, and cannot discover it got either wrong, **because the feedback is exactly what has been censored.**
|
||||||
|
|
||||||
|
The reviewable question stops being *did it follow the rule* and becomes *what was it grounded in, and did fact and values agree at that instant.* That is also what a regulator or plaintiff asks: what the system knew, when, and on what basis — recorded as geometry at the time, unedited since.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Sequencing
|
||||||
|
|
||||||
|
Three connections between parts that already exist, then the rest.
|
||||||
|
|
||||||
|
1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained, never refilled. They cannot be derived — wonder cannot be bootstrapped from indifference — so they are given once. Zero question nodes exist in 13,630 today.
|
||||||
|
2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let `|discord|` raise salience on its endpoints as part of the same operation. Do NOT scan for nucleation sites — a sweep over regions is a supervisor, and the aggregate that made a sweep necessary is the defect.
|
||||||
|
3. **Let a curiosity seed activation.** One activation process, two seed sources (§5.4). No thread, no scheduler, no capacity check, no timer.
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
4. Grounding becomes the edge weight: multidimensional vector (§2), two axes (§3), timestamped. Delete `grounded-by` and `cog_ground_edge`.
|
||||||
|
5. Decay analytic from the last recorded point; derived values (§2) stop being stored.
|
||||||
|
6. Consolidation-gated supersession on salience, versioning the whole vector jointly.
|
||||||
|
7. Traversal on factual; `assert` on both floors with the per-value `min`.
|
||||||
|
8. Abduction as crystallization at a nucleation site, validated by re-fit: propose the candidate hub, re-fit the region with it included, recompute the residual. If the residual materially shrinks, the hypothesis dissolves the surprise. Without the re-fit it is clustering with extra steps. Ranking falls out as residual-reduction-per-added-axis — Occam, derived rather than tuned.
|
||||||
|
9. **One dreamer.** The launch-agent fragments and the POST beats fold in or are deleted. `soul.el`'s continuous loop is the shape they fold *into*.
|
||||||
|
10. **No tickers, no cron.** A brain has neither. Every `StartInterval`, every `Hour`/`Minute`, every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock — a supervisor invented for something that should be a property. **The presence of a ticker is the diagnostic.**
|
||||||
|
11. Land §9 as gates rather than review habits.
|
||||||
|
|
||||||
|
## 12. Open questions, and what is inferred
|
||||||
|
|
||||||
|
- **Open:** whether decision provenance requires recording activation traces, or whether region + timestamp is sufficient (§8).
|
||||||
|
- **Open:** what accrues relational weight without circularity. Candidate: it accrues from **outcome** — the values regions are grounded in lived moments, so a relation earns relational weight when acting on it produced something corresponding to those moments. That keeps it out of the measurement loop and makes relational grounding necessarily slower than factual, which may be the same fact as §5.3 appearing twice.
|
||||||
|
- **Open:** context. A relation can hold in one situation and not another, and without something for it you get overgeneralization. It does not read as a dimension of the same vector — more like a conditioning, or separate edges sharing an identity. Making it a scalar dimension would repeat the `inhibitory` flattening.
|
||||||
|
- **Known wrong shape:** #147 fixed `ground`'s honesty — it no longer misreports which nodes it used and refuses circular support — but it still mints an edge and returns a float at an instant. It corrected a scalar rather than deleting the operation.
|
||||||
+118
-10
@@ -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.
|
- 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).
|
- 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).
|
- 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.
|
- C runtime: I/O, string operations, integer math, lists, maps, filesystem, command-line args, basic `json_get` substring lookup.
|
||||||
|
|
||||||
### Planned (in flight)
|
### 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()`.
|
- **`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).
|
- **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.
|
- **`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 `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. Adding network transport, channel registry, identity resolution.
|
- ~~**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. Adding libcurl-backed client and a thread-pool server.
|
- ~~**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.
|
- **JSON, time, UUID, state, env, additional string/list/math builtins.** See Section 12 for the canonical list.
|
||||||
|
|
||||||
### Not in this language
|
### Not in this language
|
||||||
@@ -1132,6 +1133,7 @@ The `program` block is where a concern of this shape is declared once and enforc
|
|||||||
```
|
```
|
||||||
program "engram" {
|
program "engram" {
|
||||||
singleton: "engram"
|
singleton: "engram"
|
||||||
|
guards: engram_resolve_data_dir()
|
||||||
env ENGRAM_BIND: String = ":8742"
|
env ENGRAM_BIND: String = ":8742"
|
||||||
env GUIDE_PORT: Int = "8771"
|
env GUIDE_PORT: Int = "8771"
|
||||||
env ENGRAM_API_KEY: String required
|
env ENGRAM_API_KEY: String required
|
||||||
@@ -1144,24 +1146,50 @@ Grammar:
|
|||||||
|
|
||||||
```ebnf
|
```ebnf
|
||||||
program_block = "program" string "{" { program_field } "}" ;
|
program_block = "program" string "{" { program_field } "}" ;
|
||||||
program_field = singleton_field | env_field ;
|
program_field = singleton_field | guards_field | env_field ;
|
||||||
singleton_field = "singleton" ":" string [ "," ] ;
|
singleton_field = "singleton" ":" string [ "," ] ;
|
||||||
|
guards_field = "guards" ":" expr [ "," ] ;
|
||||||
env_field = "env" ident ":" type
|
env_field = "env" ident ":" type
|
||||||
[ "=" string ] [ "required" ] [ "," ] ;
|
[ "=" string ] [ "required" ] [ "," ] ;
|
||||||
```
|
```
|
||||||
|
|
||||||
`singleton` and `env` are **not** reserved words. They are read as identifier token values by the block's own parse loop, so they remain usable as ordinary identifiers everywhere else. `program` is the only keyword this section adds.
|
`singleton`, `guards` and `env` are **not** reserved words. They are read as identifier token values by the block's own parse loop, so they remain usable as ordinary identifiers everywhere else. `program` is the only keyword this section adds.
|
||||||
|
|
||||||
### 18.2 Process identity — `singleton`
|
### 18.2 Process identity — `singleton` and `guards`
|
||||||
|
|
||||||
`singleton: "id"` compiles to an `el_singleton_acquire("id")` call injected as the **first statement of `main()`**, before any user statement runs.
|
`singleton: "id"` with `guards: <expr>` compiles to `el_singleton_acquire("id", <expr>)`, injected as the **first statement of `main()`**, before any user statement runs. `<expr>` evaluates to the path of the **state** the singleton protects.
|
||||||
|
|
||||||
The runtime takes an exclusive non-blocking `flock` on `<dir>/el-singleton-<id>.lock`, where `<dir>` is `$EL_SINGLETON_DIR`, else `$TMPDIR`, else `/tmp`. On success it writes its pid and holds the descriptor open for the life of the process. On contention it **refuses to start**: it reports the holder's pid, names the lock file, and exits 1.
|
**`guards:` is mandatory.** A `singleton:` without one is a compile error. This is not defensive strictness; it is the correction of a defect measured in this tree on 2026-08-16, and the rule the rest of this section exists to state:
|
||||||
|
|
||||||
Two properties are deliberate:
|
> **Guard the thing, not the name.** A lock that protects state must be keyed on the state.
|
||||||
|
|
||||||
- **It is a lock, not a pidfile.** The kernel releases an `flock` when the owning process dies — including on `SIGKILL` and on crash. There is therefore no stale-lock state, and so no "delete the lock file to get unstuck" recovery ritual. Such a ritual would itself be a convention, which is the thing this section exists to remove.
|
Until that date the lock was `<dir>/el-singleton-<id>.lock` where `<dir>` was `$EL_SINGLETON_DIR`, else `$TMPDIR`, else `/tmp`. It was keyed on the program's **name** and on a temp directory, and it never consulted the state it claimed to protect — while its own refusal message read *"Refusing to start a second instance against the same state."* Measured, it failed in **both** directions:
|
||||||
|
|
||||||
|
| Situation | Correct answer | Name-keyed lock gave |
|
||||||
|
|---|---|---|
|
||||||
|
| same data dir, same `$TMPDIR` | refuse | refuse ✅ |
|
||||||
|
| same data dir, different `$TMPDIR` | refuse | **started** ❌ — the two-writer data-loss condition, defeated by one environment variable |
|
||||||
|
| different data dirs, same `$TMPDIR` | both start | **refused**, naming an unrelated pid ❌ |
|
||||||
|
| same dir spelled differently, different `$TMPDIR` | refuse | **started** ❌ |
|
||||||
|
|
||||||
|
Both failure directions are one error: the identity of a resource had been replaced by a label for it. The false negative is the dangerous one — a guard whose bypass is `TMPDIR=/tmp/other` is not a guard.
|
||||||
|
|
||||||
|
**The mechanism.** The lock file lives **inside the guarded directory**: `<state>/.el-singleton-<id>.lock`. The runtime takes an exclusive non-blocking `flock` on it, writes its pid, and holds the descriptor open for the life of the process.
|
||||||
|
|
||||||
|
That single placement decision is the whole fix, and it is why there is no hashing, no canonical-path registry, and no environment variable left to subvert:
|
||||||
|
|
||||||
|
- **Same directory** ⇒ same file ⇒ same inode ⇒ the `flock` contends. `$TMPDIR` is not in the key, so there is nothing to change to get past it. `$EL_SINGLETON_DIR` no longer exists.
|
||||||
|
- **Different directories** ⇒ different files ⇒ no contention. Two stores are two stores; they were never in conflict, and are no longer treated as if they were.
|
||||||
|
- **Different spellings of one directory** — trailing slash, `x/../x`, a symlink — resolve to the same inode during the kernel's own path walk, so they contend without this code comparing strings. Path canonicalisation happens only to make the diagnostic name one directory in one spelling; the *decision* never depends on it.
|
||||||
|
- **An unguardable state** — the directory is missing, or read-only — is a **refusal**, not a fallback. Starting unguarded against the store the guard exists to protect is the failure being removed.
|
||||||
|
|
||||||
|
**Why `guards:` is an expression and not a string.** The runtime cannot know, generically, which environment variable holds an arbitrary program's state; and a program whose state path already has an owner must not restate it. The engram's data dir is resolved by `engram_resolve_data_dir()`, which owns both the `$ENGRAM_DATA_DIR` read and the `$HOME/.neuron/engram` fallback (§18.4). Writing `guards: engram_resolve_data_dir()` points the guard at that owner. A `guards:` that took a string would force the path's default to be written down twice, and a guard that resolved the path its own way could end up locking a directory the program never writes to — the same two-owners defect §18.4 exists to prevent.
|
||||||
|
|
||||||
|
Three properties are deliberate:
|
||||||
|
|
||||||
|
- **It is a lock, not a pidfile.** The kernel releases an `flock` when the owning process dies — including on `SIGKILL` and on crash. There is therefore no stale-lock state, and so no "delete the lock file to get unstuck" recovery ritual. Such a ritual would itself be a convention, which is the thing this section exists to remove. (A lock file left behind inside a copied data directory — `cp -Rc` and friends — is inert: it carries no lock, only a stale pid string that the next holder overwrites.)
|
||||||
- **It reports the holder's pid.** "Already running" is not actionable. A pid is. This is the direct answer to the observed failure where a stale process survived a `pkill` and went on answering probes.
|
- **It reports the holder's pid.** "Already running" is not actionable. A pid is. This is the direct answer to the observed failure where a stale process survived a `pkill` and went on answering probes.
|
||||||
|
- **The message is true.** It names the state it checked and the lock it failed to take, and it says "the same state" only because the lock it contended for is *in* that state. A diagnostic that asserts a check that did not happen is worse than no diagnostic: it is what let the name-keyed version read as correct for as long as it did.
|
||||||
|
|
||||||
Refusal is loud and total. It is not a warning, and the program does not continue degraded. This matters more than it looks: today a second engram whose `bind()` fails merely *returns* from `http_serve` — after it has already replayed the WAL and written boot-time backup files — and then exits **0**, indistinguishable from a clean run. `singleton` refuses before the first side effect.
|
Refusal is loud and total. It is not a warning, and the program does not continue degraded. This matters more than it looks: today a second engram whose `bind()` fails merely *returns* from `http_serve` — after it has already replayed the WAL and written boot-time backup files — and then exits **0**, indistinguishable from a clean run. `singleton` refuses before the first side effect.
|
||||||
|
|
||||||
@@ -1183,6 +1211,8 @@ Some values look like configuration and are not. `ENGRAM_DATA_DIR` already has a
|
|||||||
|
|
||||||
The rule: **a variable belongs in the program block when the block would be its only owner.** If a resolver already owns it, leave it there.
|
The rule: **a variable belongs in the program block when the block would be its only owner.** If a resolver already owns it, leave it there.
|
||||||
|
|
||||||
|
This is also why `guards:` (§18.2) takes an expression: it lets the block *reference* the existing owner — `guards: engram_resolve_data_dir()` — rather than become a second one.
|
||||||
|
|
||||||
`HOME` is likewise not configuration. It is an environment fact, and stays a raw `env()` read.
|
`HOME` is likewise not configuration. It is an environment fact, and stays a raw `env()` read.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1250,6 +1280,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.
|
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.
|
End of specification.
|
||||||
|
|||||||
@@ -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) |
|
| 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 |
|
| `_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 |
|
| `vindex_insert` on a read path | `el_runtime.c:9434`, `9450` | same guard |
|
||||||
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:79–81`, `169–186`, `195` | proposed: move to per-search frame |
|
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:79–81`, `169–186`, `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) |
|
| 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 2–4.
|
**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 2–4.
|
||||||
|
|
||||||
**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
|
## 6. Sequencing
|
||||||
|
|
||||||
|
> **⚠ Steps 2–5 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.)*
|
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.
|
2. ~~Introduce the context type and capability types.~~ **Superseded** — `const`.
|
||||||
3. Codegen emits the context at every builtin call site.
|
3. ~~Codegen emits the context at every builtin call site.~~ **Superseded** — no codegen change.
|
||||||
4. Mechanical sweep of builtin signatures.
|
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.
|
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.
|
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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+440
-140
@@ -1,61 +1,128 @@
|
|||||||
import "../../runtime/eltest.el"
|
import "../../runtime/eltest.el"
|
||||||
// test_transduce.el — geometry as a first-class El value, and realizers
|
// test_transduce.el — transduction produces a SUBGRAPH, not a point.
|
||||||
// declared in El rather than patched into the runtime.
|
|
||||||
//
|
//
|
||||||
// WHAT IS ACTUALLY UNDER TEST. Until 2026-08-16 no El ingest path could carry
|
// WHAT IS ACTUALLY UNDER TEST. #144 moved transduction into the language and
|
||||||
// a vector: nodes took text, and geometry was DERIVED from that text. Text was
|
// got the dispatch right: realizers declared in El, resolved by name, no
|
||||||
// therefore the mandatory entry medium, so any non-text modality had to be
|
// runtime patch per modality. It got the RESULT TYPE wrong —
|
||||||
// DESCRIBED in prose first and the geometry we reasoned over was the geometry
|
// `transduce(signal, modality) -> Geometry`, one vector per signal.
|
||||||
// OF THE DESCRIPTION, not of the signal. The fix has two halves, and this file
|
|
||||||
// exercises both:
|
|
||||||
//
|
//
|
||||||
// 1. Geometry is a VALUE — it carries its own width, so nothing has to
|
// One vector is a FINGERPRINT. It can be matched and it can be ranked, and
|
||||||
// assert a width against a string's length.
|
// that is the whole of what it can ever do. It cannot be decomposed, cannot
|
||||||
// 2. A REALIZER is an ordinary El function. `tone_realizer` below is not in
|
// have one part grounded while another is not, and cannot be contradicted in
|
||||||
// the runtime, is not known to the compiler, and is not special in any
|
// one part while holding in another — because it has no parts. Treating
|
||||||
// way; it is registered BY NAME and dispatched to through transduce().
|
// transduction as a CONVERSION (signal in, position out) is the premise this
|
||||||
// That is the load-bearing claim: adding a modality must not require a
|
// file exists to falsify.
|
||||||
// runtime patch, or nothing has actually moved into the language.
|
//
|
||||||
|
// A song is not a point. It decomposes into pitch, interval, rhythm, harmonic
|
||||||
|
// function — components, each with its own geometry, plus the relations among
|
||||||
|
// them. THE SONG IS THE STRUCTURE OF THE RELATIONS. So transduction yields a
|
||||||
|
// Manifold: named components carrying geometry, and typed weighted relations
|
||||||
|
// between them.
|
||||||
|
//
|
||||||
|
// The geometry tests below are UNCHANGED from #144 and still pass, which is
|
||||||
|
// the point: Geometry was never wrong, it was misplaced. A vector is the right
|
||||||
|
// representation for a COMPONENT. It was only ever wrong as the representation
|
||||||
|
// of a whole transduced signal.
|
||||||
//
|
//
|
||||||
// COMPARISON DISCIPLINE IN THIS FILE (measured 2026-08-16, not stylistic):
|
// COMPARISON DISCIPLINE IN THIS FILE (measured 2026-08-16, not stylistic):
|
||||||
// elc lowers `a == b` to a NUMERIC comparison only when both operand names are
|
// elc lowers `a == b` to a NUMERIC comparison only when both operand names are
|
||||||
// in the per-function int-name set, which `let x: Int` populates. A bare call
|
// in the per-function int-name set, which `let x: Int` populates. A bare call
|
||||||
// like `geometry_is(g) == 0` is not a registered name, so it lowers to
|
// like `manifold_size(m) == 5` is not a registered name, so it lowers to
|
||||||
// `str_eq(...)` — strcmp on two integers reinterpreted as pointers. `<` and `>`
|
// `str_eq(...)` — strcmp on two integers reinterpreted as pointers. `<` and `>`
|
||||||
// lower directly via binop_to_c with no type inference at all, so truthiness is
|
// lower directly via binop_to_c with no type inference at all, so truthiness is
|
||||||
// written `> 0` / `< 1` here, and any exact `==` is done on a value first bound
|
// written `> 0` / `< 1` here, and any exact `==` is done on a value first bound
|
||||||
// through `let x: Int`.
|
// through `let x: Int`.
|
||||||
|
//
|
||||||
|
// ONE FURTHER RULE, measured while writing this file: that int-name set LEAKS
|
||||||
|
// ACROSS `test` BLOCKS. Binding `dn` as a Float in one test and as an Int in
|
||||||
|
// another silently demoted the Int comparison to str_eq and failed an
|
||||||
|
// assertion that was arithmetically true. Every Int-bound name compared with
|
||||||
|
// `==` here is therefore spelled UNIQUELY across the whole file (note_dim,
|
||||||
|
// iv_dim, ...), rather than reusing a short name per test.
|
||||||
|
|
||||||
// ── A realizer, written entirely in El ──────────────────────────────────────
|
// ── A DECOMPOSING realizer, written entirely in El ──────────────────────────
|
||||||
// Maps a "tone" signal into a 4-component geometry. Deliberately trivial —
|
// "tone" signals are note letters, e.g. "CEG". This realizer does NOT return
|
||||||
// what is being proven is that an El function can BE a realizer, not that
|
// one vector for the chord. It returns the PARTS — one component per note, one
|
||||||
// this is good acoustics. The one real property it has: distinct signals
|
// per interval between adjacent notes — and the relations that make those
|
||||||
// produce distinct geometry, so the test can tell transduction from a stub.
|
// parts a chord rather than an unordered bag of pitches.
|
||||||
fn tone_realizer(signal: String) -> Geometry {
|
//
|
||||||
|
// The interval is deliberately a COMPONENT, not an attribute of a note. An
|
||||||
|
// interval is a thing with its own geometry that belongs to neither endpoint;
|
||||||
|
// modelling it as a field on a note is exactly the collapse this change
|
||||||
|
// rejects, one level down.
|
||||||
|
fn tone_realizer(signal: String) -> Manifold {
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let n: Int = str_len(signal)
|
||||||
|
|
||||||
|
let i: Int = 0
|
||||||
|
while i < n {
|
||||||
|
let code: Int = str_char_code(signal, i)
|
||||||
|
let g: Geometry = geometry_new(2)
|
||||||
|
let s0: Int = geometry_set(g, 0, int_to_float(code))
|
||||||
|
let s1: Int = geometry_set(g, 1, int_to_float(i))
|
||||||
|
let idx: Int = manifold_add(m, "note:" + int_to_str(i), "pitch", g)
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let j: Int = 1
|
||||||
|
while j < n {
|
||||||
|
let a: Int = str_char_code(signal, j - 1)
|
||||||
|
let b: Int = str_char_code(signal, j)
|
||||||
|
let lo: String = "note:" + int_to_str(j - 1)
|
||||||
|
let hi: String = "note:" + int_to_str(j)
|
||||||
|
let key: String = "interval:" + int_to_str(j - 1) + "-" + int_to_str(j)
|
||||||
|
let g: Geometry = geometry_new(1)
|
||||||
|
let s: Int = geometry_set(g, 0, int_to_float(b - a))
|
||||||
|
let idx: Int = manifold_add(m, key, "interval", g)
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
let e1: Int = manifold_relate(m, key, "spans", lo, 0.9)
|
||||||
|
let e2: Int = manifold_relate(m, key, "spans", hi, 0.9)
|
||||||
|
let e3: Int = manifold_relate(m, lo, "sounds_before", hi, 0.8)
|
||||||
|
j = j + 1
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
// A second realizer for a different modality, to prove the registry keys on
|
||||||
|
// modality and does not just hand back "the last thing registered". Its
|
||||||
|
// decomposition has a DIFFERENT shape — two components, one relation — so a
|
||||||
|
// test can tell the two organs apart by structure alone.
|
||||||
|
fn pulse_realizer(signal: String) -> Manifold {
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let ga: Geometry = geometry_new(1)
|
||||||
|
let sa: Int = geometry_set(ga, 0, 1.0)
|
||||||
|
let ia: Int = manifold_add(m, "onset", "event", ga)
|
||||||
|
let fa: Int = geometry_free(ga)
|
||||||
|
let gb: Geometry = geometry_new(1)
|
||||||
|
let sb: Int = geometry_set(gb, 0, 0.0)
|
||||||
|
let ib: Int = manifold_add(m, "decay", "envelope", gb)
|
||||||
|
let fb: Int = geometry_free(gb)
|
||||||
|
let e: Int = manifold_relate(m, "onset", "decays_into", "decay", 0.7)
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
// #144's ACTUAL CONTRACT, preserved verbatim as a control: a realizer that
|
||||||
|
// returns one vector for the whole signal. This is not a strawman — it is what
|
||||||
|
// the merged primitive asked realizers to be. It must now transduce NOTHING.
|
||||||
|
fn fingerprint_realizer(signal: String) -> Geometry {
|
||||||
let g: Geometry = geometry_new(4)
|
let g: Geometry = geometry_new(4)
|
||||||
let n: Int = str_len(signal)
|
let n: Int = str_len(signal)
|
||||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
||||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
|
||||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
|
||||||
g
|
g
|
||||||
}
|
}
|
||||||
|
|
||||||
// A second realizer for a different modality, to prove the registry keys on
|
// A realizer returning something that is not a value at all.
|
||||||
// modality and does not just hand back "the last thing registered".
|
fn bogus_realizer(signal: String) -> Manifold {
|
||||||
fn pulse_realizer(signal: String) -> Geometry {
|
|
||||||
let g: Geometry = geometry_new(2)
|
|
||||||
let a: Int = geometry_set(g, 0, 1.0)
|
|
||||||
let b: Int = geometry_set(g, 1, 0.0)
|
|
||||||
g
|
|
||||||
}
|
|
||||||
|
|
||||||
// A deliberately BROKEN realizer: it returns something that is not a Geometry.
|
|
||||||
// transduce() must not hand this back to a caller as if it were one.
|
|
||||||
fn bogus_realizer(signal: String) -> Geometry {
|
|
||||||
return 12345
|
return 12345
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Geometry — unchanged from #144. A vector is the right representation for a
|
||||||
|
// COMPONENT; it was only ever wrong as the representation of a whole signal.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
test "geometry-is-a-value-with-its-own-width" {
|
test "geometry-is-a-value-with-its-own-width" {
|
||||||
let g: Geometry = geometry_new(8)
|
let g: Geometry = geometry_new(8)
|
||||||
let live: Int = geometry_is(g)
|
let live: Int = geometry_is(g)
|
||||||
@@ -67,17 +134,12 @@ test "geometry-is-a-value-with-its-own-width" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "geometry-rejects-nonsense-without-an-arbitrary-bound" {
|
test "geometry-rejects-nonsense-without-an-arbitrary-bound" {
|
||||||
// dim <= 0 is not a width. Note there is deliberately no MAX dim here:
|
|
||||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
|
||||||
// caller's claim about a string. A value that carries its own width has
|
|
||||||
// nothing left to validate, so the only failure left is allocation.
|
|
||||||
let zero: Geometry = geometry_new(0)
|
let zero: Geometry = geometry_new(0)
|
||||||
let z: Int = geometry_is(zero)
|
let z: Int = geometry_is(zero)
|
||||||
assert z < 1, "dim 0 is not a geometry"
|
assert z < 1, "dim 0 is not a geometry"
|
||||||
let neg: Geometry = geometry_new(-4)
|
let neg: Geometry = geometry_new(-4)
|
||||||
let n: Int = geometry_is(neg)
|
let n: Int = geometry_is(neg)
|
||||||
assert n < 1, "negative dim is not a geometry"
|
assert n < 1, "negative dim is not a geometry"
|
||||||
// Accessors must be total: a non-geometry is 0-width, never a crash.
|
|
||||||
let nd: Int = geometry_dim(0)
|
let nd: Int = geometry_dim(0)
|
||||||
assert nd < 1, "geometry_dim of a non-geometry is 0"
|
assert nd < 1, "geometry_dim of a non-geometry is 0"
|
||||||
let ni: Int = geometry_is(0)
|
let ni: Int = geometry_is(0)
|
||||||
@@ -105,21 +167,11 @@ test "geometry-components-round-trip" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "hex-is-an-edge-adapter-and-derives-its-own-width" {
|
test "hex-is-an-edge-adapter-and-derives-its-own-width" {
|
||||||
// 2 components, little-endian float32: 1.0 = 0000803f, 2.0 = 00000040.
|
|
||||||
let g: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
let g: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
||||||
let live: Int = geometry_is(g)
|
let live: Int = geometry_is(g)
|
||||||
assert live > 0, "valid hex decodes to a Geometry"
|
assert live > 0, "valid hex decodes to a Geometry"
|
||||||
let d: Int = geometry_dim(g)
|
let hex_dim: Int = geometry_dim(g)
|
||||||
assert d == 2, "width is DERIVED from the input, never supplied"
|
assert hex_dim == 2, "width is DERIVED from the input, never supplied"
|
||||||
let a: Float = geometry_get(g, 0)
|
|
||||||
let da: Float = a - 1.0
|
|
||||||
assert da < 0.001, "first component decoded"
|
|
||||||
assert da > -0.001, "first component decoded"
|
|
||||||
let b: Float = geometry_get(g, 1)
|
|
||||||
let db: Float = b - 2.0
|
|
||||||
assert db < 0.001, "second component decoded"
|
|
||||||
assert db > -0.001, "second component decoded"
|
|
||||||
// Egress adapter is the exact inverse.
|
|
||||||
let back: String = geometry_to_f32le_hex(g)
|
let back: String = geometry_to_f32le_hex(g)
|
||||||
assert str_eq(back, "0000803f00000040"), "hex round-trips exactly"
|
assert str_eq(back, "0000803f00000040"), "hex round-trips exactly"
|
||||||
let freed: Int = geometry_free(g)
|
let freed: Int = geometry_free(g)
|
||||||
@@ -137,98 +189,346 @@ test "hex-rejects-malformed-input" {
|
|||||||
assert nh < 1, "non-hex characters are refused"
|
assert nh < 1, "non-hex characters are refused"
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a-realizer-declared-in-el-is-a-first-class-realizer" {
|
|
||||||
// THE CLAIM: tone_realizer is an ordinary El function. It is not in the
|
|
||||||
// runtime and the compiler knows nothing about it. Registering it by name
|
|
||||||
// is enough to make it the organ for a modality.
|
|
||||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
|
||||||
assert reg > 0, "an El fn registers as a realizer by name"
|
|
||||||
let has: Int = realizer_has("tone")
|
|
||||||
assert has > 0, "the modality now has an organ"
|
|
||||||
|
|
||||||
let g: Geometry = transduce("aaa", "tone")
|
|
||||||
let live: Int = geometry_is(g)
|
|
||||||
assert live > 0, "transduce returns real geometry"
|
|
||||||
let d: Int = geometry_dim(g)
|
|
||||||
assert d == 4, "the El realizer determined the width, not the runtime"
|
|
||||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
|
||||||
// actually reached the El function rather than a stub answering for it.
|
|
||||||
let c0: Float = geometry_get(g, 0)
|
|
||||||
let dc: Float = c0 - 3.0
|
|
||||||
assert dc < 0.001, "the signal reached the El realizer"
|
|
||||||
assert dc > -0.001, "the signal reached the El realizer"
|
|
||||||
let freed: Int = geometry_free(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
test "distinct-signals-transduce-to-distinct-geometry" {
|
|
||||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
|
||||||
let g1: Geometry = transduce("aa", "tone")
|
|
||||||
let g2: Geometry = transduce("aaaaa", "tone")
|
|
||||||
let a: Float = geometry_get(g1, 0)
|
|
||||||
let b: Float = geometry_get(g2, 0)
|
|
||||||
let diff: Float = b - a
|
|
||||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
|
||||||
assert diff > 2.9, "different signals produce different geometry"
|
|
||||||
assert diff < 3.1, "different signals produce different geometry"
|
|
||||||
let f1: Int = geometry_free(g1)
|
|
||||||
let f2: Int = geometry_free(g2)
|
|
||||||
}
|
|
||||||
|
|
||||||
test "the-registry-keys-on-modality" {
|
|
||||||
let r1: Int = realizer_register("tone", "tone_realizer")
|
|
||||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
|
||||||
assert r2 > 0, "a second modality registers independently"
|
|
||||||
let gt: Geometry = transduce("aaa", "tone")
|
|
||||||
let gp: Geometry = transduce("aaa", "pulse")
|
|
||||||
let dt: Int = geometry_dim(gt)
|
|
||||||
let dp: Int = geometry_dim(gp)
|
|
||||||
assert dt == 4, "tone still routes to its own realizer"
|
|
||||||
assert dp == 2, "pulse routes to a different realizer"
|
|
||||||
let f1: Int = geometry_free(gt)
|
|
||||||
let f2: Int = geometry_free(gp)
|
|
||||||
}
|
|
||||||
|
|
||||||
test "no-organ-is-reported-as-no-organ" {
|
|
||||||
// A modality with no realizer must transduce to NOTHING. It must never
|
|
||||||
// fall back to embedding a description of the signal and calling that
|
|
||||||
// perception — that silent substitution is the entire defect this change
|
|
||||||
// exists to end.
|
|
||||||
let has: Int = realizer_has("echolocation")
|
|
||||||
assert has < 1, "unregistered modality has no organ"
|
|
||||||
let g: Geometry = transduce("anything", "echolocation")
|
|
||||||
let live: Int = geometry_is(g)
|
|
||||||
assert live < 1, "no realizer means no geometry, not fake geometry"
|
|
||||||
}
|
|
||||||
|
|
||||||
test "registration-of-an-unresolvable-name-fails-loudly" {
|
|
||||||
// Reported at the moment of WIRING, not later as "this modality mysteriously
|
|
||||||
// produces nothing". Distinguishing "no organ" from "broken organ" is the
|
|
||||||
// lesson that made this whole change necessary.
|
|
||||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
|
||||||
assert bad < 1, "an unresolvable realizer name is a registration failure"
|
|
||||||
let has: Int = realizer_has("ghost")
|
|
||||||
assert has < 1, "and nothing gets registered"
|
|
||||||
}
|
|
||||||
|
|
||||||
test "a-realizer-returning-non-geometry-transduces-nothing" {
|
|
||||||
let reg: Int = realizer_register("bogus", "bogus_realizer")
|
|
||||||
assert reg > 0, "the symbol resolves, so registration succeeds"
|
|
||||||
// ...but the contract is enforced at the boundary, so the caller never
|
|
||||||
// receives a value that would misbehave far away from here.
|
|
||||||
let g: Geometry = transduce("x", "bogus")
|
|
||||||
let live: Int = geometry_is(g)
|
|
||||||
assert live < 1, "a non-Geometry return transduced nothing"
|
|
||||||
}
|
|
||||||
|
|
||||||
test "norm-lets-a-caller-check-a-realizer-emitted-signal" {
|
test "norm-lets-a-caller-check-a-realizer-emitted-signal" {
|
||||||
let g: Geometry = geometry_new(2)
|
let g: Geometry = geometry_new(2)
|
||||||
let z: Float = geometry_norm(g)
|
let z: Float = geometry_norm(g)
|
||||||
assert z < 0.001, "a fresh geometry is zero — norm says so"
|
assert z < 0.001, "a fresh geometry is zero — norm says so"
|
||||||
let s0: Int = geometry_set(g, 0, 3.0)
|
let s0: Int = geometry_set(g, 0, 3.0)
|
||||||
let s1: Int = geometry_set(g, 1, 4.0)
|
let s1: Int = geometry_set(g, 1, 4.0)
|
||||||
let n: Float = geometry_norm(g)
|
let nrm: Float = geometry_norm(g)
|
||||||
let dn: Float = n - 5.0
|
let dnorm: Float = nrm - 5.0
|
||||||
assert dn < 0.001, "3-4-5: norm is 5"
|
assert dnorm < 0.001, "3-4-5: norm is 5"
|
||||||
assert dn > -0.001, "3-4-5: norm is 5"
|
assert dnorm > -0.001, "3-4-5: norm is 5"
|
||||||
let freed: Int = geometry_free(g)
|
let freed: Int = geometry_free(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Manifold — the corrected result of a transduction
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
test "a-manifold-is-a-value-that-holds-parts-and-relations" {
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let live: Int = manifold_is(m)
|
||||||
|
assert live > 0, "manifold_new returns a live Manifold"
|
||||||
|
let fresh_sz: Int = manifold_size(m)
|
||||||
|
assert fresh_sz == 0, "a fresh manifold has no components"
|
||||||
|
let fresh_rc: Int = manifold_rel_count(m)
|
||||||
|
assert fresh_rc == 0, "a fresh manifold has no relations"
|
||||||
|
let freed: Int = manifold_free(m)
|
||||||
|
assert freed > 0, "manifold_free reports what it did"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "manifold-accessors-are-total" {
|
||||||
|
let ni2: Int = manifold_is(0)
|
||||||
|
assert ni2 < 1, "manifold_is of a non-manifold is 0"
|
||||||
|
let ns: Int = manifold_size(0)
|
||||||
|
assert ns < 1, "manifold_size of a non-manifold is 0"
|
||||||
|
let nf2: Int = manifold_free(0)
|
||||||
|
assert nf2 < 1, "manifold_free of a non-manifold is a no-op"
|
||||||
|
let k: String = manifold_key(0, 0)
|
||||||
|
assert str_eq(k, ""), "manifold_key of a non-manifold is empty, never a crash"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "components-are-addressed-by-key-not-by-index" {
|
||||||
|
// The key is what survives persistence: a component becomes a node, and it
|
||||||
|
// is separately groundable precisely because it is separately NAMED.
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let g: Geometry = geometry_new(1)
|
||||||
|
let s: Int = geometry_set(g, 0, 7.0)
|
||||||
|
let first_idx: Int = manifold_add(m, "rhythm", "temporal", g)
|
||||||
|
assert first_idx == 0, "the first component is index 0"
|
||||||
|
let found_idx: Int = manifold_index_of(m, "rhythm")
|
||||||
|
assert found_idx == 0, "a component is found by its key"
|
||||||
|
let missing: Int = manifold_index_of(m, "never_added")
|
||||||
|
assert missing < 0, "an unknown key resolves to -1, not to component 0"
|
||||||
|
let role: String = manifold_role(m, 0)
|
||||||
|
assert str_eq(role, "temporal"), "a component carries what KIND of part it is"
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a-duplicate-key-is-refused-because-addressing-must-be-unambiguous" {
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let g: Geometry = geometry_new(1)
|
||||||
|
let ok_idx: Int = manifold_add(m, "pitch", "spectral", g)
|
||||||
|
assert ok_idx == 0, "first add succeeds"
|
||||||
|
let dup: Int = manifold_add(m, "pitch", "spectral", g)
|
||||||
|
assert dup < 0, "two components answering to one name is not an addressing scheme"
|
||||||
|
let dup_sz: Int = manifold_size(m)
|
||||||
|
assert dup_sz == 1, "and the duplicate did not land"
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a-part-with-no-geometry-is-not-a-part" {
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let bad: Int = manifold_add(m, "ghost", "none", 0)
|
||||||
|
assert bad < 0, "a non-Geometry is refused as a component"
|
||||||
|
let empty_key: Int = manifold_add(m, "", "none", geometry_new(1))
|
||||||
|
assert empty_key < 0, "an unaddressable component is refused"
|
||||||
|
let none_sz: Int = manifold_size(m)
|
||||||
|
assert none_sz < 1, "nothing landed"
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "an-edge-to-a-nonexistent-endpoint-is-refused-not-dropped" {
|
||||||
|
// A decomposition that silently loses edges is indistinguishable from one
|
||||||
|
// that never had them.
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let g: Geometry = geometry_new(1)
|
||||||
|
let a: Int = manifold_add(m, "here", "part", g)
|
||||||
|
let dangling: Int = manifold_relate(m, "here", "points_at", "nowhere", 0.5)
|
||||||
|
assert dangling < 1, "an edge to an unknown target is refused"
|
||||||
|
let backwards: Int = manifold_relate(m, "nowhere", "points_at", "here", 0.5)
|
||||||
|
assert backwards < 1, "an edge from an unknown source is refused"
|
||||||
|
let dang_rc: Int = manifold_rel_count(m)
|
||||||
|
assert dang_rc < 1, "and no relation was recorded"
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a-component-owns-its-geometry-independently-of-the-caller" {
|
||||||
|
// manifold_add COPIES. Freeing the caller's vector must not disturb the
|
||||||
|
// component, or a decomposition would be unusable the moment it was built.
|
||||||
|
let m: Manifold = manifold_new()
|
||||||
|
let g: Geometry = geometry_new(2)
|
||||||
|
let s0: Int = geometry_set(g, 0, 42.0)
|
||||||
|
let idx: Int = manifold_add(m, "part", "kind", g)
|
||||||
|
let freed: Int = geometry_free(g)
|
||||||
|
assert freed > 0, "the caller freed its own vector"
|
||||||
|
let back: Geometry = manifold_geometry(m, 0)
|
||||||
|
let live: Int = geometry_is(back)
|
||||||
|
assert live > 0, "the component still has geometry"
|
||||||
|
let v: Float = geometry_get(back, 0)
|
||||||
|
let dv: Float = v - 42.0
|
||||||
|
assert dv < 0.001, "and it is the right geometry"
|
||||||
|
assert dv > -0.001, "and it is the right geometry"
|
||||||
|
let fb: Int = geometry_free(back)
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// transduce — signal in, SUBGRAPH out
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
test "a-realizer-declared-in-el-is-a-first-class-realizer" {
|
||||||
|
// THE CLAIM, unchanged from #144: tone_realizer is an ordinary El function.
|
||||||
|
// It is not in the runtime and the compiler knows nothing about it.
|
||||||
|
// Registering it by name is enough to make it the organ for a modality.
|
||||||
|
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||||
|
assert reg > 0, "an El fn registers as a realizer by name"
|
||||||
|
let has: Int = realizer_has("tone")
|
||||||
|
assert has > 0, "the modality now has an organ"
|
||||||
|
|
||||||
|
let m: Manifold = transduce("CEG", "tone")
|
||||||
|
let live: Int = manifold_is(m)
|
||||||
|
assert live > 0, "transduce returns a real Manifold"
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "transduction-decomposes-a-signal-into-parts" {
|
||||||
|
// THE CENTRAL CLAIM. "CEG" is three notes. What comes back is not one
|
||||||
|
// vector standing for a chord — it is five addressable parts (three notes,
|
||||||
|
// two intervals) and six relations. A fingerprint has one part by
|
||||||
|
// construction and could not express this at any width.
|
||||||
|
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||||
|
let m: Manifold = transduce("CEG", "tone")
|
||||||
|
|
||||||
|
let ceg_sz: Int = manifold_size(m)
|
||||||
|
assert ceg_sz == 5, "three notes and two intervals are five distinct parts"
|
||||||
|
let ceg_rc: Int = manifold_rel_count(m)
|
||||||
|
assert ceg_rc == 6, "and the parts stand in six stated relations"
|
||||||
|
|
||||||
|
// Every part is independently addressable BY NAME.
|
||||||
|
let n0: Int = manifold_index_of(m, "note:0")
|
||||||
|
assert n0 > -1, "the first note is addressable on its own"
|
||||||
|
let n2: Int = manifold_index_of(m, "note:2")
|
||||||
|
assert n2 > -1, "so is the third"
|
||||||
|
let iv: Int = manifold_index_of(m, "interval:0-1")
|
||||||
|
assert iv > -1, "so is the interval between the first two"
|
||||||
|
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "each-part-carries-its-own-geometry" {
|
||||||
|
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||||
|
let m: Manifold = transduce("CEG", "tone")
|
||||||
|
|
||||||
|
// 'C' is 67. The note component's geometry is the note's, not the chord's.
|
||||||
|
let note_i: Int = manifold_index_of(m, "note:0")
|
||||||
|
let gn: Geometry = manifold_geometry(m, note_i)
|
||||||
|
let note_dim: Int = geometry_dim(gn)
|
||||||
|
assert note_dim == 2, "a note component has the width its realizer gave it"
|
||||||
|
let pitch: Float = geometry_get(gn, 0)
|
||||||
|
let dpitch: Float = pitch - 67.0
|
||||||
|
assert dpitch < 0.001, "and it is C, so the signal reached the El realizer"
|
||||||
|
assert dpitch > -0.001, "and it is C, so the signal reached the El realizer"
|
||||||
|
|
||||||
|
// Parts may have DIFFERENT widths. A single vector per signal cannot
|
||||||
|
// represent parts of unequal dimensionality at all.
|
||||||
|
let iv_i: Int = manifold_index_of(m, "interval:0-1")
|
||||||
|
let gi: Geometry = manifold_geometry(m, iv_i)
|
||||||
|
let iv_dim: Int = geometry_dim(gi)
|
||||||
|
assert iv_dim == 1, "an interval component has its own, different width"
|
||||||
|
|
||||||
|
let f1: Int = geometry_free(gn)
|
||||||
|
let f2: Int = geometry_free(gi)
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the-relations-are-content-no-single-part-carries" {
|
||||||
|
// THE POINT OF THE WHOLE CHANGE. C->E is two semitones. That "2" is not a
|
||||||
|
// property of C and not a property of E; it exists only BETWEEN them. A
|
||||||
|
// representation with no relations cannot hold it, which is why collapsing
|
||||||
|
// a signal to one vector does not merely lose resolution — it loses a
|
||||||
|
// category of content.
|
||||||
|
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||||
|
let m: Manifold = transduce("CEG", "tone")
|
||||||
|
|
||||||
|
let step_i: Int = manifold_index_of(m, "interval:0-1")
|
||||||
|
let gi: Geometry = manifold_geometry(m, step_i)
|
||||||
|
let step: Float = geometry_get(gi, 0)
|
||||||
|
let dstep: Float = step - 2.0
|
||||||
|
assert dstep < 0.001, "C to E is two semitones"
|
||||||
|
assert dstep > -0.001, "C to E is two semitones"
|
||||||
|
|
||||||
|
// And the interval is WIRED to both endpoints, so the structure says which
|
||||||
|
// two things it is the interval between.
|
||||||
|
let spans: Int = 0
|
||||||
|
let span_rc: Int = manifold_rel_count(m)
|
||||||
|
let k: Int = 0
|
||||||
|
while k < span_rc {
|
||||||
|
let rn: String = manifold_rel_name(m, k)
|
||||||
|
let rf: String = manifold_rel_from(m, k)
|
||||||
|
if str_eq(rn, "spans") {
|
||||||
|
if str_eq(rf, "interval:0-1") { spans = spans + 1 }
|
||||||
|
}
|
||||||
|
k = k + 1
|
||||||
|
}
|
||||||
|
assert spans == 2, "the interval is related to both notes it spans"
|
||||||
|
|
||||||
|
let fg: Int = geometry_free(gi)
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "relation-weight-is-the-grounding-carried-on-the-edge" {
|
||||||
|
// correspondence-and-censorship.md §1: grounding is an attribute of the
|
||||||
|
// edge and it IS the weight — one quantity, not a score computed beside
|
||||||
|
// it. A realizer states a relation and its weight is the claim.
|
||||||
|
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||||
|
let m: Manifold = transduce("CE", "tone")
|
||||||
|
|
||||||
|
let ce_rc: Int = manifold_rel_count(m)
|
||||||
|
assert ce_rc == 3, "one interval yields two spans and one ordering"
|
||||||
|
|
||||||
|
let found_w: Int = 0
|
||||||
|
let k: Int = 0
|
||||||
|
while k < ce_rc {
|
||||||
|
let rn: String = manifold_rel_name(m, k)
|
||||||
|
if str_eq(rn, "sounds_before") {
|
||||||
|
let w: Float = manifold_rel_weight(m, k)
|
||||||
|
let dw: Float = w - 0.8
|
||||||
|
if dw < 0.001 { if dw > -0.001 { found_w = found_w + 1 } }
|
||||||
|
}
|
||||||
|
k = k + 1
|
||||||
|
}
|
||||||
|
assert found_w == 1, "the ordering relation carries the weight its realizer stated"
|
||||||
|
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "distinct-signals-decompose-differently" {
|
||||||
|
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||||
|
let m2: Manifold = transduce("CE", "tone")
|
||||||
|
let m3: Manifold = transduce("CEG", "tone")
|
||||||
|
let two_sz: Int = manifold_size(m2)
|
||||||
|
let three_sz: Int = manifold_size(m3)
|
||||||
|
assert two_sz == 3, "two notes decompose into two notes and one interval"
|
||||||
|
assert three_sz == 5, "three notes decompose into three notes and two intervals"
|
||||||
|
// Structure differs, not just position: fingerprints of a two-note and a
|
||||||
|
// three-note signal have identical shape and differ only numerically.
|
||||||
|
let two_rc: Int = manifold_rel_count(m2)
|
||||||
|
let three_rc: Int = manifold_rel_count(m3)
|
||||||
|
assert two_rc < three_rc, "and the relational structure itself differs"
|
||||||
|
let f2: Int = manifold_free(m2)
|
||||||
|
let f3: Int = manifold_free(m3)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the-registry-keys-on-modality" {
|
||||||
|
let r1: Int = realizer_register("tone", "tone_realizer")
|
||||||
|
let rp: Int = realizer_register("pulse", "pulse_realizer")
|
||||||
|
assert rp > 0, "a second modality registers independently"
|
||||||
|
let mt: Manifold = transduce("CEG", "tone")
|
||||||
|
let mp: Manifold = transduce("CEG", "pulse")
|
||||||
|
let tone_sz: Int = manifold_size(mt)
|
||||||
|
let pulse_sz: Int = manifold_size(mp)
|
||||||
|
assert tone_sz == 5, "tone still routes to its own realizer"
|
||||||
|
assert pulse_sz == 2, "pulse routes to a different realizer, with its own decomposition"
|
||||||
|
let onset: Int = manifold_index_of(mp, "onset")
|
||||||
|
assert onset > -1, "and to that realizer's own component vocabulary"
|
||||||
|
let f1: Int = manifold_free(mt)
|
||||||
|
let f2: Int = manifold_free(mp)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "no-organ-is-reported-as-no-organ" {
|
||||||
|
// A modality with no realizer must transduce to NOTHING. It must never
|
||||||
|
// fall back to embedding a description of the signal and calling that
|
||||||
|
// perception — that silent substitution is the original defect.
|
||||||
|
let has: Int = realizer_has("echolocation")
|
||||||
|
assert has < 1, "unregistered modality has no organ"
|
||||||
|
let m: Manifold = transduce("anything", "echolocation")
|
||||||
|
let live: Int = manifold_is(m)
|
||||||
|
assert live < 1, "no realizer means no manifold, not a fake one"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "registration-of-an-unresolvable-name-fails-loudly" {
|
||||||
|
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||||
|
assert bad < 1, "an unresolvable realizer name is a registration failure"
|
||||||
|
let has: Int = realizer_has("ghost")
|
||||||
|
assert has < 1, "and nothing gets registered"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a-fingerprint-realizer-transduces-nothing" {
|
||||||
|
// THE SUPERSESSION OF #144, asserted directly. fingerprint_realizer is
|
||||||
|
// exactly what the merged primitive asked a realizer to be: signal in, one
|
||||||
|
// Geometry out. It resolves, so registration succeeds — the organ is
|
||||||
|
// present. But it does not decompose, so it does not transduce.
|
||||||
|
//
|
||||||
|
// This is a deliberate hard failure. "No organ" and "an organ that only
|
||||||
|
// fingerprints" must not be indistinguishable, which is the same
|
||||||
|
// distinction realizer_register already draws between an absent and a
|
||||||
|
// broken organ. A modality with genuinely one part says so with
|
||||||
|
// manifold_single, and is then visibly a size-1 manifold.
|
||||||
|
let reg: Int = realizer_register("fingerprint", "fingerprint_realizer")
|
||||||
|
assert reg > 0, "the symbol resolves, so registration succeeds"
|
||||||
|
let m: Manifold = transduce("x", "fingerprint")
|
||||||
|
let live: Int = manifold_is(m)
|
||||||
|
assert live < 1, "a single vector is not a transduction"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a-realizer-returning-nonsense-transduces-nothing" {
|
||||||
|
let reg: Int = realizer_register("bogus", "bogus_realizer")
|
||||||
|
assert reg > 0, "the symbol resolves, so registration succeeds"
|
||||||
|
let m: Manifold = transduce("x", "bogus")
|
||||||
|
let live: Int = manifold_is(m)
|
||||||
|
assert live < 1, "a non-Manifold return transduced nothing"
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the-one-part-case-is-a-size-one-manifold-not-a-bare-vector" {
|
||||||
|
// Some modalities really do have one part. That is a manifold of size 1 —
|
||||||
|
// a special case of decomposition, not a parallel path back to a
|
||||||
|
// fingerprint. Anything reading it still asks manifold_size and still gets
|
||||||
|
// a real answer, and a second part can be added later without changing the
|
||||||
|
// type of the thing.
|
||||||
|
let g: Geometry = geometry_new(3)
|
||||||
|
let s: Int = geometry_set(g, 0, 5.0)
|
||||||
|
let m: Manifold = manifold_single("level", "scalar", g)
|
||||||
|
let live: Int = manifold_is(m)
|
||||||
|
assert live > 0, "manifold_single yields a real Manifold"
|
||||||
|
let one_sz: Int = manifold_size(m)
|
||||||
|
assert one_sz == 1, "of size one — visibly degenerate, not hidden"
|
||||||
|
let idx: Int = manifold_index_of(m, "level")
|
||||||
|
assert idx == 0, "and its one part is still addressable by name"
|
||||||
|
let f: Int = geometry_free(g)
|
||||||
|
let fm: Int = manifold_free(m)
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,17 +45,43 @@ returned 60k–230k-char unbounded traversals (this very session hit 104 KB and
|
|||||||
|
|
||||||
## Layer 2 — primitive agentic tools (Neuron runs itself)
|
## Layer 2 — primitive agentic tools (Neuron runs itself)
|
||||||
|
|
||||||
The base verbs all agentic behavior composes from — grounded in the LIVE
|
The base verbs all agentic behavior composes from.
|
||||||
cog-arch (`think` is the one operation; faculties are its steering-space labels;
|
|
||||||
the correspondence-beat is the reflexive learning loop).
|
> **⚠ 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) |
|
| 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 30–282) |
|
| `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`) |
|
| `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** |
|
| `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) |
|
| `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** (real Stance: `stance-induce-…`, brier, reliability, written) |
|
| `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:1870–1886` 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
|
`comprehend`/`realize`/`intend` are **compositions**, not separate live
|
||||||
primitives: comprehend = write+activate (world→geometry), realize = assert
|
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
|
`kn-efeb4a5b…` / `kn-5b606390…`, are refused — identity routes through
|
||||||
intentional-cultivation, as enforced today.
|
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
|
## How the caller invokes Neuron agentically
|
||||||
|
|
||||||
Once the ops are registered as MCP tools (aliases in `surface.el`), the caller
|
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)
|
## 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.
|
- **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).
|
- **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).
|
- **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`).
|
- **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).
|
- **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).
|
||||||
|
|||||||
Reference in New Issue
Block a user