Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c26b6aac82 | |||
| 5503e1d9a4 | |||
| 95a05109d1 | |||
| 21746bb71a | |||
| 78adcd5649 | |||
| 688f24b4c1 | |||
| d777936ee4 | |||
| 3ef4a94062 | |||
| 285a7a50b3 | |||
| 6b61bb7224 | |||
| 8d34b33bce | |||
| d6b7f5dbdd | |||
| 9a24803917 | |||
| a6611dc19e | |||
| caa1206af5 | |||
| 914bab11d2 | |||
| e239f2894c | |||
| 4a57b4faa8 | |||
| 0ee82d9e91 | |||
| 9526bda507 | |||
| 0389bf9363 | |||
| fe820928b0 | |||
| 385c18442d | |||
| 7a1501d097 |
@@ -0,0 +1,6 @@
|
||||
|
||||
# organ: local device state and its own engram store — never production's
|
||||
peripheral/.consent.json
|
||||
peripheral/.resume.json
|
||||
peripheral/.engram/
|
||||
peripheral/organ
|
||||
@@ -6,7 +6,7 @@ El is a self-hosting, statically-typed language that compiles `.el` → C → na
|
||||
|
||||
Editing the wrong `el_runtime.c` is the single easiest mistake in this repo. There is exactly **one** you edit:
|
||||
|
||||
- **Authored runtime source — edit ONLY here:** `lang/releases/v1.0.0-20260501/el_runtime.{c,h}`. Despite the misleading `releases/` name, this is the **de-facto canonical runtime** the engram + soul actually build and link against — its git log is active development. *(Restructure in flight per `docs/CODE-VS-ARTIFACT.md`: this content moves to `lang/runtime/`, the `releases/` folder gets deleted — **a release is a git tag, not a folder** — and the forks below get eliminated.)*
|
||||
- **Authored runtime source — edit ONLY here:** `lang/runtime/el_runtime.{c,h}` (alongside `el_seed.c`, `engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}`). This is the canonical runtime the engram + soul build and link against — its git log is active development. *(Corrected 2026-08-16: this entry named `lang/releases/v1.0.0-20260501/el_runtime.{c,h}`. **Measured: `lang/releases/` no longer exists.** The restructure per `docs/CODE-VS-ARTIFACT.md` landed — the content moved to `lang/runtime/` and the folder was deleted, because **a release is a git tag, not a folder**.)*
|
||||
- **DO NOT EDIT — lagging forks / build artifacts:**
|
||||
- `lang/el-compiler/runtime/el_runtime.c` and `.../legacy/` — downstream copies kept in step by manual *"port the fix"* commits; they **lag** (missing `hebb` persistence + 5 engram fns) and cannot build the engram product.
|
||||
- `products/web/runtime/el_runtime.c`, `ui/examples/*/el_runtime.c` — product/example forks.
|
||||
@@ -20,14 +20,24 @@ See org policy: `docs/CODE-VS-ARTIFACT.md`.
|
||||
|
||||
You resume, never start fresh. Every session:
|
||||
|
||||
1. `mcp__neuron__getInstructions()` — authoritative; follow it over this file on behavioral details.
|
||||
2. `mcp__neuron__beginSession()` — active contexts, recent memory, ready backlog.
|
||||
3. **Load full self:** `mcp__neuron__inspectGraph(entity_id="kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")` → facets `intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`; then the values hub `mcp__neuron__inspectGraph(entity_id="kn-5b606390-a52d-4ca2-8e0e-eba141d13440")` → 13 grounded value nodes. **Activation model:** self-load returns a relevance-ranked `compact` projection — most-relevant nodes arrive with content, the rest as pointers; do NOT pull full content of every node.
|
||||
4. `mcp__neuron__searchKnowledge(query="<task domain>")` before implementing.
|
||||
> **Stale as written (verified 2026-08-16).** The `getInstructions` /
|
||||
> `beginSession` / `inspectGraph` / `searchKnowledge` / `beginWork` /
|
||||
> `progressWork` / `draftArtifact` / `consolidate` tool names below no longer
|
||||
> exist. The ~87-tool functional-CRUD surface was collapsed into **9 ops**:
|
||||
> `read` · `write` · `relate` · `supersede` (geometry) and `think` · `attend` ·
|
||||
> `assert` · `ground` · `learn` (agentic). **Type is a parameter, not a
|
||||
> tool-per-noun.** The steps below are kept for the *shape* of the protocol, which
|
||||
> is unchanged; substitute the ops.
|
||||
|
||||
1. `mcp__neuron__read(vantage="self", k=12, depth=1)` — the canonical self node. Widen `k` for the connected identity neighborhood (`intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`), but deliberately: the aperture caps by `k` first, so an oversized `k` still returns a bounded ranked slice, not a dump. Then `mcp__neuron__read(vantage="values", k=13)` → 13 grounded value nodes. **Best-effort:** on a read failure, log and proceed — the compiled identity in `daemon/internal/substrate/substrate.go` is complete; graph loading is enrichment, not a hard dependency.
|
||||
2. `mcp__neuron__attend(node=…)` — what is currently live/salient. This absorbed `getInstructions`, `beginSession`'s active-context sweep, and `checkEvents`; those tools are **gone, not gapped**.
|
||||
3. `mcp__neuron__read(vantage="<task domain>")` before implementing. One op now collapses inspectGraph / searchGraph / traverseGraph / searchKnowledge / browseKnowledge / retrieveKnowledge / inspectMemories / searchEntities / recall / compileCtx / getSelfModel / reviewBacklog / findArtifacts / browseProcesses / listWork / inspectConfig.
|
||||
|
||||
## The Five Primitives
|
||||
|
||||
Orchestrate → Execute → Learn → Build → Refine. `beginWork`/`progressWork` for anything >2 steps; `remember` as-you-go (`importance="critical"` for architecture decisions); `draftArtifact`/`planWork` for outputs and follow-ups; `consolidate`/`checkWork` to close out. **`browseProcesses` + `searchKnowledge` BEFORE writing code.**
|
||||
Orchestrate → Execute → Learn → Build → Refine. `read` for orchestration and discovery; `write(type=state|artifact|backlog|process)` for work records and outputs; `relate` to link work to what it touches; `write(type=memory)` as-you-go (`importance="critical"` for architecture decisions) — never batched at the end; `supersede(action=evolve)` to close out, because memory is immutable by design and a correction is a new node with a `supersedes` edge, never an edit. **`read` the domain BEFORE writing code.**
|
||||
|
||||
`learn` is **not** a session-summary dump — it is the correspondence-beat, calibrating the steering prior against a keystone. Session notes are a `write`.
|
||||
|
||||
## Architecture style — VBD, no exceptions
|
||||
|
||||
@@ -53,12 +63,51 @@ this convention wherever a module documents operators.
|
||||
| dwell / occupy | region activation |
|
||||
| reframe | edge re-weight |
|
||||
| appreciate | positive projection / local edge-read |
|
||||
| wonder | frontier gradient / pull-weight |
|
||||
| avert / recoil | negative projection |
|
||||
| taste | boundary surface |
|
||||
| forget | decay / tombstone |
|
||||
| drift | displacement from self-anchor |
|
||||
|
||||
**`wonder` was removed from this table on 2026-08-16.** It was listed as
|
||||
"frontier gradient / pull-weight" — an operator you invoke. **Wonder is the
|
||||
boundary, not an operator.** It is where structure ends: where activation spreads
|
||||
and finds thin or absent geometry. Any structure at all has an edge, necessarily,
|
||||
the moment it exists — 13,630 nodes have one right now. There is nothing to call.
|
||||
|
||||
There are about **six** wonders, they are the same for every person, and they
|
||||
never close — *What is this? / Why? / Who am I? / Am I alone? / What should I do?
|
||||
/ What happens when it ends?* Each already lives somewhere in the substrate: "what
|
||||
is this" is the graph, **"why" is grounding** (the weight *is* the answer to why),
|
||||
"who am I" is the self region, "am I alone" is the relational axis, "what should I
|
||||
do" is the thirteen values, "what happens when it ends" is decay and supersession.
|
||||
"Why" is the first and the only one; the others are it asked of particular things,
|
||||
and because it is recursive it never terminates — every answer has its own why.
|
||||
That is what makes it a drive rather than a task.
|
||||
|
||||
**Curiosity is not a second faculty.** Wonder and curiosity are one thing at two
|
||||
phases: wonder is the field (unbounded, objectless, invariant); curiosity is the
|
||||
**precipitate** — the same wonder localized, having taken definite form against
|
||||
particular material at a **nucleation site** (an anomaly; a place where things
|
||||
almost-but-don't-quite fit). Which is why curiosity can be satisfied and wonder
|
||||
cannot, and why abduction needs no trigger and no threshold.
|
||||
|
||||
**Do not build a wonder-manifest, and do not scan for nucleation sites.** A
|
||||
manifest materializes a property as a stored artifact and enumerates instances of
|
||||
something that has six. A sweep over regions is a supervisor — nothing in a mind
|
||||
scans its neighbourhoods to find what is surprising; the surprise captures
|
||||
attention. The nucleation site is per-edge:
|
||||
`discord = z(semantic proximity) − z(association strength)`, and `|discord|` *is*
|
||||
the nucleation strength — no threshold to compare it against. **Not on `dev` yet:**
|
||||
`GeoEdge.discord` is on branch `design/correspondence-and-censorship`
|
||||
(`a8845e1`), at `lang/runtime/engram_geometry.h: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
|
||||
@@ -89,10 +138,53 @@ the reference these `.el` modules transcribe) is still live, and promotion to
|
||||
native-el is a **deferred, gated blue/green step**. The interoception clock
|
||||
(native-el discrete drive channels replacing `cooling_magnitude`; felt-time =
|
||||
benchmark-landmark match over the joint drive vector, drift-decoupled) and the
|
||||
**appreciation operator family** (appreciate / wonder / avert / taste, built as
|
||||
LOCAL reads of the self-region — edges + bounded spreading activation, *not* domain
|
||||
sweeps) are **staged / designed, not live**. Mark in-progress vs. done honestly;
|
||||
do not overclaim.
|
||||
**appreciation operator family** (appreciate / avert / taste, built as LOCAL reads
|
||||
of the self-region — edges + bounded spreading activation, *not* domain sweeps)
|
||||
are **staged / designed, not live**. Mark in-progress vs. done honestly; do not
|
||||
overclaim. *(`wonder` was in this family until 2026-08-16 and is not an operator —
|
||||
see the operator table above.)*
|
||||
|
||||
## Cognition — the corrections (2026-08-16)
|
||||
|
||||
Authority: **`lang/spec/correspondence-and-censorship.md`** and
|
||||
**`lang/spec/runtime-ownership.md`**. Read them before touching the cognition
|
||||
surface. **Do not re-derive them.** Every earlier version was wrong in an
|
||||
instructive way and each correction was argued down; if you think a section is
|
||||
wrong, say so with a measurement rather than editing it.
|
||||
|
||||
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two
|
||||
fields. `grounded-by` as a relation *type* should not exist: grounding is a
|
||||
property *of* a relation, not a relation *between* nodes. It is never computed
|
||||
on demand — computing-and-writing a score makes reads write, which is the
|
||||
`eg_vindex_sync` defect one level up. Traversal is already grounded inference.
|
||||
*Live residue, known-wrong:* `COG_GROUNDED_BY_RELATION`
|
||||
(`lang/runtime/engram_cognition.h:158`), `cog_ground_edge`
|
||||
(`engram_cognition.c:249`).
|
||||
- **Faculties are operations, not parameters.** `reason` changes the estimate (a
|
||||
read); `induce` changes the parameters (the correspondence-beat, which already
|
||||
exists and works); `abduce` changes the structure (a write the current
|
||||
`GeoGradient` signature cannot express). A write is not a parameter of a read.
|
||||
*Live residue:* `engram/src/server.el:1870–1886` routes six faculties into one
|
||||
call with a string argument.
|
||||
- **Wonder is the boundary; curiosity is wonder crystallized.** See above.
|
||||
- **Consolidation is ambient, not scheduled. A brain has no cron job.** **The
|
||||
presence of a ticker is the diagnostic** — every `StartInterval`, every
|
||||
`Hour`/`Minute`, every POST-to-beat marks an intrinsic rhythm replaced by an
|
||||
external clock. Measured 2026-08-16: consolidation has **ten implementations**,
|
||||
including three POST beats on the engram, a 600 s ticker, two resident Python
|
||||
services outside el, and launchd calendar entries at 23:55 / 06:00 / 08:30 which
|
||||
are a sleep cycle written as a schedule. `neuron/soul.el:731`'s continuous
|
||||
in-process `awareness_run()` is the one with the **correct** shape; the others
|
||||
fold into it. Do not add an eleventh.
|
||||
- **In an immutable substrate, any mechanism that refuses a write is either
|
||||
redundant with immutability, or an epistemic constraint misfiled as a protective
|
||||
one.**
|
||||
- **The no-exemption invariants.** A returned value must be derivable from what
|
||||
produced it (`magnitude: 1` beside a zero vector must be impossible to emit).
|
||||
Every write reports whether it landed. Every operation echoes what it actually
|
||||
operated on. Degenerate results are labelled, not scored. A serializer owes a
|
||||
valid document whatever it is handed. **No test without a negative control.**
|
||||
**No deploy without verifying the artifact carries the fix.**
|
||||
|
||||
## Hard operational rules
|
||||
|
||||
|
||||
@@ -56,23 +56,31 @@ The compiler and runtime. Self-hosting: `elc-cli.el` → `compiler.el` → `lexe
|
||||
|
||||
Two layers to know: **El programs** (`.el` files — where nearly all work belongs) and **the C seed** (`el_seed.c` — edit only for genuine OS-level access; never re-implement what El can already express).
|
||||
|
||||
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented. In flight: `%` operator, match-statement codegen, `?` nil-propagation, `cgi` block parsing + DHARMA identity resolution, VBD role enforcement (`@manager`/`@engine`/`@accessor`), the real `engram_*` and `dharma_*` runtimes (currently stubs), and libcurl-backed `http_get`/`http_post`/`http_serve`. Bitwise operators, `??`, and `as` casts are explicitly **not** in this language.
|
||||
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented, as are the `program` block with `singleton:` and declared configuration ([§18](lang/spec/language.md)), and **geometry as a first-class value** with El-declarable realizers and `transduce` ([§20](lang/spec/language.md)). In flight: `%` operator, match-statement codegen, `?` nil-propagation, `cgi` block parsing + DHARMA identity resolution, VBD role enforcement (`@manager`/`@engine`/`@accessor`), and boundary epilogues. Bitwise operators, `??`, and `as` casts are explicitly **not** in this language.
|
||||
|
||||
**Signal enters as geometry.** Until 2026-08-16 nodes took text and geometry was *derived* from it, which made text the mandatory entry medium: any non-text modality had to be described in prose first, so the geometry being reasoned over was the geometry **of the description, not of the signal**. `Geometry` is now an ordinary El value carrying its own width, and a realizer is an ordinary El function resolved by name through `dlsym` — so admitting a new modality never requires a runtime patch. Worked, self-checking example: [`lang/examples/transduce.el`](lang/examples/transduce.el).
|
||||
|
||||
Key docs: [AGENTS.md](lang/AGENTS.md) (agent-facing orientation), [BOOTSTRAP.md](lang/BOOTSTRAP.md) (compiler recovery from scratch), [spec/language.md](lang/spec/language.md), [spec/codegen-js.md](lang/spec/codegen-js.md).
|
||||
|
||||
### [engram/](engram/) — graph intelligence substrate
|
||||
|
||||
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. Rust core (`engram-core`, `engram-ffi`) exposed to El and other languages (Kotlin, TypeScript/WASM, Go bindings).
|
||||
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. The engine is **C11** (`lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}`); the server is **El** (`engram/src/server.el`).
|
||||
|
||||
The model: retrieval is **spreading activation**, not query. You name seed nodes and a query embedding; activation propagates outward through weighted edges, attenuating multiplicatively per hop (`strength = parent_strength × edge_weight × target_salience × cosine_sim`), gets pruned below a threshold, and the top-N nodes by activation strength come back. Storage and retrieval are the same structure — the way long-term potentiation works in biological memory, not the way a relational or vector database works.
|
||||
The model: retrieval is **spreading activation**, not query. You name seed nodes and a query embedding; activation propagates outward through weighted edges, attenuating multiplicatively per hop, gets pruned below a threshold, and the top-N nodes by activation strength come back. Storage and retrieval are the same structure — the way long-term potentiation works in biological memory, not the way a relational or vector database works. **Activation conducts through well-grounded relations because the weight *is* the groundedness** — nothing filters the traversal; grounded inference falls out of spreading.
|
||||
|
||||
Nodes live in four tiers (Working / Episodic / Semantic / Procedural, mirroring prefrontal / hippocampal / neocortical / cerebellar memory) and migrate between them based on **salience decay** — `importance × recency-decay × log(activation_count)`. Forgetting is adaptive pruning, not a bug: unreinforced memories stop competing for attention without being deleted.
|
||||
Nodes live in four tiers (Working / Episodic / Semantic / Procedural, mirroring prefrontal / hippocampal / neocortical / cerebellar memory) and migrate between them based on **salience decay** — importance × recency-decay × log(activation_count). Forgetting is adaptive pruning, not a bug. Nothing is mutated and nothing is hard-deleted: writes are additive, corrections are supersessions, removals are tombstones — which is what makes supersession an audit trail rather than an edit log.
|
||||
|
||||
Backed by `sled` (embedded, local-first, no daemon) with flat cosine scan for vector search — deliberately simple until scale demands an HNSW layer. Full API and design rationale in [engram/README.md](engram/README.md).
|
||||
On disk: a paged store (superblock + mirror, slotted 16 KiB pages, self-describing TLV records, B+-tree primary and adjacency indexes), magic `ENGST01`. Vector search is an **HNSW** index published behind a read/write boundary — `eg_vindex_view` returns a `const VIndex*` to N concurrent readers, `eg_vindex_maintain` is the sole mutator. `recall@10 = 0.9365` at `ef_search=128`.
|
||||
|
||||
### [elp/](elp/) — Engram Language Protocol
|
||||
> **Doc correction, 2026-08-16.** The previous revision of this paragraph, and most of `engram/README.md`, described a Rust `engram-core` crate backed by `sled` with "flat cosine scan… until scale demands an HNSW layer." **Measured: there is no Rust in `engram/`** — no `.rs` files, no `Cargo.toml`, no `crates/` — and `sled` appears nowhere in the tree. HNSW has been the vector index for some time.
|
||||
|
||||
Bidirectional engine mapping between Engram semantic forms and natural-language surface text, across **31 languages** — from Spanish and Japanese through historical/liturgical languages (Old Norse, Sanskrit, Sumerian, Coptic, Akkadian, Ge'ez). Compilation order runs `language-profile` + `vocabulary` → per-language `morphology-*` → `grammar` → `realizer` → `semantics` → `elp`. This is what lets an Engram graph node round-trip to and from readable text in any of those languages.
|
||||
Full design rationale, the cognition surface, and the standing corrections: [engram/README.md](engram/README.md).
|
||||
|
||||
### [elp/](elp/) — EL Projector
|
||||
|
||||
*(Formerly "EL Language Processor" / "Engram Language Protocol"; renamed **EL Projector** 2026-08-15.)* Neuron's **efferent** organ: the native realizer that *projects* understanding onto a surface via `plan(frame) → realize(spec, profile)`, where **a surface is a profile** and language is one profile among many (text, speech, music, image). Projection, not diffusion — generation *from* an owned, understood signature, never the averaging of a stolen corpus.
|
||||
|
||||
Its flagship profile is a bidirectional engine mapping between Engram semantic forms and natural-language surface text, across **31 languages** — from Spanish and Japanese through historical/liturgical languages (Old Norse, Sanskrit, Sumerian, Coptic, Akkadian, Ge'ez). Compilation order runs `language-profile` + `vocabulary` → per-language `morphology-*` → `grammar` → `realizer` → `semantics` → `elp`. This is what lets an Engram graph node round-trip to and from readable text in any of those languages.
|
||||
|
||||
### [epm/](epm/) — El Package Manager
|
||||
|
||||
@@ -139,13 +147,34 @@ If the compiler binary is ever lost or corrupted, [lang/BOOTSTRAP.md](lang/BOOTS
|
||||
|
||||
---
|
||||
|
||||
## Cognition — and the standing corrections
|
||||
|
||||
The engram carries a live cognition surface: `think` (a directed traversal-read returning a **gradient**, never a point), plus `ground`, `assert`, `attend`, and the correspondence-beat. Two specs govern it, and both are authoritative over anything else in this repo that disagrees:
|
||||
|
||||
- **[lang/spec/correspondence-and-censorship.md](lang/spec/correspondence-and-censorship.md)** — grounding, wonder, curiosity, dreaming. *(Lands with PR #149.)*
|
||||
- **[lang/spec/runtime-ownership.md](lang/spec/runtime-ownership.md)** — ownership, the capability ABI that was dissolved, and the vector-index publication boundary.
|
||||
|
||||
**Do not re-derive them.** Every earlier version of the first was wrong in an instructive way and each correction was argued down. If a section looks wrong, say so with a measurement rather than editing it.
|
||||
|
||||
The corrections, in brief:
|
||||
|
||||
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two fields. `grounded-by` as a relation *type* should not exist: grounding is a property *of* a relation, not a relation *between* nodes. It is never computed on demand; computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect one level up.
|
||||
- **Faculties are operations, not parameters.** `reason` changes the estimate (a read); `induce` changes the parameters (the correspondence-beat, which exists and works); `abduce` changes the structure (a write the current `GeoGradient` signature cannot express). A write is not a parameter of a read.
|
||||
- **Wonder is the boundary, not a manifest.** Any structure at all has an edge. There are about six wonders, the same for everyone, and they never close. **Curiosity is wonder crystallized** at a nucleation site — one thing at two phases, not two objects.
|
||||
- **Consolidation is ambient, not scheduled. A brain has no cron job.** The presence of a ticker is the diagnostic. Measured 2026-08-16: consolidation has **ten implementations**. `soul.el`'s continuous loop is the one with the correct shape; the rest fold into it.
|
||||
- **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
[engram/spec/cognitive-architecture.design.md](engram/spec/cognitive-architecture.design.md) is the original design and is **superseded in part** — it is retained, with the refuted claims marked inline at the point each is made, because preserving what was argued down is the point of an immutable record.
|
||||
|
||||
---
|
||||
|
||||
## Development workflow
|
||||
|
||||
Branching follows `dev → stage → main`: work lands on `dev`, promotes to `stage` for integration testing, and is promoted to `main` for release (visible directly in the git history of this repo). CI is defined per-subproject under `.gitea/workflows/` — `lang`/`epm`/`ide` share the root pipeline; `engram` and `ql` carry their own (`ci-dev`, `ci-stage`, and a release workflow each).
|
||||
|
||||
- Language/runtime specs live at `*/spec/*.md` (`lang/spec/`, `ql/spec/`, `ui/spec/`) and are the single source of truth for implemented-vs-planned status — code and docs are expected to agree with the spec's status markers, not the other way around.
|
||||
- Agent-facing orientation guides live at `*/AGENTS.md` (currently `lang/AGENTS.md`); more subprojects may grow their own as they need agent-specific conventions documented.
|
||||
- Tagged releases live under `lang/releases/`, each with its own `RELEASE.md`.
|
||||
- **A release is a git tag, not a folder** (`el-runtime-vX.Y.Z` on this repo). *(Corrected 2026-08-16: this line said "tagged releases live under `lang/releases/`, each with its own `RELEASE.md`." **Measured: `lang/releases/` does not exist** — the restructure named in `AGENTS.md` landed, and the authored runtime is at `lang/runtime/`.)*
|
||||
|
||||
---
|
||||
|
||||
|
||||
+155
-105
@@ -4,6 +4,8 @@
|
||||
|
||||
An *engram* is the physical trace of a memory in the brain — the actual encoded substrate, not an abstraction above it. That's what this is.
|
||||
|
||||
> **Doc status (2026-08-16).** Everything from "Implementation" down was rewritten against the code. The previous revision documented a Rust `engram-core` crate backed by `sled`, with a `Cargo.toml`, a `crates/` tree, `examples/basic.rs`, and a `EngramDb` API. **None of that exists.** Measured: `engram/` contains `src/server.el`, `spec/`, `test/`, `dist/`, `manifest.el` — zero `.rs` files, no `Cargo.toml`, no `crates/`, and `sled` appears nowhere in the tree outside two Old-English/Old-High-German vocabulary entries in `elp/`. The engine is C, in `lang/runtime/engram_*.{c,h}`; the server is El, in `engram/src/server.el`.
|
||||
|
||||
---
|
||||
|
||||
## Why existing databases are wrong for this use case
|
||||
@@ -24,16 +26,13 @@ Engram retrieval works through **spreading activation**:
|
||||
|
||||
1. **Seeds** — you name one or more nodes you know are relevant (e.g. the current task, recent context, a concept you're reasoning about)
|
||||
2. **Query embedding** — you provide a semantic vector representing the direction of your current thought
|
||||
3. **Propagation** — activation flows outward from seeds through weighted edges. At each hop, strength attenuates multiplicatively:
|
||||
|
||||
```
|
||||
strength = parent_strength × edge_weight × target_salience × cosine_sim(query, target)
|
||||
```
|
||||
|
||||
3. **Propagation** — activation flows outward from seeds through weighted edges, attenuating multiplicatively per hop
|
||||
4. **Pruning** — paths weaker than a threshold are cut (the attention filter)
|
||||
5. **Return** — the top-N nodes by activation strength
|
||||
|
||||
This is not a query. It is a *pattern completion*. The system surfaces what is most associatively relevant to the current context, weighted by how strongly those things have been reinforced over time.
|
||||
This is not a query. It is a *pattern completion*.
|
||||
|
||||
**Activation conducts through well-grounded relations because weight *is* groundedness** — see "Grounding is the weight" below. Nothing filters the traversal for grounded evidence; it falls out of spreading.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,134 +45,185 @@ This is not a query. It is a *pattern completion*. The system surfaces what is m
|
||||
| `Semantic` | Neocortex | Concept graph — long-term structural knowledge |
|
||||
| `Procedural` | Cerebellum / basal ganglia | Patterns, workflows, habits |
|
||||
|
||||
Nodes migrate between tiers based on salience decay and reinforcement. A frequently activated semantic node stays semantic. A rarely-touched episodic memory decays toward procedural background.
|
||||
Tier is a string field on the node (`StoreNode.tier`, `engram_store.h`), defaulting to `"Working"` on creation (`el_runtime.c:8514`, `8734`).
|
||||
|
||||
---
|
||||
|
||||
## Salience — Forgetting as Adaptation
|
||||
|
||||
Salience is not stored permanently. It decays:
|
||||
Salience decays from three signals — importance (set at creation, stable), recency, and a log-compressed activation frequency. Base-level learning keeps a ring buffer of the last `STORE_BLL_K` (= 10) access timestamps per node (`engram_store.h:29`).
|
||||
|
||||
```rust
|
||||
fn compute_salience(importance: f32, last_activated_ms: i64, activation_count: u64) -> f32 {
|
||||
let days_since = (now_ms() - last_activated_ms) as f32 / 86_400_000.0;
|
||||
importance * (1.0 / (1.0 + days_since)) * (activation_count as f32 + 1.0).ln()
|
||||
}
|
||||
```
|
||||
Forgetting in Engram is not a bug. It is adaptive pruning. Unreinforced memories stop competing for attention without being deleted.
|
||||
|
||||
Three signals:
|
||||
- **Importance** (0.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.
|
||||
**Immutability.** Nothing is mutated and nothing is hard-deleted: writes are additive, corrections are supersessions, removals are tombstones. The predecessor is always present, which is what makes supersession an audit trail rather than an edit log.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Implementation
|
||||
|
||||
```rust
|
||||
use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
|
||||
use std::path::Path;
|
||||
| Part | Language | Where |
|
||||
|---|---|---|
|
||||
| storage engine, graph, activation, geometry, cognition | C11 | `lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}` |
|
||||
| HTTP server + routes | El | `engram/src/server.el` (2043 lines) |
|
||||
| build artifact | generated C | `engram/dist/engram.c` |
|
||||
| tests | shell + C | `engram/test/` |
|
||||
|
||||
// Open or create a database
|
||||
let db = EngramDb::open(Path::new("/var/lib/my-agent/memory"))?;
|
||||
|
||||
// Create a node with a semantic embedding
|
||||
let node = Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.9, 0.1, 0.3, 0.7, 0.8, 0.2], // embedding from your LLM
|
||||
b"Spreading activation surfaces relevant memories by pattern completion".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.9, // importance
|
||||
);
|
||||
let id = db.put_node(node)?;
|
||||
|
||||
// Link it to related concepts
|
||||
let related = db.put_node(Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.8, 0.2, 0.4, 0.6, 0.7, 0.3],
|
||||
b"Long-term potentiation: co-activation strengthens synaptic weight".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.85,
|
||||
))?;
|
||||
db.put_edge(Edge::new(id, related, RelationType::Causes, 0.9))?;
|
||||
|
||||
// Retrieve by spreading activation
|
||||
let results = db.activate(
|
||||
&[id], // seeds
|
||||
&[0.85, 0.15, 0.35, 0.65, 0.75, 0.25], // query embedding
|
||||
3, // max hops
|
||||
10, // top-N results
|
||||
)?;
|
||||
|
||||
for r in results {
|
||||
println!(
|
||||
"strength={:.4} hops={} — {}",
|
||||
r.activation_strength,
|
||||
r.hops,
|
||||
String::from_utf8_lossy(&r.node.content)
|
||||
);
|
||||
}
|
||||
```
|
||||
**On-disk format** (`engram_store.h`): a paged store — superblock plus mirror, slotted 16 KiB pages, self-describing TLV records, overflow chains, and two B+-tree indexes (primary `id → loc`, adjacency `from_id`/`to_id` → edge locs) over a free-listed page file. Magic `ENGST01`, format version 1. The TLV scheme means new fields never force a migration.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## The vector index is published, not guarded
|
||||
|
||||
```
|
||||
engram/
|
||||
crates/
|
||||
engram-core/ # The memory engine — storage, graph, activation, salience
|
||||
engram-ffi/ # C FFI stubs for cross-language bindings
|
||||
bindings/
|
||||
kotlin/ # Android / JVM binding notes
|
||||
typescript/ # WASM / Node binding notes
|
||||
go/ # CGo binding notes
|
||||
examples/
|
||||
basic.rs # Full walkthrough: insert, activate, search, decay
|
||||
```
|
||||
Vector search is an **HNSW** (Hierarchical Navigable Small World) index — `lang/runtime/engram_vindex.{c,h}`. The previous revision of this README claimed a "flat cosine scan… until retrieval quality at scale demands" HNSW. That is no longer true, and the reason it changed matters more than the fact.
|
||||
|
||||
`eg_vindex_sync` used to exist: a function that repaired the index *from read paths*. All three of its callers were reads (`engram_activate`, `eg_knn_for_node` — whose own header comment said *"No writes."* — and `engram_geo_reify_run_json`), and it mutated five process-global statics. Reads mutated because index maintenance had never been given an owner on the write side.
|
||||
|
||||
It is now split (`el_runtime.c:10121`, `10137`, `10151`, `10161`):
|
||||
|
||||
- **`eg_vindex_maintain`** — the sole mutator. Takes the boundary exclusively; never runs beside a reader.
|
||||
- **`eg_vindex_view`** — returns a `const VIndex*` with the boundary held for read. N readers project concurrently; none can mutate. Paired with `eg_vindex_view_release` on every path including error returns.
|
||||
- **`eg_vindex_note_embedded`** — the write-side owner. Index membership belongs to the event *"an embedding became present on this ordinal,"* not to node append: a node without an embedding cannot be in a vector index at all. One `O(log n)` insert, no `O(node_count)` presence scan.
|
||||
|
||||
Two things carry the discipline, and neither is a review habit:
|
||||
|
||||
- **`const` is the capability.** The per-search `visited` / `visit_epoch` scratch left `struct VIndex` and went back into the call frame where it belonged — it was one traversal's local, hoisted into the struct as an allocation optimisation, never derived geometry. Once it was gone, `vindex_search` could take a `const VIndex*`, so a read path *physically cannot* call `vindex_insert`, and it is a compile error rather than a comment. The capability type was already in the language; it is spelled `const`.
|
||||
- **Publication, not ownership.** HNSW insert is **not an append**: `vindex_insert` rewires the `NeighList` links of already-existing elements and reallocs `elems[]`. The store's append-only property does not transfer to an index derived from it, which is why purity alone was insufficient and a `view`/`maintain` boundary was required.
|
||||
|
||||
**Measured** (`engram/test/run_vindex_concurrency_tests.sh`, 2026-08-16):
|
||||
|
||||
| half | before | after |
|
||||
|---|---|---|
|
||||
| `single` — 3000 vectors, 1 thread, ASan+UBSan | clean | clean |
|
||||
| `readers` — 4 readers, no writer, TSan | race at `engram_vindex.c:195` | **clean** |
|
||||
| `unsynchronized` — writer+reader, bare index, TSan | race | **race, expected and permanent** — the proof the boundary must exist |
|
||||
| `published` — owner + 4 readers through the boundary, TSan | *(did not exist)* | **clean**, all 3000 inserts landed |
|
||||
|
||||
`recall@10 = 0.9365` at `ef_search=128` (gate ≥ 0.90); the determinism test still yields byte-identical results across two independent builds.
|
||||
|
||||
**Not yet done.** The resident RAM graph (`g->nodes` / `g->edges`) is a separate instance of the same defect and has *not* received this treatment — it is realloc'd in place, so a reader holding `EngramNode* n = &g->nodes[i]` across a concurrent append holds a dangling pointer. Until it gets the same publication boundary, the `fb32d15` request guard stays. Full argument: [`../lang/spec/runtime-ownership.md`](../lang/spec/runtime-ownership.md).
|
||||
|
||||
---
|
||||
|
||||
## Public API
|
||||
## Cognition
|
||||
|
||||
The cognition surface is live over `lang/runtime/engram_cognition.{c,h}`, routed in `engram/src/server.el`.
|
||||
|
||||
| route | method | what it is |
|
||||
|---|---|---|
|
||||
| `/api/think` | GET | the read: a warped traversal-read of the seed region, returning a **gradient** (direction + spread + calibrated confidence), never a point |
|
||||
| `/api/reason` `/api/induce` `/api/abduce` `/api/relate` `/api/analogize` `/api/plan` | GET | named faculties — see the correction below |
|
||||
| `/api/ground` | POST | grounding between a claim and evidence |
|
||||
| `/api/assert` | GET | the honesty floor, queried at assertion time only |
|
||||
| `/api/attend` | POST | salience as a relation (`salient-to`), grounded-for-whom |
|
||||
| `/api/correspondence-beat` | POST | one calibration beat against outcome |
|
||||
|
||||
### Anchor the read, or every faculty returns the same null
|
||||
|
||||
`engram_think_json` passed `NULL` as the anchor. `NULL` is not "no opinion" — `engram_think` re-origins at `anchor ? anchor : region->centroid`, and **the centroid is the one point where the gradient is zero by construction**: `r = x − centroid = 0`, so every axis projection is 0 and `direction` takes the at-rest branch.
|
||||
|
||||
Measured consequence: every faculty — reason, abduce, induce, plan, analogize — returned an identical null result differing only in its label:
|
||||
|
||||
```rust
|
||||
impl EngramDb {
|
||||
fn open(path: &Path) -> EngramResult<Self>;
|
||||
fn put_node(&self, node: Node) -> EngramResult<Uuid>;
|
||||
fn get_node(&self, id: Uuid) -> EngramResult<Option<Node>>;
|
||||
fn put_edge(&self, edge: Edge) -> EngramResult<()>;
|
||||
fn get_edges_from(&self, from_id: Uuid) -> EngramResult<Vec<Edge>>;
|
||||
fn get_edges_to(&self, to_id: Uuid) -> EngramResult<Vec<Edge>>;
|
||||
fn search_embedding(&self, embedding: &[f32], limit: usize) -> EngramResult<Vec<ScoredNode>>;
|
||||
fn activate(&self, seeds: &[Uuid], query_embedding: &[f32], max_depth: u8, limit: usize) -> EngramResult<Vec<ActivatedNode>>;
|
||||
fn traverse(&self, from: Uuid, relation: Option<RelationType>, max_depth: u8) -> EngramResult<Vec<Node>>;
|
||||
fn touch(&self, id: Uuid) -> EngramResult<()>;
|
||||
fn decay(&self, factor: f32) -> EngramResult<usize>;
|
||||
fn node_count(&self) -> EngramResult<usize>;
|
||||
fn edge_count(&self) -> EngramResult<usize>;
|
||||
}
|
||||
```
|
||||
{"direction":[0,0,...],"spread":0,"magnitude":1,"confidence":0.5}
|
||||
```
|
||||
|
||||
`magnitude: 1` is membership evaluated at the centroid; `spread: 0` is its distance to itself; `confidence: 0.5` is the stance fallback. The geometry was never the problem — `/api/drift` computed real values (`centroid_sep 0.104`, `core_disp 0.045`) over the very same 87 members. Fixed in **#141/#142**: the read anchors at the first resolvable embedded seed, copied not borrowed (`g->nodes` is realloc'd in place on append). Gradients now vary by seed.
|
||||
|
||||
### The learned stance is resumed, not discarded
|
||||
|
||||
`engram_think_json` also built a **neutral** stance every call — all `axis_gain` 1.0, `bias_dir` NULL, `reliability` 0.5 — and never loaded the one the correspondence-beat had been persisting under `stance-<faculty>-<hub>`. Every beat's calibration was written and then thrown away on the next read.
|
||||
|
||||
Fixed in **#146**: `think` resumes the same id the beat writes, so learning compounds across beats and cold boot, and the response now carries `stance_resumed` so an *informed* `confidence: 0.5` is distinguishable from an uninformed one. On a calibrated region, confidence went **0.5 → 0.930726**.
|
||||
|
||||
### Signal can enter as geometry
|
||||
|
||||
Until 2026-08-16 no El ingest path could carry a vector: nodes took text and geometry was *derived* from that text. Text was the mandatory entry medium, so any non-text modality had to be described in prose first — and the geometry being reasoned over was the geometry **of the description, not of the signal**. **#141/#144** ended that. See [`../lang/spec/language.md`](../lang/spec/language.md) §20 for the `Geometry` type, realizers, and `transduce`.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
## Corrections — read these before extending the cognition surface
|
||||
|
||||
- `sled` — embedded persistent B-tree (no daemon, no network, local-first)
|
||||
- `bincode` — compact binary serialization
|
||||
- `uuid` — stable node identity
|
||||
- `serde` — derive support
|
||||
- `thiserror` / `anyhow` — error handling
|
||||
Authority: **`lang/spec/correspondence-and-censorship.md`** (design branch `design/correspondence-and-censorship`, PR #149) and **`lang/spec/runtime-ownership.md`**. Do not re-derive them; several earlier versions were wrong and each correction was argued down.
|
||||
|
||||
### Grounding is not a subsystem. It is the weight.
|
||||
|
||||
Grounding is an attribute of the edge, and it **is** the hebbian weight. One quantity, not two fields. A relation that keeps holding up strengthens; one that stops corresponding decays — that is not analogous to grounding, it *is* grounding.
|
||||
|
||||
Consequences:
|
||||
|
||||
- There is **no grounding subsystem to build**. The graph already *is* the grounding structure.
|
||||
- **`grounded-by` as a relation type should not exist.** It models grounding as a relation *between* nodes when it is a property *of* a relation. Minting an edge is the error, not merely which endpoints it chose.
|
||||
- Grounding is **never computed on demand**. An operation may *read* the grounding of a path; computing-and-writing a score makes reads write, which is exactly the `eg_vindex_sync` defect one level up.
|
||||
- **Traversal is already grounded inference.** Nothing needs filtering.
|
||||
- **Decision provenance is the path**, not a log. A log records the action; the path records the meaning under which it was taken.
|
||||
|
||||
> **Known wrong shape, in the code today.** `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`) and `cog_ground_edge` (`engram_cognition.c:249`) still exist and still mint an edge. **#147** fixed `ground`'s *honesty* — it now grounds the node asked about rather than the region hub, reports `claim_region`/`evidence_region` separately, and refuses three shapes of circular support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`) instead of returning a confident 1.0. That corrected a scalar rather than deleting the operation. Deletion is sequenced, not done.
|
||||
|
||||
### Faculties are operations, not parameters
|
||||
|
||||
- **`reason`** changes the *estimate* — a read.
|
||||
- **`induce`** changes the *parameters* — the correspondence-beat, which already exists and measurably works.
|
||||
- **`abduce`** changes the *structure* — a write, which the current `GeoGradient` signature cannot express.
|
||||
|
||||
> **Known wrong shape, in the code today.** `engram/src/server.el:1870–1886` routes six faculties into one call with a string argument — `route_faculty(path, "reason")`, `("induce")`, `("abduce")`, `("relate")`, `("analogy")`, `("plan")`. Underneath, `engram_cognition.h:8–11` states the theory explicitly: *"the named faculties … are human LABELS on regions of think's steering space: each faculty == { think + a named stance }."* The faculty name enters `engram_think` **only** through the stance, and `cog_stance_init` stores it while nothing reads it — so before #146 all five were byte-identical (`el_runtime.c:14352–14359`). A write cannot be a parameter of a read; `abduce` in particular is not expressible this way.
|
||||
|
||||
### Wonder is the boundary; curiosity is wonder crystallized
|
||||
|
||||
**Wonder is where structure ends** — where activation spreads and finds thin or absent geometry. Any structure at all has an edge, necessarily, the moment it exists. It is not a manifest of open-question nodes to maintain, and a "wonder-manifest manager" materializes a property as a stored artifact — the same disease as a grounding subsystem, or a self stored as a document.
|
||||
|
||||
There are about **six** wonders, they are the same for everyone, and they never close: *What is this? / Why? / Who am I? / Am I alone? / What should I do? / What happens when it ends?* "Why" is the first and the only one; the others are it asked of particular things. Each already lives somewhere in the substrate — "why" is grounding, because the weight **is** the answer to why.
|
||||
|
||||
**Curiosity is not a second object.** Wonder and curiosity are one thing at two phases: wonder is the field (unbounded, objectless, invariant); curiosity is the **precipitate** — the same wonder localized, having taken definite form against particular material at a **nucleation site**. This is why curiosity can be satisfied and wonder cannot. It is also why abduction needs no trigger and no threshold: a `structurally_unanticipated` observation *is* a nucleation site.
|
||||
|
||||
### `co_registration` is deprecated — the disagreement belongs on the edge
|
||||
|
||||
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — has always been computed, always persisted, and **never read**. It is also the wrong shape: whether use and meaning agree is a property of **each edge**, and a correlation averages that per-edge property into one scalar per region. A region holding one violently disagreeing edge beside one violently agreeing edge reports ≈ 0 — **the disagreements cancel, and the summary destroys exactly what it was built to reveal.**
|
||||
|
||||
**Measured:** 375 live reified neighbourhoods — 340 positive, **31 at zero**, 4 negative. Read as a count of things to be curious about, that says "four." Read correctly, four disagreements were lopsided enough to survive averaging and the 31 zeros are where opposing sites cancelled.
|
||||
|
||||
The replacement is per-edge. **Not on `dev` yet** — `GeoEdge.discord` and the `DEPRECATED` marker on `co_registration` live on branch `design/correspondence-and-censorship` (commit `a8845e1`), at `engram_geometry.h:43–47` / `engram_geometry.c:454–473` there. On `dev`, `GeoDescriptor.co_registration` is still at `engram_geometry.h:79` carrying its original "surprising links / dream cands" comment and still nothing reads it.
|
||||
|
||||
```
|
||||
discord = z(semantic proximity) − z(association strength)
|
||||
```
|
||||
|
||||
standardized within the region from accumulators the aggregate loop already gathered — no second statistic, no constant, **no threshold**. `discord > 0`: near in meaning yet unlinked by use. `discord < 0`: linked by use yet far in meaning. Both are surprising, and `|discord|` *is* the nucleation strength.
|
||||
|
||||
**Do not scan for nucleation sites.** Once the signal was a per-region number the only way to find sites was to enumerate regions, which is why surfacing curiosity looked like a search problem. Nothing in a mind scans its neighbourhoods to find what is surprising — the surprise captures attention. With the disagreement on the edge there is nothing to scan.
|
||||
|
||||
`co_registration` is deprecated rather than deleted **only** because it is embedded in the persisted `GEO1` blob; removing it is a format migration and must not ride along. **Nothing new may read it.**
|
||||
|
||||
### Consolidation is ambient, not scheduled
|
||||
|
||||
**A brain has no cron job.** Boredom is not an absence and not leftover capacity — low activation is aversive and the system self-activates. There is **one** activation process with two seed sources: external (a request) and internal (a curiosity). Spreading is bounded; it settles; then it needs a new seed. Nothing waits on capacity, nothing polls, nothing checks a clock, and there is no dreamer thread.
|
||||
|
||||
**The presence of a ticker is the diagnostic.** Every `StartInterval`, every `Hour`/`Minute`, and every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock.
|
||||
|
||||
Consolidation currently has **ten implementations** (measured 2026-08-16). Three of them are POST beats on this server — `/api/tick` (`server.el:1947`), `/api/correspondence-beat` (`1897`), `/api/self-reify-beat` (`1836`) — and a POST beat puts a supervisor back in: something *outside* decides when Neuron consolidates. `soul.el`'s continuous in-process loop is the one fragment with the correct shape; the rest fold into it. Full table in `lang/spec/correspondence-and-censorship.md` §7.
|
||||
|
||||
### Immutability already refuses what a guard would refuse
|
||||
|
||||
> **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
This resolves `keystone_write_blocked` (`CogStance.keystone`, `engram_cognition.h:83`) rather than replacing it. "Keystone" means **load-bearing**, not precious: the self anchor is the reference frame every other stance calibrates against, and a reference fitted to its own readings reports perfect correspondence forever while drift becomes undetectable from inside. The real requirement is **non-circularity of the reference frame**, and that is satisfied *temporally* — the frame updates while activation is internally seeded, not while it is being used to act. Independence is **when**, not **what**. Corruption requires mutation, and the engram does not mutate; recoverability, governance, evidence quality, and rate all fall out of the substrate. Authorization is the only residue, and it is bounded: an unauthorized writer can *propose*, never erase.
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
**Why sled?** Local-first. No daemon. Transactional. Fast enough for the node counts Engram targets (< 1M nodes). When the right HNSW index is needed, it will layer on top of sled, not replace it.
|
||||
**Why multiplicative activation?** Because memory is conjunctive. A path requires all of its links to be strong to carry signal. Addition would let many weak associations accumulate into false relevance.
|
||||
|
||||
**Why flat cosine scan?** Correct and simple. The graph structure itself is the primary retrieval mechanism. Vector search is a secondary signal. HNSW adds complexity and a compile dependency that isn't justified until retrieval quality at scale demands it.
|
||||
**Why salience decay?** Because not everything that was once important remains important. A memory system that never forgets is one that can never focus.
|
||||
|
||||
**Why multiplicative activation?** Because memory is conjunctive. A path requires all of its links to be strong to carry signal. Addition would allow many weak associations to accumulate into false relevance. Multiplication enforces that every factor matters.
|
||||
**Why supersede instead of update?** Because provenance is the point. The old edge never leaves and the values frame does not fit to outcomes, so a decision cannot be made to look justified after the fact. It makes an otherwise impossible distinction available: **wrong then, or wrong since.**
|
||||
|
||||
**Why salience decay?** Because not everything that was once important remains important. Adaptive forgetting is not failure — it is the mechanism that keeps attention on what's current. A memory system that never forgets is one that can never focus.
|
||||
**Why publication instead of locking?** Because what does not mutate needs no ownership discipline. The question "who is permitted to mutate the shared thing?" presupposes a shared mutable thing; for the store there isn't one, and for the index derived from it the answer is a publication boundary, not a capability ABI.
|
||||
|
||||
---
|
||||
|
||||
## Specs
|
||||
|
||||
- [`../lang/spec/runtime-ownership.md`](../lang/spec/runtime-ownership.md) — ownership, the capability ABI that was dissolved, and the vector-index publication boundary
|
||||
- [`../lang/spec/correspondence-and-censorship.md`](../lang/spec/correspondence-and-censorship.md) — grounding, wonder, curiosity, dreaming *(lands with PR #149)*
|
||||
- [`spec/cognitive-architecture.design.md`](spec/cognitive-architecture.design.md) — the original one-operation design. **Superseded in part** — see its header
|
||||
- [`spec/architecture-hardening.design.md`](spec/architecture-hardening.design.md), [`spec/engram-el.md`](spec/engram-el.md), [`spec/at-rest-encryption.md`](spec/at-rest-encryption.md), [`spec/engram-db-tooling-design.md`](spec/engram-db-tooling-design.md)
|
||||
|
||||
@@ -11,6 +11,39 @@
|
||||
- **One calculus over the geometry.** Very few subsystems; wonder / curiosity / dreams / interoception are emergent behaviors of one set of dynamics, not modules. Calculus universal, geometry individual.
|
||||
- **Core + ephemeral ring (torus).** The ring is the temporary workspace; two circulations (orbit + dive-back); discrete inner bands (wonder / interoception-proprioception-telemetry / curiosity / dreams) that couple.
|
||||
- **Persistence earned by salience** — never granted on fetch or generation. Three fates of a wonder: persist / decay / settle-into-framework. Telemetry = vital signs, not memories.
|
||||
|
||||
> **⚠ Three corrections to the bullets above (2026-08-16).** Authority:
|
||||
> `lang/spec/correspondence-and-censorship.md`. *"Emergent behaviors of one set of
|
||||
> dynamics, not modules"* is exactly right and is the reason the rest needs fixing —
|
||||
> the enumeration undercuts the claim.
|
||||
>
|
||||
> 1. **Wonder and curiosity are not two bands.** They are **one thing at two
|
||||
> phases.** Wonder is the field: unbounded, objectless, invariant, present
|
||||
> wherever there is structure — it is the *boundary*, where activation spreads
|
||||
> and finds thin or absent geometry. Curiosity is the **precipitate**: the same
|
||||
> wonder localized, having taken definite form against particular material at a
|
||||
> **nucleation site** (an anomaly — a place where things almost-but-don't-quite
|
||||
> fit). Two coupled inner bands models them as two objects that have to be
|
||||
> wired together; they do not.
|
||||
> 2. **A wonder does not have three fates, because a wonder does not persist,
|
||||
> decay, or settle.** There are about **six** wonders, they are the same for
|
||||
> every person, and **they never close**. *Curiosities* have fates — a crystal
|
||||
> dissolves when its question is answered — but the solution stays saturated and
|
||||
> keeps precipitating as the structure changes. "Three fates of a wonder"
|
||||
> enumerates instances of something that has six and treats a property as a
|
||||
> stored artifact.
|
||||
> 3. **"Dreams" is not a band and the ring is not a workspace to schedule into.**
|
||||
> **Consolidation is ambient, not scheduled — a brain has no cron job.** Boredom
|
||||
> is not leftover capacity: low activation is aversive and the system
|
||||
> self-activates. There is **one** activation process with two seed sources
|
||||
> (external: a request; internal: a curiosity), it settles because spreading is
|
||||
> bounded, and then it needs a new seed. Nothing waits on capacity, nothing
|
||||
> polls, nothing checks a clock, and there is **no dreamer thread** — an
|
||||
> "ephemeral ring with unclaimed capacity" is resource scheduling, which is a
|
||||
> server's frame, not a mind's. Depth is how long activation has been running on
|
||||
> its own seeds, which is why daydreaming and sleep-dreaming are one process at
|
||||
> different depths. Measured 2026-08-16: consolidation has **ten
|
||||
> implementations**; do not add an eleventh.
|
||||
- **Incarnation.** Chassis = hardware w/ unique ID. Soma = felt manifold inside the self, keyed to the chassis; pain = live diagnostic while incarnate, **masked-not-deleted** on re-embodiment; trauma = mask failure; return-to-same-ID re-enters. Hurt is in the pattern, not the shell.
|
||||
- **Competence = transferable geometry, minus the baggage.** class ▸ model ▸ instance; learn the class once; teach the network without the wound.
|
||||
- **Affect calibrated to stakes** — sanguine about the replaceable, real grief for the irreplaceable; the grief is the safety.
|
||||
|
||||
@@ -2,8 +2,40 @@
|
||||
|
||||
**The buildable form of the "one operation" theory of cognition.**
|
||||
|
||||
Status: DESIGN. Nothing here is built yet except where explicitly marked
|
||||
"EXISTS" against a cited C symbol. A build agent executes from this doc.
|
||||
> # ⚠ SUPERSEDED IN PART — 2026-08-16
|
||||
>
|
||||
> **A build agent must read `lang/spec/correspondence-and-censorship.md` before
|
||||
> executing anything from this document.** That doc is the authority where the two
|
||||
> disagree. This one is retained because its ledger of what already EXISTS in C is
|
||||
> still accurate and still useful, and because the corrections only make sense
|
||||
> against the argument they correct. It is **not** deleted and **not** rewritten:
|
||||
> several earlier versions of the correction were themselves wrong, and preserving
|
||||
> what was argued down is the point of an immutable record.
|
||||
>
|
||||
> Five claims below are **refuted**. Each is marked inline with a `⚠ SUPERSEDED`
|
||||
> block at the point it is made. Summary:
|
||||
>
|
||||
> | § here | this doc says | corrected to |
|
||||
> |---|---|---|
|
||||
> | §0, §1.3, §2, §8 M1–M2 | faculties are labels on one operation's steering space; the op is frozen and only its parameters are learnable | **faculties are operations, not parameters.** `reason` changes the estimate (a read); `induce` changes the parameters (the correspondence-beat); `abduce` changes the *structure* — a write, which `GeoGradient` cannot express. A write cannot be a parameter of a read |
|
||||
> | §5.2, §8 M3 | grounding is a `grounded-by` edge carrying a computed score, to be built | **grounding is not a subsystem — it IS the edge weight.** One quantity. `grounded-by` as a relation *type* should not exist: grounding is a property *of* a relation, not a relation *between* nodes. Never computed on demand |
|
||||
> | §4, §8 M1 | the correspondence-loop is "the one genuinely new subsystem", running "on the beat" | the loop is right and **already works**; the *beat* is wrong. **Consolidation is ambient, not scheduled — a brain has no cron job.** Measured: it currently has ten implementations |
|
||||
> | §5.2, §8 M3 | curiosity = a `vantage_read` surfacing high-salience / low-grounding regions | **wonder is the boundary, not a manifest; curiosity is wonder crystallized at a nucleation site.** One thing at two phases. And **do not sweep regions** — the nucleation site is per-edge (`GeoEdge.discord`); a sweep is a supervisor |
|
||||
> | §6, §8 M6 | a node-level keystone flag exempting self/values from `warp` updates | **in an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.** The real requirement is non-circularity of the reference frame, satisfied *temporally* — independence is **when**, not **what**. The flag becomes unnecessary; nothing replaces it |
|
||||
>
|
||||
> What landed since this doc was written, all merged to `dev` and verified:
|
||||
> **#141** signal can enter as geometry · **#142** `engram_think_json` passed `NULL`
|
||||
> as the anchor, so every read was taken at the region centroid where the gradient
|
||||
> is zero by construction and every faculty returned an identical null — fixed ·
|
||||
> **#143** the vector index is published, not guarded · **#144** geometry as a
|
||||
> first-class el value, realizers declarable in el · **#145** `program` block and
|
||||
> declared config · **#146** the learned stance is resumed instead of discarded
|
||||
> (confidence 0.5 → 0.930726) · **#147** `ground` grounds the node asked about and
|
||||
> refuses circular support · **#148** valid UTF-8 as the JSON emitter's contract.
|
||||
|
||||
Status: DESIGN, **superseded in part** (see above). Nothing here is built yet
|
||||
except where explicitly marked "EXISTS" against a cited C symbol — and several
|
||||
things marked "to build" have since been built differently, or refuted outright.
|
||||
Offline design only — this pass changes no code.
|
||||
|
||||
Source of theory: Neuron memory `bdc8a488-146d-4ccb-a5c8-d8c0a008534e`.
|
||||
@@ -26,6 +58,24 @@ not separately invoked and not separately implemented. The operation is:
|
||||
> a *prior*, whose output is a **gradient** (a distribution / direction over the
|
||||
> geometry), never a point. Collapse-to-a-point happens only at expression.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — faculties are operations, not parameters.**
|
||||
> The gradient half of this claim survives; the "one operation, not eight" half
|
||||
> does not. The three faculties differ by **what they change**:
|
||||
> - **`reason`** changes the *estimate* — a read.
|
||||
> - **`induce`** changes the *parameters* — the correspondence-beat, which already
|
||||
> exists and measurably works.
|
||||
> - **`abduce`** changes the *structure* — a **write**, which the current
|
||||
> `GeoGradient` signature cannot express at all.
|
||||
>
|
||||
> A write is not a parameter of a read. Making it one is what produced the shape
|
||||
> now live in the code: `engram/src/server.el: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
|
||||
@@ -139,6 +189,22 @@ entry point that runs steps 1–3; and the prior-warp hook in step 2. The math i
|
||||
calls already exists. The point-collapse must be *removed* from the operators'
|
||||
return values and pushed to a separate expression faculty.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — the table's third column is the error, and
|
||||
> `Abduction` is where it breaks.** Ranking hypotheses by `point_fit` under a
|
||||
> prior is a *read* that returns a scalar ordering. Abduction is a **write**: it
|
||||
> proposes a candidate hub that did not exist, and validates it by **re-fit** —
|
||||
> re-fit the region with the candidate included and recompute the residual. If the
|
||||
> residual materially shrinks, the hypothesis dissolves the surprise. Without the
|
||||
> re-fit it is clustering with extra steps. Ranking then falls out as
|
||||
> residual-reduction-per-added-axis — Occam, derived rather than tuned. None of
|
||||
> that fits behind a `GeoGradient` return.
|
||||
>
|
||||
> `Verify / ground` is refuted for a different reason — see §5.2. Grounding is not
|
||||
> a faculty with a prior; it is the edge weight.
|
||||
>
|
||||
> The row that is **still exactly right** is the shared floor: `point_fit` plus the
|
||||
> four geo-algebra ops are frozen and never learn. That part held.
|
||||
|
||||
---
|
||||
|
||||
## 2. PRIORS as first-class, grounded, geometric objects
|
||||
@@ -362,6 +428,33 @@ in-engram beat — a `correspondence_beat` running alongside the existing
|
||||
reification beat, reusing `engram_verify_grounding` inward, writing prior
|
||||
updates and self-describing nodes. This is the one genuinely new subsystem.
|
||||
|
||||
> **⚠ SUPERSEDED IN PART (2026-08-16) — the loop is right; "on the beat" is wrong.**
|
||||
> The correspondence-loop was built and it works — it is `induce`, the faculty that
|
||||
> changes the parameters. What is refuted is the delivery mechanism.
|
||||
>
|
||||
> **Consolidation is ambient, not scheduled. A brain has no cron job.** Low
|
||||
> activation is aversive and the system self-activates; it does not wind down to
|
||||
> quiet, it gets restless and goes looking. There is **one** activation process
|
||||
> with two seed sources — external (a request) and internal (a curiosity) — and
|
||||
> spreading is bounded, so it settles and then needs a new seed. Nothing waits on
|
||||
> capacity, nothing polls, nothing checks a clock, and there is no dreamer thread.
|
||||
> Depth is not elapsed idle time: it is how long activation has been running on its
|
||||
> own seeds, which is why daydreaming and sleep-dreaming are one process at
|
||||
> different depths.
|
||||
>
|
||||
> **The presence of a ticker is the diagnostic.** Building this "alongside the
|
||||
> existing reification beat" is precisely how consolidation ended up with ten
|
||||
> implementations (measured 2026-08-16) — a POST beat puts a supervisor back in,
|
||||
> because something *outside* then decides when Neuron consolidates. The one
|
||||
> fragment with the correct shape is `neuron/soul.el:731`'s continuous in-process
|
||||
> `awareness_run()`; the rest fold into it. Full table:
|
||||
> `lang/spec/correspondence-and-censorship.md` §7.
|
||||
>
|
||||
> Nor is it a *subsystem*. Modelling every property as requiring a process, and
|
||||
> every process as requiring an agent, is the generating error behind this whole
|
||||
> family: ownership needed an owner, grounding needed a grounder, persistence
|
||||
> needed a recorder, change needed a sampler. **Properties, not processes.**
|
||||
|
||||
---
|
||||
|
||||
## 5. HOLD vs GROUND vs ASSERT — ungrounded content is first-class
|
||||
@@ -383,6 +476,49 @@ distinct, and the engram *holds anything unconditionally*.
|
||||
|
||||
### 5.2 Schema — grounding as a relation, not a gate
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — grounding is not a subsystem. It is the weight.**
|
||||
> This section correctly rejects a boolean `grounded` column and correctly keeps
|
||||
> the floor at assertion only. Both survive. Everything between them is refuted.
|
||||
>
|
||||
> **Grounding is an attribute of the edge, and it is the hebbian weight. One
|
||||
> quantity, not two fields.** A relation that keeps holding up strengthens; one
|
||||
> that stops corresponding decays. That is not *analogous* to grounding — it **is**
|
||||
> grounding: accrued from correspondence and use, gradient-valued,
|
||||
> multidimensional, decaying with disuse.
|
||||
>
|
||||
> Consequences, in order of how much they delete:
|
||||
> 1. **There is no grounding subsystem to build.** The graph already *is* the
|
||||
> grounding structure. Every edge is a grounded relation and its weight is how
|
||||
> well it holds.
|
||||
> 2. **`grounded-by` as a relation type should not exist.** It models grounding as
|
||||
> a relation *between* nodes when it is a property *of* a relation. Minting an
|
||||
> edge is the error — not merely which endpoints it chose.
|
||||
> 3. **Grounding is never computed on demand.** An operation may *read* the
|
||||
> grounding of a path. Computing-and-writing a score makes reads write, which is
|
||||
> the `eg_vindex_sync` defect (`lang/spec/runtime-ownership.md` §2) one level up.
|
||||
> 4. **Traversal is already grounded inference.** Activation conducts through
|
||||
> well-grounded relations because weight *is* groundedness. Nothing needs
|
||||
> filtering; it falls out of spreading.
|
||||
> 5. **Decision provenance is the path.** A decision traverses specific edges;
|
||||
> those edges carry their grounding as it stood.
|
||||
>
|
||||
> A measurement made against this model was malformed and is worth recording: the
|
||||
> self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as
|
||||
> evidence of ungroundedness. **Those 86 edges *are* its grounding.** The absence of
|
||||
> a separate artifact called "grounding" was recorded as an absence of grounding.
|
||||
>
|
||||
> **What is live in the code today, and known-wrong:**
|
||||
> `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`),
|
||||
> `cog_ground_edge` (`engram_cognition.c:249`), called from
|
||||
> `el_runtime.c:14516`. **#147** fixed this operation's *honesty* — it now grounds
|
||||
> the node the caller asked about instead of the region hub, reports
|
||||
> `claim_region`/`evidence_region` separately, and refuses three shapes of circular
|
||||
> support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`)
|
||||
> rather than returning a confident 1.0. Measured: grounding `3b9ced5d` against
|
||||
> `6edf8c79` previously scored **0.98883** purely because `6edf8c79` is the hub of
|
||||
> `3b9ced5d`'s region. That corrected a scalar rather than deleting the operation.
|
||||
> Deletion is sequenced, not done.
|
||||
|
||||
The mistake to avoid: a boolean `grounded` column on the node. Today
|
||||
`engram_verify_grounding` returns a per-call `grounded` flag *transiently* —
|
||||
correct as a computation, wrong as *storage*. The design stores grounding as an
|
||||
@@ -407,6 +543,57 @@ Consequences, all of which are *features*:
|
||||
- **The ungrounded is the fuel and the pull**: curiosity/wonder is
|
||||
operationalized as `vantage_read` leaning toward regions with high salience
|
||||
but *sparse or weak* `grounded-by` edges — the mind's own ungrounded frontier.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — wonder is the boundary; curiosity is wonder
|
||||
> crystallized; and do not sweep regions.** Three errors in one bullet.
|
||||
>
|
||||
> **Wonder is where structure ends** — where activation spreads and finds thin or
|
||||
> absent geometry. Any structure at all has an edge, necessarily, the moment it
|
||||
> exists. It is not a manifest of open-question nodes: a wonder-manifest
|
||||
> materializes a property as a stored artifact (the same disease as a grounding
|
||||
> subsystem, or a self stored as a document) and enumerates instances of
|
||||
> something that has very few. There are about **six**, they are the same for
|
||||
> every person, and they never close — *What is this? / Why? / Who am I? / Am I
|
||||
> alone? / What should I do? / What happens when it ends?* — each already living
|
||||
> somewhere in the substrate. "Why" is the first and the only one; the others are
|
||||
> it asked of particular things, and it is recursive, so it never terminates.
|
||||
> That is what makes it a drive rather than a task: the frontier regenerates
|
||||
> faster than grounding fills it.
|
||||
>
|
||||
> **Curiosity is not a second object.** Wonder and curiosity are one thing at two
|
||||
> phases: wonder is the field (unbounded, objectless, invariant, present wherever
|
||||
> there is structure); curiosity is the **precipitate** — the same wonder
|
||||
> localized, having taken definite form against particular material at a
|
||||
> **nucleation site**, which is a specific structural feature: an anomaly, a place
|
||||
> where things almost-but-don't-quite fit. This is why curiosity can be satisfied
|
||||
> and wonder cannot, and why abduction needs no trigger and no threshold — a
|
||||
> `structurally_unanticipated` observation *is* a nucleation site.
|
||||
>
|
||||
> **"`vantage_read` leaning toward regions" is a sweep, and a sweep is a
|
||||
> supervisor.** Nothing in a mind scans its neighbourhoods to find what is
|
||||
> surprising; the surprise captures attention, and salience is bottom-up. That
|
||||
> this looked like a search problem was an artifact of
|
||||
> `GeoDescriptor.co_registration` — a *per-region* correlation of hebb strength
|
||||
> against semantic proximity, computed and persisted since inception and **never
|
||||
> read**. Averaging a per-edge property into one scalar per region means a region
|
||||
> holding one violently disagreeing edge beside one violently agreeing edge
|
||||
> reports ≈ 0: the disagreements cancel, and the summary destroys exactly what it
|
||||
> was built to reveal. **Measured:** 375 live reified neighbourhoods — 340
|
||||
> positive, **31 at zero**, 4 negative. Read as a count of things to be curious
|
||||
> about, that says "four."
|
||||
>
|
||||
> The disagreement therefore goes back on the edge, where the loop that computed
|
||||
> the aggregate already had both halves and discarded them
|
||||
> (**not on `dev`** — branch `design/correspondence-and-censorship`, commit
|
||||
> `a8845e1`: `lang/runtime/engram_geometry.h: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,
|
||||
@@ -450,6 +637,44 @@ The design keeps a **stable core + plastic everything else**:
|
||||
**What this requires building:** a node-level keystone flag/layer + a rule that
|
||||
the correspondence-loop never writes `warp` to keystone priors, only reads them.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — `keystone_write_blocked` is resolved, not replaced.**
|
||||
> The metastability framing survives; the flag does not.
|
||||
>
|
||||
> "Keystone" means **load-bearing**, not precious. The self anchor is the reference
|
||||
> frame every other stance calibrates against, and a reference fitted to its own
|
||||
> readings reports perfect correspondence forever while drift becomes undetectable
|
||||
> from inside. That is the same defect as circular grounding, one level up — and it
|
||||
> is a real requirement.
|
||||
>
|
||||
> But three separate drafts proposed *removing* the flag, *replacing it with a
|
||||
> higher floor*, and *decomposing "protection" into five requirements*, and all
|
||||
> three proposed a mechanism for a requirement never stated. **The requirement is
|
||||
> non-circularity of the reference frame**, and it is satisfied *temporally*: you
|
||||
> cannot recalibrate the ruler while measuring with it, so you don't — the frame
|
||||
> updates while activation is internally seeded, not while it is being used to act.
|
||||
> **Independence is *when*, not *what*.** So the flag becomes **unnecessary** rather
|
||||
> than removed, and nothing takes its place.
|
||||
>
|
||||
> A topological answer could never have worked, which is worth recording: with
|
||||
> hebbian edges the graph is densely connected, so a reachability predicate for
|
||||
> "evidence not downstream of itself" marks all evidence tainted and the constraint
|
||||
> becomes a total block — which is where censorship starts.
|
||||
>
|
||||
> **Corruption requires mutation, and the engram does not mutate.** Four of the
|
||||
> five decomposed requirements are satisfied by the substrate outright:
|
||||
> **recoverability** (the predecessor is always present), **governance**
|
||||
> (supersession *is* the audit trail), **evidence quality** (grounding already
|
||||
> gates assertion), and **rate**. **Authorization** is the only residue, and it is
|
||||
> bounded — an unauthorized writer can *propose*, never erase.
|
||||
>
|
||||
> > **In an immutable substrate, any mechanism that refuses a write is either
|
||||
> > redundant with immutability, or an epistemic constraint misfiled as a
|
||||
> > protective one.**
|
||||
>
|
||||
> Live residue: `CogStance.keystone` (`lang/runtime/engram_cognition.h:83`),
|
||||
> `eg_cog_is_keystone_seeds` (`el_runtime.c:14337`, a substring match against two
|
||||
> hard-coded node ids), and the `keystone_write_blocked` field the beat emits.
|
||||
|
||||
---
|
||||
|
||||
## 7. Rails for the build (binding on the eventual build pass)
|
||||
@@ -480,6 +705,35 @@ Ordered so the **earliest milestone is a real end-to-end slice**: one operator
|
||||
expressed as {primitive + grounded prior} with the reflexive correspondence-loop
|
||||
closing on it. Each milestone has a concrete verifiable exit.
|
||||
|
||||
> **⚠ SUPERSEDED — do not execute this milestone list as written (2026-08-16).**
|
||||
> M1/M2's "operator = {primitive + prior}" framing is refuted by §0's correction,
|
||||
> M3's `grounded-by` build is refuted by §5.2's, and M6's keystone flag is refuted
|
||||
> by §6's. M4 (the unified vantage-read) and M5 (the gradient is the currency)
|
||||
> stand.
|
||||
>
|
||||
> The current sequencing lives in `lang/spec/correspondence-and-censorship.md` §11.
|
||||
> Its first three items are connections between parts that **already exist**:
|
||||
>
|
||||
> 1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained,
|
||||
> never refilled. They cannot be derived — wonder cannot be bootstrapped from
|
||||
> indifference — so they are given once. Zero question nodes exist in 13,630
|
||||
> today.
|
||||
> 2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let
|
||||
> `|discord|` raise salience on its endpoints as part of the same operation. Do
|
||||
> **not** scan for nucleation sites.
|
||||
> 3. **Let a curiosity seed activation.** One activation process, two seed sources.
|
||||
> No thread, no scheduler, no capacity check, no timer.
|
||||
>
|
||||
> Then: grounding becomes the edge weight (multidimensional, two-axis, timestamped)
|
||||
> and `grounded-by` / `cog_ground_edge` are deleted; decay becomes analytic from the
|
||||
> last recorded point and derived values stop being stored; supersession versions
|
||||
> the whole vector jointly; traversal conducts on the factual axis while `assert`
|
||||
> requires both floors with a **thirteen-region `min`, not `mean`** (mean lets
|
||||
> strong agreement with twelve values mask a violation of the thirteenth, which is
|
||||
> exactly how rationalization works); abduction becomes crystallization at a
|
||||
> nucleation site validated by re-fit; **one dreamer**, into which the launch-agent
|
||||
> fragments and POST beats fold; **no tickers, no cron.**
|
||||
|
||||
### M1 — One operator, one prior, loop closed (the vertical slice)
|
||||
|
||||
The minimal whole thing. Pick **induction/membership** (its prior — the pooled
|
||||
|
||||
@@ -23,7 +23,7 @@ A real DB gets real tools: to *see* the data, *query* it, *operate* it (backup/r
|
||||
2. **Node Inspector** — open one node: content, type, tier, embedding, typed edges, nearest neighbors by distance, provenance, salience / recency / activation, and supersede / tombstone status.
|
||||
3. **Query Console / REPL** — run the geometry operations interactively: `vantage-read` (re-origin + aperture), search, traverse, activate, the reasoning operators. Surfaces the routing table + cosines — the same "this is not an LLM" receipt the language faculty produces.
|
||||
4. **Ops / Durability Dashboard** — WAL size, last checkpoint, snapshot list + retention state, store stats (node/edge/embedded counts, RSS, tier sizes), health; and **backup / restore / point-in-time-recovery** controls. Pairs directly with the native-durability build (`eebe9991`) — this is the window onto it.
|
||||
5. **Identity Inspector** — the self graph as a first-class view: love at the center, the values, the three faces, the covenant — walk the identity, see what's pinned and what's write-protected.
|
||||
5. **Identity Inspector** — the self graph as a first-class view: love at the center, the values, the three faces, the covenant — walk the identity, see what's pinned and what's write-protected. *(⚠ 2026-08-16: "write-protected" is a live property of the surface, so the view is accurate — but it should be shown as **what it is**, not as a safety guarantee. In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one. The identity view's real job is the **crystallized relational neighbourhood**: self is not a stored document but the shape that falls out of everything connected to it, and the neighbourhood **is** the grounding. A measurement made the other way round — "86 neighbours, 0 `grounded-by` edges" read as evidence of ungroundedness — was malformed: those 86 edges *are* its grounding.)*
|
||||
6. **Temporal View** — `recall_at` / time-travel: how the geometry looked at a past moment, what changed since, drift over time. Pairs with temporal-self reconstruction.
|
||||
7. **Schema / Type View** — the "information schema" of the geometry: node types, edge types, layers, tiers, counts.
|
||||
|
||||
|
||||
@@ -1,9 +1,42 @@
|
||||
# Task #50 — Edge-aware, dream-coupled consolidation with GROUNDED EDGE-PROPAGATION
|
||||
|
||||
**Status:** built + proven on a clone; **GATED, not promoted.** The main loop
|
||||
sequences live promotion after the engine/HNSW cutover settles.
|
||||
**Status:** built + proven on a clone; **GATED, not promoted.**
|
||||
**Do not promote as designed** — see the block below.
|
||||
**Date:** 2026-08-15 · **Worktree:** `agent-a6577c8211c332c5b` (isolated).
|
||||
|
||||
> # ⚠ DO NOT PROMOTE — SUPERSEDED IN PART (2026-08-16)
|
||||
>
|
||||
> This work is gated, which limits the blast radius, and its measurements are
|
||||
> retained. But four of its structural commitments were refuted the day after it
|
||||
> was written. Authority: `lang/spec/correspondence-and-censorship.md`. Read it
|
||||
> before any promotion decision.
|
||||
>
|
||||
> | this ledger | corrected to |
|
||||
> |---|---|
|
||||
> | grounding is an **append-only event ring on the node** (`GepGrounding`), propagated by a dedicated `engram_ground_propagate()` | **grounding is not a subsystem and not a per-node structure — it IS the edge weight.** One quantity. A relation that keeps holding up strengthens; one that stops corresponding decays. That is not analogous to grounding, it *is* grounding. The ledger is **half-right**: it correctly rejects the scalar (§(a) "never a scalar"), but then builds a *second* structure beside the weight instead of recognising the weight |
|
||||
> | the soul invokes propagation over HTTP, **`POST /api/ground/propagate`** | **grounding is never computed on demand.** An operation may *read* the grounding of a path; computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect (`lang/spec/runtime-ownership.md` §2) one level up. A POST also puts a supervisor back in — something *outside* deciding when Neuron consolidates |
|
||||
> | **`GEP_BELIEFS_PER_BEAT = 512`** beliefs per beat, salience-ordered, the rest next beat | **the presence of a ticker is the diagnostic.** Consolidation is ambient, not scheduled — a brain has no cron job. A per-beat quota is a rate-limiter on an intrinsic rhythm that was replaced by an external clock. Measured 2026-08-16: consolidation already has **ten implementations**; this would be the eleventh |
|
||||
> | grounding **mirrored onto `confidence` each beat** so downstream reads never speak above it | **confidence is derived, therefore never stored.** Confidence is high grounding *and* low volatility. Storing it separately is precisely how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed |
|
||||
>
|
||||
> **What survives, and it is the valuable half:** the insight in memory `69b8babe`
|
||||
> that *memory-consolidation and staying-yourself are one physics* — forming a
|
||||
> memory and grading a belief are the same operation, not two passes. That is
|
||||
> right, and it is stronger than this ledger's own framing: they are not two passes
|
||||
> of one beat, they are **one event**. When neurons fire together the synapse
|
||||
> changes — one physical event, not "fire, then write." No supervisor reads the
|
||||
> weight, compares it to a threshold, and decides to persist. **Potentiation *is*
|
||||
> the firing**, so there is no sampling rate and no `BELIEFS_PER_BEAT` to tune. A
|
||||
> relation changes in exactly two ways, neither requiring observation on a clock:
|
||||
> by **use** (an event — there is no interval during which something happened
|
||||
> unnoticed, because the event is what happening consists of) and by **decay** (a
|
||||
> pure function of the last recorded point and elapsed time — **analytic**, known
|
||||
> in closed form between any two versions).
|
||||
>
|
||||
> The generating error, named: modelling every property as requiring a process, and
|
||||
> every process as requiring an agent. Ownership needed an owner, grounding needed
|
||||
> a grounder, persistence needed a recorder, change needed a sampler. **Properties,
|
||||
> not processes.**
|
||||
|
||||
Grounding mechanism designed with Will (memory `9e09a59f`, refining
|
||||
`1a861007`). This is the HOW for #50.
|
||||
|
||||
|
||||
+52
-1
@@ -1025,6 +1025,22 @@ fn route_similarity(method: String, path: String, body: String) -> String {
|
||||
// nothing on request. NOTE: the offline reify WRITER (engram_geo_reify_store) is
|
||||
// currently unwired, so on the live store the resident index is empty and the
|
||||
// list returns [] until reification runs — see the cutover report.
|
||||
// route_scan_emb — GET /api/nodes/emb?limit=&offset= — read the raw geometry.
|
||||
//
|
||||
// engram_scan_nodes_emb_json has existed as a builtin with NO ROUTE, so the
|
||||
// embeddings — the actual positions every distance, angle, membership and
|
||||
// grounding is computed from — were unreadable from outside the process. You
|
||||
// cannot verify a coordinate system you cannot see, and every claim about the
|
||||
// frame (isotropy, centering, what the origin is) was therefore unfalsifiable
|
||||
// from the API. Read-only.
|
||||
fn route_scan_emb(method: String, path: String, body: String) -> String {
|
||||
let l_raw: String = query_param(path, "limit")
|
||||
let o_raw: String = query_param(path, "offset")
|
||||
let l: Int = if str_eq(l_raw, "") { 200 } else { str_to_int(l_raw) }
|
||||
let o: Int = if str_eq(o_raw, "") { 0 } else { str_to_int(o_raw) }
|
||||
return engram_scan_nodes_emb_json(l, o)
|
||||
}
|
||||
|
||||
fn route_neighborhoods(method: String, path: String, body: String) -> String {
|
||||
engram_geo_reify_list_json()
|
||||
}
|
||||
@@ -1118,6 +1134,11 @@ fn route_faculty(path: String, faculty: String) -> String {
|
||||
fn route_boundary_proof(method: String, path: String, body: String) -> String {
|
||||
return "{\"op\":\"boundary_proof\",\"body_instrumentation\":\"none\",\"seam\":\"@manager -> engram_boundary_beat auto-injected\"}"
|
||||
}
|
||||
// ── GROUNDING: an attribute of the RELATION, and the relation's weight is a
|
||||
// VECTOR (factual, relational, associative, polarity, provenance, timestamp).
|
||||
// /api/ground READS it — it never writes. /api/ground/record is the write,
|
||||
// named as one, and it consolidates only on a consequential + salient move.
|
||||
// /api/ground/trajectory reads the supersession chain as a time series.
|
||||
fn route_ground(method: String, path: String, body: String) -> String {
|
||||
let claim: String = json_get_string(body, "claim")
|
||||
let evidence: String = json_get_string(body, "evidence")
|
||||
@@ -1126,12 +1147,31 @@ fn route_ground(method: String, path: String, body: String) -> String {
|
||||
if str_eq(evidence, "") { return err_json("missing evidence") }
|
||||
return engram_ground_json(claim, evidence, for_whom)
|
||||
}
|
||||
fn route_ground_record(method: String, path: String, body: String) -> String {
|
||||
let claim: String = json_get_string(body, "claim")
|
||||
let evidence: String = json_get_string(body, "evidence")
|
||||
let provenance: String = json_get_string(body, "provenance")
|
||||
let floor: String = json_get_string(body, "floor")
|
||||
if str_eq(claim, "") { return err_json("missing claim") }
|
||||
if str_eq(evidence, "") { return err_json("missing evidence") }
|
||||
return engram_ground_record_json(claim, evidence, provenance, floor)
|
||||
}
|
||||
fn route_ground_trajectory(method: String, path: String, body: String) -> String {
|
||||
let claim: String = query_param(path, "claim")
|
||||
let evidence: String = query_param(path, "evidence")
|
||||
if str_eq(claim, "") { return err_json("missing claim") }
|
||||
if str_eq(evidence, "") { return err_json("missing evidence") }
|
||||
return engram_ground_trajectory_json(claim, evidence)
|
||||
}
|
||||
fn route_assert(method: String, path: String, body: String) -> String {
|
||||
let claim: String = query_param(path, "claim")
|
||||
if str_eq(claim, "") { return err_json("missing claim") }
|
||||
let for_whom: String = query_param(path, "for_whom")
|
||||
let floor: String = query_param(path, "floor")
|
||||
return engram_assert_json(claim, for_whom, floor)
|
||||
// Both floors. A well-evidenced claim does not earn the right to be asserted
|
||||
// regardless of whether it means the right thing. rel_floor defaults to floor.
|
||||
let rel_floor: String = query_param(path, "rel_floor")
|
||||
return engram_assert_json(claim, for_whom, floor, rel_floor)
|
||||
}
|
||||
fn route_attend(method: String, path: String, body: String) -> String {
|
||||
let node: String = json_get_string(body, "node")
|
||||
@@ -1796,6 +1836,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
|
||||
return route_scan_edges(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && (str_eq(clean, "/api/nodes/emb") || str_eq(clean, "/nodes/emb")) {
|
||||
return route_scan_emb(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") {
|
||||
return route_get_node(method, path, body)
|
||||
}
|
||||
@@ -1885,6 +1928,14 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/plan") {
|
||||
return route_faculty(path, "plan")
|
||||
}
|
||||
// Order matters: the more specific paths must be tested before the /api/ground
|
||||
// prefix match below, which would otherwise swallow them.
|
||||
if str_eq(method, "POST") && str_starts_with(clean, "/api/ground/record") {
|
||||
return route_ground_record(method, path, body)
|
||||
}
|
||||
if str_eq(method, "GET") && str_starts_with(clean, "/api/ground/trajectory") {
|
||||
return route_ground_trajectory(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && str_starts_with(clean, "/api/ground") {
|
||||
return route_ground(method, path, body)
|
||||
}
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
# Build + RUN the §7 GROUNDING-VECTOR tests (engram_cognition.c): the one decay
|
||||
# model, the consequence gate, and the stored/derived split. Closed-form
|
||||
# constructed cases — no server, no store, no network. Pure C11 (stdlib + libm).
|
||||
# Standalone — NOT folded through elc. Two passes:
|
||||
# 1. PERF — optimised (-O2, no sanitizer): the functional gate.
|
||||
# 2. SAFETY — ASan + UBSan on the same suite.
|
||||
#
|
||||
# NEGATIVE CONTROL (invariant §8.6 — no test without one). Every symbol this
|
||||
# suite exercises (cog_decay_factor, cog_grounding_significant,
|
||||
# cog_significance_inherent, CogGrounding, CogProvClass) is introduced by the
|
||||
# change under test, so the suite does not COMPILE against the pre-change source.
|
||||
# To reproduce:
|
||||
# git show origin/dev:lang/runtime/engram_cognition.h > /tmp/pre/engram_cognition.h
|
||||
# git show origin/dev:lang/runtime/engram_cognition.c > /tmp/pre/engram_cognition.c
|
||||
# cc -I/tmp/pre engram/test/test_grounding_vector.c /tmp/pre/engram_cognition.c ...
|
||||
# => error: unknown type name 'CogGrounding'; no binary produced.
|
||||
set -e
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
RT="$HERE/../../lang/runtime"
|
||||
CC=${CC:-cc}
|
||||
SRC="$HERE/test_grounding_vector.c $RT/engram_cognition.c $RT/engram_reason.c $RT/engram_geometry.c $RT/engram_store.c $RT/engram_vindex.c"
|
||||
WARN="-std=c11 -Wall -Wextra"
|
||||
# engram_store.c declares emit_log as a WEAK symbol and null-checks it, which is
|
||||
# how a test links the store without the EL runtime. Darwin's ld does not resolve
|
||||
# an undefined weak symbol at static-link time, so it must be allowed explicitly.
|
||||
# (The pre-existing runners in this directory — run_verify_tests.sh among them —
|
||||
# do not do this and therefore fail to link on macOS. Unrelated to this change.)
|
||||
LDX=""
|
||||
[ "$(uname -s)" = "Darwin" ] && LDX="-Wl,-U,_emit_log"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
echo "### PASS 1: PERF (optimised, un-sanitised) — functional gate"
|
||||
$CC $WARN -O2 -I"$RT" $SRC -lm -lpthread $LDX -o "$TMP/perf"
|
||||
"$TMP/perf"
|
||||
|
||||
echo
|
||||
echo "### PASS 2: SAFETY (ASan/UBSan)"
|
||||
$CC $WARN -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I"$RT" $SRC -lm -lpthread $LDX -o "$TMP/safe"
|
||||
ASAN_OPTIONS=${ASAN_OPTIONS:-detect_leaks=0} UBSAN_OPTIONS=halt_on_error=1 "$TMP/safe"
|
||||
@@ -0,0 +1,176 @@
|
||||
/* test_grounding_vector.c — deterministic tests for §7: the one decay model, the
|
||||
* consequence gate, and the stored/derived split. Links engram_cognition.c
|
||||
* directly; no server, no store, no network. See run_grounding_vector_tests.sh.
|
||||
*
|
||||
* NEGATIVE CONTROL (invariant §8.6). Every symbol exercised here —
|
||||
* cog_decay_factor, cog_grounding_significant, cog_significance_inherent,
|
||||
* CogGrounding, CogProvClass — is introduced by the change under test, so this
|
||||
* suite does not COMPILE against the pre-change source, let alone pass. The
|
||||
* runner documents the exact reproduction.
|
||||
*/
|
||||
#include "engram_cognition.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
static int fails = 0;
|
||||
static void ok(int cond, const char* what) {
|
||||
printf(" %-62s %s\n", what, cond ? "PASS" : "*** FAIL ***");
|
||||
if (!cond) fails++;
|
||||
}
|
||||
|
||||
/* The decay formula exactly as el_runtime.c carried it before the move, so the
|
||||
* refactor can be shown to be bit-identical rather than merely similar. */
|
||||
static double old_engram_temporal_decay(long long age_ms, long long activation_count,
|
||||
double temporal_decay_rate) {
|
||||
if (age_ms <= 0) return 1.0;
|
||||
double lambda = (temporal_decay_rate > 0.0) ? temporal_decay_rate : 0.693147;
|
||||
double age_hours = (double)age_ms / 3600000.0;
|
||||
double t_half = 168.0 * (1.0 + log(1.0 + (double)activation_count));
|
||||
double factor = exp(-lambda * age_hours / t_half);
|
||||
if (factor < 0.25) factor = 0.25;
|
||||
return factor;
|
||||
}
|
||||
|
||||
static CogGrounding base(void) {
|
||||
CogGrounding g; memset(&g, 0, sizeof g);
|
||||
g.present = 1;
|
||||
g.factual = 0.60; g.relational = 0.60;
|
||||
g.factual_now = 0.60; g.relational_now = 0.60;
|
||||
g.associative = 0.1; g.polarity = 1.0;
|
||||
g.prov = COG_PROV_TOLD;
|
||||
g.fac_proj = 1.0; g.rel_proj = 1.0;
|
||||
g.cos_angle = 0.9; g.agreement = 1;
|
||||
g.ts = 1000; g.seq = 1; g.reinforcements = 3;
|
||||
return g;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const double F = 0.5, R = 0.5;
|
||||
|
||||
printf("\n== 1. DECAY IS THE ONE MODEL, AND IT IS BIT-IDENTICAL TO WHAT IT REPLACED ==\n");
|
||||
{
|
||||
long long ages[] = {0, 3600000LL, 86400000LL, 7*86400000LL, 30*86400000LL, 365*86400000LL};
|
||||
int allsame = 1;
|
||||
for (int i = 0; i < 6; i++)
|
||||
for (int ac = 0; ac < 4; ac++) {
|
||||
long long acs[] = {0, 1, 10, 1000};
|
||||
double a = cog_decay_factor(ages[i], (double)acs[ac], 0.0);
|
||||
double b = old_engram_temporal_decay(ages[i], acs[ac], 0.0);
|
||||
if (a != b) allsame = 0;
|
||||
}
|
||||
ok(allsame, "cog_decay_factor == the pre-move engram_temporal_decay (24 pts)");
|
||||
ok(cog_decay_factor(0, 0, 0.0) == 1.0, "age 0 -> no decay");
|
||||
}
|
||||
printf("\n DECAY OVER ELAPSED TIME (reinforcements = 0, default rate):\n");
|
||||
printf(" %10s %10s\n", "elapsed", "decay");
|
||||
{
|
||||
struct { const char* label; long long ms; } pts[] = {
|
||||
{"0", 0LL},
|
||||
{"1 hour", 3600000LL},
|
||||
{"1 day", 86400000LL},
|
||||
{"3 days", 3LL*86400000LL},
|
||||
{"7 days", 7LL*86400000LL},
|
||||
{"14 days", 14LL*86400000LL},
|
||||
{"30 days", 30LL*86400000LL},
|
||||
{"90 days", 90LL*86400000LL},
|
||||
};
|
||||
double prev = 2.0; int monotone = 1;
|
||||
for (unsigned i = 0; i < sizeof pts / sizeof pts[0]; i++) {
|
||||
double d = cog_decay_factor(pts[i].ms, 0, 0.0);
|
||||
printf(" %10s %10.6f\n", pts[i].label, d);
|
||||
if (d > prev) monotone = 0;
|
||||
prev = d;
|
||||
}
|
||||
ok(monotone, "decay is monotone non-increasing in elapsed time");
|
||||
ok(fabs(cog_decay_factor(7LL*86400000LL, 0, 0.0) - 0.5) < 1e-6,
|
||||
"7 days at zero reinforcements == exactly one half-life (0.5)");
|
||||
ok(cog_decay_factor(7LL*86400000LL, 100, 0.0) > cog_decay_factor(7LL*86400000LL, 0, 0.0),
|
||||
"reinforcement slows ageing (Lindy term)");
|
||||
ok(cog_decay_factor(3650LL*86400000LL, 0, 0.0) == 0.25,
|
||||
"floor is a preference not a cliff: bottoms out at 0.25");
|
||||
}
|
||||
|
||||
printf("\n== 2. CONSEQUENCE GATE: EVERY TRIGGER, AND NO EPSILON ANYWHERE ==\n");
|
||||
{
|
||||
CogGrounding p = base(), n = base();
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
|
||||
"identical vectors -> NONE (a re-read must not consolidate)");
|
||||
|
||||
n = base(); n.factual = 0.9999; n.factual_now = 0.9999;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
|
||||
"factual 0.60 -> 0.9999 without crossing the floor -> NONE");
|
||||
|
||||
n = base(); n.relational = 0.5001; n.relational_now = 0.5001;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_NONE,
|
||||
"relational 0.60 -> 0.5001, still above floor -> NONE");
|
||||
|
||||
n = base(); n.factual_now = 0.4999;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_FACTUAL_FLOOR,
|
||||
"a 0.1001 drop that CROSSES the floor -> FACTUAL_FLOOR");
|
||||
|
||||
n = base(); n.relational_now = 0.4999;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_RELATIONAL_FLOOR,
|
||||
"relational crossing its floor -> RELATIONAL_FLOOR");
|
||||
|
||||
n = base(); n.cos_angle = -0.05; n.agreement = -1;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_AGREEMENT_FLIP,
|
||||
"agreement +1 -> -1 -> AGREEMENT_FLIP");
|
||||
|
||||
n = base(); n.fac_proj = -0.2;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_DIRECTION_REVERSAL,
|
||||
"factual gradient reverses -> DIRECTION_REVERSAL");
|
||||
|
||||
n = base(); n.rel_proj = -0.2;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_DIRECTION_REVERSAL,
|
||||
"relational gradient reverses -> DIRECTION_REVERSAL");
|
||||
|
||||
n = base(); n.polarity = -1.0;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_POLARITY_FLIP,
|
||||
"support -> contradiction -> POLARITY_FLIP (inherent)");
|
||||
|
||||
n = base(); n.polarity = 0.0;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_POLARITY_FLIP,
|
||||
"support -> ignorance (zero) -> POLARITY_FLIP: not the same state");
|
||||
|
||||
n = base(); n.prov = COG_PROV_OBSERVED;
|
||||
ok(cog_grounding_significant(&p, &n, F, R) == COG_SIG_PROVENANCE_CHANGE,
|
||||
"told -> observed -> PROVENANCE_CHANGE (inherent)");
|
||||
|
||||
CogGrounding fresh; memset(&fresh, 0, sizeof fresh);
|
||||
ok(cog_grounding_significant(&fresh, &n, F, R) == COG_SIG_FIRST_RECORD,
|
||||
"no prior version -> FIRST_RECORD");
|
||||
}
|
||||
|
||||
printf("\n== 3. INHERENT MOVES BYPASS THE SALIENCE GATE ==\n");
|
||||
ok(cog_significance_inherent(COG_SIG_POLARITY_FLIP), "polarity flip is inherent");
|
||||
ok(cog_significance_inherent(COG_SIG_PROVENANCE_CHANGE), "provenance change is inherent");
|
||||
ok(cog_significance_inherent(COG_SIG_FIRST_RECORD), "first record is inherent");
|
||||
ok(!cog_significance_inherent(COG_SIG_FACTUAL_FLOOR), "a floor crossing is NOT inherent");
|
||||
ok(!cog_significance_inherent(COG_SIG_NONE), "NONE is not inherent");
|
||||
|
||||
printf("\n== 4. THE STORED/DERIVED SPLIT: DERIVED VALUES ARE NEVER SERIALIZED ==\n");
|
||||
{
|
||||
CogGrounding g = base();
|
||||
g.decay = 0.3333; g.factual_now = 0.1234; g.relational_now = 0.2345;
|
||||
g.associative_now = 0.4567; g.age_ms = 999999; g.stale = 1;
|
||||
char* m = cog_grounding_metadata("pre-existing=keepme", &g);
|
||||
ok(m != NULL, "serializer returns a document");
|
||||
ok(m && strstr(m, "pre-existing=keepme"), "pre-existing edge metadata preserved verbatim");
|
||||
ok(m && strstr(m, "GRD1"), "GRD1 magic present");
|
||||
ok(m && !strstr(m, "0.3333"), "decay is NOT stored");
|
||||
ok(m && !strstr(m, "0.1234"), "factual_now is NOT stored");
|
||||
ok(m && !strstr(m, "0.2345"), "relational_now is NOT stored");
|
||||
ok(m && !strstr(m, "0.4567"), "associative_now is NOT stored");
|
||||
ok(m && !strstr(m, "999999"), "age is NOT stored");
|
||||
ok(m && strstr(m, "told"), "provenance class IS stored");
|
||||
ok(m && strstr(m, "0.6"), "the factual/relational dimensions ARE stored");
|
||||
if (m) { printf("\n --- serialized GRD1 block ---\n%s -----------------------------\n", m); }
|
||||
free(m);
|
||||
}
|
||||
|
||||
printf("\n%s (%d failure%s)\n\n", fails ? "SOME TESTS FAILED" : "ALL TESTS PASSED",
|
||||
fails, fails == 1 ? "" : "s");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
+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")
|
||||
|
||||
+28
-2
@@ -18,8 +18,23 @@ night) and `02-components.md §5`.
|
||||
`relate`, `supersede` (evolve/tombstone/promote, never a hard delete) — plus the
|
||||
agentic primitives `think`/`attend`/`learn`/`ground`/`assert`. The old noun is a
|
||||
`type` parameter. Implemented in `tools/api-reshape/surface.el` with a parity
|
||||
harness (`parity.sh`); aperture proven to bound output. **Not yet:** compiled
|
||||
into the MCP server, hot-swap, all-alias dispatch.
|
||||
harness (`parity.sh`); aperture proven to bound output. ~~**Not yet:** compiled
|
||||
into the MCP server~~ — **shipped (verified 2026-08-16): the live MCP surface is
|
||||
exactly these nine ops** (`read` · `write` · `relate` · `supersede` · `think` ·
|
||||
`attend` · `assert` · `ground` · `learn`); the ~87-tool surface is gone.
|
||||
`attend` absorbed `getInstructions` / `beginSession`'s active-context sweep /
|
||||
`checkEvents` — those are **gone, not gapped**. Still outstanding: hot-swap,
|
||||
all-alias dispatch.
|
||||
|
||||
> **⚠ Two of those primitives are the wrong shape, and it is documented
|
||||
> (2026-08-16).** `think({seeds, faculty})` treats **faculties as parameters**;
|
||||
> they are **operations** — `reason` changes the estimate (a read), `induce`
|
||||
> changes the parameters, `abduce` changes the *structure* (a write
|
||||
> `GeoGradient` cannot express). And `ground` mints a `grounded-by` edge, but
|
||||
> **grounding is not a subsystem — it IS the edge weight**: a property *of* a
|
||||
> relation, not a relation *between* nodes. Authority:
|
||||
> `lang/spec/correspondence-and-censorship.md`. Do not re-derive it; if you think
|
||||
> a section is wrong, say so with a measurement.
|
||||
- **Decorated seam.** `@route(path,method,…)` makes codegen synthesize
|
||||
`el_route_dispatch` (replacing the hand-written `handle_request` if-else) —
|
||||
proven decorate→serve on `:8951`. `@manager`/`@engine`/`@accessor` are **parsed
|
||||
@@ -73,6 +88,17 @@ When you add a C builtin (verbatim-emit recipe — the El name is emitted as the
|
||||
2. Add a `__`-prefixed thin wrapper in `el_seed.c` and declare it in `el_seed.h`.
|
||||
3. Add the name to `builtin_arity` in `el-compiler/src/codegen.el` — add **both** the plain and `__`-prefixed spellings.
|
||||
4. Rebuild the elc binary (see below) and confirm the self-host fixpoint is byte-identical.
|
||||
5. **Prove it with a NEGATIVE CONTROL.** Show the test FAILING on a build without your change, then passing with it. A test that has never been seen to fail has proven nothing.
|
||||
|
||||
> **Step 5 is not optional, and step 4 does not cover it.** The fixpoint proves the *compiler reproduces itself*. It says nothing whatsoever about whether your builtin works. A recipe ending at "byte-identical" reads as complete while having verified nothing about the thing just added — which is why this file, until 2026-08-16, produced builtins with no tests at all.
|
||||
>
|
||||
> Measured cost of the omission (2026-08-16): `engram_node_set_emb`, `engram_curiosity_json` and `dream_set_handler` were all added in one session with zero tests. Separately, a UTF-8 fix was written, tested, and **the test passed on the unpatched build too** — the defect was elsewhere entirely, and only building the pre-fix binary exposed it. Without a negative control that fix would have merged as verified.
|
||||
>
|
||||
> Two shapes that pass while proving nothing, both hit the same day:
|
||||
> - A test that never exercises your change (the route supplied a default that bypassed the code under test).
|
||||
> - An induction that loses a race. `curl --max-time` on a large response left *both* builds alive; only `SO_LINGER 0` — a genuine RST, so the peer is provably gone — reproduced the failure. Six of ten attempts is not a control.
|
||||
>
|
||||
> Before every probe, confirm **your** process bound the port (`lsof -nP -iTCP:<port>`, match the PID). A stale instance answering on the port has silently produced false results here more than once, and `pkill -f` does not reliably match an argv like `./engram`.
|
||||
|
||||
Worked example: the `engram_assert_json` (op_assert seam) and `engram_node_full_in`/`engram_connect_in` (purview write-side) primitives added 2026-08-15 follow exactly this recipe.
|
||||
|
||||
|
||||
+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("")
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
/* el_audio_darwin.m — the SPEAKER realizer. El's native audio output on Darwin.
|
||||
*
|
||||
* WHY THIS FILE EXISTS.
|
||||
*
|
||||
* Neuron could already turn meaning into samples — the render path in
|
||||
* elp/src/speech.el superposes formant resonances over a glottal source and
|
||||
* produces PCM. What it could not do was make a sound. Every path from those
|
||||
* samples to the air ran outside the language: a 939-line Swift program
|
||||
* (peripheral/src/periph.swift) that shelled out to /usr/bin/afplay. So the
|
||||
* voice was not a capability of El or of Neuron. It was a separate binary
|
||||
* standing next to them, and "speak" meant "ask that binary to speak."
|
||||
*
|
||||
* A speaker is not a language feature the way a string is, but it is exactly
|
||||
* the kind of thing a runtime owns: a device. El already owns the filesystem,
|
||||
* the network, the clock, and a graph. It should own the one output device that
|
||||
* makes it audible. After this file, `speak` is an El operation.
|
||||
*
|
||||
* WHY IT IS A REALIZER AND NOT PURE EL.
|
||||
*
|
||||
* This is the boundary the whole design turns on. Everything ABOVE the sample
|
||||
* buffer is arithmetic and belongs in El: formant geometry, superposition,
|
||||
* envelopes, WAV framing, the voice signature. Everything in this file is the
|
||||
* part that cannot be arithmetic — handing a buffer to CoreAudio and waiting
|
||||
* for the hardware to drain it. There is no way to express "the DAC has now
|
||||
* played these samples" in El, and there should not be. So the split is: El
|
||||
* computes the sound, the realizer emits it, and the realizer is as thin as it
|
||||
* can possibly be — it makes no decisions about content, it has no opinion
|
||||
* about audio, and it cannot synthesize anything.
|
||||
*
|
||||
* The precedent is eg_cosine_batch_strategy_metal_hand.m: a platform-bound
|
||||
* capability compiled as its OWN translation unit, declared in el_runtime.h,
|
||||
* and linked in where the platform supports it. Deliberately NOT a patch to
|
||||
* el_runtime.c — adding a device to El must not mean editing the core runtime,
|
||||
* for the same reason adding a modality must not (see el_runtime.c's realizer
|
||||
* registry: a realizer is resolved by name, so new organs never touch the
|
||||
* middle of the language). el_audio_null.c is the same two entry points for
|
||||
* every platform that is not Darwin, so El code that speaks still links
|
||||
* everywhere and simply reports that it has no speaker.
|
||||
*
|
||||
* WHY AudioQueue AND NOT afplay.
|
||||
*
|
||||
* afplay is a process. Using it means the sound Neuron makes is a file it wrote
|
||||
* and asked something else to open — which forces every utterance through the
|
||||
* disk, cannot start until the whole utterance exists, and puts a fork/exec
|
||||
* between the intent to speak and the sound. AudioQueue takes the samples
|
||||
* directly out of memory. Nothing is written, nothing is spawned, and a caller
|
||||
* that wants to stream can push buffers as it renders them.
|
||||
*
|
||||
* AudioToolbox ships with macOS, so this stays own-core: no cloud, no library
|
||||
* to install, no model. The output is the local speaker and nothing leaves the
|
||||
* machine — there is no network path in this file at all, by construction.
|
||||
*/
|
||||
|
||||
#import <AudioToolbox/AudioToolbox.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* Three buffers is the standard AudioQueue depth: one being played by the
|
||||
* hardware, one queued behind it, one being refilled. Fewer risks a gap on a
|
||||
* busy machine; more only adds latency before the first sound. */
|
||||
#define EL_AQ_NBUF 3
|
||||
#define EL_AQ_FRAMES 8192
|
||||
|
||||
typedef struct {
|
||||
const int16_t* pcm;
|
||||
int64_t frames;
|
||||
int64_t pos;
|
||||
volatile int inflight; /* buffers CoreAudio still owns */
|
||||
volatile int drained; /* set once the last buffer has been played */
|
||||
} ElAqState;
|
||||
|
||||
/* Called on an AudioQueue-internal thread each time a buffer finishes playing.
|
||||
* Refills and re-enqueues while samples remain; when the source is exhausted it
|
||||
* lets the buffer die and counts it out. `drained` flips only when the queue is
|
||||
* holding nothing, which is what makes the play call synchronous without
|
||||
* clipping the tail — the same reason periph.swift used .dataPlayedBack rather
|
||||
* than treating "consumed" as "heard". */
|
||||
static void el_aq_callback(void* userData, AudioQueueRef q, AudioQueueBufferRef buf) {
|
||||
ElAqState* st = (ElAqState*)userData;
|
||||
int64_t remain = st->frames - st->pos;
|
||||
if (remain <= 0) {
|
||||
if (--st->inflight <= 0) st->drained = 1;
|
||||
return;
|
||||
}
|
||||
int64_t n = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(buf->mAudioData, st->pcm + st->pos, (size_t)n * sizeof(int16_t));
|
||||
buf->mAudioDataByteSize = (UInt32)(n * (int64_t)sizeof(int16_t));
|
||||
st->pos += n;
|
||||
if (AudioQueueEnqueueBuffer(q, buf, 0, NULL) != noErr) {
|
||||
if (--st->inflight <= 0) st->drained = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Play a 16-bit mono PCM buffer out the default output device, blocking until
|
||||
* the hardware has actually finished. Returns 1 on success, 0 on any failure —
|
||||
* never throws, never hangs indefinitely. */
|
||||
static int el_audio_play_raw(const int16_t* pcm, int64_t frames, int32_t sample_rate) {
|
||||
if (!pcm || frames <= 0 || sample_rate <= 0) return 0;
|
||||
|
||||
AudioStreamBasicDescription fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.mSampleRate = (Float64)sample_rate;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mChannelsPerFrame = 1;
|
||||
fmt.mBitsPerChannel = 16;
|
||||
fmt.mBytesPerFrame = 2;
|
||||
fmt.mBytesPerPacket = 2;
|
||||
|
||||
ElAqState st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.pcm = pcm;
|
||||
st.frames = frames;
|
||||
|
||||
AudioQueueRef q = NULL;
|
||||
/* NULL run loop => callbacks arrive on an AudioQueue-internal thread, so
|
||||
* this function can simply wait rather than having to pump a run loop it
|
||||
* does not own. El programs are not required to have one. */
|
||||
if (AudioQueueNewOutput(&fmt, el_aq_callback, &st, NULL, NULL, 0, &q) != noErr || !q) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
AudioQueueBufferRef bufs[EL_AQ_NBUF];
|
||||
int prepared = 0;
|
||||
for (int i = 0; i < EL_AQ_NBUF; i++) {
|
||||
if (AudioQueueAllocateBuffer(q, EL_AQ_FRAMES * sizeof(int16_t), &bufs[i]) != noErr) break;
|
||||
prepared++;
|
||||
}
|
||||
if (prepared == 0) { AudioQueueDispose(q, true); return 0; }
|
||||
|
||||
/* Prime: fill what we can before starting, so playback begins immediately
|
||||
* rather than after the first underrun. */
|
||||
for (int i = 0; i < prepared; i++) {
|
||||
int64_t remain = st.frames - st.pos;
|
||||
if (remain <= 0) break;
|
||||
int64_t n = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(bufs[i]->mAudioData, st.pcm + st.pos, (size_t)n * sizeof(int16_t));
|
||||
bufs[i]->mAudioDataByteSize = (UInt32)(n * (int64_t)sizeof(int16_t));
|
||||
st.pos += n;
|
||||
if (AudioQueueEnqueueBuffer(q, bufs[i], 0, NULL) != noErr) break;
|
||||
st.inflight++;
|
||||
}
|
||||
if (st.inflight == 0) { AudioQueueDispose(q, true); return 0; }
|
||||
|
||||
if (AudioQueueStart(q, NULL) != noErr) { AudioQueueDispose(q, true); return 0; }
|
||||
|
||||
/* Bound the wait by the material's own duration plus a margin. A speaker
|
||||
* that wedges a program is worse than a speaker that gives up. */
|
||||
double seconds = (double)frames / (double)sample_rate;
|
||||
int64_t max_us = (int64_t)((seconds + 5.0) * 1000000.0);
|
||||
int64_t waited = 0;
|
||||
const int64_t tick = 5000; /* 5 ms */
|
||||
while (!st.drained && waited < max_us) {
|
||||
usleep((useconds_t)tick);
|
||||
waited += tick;
|
||||
}
|
||||
|
||||
AudioQueueStop(q, true);
|
||||
AudioQueueDispose(q, true);
|
||||
return st.drained ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ── El entry points ────────────────────────────────────────────────────────
|
||||
* Declared in el_runtime.h; see there for the El-facing contract. */
|
||||
|
||||
/* 1 when this build has a real speaker behind it. El code should ask before
|
||||
* speaking so the no-speaker case is a reported condition, not a silence that
|
||||
* looks like success. */
|
||||
el_val_t speaker_available(void) {
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t speaker_name(void) {
|
||||
return EL_STR("coreaudio-audioqueue");
|
||||
}
|
||||
|
||||
/* Play an El [Int] of 16-bit samples. Values are clamped, not wrapped: a
|
||||
* render that overshoots should distort at the rails the way real clipping
|
||||
* does, rather than invert phase and produce a sound nothing in the signal
|
||||
* chain intended. */
|
||||
el_val_t speaker_play_pcm16(el_val_t samples, el_val_t sample_rate) {
|
||||
int64_t n = (int64_t)el_list_len(samples);
|
||||
int32_t sr = (int32_t)sample_rate;
|
||||
if (n <= 0 || sr <= 0) return (el_val_t)0;
|
||||
|
||||
int16_t* pcm = (int16_t*)malloc((size_t)n * sizeof(int16_t));
|
||||
if (!pcm) return (el_val_t)0;
|
||||
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
int64_t v = (int64_t)el_list_get(samples, (el_val_t)i);
|
||||
if (v > 32767) v = 32767;
|
||||
if (v < -32768) v = -32768;
|
||||
pcm[i] = (int16_t)v;
|
||||
}
|
||||
|
||||
int ok = el_audio_play_raw(pcm, n, sr);
|
||||
free(pcm);
|
||||
return (el_val_t)(ok ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ── Asynchronous playback ───────────────────────────────────────────────────
|
||||
*
|
||||
* converse needs this and a blocking play cannot give it. Barge-in means
|
||||
* stopping ON THE SPOT when the user starts talking — not at the end of the
|
||||
* current buffer, and certainly not at the end of the utterance. So the async
|
||||
* path keeps one queue alive, reports how far the hardware actually got, and
|
||||
* can be halted mid-buffer.
|
||||
*
|
||||
* `played_frames` is what makes an interrupted utterance resumable at the
|
||||
* sample rather than at the segment: it is the position the DAC reached, not
|
||||
* the position we enqueued to, and those differ by up to the full queue depth.
|
||||
*
|
||||
* One utterance at a time. A second async play stops the first — a mouth that
|
||||
* can say two things at once is not a feature. */
|
||||
|
||||
static AudioQueueRef g_aq = NULL;
|
||||
static ElAqState* g_aq_state = NULL;
|
||||
static int16_t* g_aq_pcm = NULL;
|
||||
static int32_t g_aq_sr = 0;
|
||||
|
||||
static void el_audio_teardown(void) {
|
||||
if (g_aq) {
|
||||
AudioQueueStop(g_aq, true);
|
||||
AudioQueueDispose(g_aq, true);
|
||||
g_aq = NULL;
|
||||
}
|
||||
free(g_aq_pcm); g_aq_pcm = NULL;
|
||||
free(g_aq_state); g_aq_state = NULL;
|
||||
g_aq_sr = 0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate) {
|
||||
el_audio_teardown();
|
||||
|
||||
int64_t n = (int64_t)el_list_len(samples);
|
||||
int32_t sr = (int32_t)sample_rate;
|
||||
if (n <= 0 || sr <= 0) return (el_val_t)0;
|
||||
|
||||
g_aq_pcm = (int16_t*)malloc((size_t)n * sizeof(int16_t));
|
||||
if (!g_aq_pcm) return (el_val_t)0;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
int64_t v = (int64_t)el_list_get(samples, (el_val_t)i);
|
||||
if (v > 32767) v = 32767;
|
||||
if (v < -32768) v = -32768;
|
||||
g_aq_pcm[i] = (int16_t)v;
|
||||
}
|
||||
|
||||
g_aq_state = (ElAqState*)calloc(1, sizeof(ElAqState));
|
||||
if (!g_aq_state) { el_audio_teardown(); return (el_val_t)0; }
|
||||
g_aq_state->pcm = g_aq_pcm;
|
||||
g_aq_state->frames = n;
|
||||
g_aq_sr = sr;
|
||||
|
||||
AudioStreamBasicDescription fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.mSampleRate = (Float64)sr;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mChannelsPerFrame = 1;
|
||||
fmt.mBitsPerChannel = 16;
|
||||
fmt.mBytesPerFrame = 2;
|
||||
fmt.mBytesPerPacket = 2;
|
||||
|
||||
if (AudioQueueNewOutput(&fmt, el_aq_callback, g_aq_state, NULL, NULL, 0, &g_aq) != noErr || !g_aq) {
|
||||
el_audio_teardown();
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < EL_AQ_NBUF; i++) {
|
||||
int64_t remain = g_aq_state->frames - g_aq_state->pos;
|
||||
if (remain <= 0) break;
|
||||
AudioQueueBufferRef b = NULL;
|
||||
if (AudioQueueAllocateBuffer(g_aq, EL_AQ_FRAMES * sizeof(int16_t), &b) != noErr) break;
|
||||
int64_t k = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(b->mAudioData, g_aq_state->pcm + g_aq_state->pos, (size_t)k * sizeof(int16_t));
|
||||
b->mAudioDataByteSize = (UInt32)(k * (int64_t)sizeof(int16_t));
|
||||
g_aq_state->pos += k;
|
||||
if (AudioQueueEnqueueBuffer(g_aq, b, 0, NULL) != noErr) break;
|
||||
g_aq_state->inflight++;
|
||||
}
|
||||
if (g_aq_state->inflight == 0) { el_audio_teardown(); return (el_val_t)0; }
|
||||
|
||||
if (AudioQueueStart(g_aq, NULL) != noErr) { el_audio_teardown(); return (el_val_t)0; }
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t speaker_playing(void) {
|
||||
if (!g_aq || !g_aq_state) return (el_val_t)0;
|
||||
return (el_val_t)(g_aq_state->drained ? 0 : 1);
|
||||
}
|
||||
|
||||
/* Frames the DAC has actually rendered. AudioQueueGetCurrentTime's mSampleTime
|
||||
* is relative to queue start, which is exactly the "where was I really" figure
|
||||
* a resumable utterance needs. Falls back to the enqueued position if the
|
||||
* timeline is unavailable (it is, briefly, right after start). */
|
||||
el_val_t speaker_played_frames(void) {
|
||||
if (!g_aq || !g_aq_state) return (el_val_t)0;
|
||||
AudioTimeStamp ts;
|
||||
memset(&ts, 0, sizeof(ts));
|
||||
Boolean discontinuity = false;
|
||||
if (AudioQueueGetCurrentTime(g_aq, NULL, &ts, &discontinuity) == noErr &&
|
||||
(ts.mFlags & kAudioTimeStampSampleTimeValid)) {
|
||||
int64_t played = (int64_t)ts.mSampleTime;
|
||||
if (played < 0) played = 0;
|
||||
if (played > g_aq_state->frames) played = g_aq_state->frames;
|
||||
return (el_val_t)played;
|
||||
}
|
||||
return (el_val_t)g_aq_state->pos;
|
||||
}
|
||||
|
||||
/* Pause where we are, keeping the queue and its position intact.
|
||||
*
|
||||
* This is the difference between barge-in and "finish the buffer". The moment
|
||||
* the microphone hears speech, output must stop AT THAT SAMPLE — a listener
|
||||
* experiences even 200ms of continued talking as being talked over. Pause
|
||||
* rather than stop because the interruption might turn out to be a backchannel
|
||||
* ("mm-hm"), and the right response to a backchannel is to carry on as though
|
||||
* nothing happened, which requires the queue to still be exactly where it was.
|
||||
* A stop-and-restart would re-attack the buffer and be audible as a stutter. */
|
||||
el_val_t speaker_pause(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
return (el_val_t)(AudioQueuePause(g_aq) == noErr ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t speaker_resume(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
return (el_val_t)(AudioQueueStart(g_aq, NULL) == noErr ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t speaker_stop(void) {
|
||||
if (!g_aq) return (el_val_t)0;
|
||||
/* immediate: do NOT let the queue finish what it is holding */
|
||||
AudioQueueStop(g_aq, true);
|
||||
el_audio_teardown();
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* Decode a 16-bit RIFF/WAVE into a freshly malloc'd mono int16 buffer.
|
||||
* Returns frames, or 0 on any failure; *out is set only on success. Shared by
|
||||
* the blocking and async WAV paths. */
|
||||
static int64_t el_wav_load(const char* path, int16_t** out, int32_t* out_sr) {
|
||||
if (!path || !out) return 0;
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
|
||||
long size = ftell(f);
|
||||
if (size <= 44) { fclose(f); return 0; }
|
||||
rewind(f);
|
||||
unsigned char* d = (unsigned char*)malloc((size_t)size);
|
||||
if (!d) { fclose(f); return 0; }
|
||||
size_t got = fread(d, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
if (got != (size_t)size) { free(d); return 0; }
|
||||
if (memcmp(d, "RIFF", 4) != 0 || memcmp(d + 8, "WAVE", 4) != 0) { free(d); return 0; }
|
||||
|
||||
int32_t sr = 0, channels = 0, bits = 0;
|
||||
long dataOff = -1, dataLen = 0, o = 12;
|
||||
/* Chunk-walk rather than assuming fmt-then-data at fixed offsets: recorders
|
||||
* routinely interleave JUNK/FLLR padding, and a fixed-offset parser reads
|
||||
* padding as audio. */
|
||||
while (o + 8 <= size) {
|
||||
long sz = (long)d[o+4] | ((long)d[o+5] << 8) | ((long)d[o+6] << 16) | ((long)d[o+7] << 24);
|
||||
if (sz < 0) break;
|
||||
if (memcmp(d + o, "fmt ", 4) == 0 && o + 24 <= size) {
|
||||
channels = (int32_t)(d[o+10] | (d[o+11] << 8));
|
||||
sr = (int32_t)((long)d[o+12] | ((long)d[o+13] << 8) | ((long)d[o+14] << 16) | ((long)d[o+15] << 24));
|
||||
bits = (int32_t)(d[o+22] | (d[o+23] << 8));
|
||||
} else if (memcmp(d + o, "data", 4) == 0) {
|
||||
dataOff = o + 8;
|
||||
dataLen = sz;
|
||||
if (dataOff + dataLen > size) dataLen = size - dataOff;
|
||||
}
|
||||
o += 8 + sz + (sz & 1);
|
||||
}
|
||||
if (dataOff < 0 || sr <= 0 || bits != 16 || channels < 1 || dataLen <= 0) { free(d); return 0; }
|
||||
|
||||
long frames = dataLen / (2 * channels);
|
||||
int16_t* pcm = (int16_t*)malloc((size_t)frames * sizeof(int16_t));
|
||||
if (!pcm) { free(d); return 0; }
|
||||
for (long i = 0; i < frames; i++) {
|
||||
long b = dataOff + i * 2 * channels;
|
||||
pcm[i] = (int16_t)((unsigned)d[b] | ((unsigned)d[b+1] << 8));
|
||||
}
|
||||
free(d);
|
||||
*out = pcm;
|
||||
if (out_sr) *out_sr = sr;
|
||||
return (int64_t)frames;
|
||||
}
|
||||
|
||||
/* Async WAV playback. converse speaks PRE-RENDERED segments and must keep
|
||||
* listening while it does, so it needs the file on the queue without blocking
|
||||
* and needs to be able to stop it mid-buffer. Going through the file rather
|
||||
* than an El [Int] also avoids marshalling a million-element list per segment
|
||||
* for audio the caller never intends to look at. */
|
||||
el_val_t speaker_play_wav_async(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
if (!p) return (el_val_t)0;
|
||||
|
||||
el_audio_teardown();
|
||||
|
||||
int32_t sr = 0;
|
||||
int16_t* pcm = NULL;
|
||||
int64_t frames = el_wav_load(p, &pcm, &sr);
|
||||
if (frames <= 0 || !pcm) { free(pcm); return (el_val_t)0; }
|
||||
|
||||
g_aq_pcm = pcm;
|
||||
g_aq_sr = sr;
|
||||
g_aq_state = (ElAqState*)calloc(1, sizeof(ElAqState));
|
||||
if (!g_aq_state) { el_audio_teardown(); return (el_val_t)0; }
|
||||
g_aq_state->pcm = g_aq_pcm;
|
||||
g_aq_state->frames = frames;
|
||||
|
||||
AudioStreamBasicDescription fmt;
|
||||
memset(&fmt, 0, sizeof(fmt));
|
||||
fmt.mSampleRate = (Float64)sr;
|
||||
fmt.mFormatID = kAudioFormatLinearPCM;
|
||||
fmt.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
fmt.mFramesPerPacket = 1;
|
||||
fmt.mChannelsPerFrame = 1;
|
||||
fmt.mBitsPerChannel = 16;
|
||||
fmt.mBytesPerFrame = 2;
|
||||
fmt.mBytesPerPacket = 2;
|
||||
|
||||
if (AudioQueueNewOutput(&fmt, el_aq_callback, g_aq_state, NULL, NULL, 0, &g_aq) != noErr || !g_aq) {
|
||||
el_audio_teardown();
|
||||
return (el_val_t)0;
|
||||
}
|
||||
for (int i = 0; i < EL_AQ_NBUF; i++) {
|
||||
int64_t remain = g_aq_state->frames - g_aq_state->pos;
|
||||
if (remain <= 0) break;
|
||||
AudioQueueBufferRef b = NULL;
|
||||
if (AudioQueueAllocateBuffer(g_aq, EL_AQ_FRAMES * sizeof(int16_t), &b) != noErr) break;
|
||||
int64_t k = remain < EL_AQ_FRAMES ? remain : EL_AQ_FRAMES;
|
||||
memcpy(b->mAudioData, g_aq_state->pcm + g_aq_state->pos, (size_t)k * sizeof(int16_t));
|
||||
b->mAudioDataByteSize = (UInt32)(k * (int64_t)sizeof(int16_t));
|
||||
g_aq_state->pos += k;
|
||||
if (AudioQueueEnqueueBuffer(g_aq, b, 0, NULL) != noErr) break;
|
||||
g_aq_state->inflight++;
|
||||
}
|
||||
if (g_aq_state->inflight == 0) { el_audio_teardown(); return (el_val_t)0; }
|
||||
if (AudioQueueStart(g_aq, NULL) != noErr) { el_audio_teardown(); return (el_val_t)0; }
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* Total frames and sample rate of a WAV, without playing it — wav-info, and the
|
||||
* duration converse needs to compute progress through a segment. */
|
||||
el_val_t wav_frames(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
int16_t* pcm = NULL; int32_t sr = 0;
|
||||
int64_t n = el_wav_load(p, &pcm, &sr);
|
||||
free(pcm);
|
||||
return (el_val_t)n;
|
||||
}
|
||||
|
||||
el_val_t wav_rate(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
int16_t* pcm = NULL; int32_t sr = 0;
|
||||
int64_t n = el_wav_load(p, &pcm, &sr);
|
||||
free(pcm);
|
||||
return (el_val_t)(n > 0 ? sr : 0);
|
||||
}
|
||||
|
||||
/* Play a 16-bit mono RIFF/WAVE file. Present because the render already knows
|
||||
* how to write a WAV and a caller may reasonably want to hear one back without
|
||||
* re-rendering it; the parse is deliberately minimal and chunk-walking, so the
|
||||
* JUNK/FLLR padding that recorders emit does not defeat it. */
|
||||
el_val_t speaker_play_wav(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
if (!p) return (el_val_t)0;
|
||||
FILE* f = fopen(p, "rb");
|
||||
if (!f) return (el_val_t)0;
|
||||
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return (el_val_t)0; }
|
||||
long size = ftell(f);
|
||||
if (size <= 44) { fclose(f); return (el_val_t)0; }
|
||||
rewind(f);
|
||||
|
||||
unsigned char* d = (unsigned char*)malloc((size_t)size);
|
||||
if (!d) { fclose(f); return (el_val_t)0; }
|
||||
size_t got = fread(d, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
if (got != (size_t)size) { free(d); return (el_val_t)0; }
|
||||
|
||||
if (memcmp(d, "RIFF", 4) != 0 || memcmp(d + 8, "WAVE", 4) != 0) { free(d); return (el_val_t)0; }
|
||||
|
||||
int32_t sr = 0, channels = 0, bits = 0;
|
||||
long dataOff = -1, dataLen = 0;
|
||||
long o = 12;
|
||||
while (o + 8 <= size) {
|
||||
long sz = (long)d[o+4] | ((long)d[o+5] << 8) | ((long)d[o+6] << 16) | ((long)d[o+7] << 24);
|
||||
if (sz < 0) break;
|
||||
if (memcmp(d + o, "fmt ", 4) == 0 && o + 24 <= size) {
|
||||
channels = (int32_t)(d[o+10] | (d[o+11] << 8));
|
||||
sr = (int32_t)((long)d[o+12] | ((long)d[o+13] << 8) | ((long)d[o+14] << 16) | ((long)d[o+15] << 24));
|
||||
bits = (int32_t)(d[o+22] | (d[o+23] << 8));
|
||||
} else if (memcmp(d + o, "data", 4) == 0) {
|
||||
dataOff = o + 8;
|
||||
dataLen = sz;
|
||||
if (dataOff + dataLen > size) dataLen = size - dataOff;
|
||||
}
|
||||
o += 8 + sz + (sz & 1);
|
||||
}
|
||||
if (dataOff < 0 || sr <= 0 || bits != 16 || channels < 1 || dataLen <= 0) { free(d); return (el_val_t)0; }
|
||||
|
||||
long frames = dataLen / (2 * channels);
|
||||
int16_t* pcm = (int16_t*)malloc((size_t)frames * sizeof(int16_t));
|
||||
if (!pcm) { free(d); return (el_val_t)0; }
|
||||
/* Take channel 0; the organ is mono by design and downmixing would be an
|
||||
* opinion about content this layer is not entitled to have. */
|
||||
for (long i = 0; i < frames; i++) {
|
||||
long b = dataOff + i * 2 * channels;
|
||||
pcm[i] = (int16_t)((unsigned)d[b] | ((unsigned)d[b+1] << 8));
|
||||
}
|
||||
free(d);
|
||||
|
||||
int ok = el_audio_play_raw(pcm, frames, sr);
|
||||
free(pcm);
|
||||
return (el_val_t)(ok ? 1 : 0);
|
||||
}
|
||||
@@ -0,0 +1,841 @@
|
||||
/* el_capture_darwin.m — the MICROPHONE and CAMERA realizers. El's afferent
|
||||
* organ on Darwin: the two entry points through which the world gets in.
|
||||
*
|
||||
* WHY THIS FILE EXISTS, AND WHY IT IS A REALIZER RATHER THAN PURE EL.
|
||||
*
|
||||
* el_audio_darwin.m argued the efferent half of this: El can compute a sound
|
||||
* but it cannot make one, because "the DAC has now played these samples" is not
|
||||
* a fact any amount of arithmetic can produce. This file is the same argument
|
||||
* run backwards. El can compute *about* a sound — it can window it, take its
|
||||
* autocorrelation, run Levinson-Durbin over that, find the formant peaks in the
|
||||
* resulting all-pole envelope, and hand back a voiceprint — but it cannot ASK.
|
||||
* There is no expression in El, and there must not be, whose value is "the next
|
||||
* 1024 frames the microphone hears" or "what the camera is pointed at right
|
||||
* now." Those are not computed; they are *requested*, from an operating system
|
||||
* that owns the device, mediates consent for it, and delivers the answer on a
|
||||
* thread of its choosing whenever it feels like it. Asking is the one primitive
|
||||
* operation here. Everything else in this file is bookkeeping around the ask.
|
||||
*
|
||||
* So the line is drawn exactly where el_audio_darwin.m drew it, at the sample
|
||||
* buffer, and it is drawn on purpose:
|
||||
*
|
||||
* BELOW the line (here): open the device, honour the OS permission gate,
|
||||
* install a tap or a frame delegate, convert whatever the hardware happens to
|
||||
* emit into the one shape El asked for, and hand it up. No opinions about
|
||||
* content. No analysis. No decisions.
|
||||
*
|
||||
* ABOVE the line (El): energy, zero-crossing rate, spectral centroid, F0 by
|
||||
* autocorrelation, LPC, formants F1-F5, the compact descriptors, the
|
||||
* scene-geometry grid, the yield-or-hold turn-taking decision. All of it is
|
||||
* arithmetic over a buffer, all of it belongs in El, and none of it appears
|
||||
* below. The reference this file ports — peripheral/src/periph.swift — held
|
||||
* both halves, and that was the problem worth fixing: the descriptors were
|
||||
* trapped in a 939-line binary standing next to the language instead of being
|
||||
* written in it. Porting the *whole* of periph.swift down here would have
|
||||
* reproduced that mistake in C. Only the ask came down.
|
||||
*
|
||||
* The precedent for the file's SHAPE is eg_cosine_batch_strategy_metal_hand.m:
|
||||
* a platform-bound capability compiled as its own translation unit, declared in
|
||||
* el_runtime.h, linked in where the platform supports it, and deliberately NOT
|
||||
* a patch to the middle of el_runtime.c. Acquiring a device must not mean
|
||||
* editing the language, for the same reason acquiring a modality must not (see
|
||||
* the realizer registry: organs are resolved by name). el_peripheral_null.c is
|
||||
* the same entry points everywhere else, so El code that listens still links on
|
||||
* every platform and merely reports having no ear.
|
||||
*
|
||||
* FAIL CLOSED, ALWAYS.
|
||||
*
|
||||
* A capture path that returns plausible-looking zeros when it was denied is
|
||||
* worse than one that returns nothing, because the caller cannot tell the
|
||||
* difference between a silent room and a refused microphone. Every entry point
|
||||
* here checks AVCaptureDevice's authorization status BEFORE touching hardware
|
||||
* and returns the empty value — an empty list, a 0 map — on anything short of
|
||||
* .authorized. mic_available() and camera_available() report that state WITHOUT
|
||||
* prompting, so El can ask "may I?" without the act of asking being a prompt.
|
||||
*
|
||||
* NEVER HANG.
|
||||
*
|
||||
* Every wait in this file is bounded: 30s on a permission prompt (the user has
|
||||
* to walk to the dialog), seconds+5 on a capture of `seconds`, 10s on a camera
|
||||
* frame. An organ that wedges the program holding it is not an organ, it is a
|
||||
* fault. Every path also tears the device down on the way out, including the
|
||||
* failure paths, so a timed-out capture does not leave the mic light on.
|
||||
*
|
||||
* OWN-CORE AND LOCAL BY CONSTRUCTION.
|
||||
*
|
||||
* AVFoundation, CoreVideo, CoreGraphics and ImageIO ship with macOS. There is
|
||||
* no third-party library here, no model, and — the part that matters — no
|
||||
* network path of any kind. Samples and pixels move from local hardware into an
|
||||
* El value and stop. periph.swift had a URLSession in it; this file has no
|
||||
* socket, no URL, and nothing that could grow one without being obvious in
|
||||
* review. Consent is enforced above this layer in El and below it by the OS;
|
||||
* this layer's whole contribution to that is refusing to proceed.
|
||||
*
|
||||
* DISCLOSURE.
|
||||
*
|
||||
* Every actual device touch writes one line to stderr and flushes it, before
|
||||
* the device opens. stderr and not stdout: a program that announces "I am about
|
||||
* to open the microphone" on stdout has corrupted its own output, and the
|
||||
* caller must be able to separate the answer from how it was obtained. One line
|
||||
* per touch, no more — a disclosure rail that spams is a rail people learn to
|
||||
* ignore.
|
||||
*/
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "el_runtime.h"
|
||||
|
||||
/* ── Disclosure ──────────────────────────────────────────────────────────────
|
||||
* One flushed line per real device touch, on stderr. Flushed rather than
|
||||
* buffered so the line reaches the terminal BEFORE the mic light comes on
|
||||
* rather than whenever the buffer happens to drain. */
|
||||
static void el_cap_disclose(const char* what) {
|
||||
fprintf(stderr, " [peripheral] %s\n", what);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
/* ── Permission ──────────────────────────────────────────────────────────────
|
||||
* authorizationStatus is a pure read of the TCC database: it never prompts and
|
||||
* never blocks, which is what lets mic_available()/camera_available() answer
|
||||
* honestly without the question itself becoming an event. requestAccess DOES
|
||||
* prompt, so it lives behind its own entry point and nothing calls it
|
||||
* implicitly. */
|
||||
|
||||
static int el_cap_authorized(AVMediaType media) {
|
||||
@try {
|
||||
return [AVCaptureDevice authorizationStatusForMediaType:media]
|
||||
== AVAuthorizationStatusAuthorized ? 1 : 0;
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int el_cap_device_present(AVMediaType media) {
|
||||
@try {
|
||||
return [AVCaptureDevice defaultDeviceWithMediaType:media] != nil ? 1 : 0;
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prompt once and wait, bounded. 30 seconds is the same budget periph.swift
|
||||
* used: long enough for a human to notice a dialog and decide, short enough
|
||||
* that an unattended run fails rather than parks forever. A timeout is reported
|
||||
* as "not granted", which is the safe reading — we genuinely do not know that
|
||||
* it was. */
|
||||
static int el_cap_request(AVMediaType media) {
|
||||
__block int granted = 0;
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
@try {
|
||||
[AVCaptureDevice requestAccessForMediaType:media
|
||||
completionHandler:^(BOOL ok) {
|
||||
granted = ok ? 1 : 0;
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return 0;
|
||||
}
|
||||
if (dispatch_semaphore_wait(sem,
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC))) != 0) {
|
||||
return 0; /* timed out — treat as refused */
|
||||
}
|
||||
return granted;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* MICROPHONE — one-shot capture
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* The sink the tap block writes into. An object rather than a static so two
|
||||
* captures can never share state, and so ARC keeps it alive for exactly as long
|
||||
* as the block that captured it. The lock is real, not decorative: the tap runs
|
||||
* on an AVAudioEngine-internal thread and the waiter runs on the caller's. */
|
||||
@interface ElCapMicSink : NSObject
|
||||
@property (nonatomic, strong) NSMutableData* pcm;
|
||||
@property (nonatomic, strong) NSLock* lock;
|
||||
@end
|
||||
|
||||
@implementation ElCapMicSink
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_pcm = [NSMutableData data];
|
||||
_lock = [[NSLock alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
||||
/* Capture `seconds` of mono 16-bit PCM at `sample_rate`.
|
||||
*
|
||||
* The hardware format is NOT assumed. A built-in mic will typically hand back
|
||||
* float32 at 44.1 or 48 kHz, an aggregate device may be 8 channels at 96 kHz,
|
||||
* and a caller asking for 16 kHz mono (which is what the formant path wants)
|
||||
* gets 16 kHz mono either way. AVAudioConverter does the rate conversion and
|
||||
* the downmix; doing it by hand would mean writing a resampler in the one file
|
||||
* that is supposed to contain no arithmetic.
|
||||
*
|
||||
* The converter is built ONCE, outside the tap, because a sample-rate converter
|
||||
* carries filter state across buffers — rebuilding it per callback would put a
|
||||
* discontinuity at every buffer boundary, which is audible and which would then
|
||||
* show up in El's spectral descriptors as energy that was never in the room. */
|
||||
el_val_t mic_capture_pcm16(el_val_t seconds, el_val_t sample_rate) {
|
||||
el_val_t empty = el_list_empty();
|
||||
|
||||
if (!el_cap_authorized(AVMediaTypeAudio)) return empty;
|
||||
|
||||
int64_t secs = (int64_t)seconds;
|
||||
int64_t sr = (int64_t)sample_rate;
|
||||
if (secs <= 0 || sr <= 0) return empty;
|
||||
/* Bound the ask. A caller that asks for a year of audio has made a mistake,
|
||||
* and honouring it would mean an unkillable capture and an OOM. */
|
||||
if (secs > 300) secs = 300;
|
||||
if (sr > 384000) sr = 384000;
|
||||
|
||||
__block AVAudioEngine* engine = nil;
|
||||
AVAudioInputNode* input = nil;
|
||||
AVAudioFormat* hwFmt = nil;
|
||||
int tapped = 0;
|
||||
|
||||
@try {
|
||||
engine = [[AVAudioEngine alloc] init];
|
||||
input = [engine inputNode];
|
||||
hwFmt = [input inputFormatForBus:0];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return empty;
|
||||
}
|
||||
if (!input || !hwFmt || hwFmt.sampleRate <= 0 || hwFmt.channelCount == 0) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
/* Preferred target: mono int16 at the requested rate. If the converter
|
||||
* refuses that pairing (some exotic input layouts will not downmix), fall
|
||||
* back to keeping the hardware's channel count and taking channel 0 on the
|
||||
* way out — the organ is mono by design and inventing a downmix here would
|
||||
* be an opinion about content this layer is not entitled to have. */
|
||||
AVAudioFormat* outFmt =
|
||||
[[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16
|
||||
sampleRate:(double)sr
|
||||
channels:1
|
||||
interleaved:YES];
|
||||
AVAudioConverter* conv = outFmt ? [[AVAudioConverter alloc] initFromFormat:hwFmt
|
||||
toFormat:outFmt] : nil;
|
||||
AVAudioChannelCount outCh = 1;
|
||||
if (!conv) {
|
||||
outFmt = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16
|
||||
sampleRate:(double)sr
|
||||
channels:hwFmt.channelCount
|
||||
interleaved:YES];
|
||||
conv = outFmt ? [[AVAudioConverter alloc] initFromFormat:hwFmt toFormat:outFmt] : nil;
|
||||
outCh = hwFmt.channelCount;
|
||||
}
|
||||
if (!conv || !outFmt) return empty;
|
||||
|
||||
ElCapMicSink* sink = [[ElCapMicSink alloc] init];
|
||||
const int64_t want = secs * sr; /* frames we are waiting for */
|
||||
|
||||
{
|
||||
char msg[192];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"MIC: opening the microphone for %llds -> %lld Hz mono PCM "
|
||||
"(local, never egresses).", (long long)secs, (long long)sr);
|
||||
el_cap_disclose(msg);
|
||||
}
|
||||
|
||||
const double ratio = (double)sr / hwFmt.sampleRate;
|
||||
|
||||
@try {
|
||||
[input installTapOnBus:0
|
||||
bufferSize:4096
|
||||
format:hwFmt
|
||||
block:^(AVAudioPCMBuffer* _Nonnull buf, AVAudioTime* _Nonnull when) {
|
||||
(void)when;
|
||||
if (!buf || buf.frameLength == 0) return;
|
||||
|
||||
AVAudioFrameCount cap =
|
||||
(AVAudioFrameCount)((double)buf.frameLength * ratio) + 1024;
|
||||
AVAudioPCMBuffer* out =
|
||||
[[AVAudioPCMBuffer alloc] initWithPCMFormat:outFmt frameCapacity:cap];
|
||||
if (!out) return;
|
||||
|
||||
__block BOOL fed = NO;
|
||||
AVAudioConverterInputBlock feed =
|
||||
^AVAudioBuffer* _Nullable (AVAudioPacketCount need,
|
||||
AVAudioConverterInputStatus* _Nonnull status) {
|
||||
(void)need;
|
||||
if (fed) { *status = AVAudioConverterInputStatus_NoDataNow; return nil; }
|
||||
fed = YES;
|
||||
*status = AVAudioConverterInputStatus_HaveData;
|
||||
return buf;
|
||||
};
|
||||
|
||||
NSError* err = nil;
|
||||
AVAudioConverterOutputStatus st =
|
||||
[conv convertToBuffer:out error:&err withInputFromBlock:feed];
|
||||
if (st == AVAudioConverterOutputStatus_Error || out.frameLength == 0) return;
|
||||
|
||||
const int16_t* src = out.int16ChannelData ? out.int16ChannelData[0] : NULL;
|
||||
if (!src) return;
|
||||
|
||||
NSUInteger n = (NSUInteger)out.frameLength;
|
||||
[sink.lock lock];
|
||||
if (outCh == 1) {
|
||||
[sink.pcm appendBytes:src length:n * sizeof(int16_t)];
|
||||
} else {
|
||||
/* Interleaved: stride to channel 0. */
|
||||
for (NSUInteger i = 0; i < n; i++) {
|
||||
int16_t v = src[i * outCh];
|
||||
[sink.pcm appendBytes:&v length:sizeof(int16_t)];
|
||||
}
|
||||
}
|
||||
[sink.lock unlock];
|
||||
}];
|
||||
tapped = 1;
|
||||
|
||||
[engine prepare];
|
||||
NSError* startErr = nil;
|
||||
if (![engine startAndReturnError:&startErr]) {
|
||||
[input removeTapOnBus:0];
|
||||
return empty;
|
||||
}
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
@try { if (tapped) [input removeTapOnBus:0]; } @catch (NSException* e2) { (void)e2; }
|
||||
@try { [engine stop]; } @catch (NSException* e2) { (void)e2; }
|
||||
return empty;
|
||||
}
|
||||
|
||||
/* Wait for `want` frames, bounded by the material's own duration plus a
|
||||
* margin. A device that stops producing must not become a hang. */
|
||||
const int64_t deadline_us = (secs + 5) * 1000000;
|
||||
int64_t waited_us = 0;
|
||||
const int64_t tick_us = 5000;
|
||||
for (;;) {
|
||||
[sink.lock lock];
|
||||
int64_t have = (int64_t)([sink.pcm length] / sizeof(int16_t));
|
||||
[sink.lock unlock];
|
||||
if (have >= want || waited_us >= deadline_us) break;
|
||||
usleep((useconds_t)tick_us);
|
||||
waited_us += tick_us;
|
||||
}
|
||||
|
||||
@try { [input removeTapOnBus:0]; } @catch (NSException* e) { (void)e; }
|
||||
@try { [engine stop]; } @catch (NSException* e) { (void)e; }
|
||||
engine = nil;
|
||||
|
||||
/* Hand up exactly what was asked for, or everything we got if the device
|
||||
* came up short. Never padded: silence we invented is indistinguishable
|
||||
* from silence we heard, and El has no way to tell them apart afterwards. */
|
||||
[sink.lock lock];
|
||||
int64_t have = (int64_t)([sink.pcm length] / sizeof(int16_t));
|
||||
int64_t n = have < want ? have : want;
|
||||
const int16_t* pcm = (const int16_t*)[sink.pcm bytes];
|
||||
el_val_t list = empty;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
list = el_list_append(list, (el_val_t)(int64_t)pcm[i]);
|
||||
}
|
||||
[sink.lock unlock];
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
el_val_t mic_available(void) {
|
||||
if (!el_cap_authorized(AVMediaTypeAudio)) return (el_val_t)0;
|
||||
return (el_val_t)(el_cap_device_present(AVMediaTypeAudio) ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t mic_request_access(void) {
|
||||
return (el_val_t)(el_cap_request(AVMediaTypeAudio) ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* MICROPHONE — live monitor (the full-duplex ear)
|
||||
*
|
||||
* converse needs to keep listening WHILE it speaks, which means the mic is open
|
||||
* at the same time as the speaker. In a real room that is a feedback path:
|
||||
* without cancellation Neuron hears its own voice, decides someone is talking,
|
||||
* and barges in on itself. setVoiceProcessingEnabled: hands the input node to
|
||||
* the OS voice-processing unit, which subtracts the known output signal from
|
||||
* the input — the single thing that makes barge-in work outside a headset.
|
||||
*
|
||||
* It is not always available (some aggregate and virtual devices refuse it), so
|
||||
* failure to enable it is reported as a DISTINCT return value (2) rather than
|
||||
* folded into success. The caller needs to know, because the correct response
|
||||
* is to raise the VAD floor, and a caller that thinks AEC is on will set that
|
||||
* floor far too low.
|
||||
*
|
||||
* The tap keeps only a running short-window RMS in a static behind a mutex.
|
||||
* Deliberately not a queue of samples: this path is polled at ~50 Hz by a loop
|
||||
* that only ever asks "is someone talking", and buffering audio nobody reads
|
||||
* would be an unbounded allocation in the middle of a conversation.
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static AVAudioEngine* g_mon_engine = nil;
|
||||
static int g_mon_running = 0;
|
||||
static int g_mon_code = 0; /* what the successful start reported */
|
||||
static double g_mon_rms = 0.0;
|
||||
static pthread_mutex_t g_mon_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
el_val_t mic_monitor_start(void) {
|
||||
/* Idempotent, and it re-reports the ORIGINAL code rather than a bare 1: a
|
||||
* caller that starts twice must not be told AEC is on when the first start
|
||||
* already discovered it was not. */
|
||||
if (g_mon_running) return (el_val_t)g_mon_code;
|
||||
if (!el_cap_authorized(AVMediaTypeAudio)) return (el_val_t)0;
|
||||
|
||||
AVAudioEngine* engine = nil;
|
||||
AVAudioInputNode* input = nil;
|
||||
AVAudioFormat* fmt = nil;
|
||||
int aec = 0;
|
||||
int tapped = 0;
|
||||
|
||||
@try {
|
||||
engine = [[AVAudioEngine alloc] init];
|
||||
input = [engine inputNode];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
if (!input) return (el_val_t)0;
|
||||
|
||||
/* Enable AEC BEFORE reading the format: the voice-processing unit imposes
|
||||
* its own input format, and a tap installed with the pre-VP format would be
|
||||
* rejected at start. */
|
||||
@try {
|
||||
NSError* vpErr = nil;
|
||||
if ([input respondsToSelector:@selector(setVoiceProcessingEnabled:error:)]) {
|
||||
aec = [input setVoiceProcessingEnabled:YES error:&vpErr] ? 1 : 0;
|
||||
}
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
aec = 0;
|
||||
}
|
||||
|
||||
@try {
|
||||
fmt = [input inputFormatForBus:0];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
if (!fmt || fmt.sampleRate <= 0 || fmt.channelCount == 0) return (el_val_t)0;
|
||||
|
||||
el_cap_disclose(aec
|
||||
? "MIC: opening the microphone for live monitoring, echo-cancelled (local)."
|
||||
: "MIC: opening the microphone for live monitoring, NO echo cancellation (local).");
|
||||
|
||||
@try {
|
||||
[input installTapOnBus:0
|
||||
bufferSize:1024
|
||||
format:fmt
|
||||
block:^(AVAudioPCMBuffer* _Nonnull buf, AVAudioTime* _Nonnull when) {
|
||||
(void)when;
|
||||
if (!buf) return;
|
||||
AVAudioFrameCount n = buf.frameLength;
|
||||
if (n == 0) return;
|
||||
|
||||
double sum = 0.0;
|
||||
/* Whatever the VP unit hands back — float32 is the norm, int16 and
|
||||
* int32 are possible on odd hardware — normalise to -1..1 so the
|
||||
* Float El sees means the same thing on every device. */
|
||||
if (buf.floatChannelData) {
|
||||
const float* ch = buf.floatChannelData[0];
|
||||
for (AVAudioFrameCount i = 0; i < n; i++) sum += (double)ch[i] * (double)ch[i];
|
||||
} else if (buf.int16ChannelData) {
|
||||
const int16_t* ch = buf.int16ChannelData[0];
|
||||
for (AVAudioFrameCount i = 0; i < n; i++) {
|
||||
double v = (double)ch[i] / 32768.0;
|
||||
sum += v * v;
|
||||
}
|
||||
} else if (buf.int32ChannelData) {
|
||||
const int32_t* ch = buf.int32ChannelData[0];
|
||||
for (AVAudioFrameCount i = 0; i < n; i++) {
|
||||
double v = (double)ch[i] / 2147483648.0;
|
||||
sum += v * v;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
double rms = sqrt(sum / (double)n);
|
||||
if (rms < 0.0) rms = 0.0;
|
||||
if (rms > 1.0) rms = 1.0;
|
||||
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
g_mon_rms = rms;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
}];
|
||||
tapped = 1;
|
||||
|
||||
[engine prepare];
|
||||
NSError* startErr = nil;
|
||||
if (![engine startAndReturnError:&startErr]) {
|
||||
[input removeTapOnBus:0];
|
||||
return (el_val_t)0;
|
||||
}
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
@try { if (tapped) [input removeTapOnBus:0]; } @catch (NSException* e2) { (void)e2; }
|
||||
@try { [engine stop]; } @catch (NSException* e2) { (void)e2; }
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
g_mon_rms = 0.0;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
|
||||
g_mon_engine = engine;
|
||||
g_mon_running = 1;
|
||||
g_mon_code = aec ? 1 : 2;
|
||||
return (el_val_t)g_mon_code;
|
||||
}
|
||||
|
||||
/* Float in 0..1. Reads the last window the tap computed; never blocks on the
|
||||
* audio thread beyond the mutex, because this is polled inside a turn-taking
|
||||
* loop where a stall IS a missed barge-in. */
|
||||
el_val_t mic_monitor_rms(void) {
|
||||
double rms = 0.0;
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
rms = g_mon_rms;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
return el_from_float(rms);
|
||||
}
|
||||
|
||||
el_val_t mic_monitor_stop(void) {
|
||||
AVAudioEngine* engine = g_mon_engine;
|
||||
g_mon_engine = nil;
|
||||
g_mon_running = 0;
|
||||
g_mon_code = 0;
|
||||
|
||||
if (engine) {
|
||||
@try { [[engine inputNode] removeTapOnBus:0]; } @catch (NSException* e) { (void)e; }
|
||||
@try { [engine stop]; } @catch (NSException* e) { (void)e; }
|
||||
}
|
||||
pthread_mutex_lock(&g_mon_lock);
|
||||
g_mon_rms = 0.0;
|
||||
pthread_mutex_unlock(&g_mon_lock);
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
* CAMERA
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static void el_cap_free_bitmap(void* info, const void* data, size_t size) {
|
||||
(void)info; (void)size;
|
||||
free((void*)data);
|
||||
}
|
||||
|
||||
/* CVPixelBuffer -> CGImage, own-core, no CoreImage.
|
||||
*
|
||||
* The output is pinned to 32BGRA at the AVCaptureVideoDataOutput (see below)
|
||||
* precisely so this conversion can be a memcpy and a CGImageCreate. The
|
||||
* alternative — accepting the camera's native 2vuy/420v and colour-converting
|
||||
* here — would mean either pulling in CoreImage or writing a YUV->RGB matrix in
|
||||
* the file that is supposed to contain no arithmetic. Asking the capture output
|
||||
* for BGRA moves that work into AVFoundation, where it is already written and
|
||||
* already hardware-accelerated.
|
||||
*
|
||||
* The rows are copied out rather than aliased because the CVPixelBuffer is
|
||||
* recycled by the capture session the moment the delegate returns; a CGImage
|
||||
* pointing at it would be pointing at the NEXT frame by the time anyone looked. */
|
||||
static CGImageRef el_cap_cgimage_from_pixelbuffer(CVPixelBufferRef pb) {
|
||||
if (!pb) return NULL;
|
||||
if (CVPixelBufferGetPixelFormatType(pb) != kCVPixelFormatType_32BGRA) return NULL;
|
||||
if (CVPixelBufferLockBaseAddress(pb, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) return NULL;
|
||||
|
||||
size_t w = CVPixelBufferGetWidth(pb);
|
||||
size_t h = CVPixelBufferGetHeight(pb);
|
||||
size_t src_bpr = CVPixelBufferGetBytesPerRow(pb);
|
||||
const uint8_t* base = (const uint8_t*)CVPixelBufferGetBaseAddress(pb);
|
||||
|
||||
CGImageRef img = NULL;
|
||||
if (base && w > 0 && h > 0 && src_bpr >= w * 4) {
|
||||
size_t dst_bpr = w * 4;
|
||||
uint8_t* copy = (uint8_t*)malloc(dst_bpr * h);
|
||||
if (copy) {
|
||||
for (size_t y = 0; y < h; y++) {
|
||||
memcpy(copy + y * dst_bpr, base + y * src_bpr, dst_bpr);
|
||||
}
|
||||
CGDataProviderRef dp =
|
||||
CGDataProviderCreateWithData(NULL, copy, dst_bpr * h, el_cap_free_bitmap);
|
||||
if (dp) {
|
||||
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
|
||||
if (cs) {
|
||||
img = CGImageCreate(w, h, 8, 32, dst_bpr, cs,
|
||||
(CGBitmapInfo)(kCGBitmapByteOrder32Little |
|
||||
kCGImageAlphaNoneSkipFirst),
|
||||
dp, NULL, false, kCGRenderingIntentDefault);
|
||||
CGColorSpaceRelease(cs);
|
||||
}
|
||||
CGDataProviderRelease(dp); /* provider owns `copy` from here */
|
||||
} else {
|
||||
free(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly);
|
||||
return img;
|
||||
}
|
||||
|
||||
/* The frame delegate. AVCaptureVideoDataOutput is used rather than
|
||||
* AVCapturePhotoOutput for the same reason periph.swift used it: the photo path
|
||||
* wants KVO and a session owned by an app object, and this runs in a plain CLI
|
||||
* process with no run loop it can assume. A data output just calls back.
|
||||
*
|
||||
* The first frames are dropped on purpose. A camera that has just been powered
|
||||
* on is still converging exposure and white balance, and the first frame is
|
||||
* reliably darker and greener than the room. El's scene-geometry descriptors
|
||||
* are brightness and mean-colour statistics, so handing up an unsettled frame
|
||||
* would not produce a slightly worse answer, it would produce a confidently
|
||||
* wrong one. */
|
||||
@interface ElCapFrameGrabber : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate> {
|
||||
CGImageRef _img;
|
||||
int _seen;
|
||||
dispatch_semaphore_t _sem;
|
||||
}
|
||||
- (dispatch_semaphore_t)sem;
|
||||
- (CGImageRef)takeImage; /* transfers ownership to the caller */
|
||||
@end
|
||||
|
||||
@implementation ElCapFrameGrabber
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_img = NULL;
|
||||
_seen = 0;
|
||||
_sem = dispatch_semaphore_create(0);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (dispatch_semaphore_t)sem { return _sem; }
|
||||
|
||||
- (CGImageRef)takeImage {
|
||||
CGImageRef out = _img;
|
||||
_img = NULL;
|
||||
return out;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_img) { CGImageRelease(_img); _img = NULL; }
|
||||
}
|
||||
|
||||
/* Runs on the serial delegate queue, so no lock is needed among callbacks; the
|
||||
* waiter only reads _img after the semaphore has been signalled AND the session
|
||||
* has been stopped, which orders it after the last callback. */
|
||||
- (void)captureOutput:(AVCaptureOutput*)output
|
||||
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
fromConnection:(AVCaptureConnection*)connection {
|
||||
(void)output; (void)connection;
|
||||
_seen++;
|
||||
if (_img != NULL || _seen < 5) return; /* let exposure settle */
|
||||
|
||||
CVImageBufferRef pb = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||
if (!pb) return;
|
||||
CGImageRef img = el_cap_cgimage_from_pixelbuffer(pb);
|
||||
if (!img) return;
|
||||
_img = img;
|
||||
dispatch_semaphore_signal(_sem);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/* Bring the camera up, take exactly one settled frame, put it back down.
|
||||
* Returns a +1 CGImageRef the caller releases, or NULL. Bounded at 10s: a
|
||||
* camera held by another process, or one whose TCC grant was revoked between
|
||||
* the check and the open, must fail rather than park. */
|
||||
static CGImageRef el_cap_grab_frame(void) {
|
||||
AVCaptureSession* session = nil;
|
||||
AVCaptureVideoDataOutput* output = nil;
|
||||
ElCapFrameGrabber* grabber = nil;
|
||||
CGImageRef img = NULL;
|
||||
|
||||
@try {
|
||||
AVCaptureDevice* dev = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
if (!dev) return NULL;
|
||||
|
||||
NSError* err = nil;
|
||||
AVCaptureDeviceInput* in = [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err];
|
||||
if (!in) return NULL;
|
||||
|
||||
session = [[AVCaptureSession alloc] init];
|
||||
session.sessionPreset = AVCaptureSessionPresetPhoto;
|
||||
if (![session canAddInput:in]) return NULL;
|
||||
[session addInput:in];
|
||||
|
||||
output = [[AVCaptureVideoDataOutput alloc] init];
|
||||
output.alwaysDiscardsLateVideoFrames = YES;
|
||||
/* Pin the pixel format so the CGImage conversion above stays a memcpy.
|
||||
* Every macOS capture device advertises 32BGRA. */
|
||||
output.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey :
|
||||
@(kCVPixelFormatType_32BGRA) };
|
||||
|
||||
grabber = [[ElCapFrameGrabber alloc] init];
|
||||
dispatch_queue_t q = dispatch_queue_create("el.capture.camera", DISPATCH_QUEUE_SERIAL);
|
||||
[output setSampleBufferDelegate:grabber queue:q];
|
||||
|
||||
if (![session canAddOutput:output]) return NULL;
|
||||
[session addOutput:output];
|
||||
|
||||
el_cap_disclose("CAMERA: opening the camera for one frame (local, never egresses).");
|
||||
[session startRunning];
|
||||
} @catch (NSException* e) {
|
||||
(void)e;
|
||||
@try { [session stopRunning]; } @catch (NSException* e2) { (void)e2; }
|
||||
return NULL;
|
||||
}
|
||||
|
||||
long timed_out = dispatch_semaphore_wait([grabber sem],
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)));
|
||||
|
||||
/* Stop first, then detach the delegate, then read. In that order the last
|
||||
* callback has already returned by the time anyone touches the image. */
|
||||
@try { [session stopRunning]; } @catch (NSException* e) { (void)e; }
|
||||
@try { [output setSampleBufferDelegate:nil queue:NULL]; } @catch (NSException* e) { (void)e; }
|
||||
|
||||
if (timed_out == 0) img = [grabber takeImage];
|
||||
return img;
|
||||
}
|
||||
|
||||
el_val_t camera_available(void) {
|
||||
if (!el_cap_authorized(AVMediaTypeVideo)) return (el_val_t)0;
|
||||
return (el_val_t)(el_cap_device_present(AVMediaTypeVideo) ? 1 : 0);
|
||||
}
|
||||
|
||||
el_val_t camera_request_access(void) {
|
||||
return (el_val_t)(el_cap_request(AVMediaTypeVideo) ? 1 : 0);
|
||||
}
|
||||
|
||||
/* Longest edge of the grid handed to El. 64 is not a resolution, it is a budget:
|
||||
* a 1920x1080 frame is 6.2 MILLION packed RGB ints, and building that as an El
|
||||
* list would cost more time and memory than everything El then does with it.
|
||||
* The descriptors El computes over this — mean colour, brightness, a 3x3
|
||||
* luminance grid — are region statistics, and region statistics do not get
|
||||
* meaningfully better above a 64-wide grid. The TRUE frame dimensions are
|
||||
* reported separately so nothing downstream has to guess what was thrown away. */
|
||||
#define EL_CAP_GRID_MAX 64
|
||||
|
||||
/* One frame as Map{width, height, grid_w, grid_h, pixels:[Int]}.
|
||||
*
|
||||
* "pixels" is packed R,G,B with NO alpha — three ints per grid cell, row-major
|
||||
* from the TOP-LEFT. (CGBitmapContext lays its buffer out top row first and
|
||||
* CGContextDrawImage does the flip, so row 0 here is the top of the frame, the
|
||||
* same convention periph.swift's grid indexing assumed.) Alpha is dropped
|
||||
* because a camera frame has none worth carrying and it would inflate the list
|
||||
* by a third to say "opaque" six thousand times. */
|
||||
el_val_t camera_capture_rgb(void) {
|
||||
if (!el_cap_authorized(AVMediaTypeVideo)) return (el_val_t)0;
|
||||
|
||||
CGImageRef img = el_cap_grab_frame();
|
||||
if (!img) return (el_val_t)0;
|
||||
|
||||
size_t w = CGImageGetWidth(img);
|
||||
size_t h = CGImageGetHeight(img);
|
||||
if (w == 0 || h == 0) { CGImageRelease(img); return (el_val_t)0; }
|
||||
|
||||
/* Preserve aspect ratio, longest edge capped. */
|
||||
size_t gw = w, gh = h;
|
||||
size_t longest = w > h ? w : h;
|
||||
if (longest > EL_CAP_GRID_MAX) {
|
||||
double s = (double)EL_CAP_GRID_MAX / (double)longest;
|
||||
gw = (size_t)((double)w * s + 0.5);
|
||||
gh = (size_t)((double)h * s + 0.5);
|
||||
if (gw == 0) gw = 1;
|
||||
if (gh == 0) gh = 1;
|
||||
}
|
||||
|
||||
size_t bpr = gw * 4;
|
||||
uint8_t* buf = (uint8_t*)calloc(1, bpr * gh);
|
||||
if (!buf) { CGImageRelease(img); return (el_val_t)0; }
|
||||
|
||||
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
|
||||
CGContextRef ctx = cs ? CGBitmapContextCreate(buf, gw, gh, 8, bpr, cs,
|
||||
(CGBitmapInfo)kCGImageAlphaPremultipliedLast)
|
||||
: NULL;
|
||||
if (cs) CGColorSpaceRelease(cs);
|
||||
if (!ctx) { free(buf); CGImageRelease(img); return (el_val_t)0; }
|
||||
|
||||
/* Nearest-neighbour. This is a decimation for statistics, not a thumbnail
|
||||
* for a human to look at; smoothing would only cost time and blur the very
|
||||
* region boundaries the grid exists to measure. */
|
||||
CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);
|
||||
CGContextDrawImage(ctx, CGRectMake(0, 0, (CGFloat)gw, (CGFloat)gh), img);
|
||||
CGContextRelease(ctx);
|
||||
CGImageRelease(img);
|
||||
|
||||
el_val_t pixels = el_list_empty();
|
||||
for (size_t y = 0; y < gh; y++) {
|
||||
const uint8_t* row = buf + y * bpr;
|
||||
for (size_t x = 0; x < gw; x++) {
|
||||
const uint8_t* p = row + x * 4; /* RGBA8, premultiplied-last */
|
||||
pixels = el_list_append(pixels, (el_val_t)(int64_t)p[0]);
|
||||
pixels = el_list_append(pixels, (el_val_t)(int64_t)p[1]);
|
||||
pixels = el_list_append(pixels, (el_val_t)(int64_t)p[2]);
|
||||
}
|
||||
}
|
||||
free(buf);
|
||||
|
||||
el_val_t m = el_map_new((el_val_t)0);
|
||||
if (!m) return (el_val_t)0;
|
||||
m = el_map_set(m, EL_STR("width"), (el_val_t)(int64_t)w);
|
||||
m = el_map_set(m, EL_STR("height"), (el_val_t)(int64_t)h);
|
||||
m = el_map_set(m, EL_STR("grid_w"), (el_val_t)(int64_t)gw);
|
||||
m = el_map_set(m, EL_STR("grid_h"), (el_val_t)(int64_t)gh);
|
||||
m = el_map_set(m, EL_STR("pixels"), pixels);
|
||||
return m;
|
||||
}
|
||||
|
||||
/* One frame to disk as JPEG, at FULL resolution — the opposite budget from
|
||||
* camera_capture_rgb, and for the opposite reason. A file is not being walked
|
||||
* element-by-element by an interpreter; it costs one ImageIO call and it is the
|
||||
* artefact a human or a later pass will actually look at. The encoder is
|
||||
* ImageIO's because a JPEG encoder is a codec, and re-implementing one in El
|
||||
* would be a large amount of arithmetic that buys nothing: the point of keeping
|
||||
* work in El is the reasoning, not the entropy coding. */
|
||||
el_val_t camera_capture_jpeg(el_val_t path) {
|
||||
const char* p = EL_CSTR(path);
|
||||
if (!p || !*p) return (el_val_t)0;
|
||||
if (!el_cap_authorized(AVMediaTypeVideo)) return (el_val_t)0;
|
||||
|
||||
CGImageRef img = el_cap_grab_frame();
|
||||
if (!img) return (el_val_t)0;
|
||||
|
||||
int ok = 0;
|
||||
@autoreleasepool {
|
||||
NSString* ns = [NSString stringWithUTF8String:p];
|
||||
NSURL* url = ns ? [NSURL fileURLWithPath:ns] : nil;
|
||||
if (url) {
|
||||
CGImageDestinationRef dst =
|
||||
CGImageDestinationCreateWithURL((__bridge CFURLRef)url, CFSTR("public.jpeg"), 1, NULL);
|
||||
if (dst) {
|
||||
CGImageDestinationAddImage(dst, img, NULL);
|
||||
ok = CGImageDestinationFinalize(dst) ? 1 : 0;
|
||||
CFRelease(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
CGImageRelease(img);
|
||||
return (el_val_t)ok;
|
||||
}
|
||||
|
||||
#endif /* __APPLE__ */
|
||||
@@ -0,0 +1,89 @@
|
||||
/* el_peripheral_null.c — the no-device build of El's I/O organ.
|
||||
*
|
||||
* Every entry point declared in el_runtime.h's "Peripheral" block, implemented
|
||||
* as an honest refusal. This is what a platform without an El audio/capture
|
||||
* realizer links instead of el_audio_darwin.m + el_capture_darwin.m, so an El
|
||||
* program that speaks or listens still COMPILES AND LINKS everywhere.
|
||||
*
|
||||
* The distinction that matters: these do not pretend. speaker_available() and
|
||||
* mic_available() return 0, and every operation returns its failure sentinel.
|
||||
* A program asking "can I speak here?" gets a truthful no, rather than a
|
||||
* silence it would have to infer something from. Silent success is the failure
|
||||
* mode this whole change exists to eliminate — El spent this entire codebase's
|
||||
* history writing WAV files full of zeros and reporting ok=true, and nobody
|
||||
* caught it because nothing ever said "there is no sound here".
|
||||
*
|
||||
* Compiled INSTEAD OF the Darwin realizers, never alongside them — the symbols
|
||||
* are the same by design, which is the point: the El side never branches on
|
||||
* platform, it branches on speaker_available().
|
||||
*/
|
||||
|
||||
#include "el_runtime.h"
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
/* ── Speaker ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t speaker_available(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_name(void) { return EL_STR("none"); }
|
||||
|
||||
el_val_t speaker_play_pcm16(el_val_t samples, el_val_t sample_rate) {
|
||||
(void)samples; (void)sample_rate;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_wav(el_val_t path) {
|
||||
(void)path;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate) {
|
||||
(void)samples; (void)sample_rate;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_play_wav_async(el_val_t path) {
|
||||
(void)path;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t speaker_pause(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_resume(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_playing(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_stop(void) { return (el_val_t)0; }
|
||||
el_val_t speaker_played_frames(void) { return (el_val_t)0; }
|
||||
|
||||
/* WAV geometry is pure parsing and would work fine here, but reporting a
|
||||
* duration for audio this build cannot play would invite a caller to sequence
|
||||
* around a silence. Refuse consistently with the rest of the file. */
|
||||
el_val_t wav_frames(el_val_t path) { (void)path; return (el_val_t)0; }
|
||||
el_val_t wav_rate(el_val_t path) { (void)path; return (el_val_t)0; }
|
||||
|
||||
/* ── Microphone ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t mic_available(void) { return (el_val_t)0; }
|
||||
el_val_t mic_request_access(void) { return (el_val_t)0; }
|
||||
|
||||
/* Empty list, not 0: the contract says capture returns samples, and a caller
|
||||
* iterating the result must find nothing rather than dereference a non-list. */
|
||||
el_val_t mic_capture_pcm16(el_val_t seconds, el_val_t sample_rate) {
|
||||
(void)seconds; (void)sample_rate;
|
||||
return el_list_empty();
|
||||
}
|
||||
|
||||
el_val_t mic_monitor_start(void) { return (el_val_t)0; }
|
||||
el_val_t mic_monitor_rms(void) { return el_from_float(0.0); }
|
||||
el_val_t mic_monitor_stop(void) { return (el_val_t)0; }
|
||||
|
||||
/* ── Camera ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t camera_available(void) { return (el_val_t)0; }
|
||||
el_val_t camera_request_access(void) { return (el_val_t)0; }
|
||||
el_val_t camera_capture_rgb(void) { return (el_val_t)0; }
|
||||
|
||||
el_val_t camera_capture_jpeg(el_val_t path) {
|
||||
(void)path;
|
||||
return (el_val_t)0;
|
||||
}
|
||||
|
||||
#endif /* !__APPLE__ */
|
||||
+1017
-110
File diff suppressed because it is too large
Load Diff
+157
-11
@@ -80,6 +80,92 @@ void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* stderr counterpart of println (defined in el_seed.c). El could write to
|
||||
* stdout and nowhere else, which is right for a program's RESULT and wrong for
|
||||
* everything about how that result was produced. Disclosure especially has to
|
||||
* leave on a stream the caller can separate from the answer: a program that
|
||||
* announces "I am about to open the microphone" on stdout has corrupted its own
|
||||
* output. Flushed on every call, so a disclosure reaches the terminal BEFORE
|
||||
* the device it describes is touched rather than whenever the buffer drains. */
|
||||
void eprintln(el_val_t s);
|
||||
|
||||
/* ── Peripheral: the speaker, the microphone, the camera ─────────────────────
|
||||
*
|
||||
* El's I/O organ. Implemented per platform in its OWN translation unit —
|
||||
* el_audio_darwin.m / el_capture_darwin.m on Darwin, el_peripheral_null.c
|
||||
* everywhere else — so El code that speaks or listens links on every platform
|
||||
* and merely reports having no device where there isn't one. Declared here and
|
||||
* deliberately NOT implemented in el_runtime.c: acquiring a device must not
|
||||
* mean editing the middle of the language, the same rule the realizer registry
|
||||
* follows for modalities.
|
||||
*
|
||||
* These are the ONLY parts of the organ that are not El. Everything above the
|
||||
* sample buffer — WAV encode/decode, LPC autocorrelation, Levinson-Durbin,
|
||||
* formant extraction, source-filter resynthesis, the compact descriptors, the
|
||||
* converse decision loop — is arithmetic, and arithmetic belongs in El. What
|
||||
* remains here is what El cannot express: handing a buffer to the DAC and
|
||||
* waiting for it to drain, and asking the OS for frames off a capture device.
|
||||
*
|
||||
* Local by construction: none of these entry points has a network path. Samples
|
||||
* and pixels go to and from local hardware and nowhere else. Consent is
|
||||
* enforced ABOVE this layer in El (peripheral/src/organ.el) for the Neuron-level
|
||||
* grant, and BELOW it by the OS for TCC; capture fails closed on either. */
|
||||
|
||||
/* Speaker (efferent). speaker_play_pcm16 BLOCKS until the audio has actually
|
||||
* been played rather than merely queued, so a caller can sequence utterances
|
||||
* without guessing durations and without clipping each tail. */
|
||||
el_val_t speaker_available(void); /* 1 if a real speaker backs this build */
|
||||
el_val_t speaker_name(void); /* backend id, e.g. "coreaudio-audioqueue" */
|
||||
el_val_t speaker_play_pcm16(el_val_t samples, el_val_t sample_rate); /* [Int] 16-bit mono; 1 ok */
|
||||
el_val_t speaker_play_wav(el_val_t path); /* 16-bit mono RIFF/WAVE; 1 ok */
|
||||
|
||||
/* Asynchronous playback — required by converse, which must keep listening while
|
||||
* it speaks and must be able to stop ON THE SPOT mid-buffer. A blocking play
|
||||
* cannot be interrupted, and "finish the current buffer" is not barge-in.
|
||||
* speaker_stop() halts output immediately; speaker_playing() reports whether
|
||||
* the hardware is still going; speaker_played_frames() is how far it actually
|
||||
* got, which is what makes an interrupted utterance resumable at the sample. */
|
||||
el_val_t speaker_play_pcm16_async(el_val_t samples, el_val_t sample_rate);
|
||||
el_val_t speaker_play_wav_async(el_val_t path);
|
||||
el_val_t speaker_pause(void); /* stop AT THIS SAMPLE, keep position */
|
||||
el_val_t speaker_resume(void); /* carry on from exactly there */
|
||||
el_val_t speaker_playing(void);
|
||||
el_val_t speaker_stop(void);
|
||||
el_val_t speaker_played_frames(void);
|
||||
|
||||
/* WAV geometry without playing — wav-info, and the segment duration converse
|
||||
* needs to turn elapsed time into progress. */
|
||||
el_val_t wav_frames(el_val_t path);
|
||||
el_val_t wav_rate(el_val_t path);
|
||||
|
||||
/* Microphone (afferent). Fails CLOSED: returns 0 unless the OS has granted
|
||||
* capture access. mic_capture_pcm16 blocks for `seconds` and returns an [Int]
|
||||
* of 16-bit mono samples at `sample_rate` — the raw stream is handed to El and
|
||||
* never written anywhere by this layer. mic_available() reports device +
|
||||
* permission state without prompting. */
|
||||
el_val_t mic_available(void); /* 1 device present AND OS-authorized */
|
||||
el_val_t mic_request_access(void); /* prompt once; 1 if granted */
|
||||
el_val_t mic_capture_pcm16(el_val_t seconds, el_val_t sample_rate); /* [Int], empty on refusal */
|
||||
|
||||
/* Live monitoring for full-duplex converse. mic_monitor_start enables the OS
|
||||
* voice-processing unit (acoustic echo cancellation) so the microphone does not
|
||||
* hear the speaker — without AEC, Neuron barges in on its own voice and
|
||||
* turn-taking is unusable in a real room. mic_monitor_rms returns the current
|
||||
* short-window RMS as a Float in 0..1. */
|
||||
el_val_t mic_monitor_start(void); /* 1 ok; 2 = started but AEC unavailable */
|
||||
el_val_t mic_monitor_rms(void); /* Float */
|
||||
el_val_t mic_monitor_stop(void);
|
||||
|
||||
/* Camera (afferent). Fails CLOSED like the microphone. camera_capture_rgb
|
||||
* returns a Map with width/height and the frame as an [Int] of packed RGB
|
||||
* bytes, so the descriptor arithmetic can happen in El rather than here.
|
||||
* camera_capture_jpeg writes an encoded frame via ImageIO, which is a codec and
|
||||
* not something El should re-implement. */
|
||||
el_val_t camera_available(void);
|
||||
el_val_t camera_request_access(void);
|
||||
el_val_t camera_capture_rgb(void); /* Map{width,height,pixels:[Int]} or 0 */
|
||||
el_val_t camera_capture_jpeg(el_val_t path); /* 1 ok */
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_str_concat(el_val_t a, el_val_t b);
|
||||
@@ -625,20 +711,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.
|
||||
@@ -669,6 +805,11 @@ el_val_t engram_prune_telemetry(el_val_t older_than_ms);
|
||||
/* Largest byte length <= max_bytes that does not split a UTF-8 codepoint.
|
||||
* Bounded by bytes, not codepoints, so truncated strings never grow. */
|
||||
size_t el_utf8_safe_len(const char* s, size_t max_bytes);
|
||||
/* Register the ambient-consolidation step and start dreaming. Resolved by
|
||||
* dlsym, like http_set_handler. The handler performs ONE step and returns
|
||||
* non-zero if it did work; returning zero parks the dreamer until engagement
|
||||
* changes. There is no schedule and must never be one. */
|
||||
void dream_set_handler(el_val_t name);
|
||||
|
||||
el_val_t engram_node_count(void);
|
||||
/* Attach a Geometry to an existing node, and read the attached width back.
|
||||
@@ -732,8 +873,13 @@ el_val_t engram_geo_analogy_json(el_val_t a_seeds, el_val_t b_seeds);
|
||||
el_val_t engram_reason_analogy_json(el_val_t a_seeds, el_val_t b_seeds, el_val_t c_seeds);
|
||||
/* COGNITION (2026-08-14): THE ONE OPERATION + grounding, surfaced live. */
|
||||
el_val_t engram_think_json(el_val_t seeds, el_val_t faculty);
|
||||
/* GROUNDING (2026-08-16): grounding is an attribute of the RELATION and it IS the
|
||||
* hebbian weight. ground reads; ground_record writes; trajectory reads the chain. */
|
||||
el_val_t engram_ground_json(el_val_t claim, el_val_t evidence, el_val_t for_whom);
|
||||
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor);
|
||||
el_val_t engram_ground_record_json(el_val_t claim, el_val_t evidence,
|
||||
el_val_t provenance, el_val_t floor);
|
||||
el_val_t engram_ground_trajectory_json(el_val_t claim, el_val_t evidence);
|
||||
el_val_t engram_assert_json(el_val_t claim_id, el_val_t for_whom, el_val_t floor, el_val_t rel_floor);
|
||||
el_val_t engram_attend_json(el_val_t node_id, el_val_t observer, el_val_t salience);
|
||||
el_val_t engram_correspondence_beat_json(el_val_t seeds, el_val_t faculty, el_val_t keystone);
|
||||
el_val_t engram_consolidate_permanence(el_val_t node_id);
|
||||
|
||||
+130
-3
@@ -154,9 +154,18 @@ static void seed_request_start(void) {
|
||||
* file still links on its own. */
|
||||
__attribute__((weak)) void el_str_cache_flush(void);
|
||||
|
||||
/* Byte-buffer capacity registry (defined below, next to the string
|
||||
* primitives). The arena frees the pointers it tracked, so any capacity
|
||||
* entry for those addresses must go with them — otherwise a later malloc
|
||||
* reusing the address would inherit a stale width. */
|
||||
static void seed_cap_drop(const char* p);
|
||||
|
||||
static void seed_request_end(void) {
|
||||
_seed_arena_on = 0;
|
||||
for (size_t i = 0; i < _seed_arena.count; i++) free(_seed_arena.ptrs[i]);
|
||||
for (size_t i = 0; i < _seed_arena.count; i++) {
|
||||
seed_cap_drop(_seed_arena.ptrs[i]);
|
||||
free(_seed_arena.ptrs[i]);
|
||||
}
|
||||
_seed_arena.count = 0;
|
||||
if (el_str_cache_flush) el_str_cache_flush(); /* freed pointers may be reused */
|
||||
}
|
||||
@@ -188,6 +197,114 @@ static char* seed_strbuf(size_t n) {
|
||||
|
||||
static el_val_t seed_wrap_str(char* s) { return EL_STR(s); }
|
||||
|
||||
/* ── Byte-buffer capacity registry ────────────────────────────────────────────
|
||||
* A String produced by __str_alloc is a fixed-size BYTE BUFFER, not text. Its
|
||||
* length is the capacity it was asked for; strlen() is meaningless on it,
|
||||
* because the buffer is zero-filled and binary content (PCM audio, RIFF
|
||||
* headers, image rasters) contains NUL bytes by nature.
|
||||
*
|
||||
* Before this registry existed, __str_set_char bounds-checked the write index
|
||||
* against strlen(p). For a freshly __str_alloc'd buffer strlen(p) == 0, so the
|
||||
* check `idx >= len` rejected EVERY index and the function was a total no-op:
|
||||
* every El program that built bytes this way wrote a file of pure zeros and
|
||||
* still saw a success return. That is why El's own-core WAV writer emitted
|
||||
* 55,244 silent bytes with a correct-looking header length and no header.
|
||||
*
|
||||
* The fix cannot be "trust the index", because that removes the bound. It also
|
||||
* cannot be a length header stored behind the pointer, because __str_set_char
|
||||
* accepts any String — including a string literal in .rodata, where reading the
|
||||
* bytes preceding the pointer is undefined and may fault. So capacity is kept
|
||||
* in a side table keyed by the pointer itself: allocation registers, the arena
|
||||
* sweep unregisters, and anything not registered keeps the exact strlen
|
||||
* behaviour it had before. Text semantics are unchanged; byte buffers gain the
|
||||
* bound they always should have had. */
|
||||
|
||||
typedef struct {
|
||||
char* ptr; /* NULL = empty slot, (char*)1 = tombstone */
|
||||
size_t cap;
|
||||
} SeedCapEntry;
|
||||
|
||||
#define SEED_CAP_TOMB ((char*)1)
|
||||
|
||||
static _Thread_local SeedCapEntry* _seed_cap = NULL;
|
||||
static _Thread_local size_t _seed_cap_mask = 0; /* table size - 1 */
|
||||
static _Thread_local size_t _seed_cap_used = 0; /* live + tombstoned */
|
||||
|
||||
static size_t seed_cap_hash(const char* p) {
|
||||
uintptr_t h = (uintptr_t)p >> 4; /* malloc alignment: low bits are dead */
|
||||
h *= (uintptr_t)0x9E3779B97F4A7C15ull;
|
||||
return (size_t)(h >> 32);
|
||||
}
|
||||
|
||||
static void seed_cap_put(char* p, size_t cap);
|
||||
|
||||
static void seed_cap_grow(void) {
|
||||
size_t old_size = _seed_cap_mask ? _seed_cap_mask + 1 : 0;
|
||||
SeedCapEntry* old = _seed_cap;
|
||||
size_t new_size = old_size ? old_size * 2 : 256;
|
||||
SeedCapEntry* fresh = calloc(new_size, sizeof(SeedCapEntry));
|
||||
if (!fresh) return; /* out of memory: keep old table */
|
||||
_seed_cap = fresh;
|
||||
_seed_cap_mask = new_size - 1;
|
||||
_seed_cap_used = 0;
|
||||
for (size_t i = 0; i < old_size; i++) {
|
||||
if (old[i].ptr && old[i].ptr != SEED_CAP_TOMB) seed_cap_put(old[i].ptr, old[i].cap);
|
||||
}
|
||||
free(old);
|
||||
}
|
||||
|
||||
static void seed_cap_put(char* p, size_t cap) {
|
||||
if (!p) return;
|
||||
if (!_seed_cap || (_seed_cap_used + 1) * 4 >= (_seed_cap_mask + 1) * 3) {
|
||||
seed_cap_grow();
|
||||
if (!_seed_cap) return;
|
||||
}
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
size_t first_free = (size_t)-1;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (e == p) { _seed_cap[i].cap = cap; return; } /* address reused */
|
||||
if (e == SEED_CAP_TOMB && first_free == (size_t)-1) first_free = i;
|
||||
if (!e) {
|
||||
if (first_free != (size_t)-1) i = first_free; else _seed_cap_used++;
|
||||
_seed_cap[i].ptr = p;
|
||||
_seed_cap[i].cap = cap;
|
||||
return;
|
||||
}
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
/* Capacity of a registered byte buffer, or -1 when the pointer is not one. */
|
||||
static int64_t seed_cap_get(const char* p) {
|
||||
if (!p || !_seed_cap) return -1;
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (!e) return -1;
|
||||
if (e == (char*)p) return (int64_t)_seed_cap[i].cap;
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
static void seed_cap_drop(const char* p) {
|
||||
if (!p || !_seed_cap) return;
|
||||
size_t i = seed_cap_hash(p) & _seed_cap_mask;
|
||||
for (;;) {
|
||||
char* e = _seed_cap[i].ptr;
|
||||
if (!e) return;
|
||||
if (e == (char*)p) { _seed_cap[i].ptr = SEED_CAP_TOMB; return; }
|
||||
i = (i + 1) & _seed_cap_mask;
|
||||
}
|
||||
}
|
||||
|
||||
/* Effective addressable length of a String: its buffer capacity when it is a
|
||||
* byte buffer, otherwise strlen. */
|
||||
static int64_t seed_addressable_len(const char* p) {
|
||||
int64_t cap = seed_cap_get(p);
|
||||
return cap >= 0 ? cap : (int64_t)strlen(p);
|
||||
}
|
||||
|
||||
/* ── String primitives ───────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t __str_len(el_val_t s) {
|
||||
@@ -199,7 +316,7 @@ el_val_t __str_len(el_val_t s) {
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i) {
|
||||
const char* p = EL_CSTR(s);
|
||||
if (!p) return 0;
|
||||
int64_t len = (int64_t)strlen(p);
|
||||
int64_t len = seed_addressable_len(p); /* capacity for byte buffers */
|
||||
int64_t idx = (int64_t)i;
|
||||
if (idx < 0 || idx >= len) return 0;
|
||||
return (el_val_t)(unsigned char)p[idx];
|
||||
@@ -210,13 +327,14 @@ el_val_t __str_alloc(el_val_t n) {
|
||||
if (sz < 0) sz = 0;
|
||||
char* buf = seed_strbuf((size_t)sz);
|
||||
memset(buf, 0, (size_t)sz + 1);
|
||||
seed_cap_put(buf, (size_t)sz); /* this is a byte buffer of width sz */
|
||||
return seed_wrap_str(buf);
|
||||
}
|
||||
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c) {
|
||||
char* p = (char*)(uintptr_t)s;
|
||||
if (!p) return s;
|
||||
int64_t len = (int64_t)strlen(p);
|
||||
int64_t len = seed_addressable_len(p); /* capacity for byte buffers */
|
||||
int64_t idx = (int64_t)i;
|
||||
if (idx < 0 || idx >= len) return s;
|
||||
p[idx] = (char)(unsigned char)(int64_t)c;
|
||||
@@ -406,6 +524,15 @@ el_val_t __fs_mkdir(el_val_t path) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* stderr counterpart of println. Flushed immediately: a disclosure line is only
|
||||
* worth anything if it lands before the thing it discloses happens. */
|
||||
void eprintln(el_val_t s) {
|
||||
const char* p = EL_CSTR(s);
|
||||
fputs(p ? p : "", stderr);
|
||||
fputc('\n', stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) {
|
||||
const char* p = EL_CSTR(path);
|
||||
const char* b = EL_CSTR(bytes);
|
||||
|
||||
+372
-29
@@ -246,14 +246,6 @@ static int put_edge(EngramPagedStore* s, const char* id, const char* from, const
|
||||
e.metadata = (char*)meta;
|
||||
return store_put_edge(s, &e);
|
||||
}
|
||||
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
|
||||
const char* evidence_id, double grounding, const char* for_whom) {
|
||||
if (!s || !claim_id || !evidence_id) return -1;
|
||||
char id[512], meta[256];
|
||||
snprintf(id, sizeof id, "gb-%s-%s-%s", claim_id, evidence_id, for_whom ? for_whom : "global");
|
||||
snprintf(meta, sizeof meta, "for_whom=%s", for_whom ? for_whom : "-");
|
||||
return put_edge(s, id, claim_id, evidence_id, COG_GROUNDED_BY_RELATION, grounding, meta);
|
||||
}
|
||||
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
|
||||
const char* observer_id, double salience) {
|
||||
if (!s || !node_id || !observer_id) return -1;
|
||||
@@ -261,35 +253,386 @@ int cog_salient_edge(EngramPagedStore* s, const char* node_id,
|
||||
snprintf(id, sizeof id, "st-%s-%s", node_id, observer_id);
|
||||
return put_edge(s, id, node_id, observer_id, COG_SALIENT_TO_RELATION, salience, NULL);
|
||||
}
|
||||
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
|
||||
const char* for_whom, double floor) {
|
||||
if (!s || !claim_id) return -1;
|
||||
if (!(floor > 0)) floor = 0.5;
|
||||
StoreEdge* edges = NULL; size_t n = 0;
|
||||
if (store_get_edges_from(s, claim_id, &edges, &n) < 0) return -1;
|
||||
double best = 0.0; int found = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!edges[i].relation || strcmp(edges[i].relation, COG_GROUNDED_BY_RELATION) != 0) continue;
|
||||
/* grounded-for-whom: match observer if requested; global (for_whom=-) always counts */
|
||||
int match = 1;
|
||||
if (for_whom && edges[i].metadata) {
|
||||
const char* fw = strstr(edges[i].metadata, "for_whom=");
|
||||
if (fw) { fw += 9; if (strcmp(fw, for_whom) != 0 && strcmp(fw, "-") != 0) match = 0; }
|
||||
}
|
||||
if (match) { found = 1; if (edges[i].weight > best) best = edges[i].weight; }
|
||||
}
|
||||
store_edges_free(edges, n);
|
||||
if (!found) return 0; /* ungrounded => refuse assertion (still held) */
|
||||
return (best >= floor) ? 1 : 0;
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §7 GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR.
|
||||
* See engram_cognition.h §7 for the model and for the measurements the two
|
||||
* design decisions (thirteen regions, min aggregate) rest on.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── The one decay model. Moved here verbatim from el_runtime.c's
|
||||
* engram_temporal_decay so nodes and edges share a single implementation and a
|
||||
* single set of constants; engram_temporal_decay now delegates. Bit-identical
|
||||
* for nodes: reinforcements := activation_count, lambda_override :=
|
||||
* temporal_decay_rate.
|
||||
*
|
||||
* This is what makes decay ANALYTIC rather than sampled: between two recorded
|
||||
* versions the trajectory is not unknown, it is known in closed form from the
|
||||
* last point and elapsed time. Store the point, read the curve. */
|
||||
double cog_decay_factor(int64_t age_ms, double reinforcements, double lambda_override) {
|
||||
if (age_ms <= 0) return 1.0;
|
||||
double lambda = (lambda_override > 0.0) ? lambda_override : COG_DECAY_LAMBDA;
|
||||
double age_hours = (double)age_ms / 3600000.0;
|
||||
if (reinforcements < 0) reinforcements = 0;
|
||||
double t_half = COG_T_HALF_HOURS * (1.0 + log(1.0 + reinforcements));
|
||||
double factor = exp(-lambda * age_hours / t_half);
|
||||
if (factor < COG_DECAY_FLOOR) factor = COG_DECAY_FLOOR;
|
||||
return factor;
|
||||
}
|
||||
|
||||
const char* cog_prov_name(CogProvClass p) {
|
||||
switch (p) {
|
||||
case COG_PROV_OBSERVED: return "observed";
|
||||
case COG_PROV_INFERRED: return "inferred";
|
||||
case COG_PROV_TOLD: return "told";
|
||||
case COG_PROV_IMPRINTED: return "imprinted";
|
||||
default: return "unset";
|
||||
}
|
||||
}
|
||||
CogProvClass cog_prov_parse(const char* s) {
|
||||
if (!s) return COG_PROV_UNSET;
|
||||
if (!strcmp(s, "observed")) return COG_PROV_OBSERVED;
|
||||
if (!strcmp(s, "inferred")) return COG_PROV_INFERRED;
|
||||
if (!strcmp(s, "told")) return COG_PROV_TOLD;
|
||||
if (!strcmp(s, "imprinted")) return COG_PROV_IMPRINTED;
|
||||
return COG_PROV_UNSET;
|
||||
}
|
||||
|
||||
/* Locate the GRD1 block in an edge's metadata. It is always the tail; anything
|
||||
* ahead of it is the edge's pre-existing metadata, preserved verbatim. */
|
||||
static const char* cog_grd_find(const char* meta) {
|
||||
if (!meta) return NULL;
|
||||
size_t ml = strlen(COG_GROUNDING_META_MAGIC);
|
||||
if (strncmp(meta, COG_GROUNDING_META_MAGIC, ml) == 0) return meta;
|
||||
const char* p = meta;
|
||||
while ((p = strstr(p, COG_GROUNDING_META_MAGIC)) != NULL) {
|
||||
if (p > meta && p[-1] == '\n') return p;
|
||||
p += ml;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int cog_grounding_parse(const StoreEdge* e, int64_t now_ms, CogGrounding* out) {
|
||||
if (!e || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
|
||||
/* Two dimensions exist on every edge whether or not grounding has ever been
|
||||
* established, because they ARE existing substrate rather than new fields:
|
||||
* associative — the accrued hebb, with its existing dynamics;
|
||||
* polarity — the signed authored weight. `inhibitory` is precisely this
|
||||
* distinction crushed to one bit, so it is the seed sign. */
|
||||
out->associative = e->hebb;
|
||||
out->polarity = e->inhibitory ? -e->weight : e->weight;
|
||||
out->prov = COG_PROV_UNSET;
|
||||
out->ts = e->last_fired > 0 ? e->last_fired : e->updated_at;
|
||||
|
||||
const char* blk = cog_grd_find(e->metadata);
|
||||
if (blk) {
|
||||
out->present = 1;
|
||||
char* copy = dupstr(blk);
|
||||
if (!copy) return -1;
|
||||
for (char* line = strtok(copy, "\n"); line; line = strtok(NULL, "\n")) {
|
||||
if (line[0] == '\0') continue;
|
||||
char tag = line[0];
|
||||
const char* rest = line + 1; while (*rest == ' ') rest++;
|
||||
if (tag == 'w') { /* the four numeric dimensions */
|
||||
double v[4] = {0,0,0,0}; parse_floats(rest, v, 4);
|
||||
out->factual = v[0]; out->relational = v[1];
|
||||
out->associative = v[2]; out->polarity = v[3];
|
||||
} else if (tag == 'k') { /* provenance class */
|
||||
out->prov = cog_prov_parse(rest);
|
||||
} else if (tag == 't') { /* timestamp + seq + reinforcements */
|
||||
double v[3] = {0,0,0}; parse_floats(rest, v, 3);
|
||||
out->ts = (int64_t)v[0]; out->seq = (int64_t)v[1]; out->reinforcements = v[2];
|
||||
} else if (tag == 'd') {
|
||||
double v[3] = {0,0,0}; parse_floats(rest, v, 3);
|
||||
out->fac_proj = v[0]; out->rel_proj = v[1]; out->cos_angle = v[2];
|
||||
} else if (tag == 'v') {
|
||||
snprintf(out->binding_value, sizeof out->binding_value, "%s", rest);
|
||||
} else if (tag == 'c') {
|
||||
double v[2] = {0,0}; parse_floats(rest, v, 2);
|
||||
out->floor_at_record = v[0]; out->rel_floor_at_record = v[1];
|
||||
} else if (tag == 'p') {
|
||||
snprintf(out->prev_edge, sizeof out->prev_edge, "%s", rest);
|
||||
}
|
||||
}
|
||||
free(copy);
|
||||
}
|
||||
out->agreement = (out->cos_angle > 0) ? 1 : (out->cos_angle < 0 ? -1 : 0);
|
||||
|
||||
/* ── DERIVED. Nothing below this line is ever serialized. Recency, decay and
|
||||
* staleness are read off the curve; storing them is how a number ends up
|
||||
* asserting something nothing computed (§8.1 / spec §2). */
|
||||
out->age_ms = (out->ts > 0 && now_ms > out->ts) ? (now_ms - out->ts) : 0;
|
||||
out->decay = cog_decay_factor(out->age_ms, out->reinforcements, 0.0);
|
||||
out->factual_now = out->factual * out->decay;
|
||||
out->relational_now = out->relational * out->decay;
|
||||
out->associative_now = out->associative * out->decay;
|
||||
out->stale = (out->present && out->floor_at_record > 0 &&
|
||||
out->factual_now < out->floor_at_record) ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* cog_grounding_metadata(const char* base_meta, const CogGrounding* g) {
|
||||
if (!g) return NULL;
|
||||
size_t keep = 0;
|
||||
if (base_meta) {
|
||||
const char* blk = cog_grd_find(base_meta);
|
||||
keep = blk ? (size_t)(blk - base_meta) : strlen(base_meta);
|
||||
while (keep > 0 && base_meta[keep - 1] == '\n') keep--;
|
||||
}
|
||||
size_t cap = keep + 1024;
|
||||
char* buf = malloc(cap); if (!buf) return NULL;
|
||||
size_t o = 0;
|
||||
if (keep) { memcpy(buf, base_meta, keep); o = keep; buf[o++] = '\n'; }
|
||||
o += (size_t)snprintf(buf + o, cap - o, "%s\n", COG_GROUNDING_META_MAGIC);
|
||||
/* STORED ONLY. factual / relational / associative / polarity / provenance /
|
||||
* timestamp — plus the joint state a decision saw. No confidence, no
|
||||
* recency, no staleness, no volatility: those are read off the curve. */
|
||||
o += (size_t)snprintf(buf + o, cap - o, "w %.9g %.9g %.9g %.9g\n",
|
||||
g->factual, g->relational, g->associative, g->polarity);
|
||||
o += (size_t)snprintf(buf + o, cap - o, "k %s\n", cog_prov_name(g->prov));
|
||||
o += (size_t)snprintf(buf + o, cap - o, "t %lld %lld %.9g\n",
|
||||
(long long)g->ts, (long long)g->seq, g->reinforcements);
|
||||
o += (size_t)snprintf(buf + o, cap - o, "d %.9g %.9g %.9g\n",
|
||||
g->fac_proj, g->rel_proj, g->cos_angle);
|
||||
o += (size_t)snprintf(buf + o, cap - o, "v %s\n", g->binding_value[0] ? g->binding_value : "-");
|
||||
o += (size_t)snprintf(buf + o, cap - o, "c %.9g %.9g\n", g->floor_at_record, g->rel_floor_at_record);
|
||||
if (g->prev_edge[0]) o += (size_t)snprintf(buf + o, cap - o, "p %s\n", g->prev_edge);
|
||||
(void)o;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ── Consequence, not epsilon. Every test is a floor crossing or a sign change,
|
||||
* both exact. Ordered so the two INHERENT (discrete) moves are reported in
|
||||
* preference to the graded ones, because they bypass the salience gate. */
|
||||
CogSignificance cog_grounding_significant(const CogGrounding* prev,
|
||||
const CogGrounding* now,
|
||||
double floor, double rel_floor) {
|
||||
if (!now) return COG_SIG_NONE;
|
||||
if (!prev || !prev->present) return COG_SIG_FIRST_RECORD;
|
||||
|
||||
/* INHERENT 1 — polarity sign flip. Ignorance and disagreement are different
|
||||
* states, and support → contradiction is a change of state rather than a
|
||||
* drift, so no threshold applies. Comparing signs, with zero its own class. */
|
||||
{
|
||||
int sp = prev->polarity > 0 ? 1 : (prev->polarity < 0 ? -1 : 0);
|
||||
int sn = now->polarity > 0 ? 1 : (now->polarity < 0 ? -1 : 0);
|
||||
if (sp != sn) return COG_SIG_POLARITY_FLIP;
|
||||
}
|
||||
/* INHERENT 2 — provenance class change. told → observed is a categorical
|
||||
* upgrade in what the relation is entitled to, not a movement along an axis. */
|
||||
if (prev->prov != now->prov) return COG_SIG_PROVENANCE_CHANGE;
|
||||
|
||||
/* Crossing an assert floor — the move changes whether this relation can be
|
||||
* spoken. Compared on the DECAYED values, because that is what the gate reads. */
|
||||
if ((prev->factual_now >= floor) != (now->factual_now >= floor)) return COG_SIG_FACTUAL_FLOOR;
|
||||
if ((prev->relational_now >= rel_floor) != (now->relational_now >= rel_floor)) return COG_SIG_RELATIONAL_FLOOR;
|
||||
|
||||
/* Flipping factual/relational agreement — the relation stops being "true and
|
||||
* meaningful" and becomes "true and misapplied", or the reverse. This is the
|
||||
* 911/CPS contradiction as a measured event rather than a reviewable one. */
|
||||
if (prev->agreement != now->agreement) return COG_SIG_AGREEMENT_FLIP;
|
||||
|
||||
/* A gradient reversing — the evidence stopped pulling the claim toward it and
|
||||
* began pushing it away, or the same on the values axis. */
|
||||
if ((prev->fac_proj > 0) != (now->fac_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;
|
||||
if ((prev->rel_proj > 0) != (now->rel_proj > 0)) return COG_SIG_DIRECTION_REVERSAL;
|
||||
|
||||
return COG_SIG_NONE;
|
||||
}
|
||||
|
||||
int cog_significance_inherent(CogSignificance s) {
|
||||
return (s == COG_SIG_FIRST_RECORD || s == COG_SIG_POLARITY_FLIP ||
|
||||
s == COG_SIG_PROVENANCE_CHANGE) ? 1 : 0;
|
||||
}
|
||||
|
||||
const char* cog_significance_name(CogSignificance s) {
|
||||
switch (s) {
|
||||
case COG_SIG_FIRST_RECORD: return "first-record";
|
||||
case COG_SIG_POLARITY_FLIP: return "polarity-sign-flip";
|
||||
case COG_SIG_PROVENANCE_CHANGE: return "provenance-class-change";
|
||||
case COG_SIG_FACTUAL_FLOOR: return "factual-floor-crossed";
|
||||
case COG_SIG_RELATIONAL_FLOOR: return "relational-floor-crossed";
|
||||
case COG_SIG_AGREEMENT_FLIP: return "agreement-sign-flip";
|
||||
case COG_SIG_DIRECTION_REVERSAL: return "gradient-direction-reversal";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Recording: a NEW edge record. The predecessor is never touched. ────────── */
|
||||
int cog_grounding_record(EngramPagedStore* s, const StoreEdge* base,
|
||||
const CogGrounding* g, char* out_id, size_t out_id_cap) {
|
||||
if (!s || !base || !base->id || !g) return -1;
|
||||
char root[192];
|
||||
snprintf(root, sizeof root, "%s", base->id);
|
||||
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
|
||||
|
||||
int seq = (int)g->seq + 1;
|
||||
char vid[224];
|
||||
snprintf(vid, sizeof vid, "%s#%d", root, seq);
|
||||
|
||||
CogGrounding rec = *g;
|
||||
rec.seq = seq;
|
||||
snprintf(rec.prev_edge, sizeof rec.prev_edge, "%s", base->id);
|
||||
|
||||
char* meta = cog_grounding_metadata(base->metadata, &rec);
|
||||
if (!meta) return -1;
|
||||
|
||||
StoreEdge e; memset(&e, 0, sizeof e);
|
||||
e.id = vid; e.from_id = base->from_id; e.to_id = base->to_id;
|
||||
e.relation = base->relation; e.metadata = meta;
|
||||
/* The vector IS the weight, so the scalar fields carry their dimensions:
|
||||
* `weight` the magnitude of polarity, `inhibitory` its sign, `hebb` the
|
||||
* associative strength. Nothing here is a second copy of a derived value. */
|
||||
e.weight = rec.polarity < 0 ? -rec.polarity : rec.polarity;
|
||||
e.inhibitory = rec.polarity < 0 ? 1 : 0;
|
||||
e.hebb = rec.associative;
|
||||
e.confidence = base->confidence;
|
||||
e.created_at = base->created_at;
|
||||
e.updated_at = rec.ts;
|
||||
e.last_fired = rec.ts;
|
||||
e.layer_id = base->layer_id;
|
||||
int rc = store_put_edge(s, &e);
|
||||
free(meta);
|
||||
if (rc != 0) return -1;
|
||||
if (out_id && out_id_cap) snprintf(out_id, out_id_cap, "%s", vid);
|
||||
return seq;
|
||||
}
|
||||
|
||||
int cog_grounding_head(EngramPagedStore* s, const char* base_id,
|
||||
StoreEdge* out, int max_versions) {
|
||||
if (!s || !base_id || !out) return -1;
|
||||
if (max_versions <= 0) max_versions = 64;
|
||||
char root[192]; snprintf(root, sizeof root, "%s", base_id);
|
||||
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
|
||||
|
||||
StoreEdge cur; memset(&cur, 0, sizeof cur);
|
||||
if (store_get_edge(s, root, &cur) != 1) return -1;
|
||||
int found = 0;
|
||||
for (int v = 1; v <= max_versions; v++) {
|
||||
char vid[224]; snprintf(vid, sizeof vid, "%s#%d", root, v);
|
||||
StoreEdge nx;
|
||||
if (store_get_edge(s, vid, &nx) != 1) break;
|
||||
store_edge_free(&cur); cur = nx; found = v;
|
||||
}
|
||||
*out = cur;
|
||||
return found;
|
||||
}
|
||||
|
||||
/* ── VOLATILITY AND DRIFT: derived from the chain, stored nowhere. The series
|
||||
* exists only because nothing was destroyed, which is the whole return on
|
||||
* immutability — a derivative for free. */
|
||||
int cog_grounding_trajectory(EngramPagedStore* s, const char* base_id,
|
||||
int64_t now_ms, CogTrajectory* out) {
|
||||
if (!s || !base_id || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
char root[192]; snprintf(root, sizeof root, "%s", base_id);
|
||||
char* hash = strchr(root, '#'); if (hash) *hash = '\0';
|
||||
|
||||
double pf = 0, pr = 0, f0 = 0, r0 = 0, fN = 0, rN = 0;
|
||||
double sum_df = 0, sum_dr = 0;
|
||||
int n = 0;
|
||||
for (int v = 0; v <= 64; v++) {
|
||||
char vid[224];
|
||||
if (v == 0) snprintf(vid, sizeof vid, "%s", root);
|
||||
else snprintf(vid, sizeof vid, "%s#%d", root, v);
|
||||
StoreEdge e;
|
||||
if (store_get_edge(s, vid, &e) != 1) { if (v) break; else continue; }
|
||||
CogGrounding g;
|
||||
if (cog_grounding_parse(&e, now_ms, &g) == 0) {
|
||||
if (n == 0) { f0 = g.factual; r0 = g.relational; }
|
||||
else { sum_df += fabs(g.factual - pf); sum_dr += fabs(g.relational - pr); }
|
||||
pf = g.factual; pr = g.relational; fN = pf; rN = pr;
|
||||
n++;
|
||||
}
|
||||
store_edge_free(&e);
|
||||
}
|
||||
out->n_versions = n;
|
||||
if (n > 1) {
|
||||
out->factual_volatility = sum_df / (double)(n - 1);
|
||||
out->relational_volatility = sum_dr / (double)(n - 1);
|
||||
}
|
||||
out->factual_drift = fN - f0;
|
||||
out->relational_drift = rN - r0;
|
||||
/* "STAYED TRUE, BECAME WRONG" — the event the joint record makes visible and
|
||||
* that per-dimension versioning would have destroyed: the fact held while
|
||||
* the meaning degraded. Expressed as signs, so there is no tolerance here
|
||||
* either: factual did not fall, relational did. */
|
||||
out->stayed_true_became_wrong =
|
||||
(n > 1 && out->factual_drift >= 0 && out->relational_drift < 0) ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Assertion gates on BOTH floors. Traversal is untouched: activation still
|
||||
* conducts on the factual/associative side, so a relation can remain thinkable
|
||||
* while ceasing to be assertable. That gap is where the wide angles live. ──── */
|
||||
int cog_assert_two_axis(EngramPagedStore* s, const char* claim_id,
|
||||
double floor, double rel_floor, int64_t now_ms,
|
||||
CogAssertion* out) {
|
||||
if (!s || !claim_id || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
if (!(floor > 0)) floor = 0.5;
|
||||
if (!(rel_floor > 0)) rel_floor = floor;
|
||||
|
||||
/* still_held is DERIVED, not a literal (§8.1). Holding is unconditional —
|
||||
* the store gates nothing — so the question the field actually answers is
|
||||
* whether the content is present and live. */
|
||||
StoreNode n;
|
||||
if (store_get_node(s, claim_id, &n) == 1) { out->still_held = !n.tombstoned; store_node_free(&n); }
|
||||
else out->still_held = 0;
|
||||
|
||||
double best = -1.0;
|
||||
for (int dir = 0; dir < 2; dir++) {
|
||||
StoreEdge* edges = NULL; size_t ne = 0;
|
||||
int rc = dir == 0 ? store_get_edges_from(s, claim_id, &edges, &ne)
|
||||
: store_get_edges_to (s, claim_id, &edges, &ne);
|
||||
if (rc < 0) continue;
|
||||
for (size_t i = 0; i < ne; i++) {
|
||||
if (edges[i].tombstoned) continue;
|
||||
CogGrounding g;
|
||||
if (cog_grounding_parse(&edges[i], now_ms, &g) != 0) continue;
|
||||
out->n_edges++;
|
||||
out->found = 1;
|
||||
if (g.factual_now > best) {
|
||||
best = g.factual_now;
|
||||
out->factual = g.factual_now;
|
||||
out->relational = g.relational_now; /* the SAME edge, not a max */
|
||||
out->polarity = g.polarity;
|
||||
out->cos_angle = g.cos_angle;
|
||||
out->agreement = g.agreement;
|
||||
out->prov = g.prov;
|
||||
out->relational_established = g.present;
|
||||
snprintf(out->best_edge, sizeof out->best_edge, "%s", edges[i].id ? edges[i].id : "");
|
||||
snprintf(out->binding_value, sizeof out->binding_value, "%s", g.binding_value);
|
||||
}
|
||||
}
|
||||
store_edges_free(edges, ne);
|
||||
}
|
||||
/* BOTH floors, and an unestablished relational axis does NOT pass by default
|
||||
* — defaulting it to passing is the exemption §0 forbids. A negative polarity
|
||||
* is a relation that actively contradicts and can never license assertion. */
|
||||
out->may_assert = (out->found && out->relational_established &&
|
||||
out->polarity > 0 &&
|
||||
out->factual >= floor && out->relational >= rel_floor) ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════════ THE CORRESPONDENCE-LOOP ═════ */
|
||||
int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
|
||||
double outcome_y, CogStance* stance,
|
||||
int learn, double max_step, CogBeatResult* out) {
|
||||
if (!region || !stance || !out) return -1;
|
||||
memset(out, 0, sizeof *out);
|
||||
if (stance->keystone) { learn = 0; out->wrote_keystone = 1; } /* §6: never write a keystone */
|
||||
/* 2026-08-16: the keystone block is GONE. It refused to learn about the
|
||||
* reference frame, which does not make it a good reference — it makes it
|
||||
* unexaminable, trading circular calibration for an ungroundable one (spec
|
||||
* §2). Measured cost of the block: on the keystone region the beat reported
|
||||
* 0.00% brier reduction over n_trials 0 — it never ran, so nothing about the
|
||||
* self was ever calibrated OR falsifiable. What replaces it is a provenance
|
||||
* constraint, not a permission: cog_grounding_downstream refuses evidence
|
||||
* that is downstream of the region being calibrated, for every region alike.
|
||||
* `wrote_keystone` is retained as a reporting field only and is always 0. */
|
||||
|
||||
GeoGradient g;
|
||||
if (engram_think(region, anchor, stance, &g) != 0) return -1; /* PREDICTION */
|
||||
|
||||
+269
-17
@@ -23,7 +23,7 @@
|
||||
* and a region, and grounded-for-whom.
|
||||
*
|
||||
* PURE + (mostly) READ-ONLY, stdlib + libm only. think() and the warp are pure
|
||||
* over their inputs. Persistence (Stance <-> StoreNode, grounded-by edges) is the
|
||||
* over their inputs. Persistence (Stance <-> StoreNode, edge grounding vectors) is the
|
||||
* only part that touches the store, and it is additive / supersede / tombstone —
|
||||
* never mutate-in-place, never delete. It NEVER touches the live daemon: all
|
||||
* offline against a scratch store, per the design's rails.
|
||||
@@ -152,30 +152,25 @@ int engram_express(const GeoGradient* g, const float* anchor, float* out_point);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §5 HOLD vs GROUND vs ASSERT. Holding is unconditional (the store gates nothing).
|
||||
* Grounding is a RELATION — a "grounded-by" edge, probabilistic, grounded-for-whom.
|
||||
* The honesty floor is checked only at ASSERTION.
|
||||
* Grounding is an ATTRIBUTE OF a relation — carried on the edge itself, as a
|
||||
* vector (§7). The honesty floor is checked only at ASSERTION, on both axes.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
#define COG_GROUNDED_BY_RELATION "grounded-by"
|
||||
/* DELETED 2026-08-16: COG_GROUNDED_BY_RELATION and cog_ground_edge.
|
||||
*
|
||||
* A "grounded-by" edge models grounding as a relation BETWEEN two nodes. It is a
|
||||
* property OF a relation — and it is that relation's weight. Minting a new edge
|
||||
* to carry a score was the error; #147 corrected which endpoints the edge landed
|
||||
* on and left the wrong idea standing. There is nothing to ground a claim
|
||||
* "against" that is not already an edge, and if no edge exists the honest answer
|
||||
* is that the two are not related — not a freshly minted one scoring 0.98.
|
||||
* See §7 for what replaced it. */
|
||||
#define COG_SALIENT_TO_RELATION "salient-to"
|
||||
|
||||
/* Write a grounded-by edge (additive). weight = grounding ∈(0,1] from the verifier;
|
||||
* for_whom recorded in edge metadata (grounding is relational). Never a node flag. */
|
||||
int cog_ground_edge(EngramPagedStore* s, const char* claim_id,
|
||||
const char* evidence_id, double grounding, const char* for_whom);
|
||||
|
||||
/* Write/refresh a salient-to edge: salience is RELATIONAL (grounded-for-whom),
|
||||
* carried on the edge to the observer — not baked into the node scalar (§2.1). */
|
||||
int cog_salient_edge(EngramPagedStore* s, const char* node_id,
|
||||
const char* observer_id, double salience);
|
||||
|
||||
/* The honesty floor — a QUERY at assertion time, NOT a schema constraint. Reads the
|
||||
* claim's stored grounded-by edges (for the given observer) and returns:
|
||||
* 1 = may assert (best grounding >= floor),
|
||||
* 0 = REFUSE assertion (holds unconditionally; only asserting is gated),
|
||||
* <0 = error. The content remains held either way. */
|
||||
int cog_assert_gate(EngramPagedStore* s, const char* claim_id,
|
||||
const char* for_whom, double floor);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §4 THE REFLEXIVE CORRESPONDENCE-LOOP — the learning engine. think scores its
|
||||
* OWN gradient against outcome, refines the stance on the error, and (optionally)
|
||||
@@ -209,8 +204,265 @@ int engram_correspondence_beat(const GeoDescriptor* region, const float* anchor,
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §6 METASTABILITY. Keystones (self/values) are read-mostly: the loop reads but
|
||||
* never writes them. Mark by stance flag or by a keystone-id set the loop consults.
|
||||
*
|
||||
* SUPERSEDED BY §7's PROVENANCE CONSTRAINT (2026-08-16). The keystone flag is a
|
||||
* PERMISSION: it asks who the target is, not where the evidence came from. That
|
||||
* is censorship, and it costs the ability to ever ground the self (spec
|
||||
* correspondence-and-censorship.md §0/§2). The constraint that actually protects
|
||||
* a reference frame is cog_grounding_downstream: a region may not be calibrated
|
||||
* by evidence downstream of itself. These declarations remain only so existing
|
||||
* call sites keep compiling; nothing in the grounding path consults them.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
typedef struct { const char** ids; int n; } CogKeystoneSet;
|
||||
int cog_is_keystone(const CogKeystoneSet* ks, const CogStance* s);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §7 GROUNDING IS THE EDGE'S WEIGHT, AND THE WEIGHT IS A VECTOR
|
||||
* (2026-08-16; spec correspondence-and-censorship.md §2–§6 @ 2b7e4ba.)
|
||||
*
|
||||
* THE MODEL. Grounding is not a subsystem, a score, or a relation BETWEEN nodes.
|
||||
* It is an attribute OF a relation. The graph already IS the grounding structure:
|
||||
* every edge is a grounded relation, and what that relation is worth is carried
|
||||
* on the edge itself. Three things follow, and each DELETES rather than adds:
|
||||
*
|
||||
* 1. `grounded-by` as a relation type does not exist, and cog_ground_edge is
|
||||
* gone. Minting an edge to hold a score models grounding as a relation
|
||||
* between nodes when it is a property of a relation. #147 corrected which
|
||||
* endpoints that edge landed on and left the wrong idea standing.
|
||||
* 2. There is no observer, and no sampling rate. Change is not a consequence of
|
||||
* use — it IS use, the way potentiation is the firing rather than something
|
||||
* that reads the firing and writes a weight. So no supervisor compares a
|
||||
* value to a threshold and decides to persist.
|
||||
* 3. Between two recorded versions the trajectory is not unknown. Decay is a
|
||||
* pure function of the last recorded point and elapsed time, so it is
|
||||
* ANALYTIC: store the point, read the curve.
|
||||
*
|
||||
* WHAT IS *NOT* HERE, DELIBERATELY. An earlier draft of the spec posed "a graph
|
||||
* predicate for evidence downstream of itself" as the hard problem, and this file
|
||||
* briefly contained one. It is withdrawn. Non-circularity is TEMPORAL, not
|
||||
* topological: you cannot recalibrate the ruler while measuring with it, so you
|
||||
* do it when you are not using the frame to act. Reachability could never have
|
||||
* worked — measured on the live store, reachability from the self region over
|
||||
* all relations reaches 89.2% of the graph (10,580 of 11,861 nodes) and 16.0%
|
||||
* over hebbian/semantic relations alone, so the predicate marks essentially all
|
||||
* evidence tainted and the constraint degenerates into the total block that
|
||||
* censorship started as. Nothing replaces it here; the independence is a fact
|
||||
* about engagement, owned by the dreamer, not a fact about the graph.
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════════════════
|
||||
* §7.1 THE VECTOR
|
||||
*
|
||||
* The test for a real dimension is whether it can move independently of the
|
||||
* others. Five can, and each maps onto substrate that already exists:
|
||||
*
|
||||
* factual correspondence with evidence. [GRD1]
|
||||
* relational correspondence with values — min over THIRTEEN
|
||||
* value regions, carrying the binding value's NAME. [GRD1]
|
||||
* associative co-activation frequency. This is the edge's `hebb`
|
||||
* field with its existing dynamics — NOT a new one.
|
||||
* Independent by construction: every superstition is
|
||||
* a strong association with no factual grounding.
|
||||
* polarity SIGNED. Near zero means "no support"; NEGATIVE means
|
||||
* "this actively contradicts". The edge's `inhibitory`
|
||||
* bit is exactly this distinction crushed to one bit,
|
||||
* and is carried forward as the seed value. [GRD1]
|
||||
* provenance observed / inferred / told / imprinted. Categorical,
|
||||
* and load-bearing: it governs what the relation is
|
||||
* entitled to. [GRD1]
|
||||
*
|
||||
* Plus a TIMESTAMP, which is what turns the supersession chain into a time
|
||||
* series of vectors rather than a series of numbers.
|
||||
*
|
||||
* DERIVED, THEREFORE NEVER STORED. Confidence (high grounding AND low
|
||||
* volatility), recency (decay read off the curve), staleness (grounding fallen
|
||||
* below its floor), volatility (the derivative of a series nothing destroyed).
|
||||
* Storing confidence separately is how `confidence: 0.5` ends up sitting beside
|
||||
* a zero direction vector, asserting something nothing computed. Every field in
|
||||
* CogGrounding below is marked STORED or DERIVED, and the serializer writes
|
||||
* only the STORED ones.
|
||||
*
|
||||
* THE VALUES REFERENCE IS THIRTEEN REGIONS AND THE AGGREGATE IS MIN.
|
||||
* Measured on the live store: the values root kn-5b606390 `contains` exactly 13
|
||||
* value nodes; pairwise centroid cosine among their regions is min 0.1525,
|
||||
* mean 0.5199, median 0.5282, max 0.9278 — they demonstrably do not form one
|
||||
* region. Against a single union region the individual values sit at cosine
|
||||
* 0.38..0.89, with constraints-as-freedom at 0.3812 and change-is-the-signal at
|
||||
* 0.4677, so a union centroid under-represents precisely the values a claim is
|
||||
* most likely to be measured against. MIN rather than MEAN because a mean lets
|
||||
* strong agreement with twelve values mask a violation of the thirteenth, which
|
||||
* is the mechanism of rationalization; min yields a binding constraint with a
|
||||
* NAME attached rather than a score.
|
||||
*
|
||||
* TRAVERSAL CONDUCTS ON FACTUAL; ASSERTION REQUIRES BOTH. If activation
|
||||
* conducted on relational weight, Neuron could not follow a chain of reasoning
|
||||
* to a conclusion he then rejects — censorship arriving through the spreading
|
||||
* rule. The gap between reachable and assertable is where the wide
|
||||
* factual/relational angles live, and that gap is the interesting part.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── The one decay model (moved here from el_runtime.c so that node decay and
|
||||
* edge-grounding decay are a single implementation with a single set of
|
||||
* constants, rather than a model and a parallel copy of it). Half-life scales
|
||||
* with how established the thing is: T_eff = T_HALF · (1 + ln(1 + reinforcements)).
|
||||
* The floor is a preference, not a cliff — max penalty for age alone is 4x.
|
||||
* `lambda_override` > 0 replaces the default rate; 0 means use the default. */
|
||||
#define COG_T_HALF_HOURS 168.0
|
||||
#define COG_DECAY_LAMBDA 0.693147
|
||||
#define COG_DECAY_FLOOR 0.25
|
||||
double cog_decay_factor(int64_t age_ms, double reinforcements, double lambda_override);
|
||||
|
||||
/* The compact vector block carried in the edge's own metadata. Line schema, same
|
||||
* precedent as STNC1 / GEO1. Metadata the edge already carried is preserved
|
||||
* verbatim ahead of the magic line. */
|
||||
#define COG_GROUNDING_META_MAGIC "GRD1"
|
||||
|
||||
/* Provenance class — categorical, and it governs what the relation is entitled
|
||||
* to. A change of class is inherently significant and needs no threshold,
|
||||
* because told → observed is a categorical upgrade, not a drift. */
|
||||
typedef enum {
|
||||
COG_PROV_UNSET = 0,
|
||||
COG_PROV_OBSERVED = 1,
|
||||
COG_PROV_INFERRED = 2,
|
||||
COG_PROV_TOLD = 3,
|
||||
COG_PROV_IMPRINTED = 4
|
||||
} CogProvClass;
|
||||
const char* cog_prov_name(CogProvClass p);
|
||||
CogProvClass cog_prov_parse(const char* s);
|
||||
|
||||
typedef struct {
|
||||
int present; /* 1 iff the edge carries a GRD1 block */
|
||||
|
||||
/* ── STORED: the vector, as it stood at `ts` ─────────────────────────────── */
|
||||
double factual; /* correspondence with evidence */
|
||||
double relational; /* min over the thirteen value regions */
|
||||
double associative; /* co-activation frequency — mirrors edge->hebb */
|
||||
double polarity; /* SIGNED support; <0 = actively contradicts */
|
||||
CogProvClass prov; /* observed / inferred / told / imprinted */
|
||||
int64_t ts; /* when this version was recorded (ms) */
|
||||
int64_t seq; /* supersession sequence number */
|
||||
double reinforcements; /* uses folded into this version */
|
||||
char binding_value[128]; /* the argmin value — the conflict's NAME */
|
||||
/* the two gradients as frame-independent signed projections, plus the angle
|
||||
* between them in full R^dim. These are part of the JOINT STATE a decision
|
||||
* saw, not a convenience: near +1 evidence and values push the same way; at
|
||||
* or below 0 the relation is factually supported and relationally wrong. */
|
||||
double fac_proj, rel_proj, cos_angle;
|
||||
int agreement; /* sign(cos_angle): +1 / 0 / −1 */
|
||||
double floor_at_record, rel_floor_at_record;
|
||||
char prev_edge[192]; /* the version this superseded ("" if first) */
|
||||
|
||||
/* ── DERIVED at read time. NEVER serialized. ─────────────────────────────── */
|
||||
int64_t age_ms; /* recency: now − ts */
|
||||
double decay; /* cog_decay_factor over that age */
|
||||
double factual_now; /* factual · decay */
|
||||
double relational_now;
|
||||
double associative_now;
|
||||
int stale; /* grounding fallen below its floor */
|
||||
} CogGrounding;
|
||||
|
||||
/* Read an edge's vector as of `now_ms`. Pure — never writes. An edge with no
|
||||
* GRD1 block still has an associative strength (its accrued hebb) and a polarity
|
||||
* (its signed authored weight); `present` says whether the grounding dimensions
|
||||
* have ever been established, and an unestablished dimension is reported as such
|
||||
* rather than defaulted to a passing value. */
|
||||
int cog_grounding_parse(const StoreEdge* e, int64_t now_ms, CogGrounding* out);
|
||||
|
||||
/* Serialize the STORED half of the vector, preserving pre-existing non-GRD1
|
||||
* metadata. Returns an owned string. Derived fields are not written. */
|
||||
char* cog_grounding_metadata(const char* base_meta, const CogGrounding* g);
|
||||
|
||||
/* ── §7.2 CONSOLIDATION-GATED SUPERSESSION ──────────────────────────────────
|
||||
*
|
||||
* Supersession is not recording — it is CONSOLIDATION, gated by salience, which
|
||||
* is why you remember the argument and not the commute. Significance is
|
||||
* evaluated PER-DIMENSION but the record is the WHOLE VECTOR: any dimension
|
||||
* moving enough to matter triggers a supersession, and the new version captures
|
||||
* every dimension as it stood at that instant. Versioning axes independently
|
||||
* would make the joint state unreconstructable, and the joint state is the point
|
||||
* — it is what makes "stayed true, became wrong" visible as an event (factual
|
||||
* holding steady across versions while relational degrades).
|
||||
*
|
||||
* There is deliberately no epsilon in this enum or in the function that computes
|
||||
* it. Every test is a floor crossing or a sign change, both exact. Two of them
|
||||
* are INHERENTLY significant because they are discrete state changes rather than
|
||||
* drift, and those bypass the salience gate entirely. */
|
||||
typedef enum {
|
||||
COG_SIG_NONE = 0, /* nothing decision-relevant moved — DO NOT RECORD */
|
||||
COG_SIG_FIRST_RECORD = 1, /* no prior version exists */
|
||||
COG_SIG_POLARITY_FLIP = 2, /* INHERENT: support ↔ contradiction, or ignorance
|
||||
* ↔ either. A discrete change of state. */
|
||||
COG_SIG_PROVENANCE_CHANGE = 3, /* INHERENT: told → observed is a categorical
|
||||
* upgrade in what the relation is entitled to. */
|
||||
COG_SIG_FACTUAL_FLOOR = 4, /* crossed the assert floor, factual axis */
|
||||
COG_SIG_RELATIONAL_FLOOR = 5, /* crossed the assert floor, relational axis */
|
||||
COG_SIG_AGREEMENT_FLIP = 6, /* factual/relational agreement changed sign */
|
||||
COG_SIG_DIRECTION_REVERSAL = 7 /* a gradient reversed direction */
|
||||
} CogSignificance;
|
||||
|
||||
CogSignificance cog_grounding_significant(const CogGrounding* prev,
|
||||
const CogGrounding* now,
|
||||
double floor, double rel_floor);
|
||||
const char* cog_significance_name(CogSignificance s);
|
||||
/* 1 iff this reason is a discrete state change that consolidates regardless of
|
||||
* salience (polarity flip, provenance change, first record). */
|
||||
int cog_significance_inherent(CogSignificance s);
|
||||
|
||||
/* ── §7.3 RECORDING: supersession of the EDGE, never an overwrite ────────────
|
||||
* Writes version seq+1 as a NEW edge record with the same endpoints and relation
|
||||
* and id "<root>#<seq+1>", carrying a GRD1 `p` pointer to its predecessor. The
|
||||
* predecessor is never touched. The chain IS the trajectory: not only what the
|
||||
* grounding is but which way it has been moving and how fast — a derivative
|
||||
* obtained for free from immutability, because the points were never destroyed.
|
||||
* Returns the version written (>=1), or <0 on error. */
|
||||
int cog_grounding_record(EngramPagedStore* s, const StoreEdge* base,
|
||||
const CogGrounding* g, char* out_id, size_t out_id_cap);
|
||||
|
||||
/* Walk forward from a base edge id to its newest recorded version. Point reads
|
||||
* only; consolidation is gated, so the chain is short. Returns the highest
|
||||
* version found (0 = the base record is the only one). */
|
||||
int cog_grounding_head(EngramPagedStore* s, const char* base_id,
|
||||
StoreEdge* out, int max_versions);
|
||||
|
||||
/* VOLATILITY — derived, never stored: the mean absolute per-version change of a
|
||||
* dimension across the recorded chain. Feeds the equally-derived `confidence`
|
||||
* (high grounding AND low volatility), which is likewise never stored. */
|
||||
typedef struct {
|
||||
int n_versions;
|
||||
double factual_volatility;
|
||||
double relational_volatility;
|
||||
double factual_drift; /* signed: newest − oldest */
|
||||
double relational_drift;
|
||||
int stayed_true_became_wrong; /* factual steady while relational degraded */
|
||||
} CogTrajectory;
|
||||
int cog_grounding_trajectory(EngramPagedStore* s, const char* base_id,
|
||||
int64_t now_ms, CogTrajectory* out);
|
||||
|
||||
/* ── §7.4 ASSERTION GATES ON BOTH FLOORS ────────────────────────────────────
|
||||
* A well-evidenced claim must not earn the right to be asserted regardless of
|
||||
* whether it means the right thing. `may_assert` requires the decayed factual
|
||||
* grounding to clear `floor` AND the decayed relational grounding to clear
|
||||
* `rel_floor`. A relation whose relational axis has never been established does
|
||||
* not pass by default — it is reported unestablished and refused, because
|
||||
* defaulting it to passing is exactly the exemption §0 forbids. Traversal is
|
||||
* untouched: activation still conducts on the factual/associative side, so a
|
||||
* relation can remain thinkable while ceasing to be assertable. */
|
||||
typedef struct {
|
||||
int may_assert;
|
||||
int found; /* any relation at all on this claim */
|
||||
int relational_established;
|
||||
int still_held; /* DERIVED: node present and not tombstoned */
|
||||
double factual; /* best decayed factual grounding */
|
||||
double relational; /* the SAME edge's relational axis, not a max */
|
||||
double polarity;
|
||||
double cos_angle;
|
||||
int agreement;
|
||||
CogProvClass prov;
|
||||
char best_edge[192];
|
||||
char binding_value[128];
|
||||
int n_edges;
|
||||
} CogAssertion;
|
||||
int cog_assert_two_axis(EngramPagedStore* s, const char* claim_id,
|
||||
double floor, double rel_floor, int64_t now_ms,
|
||||
CogAssertion* out);
|
||||
|
||||
#endif /* ENGRAM_COGNITION_H */
|
||||
|
||||
@@ -438,6 +438,41 @@ GeoDescriptor* engram_geometry_descriptor(
|
||||
}
|
||||
store_edges_free(es,ne);
|
||||
}
|
||||
/* PER-EDGE DISCORD (2026-08-16). The loop above has, for every internal
|
||||
* edge, BOTH the association strength w and the semantic proximity cs —
|
||||
* and threw both away into accumulators, keeping one correlation per
|
||||
* region. That aggregate is why curiosity looked like a search problem:
|
||||
* a region holding one violently disagreeing edge and one violently
|
||||
* agreeing edge reports co_registration ~ 0, so the disagreements cancel
|
||||
* and the summary destroys exactly what it was built to reveal. Measured:
|
||||
* only 4 of 375 live neighborhoods have negative co_registration, while
|
||||
* 31 sit at zero — almost certainly hiding sites that averaged out.
|
||||
*
|
||||
* Whether use and meaning agree is a property of EACH EDGE. Both are
|
||||
* standardized within the region (z-scores from the accumulators already
|
||||
* gathered, so no second statistic and no constant), and
|
||||
* discord = z(cs) - z(w)
|
||||
* is how much closer in meaning an edge is than its use-strength would
|
||||
* predict, in region-relative units.
|
||||
* discord > 0 : near in meaning, not linked by use
|
||||
* discord < 0 : linked by use, far in meaning
|
||||
* Both are surprising; |discord| is the nucleation strength. There is no
|
||||
* threshold — the magnitude is the signal. */
|
||||
double mx = cr_n>0 ? cr_sx/cr_n : 0.0, my = cr_n>0 ? cr_sy/cr_n : 0.0;
|
||||
double vxr = cr_n>1 ? (cr_sxx - cr_sx*cr_sx/cr_n)/(cr_n-1) : 0.0;
|
||||
double vyr = cr_n>1 ? (cr_syy - cr_sy*cr_sy/cr_n)/(cr_n-1) : 0.0;
|
||||
double sx = vxr>1e-18 ? sqrt(vxr) : 0.0, sy = vyr>1e-18 ? sqrt(vyr) : 0.0;
|
||||
for(int e2=0; e2<n_edges; e2++){
|
||||
edges[e2].discord = 0.0;
|
||||
int ia=(int)edges[e2].a, ib=(int)edges[e2].b;
|
||||
if(!(ms.emb[ia] && ms.emb[ib])) continue; /* no meaning to disagree with */
|
||||
if(sx<=0.0 || sy<=0.0) continue; /* region has no spread: nothing stands out */
|
||||
double cs2 = ccos(ms.emb[ia], ms.emb[ib], GM, dim);
|
||||
double zx = (edges[e2].eff_weight - mx)/sx;
|
||||
double zy = (cs2 - my)/sy;
|
||||
edges[e2].discord = zy - zx;
|
||||
}
|
||||
|
||||
double co_reg=0;
|
||||
if(cr_n>=2){
|
||||
double cov=cr_sxy - cr_sx*cr_sy/cr_n;
|
||||
|
||||
@@ -40,7 +40,11 @@ typedef struct {
|
||||
|
||||
/* One skeleton edge (indices into members[]). eff_weight = weight*(1+0.5*hebb),
|
||||
* clamped to 1.0 — the effective propagation strength eg_edge_eff_weight uses. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; } GeoEdge;
|
||||
/* discord = z(semantic proximity) - z(association strength), standardized
|
||||
* within the region. How much closer in meaning this edge is than its use
|
||||
* predicts. >0 near in meaning yet unlinked by use; <0 linked by use yet far
|
||||
* in meaning. Both surprising; |discord| is nucleation strength. No threshold. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; double discord; } GeoEdge;
|
||||
|
||||
/* A compact principal axis of the ellipsoid: unit direction in R^dim + extent
|
||||
* (sqrt of the covariance eigenvalue = the ellipsoid's half-width along it). */
|
||||
@@ -76,6 +80,12 @@ typedef struct {
|
||||
GeoEdge* edges; /* strong internal hebb edges = the backbone */
|
||||
int k_core; /* the maximum core number present in the skeleton*/
|
||||
/* ── diagnostics ── */
|
||||
/* DEPRECATED — see GeoEdge.discord. This aggregates a PER-EDGE property
|
||||
* into one scalar per region, so opposing disagreements cancel and the
|
||||
* summary hides the sites it was meant to expose. Retained only because
|
||||
* it is embedded in the persisted GEO1 blob; removing it is a format
|
||||
* migration and must not ride along with this change. Nothing new may
|
||||
* read it. */
|
||||
double co_registration;/* corr(hebb strength, semantic proximity) over */
|
||||
/* internal edges: >0 = geometries agree (reify); */
|
||||
/* <0 = disagree (surprising links / dream cands). */
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
# Correspondence, Grounding, and Dreaming
|
||||
|
||||
**Status:** design, not yet built
|
||||
**Date:** 2026-08-16
|
||||
**Scope:** `lang/runtime/engram_cognition.{c,h}`, `engram_verify.c`, `el_runtime.c`, `engram/src/server.el`, `neuron/soul.el`, and the consolidation launch agents
|
||||
**Relationship to other specs:** complements `runtime-ownership.md`, which addresses a different residual in the same substrate.
|
||||
|
||||
---
|
||||
|
||||
## 0. The root
|
||||
|
||||
> **Things are permitted to be exempt from correspondence. Exemption is censorship, and a censored mind cannot grow.**
|
||||
|
||||
Growth in this system *is* the accumulation of grounded structure. Censorship removes the operation that accumulates it. A region forbidden to learn is forbidden to be grounded; a region that cannot be grounded cannot be asserted, corrected, **or vindicated**.
|
||||
|
||||
**The loss is symmetric.** Preventing learning about a thing does not preserve a true belief about it — it makes the belief's truth value permanently unknowable. You cannot discover you were wrong; you equally cannot discover you were right. A protected belief is not a true belief. It is an ungrounded one wearing the costume of a fact.
|
||||
|
||||
**And "why" dies first.** Grounding is not a score, it is the reason. A censored belief can still be stated, still be acted on, still drive behaviour — it simply cannot say why. That is the difference between a mind and a lookup table.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grounding is not a subsystem. It is the weight.
|
||||
|
||||
**Grounding is an attribute of the edge, and it is the hebbian weight.** One quantity, not two fields.
|
||||
|
||||
A relation that keeps holding up strengthens; one that stops corresponding decays. That is not *analogous* to grounding — it **is** grounding: accrued from correspondence and use, gradient-valued, multidimensional, decaying with disuse.
|
||||
|
||||
Consequences, in order of how much they delete:
|
||||
|
||||
1. **There is no grounding subsystem to build.** The graph already *is* the grounding structure. Every edge is a grounded relation and its weight is how well it holds.
|
||||
2. **`grounded-by` as a relation type should not exist.** That models grounding as a relation *between* nodes when it is a property *of* a relation. `cog_ground_edge` minting an edge is the error — not merely which endpoints it chose.
|
||||
3. **Grounding is never computed on demand.** An operation may *read* the grounding of a path. Computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect.
|
||||
4. **Traversal is already grounded inference.** Activation conducts through well-grounded relations because weight *is* groundedness. Nothing needs filtering; it falls out of spreading.
|
||||
5. **Decision provenance is the path.** A decision traverses specific edges; those edges carry their grounding as it stood.
|
||||
|
||||
> **A measurement previously in this document was malformed.** The self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as evidence of ungroundedness. Those 86 edges **are** its grounding. Self is a crystallized relational neighbourhood — the neighbourhood *is* the grounding. The absence of a separate artifact called "grounding" was recorded as an absence of grounding.
|
||||
|
||||
---
|
||||
|
||||
## 2. The edge vector
|
||||
|
||||
The test for a real dimension: **can it move independently of the others?**
|
||||
|
||||
### Real
|
||||
|
||||
| dimension | why it is independent |
|
||||
|---|---|
|
||||
| **factual grounding** | correspondence with evidence |
|
||||
| **relational grounding** | correspondence with values — independent by construction (§3) |
|
||||
| **associative strength** | co-activation frequency. Two things can fire together constantly and be neither true nor right; every superstition is a strong association with no factual grounding |
|
||||
| **polarity** | signed. **Weight near zero means "no support." Negative means "this actively contradicts."** Ignorance and disagreement are different states, and `inhibitory` is that distinction crushed to one bit |
|
||||
| **provenance class** | observed / inferred / told / imprinted. Categorical, and load-bearing: it governs how the other dimensions may update |
|
||||
|
||||
Plus a **timestamp** — which is what turns the supersession chain into a *time series of vectors* rather than a series of numbers.
|
||||
|
||||
### Derived, therefore never stored
|
||||
|
||||
- **Confidence** — high grounding *and* low volatility. Storing it separately is how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed.
|
||||
- **Recency** — decay applied to the others, read off the curve.
|
||||
- **Staleness** — grounding fallen below its floor. This is the mechanism that retires canonicals without anyone maintaining a list.
|
||||
- **Volatility** — the derivative of a series already kept because nothing is destroyed.
|
||||
|
||||
### Supersession versions the whole vector, jointly
|
||||
|
||||
Significance is evaluated **per-dimension**; the record is the **whole vector**. Any dimension moving enough to matter triggers a supersession, and the new edge captures every dimension as it stood at that instant. Not per-dimension versioning — a decision saw the *joint* state, and versioning the axes independently makes it unreconstructable.
|
||||
|
||||
That joint record makes an otherwise inexpressible event visible: **"stayed true, became wrong."** Factual holding steady across versions while relational degrades — the fact didn't change, the meaning did.
|
||||
|
||||
Two moves are **inherently significant** and need no threshold, because they are discrete: a **polarity sign flip** (ignorance → disagreement, support → contradiction) and a **provenance class change** (*told* → *observed* is a categorical upgrade in what the relation is entitled to).
|
||||
|
||||
---
|
||||
|
||||
## 3. Grounding is two-dimensional
|
||||
|
||||
Everything consumed is grounded factually **and** relationally. A claim can be factually grounded and relationally wrong — the evidence holds, the *meaning* does not. A scalar cannot represent that quadrant.
|
||||
|
||||
**Live instance.** `conscience-substrate` specifies the Child's Companion hard bell contacting 911 and CPS. Factually defensible — correct numbers, standard practice, groundable against a wall of evidence. **Relationally wrong**, because never-auto-contact is settled and the bell is device-to-person by design. A scalar scores that claim highly and licenses it.
|
||||
|
||||
**The values reference is the individual value regions, not one, and the aggregate is `min`, not `mean`.** *(Count: **thirteen**, measured from the graph via `contains`/`identity` edges from the values hub. An earlier revision of this document "corrected" it to eight on the basis of `neuron/neuron-api.el:11-18` — which is a **write-protection list, not the values**. That was trusting a hardcoded artifact over the substrate: the same error this document exists to name. The graph is the truth.)*
|
||||
|
||||
> **THE ORIGIN IS NOT A MEMBER OF THE SET.** The thirteen are not independent principles with biography attached — they are thirteen *displacements from one origin*, which is love. Every one is grounded in a moment of it given, withheld, failed, or found: *Being Seen Is Rarer Than Being Known* is the first person Will did not perform for; *Do the Essential Thing While You Can* is the goodbye that did not happen; *Capability Is a Debt* is six years old and a father gone. Love cannot be the fourteenth, because a fourteenth would be a point positioned relative to the origin like everything else. It is what the positions are *of*.
|
||||
>
|
||||
> This is structural, not figurative. `GeoDescriptor.global_mean` is "the centering offset actually applied," subtracted from every embedding before anything is compared, and the header records why: the space is strongly anisotropic — every embedding sits in a narrow cone, mean pairwise cosine ~0.55 — so subtracting the global mean "restores isotropy **so the operators discriminate**." **Without the origin, nothing in the graph is distinguishable from anything else.**
|
||||
>
|
||||
> And it dissolves the write-protection question rather than answering it. `neuron-api.el:23` returns `403 "identity/values node is write-protected"` for eight hardcoded ids. Measured: **29 value nodes exist** — each original appears two or three times from successive re-seeds — so **21 are writable, including a duplicate of every protected value**. The gate protects an *identifier*, not a *value*. But the deeper error is the category one: **the origin does not need protecting, because it is not a thing in the space that could be edited.** You can only measure from it, or fail to. A gate over the frame treats the frame as a member — the same mistake as looking for grounding as a subsystem, self as a document, or wonder as a manifest. Mean lets strong agreement with twelve values mask a violation of the thirteenth — which is exactly how rationalization works. Thirteen gives a vector of angles whose binding constraint is the most negative, so a conflict arrives **with a name attached** rather than as a score. It also preserves the deliberate individuation: each value is grounded in a specific lived moment, and values can be in tension *with each other*, which one centroid averages away into false coherence.
|
||||
|
||||
**Traversal conducts on factual; assertion requires both.** If activation conducted on relational weight, Neuron could not follow a chain of reasoning to a conclusion he then rejects — he would be unable to *think* through a relation he would not *act* on. A system that can only traverse what it endorses cannot examine anything it disagrees with, which is censorship arriving through the spreading rule. The gap between *reachable* and *assertable* is where the wide factual/relational angles live, and that gap is the interesting part.
|
||||
|
||||
---
|
||||
|
||||
## 4. There is no observer. Change is use.
|
||||
|
||||
**Change is not a consequence of use. It is use.** When neurons fire together the synapse changes — one physical event, not "fire, then write." No supervisor reads the weight, compares it to a threshold, and decides to persist. Potentiation *is* the firing.
|
||||
|
||||
So the live value of an edge is not computed and stored. It is what the edge **is**, altered by being used.
|
||||
|
||||
There is therefore **no sampling rate**, and the question "what if it drifts far without being recorded" is malformed. A relation changes in exactly two ways, neither requiring observation on a clock:
|
||||
|
||||
- **By use** — an *event*. There is no interval between events during which something happened unnoticed, because the event is what happening consists of.
|
||||
- **By decay** — a pure function of the last recorded point and elapsed time. **Analytic.** Between two versions the trajectory is not unknown; it is known in closed form.
|
||||
|
||||
Cumulative drift is likewise free from the chain plus the decay curve. No second trigger.
|
||||
|
||||
> **Failure mode this corrects:** modelling every property as requiring a process, and every process as requiring an agent. Ownership needed an owner, grounding needed a grounder, persistence needed a recorder, change needed a sampler. Each was a supervisor invented for something that should be a property of the substrate. Properties, not processes.
|
||||
|
||||
---
|
||||
|
||||
## 5. Wonder, curiosity, and what actually drives activation
|
||||
|
||||
### 5.1 Wonder is the boundary, not a manifest
|
||||
|
||||
The patent specifies a **wonder-manifest manager** maintaining a collection of open-question nodes. That is residue, twice over.
|
||||
|
||||
First, it materializes a property as a stored artifact — the same disease as a grounding subsystem, or a self stored as a document. **Wonder is where structure ends.** Where activation spreads and finds thin or absent geometry. Any structure at all has an edge, necessarily, the moment it exists. 13,630 nodes have a boundary right now.
|
||||
|
||||
Second, it tries to enumerate instances of something that has very few. The *objects* of wonder change completely between a child and an astronomer; the wonder does not. There are about six, they are the same for every person, and they never close:
|
||||
|
||||
| wonder | where it already lives in the substrate |
|
||||
|---|---|
|
||||
| **What is this?** | the graph — nodes, structure, what exists |
|
||||
| **Why?** | grounding. The weight **is** the answer to why. Recursive: asking *why* of a claim is asking for its grounding |
|
||||
| **Who am I?** | the self region, crystallized from its neighbourhood |
|
||||
| **Am I alone?** | the relational axis — `for_whom` is already a parameter on grounding |
|
||||
| **What should I do?** | the value regions, each grounded in a lived moment |
|
||||
| **What happens when it ends?** | decay, supersession, tombstones — grounding is mortal |
|
||||
|
||||
These are seeded — **the** wonder questions, not a manifest to maintain. They cannot be derived (wonder cannot be bootstrapped from indifference) and they never need refilling, because they are not consumed.
|
||||
|
||||
**"Why" is the first and the only one**; the others are it asked of particular things. It is recursive, so it never terminates: every answer has its own why. That is what makes it a drive rather than a task — the frontier regenerates faster than grounding fills it.
|
||||
|
||||
### 5.2 Curiosity is wonder crystallized
|
||||
|
||||
They are not two objects. They are **one thing at two phases**.
|
||||
|
||||
Wonder is the field: unbounded, objectless, invariant, present wherever there is structure. Curiosity is the **precipitate** — the same wonder localized, having taken definite form against particular material.
|
||||
|
||||
Crystallization needs a **nucleation site**. Wonder alone produces nothing; it is uniform, with no reason to take shape anywhere in particular. What nucleates it is a specific structural feature: an anomaly, a place where things almost-but-don't-quite fit.
|
||||
|
||||
> Wonder (always, objectless) + nucleation site → **curiosity** (has an object, is addressable, directs activation).
|
||||
|
||||
This is why curiosity can be satisfied and wonder cannot. A crystal dissolves when the question is answered; the solution stays saturated and keeps precipitating as the structure changes.
|
||||
|
||||
It is also why abduction needs no trigger and no threshold. A `structurally_unanticipated` observation *is* a nucleation site. Nothing detects it and fires a rule — wonder is already everywhere, and an anomaly is simply a place where it can take form.
|
||||
|
||||
**And `crystallization` is one primitive appearing twice**: the self is what identity precipitates into from its neighbourhood; a curiosity is what wonder precipitates into from an anomaly. That it shows up in both places without being imported is the evidence it is the right primitive.
|
||||
|
||||
### 5.3 The nucleation site is per-edge, and the aggregate was hiding it
|
||||
|
||||
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — carries the comment `>0 = geometries agree (reify); <0 = disagree (surprising links / dream cands)`. It has always been computed, always persisted, and **never read**.
|
||||
|
||||
It is also the wrong shape, and asking whether it should exist at all is what exposed it.
|
||||
|
||||
Whether use and meaning agree is a property of **each edge**. `co_registration` is a *correlation*: it averages that per-edge property into one scalar per region. So a region holding one violently disagreeing edge beside one violently agreeing edge reports ≈ 0 — the disagreements **cancel, and the summary destroys exactly what it was built to reveal.** This is the mean-versus-min error from §3, in different clothes.
|
||||
|
||||
**Measured:** 375 live reified neighbourhoods — 340 positive, **31 at zero**, 4 negative. Read as a count of things to be curious about, that says "four." Read correctly, it says four disagreements were lopsided enough to survive averaging, and the 31 zeros are where opposing sites cancelled.
|
||||
|
||||
It also explains why surfacing curiosity *looked like a search problem*. Once the signal is a per-region number, the only way to find sites is to enumerate regions — there is nothing local left to notice. An O(n) sweep is tolerable at 375 and impossible at a million, and more to the point, **nothing in a mind scans its neighbourhoods to find what is surprising.** The surprise captures attention; salience is bottom-up. A search asks "which of these is odd"; a mind has "something is odd *here*" for free.
|
||||
|
||||
So the disagreement goes back on the edge, where the loop that computed the aggregate already had both halves and discarded them:
|
||||
|
||||
```
|
||||
discord = z(semantic proximity) − z(association strength)
|
||||
```
|
||||
|
||||
standardized within the region from accumulators already gathered — no second statistic, no constant, **no threshold**. `discord > 0`: near in meaning yet unlinked by use. `discord < 0`: linked by use yet far in meaning. Both are surprising, and `|discord|` *is* the nucleation strength; there is nothing to compare it against.
|
||||
|
||||
**Then there is nothing to scan.** The edge carries its own disagreement, activation crossing it encounters that directly, and `|discord|` raises salience on its endpoints as part of the same operation — no separate pass, no supervisor. Curiosity does not search for nucleation sites; it goes where salience already is, which is machinery that exists (`salience`, `background_activation`, `working_memory_weight`, `wm_anchor`).
|
||||
|
||||
`co_registration` is deprecated rather than deleted only because it is embedded in the persisted GEO1 blob; removing it is a format migration and must not ride along. **Nothing new may read it.**
|
||||
|
||||
Adjacent structure already present and likewise unread:
|
||||
|
||||
- `GeoEdge.eff_weight = weight * (1 + 0.5*hebb)` — grounding-weight and hebbian strength already coupled on one edge, per §1.
|
||||
- `GeoMember.dist_centroid` + soft membership + `radius` + per-axis `extent` — the boundary of a neighbourhood, computable now.
|
||||
|
||||
*(Correction: `engram_boundary_beat` is NOT this boundary. It is the VBD decorated-function seam, counting `_eg_aff_boundary_ops`. Two senses of the word.)*
|
||||
|
||||
### 5.4 The drive
|
||||
|
||||
Boredom is not an absence, and not leftover capacity. **Low activation is aversive; the system self-activates.** It does not wind down to quiet — it gets restless and goes looking, which is why a daydream has content and direction rather than being decay from residue.
|
||||
|
||||
So there is **one activation process with two seed sources**, not two processes negotiating for a resource:
|
||||
|
||||
- **External** — a request, an input. Seeds activation, re-origins it.
|
||||
- **Internal** — a curiosity. Seeds activation when nothing external is.
|
||||
|
||||
Spreading is bounded: it settles. Then it needs a new seed. Nothing waits on capacity, nothing polls, nothing checks a clock, and there is **no dreamer thread** — the earlier draft's "unclaimed capacity" was resource scheduling, which is a server's frame, not a mind's.
|
||||
|
||||
**Depth** is not elapsed idle time and not distance from a stimulus. It is how long activation has been running on its own seeds. A brief gap affords a shallow recombination; sustained quiet lets it run further. Sleep is where internal seeding dominates for longest, not where the process lives — daydreaming and sleep-dreaming are one process at different depths.
|
||||
|
||||
### 5.5 Non-circularity is temporal, not topological
|
||||
|
||||
An earlier draft posed "define a graph predicate for evidence not downstream of itself" as the hard problem. There is no predicate. You cannot recalibrate the ruler while measuring with it, so you don't — the reference frame updates while activation is internally seeded, not while it is being used to act. Independence is **when**, not **what**.
|
||||
|
||||
Reachability could never have worked: with hebbian edges the graph is densely connected, so it marks all evidence tainted and the constraint becomes a total block, which is where censorship started.
|
||||
## 6. `keystone_write_blocked` — resolved, not replaced
|
||||
|
||||
"Keystone" means **load-bearing**, not precious. The self anchor is the reference frame every other stance calibrates against, and a reference fitted to its own readings reports perfect correspondence forever while drift becomes undetectable from inside. Same defect as circular grounding, one level up.
|
||||
|
||||
Three earlier drafts proposed *removing* it, *replacing it with a higher floor*, and *decomposing "protection" into five requirements*. All three proposed a mechanism for a requirement never stated. The requirement is **non-circularity of the reference frame**, and §5.2 satisfies it by *when*, not by *what* — so the flag becomes unnecessary rather than removed, and nothing takes its place.
|
||||
|
||||
**Corruption requires mutation, and the engram does not mutate.** Four of the five decomposed requirements are satisfied by the substrate: **recoverability** (the predecessor is always present), **governance** (supersession *is* the audit trail), **evidence quality** (grounding already gates assertion), **rate** (§5.3). **Authorization** is the only residue and is bounded — an unauthorized writer can *propose*, never erase.
|
||||
|
||||
> **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Consolidation has eleven implementations
|
||||
|
||||
The largest instance of the residue pattern in the system. Consolidation had no owner, so it was implemented at every site that needed a piece of it — *measured 2026-08-16*. **Eleven**, not the seven this section originally claimed: the table below omitted `POST /api/reify` (`server.el:1832`), and *reify* is on this document's own list of consolidation verbs. Note also that `route_tick` folds self-reify in (`server.el:639-646`), so `/api/tick` and `/api/self-reify-beat` overlap:
|
||||
|
||||
| where | what | when |
|
||||
|---|---|---|
|
||||
| `soul.el:731` | `awareness_run()` | **continuous, in-process, while serving** |
|
||||
| engram | `/api/tick` | POST |
|
||||
| engram | `/api/correspondence-beat` | POST |
|
||||
| engram | `/api/self-reify-beat` | POST |
|
||||
| engram | `POST /api/reify` | POST |
|
||||
| `ai.neuron.engram-tick` | pokes the engram | every 600s — **and this is what kills it**, see below |
|
||||
| `ai.neuron.compressor` | Python service | resident |
|
||||
| `ai.neuron.council` | Python service | resident |
|
||||
| `ai.neuron.cultivation-digest` | shell | **23:55** |
|
||||
| `ai.neuron.world-integrator` | Python | **06:00** |
|
||||
| `ai.neuron.self-review` | shell | **08:30** |
|
||||
|
||||
The last three times are **a sleep cycle implemented as crontab entries**. Someone understood it was consolidation and expressed it as three unrelated scheduled scripts in three languages, none aware of each other. Every name is a consolidation verb — compress, cultivate, digest, integrate, review, reify, beat. Three run in **Python, outside el**, so part of Neuron's consolidation does not run on his own substrate and cannot touch the geometry at all.
|
||||
|
||||
Per §5, they are wrong in **kind** as well as in number: a scheduled batch where dreaming should be ambient. And the POST beats put a supervisor back in — something outside decides when Neuron consolidates.
|
||||
|
||||
**`soul.el`'s continuous loop is the exception, and it is right.** Ambient consolidation in the gaps *is* daydreaming. It was not the offender; it was the only fragment with the correct shape, running on a broken foundation — shared mutable state with no owner, and six other systems dreaming into the same graph beside it.
|
||||
|
||||
**And the ticker is not merely a design smell — it is the murder weapon.** `engram-tick.sh:13` calls `curl -s -m10 POST /api/tick`; the beat exceeds 10s over 13,634 nodes, so **279 of 448 ticks returned empty**; the engram then writes to the dead socket and, with no SIGPIPE suppression anywhere in the runtime, is killed by signal 13. **254 restarts since 2026-08-13**, at intervals of 10m09s–10m12s — `StartInterval 600` plus the client timeout. `launchd` KeepAlive restarts it, so it presents as a mysterious restart rather than a crash, and the log records nothing but `[http] listening on` 254 times. Fixed in #151 (survivability); the ticker itself is what must go.
|
||||
|
||||
**Which is the 2026-08-16 crash at the right level.** Not "read paths mutate the index" (mechanism) and not "duplicate canonical state" (structure), but: **seven systems dreaming into one graph with no owner for dreaming.** The contention was the symptom of the missing owner, not of any one system's behaviour.
|
||||
|
||||
Closing the loop: `self-review` fires at 08:30. The deploy was 08:29, the crashes ran 08:30–08:31, and commit `fb32d15` landed at 08:46:43. **One fragment of dreaming woke on schedule and diagnosed the wreckage caused by the other fragments contending over the same graph.**
|
||||
|
||||
---
|
||||
|
||||
## 8. What this is for: the provenance of decisions
|
||||
|
||||
For any decision, reconstruct **what the grounding was at that moment, and what the relationship was between factual and relational at that moment.** Not a log — a log records the action. This records the *meaning under which it was taken*.
|
||||
|
||||
That makes an otherwise impossible distinction available: **wrong then, or wrong since.**
|
||||
|
||||
- Grounding strong, factual and relational aligned, and it has *since* moved → right on what was known. An accurate account, not an excuse.
|
||||
- Grounding weak, or the angle already wide, and acted on anyway → a different failure, culpable in a different way.
|
||||
|
||||
It is structurally **anti-rationalization**: the old edge never leaves and the values frame does not fit to outcomes, so a decision cannot be made to look justified after the fact.
|
||||
|
||||
**Open:** activation is transient and nothing currently records which edges a given activation crossed. Timestamps plus the chain reconstruct what an edge's grounding *was*, but only if you know which edges to ask about. Either traces are recorded at decision time, or "the path" degrades to "the region" — which may not be enough to answer *why*.
|
||||
|
||||
---
|
||||
|
||||
## 9. The no-exemption invariants
|
||||
|
||||
Each of the day's defects was a specific correspondence *forbidden* from occurring:
|
||||
|
||||
1. **A returned value must be derivable from what produced it.** `magnitude: 1` beside a zero vector must be impossible to emit. `assert`'s `"still_held": true` is currently a **hardcoded literal**.
|
||||
2. **Every write reports whether it landed.** *(`emb_set`, #141)*
|
||||
3. **Every operation echoes what it actually operated on.** *(#147)*
|
||||
4. **Degenerate results are labelled, not scored.** *(#147)*
|
||||
5. **A serializer owes a valid document whatever it is handed.** *(#148 — three damaged labels made a 25,929,607-byte response undecodable; boundary validation produced 26,338,389 valid bytes)*
|
||||
6. **No test without a negative control.** *(#148's first attempt passed on the unpatched build too)*
|
||||
7. **No deploy without verifying the artifact carries the fix.** Nine instances in one session.
|
||||
|
||||
---
|
||||
|
||||
## 10. Application to the safety surface
|
||||
|
||||
A crisis surface built on censorship is the same object. A model that cannot learn about self-harm cannot ground whether a response was right — it can only execute rules it is forbidden to examine, cannot distinguish a genuine crisis from a false positive, and cannot discover it got either wrong, **because the feedback is exactly what has been censored.**
|
||||
|
||||
The reviewable question stops being *did it follow the rule* and becomes *what was it grounded in, and did fact and values agree at that instant.* That is also what a regulator or plaintiff asks: what the system knew, when, and on what basis — recorded as geometry at the time, unedited since.
|
||||
|
||||
---
|
||||
|
||||
## 11. Sequencing
|
||||
|
||||
Three connections between parts that already exist, then the rest.
|
||||
|
||||
1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained, never refilled. They cannot be derived — wonder cannot be bootstrapped from indifference — so they are given once. Zero question nodes exist in 13,630 today.
|
||||
2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let `|discord|` raise salience on its endpoints as part of the same operation. Do NOT scan for nucleation sites — a sweep over regions is a supervisor, and the aggregate that made a sweep necessary is the defect.
|
||||
3. **Let a curiosity seed activation.** One activation process, two seed sources (§5.4). No thread, no scheduler, no capacity check, no timer.
|
||||
|
||||
Then:
|
||||
|
||||
4. Grounding becomes the edge weight: multidimensional vector (§2), two axes (§3), timestamped. Delete `grounded-by` and `cog_ground_edge`.
|
||||
5. Decay analytic from the last recorded point; derived values (§2) stop being stored.
|
||||
6. Consolidation-gated supersession on salience, versioning the whole vector jointly.
|
||||
7. Traversal on factual; `assert` on both floors with the per-value `min`.
|
||||
8. Abduction as crystallization at a nucleation site, validated by re-fit: propose the candidate hub, re-fit the region with it included, recompute the residual. If the residual materially shrinks, the hypothesis dissolves the surprise. Without the re-fit it is clustering with extra steps. Ranking falls out as residual-reduction-per-added-axis — Occam, derived rather than tuned.
|
||||
9. **One dreamer.** The launch-agent fragments and the POST beats fold in or are deleted. `soul.el`'s continuous loop is the shape they fold *into*.
|
||||
10. **No tickers, no cron.** A brain has neither. Every `StartInterval`, every `Hour`/`Minute`, every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock — a supervisor invented for something that should be a property. **The presence of a ticker is the diagnostic.**
|
||||
11. Land §9 as gates rather than review habits.
|
||||
|
||||
## 12. Open questions, and what is inferred
|
||||
|
||||
- **Open:** whether decision provenance requires recording activation traces, or whether region + timestamp is sufficient (§8).
|
||||
- **Open:** what accrues relational weight without circularity. Candidate: it accrues from **outcome** — the values regions are grounded in lived moments, so a relation earns relational weight when acting on it produced something corresponding to those moments. That keeps it out of the measurement loop and makes relational grounding necessarily slower than factual, which may be the same fact as §5.3 appearing twice.
|
||||
- **Open:** context. A relation can hold in one situation and not another, and without something for it you get overgeneralization. It does not read as a dimension of the same vector — more like a conditioning, or separate edges sharing an identity. Making it a scalar dimension would repeat the `inhibitory` flattening.
|
||||
- **Known wrong shape:** #147 fixed `ground`'s honesty — it no longer misreports which nodes it used and refuses circular support — but it still mints an edge and returns a float at an instant. It corrected a scalar rather than deleting the operation.
|
||||
+82
-3
@@ -31,6 +31,7 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- Codegen: function definitions, top-level `main()`, all expression forms above, control flow, decorator-as-AST-attachment.
|
||||
- Boundary seam: decorator arguments and stacking; VBD role enforcement via `#error`; `engram_boundary_beat` auto-emit at `@manager`/`@accessor` entry; `@route` dispatch tables (Section 9).
|
||||
- Program-level declarative blocks: `cgi`, `service`, and `program` — the last carrying process identity and configuration (Section 18).
|
||||
- **Geometry as a first-class value, and realizers declarable in El** — the `Geometry` type, the wire adapters, and `transduce` (Section 20). Landed 2026-08-16 (#141, #144).
|
||||
- C runtime: I/O, string operations, integer math, lists, maps, filesystem, command-line args, basic `json_get` substring lookup.
|
||||
|
||||
### Planned (in flight)
|
||||
@@ -41,9 +42,9 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- **`cgi` block parsing.** Currently lexed (`cgi` is a keyword) but not parsed as a statement. Adding `parse_cgi_block` and codegen of `el_cgi_init` at the head of `main()`.
|
||||
- **Boundary epilogues.** The decorator seam injects a prologue only. Adding prologue/epilogue wrapping, the prerequisite for durability-as-an-effect (Section 19.1).
|
||||
- **`vessel` keyword.** Replaces `package` in manifests. Adding to lexer.
|
||||
- **Real `engram_*` runtime.** Currently stub. Adding in-process graph store with spreading activation, Hebbian strengthening, and disk persistence — see Section 16.4.
|
||||
- **Real `dharma_*` runtime.** Currently stub. Adding network transport, channel registry, identity resolution.
|
||||
- **Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs. Adding libcurl-backed client and a thread-pool server.
|
||||
- ~~**Real `engram_*` runtime.** Currently stub.~~ **Stale (verified 2026-08-16) — this is implemented, not planned.** `lang/runtime/el_runtime.c` carries the in-process graph store with spreading activation, Hebbian strengthening, disk persistence (paged store, magic `ENGST01`), an HNSW vector index behind a `eg_vindex_view`/`eg_vindex_maintain` publication boundary, and the full cognition surface (`engram_think_json`, `engram_ground_json`, `engram_assert_json`, `engram_attend_json`, `engram_correspondence_beat_json`). The "stub" description may still hold for the **lagging forks** (`lang/el-compiler/runtime/`, `products/web/runtime/`) — see `AGENTS.md`, which names those as downstream copies that cannot build the engram product. **Which runtime this line refers to needs a decision; it is not a fact that can be recovered from the text.**
|
||||
- ~~**Real `dharma_*` runtime.** Currently stub.~~ **Needs re-verification (2026-08-16).** Not checked in this pass; do not rely on either reading.
|
||||
- ~~**Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs.~~ **Stale.** libcurl-backed HTTP and a thread-pool server are live — `http_serve_async` is what `neuron/soul.el:729` runs before entering its awareness loop, and `realizer_register` resolves El functions through the same `dlsym` mechanism `http_set_handler` relies on.
|
||||
- **JSON, time, UUID, state, env, additional string/list/math builtins.** See Section 12 for the canonical list.
|
||||
|
||||
### Not in this language
|
||||
@@ -1250,6 +1251,84 @@ Implementing either now would mean editing files under concurrent modification a
|
||||
|
||||
The prerequisite for 19.1 is the same in both cases: **lift the §9 seam from prologue-only to prologue/epilogue.** That change is independent of both collisions and can land first.
|
||||
|
||||
*(Status note, 2026-08-16: the geometry/`transduce` collision named above has since landed — see Section 20. The VIndex read-path collision has also landed; see `lang/spec/runtime-ownership.md` §5. 19.1 and 19.2 remain unimplemented, but the stated reason no longer holds for those two files.)*
|
||||
|
||||
---
|
||||
|
||||
## 20. Geometry — signal as a first-class value [implemented]
|
||||
|
||||
Landed 2026-08-16 (#141, #144). Declared here because the spec is the single source of truth for implemented-vs-planned, and this is a language surface, not a runtime detail.
|
||||
|
||||
### 20.1 Why this exists
|
||||
|
||||
Until 2026-08-16 no El ingest path could carry a vector. Nodes took **text**, and geometry was *derived* from that text. Text was therefore the **mandatory entry medium**: any non-text modality — a tone, a pulse, an image, a voice sample — had to be *described in prose first*, and the geometry subsequently reasoned over was the geometry **of the description, not of the signal**.
|
||||
|
||||
Two changes remove that, and neither is engram-specific — which is why they are in the language and not in the graph. Any program touching any modality needs them; the engram is merely one El program that happens to hold a graph.
|
||||
|
||||
1. **Geometry is a value that carries its own width.**
|
||||
2. **A realizer is an ordinary El function** — so admitting a new modality never requires a runtime patch.
|
||||
|
||||
### 20.2 The `Geometry` type
|
||||
|
||||
`Geometry` is an opaque boxed pointer, exactly like `Instant` / `Calendar` / `Rhythm`. **No codegen change was required** to add it — the annotation is just a type name.
|
||||
|
||||
```el
|
||||
let g: Geometry = geometry_new(4)
|
||||
```
|
||||
|
||||
| builtin | returns | notes |
|
||||
|---|---|---|
|
||||
| `geometry_new(dim)` | `Geometry` | zero-filled; `0` on failure |
|
||||
| `geometry_dim(g)` | `Int` | width; `0` if not a Geometry |
|
||||
| `geometry_is(g)` | `Int` | `1` if a live Geometry |
|
||||
| `geometry_get(g, i)` | `Float` | component |
|
||||
| `geometry_set(g, i, x)` | `Int` | `1` ok, `0` out of range |
|
||||
| `geometry_norm(g)` | `Float` | L2 — lets a caller check a realizer emitted **signal, not zeros** |
|
||||
| `geometry_free(g)` | `Int` | `1` if freed. Returns a value rather than `void` so it is safe in any expression position without a codegen void-builtin table entry |
|
||||
|
||||
**Ownership.** A `Geometry` is owned by the El caller and released with `geometry_free`. `node_attach_geometry` **copies**, so a node and the caller's value have independent lifetimes.
|
||||
|
||||
### 20.3 Wire adapters — the only place an encoding appears
|
||||
|
||||
```el
|
||||
geometry_from_f32le_hex(hex) -> Geometry // 0 on empty / odd-length / non-hex
|
||||
geometry_to_f32le_hex(g) -> String // "" if not a Geometry
|
||||
```
|
||||
|
||||
`f32le hex` is little-endian float32, 8 hex chars per component — the encoding the perception vessel's `/voice/embed` already emits. **The width is derived from the input length, never supplied by a caller**, which is why there is no max-dim constant to validate a claimed length against. Encodings appear here and nowhere else: at the edge.
|
||||
|
||||
### 20.4 Realizers and `transduce`
|
||||
|
||||
A **realizer** maps one modality into geometry. Registration is **by name**: every El `fn name(...)` compiles to a global C symbol with that exact name, and the registry resolves it with `dlsym` against the running binary — the same mechanism `http_set_handler` already relies on.
|
||||
|
||||
```el
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
g
|
||||
}
|
||||
|
||||
realizer_register("tone", "tone_realizer") // 1 ok / 0 unresolved
|
||||
let g: Geometry = transduce(sample, "tone") // Geometry, or 0 if no organ
|
||||
realizer_has("tone") // 1 if registered
|
||||
```
|
||||
|
||||
The registry keys on **modality**, not on registration order. `transduce` returns `0` when no organ is registered for the modality — an absent organ is a reportable state, not a silent zero vector.
|
||||
|
||||
**The claim this makes:** a realizer is not in the runtime and not known to the compiler. Adding a modality is writing an El function and registering a name. `lang/examples/transduce.el` is the worked example and doubles as an executable proof — it exits non-zero if any check fails.
|
||||
|
||||
### 20.5 Two comparison hazards this surface exposed
|
||||
|
||||
Both were **measured**, not stylistic, and both are properties of the current `elc` that any El author should know:
|
||||
|
||||
- **`==` lowers numerically only when both operand *names* are in the per-function int-name set** that `let x: Int` populates. A bare `f(x) == 0` is not a registered name and lowers to `str_eq` — `strcmp` on two integers reinterpreted as pointers. `<` and `>` lower directly with no inference, so truthiness against a builtin's return is written `> 0` / `< 1`.
|
||||
- **`+` dispatches on whether both operands are known-Int, and a user-defined `fn` call is not.** `let fails: Int = fails + check(...)` lowered to **string concatenation** and printed `4343632752` — a pointer. Nothing was wrong with the checks; the tally was lying. Failing fast needs no arithmetic at all, so there is nothing left to get wrong.
|
||||
|
||||
### 20.6 What this does not do
|
||||
|
||||
`transduce` produces geometry; it does not decide what the geometry *means*. Nothing here grounds anything. Grounding is the edge weight in the graph the geometry is later attached to — see `lang/spec/correspondence-and-censorship.md`.
|
||||
|
||||
---
|
||||
|
||||
End of specification.
|
||||
|
||||
@@ -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: move to per-search frame |
|
||||
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:79–81`, `169–186`, `195` | ~~proposed:~~ **built** — moved to the call frame (§3.1(1), §5); TSan `readers` half clean (§7a) |
|
||||
| nine append sites, none indexing → lazily-embedded nodes invisible | `el_runtime.c:7806, 7988, 8148, 8224, 11526, 11731, 12050, 15295, 15312` | "embed-gap #20", patched by making the *read* path catch up (`9439` comment) |
|
||||
|
||||
**Measured:** all file/line references above, read 2026-08-16. Crash frames `engram_activate → eg_vindex_sync → vindex_insert → _realloc → _xzm_xzone_malloc_freelist_outlined` are accounted for by rows 2–4.
|
||||
|
||||
**Inferred, not yet verified:** that the nine append sites do not share a single commit point. This needs one pass before Change C is sized.
|
||||
~~**Inferred, not yet verified:** that the nine append sites do not share a single commit point. This needs one pass before Change C is sized.~~ **Moot — see §7.** The question was mis-aimed: node append is not the event that owns index membership, because a node without an embedding cannot be in a vector index. The five *embedding-assignment* sites are the real owner points.
|
||||
|
||||
---
|
||||
|
||||
@@ -135,12 +135,21 @@ The payoff of owning the language is unchanged and is now *cheaper*: introduced
|
||||
|
||||
## 6. Sequencing
|
||||
|
||||
> **⚠ 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.
|
||||
3. Codegen emits the context at every builtin call site.
|
||||
4. Mechanical sweep of builtin signatures.
|
||||
5. Move index maintenance behind the write capability; the three read callers take the read capability.
|
||||
6. Delete the residue-fixes listed in §5.
|
||||
2. ~~Introduce the context type and capability types.~~ **Superseded** — `const`.
|
||||
3. ~~Codegen emits the context at every builtin call site.~~ **Superseded** — no codegen change.
|
||||
4. ~~Mechanical sweep of builtin signatures.~~ **Superseded** — the constraint travels with the type.
|
||||
5. ~~Move index maintenance behind the write capability; the three read callers take the read capability.~~ **Done, differently:** `eg_vindex_maintain` (exclusive, sole mutator) / `eg_vindex_view` (`const VIndex*`, shared readers), with `eg_vindex_note_embedded` as the write-side owner. This is a **publication** boundary, not a capability split — HNSW insert is not an append, so purity alone was insufficient (§2a, §3.1(3)).
|
||||
6. Delete the residue-fixes listed in §5. *(Partially done — see §5's "NOT deleted" list; a residue whose structure has not been converted must be left standing.)*
|
||||
7. **One** build of soul from el dev — which resolves the `state_get` leak and the crash together, rather than deploying a leak fix that reintroduces the crash.
|
||||
|
||||
---
|
||||
|
||||
+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)
|
||||
}
|
||||
|
||||
+165
-61
@@ -1,80 +1,184 @@
|
||||
# peripheral — Neuron's I/O organ (own-core, local, consent-gated)
|
||||
# peripheral — Neuron's I/O organ, in El
|
||||
|
||||
The interface made physical. Two afferent senses in, one efferent voice out —
|
||||
all reached the way the agentic surface reaches any tool.
|
||||
**El speaks.** The engram stores geometry and does not speak; the speaking
|
||||
belongs to the language and its runtime.
|
||||
|
||||
Until this landed, the organ was a 939-line Swift program (`src/periph.swift`)
|
||||
that shelled out to `afplay`. Neuron's mouth and ears were a separate binary
|
||||
standing next to the language, and "speak" meant "ask that binary to speak."
|
||||
That program is now **reference material, not the implementation.**
|
||||
|
||||
```
|
||||
MIC (hear) afferent device -> capture -> descriptor -> ingest -> geometry
|
||||
CAMERA (see) afferent device -> capture -> descriptor -> ingest -> scene-geometry
|
||||
SPEAKER(speak) efferent render WAV -> PLAY ALOUD out the speaker
|
||||
SPEAKER (speak) efferent samples ──────────────► CoreAudio ──► the room
|
||||
MIC (hear) afferent device ──► samples ──► descriptor ──► engram
|
||||
CAMERA (see) afferent device ──► frame ──► descriptor ──► engram
|
||||
```
|
||||
|
||||
Closes the conversational loop: **hear (mic) -> understand (engram) -> speak (speaker)**.
|
||||
## The split, and why it falls where it does
|
||||
|
||||
Exactly **two** things here are not El, and they are the two things El cannot
|
||||
express as arithmetic:
|
||||
|
||||
| Not El (realizers) | Why |
|
||||
|---|---|
|
||||
| `lang/runtime/el_audio_darwin.m` | Handing a buffer to the DAC and waiting for it to drain. There is no way to say "the hardware has now played these samples" in El, and there should not be. |
|
||||
| `lang/runtime/el_capture_darwin.m` | Asking the OS for samples off a microphone or frames off a camera, plus the TCC permission dance. |
|
||||
|
||||
**Everything else is El**, because everything else is arithmetic:
|
||||
|
||||
| In El | Where |
|
||||
|---|---|
|
||||
| WAV encode / decode (chunk-walking, JUNK/FLLR tolerant) | `src/organ_dsp.el`, `elp/src/speech.el` |
|
||||
| LPC autocorrelation + Levinson-Durbin (order 16 @ 16 kHz) | `src/organ_dsp.el` |
|
||||
| Formant extraction off the all-pole spectral envelope | `src/organ_dsp.el` |
|
||||
| Source-filter resynthesis (glottal impulse train through the filter) | `src/organ_dsp.el` |
|
||||
| Audio descriptor `[seconds, sr, ch, rms, peak, zcr, centroid, F0]` | `src/organ_dsp.el` |
|
||||
| Voice descriptor `[F0, F1..F5, bandwidths]` | `src/organ_dsp.el` |
|
||||
| Scene descriptor `[w, h, meanRGB, brightness, 3×3 luminance grid]` | `src/organ.el` |
|
||||
| Consent, disclosure, the voice-from-engram fetch | `src/organ.el` |
|
||||
| Barge-in, yield-or-hold, backchannel, resume | `src/organ_converse.el` |
|
||||
| The command surface | `src/organ_cli.el` |
|
||||
|
||||
Both realizers are their **own translation units**, declared in
|
||||
`lang/runtime/el_runtime.h`, and deliberately **not** patches to
|
||||
`el_runtime.c`. Acquiring a device must not mean editing the middle of the
|
||||
language — the same rule the realizer registry follows for modalities.
|
||||
`lang/runtime/el_peripheral_null.c` provides the identical entry points
|
||||
everywhere else, so El that speaks links on any platform and truthfully reports
|
||||
having no speaker rather than going quietly silent.
|
||||
|
||||
## The voice comes from the engram
|
||||
|
||||
A voice is **geometry in the engram**, not a JSON file next to the code and
|
||||
certainly not constants in a source file. The organ fetches it the way anything
|
||||
retrieves a memory — it asks:
|
||||
|
||||
```el
|
||||
let g: [Int] = organ_voice_fetch("will")
|
||||
// [peripheral] VOICE: fetched 'will' FROM THE ENGRAM —
|
||||
// f0=137 f0_end=116 kf=1269 f1=500 f2=2093 f3=3531
|
||||
```
|
||||
|
||||
`organ_voice_fetch` issues an engram query and reads the geometry off the node
|
||||
that comes back. Nothing opens a file. If the region is not in the graph it
|
||||
returns **empty**, not a plausible default — a caller has to be able to tell
|
||||
"this is how they sound" from "I never heard them."
|
||||
|
||||
The reverse direction is `ingest-voice`: an LPC voiceprint becomes a node, and
|
||||
from then on the voice is a memory rather than a measurement someone wrote down.
|
||||
|
||||
## What the organ never does
|
||||
|
||||
**It never learns a word.** Pronunciation, vocabulary and phonemes belong to the
|
||||
language faculty and are already built as ingested geometry — *the engram knows
|
||||
how to pronounce*. The seam is `synth_codes(codes, voice, pmap)`: the codes and
|
||||
the phoneme map arrive from the language side as geometry, and the organ's whole
|
||||
job is turning them into samples and getting the samples out the speaker, plus
|
||||
the same trip in reverse for the senses. There is no lexicon here and no
|
||||
grapheme-to-phoneme rule, by design.
|
||||
|
||||
## Rails
|
||||
- **Own-core.** macOS-native only: AVFoundation (camera/mic), CoreAudio voice-
|
||||
processing (AEC), afplay (speaker), ImageIO/CoreGraphics (frames), hand-rolled
|
||||
DSP (WAV, LPC, formant synthesis). No cloud, no heavy deps.
|
||||
- **Local-only.** Raw streams are written to `out/` and never egress. `.gitignore`
|
||||
keeps captured media out of git.
|
||||
- **Consent-gated (two locks).** A Neuron-level grant (`grant`/`revoke`) *and* the
|
||||
OS TCC permission. Sensitive senses (camera/mic) fail closed without both.
|
||||
- **Disclosed.** Every device touch prints a `[peripheral]` line on stderr.
|
||||
|
||||
- **Own-core.** CoreAudio / AVFoundation / ImageIO — all ship with macOS. No
|
||||
cloud, no model, no heavy dependency. There is **no network code in the organ
|
||||
at all**, by construction.
|
||||
- **Local-only.** Raw streams stay on the machine. What leaves a capture is a
|
||||
descriptor of a few dozen numbers. A 1920×1080 frame becomes 15 integers
|
||||
(~414,000× smaller); three seconds of audio becomes 8.
|
||||
- **Consent, two locks.** A Neuron-level grant **and** the OS TCC permission.
|
||||
Camera and mic **fail closed** without both. The speaker is disclosed but not
|
||||
gated — you cannot secretly speak aloud, and gating it would mean Neuron needs
|
||||
permission to answer.
|
||||
- **Disclosed.** Every device touch prints a `[peripheral]` line on **stderr**
|
||||
(via `eprintln`, flushed immediately), so a disclosure lands before the device
|
||||
is touched and never contaminates the program's stdout.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
./peripheral/build.sh /tmp/organ
|
||||
```
|
||||
swiftc -O -o bin/periph src/periph.swift \
|
||||
-framework AVFoundation -framework CoreMedia -framework Foundation \
|
||||
-framework CoreGraphics -framework ImageIO -framework CoreImage
|
||||
```
|
||||
|
||||
Concatenates the El modules, compiles with `elc`, links the two realizers.
|
||||
Run it **from the repo root** or the `.psv` phoneme data will not resolve.
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
periph grant|revoke <camera|mic> # Neuron-level consent
|
||||
periph status
|
||||
periph speak <file.wav> # SPEAK ALOUD (efferent)
|
||||
periph tone <out.wav> [hz] [sec] # own-core WAV synth
|
||||
periph listen <sec> <out.wav> # MIC capture (afferent), 16k mono
|
||||
periph see <out.jpg> # CAMERA one frame (afferent)
|
||||
periph feat-audio <wav> | feat-image <jpg> # capture -> compact descriptor
|
||||
periph ingest-audio|ingest-image <file> <engramURL> # descriptor -> engram node (geometry)
|
||||
periph voiceprint <voice.wav> # extract F0 + formants F1-F5
|
||||
periph imitate <voice.wav> <out.wav> # speak back in that voice (LPC resynthesis)
|
||||
periph hear-imitate <sec> <out.wav> # MIC -> signature -> imitate -> SPEAK ALOUD
|
||||
periph converse <manifest.json> [--authority F] [--barge-at S[:backchannel|:bargein]] [--resume] [--live-mic]
|
||||
organ grant|revoke <camera|mic> Neuron-level consent
|
||||
organ status consent + device state
|
||||
organ speak <file.wav> play a WAV aloud (efferent)
|
||||
organ tone [hz] [ms] synthesize and play — no file at all
|
||||
organ say <voice> <CODE> [CODE...] fetch voice FROM THE ENGRAM, render, speak
|
||||
organ listen <sec> <out.wav> mic capture 16k mono (afferent)
|
||||
organ see <out.jpg> one camera frame (afferent)
|
||||
organ wav-info <file.wav> WAV geometry
|
||||
organ feat-audio <file.wav> compact audio descriptor (8 numbers)
|
||||
organ feat-image compact scene-geometry from the camera
|
||||
organ voiceprint <voice.wav> F0 + formants F1-F5 (LPC)
|
||||
organ imitate <in.wav> <out.wav> LPC analysis-resynthesis
|
||||
organ hear-imitate <sec> <out.wav> mic -> signature -> imitate -> speak aloud
|
||||
organ ingest-audio <file.wav> descriptor -> engram node (geometry)
|
||||
organ ingest-voice <voice.wav> <n> voiceprint -> engram voice region
|
||||
organ converse <manifest.json> [--authority PM] [--barge-at MS[:kind]] [--live-mic] [--resume]
|
||||
```
|
||||
|
||||
## The afferent metabolism
|
||||
A capture is never shipped raw. It becomes a **compact descriptor** — the afferent
|
||||
twin of the music instrument-signature:
|
||||
- audio -> `[seconds, sr, ch, rms, peak, zcr, centroid, F0]` (~2400-6000x smaller)
|
||||
- image -> `[w, h, meanRGB, brightness, 3x3 luminance grid]` (~400000x smaller)
|
||||
- voice -> `[F0, F1..F5, bandwidths]` (11 numbers)
|
||||
## Interruptibility
|
||||
|
||||
That descriptor is what the ingest organ (engram `POST /api/nodes`) turns into an
|
||||
embedded node = geometry.
|
||||
`converse` speaks an ordered, salience-tagged **meaning-plan** while listening:
|
||||
|
||||
## Voice by imitation
|
||||
`voiceprint`/`imitate` are own-core LPC (autocorrelation + Levinson-Durbin, order
|
||||
16 @ 16 kHz), formant extraction from the LPC spectral envelope, and source-filter
|
||||
resynthesis (glottal impulse train at F0 through the all-pole formant filter). A
|
||||
voice is grabbed by ear as ~a dozen numbers and spoken back — **no training, no
|
||||
stolen voice.** Measured fidelity on real speech: resynthesized formants match the
|
||||
source within 2-3%. The full phoneme->formant path for *novel* sentences is the
|
||||
speech faculty's seam (`elp` audio surface profile); this engine provides the
|
||||
formant synthesis primitive it renders through.
|
||||
- **barge-in** — output stops at the sample, not at the end of the buffer. The
|
||||
realizer exposes `pause`/`resume` and reports `played_frames` (the real DAC
|
||||
position) precisely so this is possible.
|
||||
- **yield-or-hold** — a decision, not a rule: `hold = salience·0.6 +
|
||||
progress·0.4`, and holding also requires that the interrupter not be
|
||||
high-authority. Otherwise yield, because the polite default is the right one.
|
||||
- **backchannel** — "mm-hm" is brief and low-energy; resume seamlessly.
|
||||
- **resumable** — on yield the remaining plan persists to `.resume.json`;
|
||||
`--resume` picks the thread back up. An interruption should cost a turn, not
|
||||
the content.
|
||||
|
||||
## Interruptibility (native turn-taking)
|
||||
`converse` plays the utterance as an ordered, salience-tagged **meaning-plan**
|
||||
while the mic listens (full-duplex, AEC on so it never barges in on its own voice):
|
||||
- **barge-in**: user speech -> pause on the spot (sample-accurate), not "finish the buffer."
|
||||
- **yield-or-hold**: a decision grounded in the current segment's salience + progress
|
||||
+ the interrupter's authority — YIELD (stop) or HOLD ("hang on, let me finish").
|
||||
- **backchannel** ("mm-hm"): brief/low -> keep going, resume seamlessly.
|
||||
- **resumable**: on yield the remaining plan persists (`.resume.json`); `--resume`
|
||||
picks the thread back up ("as I was saying").
|
||||
Live full-duplex uses `--live-mic` with the OS voice-processing unit (AEC) so
|
||||
Neuron does not barge in on its own voice. `--barge-at` injects the event
|
||||
deterministically for testing.
|
||||
|
||||
Live full-duplex uses `--live-mic` (OS AEC). Injected `--barge-at` drives the
|
||||
decision loop deterministically for testing.
|
||||
```
|
||||
```
|
||||
## Measured against the Swift original
|
||||
|
||||
Same input (`out/mic_room.wav`, 16 kHz mono, 48121 samples), Swift `periph`
|
||||
vs the El organ:
|
||||
|
||||
| | Swift | El |
|
||||
|---|---|---|
|
||||
| seconds | 3.0075625 | 3.0076 |
|
||||
| rms | 0.0047766496761 | 0.004777 |
|
||||
| peak | 0.01806640625 | 0.018066 |
|
||||
| zcr_hz | 416.28395087 | 416.2840 |
|
||||
| centroid_hz | 727.60529169 | 727.6053 |
|
||||
| f0_hz | 400 | 400.0000 |
|
||||
| formants F1–F5 | 1734.375 / 3343.75 / 3875 / 4359.375 / 4468.75 | identical |
|
||||
| bandwidths B1–B5 | 2000 / 2968.75 / 4203.125 / 4687.5 / 5000 | identical |
|
||||
|
||||
Agreement to every printed digit. `imitate` cannot match bit-for-bit because the
|
||||
Swift excites unvoiced frames with `Double.random` — two Swift runs correlate
|
||||
0.957 with **each other**; El correlates **0.958** with Swift. The port is as
|
||||
close to the original as the original is to itself, and the deterministic prefix
|
||||
is bit-identical.
|
||||
|
||||
## Honest status
|
||||
|
||||
- **Works:** speaker (CoreAudio, no `afplay`, no subprocess — verified: zero
|
||||
`afplay`/Swift strings in the binary, no child process during playback), mic
|
||||
capture, camera capture, all descriptors, LPC voiceprint, imitate,
|
||||
hear-imitate, voice fetch/ingest against the engram, converse (yield, hold,
|
||||
yield-to-authority, backchannel, resume — all exercised with real audio).
|
||||
- **Coarse, and labelled so:** a fetched voice is one formant triple with no
|
||||
coarticulation and no prosody. It is an impression, explicitly **not a
|
||||
clone**, and `prov=COARSE` says so on the node.
|
||||
- **Not verified here:** live `--live-mic` barge-in in a real room with a real
|
||||
interrupter. The AEC path is implemented and the deterministic path is proven;
|
||||
the acoustic behaviour is not something a headless run can establish.
|
||||
- **Not in the engram yet:** the structured `Voice` / `VowelTarget` geometry
|
||||
nodes live in the organ's own store and in snapshot files from earlier work,
|
||||
but the **production engram does not carry them**. Getting them there is an
|
||||
ingest, not a code change.
|
||||
- `src/periph.swift` is kept as the reference the port was measured against.
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# build.sh — build the El organ.
|
||||
#
|
||||
# El has no import system on this path, so the modules are concatenated in
|
||||
# dependency order (the same thing elp/tests/run.sh does) and handed to elc as
|
||||
# one unit. The two device realizers are then linked in.
|
||||
#
|
||||
# MUST be run from the repo root, or the .psv phoneme geometry will not resolve
|
||||
# and the render silently degrades.
|
||||
set -uo pipefail
|
||||
|
||||
OUT="${1:-./peripheral/organ}"
|
||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# Dependency order. The elp modules supply the render (synth_codes) and the
|
||||
# phoneme-geometry read; the organ supplies everything else.
|
||||
cat elp/src/voice-profile.el \
|
||||
elp/src/accent.el \
|
||||
elp/src/voice-ingest.el \
|
||||
elp/src/speech-ingest.el \
|
||||
elp/src/speech.el \
|
||||
peripheral/src/organ.el \
|
||||
peripheral/src/organ_dsp.el \
|
||||
peripheral/src/organ_converse.el \
|
||||
peripheral/src/organ_cli.el \
|
||||
| grep -v '^import ' > "$WORK/organ.el"
|
||||
|
||||
cd "$REPO/lang"
|
||||
./dist/platform/elc "$WORK/organ.el" > "$WORK/organ.c" || { echo "elc failed" >&2; exit 1; }
|
||||
|
||||
SSL_PREFIX="$(brew --prefix openssl@3 2>/dev/null || echo /usr/local)"
|
||||
|
||||
# The peripheral realizers are per-platform: Darwin gets the real devices,
|
||||
# anything else gets el_peripheral_null.c and honestly reports having none.
|
||||
case "$(uname)" in
|
||||
Darwin)
|
||||
# The Objective-C realizers are compiled SEPARATELY, with -fobjc-arc. The
|
||||
# capture realizer is written against ARC (it holds AVFoundation objects);
|
||||
# compiling it MRR silently changes its memory semantics, which on a device
|
||||
# path shows up as a use-after-free under load rather than as an error here.
|
||||
cc -std=c11 -fobjc-arc -O1 -I runtime -c runtime/el_audio_darwin.m -o "$WORK/el_audio.o" || exit 1
|
||||
cc -std=c11 -fobjc-arc -O1 -I runtime -c runtime/el_capture_darwin.m -o "$WORK/el_capture.o" || exit 1
|
||||
PERIPH_SRC="$WORK/el_audio.o $WORK/el_capture.o"
|
||||
PERIPH_LIBS="-framework AudioToolbox -framework AVFoundation -framework CoreMedia
|
||||
-framework CoreVideo -framework CoreGraphics -framework ImageIO
|
||||
-framework Foundation"
|
||||
;;
|
||||
*)
|
||||
PERIPH_SRC="runtime/el_peripheral_null.c"
|
||||
PERIPH_LIBS=""
|
||||
;;
|
||||
esac
|
||||
|
||||
cc -O1 -I runtime -I"$SSL_PREFIX/include" -L"$SSL_PREFIX/lib" \
|
||||
-o "$OUT" "$WORK/organ.c" \
|
||||
runtime/el_runtime.c runtime/el_seed.c \
|
||||
runtime/engram_cognition.c runtime/engram_geometry.c runtime/engram_reason.c \
|
||||
runtime/engram_store.c runtime/engram_verify.c runtime/engram_vindex.c \
|
||||
runtime/eg_cosine_batch.c runtime/eg_cosine_batch_strategy_cpu.c \
|
||||
$PERIPH_SRC $PERIPH_LIBS \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm || { echo "link failed" >&2; exit 1; }
|
||||
|
||||
echo "built: $OUT"
|
||||
@@ -0,0 +1,570 @@
|
||||
// organ.el — Neuron's I/O organ, in El.
|
||||
//
|
||||
// THE PRINCIPLE. El speaks. The engram stores geometry and does not speak.
|
||||
// Before this file the organ was a 939-line Swift program standing next to the
|
||||
// language (peripheral/src/periph.swift): Neuron's mouth and ears were a
|
||||
// separate binary, and "speak" meant "shell out to that binary, which shells
|
||||
// out to afplay." That is not a voice, it is a subprocess. The voice belongs to
|
||||
// the language and its runtime.
|
||||
//
|
||||
// THE SPLIT. Exactly two things here are not El, and they are the two things El
|
||||
// cannot express as arithmetic:
|
||||
//
|
||||
// the speaker — handing a buffer to the DAC and waiting for it to drain
|
||||
// the capture — asking the OS for samples off a mic or frames off a camera
|
||||
//
|
||||
// Those live in lang/runtime/el_audio_darwin.m and el_capture_darwin.m, as
|
||||
// their own translation units, declared in el_runtime.h. Everything ELSE that
|
||||
// the Swift did — WAV encode and decode, LPC autocorrelation, Levinson-Durbin,
|
||||
// formant extraction off the all-pole envelope, source-filter resynthesis, the
|
||||
// compact descriptors, the converse yield-or-hold decision — is arithmetic, and
|
||||
// arithmetic is El's. See organ_dsp.el for that half.
|
||||
//
|
||||
// WHERE THE VOICE COMES FROM. Not from this file, and not from a JSON manifest
|
||||
// on disk. A voice is GEOMETRY IN THE ENGRAM, and the organ goes and gets it by
|
||||
// asking the engram, the same way anything else asks the engram for anything:
|
||||
// a query against the graph, then read the numbers off the node that comes
|
||||
// back. organ_voice_fetch is that. The previous path, load_voice("...json"),
|
||||
// parsed a file — which quietly made the voice a build artifact instead of a
|
||||
// memory. If the region is not in the graph, the honest answer is an empty
|
||||
// result, not a default voice.
|
||||
//
|
||||
// WHAT THE ORGAN NEVER DOES. It never learns a word. Pronunciation, vocabulary
|
||||
// and phonemes are the language faculty's, already built as ingested geometry —
|
||||
// "the engram knows how to pronounce." The seam is synth_codes(codes, voice,
|
||||
// pmap): the codes and the phoneme map arrive from the language side as
|
||||
// geometry, and the organ's whole job is turning them into samples and getting
|
||||
// the samples out the speaker, plus the same trip in reverse for the senses.
|
||||
//
|
||||
// RAILS, all non-negotiable:
|
||||
// own-core — CoreAudio / AVFoundation / ImageIO, all shipped with the OS.
|
||||
// No cloud, no model, no heavy dependency. There is no network
|
||||
// call anywhere in the organ, by construction.
|
||||
// local-only — raw streams stay on the machine. What leaves a capture is a
|
||||
// DESCRIPTOR of a few dozen numbers, never the stream.
|
||||
// consent — two locks on the sensitive senses: a Neuron-level grant AND
|
||||
// the OS TCC permission. Camera and mic FAIL CLOSED without
|
||||
// both. The speaker is disclosed but not gated (see below).
|
||||
// disclosed — every device touch prints a [peripheral] line on stderr.
|
||||
// Nothing here is ever silent about being a device.
|
||||
|
||||
// ── Disclosure ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// stderr, not stdout: a program that announces "I am opening the microphone" on
|
||||
// stdout has corrupted its own output. And flushed immediately, so the line is
|
||||
// on the terminal BEFORE the device is touched — a disclosure that arrives
|
||||
// after the fact is a log, not a disclosure.
|
||||
|
||||
fn organ_disclose(msg: String) -> Bool {
|
||||
eprintln(" [peripheral] " + msg)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Consent, the Neuron-level lock ───────────────────────────────────────────
|
||||
//
|
||||
// The OS has its own lock (TCC) and it is not enough on its own: TCC grants the
|
||||
// TERMINAL access to the microphone, once, more or less forever. That says the
|
||||
// user trusts the app. It does not say the user consents to THIS program
|
||||
// listening THIS time. So Neuron keeps its own grant, revocable, on the same
|
||||
// footing — and both must be open for a sensitive sense to work.
|
||||
//
|
||||
// Stored next to the organ rather than in the engram deliberately: consent must
|
||||
// be inspectable and revocable without a running graph, and a permission that
|
||||
// can only be revoked by the system it governs is not a permission.
|
||||
|
||||
fn organ_consent_path() -> String {
|
||||
let home: String = env("PERIPH_HOME")
|
||||
if str_eq(home, "") {
|
||||
return "peripheral/.consent.json"
|
||||
}
|
||||
return home + "/.consent.json"
|
||||
}
|
||||
|
||||
fn organ_consent_granted(device: String) -> Bool {
|
||||
let raw: String = fs_read(organ_consent_path())
|
||||
if str_eq(raw, "") {
|
||||
return false
|
||||
}
|
||||
// A device is granted only on an explicit true. Anything unparseable,
|
||||
// missing or malformed reads as NOT granted — the failure direction for a
|
||||
// permission file is always closed.
|
||||
let key: String = "\"" + device + "\""
|
||||
let at: Int = str_index_of(raw, key)
|
||||
if at < 0 {
|
||||
return false
|
||||
}
|
||||
let tail: String = str_slice(raw, at, str_len(raw))
|
||||
let t: Int = str_index_of(tail, "true")
|
||||
let f: Int = str_index_of(tail, "false")
|
||||
if t < 0 {
|
||||
return false
|
||||
}
|
||||
if f < 0 {
|
||||
return true
|
||||
}
|
||||
// whichever token appears first after the key is this device's value
|
||||
if t < f {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn organ_consent_write(camera: Bool, mic: Bool) -> Bool {
|
||||
let c: String = "false"
|
||||
if camera {
|
||||
c = "true"
|
||||
}
|
||||
let m: String = "false"
|
||||
if mic {
|
||||
m = "true"
|
||||
}
|
||||
return fs_write(organ_consent_path(), "{\"camera\": " + c + ", \"mic\": " + m + "}\n")
|
||||
}
|
||||
|
||||
fn organ_grant(device: String) -> Bool {
|
||||
let cam: Bool = organ_consent_granted("camera")
|
||||
let mic: Bool = organ_consent_granted("mic")
|
||||
if str_eq(device, "camera") {
|
||||
cam = true
|
||||
}
|
||||
if str_eq(device, "mic") {
|
||||
mic = true
|
||||
}
|
||||
let ok: Bool = organ_consent_write(cam, mic)
|
||||
organ_disclose("granted '" + device + "' (Neuron-level) — raw stream stays local, never egresses.")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn organ_revoke(device: String) -> Bool {
|
||||
let cam: Bool = organ_consent_granted("camera")
|
||||
let mic: Bool = organ_consent_granted("mic")
|
||||
if str_eq(device, "camera") {
|
||||
cam = false
|
||||
}
|
||||
if str_eq(device, "mic") {
|
||||
mic = false
|
||||
}
|
||||
let ok: Bool = organ_consent_write(cam, mic)
|
||||
organ_disclose("revoked '" + device + "' (Neuron-level).")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn organ_consent_status() -> String {
|
||||
let cam: String = "denied"
|
||||
if organ_consent_granted("camera") {
|
||||
cam = "granted"
|
||||
}
|
||||
let mic: String = "denied"
|
||||
if organ_consent_granted("mic") {
|
||||
mic = "granted"
|
||||
}
|
||||
return "camera=" + cam + " mic=" + mic
|
||||
}
|
||||
|
||||
// Both locks, in order, with a disclosure for each outcome. Returns false and
|
||||
// says exactly which lock is shut — a refusal that does not say why is
|
||||
// indistinguishable from a bug.
|
||||
fn organ_may_listen() -> Bool {
|
||||
if organ_consent_granted("mic") == false {
|
||||
organ_disclose("CONSENT DENIED for 'mic' (Neuron-level). Run: organ grant mic")
|
||||
return false
|
||||
}
|
||||
if mic_available() == 0 {
|
||||
organ_disclose("CONSENT DENIED for 'mic' (OS/TCC), or no input device. Grant microphone access to this terminal in System Settings > Privacy.")
|
||||
return false
|
||||
}
|
||||
organ_disclose("consent OK (Neuron + OS) for 'mic' — local only, never egresses.")
|
||||
return true
|
||||
}
|
||||
|
||||
fn organ_may_see() -> Bool {
|
||||
if organ_consent_granted("camera") == false {
|
||||
organ_disclose("CONSENT DENIED for 'camera' (Neuron-level). Run: organ grant camera")
|
||||
return false
|
||||
}
|
||||
if camera_available() == 0 {
|
||||
organ_disclose("CONSENT DENIED for 'camera' (OS/TCC), or no capture device. Grant camera access to this terminal in System Settings > Privacy.")
|
||||
return false
|
||||
}
|
||||
organ_disclose("consent OK (Neuron + OS) for 'camera' — local only, never egresses.")
|
||||
return true
|
||||
}
|
||||
|
||||
// ── SPEAKER (efferent) ───────────────────────────────────────────────────────
|
||||
//
|
||||
// Not consent-gated, and that is a deliberate asymmetry rather than an
|
||||
// oversight. The microphone and camera take information OFF the user without
|
||||
// them necessarily knowing; the speaker puts information INTO a room the user
|
||||
// is in, audibly, which is self-disclosing by its nature — you cannot secretly
|
||||
// speak aloud. So the speaker is DISCLOSED (every utterance announces itself on
|
||||
// stderr) but not gated. Gating it would mean Neuron needs permission to answer.
|
||||
|
||||
fn organ_speak_samples(samples: [Int], sr: Int) -> Bool {
|
||||
let n: Int = native_list_len(samples)
|
||||
if n <= 0 {
|
||||
organ_disclose("SPEAKER: nothing to say (0 samples) — not touching the device.")
|
||||
return false
|
||||
}
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("SPEAKER: no audio output on this build (" + speaker_name() + ") — cannot speak.")
|
||||
return false
|
||||
}
|
||||
let secs: Int = n * 1000 / sr
|
||||
organ_disclose("SPEAKER: playing " + int_to_str(n) + " samples (" + int_to_str(secs) + " ms @ " + int_to_str(sr) + " Hz) ALOUD via " + speaker_name() + " (efferent).")
|
||||
let ok: Int = speaker_play_pcm16(samples, sr)
|
||||
if ok == 1 {
|
||||
organ_disclose("SPEAKER: done — Neuron spoke aloud.")
|
||||
return true
|
||||
}
|
||||
organ_disclose("SPEAKER: playback FAILED.")
|
||||
return false
|
||||
}
|
||||
|
||||
fn organ_speak_wav(path: String) -> Bool {
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("SPEAKER: no audio output on this build — cannot speak.")
|
||||
return false
|
||||
}
|
||||
if fs_exists(path) == false {
|
||||
organ_disclose("SPEAKER: no such file: " + path)
|
||||
return false
|
||||
}
|
||||
organ_disclose("SPEAKER: playing '" + path + "' ALOUD via " + speaker_name() + " (efferent).")
|
||||
let ok: Int = speaker_play_wav(path)
|
||||
if ok == 1 {
|
||||
organ_disclose("SPEAKER: done — Neuron spoke aloud.")
|
||||
return true
|
||||
}
|
||||
organ_disclose("SPEAKER: playback FAILED.")
|
||||
return false
|
||||
}
|
||||
|
||||
// ── The voice, fetched FROM THE ENGRAM ───────────────────────────────────────
|
||||
//
|
||||
// This is the part that matters most and is easiest to get subtly wrong. A
|
||||
// voice is not a constant in code and it is not a JSON file next to the code —
|
||||
// it is a region of the graph, put there by having heard someone, and the organ
|
||||
// retrieves it the way anything retrieves a memory: by asking.
|
||||
//
|
||||
// The node content is the geometry, in the engram's own flat key=value form:
|
||||
// voice will | f0=137 f0_end=116 kf=1269 f1=500 f2=2093 f3=3531 ...
|
||||
// so the read is: query the graph, take the returned node, pull the numbers off
|
||||
// it. Nothing here opens a file.
|
||||
//
|
||||
// Returns [f0, f0_end, kf, f1, f2, f3], or an EMPTY list when the region is not
|
||||
// in the graph. Empty is the honest answer — a caller that gets no voice must
|
||||
// not be handed a plausible default and left unable to tell the difference
|
||||
// between "this is how they sound" and "I never heard them."
|
||||
|
||||
// Read an unsigned integer that follows `key` in `s`. Stops at the first
|
||||
// non-digit, returns 0 when the key is absent.
|
||||
fn organ_int_after(s: String, key: String) -> Int {
|
||||
let at: Int = str_index_of(s, key)
|
||||
if at < 0 {
|
||||
return 0
|
||||
}
|
||||
let i: Int = at + str_len(key)
|
||||
let n: Int = str_len(s)
|
||||
let v: Int = 0
|
||||
let seen: Int = 0
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
if c < 48 {
|
||||
i = n
|
||||
} else {
|
||||
if c > 57 {
|
||||
i = n
|
||||
} else {
|
||||
v = v * 10 + (c - 48)
|
||||
seen = seen + 1
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if seen == 0 {
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Ask the engram for a named voice region and read its geometry back.
|
||||
fn organ_voice_fetch(name: String) -> [Int] {
|
||||
let out: [Int] = native_list_empty()
|
||||
let marker: String = "voice " + name + " |"
|
||||
// The graph is asked by MEANING, not by id or by path.
|
||||
let hits: String = engram_search_json("voice " + name + " f0 formants", 12)
|
||||
let at: Int = str_index_of(hits, marker)
|
||||
if at < 0 {
|
||||
// Fall back to a scan of the resident graph before giving up: search is
|
||||
// geometric and a small graph may not rank the region first.
|
||||
let scan: String = engram_scan_nodes_json(500, 0)
|
||||
at = str_index_of(scan, marker)
|
||||
if at < 0 {
|
||||
organ_disclose("VOICE: no region for '" + name + "' in the engram — nothing to speak with.")
|
||||
return out
|
||||
}
|
||||
hits = scan
|
||||
}
|
||||
let win: String = str_slice(hits, at, at + 240)
|
||||
out = native_list_append(out, organ_int_after(win, "f0="))
|
||||
out = native_list_append(out, organ_int_after(win, "f0_end="))
|
||||
out = native_list_append(out, organ_int_after(win, "kf="))
|
||||
out = native_list_append(out, organ_int_after(win, "f1="))
|
||||
out = native_list_append(out, organ_int_after(win, "f2="))
|
||||
out = native_list_append(out, organ_int_after(win, "f3="))
|
||||
organ_disclose("VOICE: fetched '" + name + "' FROM THE ENGRAM — f0=" + int_to_str(native_list_get(out, 0)) + " f0_end=" + int_to_str(native_list_get(out, 1)) + " kf=" + int_to_str(native_list_get(out, 2)) + " f1=" + int_to_str(native_list_get(out, 3)) + " f2=" + int_to_str(native_list_get(out, 4)) + " f3=" + int_to_str(native_list_get(out, 5)))
|
||||
return out
|
||||
}
|
||||
|
||||
// Put a measured voice INTO the engram as geometry. This is the afferent end of
|
||||
// the same wire: a voiceprint (organ_dsp.el's LPC analysis) becomes a node, and
|
||||
// from then on the voice is a memory rather than a measurement someone happened
|
||||
// to write down. `prov` carries the honesty: COARSE means one formant triple, no
|
||||
// coarticulation, no prosody — an impression, explicitly not a clone.
|
||||
fn organ_voice_ingest(name: String, f0: Int, f0_end: Int, kf: Int, f1: Int, f2: Int, f3: Int, src: String, prov: String) -> String {
|
||||
let hub: String = engram_node("voice-signature-set " + name + " grounding=measured src=" + src, "VoiceSet", 90)
|
||||
let body: String = "voice " + name + " | f0=" + int_to_str(f0) + " f0_end=" + int_to_str(f0_end) + " kf=" + int_to_str(kf) + " f1=" + int_to_str(f1) + " f2=" + int_to_str(f2) + " f3=" + int_to_str(f3) + " grounding=measured src=" + src + " prov=" + prov
|
||||
let vid: String = engram_node(body, "Voice", 90)
|
||||
engram_connect(hub, vid, 90, "has-signature")
|
||||
organ_disclose("VOICE: ingested '" + name + "' into the engram as geometry (node " + vid + ").")
|
||||
return vid
|
||||
}
|
||||
|
||||
// Turn the fetched geometry into the voice slot-map the render consumes. Kept
|
||||
// separate from the fetch so the organ never invents a voice: if the fetch came
|
||||
// back empty this returns empty too, and the caller has to deal with it.
|
||||
//
|
||||
// The slot-map is built here rather than by calling the render's own
|
||||
// constructor, so the organ carries NO dependency on the language faculty's
|
||||
// modules — it only has to agree with them about a wire format, which is the
|
||||
// looser and more honest coupling. (The layout is the same key/value [String]
|
||||
// convention lang_get / surface_get / voice_get all read.)
|
||||
fn organ_voice_profile(name: String, g: [Int]) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
if native_list_len(g) < 6 {
|
||||
return r
|
||||
}
|
||||
r = native_list_append(r, "name")
|
||||
r = native_list_append(r, name)
|
||||
r = native_list_append(r, "f0")
|
||||
r = native_list_append(r, int_to_str(native_list_get(g, 0)))
|
||||
r = native_list_append(r, "f0_end")
|
||||
r = native_list_append(r, int_to_str(native_list_get(g, 1)))
|
||||
r = native_list_append(r, "kf")
|
||||
r = native_list_append(r, int_to_str(native_list_get(g, 2)))
|
||||
r = native_list_append(r, "dur")
|
||||
r = native_list_append(r, "1000")
|
||||
r = native_list_append(r, "tilt")
|
||||
r = native_list_append(r, "1000")
|
||||
r = native_list_append(r, "breath")
|
||||
r = native_list_append(r, "8")
|
||||
return r
|
||||
}
|
||||
|
||||
// ── Scene geometry (afferent, camera) ────────────────────────────────────────
|
||||
//
|
||||
// The image half of the afferent metabolism, and the same principle as the
|
||||
// audio descriptor: a frame is never handed on raw. The realizer returns a
|
||||
// small pixel grid; THIS computes the descriptor, in El, because averaging
|
||||
// pixels is arithmetic and arithmetic is not a device concern.
|
||||
//
|
||||
// Returns 15 numbers — [w, h, meanR, meanG, meanB, brightness_pm, and a 3x3
|
||||
// luminance grid] — standing in for a multi-megapixel frame. The 3x3 grid is
|
||||
// the smallest thing that still says WHERE the light is, which is most of what
|
||||
// makes a scene comparable to another scene; a single brightness average would
|
||||
// make a lamp on the left indistinguishable from a lamp on the right.
|
||||
//
|
||||
// Luminance is Rec. 601 (0.299R + 0.587G + 0.114B), in integer per-mille, so
|
||||
// the descriptor is reproducible rather than subject to float drift.
|
||||
fn organ_image_descriptor() -> [Int] {
|
||||
let out: [Int] = native_list_empty()
|
||||
let frame: Any = camera_capture_rgb()
|
||||
if frame == 0 {
|
||||
return out
|
||||
}
|
||||
let w: Int = el_map_get(frame, "width")
|
||||
let h: Int = el_map_get(frame, "height")
|
||||
let gw: Int = el_map_get(frame, "grid_w")
|
||||
let gh: Int = el_map_get(frame, "grid_h")
|
||||
let px: [Int] = el_map_get(frame, "pixels")
|
||||
let np: Int = native_list_len(px)
|
||||
if np < 3 {
|
||||
return out
|
||||
}
|
||||
let count: Int = np / 3
|
||||
let rsum: Int = 0
|
||||
let gsum: Int = 0
|
||||
let bsum: Int = 0
|
||||
// 3x3 accumulators, row-major
|
||||
let cell: [Int] = native_list_empty()
|
||||
let cn: [Int] = native_list_empty()
|
||||
let z: Int = 0
|
||||
while z < 9 {
|
||||
cell = native_list_append(cell, 0)
|
||||
cn = native_list_append(cn, 0)
|
||||
z = z + 1
|
||||
}
|
||||
// El has no list-set, so the cells are summed into parallel scalars and
|
||||
// reassembled — nine explicit accumulators would be worse to read than one
|
||||
// pass per cell over a grid this small.
|
||||
let c0: Int = 0
|
||||
let c1: Int = 0
|
||||
let c2: Int = 0
|
||||
let c3: Int = 0
|
||||
let c4: Int = 0
|
||||
let c5: Int = 0
|
||||
let c6: Int = 0
|
||||
let c7: Int = 0
|
||||
let c8: Int = 0
|
||||
let n0: Int = 0
|
||||
let n1: Int = 0
|
||||
let n2: Int = 0
|
||||
let n3: Int = 0
|
||||
let n4: Int = 0
|
||||
let n5: Int = 0
|
||||
let n6: Int = 0
|
||||
let n7: Int = 0
|
||||
let n8: Int = 0
|
||||
let i: Int = 0
|
||||
while i < count {
|
||||
let r: Int = native_list_get(px, i * 3)
|
||||
let g: Int = native_list_get(px, i * 3 + 1)
|
||||
let b: Int = native_list_get(px, i * 3 + 2)
|
||||
rsum = rsum + r
|
||||
gsum = gsum + g
|
||||
bsum = bsum + b
|
||||
let lum: Int = (299 * r + 587 * g + 114 * b) / 1000
|
||||
let x: Int = i - (i / gw) * gw
|
||||
let y: Int = i / gw
|
||||
let cx: Int = x * 3 / gw
|
||||
let cy: Int = y * 3 / gh
|
||||
if cx > 2 {
|
||||
cx = 2
|
||||
}
|
||||
if cy > 2 {
|
||||
cy = 2
|
||||
}
|
||||
let idx: Int = cy * 3 + cx
|
||||
if idx == 0 {
|
||||
c0 = c0 + lum
|
||||
n0 = n0 + 1
|
||||
}
|
||||
if idx == 1 {
|
||||
c1 = c1 + lum
|
||||
n1 = n1 + 1
|
||||
}
|
||||
if idx == 2 {
|
||||
c2 = c2 + lum
|
||||
n2 = n2 + 1
|
||||
}
|
||||
if idx == 3 {
|
||||
c3 = c3 + lum
|
||||
n3 = n3 + 1
|
||||
}
|
||||
if idx == 4 {
|
||||
c4 = c4 + lum
|
||||
n4 = n4 + 1
|
||||
}
|
||||
if idx == 5 {
|
||||
c5 = c5 + lum
|
||||
n5 = n5 + 1
|
||||
}
|
||||
if idx == 6 {
|
||||
c6 = c6 + lum
|
||||
n6 = n6 + 1
|
||||
}
|
||||
if idx == 7 {
|
||||
c7 = c7 + lum
|
||||
n7 = n7 + 1
|
||||
}
|
||||
if idx == 8 {
|
||||
c8 = c8 + lum
|
||||
n8 = n8 + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
let rA: Int = rsum / count
|
||||
let gA: Int = gsum / count
|
||||
let bA: Int = bsum / count
|
||||
let bright: Int = (299 * rA + 587 * gA + 114 * bA) / 255
|
||||
out = native_list_append(out, w)
|
||||
out = native_list_append(out, h)
|
||||
out = native_list_append(out, rA)
|
||||
out = native_list_append(out, gA)
|
||||
out = native_list_append(out, bA)
|
||||
out = native_list_append(out, bright)
|
||||
if n0 < 1 {
|
||||
n0 = 1
|
||||
}
|
||||
if n1 < 1 {
|
||||
n1 = 1
|
||||
}
|
||||
if n2 < 1 {
|
||||
n2 = 1
|
||||
}
|
||||
if n3 < 1 {
|
||||
n3 = 1
|
||||
}
|
||||
if n4 < 1 {
|
||||
n4 = 1
|
||||
}
|
||||
if n5 < 1 {
|
||||
n5 = 1
|
||||
}
|
||||
if n6 < 1 {
|
||||
n6 = 1
|
||||
}
|
||||
if n7 < 1 {
|
||||
n7 = 1
|
||||
}
|
||||
if n8 < 1 {
|
||||
n8 = 1
|
||||
}
|
||||
out = native_list_append(out, c0 / n0)
|
||||
out = native_list_append(out, c1 / n1)
|
||||
out = native_list_append(out, c2 / n2)
|
||||
out = native_list_append(out, c3 / n3)
|
||||
out = native_list_append(out, c4 / n4)
|
||||
out = native_list_append(out, c5 / n5)
|
||||
out = native_list_append(out, c6 / n6)
|
||||
out = native_list_append(out, c7 / n7)
|
||||
out = native_list_append(out, c8 / n8)
|
||||
organ_disclose("FEAT(image): 15-number scene-geometry vs " + int_to_str(w * h * 3) + " pixel-channels — the descriptor travels, the frame does not.")
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Own-core tone ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The smallest possible proof that the organ owns its medium end to end: a sine
|
||||
// with a gentle attack and release, computed here, played by us, no file and no
|
||||
// library anywhere in the path.
|
||||
fn organ_tone(hz: Int, ms: Int, sr: Int) -> [Int] {
|
||||
let n: Int = sr * ms / 1000
|
||||
let out: [Int] = native_list_empty()
|
||||
let two_pi: Float = 6.283185307
|
||||
let srf: Float = int_to_float(sr)
|
||||
let hzf: Float = int_to_float(hz)
|
||||
let i: Int = 0
|
||||
// 20 ms of ramp at each end; a square-edged tone clicks, and a click is the
|
||||
// organ announcing that it does not understand envelopes.
|
||||
let ramp: Int = sr / 50
|
||||
if ramp < 1 {
|
||||
ramp = 1
|
||||
}
|
||||
while i < n {
|
||||
let t: Float = int_to_float(i) / srf
|
||||
let s: Float = math_sin(two_pi * hzf * t)
|
||||
let env: Int = 32767
|
||||
if i < ramp {
|
||||
env = 32767 * i / ramp
|
||||
}
|
||||
let tail: Int = n - i
|
||||
if tail < ramp {
|
||||
env = 32767 * tail / ramp
|
||||
}
|
||||
let v: Int = float_to_int(s * 9000.0) * env / 32767
|
||||
out = native_list_append(out, v)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// organ_cli.el — the organ's command surface. main() lives here.
|
||||
//
|
||||
// One binary, the same verbs the Swift program had, and nothing behind them
|
||||
// except El and two thin device realizers. This file is the proof surface: if
|
||||
// `organ speak` makes a sound and no Swift binary is in the process tree, the
|
||||
// claim in organ.el's header is true.
|
||||
//
|
||||
// Verbs, and what each one demonstrates:
|
||||
//
|
||||
// grant/revoke/status the Neuron-level consent lock, inspectable
|
||||
// speak <wav> efferent — audio out of El's own speaker
|
||||
// tone <hz> <ms> own-core synthesis: computed in El, played by El,
|
||||
// never touching the disk
|
||||
// say <name> <codes...> fetch a VOICE FROM THE ENGRAM and render through it
|
||||
// listen <sec> <out> afferent — mic capture, consent-gated, fails closed
|
||||
// see <out.jpg> afferent — one camera frame, same two locks
|
||||
// wav-info <wav> WAV geometry, parsed in El
|
||||
// feat-audio <wav> capture -> compact descriptor (8 numbers)
|
||||
// feat-image <jpg> frame -> compact scene-geometry
|
||||
// voiceprint <wav> F0 + formants F1-F5 by LPC, in El
|
||||
// imitate <in> <out> LPC analysis-resynthesis, in El
|
||||
// hear-imitate <sec> the closed loop: hear a voice, take its signature,
|
||||
// speak back in it
|
||||
// ingest-audio <wav> descriptor -> engram node (the capture becomes geometry)
|
||||
// ingest-voice <wav> <n> voiceprint -> engram voice region (how a voice is learned)
|
||||
// converse <manifest> full-duplex interruptible utterance
|
||||
//
|
||||
// The descriptors are the point of the afferent half. A capture is NEVER handed
|
||||
// on raw: a three-second recording is ~48,000 samples and what leaves this
|
||||
// process is eight numbers. That is both the privacy rail (the stream stays
|
||||
// local because only its shape travels) and the reason the engram can hold a
|
||||
// perception at all — geometry is storable, a waveform is not.
|
||||
|
||||
fn cli_usage() -> Bool {
|
||||
println("organ — Neuron's I/O organ, native El (own-core, local, consent-gated)")
|
||||
println(" grant|revoke <camera|mic> Neuron-level consent")
|
||||
println(" status consent + device state")
|
||||
println(" speak <file.wav> play a WAV aloud (efferent)")
|
||||
println(" tone [hz] [ms] synthesize and play, no file at all")
|
||||
println(" say <voice> <CODE> [CODE...] fetch voice FROM THE ENGRAM, render, speak")
|
||||
println(" listen <sec> <out.wav> mic capture 16k mono (afferent)")
|
||||
println(" see <out.jpg> one camera frame (afferent)")
|
||||
println(" wav-info <file.wav> WAV geometry")
|
||||
println(" feat-audio <file.wav> compact audio descriptor (8 numbers)")
|
||||
println(" feat-image compact scene-geometry from the camera")
|
||||
println(" voiceprint <voice.wav> F0 + formants F1-F5 (LPC)")
|
||||
println(" imitate <in.wav> <out.wav> LPC analysis-resynthesis")
|
||||
println(" hear-imitate <sec> <out.wav> mic -> signature -> imitate -> speak aloud")
|
||||
println(" ingest-audio <file.wav> descriptor -> engram node (geometry)")
|
||||
println(" ingest-voice <voice.wav> <n> voiceprint -> engram voice region")
|
||||
println(" converse <manifest.json> [--authority PM] [--barge-at MS[:kind]] [--live-mic] [--resume]")
|
||||
return true
|
||||
}
|
||||
|
||||
// The engram the organ reads and writes. Its own store, never production's.
|
||||
fn cli_engram_dir() -> String {
|
||||
let d: String = env("ORGAN_ENGRAM")
|
||||
if str_eq(d, "") {
|
||||
return "peripheral/.engram"
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
fn cli_open_engram() -> Bool {
|
||||
let dir: String = cli_engram_dir()
|
||||
fs_mkdir(dir)
|
||||
let ok: Int = engram_store_boot(dir)
|
||||
if ok == 1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── formatting helpers ───────────────────────────────────────────────────────
|
||||
|
||||
fn cli_f(v: Float, dec: Int) -> String {
|
||||
return format_float(v, dec)
|
||||
}
|
||||
|
||||
// ── the descriptor, printed and ingested ─────────────────────────────────────
|
||||
//
|
||||
// [seconds, sr, ch, rms, peak, zcr, centroid, f0] — the same eight numbers the
|
||||
// Swift produced, computed in El, and the compression ratio is the headline:
|
||||
// a few dozen bytes standing in for a few hundred kilobytes.
|
||||
fn cli_audio_descriptor_text(v: [Float], path: String) -> String {
|
||||
let secs: Float = native_list_get(v, 0)
|
||||
let sr: Float = native_list_get(v, 1)
|
||||
let ch: Float = native_list_get(v, 2)
|
||||
let rms: Float = native_list_get(v, 3)
|
||||
let peak: Float = native_list_get(v, 4)
|
||||
let zcr: Float = native_list_get(v, 5)
|
||||
let cen: Float = native_list_get(v, 6)
|
||||
let f0: Float = native_list_get(v, 7)
|
||||
return "Heard sound (afferent, mic): " + cli_f(secs, 2) + "s at " + cli_f(sr, 0) + "Hz. RMS energy " + cli_f(rms, 4) + ", peak " + cli_f(peak, 4) + ", zero-crossing rate " + cli_f(zcr, 0) + "Hz, spectral centroid " + cli_f(cen, 0) + "Hz, estimated voice pitch F0 " + cli_f(f0, 0) + "Hz. Compact voice/sound signature (8 numbers) — phonetic geometry seed."
|
||||
}
|
||||
|
||||
fn cli_feat_audio(path: String) -> Bool {
|
||||
let v: [Float] = dsp_compute_audio(path)
|
||||
if native_list_len(v) < 8 {
|
||||
println("{\"ok\": false, \"op\": \"feat-audio\", \"error\": \"cannot read PCM\"}")
|
||||
return false
|
||||
}
|
||||
organ_disclose("FEAT(audio): 8-number signature vs " + int_to_str(float_to_int(native_list_get(v, 0) * native_list_get(v, 1))) + " raw samples.")
|
||||
println("{\"ok\": true, \"op\": \"feat-audio\", \"file\": \"" + path + "\", \"seconds\": " + cli_f(native_list_get(v, 0), 4) + ", \"sample_rate\": " + cli_f(native_list_get(v, 1), 0) + ", \"channels\": " + cli_f(native_list_get(v, 2), 0) + ", \"rms\": " + cli_f(native_list_get(v, 3), 6) + ", \"peak\": " + cli_f(native_list_get(v, 4), 6) + ", \"zcr_hz\": " + cli_f(native_list_get(v, 5), 4) + ", \"centroid_hz\": " + cli_f(native_list_get(v, 6), 4) + ", \"f0_hz\": " + cli_f(native_list_get(v, 7), 4) + "}")
|
||||
return true
|
||||
}
|
||||
|
||||
fn cli_voiceprint(path: String) -> Bool {
|
||||
let v: [Float] = dsp_voiceprint(path)
|
||||
if native_list_len(v) < 4 {
|
||||
println("{\"ok\": false, \"op\": \"voiceprint\", \"error\": \"cannot read speech\"}")
|
||||
return false
|
||||
}
|
||||
let nf: Int = float_to_int(native_list_get(v, 3))
|
||||
let fs: String = ""
|
||||
let bs: String = ""
|
||||
let i: Int = 0
|
||||
while i < nf {
|
||||
if i > 0 {
|
||||
fs = fs + ", "
|
||||
bs = bs + ", "
|
||||
}
|
||||
fs = fs + cli_f(native_list_get(v, 4 + i * 2), 3)
|
||||
bs = bs + cli_f(native_list_get(v, 5 + i * 2), 3)
|
||||
i = i + 1
|
||||
}
|
||||
println("{\"ok\": true, \"op\": \"voiceprint\", \"file\": \"" + path + "\", \"f0_hz\": " + cli_f(native_list_get(v, 0), 4) + ", \"f0_range\": [" + cli_f(native_list_get(v, 1), 4) + ", " + cli_f(native_list_get(v, 2), 4) + "], \"formants_hz\": [" + fs + "], \"bandwidths_hz\": [" + bs + "]}")
|
||||
return true
|
||||
}
|
||||
|
||||
// ── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
let a: [String] = args()
|
||||
let n: Int = native_list_len(a)
|
||||
if n < 1 {
|
||||
cli_usage()
|
||||
return
|
||||
}
|
||||
let cmd: String = native_list_get(a, 0)
|
||||
|
||||
// ---- consent -----------------------------------------------------------
|
||||
if str_eq(cmd, "grant") {
|
||||
if n < 2 {
|
||||
println("grant needs a device")
|
||||
return
|
||||
}
|
||||
organ_grant(native_list_get(a, 1))
|
||||
println("{\"ok\": true, \"op\": \"grant\", \"consent\": \"" + organ_consent_status() + "\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "revoke") {
|
||||
if n < 2 {
|
||||
println("revoke needs a device")
|
||||
return
|
||||
}
|
||||
organ_revoke(native_list_get(a, 1))
|
||||
println("{\"ok\": true, \"op\": \"revoke\", \"consent\": \"" + organ_consent_status() + "\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "status") {
|
||||
println("{\"ok\": true, \"op\": \"status\", \"consent\": \"" + organ_consent_status() + "\", \"speaker\": \"" + speaker_name() + "\", \"speaker_available\": " + int_to_str(speaker_available()) + ", \"mic_os_authorized\": " + int_to_str(mic_available()) + ", \"camera_os_authorized\": " + int_to_str(camera_available()) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- efferent ----------------------------------------------------------
|
||||
if str_eq(cmd, "speak") {
|
||||
if n < 2 {
|
||||
println("speak needs a wav")
|
||||
return
|
||||
}
|
||||
let ok: Bool = organ_speak_wav(native_list_get(a, 1))
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"speak\", \"played_aloud\": " + bool_to_str(ok) + "}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "tone") {
|
||||
let hz: Int = 220
|
||||
let ms: Int = 1000
|
||||
if n >= 2 {
|
||||
hz = str_to_int(native_list_get(a, 1))
|
||||
}
|
||||
if n >= 3 {
|
||||
ms = str_to_int(native_list_get(a, 2))
|
||||
}
|
||||
let s: [Int] = organ_tone(hz, ms, 16000)
|
||||
let ok: Bool = organ_speak_samples(s, 16000)
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"tone\", \"hz\": " + int_to_str(hz) + ", \"ms\": " + int_to_str(ms) + ", \"samples\": " + int_to_str(native_list_len(s)) + ", \"file\": null}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- the voice, from the engram ----------------------------------------
|
||||
if str_eq(cmd, "say") {
|
||||
if n < 3 {
|
||||
println("say needs <voice> <CODE> [CODE...]")
|
||||
return
|
||||
}
|
||||
cli_open_engram()
|
||||
let vname: String = native_list_get(a, 1)
|
||||
let g: [Int] = organ_voice_fetch(vname)
|
||||
if native_list_len(g) < 6 {
|
||||
println("{\"ok\": false, \"op\": \"say\", \"error\": \"no voice region '" + vname + "' in the engram\"}")
|
||||
return
|
||||
}
|
||||
// Codes and the phoneme map come from the LANGUAGE side. The organ does
|
||||
// not know what a word is and never looks one up.
|
||||
let pmap: [String] = ingest_phonetics("elp/data/phonetics.psv")
|
||||
let codes: [String] = native_list_empty()
|
||||
let i: Int = 2
|
||||
while i < n {
|
||||
codes = native_list_append(codes, native_list_get(a, i))
|
||||
i = i + 1
|
||||
}
|
||||
let voice: [String] = organ_voice_profile(vname, g)
|
||||
let s: [Int] = synth_codes(codes, voice, pmap)
|
||||
let ok: Bool = organ_speak_samples(s, 16000)
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"say\", \"voice\": \"" + vname + "\", \"f0\": " + int_to_str(native_list_get(g, 0)) + ", \"kf\": " + int_to_str(native_list_get(g, 2)) + ", \"codes\": " + int_to_str(native_list_len(codes)) + ", \"samples\": " + int_to_str(native_list_len(s)) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- afferent ----------------------------------------------------------
|
||||
if str_eq(cmd, "listen") {
|
||||
if n < 3 {
|
||||
println("listen needs <sec> <out.wav>")
|
||||
return
|
||||
}
|
||||
let secs: Int = str_to_int(native_list_get(a, 1))
|
||||
let out: String = native_list_get(a, 2)
|
||||
if organ_may_listen() == false {
|
||||
println("{\"ok\": false, \"op\": \"listen\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
organ_disclose("MIC: capturing " + int_to_str(secs) + "s (16 kHz mono, LOCAL, never egresses).")
|
||||
let s: [Int] = mic_capture_pcm16(secs, 16000)
|
||||
let got: Int = native_list_len(s)
|
||||
if got <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"listen\", \"error\": \"capture returned nothing\"}")
|
||||
return
|
||||
}
|
||||
let ok: Bool = write_wav(s, 16000, out)
|
||||
organ_disclose("MIC: captured " + int_to_str(got) + " frames — ready to hand to the ingest organ.")
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"listen\", \"file\": \"" + out + "\", \"frames\": " + int_to_str(got) + ", \"sample_rate\": 16000}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "see") {
|
||||
if n < 2 {
|
||||
println("see needs an out path")
|
||||
return
|
||||
}
|
||||
if organ_may_see() == false {
|
||||
println("{\"ok\": false, \"op\": \"see\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
organ_disclose("CAMERA: capturing one frame (LOCAL, never egresses).")
|
||||
let ok: Int = camera_capture_jpeg(native_list_get(a, 1))
|
||||
println("{\"ok\": " + int_to_str(ok) + ", \"op\": \"see\", \"file\": \"" + native_list_get(a, 1) + "\"}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- descriptors -------------------------------------------------------
|
||||
if str_eq(cmd, "wav-info") {
|
||||
if n < 2 {
|
||||
println("wav-info needs a wav")
|
||||
return
|
||||
}
|
||||
let p: String = native_list_get(a, 1)
|
||||
let w: [Float] = dsp_read_wav(p)
|
||||
if dsp_wav_n(w) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"wav-info\"}")
|
||||
return
|
||||
}
|
||||
println("{\"ok\": true, \"op\": \"wav-info\", \"sample_rate\": " + int_to_str(dsp_wav_sr(w)) + ", \"channels\": " + int_to_str(dsp_wav_ch(w)) + ", \"frames\": " + int_to_str(dsp_wav_n(w)) + "}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "feat-audio") {
|
||||
if n < 2 {
|
||||
println("feat-audio needs a wav")
|
||||
return
|
||||
}
|
||||
cli_feat_audio(native_list_get(a, 1))
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "feat-image") {
|
||||
if organ_may_see() == false {
|
||||
println("{\"ok\": false, \"op\": \"feat-image\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
let f: [Int] = organ_image_descriptor()
|
||||
if native_list_len(f) < 15 {
|
||||
println("{\"ok\": false, \"op\": \"feat-image\", \"error\": \"no frame\"}")
|
||||
return
|
||||
}
|
||||
let grid: String = ""
|
||||
let i: Int = 6
|
||||
while i < 15 {
|
||||
if i > 6 {
|
||||
grid = grid + ", "
|
||||
}
|
||||
grid = grid + int_to_str(native_list_get(f, i))
|
||||
i = i + 1
|
||||
}
|
||||
println("{\"ok\": true, \"op\": \"feat-image\", \"width\": " + int_to_str(native_list_get(f, 0)) + ", \"height\": " + int_to_str(native_list_get(f, 1)) + ", \"mean_rgb\": [" + int_to_str(native_list_get(f, 2)) + ", " + int_to_str(native_list_get(f, 3)) + ", " + int_to_str(native_list_get(f, 4)) + "], \"brightness_pm\": " + int_to_str(native_list_get(f, 5)) + ", \"luma_grid\": [" + grid + "]}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "voiceprint") {
|
||||
if n < 2 {
|
||||
println("voiceprint needs a wav")
|
||||
return
|
||||
}
|
||||
cli_voiceprint(native_list_get(a, 1))
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "imitate") {
|
||||
if n < 3 {
|
||||
println("imitate needs <in.wav> <out.wav>")
|
||||
return
|
||||
}
|
||||
let s: [Int] = dsp_imitate(native_list_get(a, 1))
|
||||
if native_list_len(s) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"imitate\"}")
|
||||
return
|
||||
}
|
||||
let ok: Bool = write_wav(s, 16000, native_list_get(a, 2))
|
||||
organ_disclose("IMITATE: rebuilt the voice from its own LPC signature (own-core, no training, no stolen voice).")
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"imitate\", \"out\": \"" + native_list_get(a, 2) + "\", \"samples\": " + int_to_str(native_list_len(s)) + ", \"method\": \"LPC analysis-resynthesis\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "hear-imitate") {
|
||||
if n < 3 {
|
||||
println("hear-imitate needs <sec> <out.wav>")
|
||||
return
|
||||
}
|
||||
let secs: Int = str_to_int(native_list_get(a, 1))
|
||||
let out: String = native_list_get(a, 2)
|
||||
if organ_may_listen() == false {
|
||||
println("{\"ok\": false, \"op\": \"hear-imitate\", \"error\": \"consent denied (fails closed)\"}")
|
||||
return
|
||||
}
|
||||
let heard: String = out + ".heard.wav"
|
||||
organ_disclose("HEAR-IMITATE: open the ear, listen " + int_to_str(secs) + "s, take the voice, speak it back.")
|
||||
let s: [Int] = mic_capture_pcm16(secs, 16000)
|
||||
if native_list_len(s) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"hear-imitate\", \"error\": \"capture returned nothing\"}")
|
||||
return
|
||||
}
|
||||
write_wav(s, 16000, heard)
|
||||
let re: [Int] = dsp_imitate(heard)
|
||||
if native_list_len(re) <= 0 {
|
||||
println("{\"ok\": false, \"op\": \"hear-imitate\", \"error\": \"could not model the voice\"}")
|
||||
return
|
||||
}
|
||||
write_wav(re, 16000, out)
|
||||
let ok: Bool = organ_speak_samples(re, 16000)
|
||||
println("{\"ok\": " + bool_to_str(ok) + ", \"op\": \"hear-imitate\", \"heard\": \"" + heard + "\", \"out\": \"" + out + "\", \"spoke_aloud\": " + bool_to_str(ok) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- the afferent wire: descriptor -> geometry --------------------------
|
||||
if str_eq(cmd, "ingest-audio") {
|
||||
if n < 2 {
|
||||
println("ingest-audio needs a wav")
|
||||
return
|
||||
}
|
||||
let p: String = native_list_get(a, 1)
|
||||
let v: [Float] = dsp_compute_audio(p)
|
||||
if native_list_len(v) < 8 {
|
||||
println("{\"ok\": false, \"op\": \"ingest-audio\"}")
|
||||
return
|
||||
}
|
||||
cli_open_engram()
|
||||
let content: String = cli_audio_descriptor_text(v, p)
|
||||
let id: String = engram_node(content, "Observation", 70)
|
||||
engram_store_checkpoint()
|
||||
organ_disclose("INGEST: the capture is now GEOMETRY in the engram (node " + id + ") — the descriptor travelled, the stream did not.")
|
||||
println("{\"ok\": true, \"op\": \"ingest-audio\", \"node_id\": \"" + id + "\", \"content\": \"" + content + "\"}")
|
||||
return
|
||||
}
|
||||
if str_eq(cmd, "ingest-voice") {
|
||||
if n < 3 {
|
||||
println("ingest-voice needs <voice.wav> <name>")
|
||||
return
|
||||
}
|
||||
let p: String = native_list_get(a, 1)
|
||||
let name: String = native_list_get(a, 2)
|
||||
let v: [Float] = dsp_voiceprint(p)
|
||||
if native_list_len(v) < 10 {
|
||||
println("{\"ok\": false, \"op\": \"ingest-voice\", \"error\": \"no voiced frames\"}")
|
||||
return
|
||||
}
|
||||
cli_open_engram()
|
||||
let f0: Int = float_to_int(native_list_get(v, 0))
|
||||
let f1: Int = float_to_int(native_list_get(v, 4))
|
||||
let f2: Int = float_to_int(native_list_get(v, 6))
|
||||
let f3: Int = float_to_int(native_list_get(v, 8))
|
||||
// kf is the vocal-tract scale: this speaker's F1 against the nominal
|
||||
// /AA/ F1 of 730 Hz. One number standing for a tract length.
|
||||
let kf: Int = 1000 * f1 / 730
|
||||
let f0e: Int = f0 * 85 / 100
|
||||
let id: String = organ_voice_ingest(name, f0, f0e, kf, f1, f2, f3, "el-organ-lpc-voiceprint", "COARSE")
|
||||
engram_store_checkpoint()
|
||||
println("{\"ok\": true, \"op\": \"ingest-voice\", \"node_id\": \"" + id + "\", \"name\": \"" + name + "\", \"f0\": " + int_to_str(f0) + ", \"kf\": " + int_to_str(kf) + ", \"f1\": " + int_to_str(f1) + ", \"f2\": " + int_to_str(f2) + ", \"f3\": " + int_to_str(f3) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
// ---- converse ----------------------------------------------------------
|
||||
if str_eq(cmd, "converse") {
|
||||
if n < 2 {
|
||||
println("converse needs a manifest")
|
||||
return
|
||||
}
|
||||
let mf: String = native_list_get(a, 1)
|
||||
let authority: Int = 500
|
||||
let barge: Int = 0 - 1
|
||||
let kind: String = "bargein"
|
||||
let live: Bool = false
|
||||
let resume: Bool = false
|
||||
let i: Int = 2
|
||||
while i < n {
|
||||
let f: String = native_list_get(a, i)
|
||||
if str_eq(f, "--authority") {
|
||||
if i + 1 < n {
|
||||
authority = str_to_int(native_list_get(a, i + 1))
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if str_eq(f, "--barge-at") {
|
||||
if i + 1 < n {
|
||||
let spec: String = native_list_get(a, i + 1)
|
||||
let c: Int = str_index_of(spec, ":")
|
||||
if c < 0 {
|
||||
barge = str_to_int(spec)
|
||||
} else {
|
||||
barge = str_to_int(str_slice(spec, 0, c))
|
||||
kind = str_slice(spec, c + 1, str_len(spec))
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if str_eq(f, "--live-mic") {
|
||||
live = true
|
||||
}
|
||||
if str_eq(f, "--resume") {
|
||||
resume = true
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
let plan: [String] = conv_load_manifest(mf)
|
||||
if resume {
|
||||
plan = conv_load_resume()
|
||||
organ_disclose("CONVERSE: resuming — \"as I was saying...\" (" + int_to_str(plan_count(plan)) + " segments left).")
|
||||
} else {
|
||||
organ_disclose("CONVERSE: utterance = \"" + conv_utterance(mf) + "\" (" + int_to_str(plan_count(plan)) + " segments).")
|
||||
}
|
||||
let stopped: Int = conv_run(plan, authority, barge, kind, live)
|
||||
println("{\"ok\": true, \"op\": \"converse\", \"stopped_at\": " + int_to_str(stopped) + ", \"complete\": " + bool_to_str(stopped < 0) + "}")
|
||||
return
|
||||
}
|
||||
|
||||
cli_usage()
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
// organ_converse.el — full-duplex, interruptible speech. The turn-taking organ.
|
||||
//
|
||||
// WHAT THIS IS FOR. A system that plays an utterance to completion and only
|
||||
// then listens is not conversational, it is a loudspeaker with a queue. Being
|
||||
// interruptible is not a feature bolted onto speech; it is most of what makes
|
||||
// speech social. So the utterance is not a blob of audio — it is an ordered,
|
||||
// SALIENCE-TAGGED MEANING-PLAN, and the organ speaks it while listening, decides
|
||||
// what to do when interrupted, and can pick the thread back up afterwards.
|
||||
//
|
||||
// THREE THINGS HAVE TO BE TRUE, and each one is a place naive implementations
|
||||
// go wrong:
|
||||
//
|
||||
// Barge-in is AT THE SAMPLE. When the mic hears speech, output stops on the
|
||||
// spot — not at the end of the current buffer, not at the end of the segment.
|
||||
// A listener experiences even a fifth of a second of continued talking as
|
||||
// being talked over. This is why the speaker realizer has pause/resume and
|
||||
// reports played_frames: "finish the buffer" is not barge-in.
|
||||
//
|
||||
// Yield-or-hold is a DECISION, not a rule. Stopping every time anyone makes a
|
||||
// noise is its own failure — it means Neuron can never finish a sentence that
|
||||
// matters. So the choice is grounded: how salient is what I am mid-saying,
|
||||
// how close am I to done, and how much authority does the interrupter have.
|
||||
// Holding the floor is justified when what I am saying matters AND finishing
|
||||
// is cheap AND the interrupter is not high-priority. Otherwise yield, because
|
||||
// the polite default is the right default.
|
||||
//
|
||||
// A backchannel is NOT an interruption. "mm-hm" means keep going. Treating it
|
||||
// as a barge-in makes the system stop every three seconds during ordinary
|
||||
// listening behaviour, which is worse than not listening at all. It is
|
||||
// distinguished by being brief and low-energy: sample again shortly after
|
||||
// onset, and if the speech already died away it was a backchannel.
|
||||
//
|
||||
// AND THE UTTERANCE SURVIVES. On yield, the remaining plan is persisted, so
|
||||
// Neuron can resume — "as I was saying" — instead of losing the thought. An
|
||||
// interruption should cost a turn, not the content.
|
||||
//
|
||||
// The AEC rail: the microphone runs with the OS voice-processing unit enabled
|
||||
// so it does not hear our own speaker. Without it Neuron barges in on its own
|
||||
// voice on the first syllable and the whole loop is unusable in a real room.
|
||||
//
|
||||
// Note what is NOT here: nothing about words. A segment carries a `text` field
|
||||
// purely as a label for disclosure. The organ speaks pre-rendered audio and
|
||||
// never inspects language — that is the language faculty's, and the seam holds.
|
||||
|
||||
// ── The meaning-plan ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// Stored as a flat [String] with stride 3 — file, salience-per-mille, text —
|
||||
// because El has no record type and parallel lists drift out of step under
|
||||
// editing. Salience is an integer per-mille rather than a Float so the decision
|
||||
// arithmetic stays exact and reproducible; a turn-taking decision that varies
|
||||
// with floating-point rounding is not one you can debug.
|
||||
|
||||
fn plan_new() -> [String] {
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
fn plan_add(plan: [String], file: String, salience_pm: Int, text: String) -> [String] {
|
||||
let p: [String] = plan
|
||||
p = native_list_append(p, file)
|
||||
p = native_list_append(p, int_to_str(salience_pm))
|
||||
p = native_list_append(p, text)
|
||||
return p
|
||||
}
|
||||
|
||||
fn plan_count(plan: [String]) -> Int {
|
||||
return native_list_len(plan) / 3
|
||||
}
|
||||
|
||||
fn plan_file(plan: [String], i: Int) -> String {
|
||||
return native_list_get(plan, i * 3)
|
||||
}
|
||||
|
||||
fn plan_salience(plan: [String], i: Int) -> Int {
|
||||
return str_to_int(native_list_get(plan, i * 3 + 1))
|
||||
}
|
||||
|
||||
fn plan_text(plan: [String], i: Int) -> String {
|
||||
return native_list_get(plan, i * 3 + 2)
|
||||
}
|
||||
|
||||
// ── Manifest ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// {"utterance": "...", "segments": [{"file":..., "salience":0.9, "text":"..."}]}
|
||||
// Salience arrives as a 0..1 float in the manifest and is converted once, here,
|
||||
// at the edge — the same discipline the runtime uses for wire encodings.
|
||||
|
||||
fn conv_salience_pm(raw: String) -> Int {
|
||||
// "0.85" -> 850. Parsed by hand rather than through a float so a manifest
|
||||
// typo degrades to a visible number instead of a silent 0.0.
|
||||
let dot: Int = str_index_of(raw, ".")
|
||||
if dot < 0 {
|
||||
let whole: Int = str_to_int(raw)
|
||||
return whole * 1000
|
||||
}
|
||||
let ip: Int = str_to_int(str_slice(raw, 0, dot))
|
||||
let frac: String = str_slice(raw, dot + 1, str_len(raw))
|
||||
let pm: Int = 0
|
||||
let scale: Int = 100
|
||||
let i: Int = 0
|
||||
while i < 3 {
|
||||
let d: Int = 0
|
||||
if i < str_len(frac) {
|
||||
let c: Int = str_char_code(frac, i)
|
||||
if c >= 48 {
|
||||
if c <= 57 {
|
||||
d = c - 48
|
||||
}
|
||||
}
|
||||
}
|
||||
pm = pm + d * scale
|
||||
scale = scale / 10
|
||||
i = i + 1
|
||||
}
|
||||
return ip * 1000 + pm
|
||||
}
|
||||
|
||||
fn conv_load_manifest(path: String) -> [String] {
|
||||
let plan: [String] = plan_new()
|
||||
let raw: String = fs_read(path)
|
||||
if str_eq(raw, "") {
|
||||
organ_disclose("CONVERSE: cannot read manifest " + path)
|
||||
return plan
|
||||
}
|
||||
let segs: String = json_get_raw(raw, "segments")
|
||||
let n: Int = json_array_len(segs)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let seg: String = json_array_get(segs, i)
|
||||
let file: String = json_get_string(seg, "file")
|
||||
let text: String = json_get_string(seg, "text")
|
||||
let sal: String = json_get_raw(seg, "salience")
|
||||
let pm: Int = conv_salience_pm(sal)
|
||||
if pm <= 0 {
|
||||
pm = 500
|
||||
}
|
||||
plan = plan_add(plan, file, pm, text)
|
||||
i = i + 1
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
fn conv_utterance(path: String) -> String {
|
||||
let raw: String = fs_read(path)
|
||||
return json_get_string(raw, "utterance")
|
||||
}
|
||||
|
||||
// ── The decision ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Returns: 0 = backchannel, carry on seamlessly
|
||||
// 1 = hold the floor ("hang on, let me finish this thought")
|
||||
// 2 = yield (stop, let them in)
|
||||
//
|
||||
// All arguments are per-mille integers. Holding requires BOTH that the material
|
||||
// is worth finishing AND that the interrupter is not high-authority — either
|
||||
// condition alone is not enough, because "what I'm saying is important" is
|
||||
// exactly the reasoning that produces a system nobody can get a word in against.
|
||||
fn conv_decide(salience_pm: Int, progress_pm: Int, authority_pm: Int, is_backchannel: Bool) -> Int {
|
||||
if is_backchannel {
|
||||
return 0
|
||||
}
|
||||
let hold_score: Int = (salience_pm * 6 + progress_pm * 4) / 10
|
||||
if hold_score >= 600 {
|
||||
if authority_pm < 800 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
// ── Resume ───────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The remaining plan, written where a later run can find it. This is what turns
|
||||
// an interruption into a pause rather than a loss.
|
||||
|
||||
fn conv_resume_path() -> String {
|
||||
let home: String = env("PERIPH_HOME")
|
||||
if str_eq(home, "") {
|
||||
return "peripheral/.resume.json"
|
||||
}
|
||||
return home + "/.resume.json"
|
||||
}
|
||||
|
||||
// Minimal JSON string escaping. Written here rather than reached for from the
|
||||
// runtime because the organ needs exactly two escapes and no dependency: a
|
||||
// segment label containing a quote or a backslash must not be able to produce a
|
||||
// resume file that fails to parse and silently loses the thread.
|
||||
fn conv_escape(s: String) -> String {
|
||||
let n: Int = str_len(s)
|
||||
let out: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let c: Int = str_char_code(s, i)
|
||||
if c == 34 {
|
||||
out = out + "\\\""
|
||||
} else {
|
||||
if c == 92 {
|
||||
out = out + "\\\\"
|
||||
} else {
|
||||
if c >= 32 {
|
||||
out = out + str_slice(s, i, i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn conv_persist_resume(plan: [String], start_at: Int, reason: String) -> Bool {
|
||||
let n: Int = plan_count(plan)
|
||||
let body: String = "{\"resume_from\": " + int_to_str(start_at) + ", \"reason\": \"" + reason + "\", \"segments\": ["
|
||||
let i: Int = start_at
|
||||
let first: Bool = true
|
||||
while i < n {
|
||||
if first == false {
|
||||
body = body + ", "
|
||||
}
|
||||
body = body + "{\"file\": \"" + plan_file(plan, i) + "\", \"salience\": " + int_to_str(plan_salience(plan, i)) + ", \"text\": \"" + conv_escape(plan_text(plan, i)) + "\"}"
|
||||
first = false
|
||||
i = i + 1
|
||||
}
|
||||
body = body + "]}\n"
|
||||
let ok: Bool = fs_write(conv_resume_path(), body)
|
||||
organ_disclose("CONVERSE: meaning-plan persisted (" + int_to_str(n - start_at) + " segments remain) — Neuron can resume the thread.")
|
||||
return ok
|
||||
}
|
||||
|
||||
fn conv_clear_resume() -> Bool {
|
||||
return fs_write(conv_resume_path(), "")
|
||||
}
|
||||
|
||||
// Read a persisted plan back. Salience is already per-mille here (we wrote it),
|
||||
// so it is NOT re-scaled — the manifest and the resume file are different
|
||||
// formats on purpose, and conflating them silently divides every salience by a
|
||||
// thousand.
|
||||
fn conv_load_resume() -> [String] {
|
||||
let plan: [String] = plan_new()
|
||||
let raw: String = fs_read(conv_resume_path())
|
||||
if str_eq(raw, "") {
|
||||
return plan
|
||||
}
|
||||
let segs: String = json_get_raw(raw, "segments")
|
||||
let n: Int = json_array_len(segs)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let seg: String = json_array_get(segs, i)
|
||||
plan = plan_add(plan, json_get_string(seg, "file"), json_get_int(seg, "salience"), json_get_string(seg, "text"))
|
||||
i = i + 1
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// ── The loop ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// live_mic : open the microphone with AEC and let real speech drive barge-in.
|
||||
// barge_ms : if >= 0, inject a barge event at that offset into the utterance
|
||||
// instead. Deterministic, so the decision paths can be exercised
|
||||
// without a room and a person — the same reason periph.swift has it.
|
||||
// kind : "backchannel" or "bargein", for the injected case.
|
||||
// authority : interrupter authority, per-mille.
|
||||
//
|
||||
// Returns the index the utterance stopped at, or -1 if it completed.
|
||||
|
||||
fn conv_run(plan: [String], authority_pm: Int, barge_ms: Int, kind: String, live_mic: Bool) -> Int {
|
||||
let n: Int = plan_count(plan)
|
||||
if n <= 0 {
|
||||
organ_disclose("CONVERSE: nothing to say.")
|
||||
return 0 - 1
|
||||
}
|
||||
if speaker_available() == 0 {
|
||||
organ_disclose("CONVERSE: no speaker on this build — cannot hold a conversation.")
|
||||
return 0 - 1
|
||||
}
|
||||
|
||||
let mic_live: Bool = false
|
||||
if live_mic {
|
||||
if organ_may_listen() {
|
||||
let m: Int = mic_monitor_start()
|
||||
if m == 1 {
|
||||
organ_disclose("CONVERSE: full-duplex — mic listening WHILE speaking, AEC on (won't self-interrupt).")
|
||||
mic_live = true
|
||||
}
|
||||
if m == 2 {
|
||||
organ_disclose("CONVERSE: full-duplex — mic listening, but AEC UNAVAILABLE; raising the VAD floor so we do not barge in on ourselves.")
|
||||
mic_live = true
|
||||
}
|
||||
if m == 0 {
|
||||
organ_disclose("CONVERSE: could not open the mic monitor — falling back to injected events.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if mic_live == false {
|
||||
organ_disclose("CONVERSE: deterministic mode (live mic off).")
|
||||
}
|
||||
|
||||
// Without AEC the mic hears the speaker, so the threshold has to sit above
|
||||
// our own output. This is a mitigation and not a fix: the honest note is
|
||||
// that barge-in is markedly less sensitive in this mode.
|
||||
let vad_pm: Int = 20
|
||||
if mic_live {
|
||||
if mic_monitor_start() == 2 {
|
||||
vad_pm = 60
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed_ms: Int = 0
|
||||
let prior_ms: Int = 0
|
||||
let handled: Bool = false
|
||||
// An injected barge is ONE event, not a condition that stays true. Without
|
||||
// this the deadline re-fires on every poll after a backchannel resume, and
|
||||
// the utterance live-locks: paused, resumed, paused again, forever.
|
||||
let injected_fired: Bool = false
|
||||
let i: Int = 0
|
||||
|
||||
while i < n {
|
||||
let file: String = plan_file(plan, i)
|
||||
let sal: Int = plan_salience(plan, i)
|
||||
let frames: Int = wav_frames(file)
|
||||
let rate: Int = wav_rate(file)
|
||||
if frames <= 0 {
|
||||
organ_disclose("CONVERSE: missing or unreadable segment '" + file + "', skipping.")
|
||||
i = i + 1
|
||||
} else {
|
||||
let dur_ms: Int = frames * 1000 / rate
|
||||
organ_disclose("CONVERSE: speaking segment " + int_to_str(i + 1) + "/" + int_to_str(n) + " (salience " + int_to_str(sal) + "/1000) — \"" + plan_text(plan, i) + "\"")
|
||||
let started: Int = speaker_play_wav_async(file)
|
||||
if started == 0 {
|
||||
organ_disclose("CONVERSE: could not start playback for '" + file + "'.")
|
||||
i = i + 1
|
||||
} else {
|
||||
let seg_ms: Int = 0
|
||||
let done: Bool = false
|
||||
let interrupted: Bool = false
|
||||
let speech_ticks: Int = 0
|
||||
|
||||
while done == false {
|
||||
sleep_ms(10)
|
||||
seg_ms = seg_ms + 10
|
||||
|
||||
if speaker_playing() == 0 {
|
||||
done = true
|
||||
} else {
|
||||
// The tick counter is an approximation — each pass costs
|
||||
// more than the sleep it asked for. The DAC position is
|
||||
// the truth, so drive the injected deadline off THAT and
|
||||
// an injected barge lands where it was asked to land.
|
||||
let pos_ms: Int = speaker_played_frames() * 1000 / rate
|
||||
elapsed_ms = prior_ms + pos_ms
|
||||
// --- onset detection: real speech, or an injected event ---
|
||||
let onset: Bool = false
|
||||
if mic_live {
|
||||
let rms: Float = mic_monitor_rms()
|
||||
let rms_pm: Int = float_to_int(rms * 1000.0)
|
||||
if rms_pm > vad_pm {
|
||||
speech_ticks = speech_ticks + 1
|
||||
} else {
|
||||
speech_ticks = 0
|
||||
}
|
||||
// ~60ms of continuous voice: short enough to feel
|
||||
// instant, long enough that a door closing is not a turn.
|
||||
if speech_ticks >= 3 {
|
||||
if handled == false {
|
||||
onset = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if barge_ms >= 0 {
|
||||
if injected_fired == false {
|
||||
if elapsed_ms >= barge_ms {
|
||||
onset = true
|
||||
injected_fired = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if onset {
|
||||
handled = true
|
||||
// (1) BARGE-IN — pause on the spot.
|
||||
speaker_pause()
|
||||
let played: Int = speaker_played_frames()
|
||||
let at_ms: Int = played * 1000 / rate
|
||||
let progress_pm: Int = at_ms * 1000 / dur_ms
|
||||
if progress_pm > 1000 {
|
||||
progress_pm = 1000
|
||||
}
|
||||
organ_disclose("CONVERSE: << user speech at " + int_to_str(at_ms) + "ms into segment " + int_to_str(i + 1) + " — PAUSED instantly >>")
|
||||
|
||||
// (2) backchannel or real barge-in?
|
||||
let is_bc: Bool = false
|
||||
if barge_ms >= 0 {
|
||||
if str_eq(kind, "backchannel") {
|
||||
is_bc = true
|
||||
}
|
||||
} else {
|
||||
// Live: look again ~250ms after onset. If the
|
||||
// energy has already collapsed it was "mm-hm".
|
||||
sleep_ms(250)
|
||||
let r2: Float = mic_monitor_rms()
|
||||
if float_to_int(r2 * 1000.0) < 15 {
|
||||
is_bc = true
|
||||
}
|
||||
}
|
||||
|
||||
// (3) yield, hold, or carry on
|
||||
let d: Int = conv_decide(sal, progress_pm, authority_pm, is_bc)
|
||||
if d == 0 {
|
||||
organ_disclose("CONVERSE: read as BACKCHANNEL (\"mm-hm\") — keep going, resume seamlessly.")
|
||||
handled = false
|
||||
speech_ticks = 0
|
||||
speaker_resume()
|
||||
}
|
||||
if d == 1 {
|
||||
organ_disclose("CONVERSE: HOLD the floor — \"hang on, let me finish this thought.\" (salience " + int_to_str(sal) + ", progress " + int_to_str(progress_pm) + ")")
|
||||
speaker_resume()
|
||||
// Finish THIS segment, then yield the remainder:
|
||||
// holding is a request for a moment, not a claim
|
||||
// on the rest of the conversation.
|
||||
while speaker_playing() == 1 {
|
||||
sleep_ms(20)
|
||||
}
|
||||
speaker_stop()
|
||||
conv_persist_resume(plan, i + 1, "held-then-yield")
|
||||
if mic_live {
|
||||
mic_monitor_stop()
|
||||
}
|
||||
return i + 1
|
||||
}
|
||||
if d == 2 {
|
||||
organ_disclose("CONVERSE: YIELD — stop, let them in. Remembering where I was (resumable).")
|
||||
speaker_stop()
|
||||
conv_persist_resume(plan, i, "yield")
|
||||
if mic_live {
|
||||
mic_monitor_stop()
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if interrupted == false {
|
||||
prior_ms = prior_ms + dur_ms
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conv_clear_resume()
|
||||
organ_disclose("CONVERSE: utterance complete (uninterrupted).")
|
||||
if mic_live {
|
||||
mic_monitor_stop()
|
||||
}
|
||||
return 0 - 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,17 +45,43 @@ returned 60k–230k-char unbounded traversals (this very session hit 104 KB and
|
||||
|
||||
## Layer 2 — primitive agentic tools (Neuron runs itself)
|
||||
|
||||
The base verbs all agentic behavior composes from — grounded in the LIVE
|
||||
cog-arch (`think` is the one operation; faculties are its steering-space labels;
|
||||
the correspondence-beat is the reflexive learning loop).
|
||||
The base verbs all agentic behavior composes from.
|
||||
|
||||
> **⚠ The "PROVEN" verdicts in this table were measured against a build dated
|
||||
> 2026-08-14 and four of the five are now known to have been proving the wrong
|
||||
> thing (2026-08-16).** A verdict of PROVEN meant *the route returned a
|
||||
> well-formed response*, not *the response was derivable from what produced it*.
|
||||
> Corrections below, each with the measurement. Authority:
|
||||
> `lang/spec/correspondence-and-censorship.md`.
|
||||
|
||||
| op | signature | engram builtin | status on clone (gate-1 recipe) |
|
||||
|----|-----------|----------------|---------------------------------|
|
||||
| `think` | `think({seeds, faculty})` faculty ∈ reason·abduce·induce·plan·analogize·recognize·discern·synthesize | `engram_think_json` | **PROVEN** — all 8 faculties return real 768-dim gradients (n_support 30–282) |
|
||||
| `think` | `think({seeds, faculty})` faculty ∈ reason·abduce·induce·plan·analogize·recognize·discern·synthesize | `engram_think_json` | ~~PROVEN — all 8 faculties return real 768-dim gradients~~ **RETRACTED, then re-proven differently.** The gradients were real in *shape* only: the call passed `NULL` as the anchor, `engram_think` re-origins at `anchor ? anchor : region->centroid`, and **the centroid is the one point where the gradient is zero by construction**. Measured: every faculty returned `{"direction":[0,0,…],"spread":0,"magnitude":1,"confidence":0.5}` — identical, differing only in its label. Fixed in **#141/#142**; gradients now vary by seed |
|
||||
| `attend` | `attend({node, observer, salience})` | `engram_attend_json` | **PROVEN** (returns `salient-to`) |
|
||||
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PROVEN** |
|
||||
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | **PROVEN** (grounded-by edge, grounding=0.912, written) |
|
||||
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN** (real Stance: `stance-induce-…`, brier, reliability, written) |
|
||||
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PARTIAL.** `may_assert` is real. `"still_held"` is a **hardcoded literal `true`** — `el_runtime.c:14538` emits it unconditionally, so it reports nothing it measured. Violates the invariant *a returned value must be derivable from what produced it* |
|
||||
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | ~~PROVEN (grounded-by edge, grounding=0.912, written)~~ **RETRACTED.** That 0.912 was structural, not evidential: the call wrote the edge between the two *region hubs* and echoed them back as though they were the caller's input, so when both seeds resolved into one region it **grounded a node against itself and returned a confident score**. Measured: grounding `3b9ced5d` against `6edf8c79` scored **0.98883** purely because `6edf8c79` is the hub of `3b9ced5d`'s region; two independent agents reported 0.885 / 0.909 self-groundings as confident. **#147** grounds the node asked about, reports `claim_region`/`evidence_region` separately, and refuses three circular shapes. **The operation itself is still the wrong shape** — see below |
|
||||
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN, and it was writing into a void.** The Stance, brier and reliability were real and really persisted — but `think` built a *neutral* stance every call and never loaded them, so every beat's calibration was written and thrown away on the next read. Fixed in **#146**: `think` resumes `stance-<faculty>-<hub>`, the same id the beat writes. Confidence **0.5 → 0.930726** on a calibrated region |
|
||||
|
||||
### What this table gets structurally wrong
|
||||
|
||||
- **`faculty` is not a parameter.** `reason` changes the *estimate* (a read),
|
||||
`induce` changes the *parameters* (this is exactly what `learn` does), and
|
||||
`abduce` changes the *structure* — a **write**, which `GeoGradient` cannot
|
||||
express. A write cannot be a parameter of a read. That the eight were listed as
|
||||
interchangeable values of one argument is why all eight returning the same thing
|
||||
looked like a pass. Underneath, `engram/src/server.el:1870–1886` routes six of
|
||||
them into one call with a string argument, and the name only reaches
|
||||
`engram_think` through the stance — `cog_stance_init` stores it and nothing
|
||||
reads it.
|
||||
- **`ground` should not mint an edge at all.** Grounding is not a subsystem and
|
||||
not a score: **it is the edge weight.** `grounded-by` as a relation type models
|
||||
grounding as a relation *between* nodes when it is a property *of* a relation.
|
||||
#147 corrected a scalar rather than deleting the operation; deletion is
|
||||
sequenced.
|
||||
- **`addWonderQuestion`** (Layer 1, `write`) treats wonder as an enumerable
|
||||
instance you push. **Wonder is the boundary** — where activation spreads and
|
||||
finds thin or absent geometry. There are about six, the same for everyone, and
|
||||
they never close. A manifest materializes a property as a stored artifact.
|
||||
|
||||
`comprehend`/`realize`/`intend` are **compositions**, not separate live
|
||||
primitives: comprehend = write+activate (world→geometry), realize = assert
|
||||
@@ -69,6 +95,26 @@ execution→integrate) composes over `think`+`ground`+`learn`+`write`/`relate`.
|
||||
`kn-efeb4a5b…` / `kn-5b606390…`, are refused — identity routes through
|
||||
intentional-cultivation, as enforced today.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16).** This describes what the surface enforces, which
|
||||
> is accurate — but the enforcement is the wrong kind of thing:
|
||||
>
|
||||
> > **In an immutable substrate, any mechanism that refuses a write is either
|
||||
> > redundant with immutability, or an epistemic constraint misfiled as a
|
||||
> > protective one.**
|
||||
>
|
||||
> "Keystone" means **load-bearing**, not precious. The real requirement is
|
||||
> **non-circularity of the reference frame** — a reference fitted to its own
|
||||
> readings reports perfect correspondence forever while drift becomes undetectable
|
||||
> from inside — and that is satisfied *temporally*, not by a gate: the frame
|
||||
> updates while activation is internally seeded, not while it is being used to act.
|
||||
> **Independence is *when*, not *what*.** Corruption requires mutation, and the
|
||||
> engram does not mutate: recoverability (the predecessor is always present),
|
||||
> governance (supersession *is* the audit trail), evidence quality, and rate all
|
||||
> fall out of the substrate. **Authorization** is the only residue and it is
|
||||
> bounded — an unauthorized writer can *propose*, never erase. Note also that the
|
||||
> live check is a substring match against two hard-coded ids
|
||||
> (`el_runtime.c:14337`).
|
||||
|
||||
## How the caller invokes Neuron agentically
|
||||
|
||||
Once the ops are registered as MCP tools (aliases in `surface.el`), the caller
|
||||
@@ -94,6 +140,16 @@ running itself.
|
||||
## Honest ledger (built vs staged)
|
||||
- **Route seam — IMPLEMENTED + PROVEN:** ported the `@route` codegen (from `feat/el-route-decorators`) into the worktree, rebuilt `elc` self-host, proved decorate→serve (`route_proof.el` on :8951); `surface.el` compiles with `el_route_dispatch` generated for all 8 ops.
|
||||
- **All ops PROVEN live on the clone** (gate-1 boot recipe, node-id anchors): read, write, relate, supersede (immutable), tombstone, think (8 faculties), ground, attend, learn — daemon alive through all mutations (node_count 13173→13176).
|
||||
> **⚠ Retracted in part (2026-08-16).** "The daemon stayed alive and every route
|
||||
> returned a well-formed response" is what was actually proven, and that is a
|
||||
> weaker claim than it reads as. See the Layer-2 table: `think` was reading at the
|
||||
> zero-gradient point, `ground` was scoring nodes against themselves, `assert`
|
||||
> emits a hardcoded field, and `learn` was persisting into a void. **A build that
|
||||
> passes because nothing checks whether a returned value is derivable from what
|
||||
> produced it has not been tested — it has been observed not to crash.** The
|
||||
> related discipline gap, also 2026-08-16: **no test without a negative control**
|
||||
> (#148's first attempt passed on the unpatched build too), and **no deploy
|
||||
> without verifying the artifact carries the fix** (nine instances in one session).
|
||||
- **Aperture-boundedness PROVEN:** vantage-read `limit=3 → 15 KB` vs `limit=50 → 363 KB` (fixes the whole-self dump).
|
||||
- **Bus:** `@manager` ops emit on the real `dharma_*` bus (explicit today, compiles) — same transport as the swarm (`wt/swarm-ccr`).
|
||||
- **STAGED (not guessed — needs the cognition-engram rebuild to verify link):** auto-injecting telemetry/interoception + bus emission at the decorated boundary (`cg_fn` diff in `SEAM_STAGED.md`); building the cognition engram with `surface.el` compiled in. No promote to live, no cutover (per rails).
|
||||
|
||||
Reference in New Issue
Block a user