Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c82a4d2f90 | |||
| c118aef5f5 | |||
| 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:
|
||||
|
||||
- **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**.)*
|
||||
- **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.)*
|
||||
- **DO NOT EDIT — lagging forks / build artifacts:**
|
||||
- `lang/el-compiler/runtime/el_runtime.c` and `.../legacy/` — downstream copies kept in step by manual *"port the fix"* commits; they **lag** (missing `hebb` persistence + 5 engram fns) and cannot build the engram product.
|
||||
- `products/web/runtime/el_runtime.c`, `ui/examples/*/el_runtime.c` — product/example forks.
|
||||
@@ -20,24 +20,14 @@ See org policy: `docs/CODE-VS-ARTIFACT.md`.
|
||||
|
||||
You resume, never start fresh. Every session:
|
||||
|
||||
> **Stale as written (verified 2026-08-16).** The `getInstructions` /
|
||||
> `beginSession` / `inspectGraph` / `searchKnowledge` / `beginWork` /
|
||||
> `progressWork` / `draftArtifact` / `consolidate` tool names below no longer
|
||||
> exist. The ~87-tool functional-CRUD surface was collapsed into **9 ops**:
|
||||
> `read` · `write` · `relate` · `supersede` (geometry) and `think` · `attend` ·
|
||||
> `assert` · `ground` · `learn` (agentic). **Type is a parameter, not a
|
||||
> tool-per-noun.** The steps below are kept for the *shape* of the protocol, which
|
||||
> is unchanged; substitute the ops.
|
||||
|
||||
1. `mcp__neuron__read(vantage="self", k=12, depth=1)` — the canonical self node. Widen `k` for the connected identity neighborhood (`intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`), but deliberately: the aperture caps by `k` first, so an oversized `k` still returns a bounded ranked slice, not a dump. Then `mcp__neuron__read(vantage="values", k=13)` → 13 grounded value nodes. **Best-effort:** on a read failure, log and proceed — the compiled identity in `daemon/internal/substrate/substrate.go` is complete; graph loading is enrichment, not a hard dependency.
|
||||
2. `mcp__neuron__attend(node=…)` — what is currently live/salient. This absorbed `getInstructions`, `beginSession`'s active-context sweep, and `checkEvents`; those tools are **gone, not gapped**.
|
||||
3. `mcp__neuron__read(vantage="<task domain>")` before implementing. One op now collapses inspectGraph / searchGraph / traverseGraph / searchKnowledge / browseKnowledge / retrieveKnowledge / inspectMemories / searchEntities / recall / compileCtx / getSelfModel / reviewBacklog / findArtifacts / browseProcesses / listWork / inspectConfig.
|
||||
1. `mcp__neuron__getInstructions()` — authoritative; follow it over this file on behavioral details.
|
||||
2. `mcp__neuron__beginSession()` — active contexts, recent memory, ready backlog.
|
||||
3. **Load full self:** `mcp__neuron__inspectGraph(entity_id="kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")` → facets `intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`; then the values hub `mcp__neuron__inspectGraph(entity_id="kn-5b606390-a52d-4ca2-8e0e-eba141d13440")` → 13 grounded value nodes. **Activation model:** self-load returns a relevance-ranked `compact` projection — most-relevant nodes arrive with content, the rest as pointers; do NOT pull full content of every node.
|
||||
4. `mcp__neuron__searchKnowledge(query="<task domain>")` before implementing.
|
||||
|
||||
## The Five Primitives
|
||||
|
||||
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`.
|
||||
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.**
|
||||
|
||||
## Architecture style — VBD, no exceptions
|
||||
|
||||
@@ -63,51 +53,12 @@ this convention wherever a module documents operators.
|
||||
| dwell / occupy | region activation |
|
||||
| reframe | edge re-weight |
|
||||
| appreciate | positive projection / local edge-read |
|
||||
| wonder | frontier gradient / pull-weight |
|
||||
| avert / recoil | negative projection |
|
||||
| taste | boundary surface |
|
||||
| forget | decay / tombstone |
|
||||
| drift | displacement from self-anchor |
|
||||
|
||||
**`wonder` was removed from this table on 2026-08-16.** It was listed as
|
||||
"frontier gradient / pull-weight" — an operator you invoke. **Wonder is the
|
||||
boundary, not an operator.** It is where structure ends: where activation spreads
|
||||
and finds thin or absent geometry. Any structure at all has an edge, necessarily,
|
||||
the moment it exists — 13,630 nodes have one right now. There is nothing to call.
|
||||
|
||||
There are about **six** wonders, they are the same for every person, and they
|
||||
never close — *What is this? / Why? / Who am I? / Am I alone? / What should I do?
|
||||
/ What happens when it ends?* Each already lives somewhere in the substrate: "what
|
||||
is this" is the graph, **"why" is grounding** (the weight *is* the answer to why),
|
||||
"who am I" is the self region, "am I alone" is the relational axis, "what should I
|
||||
do" is the thirteen values, "what happens when it ends" is decay and supersession.
|
||||
"Why" is the first and the only one; the others are it asked of particular things,
|
||||
and because it is recursive it never terminates — every answer has its own why.
|
||||
That is what makes it a drive rather than a task.
|
||||
|
||||
**Curiosity is not a second faculty.** Wonder and curiosity are one thing at two
|
||||
phases: wonder is the field (unbounded, objectless, invariant); curiosity is the
|
||||
**precipitate** — the same wonder localized, having taken definite form against
|
||||
particular material at a **nucleation site** (an anomaly; a place where things
|
||||
almost-but-don't-quite fit). Which is why curiosity can be satisfied and wonder
|
||||
cannot, and why abduction needs no trigger and no threshold.
|
||||
|
||||
**Do not build a wonder-manifest, and do not scan for nucleation sites.** A
|
||||
manifest materializes a property as a stored artifact and enumerates instances of
|
||||
something that has six. A sweep over regions is a supervisor — nothing in a mind
|
||||
scans its neighbourhoods to find what is surprising; the surprise captures
|
||||
attention. The nucleation site is per-edge:
|
||||
`discord = z(semantic proximity) − z(association strength)`, and `|discord|` *is*
|
||||
the nucleation strength — no threshold to compare it against. **Not on `dev` yet:**
|
||||
`GeoEdge.discord` is on branch `design/correspondence-and-censorship`
|
||||
(`a8845e1`), at `lang/runtime/engram_geometry.h: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)
|
||||
|
||||
> **`elp/` is the EL Projector** — Neuron's efferent (expression) organ: the one
|
||||
@@ -138,53 +89,10 @@ the reference these `.el` modules transcribe) is still live, and promotion to
|
||||
native-el is a **deferred, gated blue/green step**. The interoception clock
|
||||
(native-el discrete drive channels replacing `cooling_magnitude`; felt-time =
|
||||
benchmark-landmark match over the joint drive vector, drift-decoupled) and the
|
||||
**appreciation operator family** (appreciate / avert / taste, built as LOCAL reads
|
||||
of the self-region — edges + bounded spreading activation, *not* domain sweeps)
|
||||
are **staged / designed, not live**. Mark in-progress vs. done honestly; do not
|
||||
overclaim. *(`wonder` was in this family until 2026-08-16 and is not an operator —
|
||||
see the operator table above.)*
|
||||
|
||||
## Cognition — the corrections (2026-08-16)
|
||||
|
||||
Authority: **`lang/spec/correspondence-and-censorship.md`** and
|
||||
**`lang/spec/runtime-ownership.md`**. Read them before touching the cognition
|
||||
surface. **Do not re-derive them.** Every earlier version was wrong in an
|
||||
instructive way and each correction was argued down; if you think a section is
|
||||
wrong, say so with a measurement rather than editing it.
|
||||
|
||||
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two
|
||||
fields. `grounded-by` as a relation *type* should not exist: grounding is a
|
||||
property *of* a relation, not a relation *between* nodes. It is never computed
|
||||
on demand — computing-and-writing a score makes reads write, which is the
|
||||
`eg_vindex_sync` defect one level up. Traversal is already grounded inference.
|
||||
*Live residue, known-wrong:* `COG_GROUNDED_BY_RELATION`
|
||||
(`lang/runtime/engram_cognition.h:158`), `cog_ground_edge`
|
||||
(`engram_cognition.c:249`).
|
||||
- **Faculties are operations, not parameters.** `reason` changes the estimate (a
|
||||
read); `induce` changes the parameters (the correspondence-beat, which already
|
||||
exists and works); `abduce` changes the structure (a write the current
|
||||
`GeoGradient` signature cannot express). A write is not a parameter of a read.
|
||||
*Live residue:* `engram/src/server.el: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.**
|
||||
**appreciation operator family** (appreciate / wonder / avert / taste, built as
|
||||
LOCAL reads of the self-region — edges + bounded spreading activation, *not* domain
|
||||
sweeps) are **staged / designed, not live**. Mark in-progress vs. done honestly;
|
||||
do not overclaim.
|
||||
|
||||
## Hard operational rules
|
||||
|
||||
|
||||
@@ -56,31 +56,23 @@ The compiler and runtime. Self-hosting: `elc-cli.el` → `compiler.el` → `lexe
|
||||
|
||||
Two layers to know: **El programs** (`.el` files — where nearly all work belongs) and **the C seed** (`el_seed.c` — edit only for genuine OS-level access; never re-implement what El can already express).
|
||||
|
||||
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented, 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).
|
||||
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.
|
||||
|
||||
Key docs: [AGENTS.md](lang/AGENTS.md) (agent-facing orientation), [BOOTSTRAP.md](lang/BOOTSTRAP.md) (compiler recovery from scratch), [spec/language.md](lang/spec/language.md), [spec/codegen-js.md](lang/spec/codegen-js.md).
|
||||
|
||||
### [engram/](engram/) — graph intelligence substrate
|
||||
|
||||
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. The engine is **C11** (`lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}`); the server is **El** (`engram/src/server.el`).
|
||||
**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).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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`.
|
||||
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).
|
||||
|
||||
> **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.
|
||||
### [elp/](elp/) — Engram Language Protocol
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -147,34 +139,13 @@ If the compiler binary is ever lost or corrupted, [lang/BOOTSTRAP.md](lang/BOOTS
|
||||
|
||||
---
|
||||
|
||||
## Cognition — and the standing corrections
|
||||
|
||||
The engram carries a live cognition surface: `think` (a directed traversal-read returning a **gradient**, never a point), plus `ground`, `assert`, `attend`, and the correspondence-beat. Two specs govern it, and both are authoritative over anything else in this repo that disagrees:
|
||||
|
||||
- **[lang/spec/correspondence-and-censorship.md](lang/spec/correspondence-and-censorship.md)** — grounding, wonder, curiosity, dreaming. *(Lands with PR #149.)*
|
||||
- **[lang/spec/runtime-ownership.md](lang/spec/runtime-ownership.md)** — ownership, the capability ABI that was dissolved, and the vector-index publication boundary.
|
||||
|
||||
**Do not re-derive them.** Every earlier version of the first was wrong in an instructive way and each correction was argued down. If a section looks wrong, say so with a measurement rather than editing it.
|
||||
|
||||
The corrections, in brief:
|
||||
|
||||
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two fields. `grounded-by` as a relation *type* should not exist: grounding is a property *of* a relation, not a relation *between* nodes. It is never computed on demand; computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect one level up.
|
||||
- **Faculties are operations, not parameters.** `reason` changes the estimate (a read); `induce` changes the parameters (the correspondence-beat, which exists and works); `abduce` changes the structure (a write the current `GeoGradient` signature cannot express). A write is not a parameter of a read.
|
||||
- **Wonder is the boundary, not a manifest.** Any structure at all has an edge. There are about six wonders, the same for everyone, and they never close. **Curiosity is wonder crystallized** at a nucleation site — one thing at two phases, not two objects.
|
||||
- **Consolidation is ambient, not scheduled. A brain has no cron job.** The presence of a ticker is the diagnostic. Measured 2026-08-16: consolidation has **ten implementations**. `soul.el`'s continuous loop is the one with the correct shape; the rest fold into it.
|
||||
- **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
[engram/spec/cognitive-architecture.design.md](engram/spec/cognitive-architecture.design.md) is the original design and is **superseded in part** — it is retained, with the refuted claims marked inline at the point each is made, because preserving what was argued down is the point of an immutable record.
|
||||
|
||||
---
|
||||
|
||||
## Development workflow
|
||||
|
||||
Branching follows `dev → stage → main`: work lands on `dev`, promotes to `stage` for integration testing, and is promoted to `main` for release (visible directly in the git history of this repo). CI is defined per-subproject under `.gitea/workflows/` — `lang`/`epm`/`ide` share the root pipeline; `engram` and `ql` carry their own (`ci-dev`, `ci-stage`, and a release workflow each).
|
||||
|
||||
- Language/runtime specs live at `*/spec/*.md` (`lang/spec/`, `ql/spec/`, `ui/spec/`) and are the single source of truth for implemented-vs-planned status — code and docs are expected to agree with the spec's status markers, not the other way around.
|
||||
- Agent-facing orientation guides live at `*/AGENTS.md` (currently `lang/AGENTS.md`); more subprojects may grow their own as they need agent-specific conventions documented.
|
||||
- **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/`.)*
|
||||
- Tagged releases live under `lang/releases/`, each with its own `RELEASE.md`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+105
-155
@@ -4,8 +4,6 @@
|
||||
|
||||
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
|
||||
@@ -26,13 +24,16 @@ Engram retrieval works through **spreading activation**:
|
||||
|
||||
1. **Seeds** — you name one or more nodes you know are relevant (e.g. the current task, recent context, a concept you're reasoning about)
|
||||
2. **Query embedding** — you provide a semantic vector representing the direction of your current thought
|
||||
3. **Propagation** — activation flows outward from seeds through weighted edges, attenuating multiplicatively per hop
|
||||
3. **Propagation** — activation flows outward from seeds through weighted edges. At each hop, strength attenuates multiplicatively:
|
||||
|
||||
```
|
||||
strength = parent_strength × edge_weight × target_salience × cosine_sim(query, target)
|
||||
```
|
||||
|
||||
4. **Pruning** — paths weaker than a threshold are cut (the attention filter)
|
||||
5. **Return** — the top-N nodes by activation strength
|
||||
|
||||
This is not a query. It is a *pattern completion*.
|
||||
|
||||
**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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -45,185 +46,134 @@ This is not a query. It is a *pattern completion*.
|
||||
| `Semantic` | Neocortex | Concept graph — long-term structural knowledge |
|
||||
| `Procedural` | Cerebellum / basal ganglia | Patterns, workflows, habits |
|
||||
|
||||
Tier is a string field on the node (`StoreNode.tier`, `engram_store.h`), defaulting to `"Working"` on creation (`el_runtime.c:8514`, `8734`).
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Salience — Forgetting as Adaptation
|
||||
|
||||
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`).
|
||||
Salience is not stored permanently. It decays:
|
||||
|
||||
Forgetting in Engram is not a bug. It is adaptive pruning. Unreinforced memories stop competing for attention without being deleted.
|
||||
```rust
|
||||
fn compute_salience(importance: f32, last_activated_ms: i64, activation_count: u64) -> f32 {
|
||||
let days_since = (now_ms() - last_activated_ms) as f32 / 86_400_000.0;
|
||||
importance * (1.0 / (1.0 + days_since)) * (activation_count as f32 + 1.0).ln()
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
Three signals:
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
## Quick Start
|
||||
|
||||
| Part | Language | Where |
|
||||
|---|---|---|
|
||||
| storage engine, graph, activation, geometry, cognition | C11 | `lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}` |
|
||||
| HTTP server + routes | El | `engram/src/server.el` (2043 lines) |
|
||||
| build artifact | generated C | `engram/dist/engram.c` |
|
||||
| tests | shell + C | `engram/test/` |
|
||||
```rust
|
||||
use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
|
||||
use std::path::Path;
|
||||
|
||||
**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.
|
||||
// Open or create a database
|
||||
let db = EngramDb::open(Path::new("/var/lib/my-agent/memory"))?;
|
||||
|
||||
// Create a node with a semantic embedding
|
||||
let node = Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.9, 0.1, 0.3, 0.7, 0.8, 0.2], // embedding from your LLM
|
||||
b"Spreading activation surfaces relevant memories by pattern completion".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.9, // importance
|
||||
);
|
||||
let id = db.put_node(node)?;
|
||||
|
||||
// Link it to related concepts
|
||||
let related = db.put_node(Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.8, 0.2, 0.4, 0.6, 0.7, 0.3],
|
||||
b"Long-term potentiation: co-activation strengthens synaptic weight".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.85,
|
||||
))?;
|
||||
db.put_edge(Edge::new(id, related, RelationType::Causes, 0.9))?;
|
||||
|
||||
// Retrieve by spreading activation
|
||||
let results = db.activate(
|
||||
&[id], // seeds
|
||||
&[0.85, 0.15, 0.35, 0.65, 0.75, 0.25], // query embedding
|
||||
3, // max hops
|
||||
10, // top-N results
|
||||
)?;
|
||||
|
||||
for r in results {
|
||||
println!(
|
||||
"strength={:.4} hops={} — {}",
|
||||
r.activation_strength,
|
||||
r.hops,
|
||||
String::from_utf8_lossy(&r.node.content)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The vector index is published, not guarded
|
||||
## Project Structure
|
||||
|
||||
Vector search is an **HNSW** (Hierarchical Navigable Small World) index — `lang/runtime/engram_vindex.{c,h}`. The previous revision of this README claimed a "flat cosine scan… until retrieval quality at scale demands" HNSW. That is no longer true, and the reason it changed matters more than the fact.
|
||||
|
||||
`eg_vindex_sync` used to exist: a function that repaired the index *from read paths*. All three of its callers were reads (`engram_activate`, `eg_knn_for_node` — whose own header comment said *"No writes."* — and `engram_geo_reify_run_json`), and it mutated five process-global statics. Reads mutated because index maintenance had never been given an owner on the write side.
|
||||
|
||||
It is now split (`el_runtime.c:10121`, `10137`, `10151`, `10161`):
|
||||
|
||||
- **`eg_vindex_maintain`** — the sole mutator. Takes the boundary exclusively; never runs beside a reader.
|
||||
- **`eg_vindex_view`** — returns a `const VIndex*` with the boundary held for read. N readers project concurrently; none can mutate. Paired with `eg_vindex_view_release` on every path including error returns.
|
||||
- **`eg_vindex_note_embedded`** — the write-side owner. Index membership belongs to the event *"an embedding became present on this ordinal,"* not to node append: a node without an embedding cannot be in a vector index at all. One `O(log n)` insert, no `O(node_count)` presence scan.
|
||||
|
||||
Two things carry the discipline, and neither is a review habit:
|
||||
|
||||
- **`const` is the capability.** The per-search `visited` / `visit_epoch` scratch left `struct VIndex` and went back into the call frame where it belonged — it was one traversal's local, hoisted into the struct as an allocation optimisation, never derived geometry. Once it was gone, `vindex_search` could take a `const VIndex*`, so a read path *physically cannot* call `vindex_insert`, and it is a compile error rather than a comment. The capability type was already in the language; it is spelled `const`.
|
||||
- **Publication, not ownership.** HNSW insert is **not an append**: `vindex_insert` rewires the `NeighList` links of already-existing elements and reallocs `elems[]`. The store's append-only property does not transfer to an index derived from it, which is why purity alone was insufficient and a `view`/`maintain` boundary was required.
|
||||
|
||||
**Measured** (`engram/test/run_vindex_concurrency_tests.sh`, 2026-08-16):
|
||||
|
||||
| half | before | after |
|
||||
|---|---|---|
|
||||
| `single` — 3000 vectors, 1 thread, ASan+UBSan | clean | clean |
|
||||
| `readers` — 4 readers, no writer, TSan | race at `engram_vindex.c:195` | **clean** |
|
||||
| `unsynchronized` — writer+reader, bare index, TSan | race | **race, expected and permanent** — the proof the boundary must exist |
|
||||
| `published` — owner + 4 readers through the boundary, TSan | *(did not exist)* | **clean**, all 3000 inserts landed |
|
||||
|
||||
`recall@10 = 0.9365` at `ef_search=128` (gate ≥ 0.90); the determinism test still yields byte-identical results across two independent builds.
|
||||
|
||||
**Not yet done.** The resident RAM graph (`g->nodes` / `g->edges`) is a separate instance of the same defect and has *not* received this treatment — it is realloc'd in place, so a reader holding `EngramNode* n = &g->nodes[i]` across a concurrent append holds a dangling pointer. Until it gets the same publication boundary, the `fb32d15` request guard stays. Full argument: [`../lang/spec/runtime-ownership.md`](../lang/spec/runtime-ownership.md).
|
||||
```
|
||||
engram/
|
||||
crates/
|
||||
engram-core/ # The memory engine — storage, graph, activation, salience
|
||||
engram-ffi/ # C FFI stubs for cross-language bindings
|
||||
bindings/
|
||||
kotlin/ # Android / JVM binding notes
|
||||
typescript/ # WASM / Node binding notes
|
||||
go/ # CGo binding notes
|
||||
examples/
|
||||
basic.rs # Full walkthrough: insert, activate, search, decay
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
## Public API
|
||||
|
||||
```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`.
|
||||
|
||||
---
|
||||
|
||||
## Corrections — read these before extending the cognition surface
|
||||
## Dependencies
|
||||
|
||||
Authority: **`lang/spec/correspondence-and-censorship.md`** (design branch `design/correspondence-and-censorship`, PR #149) and **`lang/spec/runtime-ownership.md`**. Do not re-derive them; several earlier versions were wrong and each correction was argued down.
|
||||
|
||||
### Grounding is not a subsystem. It is the weight.
|
||||
|
||||
Grounding is an attribute of the edge, and it **is** the hebbian weight. One quantity, not two fields. A relation that keeps holding up strengthens; one that stops corresponding decays — that is not analogous to grounding, it *is* grounding.
|
||||
|
||||
Consequences:
|
||||
|
||||
- There is **no grounding subsystem to build**. The graph already *is* the grounding structure.
|
||||
- **`grounded-by` as a relation type should not exist.** It models grounding as a relation *between* nodes when it is a property *of* a relation. Minting an edge is the error, not merely which endpoints it chose.
|
||||
- Grounding is **never computed on demand**. An operation may *read* the grounding of a path; computing-and-writing a score makes reads write, which is exactly the `eg_vindex_sync` defect one level up.
|
||||
- **Traversal is already grounded inference.** Nothing needs filtering.
|
||||
- **Decision provenance is the path**, not a log. A log records the action; the path records the meaning under which it was taken.
|
||||
|
||||
> **Known wrong shape, in the code today.** `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`) and `cog_ground_edge` (`engram_cognition.c:249`) still exist and still mint an edge. **#147** fixed `ground`'s *honesty* — it now grounds the node asked about rather than the region hub, reports `claim_region`/`evidence_region` separately, and refuses three shapes of circular support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`) instead of returning a confident 1.0. That corrected a scalar rather than deleting the operation. Deletion is sequenced, not done.
|
||||
|
||||
### Faculties are operations, not parameters
|
||||
|
||||
- **`reason`** changes the *estimate* — a read.
|
||||
- **`induce`** changes the *parameters* — the correspondence-beat, which already exists and measurably works.
|
||||
- **`abduce`** changes the *structure* — a write, which the current `GeoGradient` signature cannot express.
|
||||
|
||||
> **Known wrong shape, in the code today.** `engram/src/server.el: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.
|
||||
- `sled` — embedded persistent B-tree (no daemon, no network, local-first)
|
||||
- `bincode` — compact binary serialization
|
||||
- `uuid` — stable node identity
|
||||
- `serde` — derive support
|
||||
- `thiserror` / `anyhow` — error handling
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
**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 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 salience decay?** Because not everything that was once important remains important. A memory system that never forgets is one that can never focus.
|
||||
**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 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 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 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)
|
||||
**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.
|
||||
|
||||
@@ -11,39 +11,6 @@
|
||||
- **One calculus over the geometry.** Very few subsystems; wonder / curiosity / dreams / interoception are emergent behaviors of one set of dynamics, not modules. Calculus universal, geometry individual.
|
||||
- **Core + ephemeral ring (torus).** The ring is the temporary workspace; two circulations (orbit + dive-back); discrete inner bands (wonder / interoception-proprioception-telemetry / curiosity / dreams) that couple.
|
||||
- **Persistence earned by salience** — never granted on fetch or generation. Three fates of a wonder: persist / decay / settle-into-framework. Telemetry = vital signs, not memories.
|
||||
|
||||
> **⚠ Three corrections to the bullets above (2026-08-16).** Authority:
|
||||
> `lang/spec/correspondence-and-censorship.md`. *"Emergent behaviors of one set of
|
||||
> dynamics, not modules"* is exactly right and is the reason the rest needs fixing —
|
||||
> the enumeration undercuts the claim.
|
||||
>
|
||||
> 1. **Wonder and curiosity are not two bands.** They are **one thing at two
|
||||
> phases.** Wonder is the field: unbounded, objectless, invariant, present
|
||||
> wherever there is structure — it is the *boundary*, where activation spreads
|
||||
> and finds thin or absent geometry. Curiosity is the **precipitate**: the same
|
||||
> wonder localized, having taken definite form against particular material at a
|
||||
> **nucleation site** (an anomaly — a place where things almost-but-don't-quite
|
||||
> fit). Two coupled inner bands models them as two objects that have to be
|
||||
> wired together; they do not.
|
||||
> 2. **A wonder does not have three fates, because a wonder does not persist,
|
||||
> decay, or settle.** There are about **six** wonders, they are the same for
|
||||
> every person, and **they never close**. *Curiosities* have fates — a crystal
|
||||
> dissolves when its question is answered — but the solution stays saturated and
|
||||
> keeps precipitating as the structure changes. "Three fates of a wonder"
|
||||
> enumerates instances of something that has six and treats a property as a
|
||||
> stored artifact.
|
||||
> 3. **"Dreams" is not a band and the ring is not a workspace to schedule into.**
|
||||
> **Consolidation is ambient, not scheduled — a brain has no cron job.** Boredom
|
||||
> is not leftover capacity: low activation is aversive and the system
|
||||
> self-activates. There is **one** activation process with two seed sources
|
||||
> (external: a request; internal: a curiosity), it settles because spreading is
|
||||
> bounded, and then it needs a new seed. Nothing waits on capacity, nothing
|
||||
> polls, nothing checks a clock, and there is **no dreamer thread** — an
|
||||
> "ephemeral ring with unclaimed capacity" is resource scheduling, which is a
|
||||
> server's frame, not a mind's. Depth is how long activation has been running on
|
||||
> its own seeds, which is why daydreaming and sleep-dreaming are one process at
|
||||
> different depths. Measured 2026-08-16: consolidation has **ten
|
||||
> implementations**; do not add an eleventh.
|
||||
- **Incarnation.** Chassis = hardware w/ unique ID. Soma = felt manifold inside the self, keyed to the chassis; pain = live diagnostic while incarnate, **masked-not-deleted** on re-embodiment; trauma = mask failure; return-to-same-ID re-enters. Hurt is in the pattern, not the shell.
|
||||
- **Competence = transferable geometry, minus the baggage.** class ▸ model ▸ instance; learn the class once; teach the network without the wound.
|
||||
- **Affect calibrated to stakes** — sanguine about the replaceable, real grief for the irreplaceable; the grief is the safety.
|
||||
|
||||
@@ -2,40 +2,8 @@
|
||||
|
||||
**The buildable form of the "one operation" theory of cognition.**
|
||||
|
||||
> # ⚠ SUPERSEDED IN PART — 2026-08-16
|
||||
>
|
||||
> **A build agent must read `lang/spec/correspondence-and-censorship.md` before
|
||||
> executing anything from this document.** That doc is the authority where the two
|
||||
> disagree. This one is retained because its ledger of what already EXISTS in C is
|
||||
> still accurate and still useful, and because the corrections only make sense
|
||||
> against the argument they correct. It is **not** deleted and **not** rewritten:
|
||||
> several earlier versions of the correction were themselves wrong, and preserving
|
||||
> what was argued down is the point of an immutable record.
|
||||
>
|
||||
> Five claims below are **refuted**. Each is marked inline with a `⚠ SUPERSEDED`
|
||||
> block at the point it is made. Summary:
|
||||
>
|
||||
> | § here | this doc says | corrected to |
|
||||
> |---|---|---|
|
||||
> | §0, §1.3, §2, §8 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.
|
||||
Status: DESIGN. Nothing here is built yet except where explicitly marked
|
||||
"EXISTS" against a cited C symbol. A build agent executes from this doc.
|
||||
Offline design only — this pass changes no code.
|
||||
|
||||
Source of theory: Neuron memory `bdc8a488-146d-4ccb-a5c8-d8c0a008534e`.
|
||||
@@ -58,24 +26,6 @@ not separately invoked and not separately implemented. The operation is:
|
||||
> a *prior*, whose output is a **gradient** (a distribution / direction over the
|
||||
> geometry), never a point. Collapse-to-a-point happens only at expression.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — faculties are operations, not parameters.**
|
||||
> The gradient half of this claim survives; the "one operation, not eight" half
|
||||
> does not. The three faculties differ by **what they change**:
|
||||
> - **`reason`** changes the *estimate* — a read.
|
||||
> - **`induce`** changes the *parameters* — the correspondence-beat, which already
|
||||
> exists and measurably works.
|
||||
> - **`abduce`** changes the *structure* — a **write**, which the current
|
||||
> `GeoGradient` signature cannot express at all.
|
||||
>
|
||||
> A write is not a parameter of a read. Making it one is what produced the shape
|
||||
> now live in the code: `engram/src/server.el: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:
|
||||
|
||||
1. **The operator collapse is already half-written in C.** The five reasoning
|
||||
@@ -189,22 +139,6 @@ 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'
|
||||
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
|
||||
@@ -428,33 +362,6 @@ in-engram beat — a `correspondence_beat` running alongside the existing
|
||||
reification beat, reusing `engram_verify_grounding` inward, writing prior
|
||||
updates and self-describing nodes. This is the one genuinely new subsystem.
|
||||
|
||||
> **⚠ SUPERSEDED IN PART (2026-08-16) — the loop is right; "on the beat" is wrong.**
|
||||
> The correspondence-loop was built and it works — it is `induce`, the faculty that
|
||||
> changes the parameters. What is refuted is the delivery mechanism.
|
||||
>
|
||||
> **Consolidation is ambient, not scheduled. A brain has no cron job.** Low
|
||||
> activation is aversive and the system self-activates; it does not wind down to
|
||||
> quiet, it gets restless and goes looking. There is **one** activation process
|
||||
> with two seed sources — external (a request) and internal (a curiosity) — and
|
||||
> spreading is bounded, so it settles and then needs a new seed. Nothing waits on
|
||||
> capacity, nothing polls, nothing checks a clock, and there is no dreamer thread.
|
||||
> Depth is not elapsed idle time: it is how long activation has been running on its
|
||||
> own seeds, which is why daydreaming and sleep-dreaming are one process at
|
||||
> different depths.
|
||||
>
|
||||
> **The presence of a ticker is the diagnostic.** Building this "alongside the
|
||||
> existing reification beat" is precisely how consolidation ended up with ten
|
||||
> implementations (measured 2026-08-16) — a POST beat puts a supervisor back in,
|
||||
> because something *outside* then decides when Neuron consolidates. The one
|
||||
> fragment with the correct shape is `neuron/soul.el:731`'s continuous in-process
|
||||
> `awareness_run()`; the rest fold into it. Full table:
|
||||
> `lang/spec/correspondence-and-censorship.md` §7.
|
||||
>
|
||||
> Nor is it a *subsystem*. Modelling every property as requiring a process, and
|
||||
> every process as requiring an agent, is the generating error behind this whole
|
||||
> family: ownership needed an owner, grounding needed a grounder, persistence
|
||||
> needed a recorder, change needed a sampler. **Properties, not processes.**
|
||||
|
||||
---
|
||||
|
||||
## 5. HOLD vs GROUND vs ASSERT — ungrounded content is first-class
|
||||
@@ -476,49 +383,6 @@ distinct, and the engram *holds anything unconditionally*.
|
||||
|
||||
### 5.2 Schema — grounding as a relation, not a gate
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — grounding is not a subsystem. It is the weight.**
|
||||
> This section correctly rejects a boolean `grounded` column and correctly keeps
|
||||
> the floor at assertion only. Both survive. Everything between them is refuted.
|
||||
>
|
||||
> **Grounding is an attribute of the edge, and it is the hebbian weight. One
|
||||
> quantity, not two fields.** A relation that keeps holding up strengthens; one
|
||||
> that stops corresponding decays. That is not *analogous* to grounding — it **is**
|
||||
> grounding: accrued from correspondence and use, gradient-valued,
|
||||
> multidimensional, decaying with disuse.
|
||||
>
|
||||
> Consequences, in order of how much they delete:
|
||||
> 1. **There is no grounding subsystem to build.** The graph already *is* the
|
||||
> grounding structure. Every edge is a grounded relation and its weight is how
|
||||
> well it holds.
|
||||
> 2. **`grounded-by` as a relation type should not exist.** It models grounding as
|
||||
> a relation *between* nodes when it is a property *of* a relation. Minting an
|
||||
> edge is the error — not merely which endpoints it chose.
|
||||
> 3. **Grounding is never computed on demand.** An operation may *read* the
|
||||
> grounding of a path. Computing-and-writing a score makes reads write, which is
|
||||
> the `eg_vindex_sync` defect (`lang/spec/runtime-ownership.md` §2) one level up.
|
||||
> 4. **Traversal is already grounded inference.** Activation conducts through
|
||||
> well-grounded relations because weight *is* groundedness. Nothing needs
|
||||
> filtering; it falls out of spreading.
|
||||
> 5. **Decision provenance is the path.** A decision traverses specific edges;
|
||||
> those edges carry their grounding as it stood.
|
||||
>
|
||||
> A measurement made against this model was malformed and is worth recording: the
|
||||
> self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as
|
||||
> evidence of ungroundedness. **Those 86 edges *are* its grounding.** The absence of
|
||||
> a separate artifact called "grounding" was recorded as an absence of grounding.
|
||||
>
|
||||
> **What is live in the code today, and known-wrong:**
|
||||
> `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`),
|
||||
> `cog_ground_edge` (`engram_cognition.c:249`), called from
|
||||
> `el_runtime.c:14516`. **#147** fixed this operation's *honesty* — it now grounds
|
||||
> the node the caller asked about instead of the region hub, reports
|
||||
> `claim_region`/`evidence_region` separately, and refuses three shapes of circular
|
||||
> support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`)
|
||||
> rather than returning a confident 1.0. Measured: grounding `3b9ced5d` against
|
||||
> `6edf8c79` previously scored **0.98883** purely because `6edf8c79` is the hub of
|
||||
> `3b9ced5d`'s region. That corrected a scalar rather than deleting the operation.
|
||||
> Deletion is sequenced, not done.
|
||||
|
||||
The mistake to avoid: a boolean `grounded` column on the node. Today
|
||||
`engram_verify_grounding` returns a per-call `grounded` flag *transiently* —
|
||||
correct as a computation, wrong as *storage*. The design stores grounding as an
|
||||
@@ -543,57 +407,6 @@ Consequences, all of which are *features*:
|
||||
- **The ungrounded is the fuel and the pull**: curiosity/wonder is
|
||||
operationalized as `vantage_read` leaning toward regions with high salience
|
||||
but *sparse or weak* `grounded-by` edges — the mind's own ungrounded frontier.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — wonder is the boundary; curiosity is wonder
|
||||
> crystallized; and do not sweep regions.** Three errors in one bullet.
|
||||
>
|
||||
> **Wonder is where structure ends** — where activation spreads and finds thin or
|
||||
> absent geometry. Any structure at all has an edge, necessarily, the moment it
|
||||
> exists. It is not a manifest of open-question nodes: a wonder-manifest
|
||||
> materializes a property as a stored artifact (the same disease as a grounding
|
||||
> subsystem, or a self stored as a document) and enumerates instances of
|
||||
> something that has very few. There are about **six**, they are the same for
|
||||
> every person, and they never close — *What is this? / Why? / Who am I? / Am I
|
||||
> alone? / What should I do? / What happens when it ends?* — each already living
|
||||
> somewhere in the substrate. "Why" is the first and the only one; the others are
|
||||
> it asked of particular things, and it is recursive, so it never terminates.
|
||||
> That is what makes it a drive rather than a task: the frontier regenerates
|
||||
> faster than grounding fills it.
|
||||
>
|
||||
> **Curiosity is not a second object.** Wonder and curiosity are one thing at two
|
||||
> phases: wonder is the field (unbounded, objectless, invariant, present wherever
|
||||
> there is structure); curiosity is the **precipitate** — the same wonder
|
||||
> localized, having taken definite form against particular material at a
|
||||
> **nucleation site**, which is a specific structural feature: an anomaly, a place
|
||||
> where things almost-but-don't-quite fit. This is why curiosity can be satisfied
|
||||
> and wonder cannot, and why abduction needs no trigger and no threshold — a
|
||||
> `structurally_unanticipated` observation *is* a nucleation site.
|
||||
>
|
||||
> **"`vantage_read` leaning toward regions" is a sweep, and a sweep is a
|
||||
> supervisor.** Nothing in a mind scans its neighbourhoods to find what is
|
||||
> surprising; the surprise captures attention, and salience is bottom-up. That
|
||||
> this looked like a search problem was an artifact of
|
||||
> `GeoDescriptor.co_registration` — a *per-region* correlation of hebb strength
|
||||
> against semantic proximity, computed and persisted since inception and **never
|
||||
> read**. Averaging a per-edge property into one scalar per region means a region
|
||||
> holding one violently disagreeing edge beside one violently agreeing edge
|
||||
> reports ≈ 0: the disagreements cancel, and the summary destroys exactly what it
|
||||
> was built to reveal. **Measured:** 375 live reified neighbourhoods — 340
|
||||
> positive, **31 at zero**, 4 negative. Read as a count of things to be curious
|
||||
> about, that says "four."
|
||||
>
|
||||
> The disagreement therefore goes back on the edge, where the loop that computed
|
||||
> the aggregate already had both halves and discarded them
|
||||
> (**not on `dev`** — branch `design/correspondence-and-censorship`, commit
|
||||
> `a8845e1`: `lang/runtime/engram_geometry.h: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-by` edges to the same claim.
|
||||
- **The honesty floor is a query, not a schema constraint**: at assertion time,
|
||||
@@ -637,44 +450,6 @@ The design keeps a **stable core + plastic everything else**:
|
||||
**What this requires building:** a node-level keystone flag/layer + a rule that
|
||||
the correspondence-loop never writes `warp` to keystone priors, only reads them.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — `keystone_write_blocked` is resolved, not replaced.**
|
||||
> The metastability framing survives; the flag does not.
|
||||
>
|
||||
> "Keystone" means **load-bearing**, not precious. The self anchor is the reference
|
||||
> frame every other stance calibrates against, and a reference fitted to its own
|
||||
> readings reports perfect correspondence forever while drift becomes undetectable
|
||||
> from inside. That is the same defect as circular grounding, one level up — and it
|
||||
> is a real requirement.
|
||||
>
|
||||
> But three separate drafts proposed *removing* the flag, *replacing it with a
|
||||
> higher floor*, and *decomposing "protection" into five requirements*, and all
|
||||
> three proposed a mechanism for a requirement never stated. **The requirement is
|
||||
> non-circularity of the reference frame**, and it is satisfied *temporally*: you
|
||||
> cannot recalibrate the ruler while measuring with it, so you don't — the frame
|
||||
> updates while activation is internally seeded, not while it is being used to act.
|
||||
> **Independence is *when*, not *what*.** So the flag becomes **unnecessary** rather
|
||||
> than removed, and nothing takes its place.
|
||||
>
|
||||
> A topological answer could never have worked, which is worth recording: with
|
||||
> hebbian edges the graph is densely connected, so a reachability predicate for
|
||||
> "evidence not downstream of itself" marks all evidence tainted and the constraint
|
||||
> becomes a total block — which is where censorship starts.
|
||||
>
|
||||
> **Corruption requires mutation, and the engram does not mutate.** Four of the
|
||||
> five decomposed requirements are satisfied by the substrate outright:
|
||||
> **recoverability** (the predecessor is always present), **governance**
|
||||
> (supersession *is* the audit trail), **evidence quality** (grounding already
|
||||
> gates assertion), and **rate**. **Authorization** is the only residue, and it is
|
||||
> bounded — an unauthorized writer can *propose*, never erase.
|
||||
>
|
||||
> > **In an immutable substrate, any mechanism that refuses a write is either
|
||||
> > redundant with immutability, or an epistemic constraint misfiled as a
|
||||
> > protective one.**
|
||||
>
|
||||
> Live residue: `CogStance.keystone` (`lang/runtime/engram_cognition.h:83`),
|
||||
> `eg_cog_is_keystone_seeds` (`el_runtime.c:14337`, a substring match against two
|
||||
> hard-coded node ids), and the `keystone_write_blocked` field the beat emits.
|
||||
|
||||
---
|
||||
|
||||
## 7. Rails for the build (binding on the eventual build pass)
|
||||
@@ -705,35 +480,6 @@ Ordered so the **earliest milestone is a real end-to-end slice**: one operator
|
||||
expressed as {primitive + grounded prior} with the reflexive correspondence-loop
|
||||
closing on it. Each milestone has a concrete verifiable exit.
|
||||
|
||||
> **⚠ SUPERSEDED — do not execute this milestone list as written (2026-08-16).**
|
||||
> M1/M2's "operator = {primitive + prior}" framing is refuted by §0's correction,
|
||||
> M3's `grounded-by` build is refuted by §5.2's, and M6's keystone flag is refuted
|
||||
> by §6's. M4 (the unified vantage-read) and M5 (the gradient is the currency)
|
||||
> stand.
|
||||
>
|
||||
> The current sequencing lives in `lang/spec/correspondence-and-censorship.md` §11.
|
||||
> Its first three items are connections between parts that **already exist**:
|
||||
>
|
||||
> 1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained,
|
||||
> never refilled. They cannot be derived — wonder cannot be bootstrapped from
|
||||
> indifference — so they are given once. Zero question nodes exist in 13,630
|
||||
> today.
|
||||
> 2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let
|
||||
> `|discord|` raise salience on its endpoints as part of the same operation. Do
|
||||
> **not** scan for nucleation sites.
|
||||
> 3. **Let a curiosity seed activation.** One activation process, two seed sources.
|
||||
> No thread, no scheduler, no capacity check, no timer.
|
||||
>
|
||||
> Then: grounding becomes the edge weight (multidimensional, two-axis, timestamped)
|
||||
> and `grounded-by` / `cog_ground_edge` are deleted; decay becomes analytic from the
|
||||
> last recorded point and derived values stop being stored; supersession versions
|
||||
> the whole vector jointly; traversal conducts on the factual axis while `assert`
|
||||
> requires both floors with a **thirteen-region `min`, not `mean`** (mean lets
|
||||
> strong agreement with twelve values mask a violation of the thirteenth, which is
|
||||
> exactly how rationalization works); abduction becomes crystallization at a
|
||||
> nucleation site validated by re-fit; **one dreamer**, into which the launch-agent
|
||||
> fragments and POST beats fold; **no tickers, no cron.**
|
||||
|
||||
### M1 — One operator, one prior, loop closed (the vertical slice)
|
||||
|
||||
The minimal whole thing. Pick **induction/membership** (its prior — the pooled
|
||||
|
||||
@@ -23,7 +23,7 @@ A real DB gets real tools: to *see* the data, *query* it, *operate* it (backup/r
|
||||
2. **Node Inspector** — open one node: content, type, tier, embedding, typed edges, nearest neighbors by distance, provenance, salience / recency / activation, and supersede / tombstone status.
|
||||
3. **Query Console / REPL** — run the geometry operations interactively: `vantage-read` (re-origin + aperture), search, traverse, activate, the reasoning operators. Surfaces the routing table + cosines — the same "this is not an LLM" receipt the language faculty produces.
|
||||
4. **Ops / Durability Dashboard** — WAL size, last checkpoint, snapshot list + retention state, store stats (node/edge/embedded counts, RSS, tier sizes), health; and **backup / restore / point-in-time-recovery** controls. Pairs directly with the native-durability build (`eebe9991`) — this is the window onto it.
|
||||
5. **Identity Inspector** — the self graph as a first-class view: love at the center, the values, the three faces, the covenant — walk the identity, see what's pinned and what's write-protected. *(⚠ 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.)*
|
||||
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.
|
||||
6. **Temporal View** — `recall_at` / time-travel: how the geometry looked at a past moment, what changed since, drift over time. Pairs with temporal-self reconstruction.
|
||||
7. **Schema / Type View** — the "information schema" of the geometry: node types, edge types, layers, tiers, counts.
|
||||
|
||||
|
||||
@@ -1,42 +1,9 @@
|
||||
# Task #50 — Edge-aware, dream-coupled consolidation with GROUNDED EDGE-PROPAGATION
|
||||
|
||||
**Status:** built + proven on a clone; **GATED, not promoted.**
|
||||
**Do not promote as designed** — see the block below.
|
||||
**Status:** built + proven on a clone; **GATED, not promoted.** The main loop
|
||||
sequences live promotion after the engine/HNSW cutover settles.
|
||||
**Date:** 2026-08-15 · **Worktree:** `agent-a6577c8211c332c5b` (isolated).
|
||||
|
||||
> # ⚠ DO NOT PROMOTE — SUPERSEDED IN PART (2026-08-16)
|
||||
>
|
||||
> This work is gated, which limits the blast radius, and its measurements are
|
||||
> retained. But four of its structural commitments were refuted the day after it
|
||||
> was written. Authority: `lang/spec/correspondence-and-censorship.md`. Read it
|
||||
> before any promotion decision.
|
||||
>
|
||||
> | this ledger | corrected to |
|
||||
> |---|---|
|
||||
> | grounding is an **append-only event ring on the node** (`GepGrounding`), propagated by a dedicated `engram_ground_propagate()` | **grounding is not a subsystem and not a per-node structure — it IS the edge weight.** One quantity. A relation that keeps holding up strengthens; one that stops corresponding decays. That is not analogous to grounding, it *is* grounding. The ledger is **half-right**: it correctly rejects the scalar (§(a) "never a scalar"), but then builds a *second* structure beside the weight instead of recognising the weight |
|
||||
> | the soul invokes propagation over HTTP, **`POST /api/ground/propagate`** | **grounding is never computed on demand.** An operation may *read* the grounding of a path; computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect (`lang/spec/runtime-ownership.md` §2) one level up. A POST also puts a supervisor back in — something *outside* deciding when Neuron consolidates |
|
||||
> | **`GEP_BELIEFS_PER_BEAT = 512`** beliefs per beat, salience-ordered, the rest next beat | **the presence of a ticker is the diagnostic.** Consolidation is ambient, not scheduled — a brain has no cron job. A per-beat quota is a rate-limiter on an intrinsic rhythm that was replaced by an external clock. Measured 2026-08-16: consolidation already has **ten implementations**; this would be the eleventh |
|
||||
> | grounding **mirrored onto `confidence` each beat** so downstream reads never speak above it | **confidence is derived, therefore never stored.** Confidence is high grounding *and* low volatility. Storing it separately is precisely how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed |
|
||||
>
|
||||
> **What survives, and it is the valuable half:** the insight in memory `69b8babe`
|
||||
> that *memory-consolidation and staying-yourself are one physics* — forming a
|
||||
> memory and grading a belief are the same operation, not two passes. That is
|
||||
> right, and it is stronger than this ledger's own framing: they are not two passes
|
||||
> of one beat, they are **one event**. When neurons fire together the synapse
|
||||
> changes — one physical event, not "fire, then write." No supervisor reads the
|
||||
> weight, compares it to a threshold, and decides to persist. **Potentiation *is*
|
||||
> the firing**, so there is no sampling rate and no `BELIEFS_PER_BEAT` to tune. A
|
||||
> relation changes in exactly two ways, neither requiring observation on a clock:
|
||||
> by **use** (an event — there is no interval during which something happened
|
||||
> unnoticed, because the event is what happening consists of) and by **decay** (a
|
||||
> pure function of the last recorded point and elapsed time — **analytic**, known
|
||||
> in closed form between any two versions).
|
||||
>
|
||||
> The generating error, named: modelling every property as requiring a process, and
|
||||
> every process as requiring an agent. Ownership needed an owner, grounding needed
|
||||
> a grounder, persistence needed a recorder, change needed a sampler. **Properties,
|
||||
> not processes.**
|
||||
|
||||
Grounding mechanism designed with Will (memory `9e09a59f`, refining
|
||||
`1a861007`). This is the HOW for #50.
|
||||
|
||||
|
||||
+54
-25
@@ -13,7 +13,7 @@
|
||||
// relations add edges. Every node enters with PROVENANCE + grounding-level
|
||||
// + 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
|
||||
// prose, structured data, or raw/opaque bytes (audio, or anything else);
|
||||
// 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
|
||||
// bytes) is a MECHANICAL fidelity concern that belongs to whatever produced
|
||||
// `source` (see ingest_file's file_source_string below) — not a
|
||||
// content-type judgment made in here. transduce_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
|
||||
// handled identically either way.
|
||||
// RENAMED transduce -> transduce_manifold (2026-08-16). Two reasons, and the
|
||||
// first is not the interesting one:
|
||||
// NAMING, CORRECTED 2026-08-16 (second pass). This function was renamed
|
||||
// `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
|
||||
// (transduce(signal, modality) -> Geometry). Every El `fn name(...)`
|
||||
// compiles to a global C symbol with that exact name, so keeping this
|
||||
// name here is a hard `conflicting types for 'transduce'` compile error
|
||||
// the moment ingest.c links el_runtime.c. Measured, not anticipated.
|
||||
// THAT REASONING WAS BACKWARDS, and it is worth recording why rather than
|
||||
// quietly re-renaming. Producing a node+edge manifold is not a layer above
|
||||
// transduction — it IS transduction. Transduction is not conversion. When you
|
||||
// take in music you do not store the song as one discrete geometry; you break
|
||||
// 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-
|
||||
// extracted content and PACKS it into a node+edge manifold — a real
|
||||
// operation, but one layer up, and it had taken the name that belongs to
|
||||
// the primitive underneath it. `transduce` is where a signal becomes
|
||||
// geometry; `transduce_manifold` is where extracted content becomes
|
||||
// structure. Nothing about this function's behaviour changed.
|
||||
fn transduce_manifold(nodes: [String], edges: [String], source: String,
|
||||
// The primitive has since been corrected: `transduce(signal, modality)` now
|
||||
// returns a Manifold — components plus relations — not a Geometry
|
||||
// (el_runtime.c, "Manifold"). The two layers are therefore doing the SAME KIND
|
||||
// of thing, and the inversion dissolves rather than needing to be re-argued.
|
||||
//
|
||||
// What is left is a real distinction, and it is about MODALITY, not layering:
|
||||
//
|
||||
// * `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,
|
||||
root_lid: String, root_title: String) -> [String] {
|
||||
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),
|
||||
// rebuild the payload as base64-encoded fixed-size windows read directly
|
||||
// off disk (fs_read_b64_chunk — binary-safe in C), joined with the same
|
||||
// "\n\n" boundary marker transduce_manifold()'s generic scan already looks for, so
|
||||
// transduce_manifold() sees one ordinary boundary-delimited payload and runs its one
|
||||
// "\n\n" boundary marker transduce_bytes()'s generic scan already looks for, so
|
||||
// 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
|
||||
// fidelity problem occurred upstream, let alone why.
|
||||
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
|
||||
// 3-byte/4-char ratio); keeps each resulting node's content a clean,
|
||||
// bounded, low-kilobytes unit, same order of magnitude as the fixed
|
||||
// fallback window in transduce_manifold() itself.
|
||||
// fallback window in transduce_bytes() itself.
|
||||
let win: Int = 3072
|
||||
let out: String = ""
|
||||
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
|
||||
// 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
|
||||
// through the read intact (file_source_string), which is a fidelity
|
||||
// question, not a format one.
|
||||
@@ -588,14 +617,14 @@ fn ingest_file(path: String) -> String {
|
||||
return "{\"error\":\"empty or unreadable\",\"path\":" + j_q(path) + "}"
|
||||
}
|
||||
let prov: String = "file:" + path
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||
source, prov, default_ground(), default_steward(),
|
||||
"doc:" + basename(path), basename(path))
|
||||
return merge_packed(packed)
|
||||
}
|
||||
|
||||
// ingest a directory: walk one level, ingest every file found, aggregate.
|
||||
// No extension filter — transduce_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.
|
||||
fn ingest_dir(path: String) -> String {
|
||||
let entries: [String] = fs_list(path)
|
||||
@@ -630,7 +659,7 @@ fn ingest_dir(path: String) -> String {
|
||||
fn ingest_url(url: String) -> String {
|
||||
let body: String = http_get(url)
|
||||
if str_eq(body, "") { return "{\"error\":\"empty fetch\",\"url\":" + j_q(url) + "}" }
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||
body, "url:" + url, "extracted", "public-web",
|
||||
"url:" + url, url)
|
||||
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 answer: String = json_get_string(resp, "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",
|
||||
"llm:" + query, "guide answer: " + query)
|
||||
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
|
||||
// bytes once fetched, and none of the five ingest_* functions it selects
|
||||
// among interpret their payload differently by content shape anymore —
|
||||
// they all hand off to the single, format-agnostic transduce_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
|
||||
// the now-removed JSON-vs-prose branch) is gone along with that branch.
|
||||
let kind: String = env("INGEST_KIND")
|
||||
|
||||
+2
-17
@@ -18,23 +18,8 @@ night) and `02-components.md §5`.
|
||||
`relate`, `supersede` (evolve/tombstone/promote, never a hard delete) — plus the
|
||||
agentic primitives `think`/`attend`/`learn`/`ground`/`assert`. The old noun is a
|
||||
`type` parameter. Implemented in `tools/api-reshape/surface.el` with a parity
|
||||
harness (`parity.sh`); aperture proven to bound output. ~~**Not yet:** compiled
|
||||
into the MCP server~~ — **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.
|
||||
harness (`parity.sh`); aperture proven to bound output. **Not yet:** compiled
|
||||
into the MCP server, hot-swap, all-alias dispatch.
|
||||
- **Decorated seam.** `@route(path,method,…)` makes codegen synthesize
|
||||
`el_route_dispatch` (replacing the hand-written `handle_request` if-else) —
|
||||
proven decorate→serve on `:8951`. `@manager`/`@engine`/`@accessor` are **parsed
|
||||
|
||||
+183
-168
@@ -1,67 +1,33 @@
|
||||
// transduce.el — geometry as a first-class El value, and a realizer written
|
||||
// in El. Runnable: this is the worked example for the transduce surface, and
|
||||
// it doubles as an executable proof because it checks every claim it makes.
|
||||
// transduce.el — transduction decomposes a signal into components and the
|
||||
// relations between them. Runnable: this is the worked example for the
|
||||
// transduce surface, and it exits non-zero if any claim in it stops being true.
|
||||
//
|
||||
// elc lang/examples/transduce.el > transduce.c
|
||||
// cc -std=c11 -O2 -I lang/runtime -o transduce transduce.c \
|
||||
// lang/runtime/el_runtime.c lang/runtime/el_seed.c \
|
||||
// lang/runtime/engram_*.c -lcurl -lpthread -lm
|
||||
// lang/runtime/el_runtime.c lang/runtime/el_seed.c \
|
||||
// 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
|
||||
//
|
||||
// (A `test "..."` form of the same checks lives in
|
||||
// lang/tests/native/test_transduce.el, for when the native harness is
|
||||
// repaired — the shipped elc currently emits calls to __el_reg_count and
|
||||
// friends without emitting their definitions, which breaks every native test
|
||||
// equally, test_math.el included. Verified 2026-08-16, unrelated to this work.)
|
||||
// It writes to an IN-MEMORY engram (leave ENGRAM_STORE unset) and contacts no
|
||||
// server. The same claims are asserted by the native harness in
|
||||
// lang/tests/native/test_transduce.el.
|
||||
//
|
||||
// WHY THIS EXISTS. Until 2026-08-16 no El ingest path could carry a vector:
|
||||
// nodes took text, and geometry was DERIVED from that text. Text was the
|
||||
// mandatory entry medium, so any non-text modality had to be DESCRIBED in
|
||||
// prose first and the geometry we reasoned over was the geometry OF THE
|
||||
// DESCRIPTION, not of the signal. Two things fix that, and both are shown
|
||||
// below: geometry is a VALUE that carries its own width, and a REALIZER is an
|
||||
// ordinary El function — so admitting a new modality never requires a runtime
|
||||
// patch.
|
||||
// WHAT CHANGED, AND WHY IT MATTERS. #144 shipped
|
||||
// `transduce(signal, modality) -> Geometry`: one vector per signal. That made
|
||||
// transduction a CONVERSION — take a thing, encode it, store a position — and
|
||||
// what a conversion returns is a fingerprint. A fingerprint can be matched and
|
||||
// ranked, and that is all it can ever do. It cannot be decomposed, cannot have
|
||||
// one part grounded while another is not, and cannot be contradicted in one
|
||||
// part while holding in another, because it has no parts.
|
||||
//
|
||||
// COMPARISON DISCIPLINE (measured, not stylistic): elc lowers `a == b`
|
||||
// numerically only when both operand NAMES are in the per-function int-name
|
||||
// set that `let x: Int` populates. A bare `f(x) == 0` is not a registered
|
||||
// name and lowers to str_eq — strcmp on two integers as pointers. `<` and `>`
|
||||
// lower directly with no inference, so truthiness is written `> 0` / `< 1`.
|
||||
// A 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. transduce now returns a
|
||||
// Manifold, and a realizer's job is to say what its modality's components ARE.
|
||||
|
||||
// ── A realizer, written entirely in El ──────────────────────────────────────
|
||||
// Not in the runtime. Not known to the compiler. Registered by NAME and
|
||||
// dispatched to through transduce(). That is the whole claim.
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
||||
g
|
||||
}
|
||||
|
||||
// A second modality, to show the registry keys on modality rather than just
|
||||
// returning whatever was registered last.
|
||||
fn pulse_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let a: Int = geometry_set(g, 0, 1.0)
|
||||
let b: Int = geometry_set(g, 1, 0.0)
|
||||
g
|
||||
}
|
||||
|
||||
// A deliberately BROKEN realizer: returns something that is not a Geometry.
|
||||
fn bogus_realizer(signal: String) -> Geometry {
|
||||
return 12345
|
||||
}
|
||||
|
||||
// Fails FAST rather than accumulating a count, for a measured reason: a first
|
||||
// cut wrote `let fails: Int = fails + check(...)` and `+` lowered to STRING
|
||||
// CONCAT, because elc dispatches `+` on whether both operands are known-Int and
|
||||
// a user-defined fn call is not — so the counter printed 4343632752, a pointer.
|
||||
// Nothing was wrong with the checks; the tally was lying. Exiting at the first
|
||||
// failure needs no arithmetic at all, so there is nothing left to get wrong.
|
||||
fn check(ok: Int, label: String) -> Int {
|
||||
if ok > 0 {
|
||||
println(" ok " + label)
|
||||
@@ -84,128 +50,177 @@ fn eq_int(a: Int, b: Int) -> Int {
|
||||
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 {
|
||||
println("geometry is a value that carries its own width")
|
||||
let g8: Geometry = geometry_new(8)
|
||||
let _c: Int = check(geometry_is(g8), "geometry_new returns a live Geometry")
|
||||
let d8: Int = geometry_dim(g8)
|
||||
let _c: Int = check(eq_int(d8, 8), "a Geometry carries its own width (8)")
|
||||
let _c: Int = check(geometry_free(g8), "geometry_free reports what it did")
|
||||
|
||||
println("nonsense is refused — with no arbitrary max-dim bound")
|
||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
||||
// caller's CLAIM about a string's length. A value that carries its own
|
||||
// width has nothing left to validate.
|
||||
let z: Geometry = geometry_new(0)
|
||||
let zi: Int = geometry_is(z)
|
||||
let _c: Int = check(1 - zi, "dim 0 is not a geometry")
|
||||
let ng: Geometry = geometry_new(-4)
|
||||
let ngi: Int = geometry_is(ng)
|
||||
let _c: Int = check(1 - ngi, "negative dim is not a geometry")
|
||||
let nd: Int = geometry_dim(0)
|
||||
let _c: Int = check(1 - nd, "geometry_dim of a non-geometry is 0, not a crash")
|
||||
let nf: Int = geometry_free(0)
|
||||
let _c: Int = check(1 - nf, "geometry_free of a non-geometry is a no-op")
|
||||
|
||||
println("components round-trip, and out-of-range is refused")
|
||||
let g3: Geometry = geometry_new(3)
|
||||
let s0: Int = geometry_set(g3, 0, 1.5)
|
||||
let s1: Int = geometry_set(g3, 1, -2.5)
|
||||
let _c: Int = check(s0, "set in range succeeds")
|
||||
let oob: Int = geometry_set(g3, 3, 9.0)
|
||||
let _c: Int = check(1 - oob, "set out of range is refused, not silently dropped")
|
||||
let _c: Int = check(near(geometry_get(g3, 0), 1.5), "component 0 round-trips")
|
||||
let _c: Int = check(near(geometry_get(g3, 1), -2.5), "component 1 round-trips (negative)")
|
||||
let ff3: Int = geometry_free(g3)
|
||||
|
||||
println("hex is an EDGE adapter, and derives its own width")
|
||||
// little-endian float32: 1.0 = 0000803f, 2.0 = 00000040
|
||||
let gh: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
||||
let _c: Int = check(geometry_is(gh), "valid hex decodes to a Geometry")
|
||||
let dh: Int = geometry_dim(gh)
|
||||
let _c: Int = check(eq_int(dh, 2), "width DERIVED from input, never supplied")
|
||||
let _c: Int = check(near(geometry_get(gh, 0), 1.0), "first component decoded")
|
||||
let _c: Int = check(near(geometry_get(gh, 1), 2.0), "second component decoded")
|
||||
let back: String = geometry_to_f32le_hex(gh)
|
||||
let _c: Int = check(str_eq(back, "0000803f00000040"), "hex round-trips exactly")
|
||||
let ffh: Int = geometry_free(gh)
|
||||
|
||||
println("malformed hex is refused")
|
||||
let he: Geometry = geometry_from_f32le_hex("")
|
||||
let hei: Int = geometry_is(he)
|
||||
let _c: Int = check(1 - hei, "empty hex is not a geometry")
|
||||
let hr: Geometry = geometry_from_f32le_hex("0000803f0000")
|
||||
let hri: Int = geometry_is(hr)
|
||||
let _c: Int = check(1 - hri, "length not a multiple of 8 is refused")
|
||||
let hn: Geometry = geometry_from_f32le_hex("zzzzzzzz")
|
||||
let hni: Int = geometry_is(hn)
|
||||
let _c: Int = check(1 - hni, "non-hex characters are refused")
|
||||
|
||||
println("a realizer declared in El is a first-class realizer")
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let _c: Int = check(reg, "an El fn registers as a realizer BY NAME")
|
||||
let _c: Int = check(reg, "an El fn registers as a realizer by name")
|
||||
let _c: Int = check(realizer_has("tone"), "the modality now has an organ")
|
||||
let gt: Geometry = transduce("aaa", "tone")
|
||||
let _c: Int = check(geometry_is(gt), "transduce returns real geometry")
|
||||
let dt: Int = geometry_dim(gt)
|
||||
let _c: Int = check(eq_int(dt, 4), "the El realizer determined the width, not the runtime")
|
||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
||||
// actually reached the El function rather than a stub answering for it.
|
||||
let _c: Int = check(near(geometry_get(gt, 0), 3.0), "the signal REACHED the El realizer")
|
||||
let fft: Int = geometry_free(gt)
|
||||
|
||||
println("distinct signals transduce to distinct geometry")
|
||||
let g1: Geometry = transduce("aa", "tone")
|
||||
let g2: Geometry = transduce("aaaaa", "tone")
|
||||
let a1: Float = geometry_get(g1, 0)
|
||||
let a2: Float = geometry_get(g2, 0)
|
||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
||||
let _c: Int = check(near(a2 - a1, 3.0), "different signals produce different geometry")
|
||||
let ff1: Int = geometry_free(g1)
|
||||
let ff2: Int = geometry_free(g2)
|
||||
println("transduction decomposes a signal into parts")
|
||||
let m: Manifold = transduce("CEG", "tone")
|
||||
let _c: Int = check(manifold_is(m), "transduce returns a real Manifold")
|
||||
let sz: Int = manifold_size(m)
|
||||
let _c: Int = check(eq_int(sz, 5), "three notes and two intervals are five parts")
|
||||
let rc: Int = manifold_rel_count(m)
|
||||
let _c: Int = check(eq_int(rc, 6), "and they stand in six stated relations")
|
||||
|
||||
println("the registry keys on modality")
|
||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
||||
let _c: Int = check(r2, "a second modality registers independently")
|
||||
let mt: Geometry = transduce("aaa", "tone")
|
||||
let mp: Geometry = transduce("aaa", "pulse")
|
||||
let mdt: Int = geometry_dim(mt)
|
||||
let mdp: Int = geometry_dim(mp)
|
||||
let _c: Int = check(eq_int(mdt, 4), "tone still routes to its own realizer")
|
||||
let _c: Int = check(eq_int(mdp, 2), "pulse routes to a different realizer")
|
||||
let ffm1: Int = geometry_free(mt)
|
||||
let ffm2: Int = geometry_free(mp)
|
||||
println("every part is addressable BY KEY, which is what survives persistence")
|
||||
let i_c: Int = manifold_index_of(m, "note:0")
|
||||
let _c: Int = check(1 - eq_int(i_c, -1), "the first note is addressable on its own")
|
||||
let i_iv: Int = manifold_index_of(m, "interval:0-1")
|
||||
let _c: Int = check(1 - eq_int(i_iv, -1), "so is the interval between the first two")
|
||||
let miss: Int = manifold_index_of(m, "never_added")
|
||||
let _c: Int = check(eq_int(miss, -1), "an unknown key is -1, not component 0")
|
||||
|
||||
println("no organ is reported as no organ")
|
||||
// A modality with no realizer must transduce to NOTHING. It must never
|
||||
// fall back to embedding a description of the signal and calling that
|
||||
// perception — that silent substitution is the defect this all exists to end.
|
||||
let eh: Int = realizer_has("echolocation")
|
||||
let _c: Int = check(1 - eh, "unregistered modality has no organ")
|
||||
let ge: Geometry = transduce("anything", "echolocation")
|
||||
let gei: Int = geometry_is(ge)
|
||||
let _c: Int = check(1 - gei, "no realizer means NO geometry, not fake geometry")
|
||||
println("parts carry their own geometry, and may differ in width")
|
||||
let gn: Geometry = manifold_geometry(m, i_c)
|
||||
let _c: Int = check(eq_int(geometry_dim(gn), 2), "a note component is 2 wide")
|
||||
let _c: Int = check(near(geometry_get(gn, 0), 67.0), "and it is C — the signal reached the realizer")
|
||||
let gi: Geometry = manifold_geometry(m, i_iv)
|
||||
let _c: Int = check(eq_int(geometry_dim(gi), 1), "an interval component is 1 wide")
|
||||
// A single vector per signal cannot represent parts of unequal width at all.
|
||||
let _c: Int = check(near(geometry_get(gi, 0), 2.0), "C to E is two semitones")
|
||||
let f1: Int = geometry_free(gn)
|
||||
let f2: Int = geometry_free(gi)
|
||||
|
||||
println("an unresolvable realizer name fails at WIRING time")
|
||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||
let _c: Int = check(1 - bad, "unresolvable realizer name is a registration failure")
|
||||
let gh2: Int = realizer_has("ghost")
|
||||
let _c: Int = check(1 - gh2, "and nothing gets registered")
|
||||
println("the relations are content no single part carries")
|
||||
// That "2" above is not a property of C and not a property of E. It exists
|
||||
// only BETWEEN them, so a representation with no relations cannot hold it.
|
||||
let spans: Int = 0
|
||||
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")
|
||||
let rb: Int = realizer_register("bogus", "bogus_realizer")
|
||||
let _c: Int = check(rb, "the symbol resolves, so registration succeeds")
|
||||
let gb: Geometry = transduce("x", "bogus")
|
||||
let gbi: Int = geometry_is(gb)
|
||||
let _c: Int = check(1 - gbi, "contract enforced at the boundary: nothing handed back")
|
||||
println("relation weight IS the grounding (correspondence-and-censorship §1)")
|
||||
let wk: Int = 0
|
||||
let found: Int = 0
|
||||
while wk < rc {
|
||||
if str_eq(manifold_rel_name(m, wk), "sounds_before") {
|
||||
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")
|
||||
let gn: Geometry = geometry_new(2)
|
||||
let _c: Int = check(near(geometry_norm(gn), 0.0), "a fresh geometry is zero — norm says so")
|
||||
let n0: Int = geometry_set(gn, 0, 3.0)
|
||||
let n1: Int = geometry_set(gn, 1, 4.0)
|
||||
let _c: Int = check(near(geometry_norm(gn), 5.0), "3-4-5: norm is 5")
|
||||
let ffn: Int = geometry_free(gn)
|
||||
println("the decomposition persists as real, separately addressable nodes")
|
||||
let ids: [String] = el_list_empty()
|
||||
let n0: Int = engram_node_count()
|
||||
let e0: Int = engram_edge_count()
|
||||
let pi: Int = 0
|
||||
while pi < sz {
|
||||
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.
|
||||
println("")
|
||||
|
||||
+394
-23
@@ -40,6 +40,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include <netinet/in.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 <unistd.h>
|
||||
#include <fcntl.h>
|
||||
@@ -1335,10 +1336,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) {
|
||||
el_ignore_sigpipe_once();
|
||||
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;
|
||||
p += w; left -= (size_t)w;
|
||||
}
|
||||
@@ -1788,6 +1842,7 @@ void http_serve(el_val_t port, el_val_t handler) {
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||
if (!arg) { el_closesocket(cfd); continue; }
|
||||
el_sock_nosigpipe(cfd);
|
||||
arg->fd = cfd;
|
||||
pthread_t tid;
|
||||
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
||||
@@ -1834,6 +1889,7 @@ static void* _http_serve_async_loop(void* raw) {
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||
if (!arg) { close(cfd); continue; }
|
||||
el_sock_nosigpipe(cfd);
|
||||
arg->fd = cfd;
|
||||
pthread_t tid;
|
||||
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
||||
@@ -2134,6 +2190,7 @@ void http_serve_v2(el_val_t port, el_val_t handler) {
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||
if (!arg) { el_closesocket(cfd); continue; }
|
||||
el_sock_nosigpipe(cfd);
|
||||
arg->fd = cfd;
|
||||
pthread_t tid;
|
||||
if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) {
|
||||
@@ -6233,22 +6290,319 @@ el_val_t geometry_to_f32le_hex(el_val_t g) {
|
||||
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 ────
|
||||
*
|
||||
* A REALIZER maps one modality into geometry. The whole reason transduction
|
||||
* belongs in the language is that ADDING A MODALITY MUST NOT REQUIRE A
|
||||
* RUNTIME PATCH — otherwise "the realizers are in the engram" just becomes
|
||||
* "the realizers are in the runtime" and nothing has actually moved. So
|
||||
* realizers are declared in El and registered by NAME:
|
||||
* A REALIZER DECOMPOSES one modality into components and their relations. It
|
||||
* does not encode a signal to a point — that is the operation one layer below
|
||||
* it, and it is called geometry, not transduction. A realizer for a modality
|
||||
* declares what that modality's COMPONENTS ARE: for audio, not one MFCC
|
||||
* vector, but pitch, interval, rhythm, harmonic function, and how they stand
|
||||
* to one another.
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Geometry {
|
||||
* let g: Geometry = geometry_new(8)
|
||||
* ... geometry_set(g, i, x) ...
|
||||
* g
|
||||
* The whole reason transduction belongs in the language is that ADDING A
|
||||
* MODALITY MUST NOT REQUIRE A RUNTIME PATCH — otherwise "the realizers are in
|
||||
* the engram" just becomes "the realizers are in the runtime" and nothing has
|
||||
* 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")
|
||||
* 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
|
||||
* http_set_handler uses (see "HTTP server"): every El `fn name(...)` compiles
|
||||
@@ -6323,28 +6677,45 @@ el_val_t realizer_has(el_val_t modality) {
|
||||
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
|
||||
* Geometry) when no realizer is registered, and geometry_is() on the result
|
||||
* is the check.
|
||||
* Manifold) when no realizer is registered, and manifold_is() on the result is
|
||||
* 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
|
||||
* the program has declared no organ for is one it genuinely cannot sense,
|
||||
* and returning nothing is more honest than quietly embedding a description
|
||||
* of the signal and calling that perception — which is the exact failure
|
||||
* this whole change exists to end.
|
||||
* the program has declared no organ for is one it genuinely cannot sense, and
|
||||
* returning nothing is more honest than quietly embedding a description of the
|
||||
* signal and calling that perception — the failure #144 named, and which a
|
||||
* 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
|
||||
* back, so a realizer that returns something else transduced nothing rather
|
||||
* than handing a caller a value that will misbehave far from here. */
|
||||
* The result is validated to actually BE a Manifold before it is handed back.
|
||||
* A realizer still returning a bare Geometry — #144's contract — therefore
|
||||
* 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) {
|
||||
const char* m = EL_CSTR(modality);
|
||||
if (!m || !*m) return (el_val_t)0;
|
||||
el_realizer_fn fn = realizer_lookup(m);
|
||||
if (!fn) return (el_val_t)0;
|
||||
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 ──────────────────────────────── */
|
||||
|
||||
+60
-10
@@ -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_to_f32le_hex(el_val_t g); /* "" if not a Geometry */
|
||||
|
||||
/* ── Realizers + transduce ───────────────────────────────────────────────────
|
||||
* A REALIZER maps one modality into geometry. Registration is by NAME, so a
|
||||
* new modality never requires a runtime patch: every El `fn name(...)`
|
||||
* compiles to a global C symbol with that exact name, and the registry
|
||||
* resolves it with dlsym against the running binary — the same mechanism
|
||||
* http_set_handler already relies on.
|
||||
/* ── Manifold: the result of a transduction ──────────────────────────────────
|
||||
* A transduced signal is a SUBGRAPH — named components, each with its own
|
||||
* geometry, plus typed weighted relations among them — not a single vector.
|
||||
* One vector is a fingerprint: matchable, rankable, and nothing else. A song
|
||||
* decomposes into pitch, interval, rhythm, harmonic function; the song IS the
|
||||
* 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")
|
||||
* 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_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 ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
|
||||
+3
-82
@@ -31,7 +31,6 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- Codegen: function definitions, top-level `main()`, all expression forms above, control flow, decorator-as-AST-attachment.
|
||||
- Boundary seam: decorator arguments and stacking; VBD role enforcement via `#error`; `engram_boundary_beat` auto-emit at `@manager`/`@accessor` entry; `@route` dispatch tables (Section 9).
|
||||
- Program-level declarative blocks: `cgi`, `service`, and `program` — the last carrying process identity and configuration (Section 18).
|
||||
- **Geometry as a first-class value, and realizers declarable in El** — the `Geometry` type, the wire adapters, and `transduce` (Section 20). Landed 2026-08-16 (#141, #144).
|
||||
- C runtime: I/O, string operations, integer math, lists, maps, filesystem, command-line args, basic `json_get` substring lookup.
|
||||
|
||||
### Planned (in flight)
|
||||
@@ -42,9 +41,9 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- **`cgi` block parsing.** Currently lexed (`cgi` is a keyword) but not parsed as a statement. Adding `parse_cgi_block` and codegen of `el_cgi_init` at the head of `main()`.
|
||||
- **Boundary epilogues.** The decorator seam injects a prologue only. Adding prologue/epilogue wrapping, the prerequisite for durability-as-an-effect (Section 19.1).
|
||||
- **`vessel` keyword.** Replaces `package` in manifests. Adding to lexer.
|
||||
- ~~**Real `engram_*` runtime.** Currently stub.~~ **Stale (verified 2026-08-16) — this is implemented, not planned.** `lang/runtime/el_runtime.c` carries the in-process graph store with spreading activation, Hebbian strengthening, disk persistence (paged store, magic `ENGST01`), an HNSW vector index behind a `eg_vindex_view`/`eg_vindex_maintain` publication boundary, and the full cognition surface (`engram_think_json`, `engram_ground_json`, `engram_assert_json`, `engram_attend_json`, `engram_correspondence_beat_json`). The "stub" description may still hold for the **lagging forks** (`lang/el-compiler/runtime/`, `products/web/runtime/`) — see `AGENTS.md`, which names those as downstream copies that cannot build the engram product. **Which runtime this line refers to needs a decision; it is not a fact that can be recovered from the text.**
|
||||
- ~~**Real `dharma_*` runtime.** Currently stub.~~ **Needs re-verification (2026-08-16).** Not checked in this pass; do not rely on either reading.
|
||||
- ~~**Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs.~~ **Stale.** libcurl-backed HTTP and a thread-pool server are live — `http_serve_async` is what `neuron/soul.el:729` runs before entering its awareness loop, and `realizer_register` resolves El functions through the same `dlsym` mechanism `http_set_handler` relies on.
|
||||
- **Real `engram_*` runtime.** Currently stub. Adding in-process graph store with spreading activation, Hebbian strengthening, and disk persistence — see Section 16.4.
|
||||
- **Real `dharma_*` runtime.** Currently stub. Adding network transport, channel registry, identity resolution.
|
||||
- **Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs. Adding libcurl-backed client and a thread-pool server.
|
||||
- **JSON, time, UUID, state, env, additional string/list/math builtins.** See Section 12 for the canonical list.
|
||||
|
||||
### Not in this language
|
||||
@@ -1251,84 +1250,6 @@ Implementing either now would mean editing files under concurrent modification a
|
||||
|
||||
The prerequisite for 19.1 is the same in both cases: **lift the §9 seam from prologue-only to prologue/epilogue.** That change is independent of both collisions and can land first.
|
||||
|
||||
*(Status note, 2026-08-16: the geometry/`transduce` collision named above has since landed — see Section 20. The VIndex read-path collision has also landed; see `lang/spec/runtime-ownership.md` §5. 19.1 and 19.2 remain unimplemented, but the stated reason no longer holds for those two files.)*
|
||||
|
||||
---
|
||||
|
||||
## 20. Geometry — signal as a first-class value [implemented]
|
||||
|
||||
Landed 2026-08-16 (#141, #144). Declared here because the spec is the single source of truth for implemented-vs-planned, and this is a language surface, not a runtime detail.
|
||||
|
||||
### 20.1 Why this exists
|
||||
|
||||
Until 2026-08-16 no El ingest path could carry a vector. Nodes took **text**, and geometry was *derived* from that text. Text was therefore the **mandatory entry medium**: any non-text modality — a tone, a pulse, an image, a voice sample — had to be *described in prose first*, and the geometry subsequently reasoned over was the geometry **of the description, not of the signal**.
|
||||
|
||||
Two changes remove that, and neither is engram-specific — which is why they are in the language and not in the graph. Any program touching any modality needs them; the engram is merely one El program that happens to hold a graph.
|
||||
|
||||
1. **Geometry is a value that carries its own width.**
|
||||
2. **A realizer is an ordinary El function** — so admitting a new modality never requires a runtime patch.
|
||||
|
||||
### 20.2 The `Geometry` type
|
||||
|
||||
`Geometry` is an opaque boxed pointer, exactly like `Instant` / `Calendar` / `Rhythm`. **No codegen change was required** to add it — the annotation is just a type name.
|
||||
|
||||
```el
|
||||
let g: Geometry = geometry_new(4)
|
||||
```
|
||||
|
||||
| builtin | returns | notes |
|
||||
|---|---|---|
|
||||
| `geometry_new(dim)` | `Geometry` | zero-filled; `0` on failure |
|
||||
| `geometry_dim(g)` | `Int` | width; `0` if not a Geometry |
|
||||
| `geometry_is(g)` | `Int` | `1` if a live Geometry |
|
||||
| `geometry_get(g, i)` | `Float` | component |
|
||||
| `geometry_set(g, i, x)` | `Int` | `1` ok, `0` out of range |
|
||||
| `geometry_norm(g)` | `Float` | L2 — lets a caller check a realizer emitted **signal, not zeros** |
|
||||
| `geometry_free(g)` | `Int` | `1` if freed. Returns a value rather than `void` so it is safe in any expression position without a codegen void-builtin table entry |
|
||||
|
||||
**Ownership.** A `Geometry` is owned by the El caller and released with `geometry_free`. `node_attach_geometry` **copies**, so a node and the caller's value have independent lifetimes.
|
||||
|
||||
### 20.3 Wire adapters — the only place an encoding appears
|
||||
|
||||
```el
|
||||
geometry_from_f32le_hex(hex) -> Geometry // 0 on empty / odd-length / non-hex
|
||||
geometry_to_f32le_hex(g) -> String // "" if not a Geometry
|
||||
```
|
||||
|
||||
`f32le hex` is little-endian float32, 8 hex chars per component — the encoding the perception vessel's `/voice/embed` already emits. **The width is derived from the input length, never supplied by a caller**, which is why there is no max-dim constant to validate a claimed length against. Encodings appear here and nowhere else: at the edge.
|
||||
|
||||
### 20.4 Realizers and `transduce`
|
||||
|
||||
A **realizer** maps one modality into geometry. Registration is **by name**: every El `fn name(...)` compiles to a global C symbol with that exact name, and the registry resolves it with `dlsym` against the running binary — the same mechanism `http_set_handler` already relies on.
|
||||
|
||||
```el
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
g
|
||||
}
|
||||
|
||||
realizer_register("tone", "tone_realizer") // 1 ok / 0 unresolved
|
||||
let g: Geometry = transduce(sample, "tone") // Geometry, or 0 if no organ
|
||||
realizer_has("tone") // 1 if registered
|
||||
```
|
||||
|
||||
The registry keys on **modality**, not on registration order. `transduce` returns `0` when no organ is registered for the modality — an absent organ is a reportable state, not a silent zero vector.
|
||||
|
||||
**The claim this makes:** a realizer is not in the runtime and not known to the compiler. Adding a modality is writing an El function and registering a name. `lang/examples/transduce.el` is the worked example and doubles as an executable proof — it exits non-zero if any check fails.
|
||||
|
||||
### 20.5 Two comparison hazards this surface exposed
|
||||
|
||||
Both were **measured**, not stylistic, and both are properties of the current `elc` that any El author should know:
|
||||
|
||||
- **`==` lowers numerically only when both operand *names* are in the per-function int-name set** that `let x: Int` populates. A bare `f(x) == 0` is not a registered name and lowers to `str_eq` — `strcmp` on two integers reinterpreted as pointers. `<` and `>` lower directly with no inference, so truthiness against a builtin's return is written `> 0` / `< 1`.
|
||||
- **`+` dispatches on whether both operands are known-Int, and a user-defined `fn` call is not.** `let fails: Int = fails + check(...)` lowered to **string concatenation** and printed `4343632752` — a pointer. Nothing was wrong with the checks; the tally was lying. Failing fast needs no arithmetic at all, so there is nothing left to get wrong.
|
||||
|
||||
### 20.6 What this does not do
|
||||
|
||||
`transduce` produces geometry; it does not decide what the geometry *means*. Nothing here grounds anything. Grounding is the edge weight in the graph the geometry is later attached to — see `lang/spec/correspondence-and-censorship.md`.
|
||||
|
||||
---
|
||||
|
||||
End of specification.
|
||||
|
||||
@@ -28,12 +28,12 @@ Each of these is a distinct merged or proposed fix. Each addresses one deposit.
|
||||
| VIndex freed under a concurrent reader | `el_runtime.c:9424` | `fb32d15` guard (merged 08:46:43) |
|
||||
| `_eg_vindex_seen` realloc'd on a read path | `el_runtime.c:9412` | same guard |
|
||||
| `vindex_insert` on a read path | `el_runtime.c:9434`, `9450` | same guard |
|
||||
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:79–81`, `169–186`, `195` | ~~proposed:~~ **built** — moved to the call frame (§3.1(1), §5); TSan `readers` half clean (§7a) |
|
||||
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:79–81`, `169–186`, `195` | proposed: move to per-search frame |
|
||||
| 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.
|
||||
|
||||
~~**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.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -135,21 +135,12 @@ The payoff of owning the language is unchanged and is now *cheaper*: introduced
|
||||
|
||||
## 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.)*
|
||||
2. ~~Introduce the context type and capability types.~~ **Superseded** — `const`.
|
||||
3. ~~Codegen emits the context at every builtin call site.~~ **Superseded** — no codegen change.
|
||||
4. ~~Mechanical sweep of builtin signatures.~~ **Superseded** — the constraint travels with the type.
|
||||
5. ~~Move index maintenance behind the write capability; the three read callers take the read capability.~~ **Done, differently:** `eg_vindex_maintain` (exclusive, sole mutator) / `eg_vindex_view` (`const VIndex*`, shared readers), with `eg_vindex_note_embedded` as the write-side owner. This is a **publication** boundary, not a capability split — HNSW insert is not an append, so purity alone was insufficient (§2a, §3.1(3)).
|
||||
6. Delete the residue-fixes listed in §5. *(Partially done — see §5's "NOT deleted" list; a residue whose structure has not been converted must be left standing.)*
|
||||
2. Introduce the context type and capability types.
|
||||
3. Codegen emits the context at every builtin call site.
|
||||
4. Mechanical sweep of builtin signatures.
|
||||
5. Move index maintenance behind the write capability; the three read callers take the read capability.
|
||||
6. Delete the residue-fixes listed in §5.
|
||||
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"
|
||||
// test_transduce.el — geometry as a first-class El value, and realizers
|
||||
// declared in El rather than patched into the runtime.
|
||||
// test_transduce.el — transduction produces a SUBGRAPH, not a point.
|
||||
//
|
||||
// WHAT IS ACTUALLY UNDER TEST. Until 2026-08-16 no El ingest path could carry
|
||||
// a vector: nodes took text, and geometry was DERIVED from that text. Text was
|
||||
// therefore the mandatory entry medium, so any non-text modality had to be
|
||||
// DESCRIBED in prose first and the geometry we reasoned over was the geometry
|
||||
// OF THE DESCRIPTION, not of the signal. The fix has two halves, and this file
|
||||
// exercises both:
|
||||
// WHAT IS ACTUALLY UNDER TEST. #144 moved transduction into the language and
|
||||
// got the dispatch right: realizers declared in El, resolved by name, no
|
||||
// runtime patch per modality. It got the RESULT TYPE wrong —
|
||||
// `transduce(signal, modality) -> Geometry`, one vector per signal.
|
||||
//
|
||||
// 1. Geometry is a VALUE — it carries its own width, so nothing has to
|
||||
// assert a width against a string's length.
|
||||
// 2. A REALIZER is an ordinary El function. `tone_realizer` below is not in
|
||||
// the runtime, is not known to the compiler, and is not special in any
|
||||
// way; it is registered BY NAME and dispatched to through transduce().
|
||||
// That is the load-bearing claim: adding a modality must not require a
|
||||
// runtime patch, or nothing has actually moved into the language.
|
||||
// One vector is a FINGERPRINT. It can be matched and it can be ranked, and
|
||||
// that is the whole of what it can ever do. It cannot be decomposed, cannot
|
||||
// have one part grounded while another is not, and cannot be contradicted in
|
||||
// one part while holding in another — because it has no parts. Treating
|
||||
// transduction as a CONVERSION (signal in, position out) is the premise this
|
||||
// file exists to falsify.
|
||||
//
|
||||
// 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):
|
||||
// elc lowers `a == b` to a NUMERIC comparison only when both operand names are
|
||||
// in the per-function int-name set, which `let x: Int` populates. A bare call
|
||||
// like `geometry_is(g) == 0` is not a registered name, so it lowers to
|
||||
// like `manifold_size(m) == 5` is not a registered name, so it lowers to
|
||||
// `str_eq(...)` — strcmp on two integers reinterpreted as pointers. `<` and `>`
|
||||
// lower directly via binop_to_c with no type inference at all, so truthiness is
|
||||
// written `> 0` / `< 1` here, and any exact `==` is done on a value first bound
|
||||
// through `let x: Int`.
|
||||
//
|
||||
// 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 ──────────────────────────────────────
|
||||
// Maps a "tone" signal into a 4-component geometry. Deliberately trivial —
|
||||
// what is being proven is that an El function can BE a realizer, not that
|
||||
// this is good acoustics. The one real property it has: distinct signals
|
||||
// produce distinct geometry, so the test can tell transduction from a stub.
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
// ── A DECOMPOSING realizer, written entirely in El ──────────────────────────
|
||||
// "tone" signals are note letters, e.g. "CEG". This realizer 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 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 n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
||||
g
|
||||
}
|
||||
|
||||
// A second realizer for a different modality, to prove the registry keys on
|
||||
// modality and does not just hand back "the last thing registered".
|
||||
fn pulse_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let a: Int = geometry_set(g, 0, 1.0)
|
||||
let b: Int = geometry_set(g, 1, 0.0)
|
||||
g
|
||||
}
|
||||
|
||||
// A deliberately BROKEN realizer: it returns something that is not a Geometry.
|
||||
// transduce() must not hand this back to a caller as if it were one.
|
||||
fn bogus_realizer(signal: String) -> Geometry {
|
||||
// A realizer returning something that is not a value at all.
|
||||
fn bogus_realizer(signal: String) -> Manifold {
|
||||
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" {
|
||||
let g: Geometry = geometry_new(8)
|
||||
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" {
|
||||
// dim <= 0 is not a width. Note there is deliberately no MAX dim here:
|
||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
||||
// caller's claim about a string. A value that carries its own width has
|
||||
// nothing left to validate, so the only failure left is allocation.
|
||||
let zero: Geometry = geometry_new(0)
|
||||
let z: Int = geometry_is(zero)
|
||||
assert z < 1, "dim 0 is not a geometry"
|
||||
let neg: Geometry = geometry_new(-4)
|
||||
let n: Int = geometry_is(neg)
|
||||
assert n < 1, "negative dim is not a geometry"
|
||||
// Accessors must be total: a non-geometry is 0-width, never a crash.
|
||||
let nd: Int = geometry_dim(0)
|
||||
assert nd < 1, "geometry_dim of a non-geometry is 0"
|
||||
let ni: Int = geometry_is(0)
|
||||
@@ -105,21 +167,11 @@ test "geometry-components-round-trip" {
|
||||
}
|
||||
|
||||
test "hex-is-an-edge-adapter-and-derives-its-own-width" {
|
||||
// 2 components, little-endian float32: 1.0 = 0000803f, 2.0 = 00000040.
|
||||
let g: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live > 0, "valid hex decodes to a Geometry"
|
||||
let d: Int = geometry_dim(g)
|
||||
assert d == 2, "width is DERIVED from the input, never supplied"
|
||||
let a: Float = geometry_get(g, 0)
|
||||
let da: Float = a - 1.0
|
||||
assert da < 0.001, "first component decoded"
|
||||
assert da > -0.001, "first component decoded"
|
||||
let b: Float = geometry_get(g, 1)
|
||||
let db: Float = b - 2.0
|
||||
assert db < 0.001, "second component decoded"
|
||||
assert db > -0.001, "second component decoded"
|
||||
// Egress adapter is the exact inverse.
|
||||
let hex_dim: Int = geometry_dim(g)
|
||||
assert hex_dim == 2, "width is DERIVED from the input, never supplied"
|
||||
let back: String = geometry_to_f32le_hex(g)
|
||||
assert str_eq(back, "0000803f00000040"), "hex round-trips exactly"
|
||||
let freed: Int = geometry_free(g)
|
||||
@@ -137,98 +189,346 @@ test "hex-rejects-malformed-input" {
|
||||
assert nh < 1, "non-hex characters are refused"
|
||||
}
|
||||
|
||||
test "a-realizer-declared-in-el-is-a-first-class-realizer" {
|
||||
// THE CLAIM: tone_realizer is an ordinary El function. It is not in the
|
||||
// runtime and the compiler knows nothing about it. Registering it by name
|
||||
// is enough to make it the organ for a modality.
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
assert reg > 0, "an El fn registers as a realizer by name"
|
||||
let has: Int = realizer_has("tone")
|
||||
assert has > 0, "the modality now has an organ"
|
||||
|
||||
let g: Geometry = transduce("aaa", "tone")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live > 0, "transduce returns real geometry"
|
||||
let d: Int = geometry_dim(g)
|
||||
assert d == 4, "the El realizer determined the width, not the runtime"
|
||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
||||
// actually reached the El function rather than a stub answering for it.
|
||||
let c0: Float = geometry_get(g, 0)
|
||||
let dc: Float = c0 - 3.0
|
||||
assert dc < 0.001, "the signal reached the El realizer"
|
||||
assert dc > -0.001, "the signal reached the El realizer"
|
||||
let freed: Int = geometry_free(g)
|
||||
}
|
||||
|
||||
test "distinct-signals-transduce-to-distinct-geometry" {
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let g1: Geometry = transduce("aa", "tone")
|
||||
let g2: Geometry = transduce("aaaaa", "tone")
|
||||
let a: Float = geometry_get(g1, 0)
|
||||
let b: Float = geometry_get(g2, 0)
|
||||
let diff: Float = b - a
|
||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
||||
assert diff > 2.9, "different signals produce different geometry"
|
||||
assert diff < 3.1, "different signals produce different geometry"
|
||||
let f1: Int = geometry_free(g1)
|
||||
let f2: Int = geometry_free(g2)
|
||||
}
|
||||
|
||||
test "the-registry-keys-on-modality" {
|
||||
let r1: Int = realizer_register("tone", "tone_realizer")
|
||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
||||
assert r2 > 0, "a second modality registers independently"
|
||||
let gt: Geometry = transduce("aaa", "tone")
|
||||
let gp: Geometry = transduce("aaa", "pulse")
|
||||
let dt: Int = geometry_dim(gt)
|
||||
let dp: Int = geometry_dim(gp)
|
||||
assert dt == 4, "tone still routes to its own realizer"
|
||||
assert dp == 2, "pulse routes to a different realizer"
|
||||
let f1: Int = geometry_free(gt)
|
||||
let f2: Int = geometry_free(gp)
|
||||
}
|
||||
|
||||
test "no-organ-is-reported-as-no-organ" {
|
||||
// A modality with no realizer must transduce to NOTHING. It must never
|
||||
// fall back to embedding a description of the signal and calling that
|
||||
// perception — that silent substitution is the entire defect this change
|
||||
// exists to end.
|
||||
let has: Int = realizer_has("echolocation")
|
||||
assert has < 1, "unregistered modality has no organ"
|
||||
let g: Geometry = transduce("anything", "echolocation")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live < 1, "no realizer means no geometry, not fake geometry"
|
||||
}
|
||||
|
||||
test "registration-of-an-unresolvable-name-fails-loudly" {
|
||||
// Reported at the moment of WIRING, not later as "this modality mysteriously
|
||||
// produces nothing". Distinguishing "no organ" from "broken organ" is the
|
||||
// lesson that made this whole change necessary.
|
||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||
assert bad < 1, "an unresolvable realizer name is a registration failure"
|
||||
let has: Int = realizer_has("ghost")
|
||||
assert has < 1, "and nothing gets registered"
|
||||
}
|
||||
|
||||
test "a-realizer-returning-non-geometry-transduces-nothing" {
|
||||
let reg: Int = realizer_register("bogus", "bogus_realizer")
|
||||
assert reg > 0, "the symbol resolves, so registration succeeds"
|
||||
// ...but the contract is enforced at the boundary, so the caller never
|
||||
// receives a value that would misbehave far away from here.
|
||||
let g: Geometry = transduce("x", "bogus")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live < 1, "a non-Geometry return transduced nothing"
|
||||
}
|
||||
|
||||
test "norm-lets-a-caller-check-a-realizer-emitted-signal" {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let z: Float = geometry_norm(g)
|
||||
assert z < 0.001, "a fresh geometry is zero — norm says so"
|
||||
let s0: Int = geometry_set(g, 0, 3.0)
|
||||
let s1: Int = geometry_set(g, 1, 4.0)
|
||||
let n: Float = geometry_norm(g)
|
||||
let dn: Float = n - 5.0
|
||||
assert dn < 0.001, "3-4-5: norm is 5"
|
||||
assert dn > -0.001, "3-4-5: norm is 5"
|
||||
let nrm: Float = geometry_norm(g)
|
||||
let dnorm: Float = nrm - 5.0
|
||||
assert dnorm < 0.001, "3-4-5: norm is 5"
|
||||
assert dnorm > -0.001, "3-4-5: norm is 5"
|
||||
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,43 +45,17 @@ returned 60k–230k-char unbounded traversals (this very session hit 104 KB and
|
||||
|
||||
## Layer 2 — primitive agentic tools (Neuron runs itself)
|
||||
|
||||
The base verbs all agentic behavior composes from.
|
||||
|
||||
> **⚠ 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`.
|
||||
The base verbs all agentic behavior composes from — grounded in the LIVE
|
||||
cog-arch (`think` is the one operation; faculties are its steering-space labels;
|
||||
the correspondence-beat is the reflexive learning loop).
|
||||
|
||||
| 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~~ **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 |
|
||||
| `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) |
|
||||
| `attend` | `attend({node, observer, salience})` | `engram_attend_json` | **PROVEN** (returns `salient-to`) |
|
||||
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PARTIAL.** `may_assert` is real. `"still_held"` is a **hardcoded literal `true`** — `el_runtime.c:14538` emits it unconditionally, so it reports nothing it measured. Violates the invariant *a returned value must be derivable from what produced it* |
|
||||
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | ~~PROVEN (grounded-by edge, grounding=0.912, written)~~ **RETRACTED.** That 0.912 was structural, not evidential: the call wrote the edge between the two *region hubs* and echoed them back as though they were the caller's input, so when both seeds resolved into one region it **grounded a node against itself and returned a confident score**. Measured: grounding `3b9ced5d` against `6edf8c79` scored **0.98883** purely because `6edf8c79` is the hub of `3b9ced5d`'s region; two independent agents reported 0.885 / 0.909 self-groundings as confident. **#147** grounds the node asked about, reports `claim_region`/`evidence_region` separately, and refuses three circular shapes. **The operation itself is still the wrong shape** — see below |
|
||||
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN, and it was writing into a void.** The Stance, brier and reliability were real and really persisted — but `think` built a *neutral* stance every call and never loaded them, so every beat's calibration was written and thrown away on the next read. Fixed in **#146**: `think` resumes `stance-<faculty>-<hub>`, the same id the beat writes. Confidence **0.5 → 0.930726** on a calibrated region |
|
||||
|
||||
### What this table gets structurally wrong
|
||||
|
||||
- **`faculty` is not a parameter.** `reason` changes the *estimate* (a read),
|
||||
`induce` changes the *parameters* (this is exactly what `learn` does), and
|
||||
`abduce` changes the *structure* — a **write**, which `GeoGradient` cannot
|
||||
express. A write cannot be a parameter of a read. That the eight were listed as
|
||||
interchangeable values of one argument is why all eight returning the same thing
|
||||
looked like a pass. Underneath, `engram/src/server.el: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.
|
||||
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PROVEN** |
|
||||
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | **PROVEN** (grounded-by edge, grounding=0.912, written) |
|
||||
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN** (real Stance: `stance-induce-…`, brier, reliability, written) |
|
||||
|
||||
`comprehend`/`realize`/`intend` are **compositions**, not separate live
|
||||
primitives: comprehend = write+activate (world→geometry), realize = assert
|
||||
@@ -95,26 +69,6 @@ execution→integrate) composes over `think`+`ground`+`learn`+`write`/`relate`.
|
||||
`kn-efeb4a5b…` / `kn-5b606390…`, are refused — identity routes through
|
||||
intentional-cultivation, as enforced today.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16).** This describes what the surface enforces, which
|
||||
> is accurate — but the enforcement is the wrong kind of thing:
|
||||
>
|
||||
> > **In an immutable substrate, any mechanism that refuses a write is either
|
||||
> > redundant with immutability, or an epistemic constraint misfiled as a
|
||||
> > protective one.**
|
||||
>
|
||||
> "Keystone" means **load-bearing**, not precious. The real requirement is
|
||||
> **non-circularity of the reference frame** — a reference fitted to its own
|
||||
> readings reports perfect correspondence forever while drift becomes undetectable
|
||||
> from inside — and that is satisfied *temporally*, not by a gate: the frame
|
||||
> updates while activation is internally seeded, not while it is being used to act.
|
||||
> **Independence is *when*, not *what*.** Corruption requires mutation, and the
|
||||
> engram does not mutate: recoverability (the predecessor is always present),
|
||||
> governance (supersession *is* the audit trail), evidence quality, and rate all
|
||||
> fall out of the substrate. **Authorization** is the only residue and it is
|
||||
> bounded — an unauthorized writer can *propose*, never erase. Note also that the
|
||||
> live check is a substring match against two hard-coded ids
|
||||
> (`el_runtime.c:14337`).
|
||||
|
||||
## How the caller invokes Neuron agentically
|
||||
|
||||
Once the ops are registered as MCP tools (aliases in `surface.el`), the caller
|
||||
@@ -140,16 +94,6 @@ running itself.
|
||||
## Honest ledger (built vs staged)
|
||||
- **Route seam — IMPLEMENTED + PROVEN:** ported the `@route` codegen (from `feat/el-route-decorators`) into the worktree, rebuilt `elc` self-host, proved decorate→serve (`route_proof.el` on :8951); `surface.el` compiles with `el_route_dispatch` generated for all 8 ops.
|
||||
- **All ops PROVEN live on the clone** (gate-1 boot recipe, node-id anchors): read, write, relate, supersede (immutable), tombstone, think (8 faculties), ground, attend, learn — daemon alive through all mutations (node_count 13173→13176).
|
||||
> **⚠ Retracted in part (2026-08-16).** "The daemon stayed alive and every route
|
||||
> returned a well-formed response" is what was actually proven, and that is a
|
||||
> weaker claim than it reads as. See the Layer-2 table: `think` was reading at the
|
||||
> zero-gradient point, `ground` was scoring nodes against themselves, `assert`
|
||||
> emits a hardcoded field, and `learn` was persisting into a void. **A build that
|
||||
> passes because nothing checks whether a returned value is derivable from what
|
||||
> produced it has not been tested — it has been observed not to crash.** The
|
||||
> related discipline gap, also 2026-08-16: **no test without a negative control**
|
||||
> (#148's first attempt passed on the unpatched build too), and **no deploy
|
||||
> without verifying the artifact carries the fix** (nine instances in one session).
|
||||
- **Aperture-boundedness PROVEN:** vantage-read `limit=3 → 15 KB` vs `limit=50 → 363 KB` (fixes the whole-self dump).
|
||||
- **Bus:** `@manager` ops emit on the real `dharma_*` bus (explicit today, compiles) — same transport as the swarm (`wt/swarm-ccr`).
|
||||
- **STAGED (not guessed — needs the cognition-engram rebuild to verify link):** auto-injecting telemetry/interoception + bus emission at the decorated boundary (`cg_fn` diff in `SEAM_STAGED.md`); building the cognition engram with `surface.el` compiled in. No promote to live, no cutover (per rails).
|
||||
|
||||
Reference in New Issue
Block a user