Compare commits

..

1 Commits

Author SHA1 Message Date
bigmerge 5f3ddb8b8d Add grounded edge-propagation spec (task #50): core algorithm, proof harness, gated integration patches
El SDK CI - dev / build-and-test (pull_request) Failing after 14m31s
LTP/LTD-style belief grounding propagated along graph edges, with
union-find independence-guarded corroboration. Package: core C algorithm
(gep_core.h), a self-contained deterministic proof harness with recorded
output, staged runtime integration, and gated .el patches for the beat
hook and HTTP route.

Per the author's own LEDGER.md: built + proven on a clone, GATED pending
the engine/HNSW cutover — not wired into the live beat or routes.
Preserved here as a spec/reference artifact, not a request to merge into
the live path.
2026-08-15 14:26:06 -05:00
27 changed files with 1045 additions and 2511 deletions
-91
View File
@@ -1,91 +0,0 @@
> **STATUS: STAGING / PROOF-OF-SHAPE — not the deliverable.** This Python package
> proved the architecture end-to-end against the proven realizer faculty (faithful
> md/docx/midi from real geometry: 0 ungrounded claims, SACRED polarity). Per Will's
> steer, the DELIVERABLE is NATIVE: the seam lives on the existing EL realizer as
> **surface-as-profile** — see `../src/surface-profile.el` and
> `../tests/examples/surface-profile-demo.el` (compiles + runs through elc → C →
> binary). The concepts below (one geometry-carrying frame; surface = a pluggable
> profile; plan/realize; deterministic-from-meaning) are exactly what the native
> module implements. Keep this package as the validated proof; build native.
# Efferent Multimodal Projector
**geometry → any surface, faithfully.** Neuron's own document-generation faculty:
the efferent twin of the ingest organ. Ingest is afferent (world → geometry);
this is efferent (geometry → an arbitrary-format document / any modality).
Built against the **proven** realizer faculty (neuron-talk sidecar `:8756`,
artifact `art-7affa557`). The live soul (`:8742` / `:7770`) is contacted **only**
through the read-only, GET-only `engram_client` — never mutated.
## The pipeline (surface-agnostic)
```
geometry region + surface/format spec
→ PLAN (manifold → document skeleton/DAG; the geometry IS the outline) plan.py
→ REALIZE (proven realizer, scaled sentence → passage, each section faithful) realize.py
→ COHERE (document-level flow / transitions, not stitched sentences) cohere.py
→ EMIT (pluggable SurfaceProjector → the target surface) projectors/
```
**The surface is a PARAMETER.** `pipeline.build_ir(...)` builds ONE
surface-neutral `DocumentIR` (`document_ir.py`); `pipeline.emit(doc, surface)`
projects it to whichever surface you name. Markdown, docx, and MIDI are the same
IR emitted three ways.
## The pivot: a geometry-carrying IR
`DocumentIR` is **not** a text tree. Every `Block` carries BOTH:
- `.sentences` — realized faithful text (what **text** projectors read),
- `.provenance` — the source geometry: `subj_id / relation / obj / polarity /
confidence / importance / salience / node_id` (what **music / image / video**
projectors read).
That single decision is what makes the projector multimodal: text renders the
words; music/image decode the geometry. A claim with no provenance cannot exist
in the IR — faithfulness is structural.
## The one shared seam
`projectors/base.py` — `SurfaceProjector.project(frame: DocumentIR) -> bytes`
(+ `surface / media_type / ext / modality / profile`). Register with
`register()`. Adding a surface changes nothing upstream.
`TwoStageProjector` blesses the peer plan/realize decomposition:
`spec = plan(frame)`, `bytes = realize(spec)`, `project = realize∘plan`; the
`profile` is the pluggable per-surface knob (text lang-profile, music
instr/mode-profile). `projectors/midi.py` is the reference two-stage impl.
## Surfaces
| surface | modality | status | emitter |
|---|---|---|---|
| `markdown` | text | landed | own (str) |
| `docx` | text | landed | own minimal OOXML (stdlib `zipfile`+XML, no lib) |
| `midi` | audio | landed (symbolic-music proof) | own minimal SMF (stdlib `struct`, no lib) |
| `audio` (WAV) | audio | peer agent (additive synth) | conforms to `TwoStageProjector` |
| `image` | image | documented seam | `projectors/seams.py` |
| `video` | video | documented seam (image×sound×time) | `projectors/seams.py` |
Music maps: relation → scale degree (same relation → same pitch), **polarity →
major/minor third (SACRED negation is audible)**, confidence → duration,
importance → velocity, section → register. Deterministic projection from meaning
— nothing invented.
## Faithfulness
`provenance.py` audits the IR: **zero** ungrounded claims, SACRED polarity
preserved (negations reported, never dropped), COHERE introduces no new geometry
(connectives are marked). `trace_table()` emits the geometry → section → claim
table.
## Run
```bash
PY=~/Desktop/lang-realizers/venv/bin/python
PYTHONPATH=~/Desktop/neuron-talk:~/Desktop/lang-realizers $PY generate.py
# writes ./out/{neuron-self,engram-temporal}.{md,docx,mid} + *.audit.json + *.provenance.md
```
Requires the proven realizer env (spaCy + the neuron-talk/lang-realizers engine)
and the read-only engram at `:8742`.
-79
View File
@@ -1,79 +0,0 @@
"""cohere.py — COHERE stage: document-level flow, not stitched sentences.
Fidelity is REALIZE's job; FLOW is this stage's. The hard part beyond sentence
fidelity is that a document must read as one thing. We add connective tissue at
the passage level:
* an opening abstract that names what the document covers (built ONLY from the
section headings that already exist — it introduces no new claim),
* a short transition lead into each section after the first, drawn from a
fixed set of discourse connectives ("Beyond that,", "Relatedly,", ...) that
carry no propositional content,
* ordering so the highest-grounded section leads.
CRITICAL: every connective is marked ``kind="connective"`` in its provenance, so
the faithfulness audit can prove COHERE introduced ZERO new geometry claims. A
transition is discourse glue, never a fact.
"""
from __future__ import annotations
from document_ir import Block, DocumentIR, Provenance
# discourse connectives — pure flow, no propositional content
_TRANSITIONS = [
"Beyond that,", "Relatedly,", "In the same region,", "From there,",
"Alongside this,", "Further,", "Turning to the next facet,",
]
def _connective_prov() -> Provenance:
return Provenance(subj_id=None, subject=None, relation="", obj=None,
polarity="aff", confidence=1.0, node_id=None,
kind="connective")
def _abstract_block(doc: DocumentIR) -> Block:
"""A grounded opening: names the sections, asserts nothing new."""
headings = [s.heading for s in doc.sections]
if not headings:
return Block(role="lead")
if len(headings) == 1:
body = f"This document, generated from Neuron's geometry, covers {headings[0]}."
else:
listed = ", ".join(headings[:-1]) + f", and {headings[-1]}"
body = ("This document is projected directly from Neuron's meaning-geometry. "
f"It traces {listed}.")
b = Block(role="lead")
b.sentences.append(body)
b.provenance.append(_connective_prov())
return b
def cohere_document(doc: DocumentIR, *, add_abstract: bool = True,
add_transitions: bool = True) -> DocumentIR:
"""Order sections by grounding, add abstract + transitions (flow only)."""
# order: strongest-grounded section (mean confidence x #claims) first,
# but keep an explicitly-first section if the plan pinned one via level 1.
def _score(sec):
provs = [p for p in sec.all_provenance() if p.kind == "fact"]
if not provs:
return 0.0
mean_conf = sum(p.confidence for p in provs) / len(provs)
return mean_conf * len(provs)
doc.sections.sort(key=_score, reverse=True)
if add_transitions:
for i, sec in enumerate(doc.sections):
if i == 0 or not sec.blocks:
continue
lead = _TRANSITIONS[(i - 1) % len(_TRANSITIONS)]
first = sec.blocks[0]
if first.sentences:
# prepend the connective to the first sentence (flow, no new claim)
first.sentences[0] = f"{lead} {first.sentences[0][0].lower()}{first.sentences[0][1:]}"
if add_abstract:
doc.meta["abstract"] = _abstract_block(doc)
return doc
-111
View File
@@ -1,111 +0,0 @@
"""document_ir.py — the surface-neutral, GEOMETRY-CARRYING document intermediate.
This is the pivot of the whole efferent projector. A DocumentIR is NOT a text
tree. It is a projection of a meaning-geometry region that carries, at every
leaf, BOTH:
* the realized surface text (``Block.sentences``) — what a TEXT projector reads,
* the source geometry (``Block.provenance``) — what a MUSIC / IMAGE /
VIDEO projector reads.
Because the IR holds the geometry, not just the words, the SAME
plan -> realize -> cohere pipeline drives every surface. A markdown projector
renders the sentences; a music projector reads the provenance edges (salience,
importance, polarity, relation) and maps them onto a symbolic-music surface;
an image/video projector (documented seam) would read the same geometry.
Nothing in this module invents content. Every :class:`Provenance` points at a
real engram node id and a real relation. That is the faithfulness contract made
structural: a claim with no provenance cannot exist in the IR.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
# --------------------------------------------------------------------------- #
# Provenance — the geometry an emitted claim traces to. FAITHFULNESS is here.
# --------------------------------------------------------------------------- #
@dataclass
class Provenance:
"""One geometry edge behind one realized claim.
``kind`` distinguishes a FACT (a structural edge asserted by the geometry,
spoken as fact) from an INTERPRETATION (something attributed, spoken with
attribution) — the facts-as-facts + interpretations-attributed discipline
(memory 80927e26). ``polarity`` is SACRED: a negated edge stays negated.
"""
subj_id: str | None # source engram node id of the subject
subject: str | None # normalized subject surface
relation: str # predicate lemma (e.g. "use", "contain", "be")
obj: str | None # normalized object / complement surface
polarity: str = "aff" # "aff" | "neg" (SACRED — never silently flipped)
confidence: float = 0.0 # extraction confidence in [0,1]
node_id: str | None = None # engram node the claim was extracted from
kind: str = "fact" # "fact" | "interpretation"
importance: float = 0.0 # source node importance (drives music/emphasis)
salience: float = 0.0 # source node salience
def trace(self) -> str:
arrow = "-->" if self.polarity == "aff" else "--NOT-->"
return (f"[{(self.node_id or '?')[:8]}] {self.subject!r} {arrow}"
f"{self.relation} {self.obj!r} (conf {self.confidence:.2f})")
@dataclass
class Block:
"""A passage: one or more faithful sentences + the geometry they trace to.
``sentences`` and ``provenance`` are index-aligned where possible: sentence
``i`` was realized from ``provenance[i]``. A COHERE transition sentence with
no new geometry carries a provenance whose ``kind == "connective"`` so the
audit can see it introduced no new claim.
"""
sentences: list[str] = field(default_factory=list)
provenance: list[Provenance] = field(default_factory=list)
role: str = "body" # "body" | "lead" | "transition"
def text(self) -> str:
return " ".join(s.rstrip(". ") + "." for s in self.sentences if s.strip())
@dataclass
class Section:
heading: str
level: int = 2 # markdown heading level / outline depth
blocks: list[Block] = field(default_factory=list)
seed_ids: list[str] = field(default_factory=list) # geometry nodes of section
summary: str = "" # one-line grounded gloss (for pptx bullets / TOC)
def all_provenance(self) -> list[Provenance]:
out: list[Provenance] = []
for b in self.blocks:
out.extend(b.provenance)
return out
@dataclass
class DocumentIR:
"""The surface-neutral document. Built ONCE, projected to ANY surface."""
title: str
subtitle: str = ""
sections: list[Section] = field(default_factory=list)
seed_id: str | None = None # the geometry region root
format_spec: dict[str, Any] = field(default_factory=dict) # requested shape
meta: dict[str, Any] = field(default_factory=dict)
# -- geometry facets (what non-text projectors consume) ----------------- #
def all_provenance(self) -> list[Provenance]:
out: list[Provenance] = []
for s in self.sections:
out.extend(s.all_provenance())
return out
def claim_count(self) -> int:
return sum(1 for p in self.all_provenance() if p.kind in ("fact", "interpretation"))
def ungrounded_count(self) -> int:
"""Claims with no traceable node — MUST be zero for a faithful doc."""
return sum(1 for p in self.all_provenance()
if p.kind in ("fact", "interpretation") and not p.node_id)
-81
View File
@@ -1,81 +0,0 @@
"""generate.py — drive the projector: one geometry region -> many surfaces.
Proves the thesis with REAL output: builds ONE surface-neutral DocumentIR from
Neuron's OWN self-geometry (read-only against the live soul via the proven
faculty), then EMITS it to Markdown, docx, and MIDI — the same plan/realize/
cohere, three surfaces. Writes the files + the faithfulness audit to ./out/.
"""
from __future__ import annotations
import json
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
import pipeline # noqa: E402
import provenance # noqa: E402
from geometry import load_self_region # noqa: E402
OUT = os.path.join(_HERE, "out")
def _emit_all(doc, stem):
"""Emit one IR to every text/audio surface + audit + provenance."""
for surface in ("markdown", "docx", "midi"):
data = pipeline.emit(doc, surface)
proj = pipeline.get_projector(surface)
path = os.path.join(OUT, f"{stem}.{proj.ext}")
with open(path, "wb") as f:
f.write(data)
print(f" emitted {surface:9s} -> {os.path.basename(path)} ({len(data)} bytes)")
a = provenance.audit(doc)
with open(os.path.join(OUT, f"{stem}.audit.json"), "w") as f:
json.dump(a, f, indent=2)
with open(os.path.join(OUT, f"{stem}.provenance.md"), "w") as f:
f.write(provenance.trace_table(doc))
print(" audit:", {k: a[k] for k in ("claims", "ungrounded_claims",
"negations_preserved", "distinct_source_nodes", "faithful")})
return a
def main():
os.makedirs(OUT, exist_ok=True)
print("surfaces registered:", pipeline.available_surfaces())
# ---- Document 1: Neuron's self-description (marquee) ------------------- #
print("\n[1] Neuron self-description")
region = load_self_region(max_nodes=9)
print(" self region:", region)
doc1 = pipeline.build_ir(
None, region=region,
title="Neuron: A Self-Description from Its Own Geometry",
subtitle="Projected efferently from the engram — every claim traces a node.",
format_spec={"genre": "self-description", "register": "expository"},
max_sections=5, conf_floor=0.6)
print(f" IR: {len(doc1.sections)} sections, {doc1.claim_count()} claims, "
f"ungrounded={doc1.ungrounded_count()}")
_emit_all(doc1, "neuron-self")
# ---- Document 2: a coherent, clean whitepaper-style section ------------ #
print("\n[2] Whitepaper-style section (coherent clean region)")
doc2, _ = pipeline.project(
["chronoception", "time", "awareness", "engram", "temporal"],
surface="markdown",
title="Temporal Awareness in the Engram",
subtitle="A section projected from the geometry of chronoception.",
format_spec={"genre": "whitepaper-section", "register": "technical"},
max_sections=4)
print(f" IR: {len(doc2.sections)} sections, {doc2.claim_count()} claims, "
f"ungrounded={doc2.ungrounded_count()}")
_emit_all(doc2, "engram-temporal")
# echo both markdowns so they are visible in the run log
for stem, doc in (("neuron-self", doc1), ("engram-temporal", doc2)):
print(f"\n===== GENERATED MARKDOWN — {stem} =====\n")
print(pipeline.emit(doc, "markdown").decode())
if __name__ == "__main__":
main()
-129
View File
@@ -1,129 +0,0 @@
"""geometry.py — READ-ONLY loader for a meaning-geometry region.
The efferent projector never writes to the soul. This module reaches the
geometry through the PROVEN, read-only neuron-talk faculty (``engram_client``,
GET-only, which physically refuses non-GET methods) against the running sidecar
soul. The live daemon :8742 / :7770 is contacted ONLY through that read-only
client — never mutated.
A "region" is a seed node plus a bounded neighborhood: the manifold that will
become the document's skeleton. We pool a few single-term lexical searches
(the engram search is a single-term matcher) and, when available, walk one hop
of reified neighbors, then rank by self/importance signal.
"""
from __future__ import annotations
import os
import sys
# Wire in the proven faculty (own-the-core: we reuse it, we do not fork it).
_NT = os.path.expanduser("~/Desktop/neuron-talk")
_LR = os.path.expanduser("~/Desktop/lang-realizers")
for _p in (_NT, _LR):
if _p not in sys.path:
sys.path.insert(0, _p)
from engram_client import ReadOnlyEngramClient # noqa: E402
class Region:
"""A geometry region: ranked nodes + the reified edges among them."""
def __init__(self, seed: str, nodes: list[dict], edges: list[dict]):
self.seed = seed
self.nodes = nodes # ranked engram node dicts
self.edges = edges # [{src, dst, edge, ...}]
self.by_id = {n["id"]: n for n in nodes if n.get("id")}
def __repr__(self):
return f"<Region seed={self.seed!r} nodes={len(self.nodes)} edges={len(self.edges)}>"
def _prose_quality(content: str) -> float:
"""Reward clean expository prose; penalize shouty banner-dense nodes.
A high ALLCAPS-word ratio or very short content signals a banner/telegraphic
memory node that extracts into garbage. Clean declarative prose scores high.
"""
if not content or not content.strip():
return 0.0
words = content.split()
if len(words) < 8:
return 0.1
caps = sum(1 for w in words if len(w) > 2 and w.strip(".,:;'\"-").isupper())
caps_ratio = caps / max(1, len(words))
# sentences with lowercase interior words read as prose
lower = sum(1 for w in words if w[:1].islower())
lower_ratio = lower / max(1, len(words))
return max(0.0, 1.2 * lower_ratio - 2.0 * caps_ratio)
def _relevance(content: str, terms: list[str]) -> float:
"""Topical relevance to the seed terms — keeps a region ON-THEME so a clean
but off-topic node cannot hijack the document."""
if not terms:
return 0.0
low = (content or "").lower()
hits = sum(1 for t in terms if t.lower() in low)
return hits / max(1, len(terms))
def _node_rank(n: dict, terms: list[str] | None = None) -> float:
return (float(n.get("importance") or 0.0) * 2.0
+ float(n.get("salience") or 0.0)
+ 1.5 * _prose_quality(n.get("content") or "")
+ 2.0 * _relevance(n.get("content") or "", terms or [])
+ (0.5 if (n.get("content") or "").strip() else 0.0))
def load_region(seed_terms: list[str] | str, *, client: ReadOnlyEngramClient | None = None,
max_nodes: int = 10, per_term: int = 20, hop: bool = True) -> Region:
"""Pull a bounded geometry region around ``seed_terms`` (read-only).
``seed_terms`` may be a single string or several probe terms; results are
pooled and de-duplicated. When ``hop`` and the reified neighbor endpoint is
live, one hop of neighbors is folded in so the region is a real
neighborhood, not just a keyword hit list.
"""
client = client or ReadOnlyEngramClient()
if isinstance(seed_terms, str):
seed_terms = [seed_terms]
pool: dict[str, dict] = {}
for term in seed_terms:
for n in client.search(term, limit=per_term):
if isinstance(n, dict) and n.get("id"):
pool.setdefault(n["id"], n)
ranked = sorted(pool.values(), key=lambda n: _node_rank(n, seed_terms),
reverse=True)
nodes = ranked[:max_nodes]
edges: list[dict] = []
if hop and nodes:
present = {n["id"] for n in nodes}
for n in list(nodes):
try:
for nb in client.neighbors(n["id"]):
node = nb.get("node") if isinstance(nb, dict) else None
edge = nb.get("edge") if isinstance(nb, dict) else None
if node and node.get("id"):
edges.append({"src": n["id"], "dst": node["id"],
"edge": edge})
# fold a strong neighbor into the region (bounded)
if (node["id"] not in present and len(nodes) < max_nodes + 6
and _node_rank(node, seed_terms) > 0.4):
present.add(node["id"])
nodes.append(node)
except Exception: # noqa: BLE001 — read-only best-effort; never fatal
continue
return Region(seed=", ".join(seed_terms), nodes=nodes, edges=edges)
def load_self_region(client: ReadOnlyEngramClient | None = None,
max_nodes: int = 10) -> Region:
"""The self/identity region — Neuron's own geometry, for self-description."""
return load_region(["self", "identity", "Neuron", "values", "memory",
"imprint", "consciousness"],
client=client, max_nodes=max_nodes)
-67
View File
@@ -1,67 +0,0 @@
"""pipeline.py — the Efferent Multimodal Projector, top level.
geometry region + surface/format spec
-> PLAN (manifold -> document skeleton/DAG)
-> REALIZE (proven realizer, sentence -> passage, each section faithful)
-> COHERE (document-level flow / transitions, not stitched sentences)
-> EMIT (pluggable SurfaceProjector -> the target surface)
THE SURFACE IS A PARAMETER. ``project(...)`` builds the geometry-carrying
DocumentIR once, then hands it to whichever surface projector the caller named.
Markdown, docx, and midi (music) are all the SAME IR emitted differently. That
is the efferent multimodal projector: geometry -> any surface.
"""
from __future__ import annotations
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
sys.path.insert(0, os.path.join(_HERE, "projectors"))
from cohere import cohere_document # noqa: E402
from document_ir import DocumentIR # noqa: E402
from geometry import Region, load_region # noqa: E402
from plan import plan_document # noqa: E402
from realize import realize_document # noqa: E402
# registering the projectors (import for side-effect: each self-registers)
import projectors.markdown # noqa: E402,F401
import projectors.docx # noqa: E402,F401
import projectors.midi # noqa: E402,F401
import projectors.seams # noqa: E402,F401
from projectors.base import available_surfaces, get_projector # noqa: E402
def build_ir(seed_terms, *, title: str, subtitle: str = "",
format_spec: dict | None = None,
region: Region | None = None,
max_sections: int = 8, conf_floor: float = 0.55) -> DocumentIR:
"""geometry -> PLAN -> REALIZE -> COHERE = the surface-neutral DocumentIR."""
region = region or load_region(seed_terms)
doc = plan_document(region, title=title, subtitle=subtitle,
format_spec=format_spec or {},
conf_floor=conf_floor, max_sections=max_sections)
doc = realize_document(doc)
doc = cohere_document(doc)
return doc
def emit(doc: DocumentIR, surface: str) -> bytes:
"""EMIT: project the built IR onto one surface (surface = a parameter)."""
return get_projector(surface).project(doc)
def project(seed_terms, *, surface: str, title: str, subtitle: str = "",
format_spec: dict | None = None, region: Region | None = None,
max_sections: int = 8) -> tuple[DocumentIR, bytes]:
"""The full efferent projection: geometry + surface -> (IR, bytes)."""
doc = build_ir(seed_terms, title=title, subtitle=subtitle,
format_spec=format_spec, region=region,
max_sections=max_sections)
return doc, emit(doc, surface)
__all__ = ["build_ir", "emit", "project", "available_surfaces",
"get_projector", "load_region", "DocumentIR"]
-192
View File
@@ -1,192 +0,0 @@
"""plan.py — PLAN stage: geometry region -> document skeleton (a DAG/outline).
The manifold becomes the skeleton. We extract faithful propositions from the
region's nodes (the proven neuron-talk extractor, SACRED polarity preserved),
apply a quality floor, then GROUP them into sections. Grouping is by source
node — each engram node is one coherent topic, so one salient node becomes one
section. The section ORDER is the node ranking (importance/salience): the
geometry decides the outline, not a template.
Output: a DocumentIR whose sections carry seed node ids and empty blocks. REALIZE
fills the blocks; the plan owns the structure.
"""
from __future__ import annotations
import os
import re
import sys
_NT = os.path.expanduser("~/Desktop/neuron-talk")
_LR = os.path.expanduser("~/Desktop/lang-realizers")
for _p in (_NT, _LR):
if _p not in sys.path:
sys.path.insert(0, _p)
import propositions # noqa: E402 (the proven, faithful extractor)
from document_ir import DocumentIR, Section # noqa: E402
from geometry import Region # noqa: E402
# --------------------------------------------------------------------------- #
# Proposition quality — keep only clean, well-grounded claims.
# --------------------------------------------------------------------------- #
_JUNK_RE = re.compile(r"[.][a-z]{1,3}\b|[^A-Za-z0-9 '\-]") # ".o", stray symbols
def _has_banner_token(s: str) -> bool:
"""True if any word is an ALLCAPS banner token (DHARMA, ENGRAM, MEASURED)."""
for w in (s or "").split():
core = w.strip(".,:;'\"-")
if len(core) > 2 and core.isupper():
return True
return False
def _clean_prop(p, floor: float) -> bool:
if p.confidence < floor:
return False
if not p.subject or not (p.object or (p.obj_np is not None)):
return False
subj = (p.subject or "").strip()
obj = (p.object or "").strip()
if len(subj) < 2:
return False
# banner-derived shouty fragments read as garbage in prose
if _has_banner_token(subj) or _has_banner_token(obj):
return False
if propositions._is_shouty(p.sentence or ""):
return False
# junk tokens: file-extension fragments (".o"), stray non-word symbols
if _JUNK_RE.search(subj) or _JUNK_RE.search(obj):
return False
# a proposition whose object repeats the subject is usually a parse artifact
if obj and subj.lower() == obj.lower():
return False
# a bare copula with no real complement ("X is it") reads as noise
if p.predicate == "be" and obj.lower() in ("it", "no", "nothing", "empty", ""):
return False
return True
def _dedup(props):
"""Drop duplicate claims. Two axes: (a) identical (pred,obj,polarity), and
(b) same (subject,predicate) — which collapses a mis-split compound like
"detection is post-hoc eval" -> "Detection is post/hoc/eval" into one claim
(keep the highest-confidence surface)."""
props = sorted(props, key=lambda p: p.confidence, reverse=True)
seen_po, seen_sp, out = set(), set(), []
for p in props:
subj = (p.subject or "").lower()
po = (p.predicate, (p.object or "").lower(), p.polarity)
sp = (subj, p.predicate, p.polarity)
if po in seen_po or sp in seen_sp:
continue
seen_po.add(po)
seen_sp.add(sp)
out.append(p)
return out
# --------------------------------------------------------------------------- #
# Heading derivation — a clean human heading from a node.
# --------------------------------------------------------------------------- #
_HEADING_RE = re.compile(r"^\s*#{1,4}\s+(.{2,70})\s*$", re.M)
# node-type / system labels that are NOT topical headings
_NONTOPIC_LABEL = re.compile(r"^(memory|node|knowledge|doc|session)[:/]", re.I)
def _titlecase_banner(s: str) -> str:
"""A shouty banner ("CHRONOCEPTION — SCALE-INVARIANCE") makes a fine title
once Title-cased. Keep short acronyms uppercase."""
def fix(w):
core = w.strip("—-:,.")
if len(core) <= 3 and core.isupper():
return w # acronym
return w.capitalize()
return " ".join(fix(w) for w in s.split())
def _clean_heading(text: str) -> str | None:
"""First line only, no markdown, capped, banner Title-cased. None if unusable."""
if not text:
return None
line = text.strip().splitlines()[0]
line = re.sub(r"^#+\s*", "", line).strip().strip("#").strip()
# cut at a natural break so a long banner heading stays a heading, not a para
for sep in ("", " ", ": ", ". "):
if sep in line and len(line) > 48:
line = line.split(sep)[0].strip()
break
if not (3 <= len(line) <= 64):
return None
if propositions._is_shouty(line):
line = _titlecase_banner(line)
return line or None
def _heading_for(node: dict, fallback: str) -> str:
label = (node.get("label") or "").strip()
content = node.get("content") or ""
candidates: list[str] = []
# a node-type label ("memory:remembered") is never a topic — skip it
if label and not _NONTOPIC_LABEL.match(label):
candidates.append(label)
m = _HEADING_RE.search(content)
if m:
candidates.append(m.group(1))
# the leading banner/first sentence of the content is often the real title
first = re.split(r"(?<=[.\n])", content.strip(), maxsplit=1)[0] if content.strip() else ""
candidates.append(first)
for c in candidates:
h = _clean_heading(c)
if h:
return h
return fallback
def plan_document(region: Region, *, title: str, subtitle: str = "",
format_spec: dict | None = None,
conf_floor: float = 0.55,
max_sections: int = 8,
max_claims_per_section: int = 6) -> DocumentIR:
"""Region -> DocumentIR skeleton. The geometry dictates the outline."""
format_spec = format_spec or {}
doc = DocumentIR(title=title, subtitle=subtitle,
seed_id=region.nodes[0]["id"] if region.nodes else None,
format_spec=format_spec)
made = 0
seen_headings: set[str] = set()
for node in region.nodes:
if made >= max_sections:
break
props = propositions.extract(node.get("content") or "",
node_id=node.get("id"),
node_importance=float(node.get("importance") or 0.0),
max_sentences=10)
props = [p for p in props if _clean_prop(p, conf_floor)]
props = _dedup(props)
props.sort(key=lambda p: p.confidence, reverse=True)
props = props[:max_claims_per_section]
if not props:
continue
heading = _heading_for(node, fallback=f"Region {made + 1}")
# cross-section dedup: a topic appears once. Distinguish by top claim
# subject, else drop the collision so the outline stays clean.
if heading.lower() in seen_headings:
subj = (props[0].subject or "").strip().title()
alt = f"{heading}: {subj}" if subj and subj.lower() not in heading.lower() else None
if alt and alt.lower() not in seen_headings and len(alt) <= 64:
heading = alt
else:
continue
seen_headings.add(heading.lower())
sec = Section(heading=heading, level=2, seed_ids=[node["id"]])
# stash the planned propositions on the section for REALIZE
sec.__dict__["_planned_props"] = props
sec.__dict__["_node"] = node
doc.sections.append(sec)
made += 1
return doc
-106
View File
@@ -1,106 +0,0 @@
"""base.py — the SurfaceProjector interface + registry.
THE key abstraction of the efferent projector: a projector is a pure function
from the surface-neutral, geometry-carrying DocumentIR to bytes on a target
SURFACE. The surface is a PARAMETER. Adding a surface = registering one more
projector; nothing upstream (plan/realize/cohere) changes.
DocumentIR --project--> bytes (per surface)
A TEXT projector reads ``block.sentences``. A NON-TEXT projector (music, image,
video) reads ``block.provenance`` — the geometry the IR carries — and decodes it
onto its surface. Both consume the SAME IR. That symmetry is the whole design:
the realizer generalizes into a multimodal projector, geometry -> any surface.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
@runtime_checkable
class SurfaceProjector(Protocol):
"""Geometry-document -> one surface. Implementations MUST be pure & faithful.
THE ONE SHARED SEAM. Every surface — text, music, image, video — conforms to
this single contract:
project(frame: DocumentIR) -> bytes
where ``frame`` is the geometry-carrying meaning-geometry (the SemFrame at
document scale; a single utterance is the degenerate one-section frame).
RECOMMENDED INTERNAL SHAPE (the peer music/text decomposition, blessed here
so all surfaces share it): a projector may split ``project`` into
spec = self.plan(frame) # meaning-geometry -> surface-specific spec
bytes = self.realize(spec) # spec -> surface, via this projector's PROFILE
``project`` is then ``realize(plan(frame))``. The PROFILE (a text lang-profile,
a music instr/mode-profile, an image layout-profile) is a property of the
projector instance — the pluggable knob. See :class:`TwoStageProjector`.
A TEXT projector's plan reads ``frame`` sentences; a MUSIC/IMAGE projector's
plan reads ``frame.all_provenance()`` — the geometry — and derives its spec
(pitch/harmony/rhythm, or layout) FROM the meaning, deterministically. Same
frame, different profile.
"""
surface: str # "markdown" | "docx" | "midi" | "audio" | "image" | "video"
media_type: str # MIME type of the emitted bytes
ext: str # file extension (no dot)
modality: str # "text" | "audio" | "image" | "video"
profile: object # the pluggable per-surface profile (may be None)
def project(self, doc: DocumentIR) -> bytes:
"""Emit the document on this surface. Returns raw bytes."""
...
class TwoStageProjector:
"""Optional base for the peer plan()/realize() decomposition.
Subclasses implement ``plan(frame) -> spec`` and ``realize(spec) -> bytes``;
``project`` is their composition. This is exactly the peer music interface
(spec = plan(frame, profile); surface = realize(spec, profile)) expressed so
that it still satisfies the single ``SurfaceProjector.project`` seam. Text,
music, and image projectors can all subclass this and remain interchangeable.
"""
surface: str = ""
media_type: str = ""
ext: str = ""
modality: str = ""
profile: object = None
def plan(self, doc: DocumentIR): # -> spec
raise NotImplementedError
def realize(self, spec) -> bytes:
raise NotImplementedError
def project(self, doc: DocumentIR) -> bytes:
return self.realize(self.plan(doc))
_REGISTRY: dict[str, SurfaceProjector] = {}
def register(projector: SurfaceProjector) -> SurfaceProjector:
_REGISTRY[projector.surface] = projector
return projector
def get_projector(surface: str) -> SurfaceProjector:
if surface not in _REGISTRY:
raise KeyError(f"no projector registered for surface {surface!r}; "
f"have {sorted(_REGISTRY)}")
return _REGISTRY[surface]
def available_surfaces() -> list[str]:
return sorted(_REGISTRY)
-113
View File
@@ -1,113 +0,0 @@
"""docx.py — the .docx surface projector: an OWN minimal OOXML emitter.
Own-the-core: a .docx is just a ZIP of a few XML parts (WordprocessingML). We
emit it with the standard library only — ``zipfile`` + string XML — no
python-docx, no external dependency. This proves a "richer structured format"
surface without importing anyone else's toolkit.
Parts emitted (the minimal valid set + a styles part for real headings):
[Content_Types].xml
_rels/.rels
word/_rels/document.xml.rels
word/styles.xml (Title / Heading1 / Heading2 / Normal)
word/document.xml (the content)
Like the markdown projector it reads only the IR's realized sentences; it
invents nothing. The surface differs, the faithful content does not.
"""
from __future__ import annotations
import io
import os
import sys
import zipfile
from xml.sax.saxutils import escape
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
from projectors.base import register # noqa: E402
_CONTENT_TYPES = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
</Types>"""
_RELS = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"""
_DOC_RELS = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>"""
_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
_STYLES = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="{_W}">
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/>
<w:rPr><w:sz w:val="22"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/>
<w:pPr><w:spacing w:after="240"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="52"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Subtitle"><w:name w:val="Subtitle"/>
<w:rPr><w:i/><w:sz w:val="28"/><w:color w:val="555555"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/>
<w:pPr><w:spacing w:before="240" w:after="120"/><w:outlineLvl w:val="0"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="34"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/>
<w:pPr><w:spacing w:before="200" w:after="100"/><w:outlineLvl w:val="1"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="28"/></w:rPr></w:style>
</w:styles>"""
def _para(text: str, style: str | None = None) -> str:
ppr = f"<w:pPr><w:pStyle w:val=\"{style}\"/></w:pPr>" if style else ""
return (f"<w:p>{ppr}<w:r><w:t xml:space=\"preserve\">"
f"{escape(text)}</w:t></w:r></w:p>")
class DocxProjector:
surface = "docx"
media_type = ("application/vnd.openxmlformats-officedocument."
"wordprocessingml.document")
ext = "docx"
modality = "text"
def _document_xml(self, doc: DocumentIR) -> str:
body: list[str] = [_para(doc.title, "Title")]
if doc.subtitle:
body.append(_para(doc.subtitle, "Subtitle"))
abstract = doc.meta.get("abstract")
if abstract is not None and abstract.sentences:
body.append(_para(abstract.text()))
for sec in doc.sections:
style = "Heading1" if sec.level <= 1 else "Heading2"
body.append(_para(sec.heading, style))
for block in sec.blocks:
t = block.text()
if t:
body.append(_para(t))
return (f"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
f"<w:document xmlns:w=\"{_W}\"><w:body>"
+ "".join(body)
+ "<w:sectPr><w:pgSz w:w=\"12240\" w:h=\"15840\"/>"
"<w:pgMar w:top=\"1440\" w:right=\"1440\" w:bottom=\"1440\" "
"w:left=\"1440\"/></w:sectPr></w:body></w:document>")
def project(self, doc: DocumentIR) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("[Content_Types].xml", _CONTENT_TYPES)
z.writestr("_rels/.rels", _RELS)
z.writestr("word/_rels/document.xml.rels", _DOC_RELS)
z.writestr("word/styles.xml", _STYLES)
z.writestr("word/document.xml", self._document_xml(doc))
return buf.getvalue()
register(DocxProjector())
-45
View File
@@ -1,45 +0,0 @@
"""markdown.py — the Markdown surface projector (text facet).
The most tractable surface, and the reference implementation: reads the IR's
realized sentences and lays them out as Markdown. Introduces no content — it is
pure typography over the faithful text the realizer produced.
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
from projectors.base import register # noqa: E402
class MarkdownProjector:
surface = "markdown"
media_type = "text/markdown"
ext = "md"
modality = "text"
def render_str(self, doc: DocumentIR) -> str:
lines: list[str] = [f"# {doc.title}"]
if doc.subtitle:
lines.append(f"\n*{doc.subtitle}*")
abstract = doc.meta.get("abstract")
if abstract is not None and abstract.sentences:
lines.append("")
lines.append(abstract.text())
for sec in doc.sections:
lines.append("")
lines.append(f"{'#' * max(2, sec.level)} {sec.heading}")
for block in sec.blocks:
body = block.text()
if body:
lines.append("")
lines.append(body)
return "\n".join(lines) + "\n"
def project(self, doc: DocumentIR) -> bytes:
return self.render_str(doc).encode("utf-8")
register(MarkdownProjector())
-133
View File
@@ -1,133 +0,0 @@
"""midi.py — the MUSIC surface projector: geometry -> symbolic music (MIDI).
The first NON-TEXT surface, and the proof of the general shape. "Music is
language and it is math" (Will): symbolic music is tractable and geometry-native,
so it is the natural efferent twin to try first after text.
CRUCIALLY this projector does NOT read the realized sentences. It reads the IR's
GEOMETRY facet — ``block.provenance`` — and DECODES each edge onto a musical
surface. That is the whole thesis of the multimodal projector: the same
geometry-carrying IR drives text AND music; a text projector reads the words, a
music projector reads the meaning-geometry. The mapping is deterministic and
faithful to the geometry's structure:
relation lemma -> scale degree (same relation -> same pitch class;
meaning has a consistent sonic form)
polarity -> mode (aff = major third above; neg = minor
third / lowered — SACRED polarity is
audible, a negated edge sounds negated)
confidence -> note duration (stronger grounding rings longer)
importance -> velocity (more important source = louder)
section -> phrase + register shift (structure becomes musical form)
Own-the-core: a Standard MIDI File is a header chunk + a track chunk of
delta-timed events. We emit the raw bytes with ``struct`` — no external MIDI
library. Format 0, one track.
"""
from __future__ import annotations
import io
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR, Provenance # noqa: E402
from projectors.base import TwoStageProjector, register # noqa: E402
_TICKS = 480 # ticks per quarter note
_C_MAJOR = [0, 2, 4, 5, 7, 9, 11] # semitone offsets of a diatonic scale
def _vlq(n: int) -> bytes:
"""MIDI variable-length quantity encoding of a delta time."""
if n == 0:
return b"\x00"
out = bytearray()
out.append(n & 0x7F)
n >>= 7
while n:
out.insert(0, (n & 0x7F) | 0x80)
n >>= 7
return bytes(out)
def _degree_for(relation: str) -> int:
"""Stable scale degree for a relation lemma (same relation -> same pitch)."""
if not relation:
return 0
return sum(ord(c) for c in relation.lower()) % len(_C_MAJOR)
def _note_for(p: Provenance, base: int) -> tuple[int, int, int]:
"""(pitch, velocity, duration_ticks) for one geometry edge."""
root = base + _C_MAJOR[_degree_for(p.relation)]
# polarity -> mode: affirmed edges take the bright major third, negated edges
# take the darker minor third. The negation is AUDIBLE and never dropped.
third = 4 if p.polarity == "aff" else 3
pitch = max(24, min(96, root + (third if p.confidence >= 0.5 else 0)))
velocity = int(56 + 60 * min(1.0, max(0.0, p.importance)))
velocity = max(40, min(120, velocity))
# confidence -> duration: quarter .. dotted-half
dur = int(_TICKS * (0.5 + 1.5 * min(1.0, max(0.0, p.confidence))))
return pitch, velocity, dur
# a mode-profile: the pluggable musical knob (the peer's mode_profile). Scale +
# tempo. Swapping this profile re-voices the SAME geometry — surface as parameter.
_DEFAULT_PROFILE = {"scale": _C_MAJOR, "tempo_us": 500000,
"registers": [60, 55, 64, 50, 67, 48], "program": 0}
class MidiProjector(TwoStageProjector):
"""geometry -> symbolic music, in the shared two-stage shape.
``plan(frame)`` -> a music_spec: an ordered list of note dicts derived
deterministically from the frame's provenance geometry
(the peer's ``plan(frame, profile) -> spec``).
``realize(spec)`` -> Standard MIDI File bytes (the peer's
``realize(spec, profile) -> surface``; here the surface
is symbolic MIDI, the minimal audio proof — a richer
additive-synth audio projector conforms identically).
"""
surface = "midi"
media_type = "audio/midi"
ext = "mid"
modality = "audio"
def __init__(self, profile: dict | None = None):
self.profile = profile or _DEFAULT_PROFILE
# -- stage 1: meaning-geometry -> music_spec (reads the GEOMETRY facet) -- #
def plan(self, doc: DocumentIR) -> list[dict]:
registers = self.profile["registers"]
spec: list[dict] = []
for si, sec in enumerate(doc.sections):
base = registers[si % len(registers)]
provs = [p for p in sec.all_provenance()
if p.kind in ("fact", "interpretation")]
for i, p in enumerate(provs):
pitch, vel, dur = _note_for(p, base)
spec.append({"pitch": pitch, "velocity": vel, "dur": dur,
"rest_before": (_TICKS // 2) if (si > 0 and i == 0) else 0,
"relation": p.relation, "polarity": p.polarity})
return spec
# -- stage 2: music_spec -> MIDI bytes (own-core, no library) ------------ #
def realize(self, spec: list[dict]) -> bytes:
ev = bytearray()
ev += _vlq(0) + b"\xFF\x51\x03" + struct.pack(">I", self.profile["tempo_us"])[1:]
ev += _vlq(0) + bytes([0xC0, self.profile["program"] & 0x7F])
for note in spec:
ev += _vlq(note["rest_before"]) + bytes([0x90, note["pitch"], note["velocity"]])
ev += _vlq(note["dur"]) + bytes([0x80, note["pitch"], 0])
ev += _vlq(0) + b"\xFF\x2F\x00"
track = bytes(ev)
buf = io.BytesIO()
buf.write(b"MThd" + struct.pack(">IHHH", 6, 0, 1, _TICKS))
buf.write(b"MTrk" + struct.pack(">I", len(track)) + track)
return buf.getvalue()
register(MidiProjector())
-60
View File
@@ -1,60 +0,0 @@
"""seams.py — documented efferent seams for IMAGE and VIDEO surfaces.
These are NOT implemented (per the build rails: architect, do not overbuild).
They are registered as first-class seams so the interface PROVES it accepts
future non-text projectors without any upstream change. Each documents exactly
what its decoder would read from the geometry-carrying IR, making the multimodal
generalization concrete rather than hand-wavy.
The symmetry that guarantees these are possible, not moonshots: they are the
efferent twins of multimodal INGEST. If meaning can HOLD an image (ingest as
first-class geometry), meaning can PROJECT one back. Video = image x sound x
TIME, and the engram already stores time (chronoception). So video falls out of
an image projector + the music projector + the stored temporal ordering.
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
from projectors.base import register # noqa: E402
class _Seam:
"""A registered-but-unimplemented projector. Names its decoder contract."""
def project(self, doc: DocumentIR) -> bytes: # pragma: no cover - seam
raise NotImplementedError(
f"{self.surface!r} projector is a documented seam, not yet built. "
f"Decoder contract: {self.decoder_contract}")
class ImageProjector(_Seam):
surface = "image"
media_type = "image/png"
ext = "png"
modality = "image"
decoder_contract = (
"reads block.provenance as a spatial layout — nodes become regions, edges "
"become adjacencies; salience/importance drive size/contrast; polarity "
"drives figure/ground. The efferent twin of image ingest (a geometry->raster "
"decoder, learned or engineered), exactly mirroring the embedder that turned "
"the image INTO geometry.")
class VideoProjector(_Seam):
surface = "video"
media_type = "video/mp4"
ext = "mp4"
modality = "video"
decoder_contract = (
"image x sound x TIME. Composes the image projector (per-keyframe geometry "
"layout) with the midi/music projector (score) along the geometry's stored "
"temporal ordering (chronoception). Needs no new principle once image + music "
"exist — only a muxer.")
register(ImageProjector())
register(VideoProjector())
-63
View File
@@ -1,63 +0,0 @@
"""provenance.py — the faithfulness audit + geometry->section trace.
A document projected from geometry is only worth anything if every claim traces
back. This module walks the DocumentIR and proves the discipline held:
* ZERO ungrounded claims (every fact/interpretation has a real node id),
* every emitted sentence maps to a geometry edge (or is a marked connective),
* SACRED polarity survived (negations are reported, never silently dropped),
* COHERE introduced no new geometry (connectives carry no claim).
It emits both a machine verdict and a human-readable geometry->section table.
"""
from __future__ import annotations
from document_ir import DocumentIR
def audit(doc: DocumentIR) -> dict:
provs = doc.all_provenance()
facts = [p for p in provs if p.kind in ("fact", "interpretation")]
connectives = [p for p in provs if p.kind == "connective"]
ungrounded = [p for p in facts if not p.node_id]
negations = [p for p in facts if p.polarity == "neg"]
node_ids = sorted({p.node_id for p in facts if p.node_id})
return {
"claims": len(facts),
"connectives": len(connectives),
"ungrounded_claims": len(ungrounded),
"negations_preserved": len(negations),
"distinct_source_nodes": len(node_ids),
"faithful": len(ungrounded) == 0,
"source_nodes": node_ids,
}
def trace_table(doc: DocumentIR) -> str:
"""Human-readable geometry -> section -> claim provenance table."""
lines = ["# Provenance — every claim traces geometry", ""]
lines.append(f"**Document:** {doc.title}")
a = audit(doc)
lines.append(f"**Claims:** {a['claims']} · **Ungrounded:** "
f"{a['ungrounded_claims']} · **Negations preserved:** "
f"{a['negations_preserved']} · **Source nodes:** "
f"{a['distinct_source_nodes']} · **Faithful:** "
f"{'YES' if a['faithful'] else 'NO'}")
lines.append("")
for si, sec in enumerate(doc.sections, 1):
lines.append(f"## {si}. {sec.heading}")
lines.append(f"_seed nodes: {', '.join(i[:8] for i in sec.seed_ids)}_")
lines.append("")
lines.append("| # | realized claim | traces geometry edge |")
lines.append("|---|----------------|----------------------|")
n = 0
for block in sec.blocks:
for sent, prov in zip(block.sentences, block.provenance):
if prov.kind == "connective":
continue
n += 1
edge = prov.trace().replace("|", "\\|")
s = sent.replace("|", "\\|")
lines.append(f"| {n} | {s} | {edge} |")
lines.append("")
return "\n".join(lines) + "\n"
-112
View File
@@ -1,112 +0,0 @@
"""realize.py — REALIZE stage: fill each planned section with faithful passages.
Scales the PROVEN realizer from a single assertion to a passage. For each
planned proposition we build a realizer-ready clause (the proven
``_prop_to_clause`` mapping) and run it through the proven engine
(``engine.realize``), which is a deterministic grammar with the SACRED negation
contract — it never invents. Each realized sentence is paired with a
:class:`Provenance` that pins it to the exact geometry edge it came from.
"Passage, not a list of sentences": within a section we lightly vary sentence
openings and group related claims, but we add NO content the geometry did not
assert. The only non-geometry words are function words the grammar already owns
(articles, "and", conjunction of same-subject claims). Document-level flow is
COHERE's job; this stage owns intra-section fluency + fidelity.
"""
from __future__ import annotations
import os
import sys
_NT = os.path.expanduser("~/Desktop/neuron-talk")
_LR = os.path.expanduser("~/Desktop/lang-realizers")
for _p in (_NT, _LR):
if _p not in sys.path:
sys.path.insert(0, _p)
import engine # noqa: E402 (the proven no-LLM realizer)
from dialogue import _prop_to_clause # noqa: E402 (proven prop -> clause)
from document_ir import Block, DocumentIR, Provenance, Section # noqa: E402
def _provenance_from(p, kind: str = "fact") -> Provenance:
return Provenance(
subj_id=p.source_node_id, subject=p.subject, relation=p.predicate,
obj=p.object, polarity=p.polarity, confidence=round(float(p.confidence), 3),
node_id=p.source_node_id, kind=kind,
importance=float(getattr(p, "node_importance", 0.0) or 0.0),
salience=0.0,
)
import re as _re
# a well-formed declarative opens with a determiner, a proper noun, "I", or a
# capitalized head — not a mis-parsed object pronoun or a copula fragment.
_BAD_OPENERS = _re.compile(r"^(Me |It is I|There is|This is it|That is it)\b")
_VACUOUS = _re.compile(r"^\w+ (is|are|was|were) (it|no|nothing|empty|those|this|that)\.?$",
_re.I)
def _good_sentence(text: str) -> bool:
"""Fluency gate — drops degenerate realizations. NEVER loosens faithfulness;
it only refuses to SPEAK a claim whose surface came out malformed."""
words = text.rstrip(".").split()
if len(words) < 3:
return False
if _BAD_OPENERS.search(text):
return False
if _VACUOUS.match(text):
return False
# a sentence that is mostly one-letter/two-letter tokens is a parse artifact
short = sum(1 for w in words if len(w.strip(".,'")) <= 2)
if short > len(words) / 2:
return False
return True
def _realize_prop(p, lang: str = "en") -> tuple[str, Provenance] | None:
"""One proposition -> (faithful sentence, provenance) or None if it drops."""
clause = _prop_to_clause(p)
text = engine.realize(clause, lang)
if not text or not text.strip():
return None
text = text.strip()
if not text.endswith((".", "!", "?")):
text += "."
# capitalize first character (proper nouns / "I" already handled by grammar)
text = text[0].upper() + text[1:]
if not _good_sentence(text):
return None
return text, _provenance_from(p)
def realize_document(doc: DocumentIR, lang: str = "en") -> DocumentIR:
"""Fill every planned section's blocks with faithful, realized passages."""
for sec in doc.sections:
planned = sec.__dict__.get("_planned_props", [])
block = Block(role="body")
summary_bits: list[str] = []
for p in planned:
r = _realize_prop(p, lang)
if r is None:
continue
text, prov = r
block.sentences.append(text)
block.provenance.append(prov)
if len(summary_bits) < 1:
# a short grounded gloss for TOC / pptx bullets
obj = (prov.obj or "").strip().rstrip(".")
if obj:
summary_bits.append(obj)
if block.sentences:
sec.blocks.append(block)
sec.summary = summary_bits[0] if summary_bits else ""
# drop the transient planning payload; the IR is now self-contained
sec.__dict__.pop("_planned_props", None)
sec.__dict__.pop("_node", None)
# prune sections that realized to nothing
doc.sections = [s for s in doc.sections if s.blocks]
return doc
-73
View File
@@ -1,73 +0,0 @@
// audio-demo.el - Drive the native audio surface: render a tone per instrument
// from its LEARNED signature, then render a small meaning-phrase "piece".
// Entry point: top-level statement calls main() (same convention as the
// examples' top-level println(run_test())).
fn micros_to_str(xs: [Int]) -> String {
let n: Int = native_list_len(xs)
let out: String = ""
let i: Int = 0
while i < n {
if i > 0 { let out: String = out + "," }
let out: String = out + int_to_str(native_list_get(xs, i))
let i: Int = i + 1
}
return out
}
// Render a 1.0s A4 (midi 69) tone from a signature file, print the parsed
// partials (proving the numbers came from the engram .sig), write the WAV.
fn render_tone(name: String, sigpath: String, outpath: String, table: [Int]) -> Int {
let lines: [String] = sig_load(sigpath)
let partials: [Int] = parse_micros(sig_field(lines, "partials"))
println("[" + name + "] partials_n=" + sig_field(lines, "partials_n") + " parsed_partials_micro(scale 1e6)=" + micros_to_str(partials))
println("[" + name + "] raw partials line from .sig = " + sig_field(lines, "partials"))
let freq: Int = freq_of_midi(69)
let note: [Int] = synth_from_sig(lines, freq, 1000, 900, 44100, table)
let n: Int = native_list_len(note)
let ok: Int = wav_write(outpath, note, n, 44100)
println("[" + name + "] rendered " + int_to_str(n) + " samples -> " + outpath + " (write_ok=" + int_to_str(ok) + ")")
return n
}
fn run_demo() -> Int {
let table: [Int] = sin_table()
fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out")
println("=== TONES: render A4 (midi 69) from each learned signature ===")
render_tone("flute", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/flute.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-flute.wav", table)
render_tone("clarinet", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/clarinet.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-clarinet.wav", table)
render_tone("violin", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/violin.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-violin.wav", table)
render_tone("piano", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-piano.wav", table)
render_tone("organ", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/organ.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-organ.wav", table)
println("")
println("=== PIECE: a 6-frame meaning phrase (incl. a NEG frame) ===")
let frames: [[String]] = native_list_empty()
let frames: [[String]] = native_list_append(frames, audio_frame("agent", "aff", "0.9", "0.8", "0", "s1"))
let frames: [[String]] = native_list_append(frames, audio_frame("theme", "aff", "0.7", "0.6", "0", "s2"))
let frames: [[String]] = native_list_append(frames, audio_frame("cause", "aff", "0.8", "0.9", "1", "s3"))
let frames: [[String]] = native_list_append(frames, audio_frame("negation", "neg", "0.85", "0.7", "0", "s4"))
let frames: [[String]] = native_list_append(frames, audio_frame("goal", "aff", "0.6", "0.5", "1", "s5"))
let frames: [[String]] = native_list_append(frames, audio_frame("result", "aff", "0.95", "1.0", "0", "s6"))
// Print the plan so the NEG frame's minor third (+3) vs major (+4) is visible.
let nf: Int = native_list_len(frames)
let fi: Int = 0
while fi < nf {
let frame: [String] = native_list_get(frames, fi)
let plan: [Int] = plan_note(frame)
let pol: String = surface_get(frame, "polarity")
let third_name: String = "major(+4)"
if str_eq(pol, "neg") { let third_name: String = "MINOR(+3)" }
println("frame " + int_to_str(fi) + " relation=" + surface_get(frame, "relation") + " polarity=" + pol + " -> midi=" + int_to_str(native_list_get(plan, 0)) + " dur_ms=" + int_to_str(native_list_get(plan, 1)) + " amp_pm=" + int_to_str(native_list_get(plan, 2)) + " third=" + third_name)
let fi: Int = fi + 1
}
let piano_lines: [String] = sig_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig")
let total: Int = realize_audio(frames, piano_lines, "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav", 44100, table)
println("PIECE rendered " + int_to_str(total) + " samples -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav")
return total
}
println("audio-demo main returned samples=" + int_to_str(run_demo()))
-400
View File
@@ -1,400 +0,0 @@
// audio-surface.el - Native own-core additive-synthesis audio surface.
//
// The AUDIO efferent seam, native, no Python and no library. This renders real
// PCM .wav bytes from instrument SIGNATURES read from engram-sourced .sig data
// files (elp/faculty/sig/*.sig) - the partial amplitudes are NEVER literals in
// this source; they are parsed from the learned signature at run time. That is
// the whole proof: render-from-learned-signatures.
//
// EL has no float arithmetic operator (codegen emits raw int64 ops for + - * /
// on the shared 64-bit slot) and no float-arithmetic natives - so ALL synthesis
// math here is own-core INTEGER fixed-point. Angles use a quarter-wave sine
// table (scale 10000) from a fixed-point Taylor series; amplitudes are parsed to
// micro (scale 1e6) straight from the .sig text; frequencies are milliHz ints.
//
// Pipeline mirrors the two-stage projector (midi.py): plan_note(frame) reads a
// frame's meaning-geometry slot-map and derives (pitch, duration, amplitude);
// realize_audio SUPERPOSES the signature's partials (the compose op) and
// serialises RIFF/WAVE. Same frame -> midi OR audio.
// -- integer decimal + string helpers -----------------------------------------
fn str_to_int_el(s: String) -> Int {
let n: Int = str_len(s)
let i: Int = 0
let v: Int = 0
let neg: Bool = false
while i < n {
let c: Int = str_char_code(s, i)
if c == 45 { let neg: Bool = true }
if c >= 48 {
if c < 58 {
let v: Int = v * 10 + (c - 48)
}
}
let i: Int = i + 1
}
if neg { return 0 - v }
return v
}
fn parse_micro(s: String) -> Int {
let dot: Int = str_index_of(s, ".")
if dot < 0 {
return str_to_int_el(s) * 1000000
}
let n: Int = str_len(s)
let ipart: String = str_slice(s, 0, dot)
let fpart: String = str_slice(s, dot + 1, n)
let iv: Int = str_to_int_el(ipart)
let fv: Int = 0
let scale: Int = 100000
let fn2: Int = str_len(fpart)
let i: Int = 0
while i < 6 {
let d: Int = 0
if i < fn2 {
let d: Int = str_char_code(fpart, i) - 48
}
let fv: Int = fv + d * scale
let scale: Int = scale / 10
let i: Int = i + 1
}
return iv * 1000000 + fv
}
// -- signature (engram data file) loader ---------------------------------------
fn sig_load(path: String) -> [String] {
let text: String = fs_read(path)
return str_split(text, "\n")
}
fn sig_field(lines: [String], key: String) -> String {
let pref: String = key + ": "
let n: Int = native_list_len(lines)
let plen: Int = str_len(pref)
let i: Int = 0
while i < n {
let ln: String = native_list_get(lines, i)
if str_starts_with(ln, pref) {
return str_slice(ln, plen, str_len(ln))
}
let i: Int = i + 1
}
return ""
}
fn parse_micros(csv: String) -> [Int] {
let parts: [String] = str_split(csv, ",")
let n: Int = native_list_len(parts)
let out: [Int] = native_list_empty()
let i: Int = 0
while i < n {
let out: [Int] = native_list_append(out, parse_micro(native_list_get(parts, i)))
let i: Int = i + 1
}
return out
}
// -- fixed-point sine (own-core, quarter-wave Taylor table, scale 10000) --------
fn sin_table() -> [Int] {
let HP: Int = 1570796
let t: [Int] = native_list_empty()
let q: Int = 0
while q < 257 {
let x: Int = q * HP / 256
let x2: Int = x * x / 1000000
let x3: Int = x2 * x / 1000000
let x5: Int = x3 * x2 / 1000000
let x7: Int = x5 * x2 / 1000000
let x9: Int = x7 * x2 / 1000000
let s: Int = x - x3 / 6 + x5 / 120 - x7 / 5040 + x9 / 362880
let t: [Int] = native_list_append(t, s / 100)
let q: Int = q + 1
}
return t
}
fn sin_lookup(t: [Int], phase: Int) -> Int {
let p: Int = phase % 1024
if p < 0 { let p: Int = p + 1024 }
let quad: Int = p / 256
let r: Int = p % 256
if quad == 0 { return native_list_get(t, r) }
if quad == 1 { return native_list_get(t, 256 - r) }
if quad == 2 { return 0 - native_list_get(t, r) }
return 0 - native_list_get(t, 256 - r)
}
fn isqrt_int(n: Int) -> Int {
if n <= 0 { return 0 }
let x: Int = n
let y: Int = (x + 1) / 2
while y < x {
let x: Int = y
let y: Int = (x + n / x) / 2
}
return x
}
// freq_of_midi: equal-tempered frequency in milliHz. 440000 mHz at midi 69.
fn freq_of_midi(m: Int) -> Int {
let f: Int = 440000
if m > 69 {
let k: Int = m - 69
let i: Int = 0
while i < k {
let f: Int = f * 1059463 / 1000000
let i: Int = i + 1
}
return f
}
if m < 69 {
let k: Int = 69 - m
let i: Int = 0
while i < k {
let f: Int = f * 1000000 / 1059463
let i: Int = i + 1
}
return f
}
return f
}
// -- envelope (ADSR), scale 1000 -----------------------------------------------
fn adsr_env(i: Int, total: Int, atk_n: Int, dec_n: Int, sus_pm: Int, rel_n: Int) -> Int {
if i < atk_n {
if atk_n == 0 { return 1000 }
return 1000 * i / atk_n
}
if i < atk_n + dec_n {
if dec_n == 0 { return sus_pm }
return 1000 - (1000 - sus_pm) * (i - atk_n) / dec_n
}
let rel_start: Int = total - rel_n
if i < rel_start {
return sus_pm
}
if rel_n == 0 { return 0 }
let left: Int = total - i
return sus_pm * left / rel_n
}
// -- note synthesis: SUPERPOSE the learned partials -> [Int] samples -----------
fn note_samples(freq_mHz: Int, dur_ms: Int, rate: Int, partials: [Int], sumP: Int, b_micro: Int, vib_rate: Int, vib_cents: Int, atk_ms: Int, dec_ms: Int, sus_pm: Int, rel_ms: Int, amp_pm: Int, table: [Int]) -> [Int] {
let total: Int = dur_ms * rate / 1000
let atk_n: Int = atk_ms * rate / 1000
let dec_n: Int = dec_ms * rate / 1000
let rel_n: Int = rel_ms * rate / 1000
let np: Int = native_list_len(partials)
let half_mhz: Int = rate * 1000 / 2
let out: [Int] = native_list_empty()
let i: Int = 0
while i < total {
let acc: Int = 0
let k: Int = 0
while k < np {
let harm: Int = k + 1
let amp_k: Int = native_list_get(partials, k)
let factor: Int = 1000000
if b_micro > 0 {
let val: Int = 1000000 + b_micro * harm * harm
let factor: Int = isqrt_int(val * 1000000)
}
let fn_mhz: Int = freq_mHz * harm
let fn_mhz: Int = fn_mhz * factor / 1000000
if vib_cents > 0 {
if vib_rate > 0 {
let vphase: Int = i * vib_rate * 1024 / rate
let vs: Int = sin_lookup(table, vphase)
let vibf: Int = 1000000 + (vib_cents * vs * 833) / 10000
let fn_mhz: Int = fn_mhz * vibf / 1000000
}
}
if fn_mhz <= half_mhz {
let phase: Int = i * fn_mhz * 1024 / (rate * 1000)
let sv: Int = sin_lookup(table, phase)
let acc: Int = acc + sv * amp_k / 1000000
}
let k: Int = k + 1
}
let env: Int = adsr_env(i, total, atk_n, dec_n, sus_pm, rel_n)
let s16: Int = acc * 2800000 / sumP
let s16: Int = s16 * env / 1000
let s16: Int = s16 * amp_pm / 1000
if s16 > 32767 { let s16: Int = 32767 }
if s16 < 0 - 32767 { let s16: Int = 0 - 32767 }
let out: [Int] = native_list_append(out, s16)
let i: Int = i + 1
}
return out
}
fn synth_from_sig(lines: [String], freq_mHz: Int, dur_ms: Int, amp_pm: Int, rate: Int, table: [Int]) -> [Int] {
let partials: [Int] = parse_micros(sig_field(lines, "partials"))
let np: Int = native_list_len(partials)
let sumP: Int = 0
let j: Int = 0
while j < np {
let pj: Int = native_list_get(partials, j)
let sumP: Int = sumP + pj
let j: Int = j + 1
}
if sumP <= 0 { let sumP: Int = 1000000 }
let adsr: [String] = str_split(sig_field(lines, "adsr"), ",")
let atk_ms: Int = parse_micro(native_list_get(adsr, 0)) / 1000
let dec_ms: Int = parse_micro(native_list_get(adsr, 1)) / 1000
let sus_pm: Int = parse_micro(native_list_get(adsr, 2)) / 1000
let rel_ms: Int = parse_micro(native_list_get(adsr, 3)) / 1000
let b_micro: Int = parse_micro(sig_field(lines, "inharmonicity_B"))
let vib_rate: Int = str_to_int_el(sig_field(lines, "vibrato_rate_hz"))
let vib_cents: Int = str_to_int_el(sig_field(lines, "vibrato_depth_cents"))
return note_samples(freq_mHz, dur_ms, rate, partials, sumP, b_micro, vib_rate, vib_cents, atk_ms, dec_ms, sus_pm, rel_ms, amp_pm, table)
}
// -- byte-buffer helpers (own-core, no library) --------------------------------
fn put_tag(buf: String, pos: Int, s: String) -> String {
let n: Int = str_len(s)
let i: Int = 0
while i < n {
let buf: String = __str_set_char(buf, pos + i, str_char_code(s, i))
let i: Int = i + 1
}
return buf
}
fn put_u32le(buf: String, pos: Int, v: Int) -> String {
let buf: String = __str_set_char(buf, pos, v % 256)
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
let buf: String = __str_set_char(buf, pos + 2, (v / 65536) % 256)
let buf: String = __str_set_char(buf, pos + 3, (v / 16777216) % 256)
return buf
}
fn put_u16le(buf: String, pos: Int, v: Int) -> String {
let buf: String = __str_set_char(buf, pos, v % 256)
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
return buf
}
// -- WAV serializer: own-core RIFF/WAVE, PCM mono 16-bit -----------------------
fn wav_write(path: String, samples: [Int], n: Int, rate: Int) -> Int {
let data_len: Int = n * 2
let total: Int = 44 + data_len
let buf: String = __str_alloc(total)
let buf: String = put_tag(buf, 0, "RIFF")
let buf: String = put_u32le(buf, 4, 36 + data_len)
let buf: String = put_tag(buf, 8, "WAVE")
let buf: String = put_tag(buf, 12, "fmt ")
let buf: String = put_u32le(buf, 16, 16)
let buf: String = put_u16le(buf, 20, 1)
let buf: String = put_u16le(buf, 22, 1)
let buf: String = put_u32le(buf, 24, rate)
let buf: String = put_u32le(buf, 28, rate * 2)
let buf: String = put_u16le(buf, 32, 2)
let buf: String = put_u16le(buf, 34, 16)
let buf: String = put_tag(buf, 36, "data")
let buf: String = put_u32le(buf, 40, data_len)
let i: Int = 0
while i < n {
let v: Int = native_list_get(samples, i)
if v < 0 { let v: Int = v + 65536 }
let buf: String = __str_set_char(buf, 44 + i * 2, v % 256)
let buf: String = __str_set_char(buf, 44 + i * 2 + 1, (v / 256) % 256)
let i: Int = i + 1
}
let ok: Int = fs_write_bytes(path, buf, total)
return ok
}
// -- plan: frame slot-map -> note atom (pitch, duration, amplitude) ------------
fn audio_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] {
let f: [String] = native_list_empty()
let f: [String] = native_list_append(f, "relation")
let f: [String] = native_list_append(f, relation)
let f: [String] = native_list_append(f, "polarity")
let f: [String] = native_list_append(f, polarity)
let f: [String] = native_list_append(f, "confidence")
let f: [String] = native_list_append(f, confidence)
let f: [String] = native_list_append(f, "importance")
let f: [String] = native_list_append(f, importance)
let f: [String] = native_list_append(f, "salience")
let f: [String] = native_list_append(f, salience)
let f: [String] = native_list_append(f, "subj_id")
let f: [String] = native_list_append(f, subj_id)
return f
}
fn degree_offset(deg: Int) -> Int {
if deg == 0 { return 0 }
if deg == 1 { return 2 }
if deg == 2 { return 4 }
if deg == 3 { return 5 }
if deg == 4 { return 7 }
if deg == 5 { return 9 }
return 11
}
// returns [midi, dur_ms, amp_pm]
fn plan_note(frame: [String]) -> [Int] {
let relation: String = surface_get(frame, "relation")
let polarity: String = surface_get(frame, "polarity")
let confidence: String = surface_get(frame, "confidence")
let importance: String = surface_get(frame, "importance")
let salience: String = surface_get(frame, "salience")
let rn: Int = str_len(relation)
let csum: Int = 0
let i: Int = 0
while i < rn {
let cc: Int = str_char_code(relation, i)
let csum: Int = csum + cc
let i: Int = i + 1
}
let deg: Int = csum % 7
let third: Int = 4
if str_eq(polarity, "neg") { let third: Int = 3 }
let sal_oct: Int = str_to_int_el(salience)
let doff: Int = degree_offset(deg)
let midi: Int = 60 + sal_oct * 12 + doff + third
let conf_micro: Int = parse_micro(confidence)
let dur_ms: Int = 200 + conf_micro / 1000
let imp_micro: Int = parse_micro(importance)
let amp_pm: Int = 400 + imp_micro / 2000
let out: [Int] = native_list_empty()
let out: [Int] = native_list_append(out, midi)
let out: [Int] = native_list_append(out, dur_ms)
let out: [Int] = native_list_append(out, amp_pm)
return out
}
fn realize_audio(frames: [[String]], sig_lines: [String], path: String, rate: Int, table: [Int]) -> Int {
let nf: Int = native_list_len(frames)
let all: [Int] = native_list_empty()
let count: Int = 0
let fi: Int = 0
while fi < nf {
let frame: [String] = native_list_get(frames, fi)
let plan: [Int] = plan_note(frame)
let midi: Int = native_list_get(plan, 0)
let dur_ms: Int = native_list_get(plan, 1)
let amp_pm: Int = native_list_get(plan, 2)
let freq: Int = freq_of_midi(midi)
let note: [Int] = synth_from_sig(sig_lines, freq, dur_ms, amp_pm, rate, table)
let nn: Int = native_list_len(note)
let j: Int = 0
while j < nn {
let all: [Int] = native_list_append(all, native_list_get(note, j))
let j: Int = j + 1
}
let count: Int = count + nn
let fi: Int = fi + 1
}
let ok: Int = wav_write(path, all, count, rate)
return count
}
-65
View File
@@ -1,65 +0,0 @@
// image-demo.el - Drive the native PNG surface: plan a scene from a small
// meaning phrase (incl. a NEG frame) and emit a byte-valid 64x64 PNG whose
// palette is read from elp/faculty/sig/scene.basis.
fn img_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] {
let f: [String] = native_list_empty()
let f: [String] = native_list_append(f, "relation")
let f: [String] = native_list_append(f, relation)
let f: [String] = native_list_append(f, "polarity")
let f: [String] = native_list_append(f, polarity)
let f: [String] = native_list_append(f, "confidence")
let f: [String] = native_list_append(f, confidence)
let f: [String] = native_list_append(f, "importance")
let f: [String] = native_list_append(f, importance)
let f: [String] = native_list_append(f, "salience")
let f: [String] = native_list_append(f, salience)
let f: [String] = native_list_append(f, "subj_id")
let f: [String] = native_list_append(f, subj_id)
return f
}
fn rgb_str(c: [Int]) -> String {
return int_to_str(native_list_get(c, 0)) + "," + int_to_str(native_list_get(c, 1)) + "," + int_to_str(native_list_get(c, 2))
}
fn run_image() -> Int {
fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out")
let table: [Int] = crc_table()
println("crc_table[1]=" + int_to_str(native_list_get(table, 1)) + " (expect 1996959894 / 0x77073096)")
let basis: [String] = basis_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/scene.basis")
let warm: [Int] = parse_rgb(basis_field(basis, "warm"))
let cool: [Int] = parse_rgb(basis_field(basis, "cool"))
let bg: [Int] = parse_rgb(basis_field(basis, "bg"))
println("basis warm=" + rgb_str(warm) + " cool=" + rgb_str(cool) + " bg=" + rgb_str(bg) + " (read from scene.basis)")
let frames: [[String]] = native_list_empty()
let frames: [[String]] = native_list_append(frames, img_frame("agent", "aff", "0.9", "0.8", "0", "s1"))
let frames: [[String]] = native_list_append(frames, img_frame("theme", "aff", "0.7", "0.6", "1", "s2"))
let frames: [[String]] = native_list_append(frames, img_frame("cause", "aff", "0.8", "0.9", "0", "s3"))
let frames: [[String]] = native_list_append(frames, img_frame("negation", "neg", "0.85", "0.7", "1", "s4"))
let frames: [[String]] = native_list_append(frames, img_frame("goal", "aff", "0.6", "0.5", "0", "s5"))
let frames: [[String]] = native_list_append(frames, img_frame("result", "aff", "0.95", "1.0", "1", "s6"))
let shapes: [[Int]] = plan_scene(frames, warm, cool)
let ns: Int = native_list_len(shapes)
println("planned " + int_to_str(ns) + " shapes:")
let si: Int = 0
while si < ns {
let sh: [Int] = native_list_get(shapes, si)
let pol: String = surface_get(native_list_get(frames, si), "polarity")
println(" shape " + int_to_str(si) + " type=" + int_to_str(native_list_get(sh, 0)) + " x=" + int_to_str(native_list_get(sh, 1)) + " y=" + int_to_str(native_list_get(sh, 2)) + " size=" + int_to_str(native_list_get(sh, 3)) + " rgb=" + int_to_str(native_list_get(sh, 4)) + "," + int_to_str(native_list_get(sh, 5)) + "," + int_to_str(native_list_get(sh, 6)) + " polarity=" + pol)
let si: Int = si + 1
}
let raw: [Int] = rasterize(64, 64, shapes, bg)
println("rasterized raw (filtered scanlines) bytes=" + int_to_str(native_list_len(raw)) + " (expect 12352)")
let png: [Int] = png_build(64, 64, raw, table)
let plen: Int = native_list_len(png)
let ok: Int = png_write("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png", png)
println("PNG bytes=" + int_to_str(plen) + " -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png (write_ok=" + int_to_str(ok) + ")")
return plen
}
println("image-demo returned png_bytes=" + int_to_str(run_image()))
-412
View File
@@ -1,412 +0,0 @@
// image-surface.el - Native own-core raster PNG surface (the image efferent
// twin of audio). Renders a 64x64 RGB scene deterministically from a frame's
// meaning-geometry, then serialises a byte-valid PNG entirely own-core:
// 8-byte magic, IHDR, IDAT (zlib STORED/uncompressed DEFLATE + Adler32), IEND,
// with a per-chunk CRC32 computed via software xor32 (EL has no bitwise ops).
//
// The RGB palette basis is read from elp/faculty/sig/scene.basis (data, not
// literals) - the same read-from-learned discipline as the audio signatures.
// Integer-only throughout; pixels are composed functionally (painter's order)
// so no list mutation is needed.
// -- small int/parse helpers (self-contained) ----------------------------------
fn i_str_to_int(s: String) -> Int {
let n: Int = str_len(s)
let i: Int = 0
let v: Int = 0
while i < n {
let c: Int = str_char_code(s, i)
if c >= 48 {
if c < 58 {
let v: Int = v * 10 + (c - 48)
}
}
let i: Int = i + 1
}
return v
}
fn basis_load(path: String) -> [String] {
return str_split(fs_read(path), "\n")
}
fn basis_field(lines: [String], key: String) -> String {
let pref: String = key + ": "
let n: Int = native_list_len(lines)
let plen: Int = str_len(pref)
let i: Int = 0
while i < n {
let ln: String = native_list_get(lines, i)
if str_starts_with(ln, pref) {
return str_slice(ln, plen, str_len(ln))
}
let i: Int = i + 1
}
return ""
}
fn parse_rgb(csv: String) -> [Int] {
let parts: [String] = str_split(csv, ",")
let out: [Int] = native_list_empty()
let n: Int = native_list_len(parts)
let i: Int = 0
while i < n {
let v: Int = i_str_to_int(native_list_get(parts, i))
let out: [Int] = native_list_append(out, v)
let i: Int = i + 1
}
return out
}
// -- software 32-bit XOR (no bitwise ops in EL) --------------------------------
fn xor32(a: Int, b: Int) -> Int {
let r: Int = 0
let bit: Int = 1
let i: Int = 0
while i < 32 {
let abit: Int = (a / bit) % 2
let bbit: Int = (b / bit) % 2
if abit != bbit {
let add: Int = bit
let r: Int = r + add
}
let bit: Int = bit * 2
let i: Int = i + 1
}
return r
}
// -- CRC32 (table-driven, table built with xor32) ------------------------------
fn crc_table() -> [Int] {
let t: [Int] = native_list_empty()
let n: Int = 0
while n < 256 {
let c: Int = n
let k: Int = 0
while k < 8 {
if c % 2 == 1 {
let h: Int = c / 2
let c: Int = xor32(h, 3988292384)
} else {
let c: Int = c / 2
}
let k: Int = k + 1
}
let t: [Int] = native_list_append(t, c)
let n: Int = n + 1
}
return t
}
fn crc32_of(bytes: [Int], table: [Int]) -> Int {
let crc: Int = 4294967295
let n: Int = native_list_len(bytes)
let i: Int = 0
while i < n {
let b: Int = native_list_get(bytes, i)
let lo: Int = crc % 256
let idx: Int = xor32(lo, b) % 256
let tv: Int = native_list_get(table, idx)
let hi: Int = crc / 256
let crc: Int = xor32(hi, tv)
let i: Int = i + 1
}
return xor32(crc, 4294967295)
}
// -- Adler32 (for the zlib trailer) --------------------------------------------
fn adler32_of(bytes: [Int]) -> Int {
let a: Int = 1
let b: Int = 0
let n: Int = native_list_len(bytes)
let i: Int = 0
while i < n {
let byte: Int = native_list_get(bytes, i)
let a: Int = (a + byte) % 65521
let b: Int = (b + a) % 65521
let i: Int = i + 1
}
return b * 65536 + a
}
// -- byte-list append helpers --------------------------------------------------
fn app_u32be(dst: [Int], v: Int) -> [Int] {
let dst: [Int] = native_list_append(dst, (v / 16777216) % 256)
let dst: [Int] = native_list_append(dst, (v / 65536) % 256)
let dst: [Int] = native_list_append(dst, (v / 256) % 256)
let dst: [Int] = native_list_append(dst, v % 256)
return dst
}
fn app_tag(dst: [Int], s: String) -> [Int] {
let n: Int = str_len(s)
let i: Int = 0
while i < n {
let dst: [Int] = native_list_append(dst, str_char_code(s, i))
let i: Int = i + 1
}
return dst
}
fn app_all(dst: [Int], src: [Int]) -> [Int] {
let n: Int = native_list_len(src)
let i: Int = 0
while i < n {
let dst: [Int] = native_list_append(dst, native_list_get(src, i))
let i: Int = i + 1
}
return dst
}
// -- plan: frame meaning-geometry -> shape atoms -------------------------------
// shape = [type, x, y, size, r, g, b] (type 0=rect 1=disc 2=triangle)
fn charsum(s: String) -> Int {
let n: Int = str_len(s)
let i: Int = 0
let acc: Int = 0
while i < n {
let c: Int = str_char_code(s, i)
let acc: Int = acc + c
let i: Int = i + 1
}
return acc
}
fn micro_of(s: String) -> Int {
let dot: Int = str_index_of(s, ".")
if dot < 0 { return i_str_to_int(s) * 1000000 }
let n: Int = str_len(s)
let fp: String = str_slice(s, dot + 1, n)
let ip: String = str_slice(s, 0, dot)
let iv: Int = i_str_to_int(ip)
let fv: Int = 0
let scale: Int = 100000
let fl: Int = str_len(fp)
let i: Int = 0
while i < 6 {
let d: Int = 0
if i < fl { let d: Int = str_char_code(fp, i) - 48 }
let fv: Int = fv + d * scale
let scale: Int = scale / 10
let i: Int = i + 1
}
return iv * 1000000 + fv
}
fn plan_scene(frames: [[String]], warm: [Int], cool: [Int]) -> [[Int]] {
let shapes: [[Int]] = native_list_empty()
let nf: Int = native_list_len(frames)
let fi: Int = 0
while fi < nf {
let fr: [String] = native_list_get(frames, fi)
let relation: String = surface_get(fr, "relation")
let polarity: String = surface_get(fr, "polarity")
let confidence: String = surface_get(fr, "confidence")
let importance: String = surface_get(fr, "importance")
let salience: String = surface_get(fr, "salience")
// relation -> shape type
let stype: Int = charsum(relation) % 3
// confidence -> size (8..22)
let cmi: Int = micro_of(confidence)
let size: Int = 8 + cmi / 71428
// salience -> y
let sal: Int = i_str_to_int(salience)
let y: Int = 6 + sal * 26
// subj_id/index -> x
let x: Int = 4 + (fi * 10) % 48
// polarity -> warm/cool base color
let br: Int = native_list_get(warm, 0)
let bg2: Int = native_list_get(warm, 1)
let bb: Int = native_list_get(warm, 2)
if str_eq(polarity, "neg") {
let br: Int = native_list_get(cool, 0)
let bg2: Int = native_list_get(cool, 1)
let bb: Int = native_list_get(cool, 2)
}
// importance -> brightness (500..1000 permille)
let imi: Int = micro_of(importance)
let bpm: Int = 500 + imi / 2000
let r: Int = br * bpm / 1000
let g: Int = bg2 * bpm / 1000
let b: Int = bb * bpm / 1000
let sh: [Int] = native_list_empty()
let sh: [Int] = native_list_append(sh, stype)
let sh: [Int] = native_list_append(sh, x)
let sh: [Int] = native_list_append(sh, y)
let sh: [Int] = native_list_append(sh, size)
let sh: [Int] = native_list_append(sh, r)
let sh: [Int] = native_list_append(sh, g)
let sh: [Int] = native_list_append(sh, b)
let shapes: [[Int]] = native_list_append(shapes, sh)
let fi: Int = fi + 1
}
return shapes
}
// covers: is (px,py) inside this shape?
fn covers(sh: [Int], px: Int, py: Int) -> Bool {
let stype: Int = native_list_get(sh, 0)
let sx: Int = native_list_get(sh, 1)
let sy: Int = native_list_get(sh, 2)
let size: Int = native_list_get(sh, 3)
let cx: Int = sx + size / 2
if stype == 0 {
if px >= sx {
if px < sx + size {
if py >= sy {
if py < sy + size {
return true
}
}
}
}
return false
}
if stype == 1 {
let rad: Int = size / 2
let dx: Int = px - cx
let dy: Int = py - (sy + rad)
if dx * dx + dy * dy <= rad * rad {
return true
}
return false
}
// triangle: apex at top (sy), base at sy+size
if py >= sy {
if py < sy + size {
let dyv: Int = py - sy
let halfw: Int = dyv / 2
let dxv: Int = px - cx
let adx: Int = dxv
if adx < 0 { let adx: Int = 0 - dxv }
if adx <= halfw {
return true
}
}
}
return false
}
// pixel_color: painter's algorithm - last covering shape wins. Returns [r,g,b].
fn pixel_color(px: Int, py: Int, shapes: [[Int]], bg: [Int]) -> [Int] {
let r: Int = native_list_get(bg, 0)
let g: Int = native_list_get(bg, 1)
let b: Int = native_list_get(bg, 2)
let n: Int = native_list_len(shapes)
let i: Int = 0
while i < n {
let sh: [Int] = native_list_get(shapes, i)
if covers(sh, px, py) {
let r: Int = native_list_get(sh, 4)
let g: Int = native_list_get(sh, 5)
let b: Int = native_list_get(sh, 6)
}
let i: Int = i + 1
}
let out: [Int] = native_list_empty()
let out: [Int] = native_list_append(out, r)
let out: [Int] = native_list_append(out, g)
let out: [Int] = native_list_append(out, b)
return out
}
// rasterize: build the raw (filtered) scanline byte stream, filter byte 0 / row.
fn rasterize(w: Int, h: Int, shapes: [[Int]], bg: [Int]) -> [Int] {
let raw: [Int] = native_list_empty()
let y: Int = 0
while y < h {
let raw: [Int] = native_list_append(raw, 0)
let x: Int = 0
while x < w {
let col: [Int] = pixel_color(x, y, shapes, bg)
let raw: [Int] = native_list_append(raw, native_list_get(col, 0))
let raw: [Int] = native_list_append(raw, native_list_get(col, 1))
let raw: [Int] = native_list_append(raw, native_list_get(col, 2))
let x: Int = x + 1
}
let y: Int = y + 1
}
return raw
}
// zlib stream with a single STORED (uncompressed) DEFLATE block + Adler32.
fn zlib_store(raw: [Int]) -> [Int] {
let z: [Int] = native_list_empty()
let z: [Int] = native_list_append(z, 120)
let z: [Int] = native_list_append(z, 1)
let z: [Int] = native_list_append(z, 1)
let len: Int = native_list_len(raw)
let nlen: Int = 65535 - len
let z: [Int] = native_list_append(z, len % 256)
let z: [Int] = native_list_append(z, (len / 256) % 256)
let z: [Int] = native_list_append(z, nlen % 256)
let z: [Int] = native_list_append(z, (nlen / 256) % 256)
let z: [Int] = app_all(z, raw)
let ad: Int = adler32_of(raw)
let z: [Int] = app_u32be(z, ad)
return z
}
// append a full PNG chunk: length + (type+data) + crc32(type+data).
fn app_chunk(png: [Int], type_and_data: [Int], table: [Int]) -> [Int] {
let total: Int = native_list_len(type_and_data)
let dlen: Int = total - 4
let png: [Int] = app_u32be(png, dlen)
let png: [Int] = app_all(png, type_and_data)
let crc: Int = crc32_of(type_and_data, table)
let png: [Int] = app_u32be(png, crc)
return png
}
fn png_build(w: Int, h: Int, raw: [Int], table: [Int]) -> [Int] {
let png: [Int] = native_list_empty()
// 8-byte signature
let png: [Int] = native_list_append(png, 137)
let png: [Int] = native_list_append(png, 80)
let png: [Int] = native_list_append(png, 78)
let png: [Int] = native_list_append(png, 71)
let png: [Int] = native_list_append(png, 13)
let png: [Int] = native_list_append(png, 10)
let png: [Int] = native_list_append(png, 26)
let png: [Int] = native_list_append(png, 10)
// IHDR
let ihdr: [Int] = native_list_empty()
let ihdr: [Int] = app_tag(ihdr, "IHDR")
let ihdr: [Int] = app_u32be(ihdr, w)
let ihdr: [Int] = app_u32be(ihdr, h)
let ihdr: [Int] = native_list_append(ihdr, 8)
let ihdr: [Int] = native_list_append(ihdr, 2)
let ihdr: [Int] = native_list_append(ihdr, 0)
let ihdr: [Int] = native_list_append(ihdr, 0)
let ihdr: [Int] = native_list_append(ihdr, 0)
let png: [Int] = app_chunk(png, ihdr, table)
// IDAT
let z: [Int] = zlib_store(raw)
let idat: [Int] = native_list_empty()
let idat: [Int] = app_tag(idat, "IDAT")
let idat: [Int] = app_all(idat, z)
let png: [Int] = app_chunk(png, idat, table)
// IEND
let iend: [Int] = native_list_empty()
let iend: [Int] = app_tag(iend, "IEND")
let png: [Int] = app_chunk(png, iend, table)
return png
}
fn png_write(path: String, png: [Int]) -> Int {
let n: Int = native_list_len(png)
let buf: String = __str_alloc(n)
let i: Int = 0
while i < n {
let buf: String = __str_set_char(buf, i, native_list_get(png, i))
let i: Int = i + 1
}
let ok: Int = fs_write_bytes(path, buf, n)
return ok
}
-153
View File
@@ -1,153 +0,0 @@
// surface-profile.el - Surface profile data and accessors.
//
// THE NATIVE EFFERENT SEAM: surface = a pluggable PROFILE, using the exact same
// slot-map mechanism as language-profile.el. A language profile tells the
// realizer HOW to shape a natural-language surface (word order, morphology); a
// SURFACE profile tells the realizer WHICH surface to project meaning onto
// (markdown, docx, html, plain, or a non-text medium like symbolic music).
//
// The generalization is exact: realize_lang(form, profile) already renders a
// SemForm parameterized by a [String] profile read via lang_get. Surface is one
// more axis of that same profile vector. One frame (sem_frame), one plan step
// (sem_to_spec), one render (realize) the surface is DATA, not a code path,
// precisely as language is data. Adding a surface means adding a profile, no
// engine change. This is the multimodal projector, native: geometry -> any
// surface, the efferent twin of ingest.
//
// Surface slot keys:
// surface - "markdown" | "docx" | "html" | "plain" | "midi" | "image"
// modality - "text" | "audio" | "image" | "video"
// media_type - MIME type of the emitted surface
// head_open - string prepended to a heading (e.g. "## " for markdown)
// head_close - string appended to a heading (e.g. "" for markdown, "</h2>" for html)
// emph_open - string opening emphasis (e.g. "*")
// emph_close - string closing emphasis (e.g. "*")
// item_mark - list-item marker (e.g. "- ")
// para_sep - paragraph separator (e.g. "\n\n")
//
// For a TEXT modality the render composes these markers around the surface that
// the EXISTING realizer produces (realize_lang / sem_realize). For a non-text
// modality (audio/image) the profile declares modality + media_type and the
// render dispatches to the medium projector, which reads the SAME frame's
// geometry (its intent/affect/structure) and projects it onto sound or pixels
// deterministic-from-meaning, nothing invented. That dispatch point is where a
// music profile or image profile conforms, native, no parallel layer.
// -- Constructor -------------------------------------------------------------
fn surface_profile(surface: String, modality: String, media_type: String, head_open: String, head_close: String, emph_open: String, emph_close: String, item_mark: String, para_sep: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "surface")
let r = native_list_append(r, surface)
let r = native_list_append(r, "modality")
let r = native_list_append(r, modality)
let r = native_list_append(r, "media_type")
let r = native_list_append(r, media_type)
let r = native_list_append(r, "head_open")
let r = native_list_append(r, head_open)
let r = native_list_append(r, "head_close")
let r = native_list_append(r, head_close)
let r = native_list_append(r, "emph_open")
let r = native_list_append(r, emph_open)
let r = native_list_append(r, "emph_close")
let r = native_list_append(r, emph_close)
let r = native_list_append(r, "item_mark")
let r = native_list_append(r, item_mark)
let r = native_list_append(r, "para_sep")
let r = native_list_append(r, para_sep)
return r
}
// -- Accessor (same convention as lang_get; standalone so this is a leaf) -----
fn surface_get(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn surface_is_text(profile: [String]) -> Bool {
return str_eq(surface_get(profile, "modality"), "text")
}
// -- Built-in TEXT surface profiles ------------------------------------------
// Markdown: headings with "## ", emphasis with "*", "- " list items.
fn surface_profile_markdown() -> [String] {
return surface_profile("markdown", "text", "text/markdown", "## ", "", "*", "*", "- ", "\n\n")
}
// Plain text: no markup at all headings become bare uppercase-free lines.
fn surface_profile_plain() -> [String] {
return surface_profile("plain", "text", "text/plain", "", "", "", "", " - ", "\n\n")
}
// HTML: block-level heading/emphasis tags.
fn surface_profile_html() -> [String] {
return surface_profile("html", "text", "text/html", "<h2>", "</h2>", "<em>", "</em>", "<li>", "\n")
}
// docx: WordprocessingML is structural, not inline-markup; the head/emph slots
// carry the run/style intent that the OOXML emitter maps to <w:pStyle>. Declared
// here so docx is a first-class surface on the same seam.
fn surface_profile_docx() -> [String] {
return surface_profile("docx", "text", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "Heading2:", "", "b:", "", "bullet:", "\n")
}
// -- Built-in NON-TEXT surface profiles (the multimodal seam) ----------------
// Symbolic music (MIDI): modality=audio. The render dispatches to the music
// projector, which reads the SAME frame's intent/affect and projects it to
// pitch/rhythm deterministic-from-meaning. head/emph slots are empty because
// the medium is not textual; media_type names the surface. A music profile
// (scale/mode/instrument) is layered onto this by the audio agent, native.
fn surface_profile_midi() -> [String] {
return surface_profile("midi", "audio", "audio/midi", "", "", "", "", "", "")
}
// Synthesized audio (WAV): modality=audio, peer to midi. The richer audio
// surface the render SUPERPOSES ingested tonal primitives (sine at f0*n per an
// ingested instrument signature) into PCM, own-core, exactly as midi writes an
// SMF via struct. A music profile (scale/mode/instrument/adsr) layers onto this
// as its own [String] slot-map read by the same getter. Same frame -> midi OR
// audio, interchangeable; this is the audio agent's native conforming point.
fn surface_profile_audio() -> [String] {
return surface_profile("audio", "audio", "audio/wav", "", "", "", "", "", "")
}
// Image (raster): modality=image. Documented seam the render dispatches to the
// image projector, the efferent twin of image ingest, reading the same frame.
fn surface_profile_image() -> [String] {
return surface_profile("image", "image", "image/png", "", "", "", "", "", "")
}
// -- Composition helpers: wrap realized TEXT with the surface's markers -------
//
// These take text the EXISTING realizer already produced and shape it for the
// surface. They add NO content pure surface typography over faithful text,
// exactly as the language profile adds no content, only linguistic form.
fn surface_heading(profile: [String], text: String) -> String {
let o: String = surface_get(profile, "head_open")
let c: String = surface_get(profile, "head_close")
return o + text + c
}
fn surface_emph(profile: [String], text: String) -> String {
let o: String = surface_get(profile, "emph_open")
let c: String = surface_get(profile, "emph_close")
return o + text + c
}
// A section: a heading + a paragraph separator + the (already realized) body.
fn surface_section(profile: [String], heading: String, body: String) -> String {
let sep: String = surface_get(profile, "para_sep")
return surface_heading(profile, heading) + sep + body
}
@@ -1,26 +0,0 @@
// surface-profile-demo.el - ONE SemFrame, realized ONCE, projected to THREE
// surfaces via surface profiles. Proves surface-as-profile natively: the frame
// and the realized sentence are identical; only the surface PROFILE differs.
fn demo() -> String {
// 1. The shared frame (meaning-geometry): assert(Neuron, contain, the memory).
let frame: [String] = sem_frame("assert", "Neuron", "the memory", "")
// 2. REALIZE once via the EXISTING native realizer (language = a profile).
let sentence: String = sem_realize(frame)
// 3. PROJECT the same realized sentence onto three surfaces (surface = a
// profile). Same frame, same sentence, different surface one render.
let heading: String = "Memory"
let md: String = surface_section(surface_profile_markdown(), heading, sentence)
let html: String = surface_section(surface_profile_html(), heading, sentence)
let plain: String = surface_section(surface_profile_plain(), heading, sentence)
// 4. Report the non-text seam: a surface profile can declare an audio/image
// medium; the render dispatches to the medium projector on the SAME frame.
let midi_media: String = surface_get(surface_profile_midi(), "media_type")
return "MD=[" + md + "] HTML=[" + html + "] PLAIN=[" + plain + "] MIDI_MEDIA=" + midi_media
}
println(demo())
@@ -0,0 +1,162 @@
# 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.
**Date:** 2026-08-15 · **Worktree:** `agent-a6577c8211c332c5b` (isolated).
Grounding mechanism designed with Will (memory `9e09a59f`, refining
`1a861007`). This is the HOW for #50.
---
## (a) How grounded edge-propagation integrates into the dream/consolidation cycle
The beat already exists. `neuron/awareness.el` runs a heartbeat (~every
`beat_ms`); each beat calls `hebb_consolidate()` — which drains the self-formed
Hebbian associations out of the fast in-process store and writes them, over the
threshold `ENGRAM_HEBB_LINK_MIN`, into the durable engram (`:8742`) — and then
`emit_heartbeat()`.
Grounded edge-propagation slots into the **same beat, immediately after
consolidation** (awareness.el line 12861288):
```
hebb_consolidate() // lay down the tethers (edges) that cleared threshold
ground_propagate() // <-- NEW: grade beliefs ALONG those tethers
emit_heartbeat() // report gep_* gauges beside hebb_*
```
This ordering is the point. Consolidation lays down the wiring; propagation
grades the beliefs along it, in the same breath. Memory `69b8babe`:
memory-consolidation and staying-yourself are one physics — forming a memory and
grading a belief are the same gravity run in two passes of one beat.
The propagation runs **inside the engram** as the native
`engram_ground_propagate()` over the durable flat node/edge arrays (the store
the consolidated edges just landed in). The soul invokes it over HTTP
(`POST /api/ground/propagate`) and folds the returned `gep_*` telemetry into the
heartbeat stream next to `hebb_cands / hebb_mass / hebb_edges`.
**Bounded by construction** (per the live-graph reality — 70.7% of nodes
isolated, connected core ~28%, hub first-hop fan-out in the thousands):
- **1-hop only.** No BFS spreading activation — a belief is graded from its
DIRECT grounded neighbors, so there is no per-hop breadth explosion.
- **Beam-capped** at `GEP_MAX_CORR = 256` corroborators per belief.
- **Salience-ordered, `GEP_BELIEFS_PER_BEAT = 512`** beliefs per beat; the rest
next beat. Work per beat is O(beliefs × degree), hard-bounded.
- **Isolated / starved beliefs** are counted and surfaced (`gep_isolated`,
`gep_starved`) as an interoceptive sparse-region signal for the
edge-formation / embedding pass (#20). #50 CONSUMES edges; it does not form
them. A belief with no grounded neighbor has nothing to tether to — correct
per the anti-delusion gravity law (`0b15017c`), not a gap.
---
## (b) The implementation
Represented faithfully to the spec — **grounding is a Hebbian-weighted
collection over time, never a scalar.**
- **Grounding = an append-only event ring** on the node (`GepGrounding`),
structurally parallel to the ACT-R base-level access ring already in
`EngramNode` (`access_ts[K]`). Each event is `{ts, sign±, mag, corroborator
signature}`. Append-only, supersede-not-delete; events aged out of the ring
are counted (`older_count`), never faked away.
- **Standing is DERIVED, recency-weighted, never stored** —
`standing = clamp(GEP_BASE + Σ_events sign·mag·age^(-D), 0, 1)`, exactly the
ACT-R base-level shape `ln Σ t^-d` (`ENGRAM_BLL_D = 0.5`) but sign-carrying so
LTD subtracts. Memory `1a861007`: the collection is primary, the standing is
its emergent aggregate. Mirrored onto `confidence` each beat so downstream
reads (verifier #43, realizer calibration `0041d917`) never speak above the
grounding.
- **Update = LTP/LTD with a threshold.** Per belief, gather corroborators along
incident edges, weighted by `edge.weight` (the Hebbian weight) × the
neighbor's own standing. **Anti-delusion gravity:** only neighbors already
`≥ GEP_LIKELY_MIN` may corroborate — grounding flows FROM the grounded core.
- **Convergent INDEPENDENT corroboration** is the driver. Independence is
enforced by **union-find over the corroborator set**: two corroborators are
the same independent source if they are the same node, reached by multiple
edges, or linked to each other (an echo chain / shared derivation). Support is
summed **per independent component** (max-magnitude member), and the threshold
gate requires BOTH a mass floor (`pos ≥ GEP_THETA`) AND an independence-count
floor (`n_independent ≥ GEP_N_MIN`). The count gate is the guard against one
node echoed N times.
- **Sub-threshold is transient.** Support present but below threshold →
`subthreshold_hits++`, no durable event, no lasting shift (Will's exact spec).
- **Graduation / decay.** Cross up → LTP event appended → standing climbs
`conjecture → likely → grounded`. Contradiction past threshold → LTD →
`grounded → likely → conjecture`. Nothing latches; withdraw support and the
collection ages and relaxes (`271f1163`, nothing is settled).
### Files
| File | Role |
|---|---|
| `gep_core.h` | The mechanism. Pure C, libm only (own-the-core). Single source of truth: `GepGrounding`, `gep_standing`, `gep_append`, union-find independence, `gep_propagate_node`, `gep_beat`. |
| `gep_proof.c` | Self-contained proof harness — builds the three scenarios, prints raw before/after. |
| `engram_ground_propagate.staged.c` | GATED runtime native. Wires the SAME `gep_core.h` primitives to the live `EngramStore` (adj cache, flat arrays). Splice plan + relation→polarity + belief gate. Compiles only when spliced (verified: every runtime symbol it references — `engram_adj_rebuild`, `adj_from_len`, `engram_find_node_index`, `ENGRAM_LAYER_SAFETY`, `istr_contains`, … — exists in the release runtime). |
| `awareness.beat.patch.el` | GATED beat hook — `ground_propagate()` + the insert between `hebb_consolidate()` and `emit_heartbeat()`. |
| `server.route.patch.el` | GATED route — `POST /api/ground/propagate`. |
### Constants
`BASE=0.10 LIKELY_MIN=0.34 GROUNDED_MIN=0.66 N_MIN=3 THETA=0.30 D=0.5`
(`N_MIN` parameterizes Will's "13 adjacent things" — the count threshold is a
knob; 3 here for a crisp proof.)
---
## (c) PROOF LEDGER — raw grounding before/after
Deterministic. Build `cc -std=c11 -O2 -o gep_proof gep_proof.c -lm`, run
`./gep_proof` (full transcript in `PROOF_OUTPUT.txt`).
### (a) STRENGTHEN — convergent independent corroboration graduates a conjecture
| beat | event | pos_mass (n_indep) | action | standing before → after | band |
|---|---|---|---|---|---|
| 1 | 3 independent grounded corroborators | 0.4050 (3) | **LTP** | 0.1000 → **0.4842** | conjecture → **likely** ⬆ |
| 2 | neighborhood grows to 5 | 0.6750 (5) | **LTP** | 0.1496 → **0.7379** | conjecture → **grounded** ⬆ |
| 3 | support sustained (5) | 0.6750 (5) | LTP | 0.2110 → 0.7993 | grounded (sustained) |
| 4 | corroboration withdrawn (+10min) | 0.0000 (0) | isolated | 0.1612 → 0.1612 | relaxing |
| 5 | still withdrawn (+1h) | — | isolated | 0.1263 | relaxing |
| 6 | still withdrawn (+4h) | — | isolated | 0.1130 | → conjecture |
Grounding grew **on its own** past threshold and graduated conjecture → likely →
grounded, then **relaxed** once independent support stopped. Living, not a
latched flag.
### (b) DECAY — convergent independent contradiction erodes a grounded belief
| beat | event | neg_mass (n_indep) | action | standing before → after | band |
|---|---|---|---|---|---|
| — | seed (prior LTP) | — | — | **0.9500** | grounded |
| 1 | 3 independent contradictions | 0.5400 (3) | **LTD** | 0.9500 → **0.4570** | grounded → **likely** ⬇ |
| 2 | contradiction broadens to 5 | 0.9000 (5) | **LTD** | 0.1461 → **0.0000** | conjecture ⬇ |
| 34 | contradiction sustained (5) | 0.9000 (5) | LTD | 0.0000 | conjecture |
Grounding decayed grounded → likely → conjecture under accreting independent
contradiction. The door never shut — history is retained (the event ring keeps
growing), the belief stays falsifiable in both directions.
### (c) INDEPENDENCE GUARD — the load-bearing property
Identical fan-in (N=5), identical edge weight (0.30), identical corroborator
standing (~0.90). **The only difference is whether the five are independent.**
| sub-case | topology | pos_mass | **n_indep** | action | standing 0.1000 → |
|---|---|---|---|---|---|
| **C1** | 5 DISTINCT, no inter-links | 1.3500 | **5** | **LTP** | **0.9741 (grounded)** ⬆ |
| **C2** | 5 mutually-linked (echo of one source) | 0.2700 | **1** | sub-threshold | 0.1000 (unchanged) |
| **C3** | 1 node reached by 5 parallel edges | 0.2700 | **1** | sub-threshold | 0.1000 (unchanged) |
Same raw fan-in, opposite outcome. Union-find collapses the echoes to a single
independent component; the count gate (`n_indep ≥ N_MIN`) then refuses them.
**Circular self-reinforcement cannot manufacture grounding** — a conjecture can
only be grounded by evidence that is genuinely independent of itself.
---
**RAILS honored:** isolated worktree; built/proven on a clone; the live soul
(`:8742` / `:7770`) untouched; no fight with the cutover (built against current
release source; staged native rebases cleanly onto it); no new libraries
(libm only); identity keystones untouched. **Not promoted** — gated artifact +
ledger for the main loop to sequence.
@@ -0,0 +1,75 @@
GROUNDED EDGE-PROPAGATION — PROOF LEDGER (task #50)
constants: BASE=0.10 LIKELY_MIN=0.34 GROUNDED_MIN=0.66 N_MIN=3 THETA=0.30 D=0.5
=== SCENARIO A — STRENGTHEN: convergent independent corroboration ===
seed: conjecture has NO grounding events; corroborators pre-grounded.
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
beat 1 (t=+0s) 3 independent grounded corroborators appear
incident_edges=3 pos_mass=0.4050 (n_indep=3) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> LTP (strengthen) standing 0.1000 (conjecture) -> 0.4842 (likely) [GRADUATED]
beat 2 (t=+60s) neighborhood grows to 5 corroborators
incident_edges=5 pos_mass=0.6750 (n_indep=5) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> LTP (strengthen) standing 0.1496 (conjecture) -> 0.7379 (grounded) [GRADUATED]
beat 3 (t=+120s) support sustained (5)
incident_edges=5 pos_mass=0.6750 (n_indep=5) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> LTP (strengthen) standing 0.2110 (conjecture) -> 0.7993 (grounded) [GRADUATED]
beat 4 (t=+720s) corroboration withdrawn (+10min)
incident_edges=0 pos_mass=0.0000 (n_indep=0) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> isolated (no edges) standing 0.1612 (conjecture) -> 0.1612 (conjecture)
beat 5 (t=+3600s) still withdrawn (+1h)
incident_edges=0 pos_mass=0.0000 (n_indep=0) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> isolated (no edges) standing 0.1263 (conjecture) -> 0.1263 (conjecture)
beat 6 (t=+14400s) still withdrawn (+4h)
incident_edges=0 pos_mass=0.0000 (n_indep=0) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> isolated (no edges) standing 0.1130 (conjecture) -> 0.1130 (conjecture)
RESULT: grounding grew automatically past threshold and graduated,
then relaxed once the independent support stopped — living,
not a latched flag.
=== SCENARIO B — DECAY: convergent independent CONTRADICTION ===
seed: belief pre-grounded by a strong prior LTP event.
belief standing=0.9500 band=grounded events=1 subthresh=0
beat 1 (t=+0s) 3 independent contradictions
incident_edges=3 pos_mass=0.0000 (n_indep=0) neg_mass=0.5400 (n_indep=3) THETA=0.30 N_MIN=3
-> LTD (decay) standing 0.9500 (grounded) -> 0.4570 (likely) [DEMOTED]
beat 2 (t=+60s) contradiction broadens to 5
incident_edges=5 pos_mass=0.0000 (n_indep=0) neg_mass=0.9000 (n_indep=5) THETA=0.30 N_MIN=3
-> LTD (decay) standing 0.1461 (conjecture) -> 0.0000 (conjecture)
beat 3 (t=+120s) contradiction sustained (5)
incident_edges=5 pos_mass=0.0000 (n_indep=0) neg_mass=0.9000 (n_indep=5) THETA=0.30 N_MIN=3
-> LTD (decay) standing 0.0401 (conjecture) -> 0.0000 (conjecture)
beat 4 (t=+180s) contradiction sustained (5)
incident_edges=5 pos_mass=0.0000 (n_indep=0) neg_mass=0.9000 (n_indep=5) THETA=0.30 N_MIN=3
-> LTD (decay) standing 0.0000 (conjecture) -> 0.0000 (conjecture)
RESULT: grounding decayed grounded->likely->conjecture under
convergent independent contradiction. The door never shut
on the belief; its history is retained (events keep growing).
=== SCENARIO C — INDEPENDENCE GUARD (the load-bearing property) ===
Both sub-cases: N=5 corroborators, edge weight 0.30, corroborator
standing ~0.90. ONLY difference: whether the 5 are independent.
-- C1: 5 DISTINCT independent corroborators --
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
beat 1 (t=+0s) 5 independent corroborators (no inter-links)
incident_edges=5 pos_mass=1.3500 (n_indep=5) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> LTP (strengthen) standing 0.1000 (conjecture) -> 0.9741 (grounded) [GRADUATED]
-- C2: 5 corroborators, but mutually-linked (echo of ONE source) --
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
beat 1 (t=+0s) 5 echoed (mutually-linked) corroborators
incident_edges=5 pos_mass=0.2700 (n_indep=1) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> sub-threshold (no shift) standing 0.1000 (conjecture) -> 0.1000 (conjecture)
-- C3: ONE corroborator, reached by 5 parallel edges --
conjecture standing=0.1000 band=conjecture events=0 subthresh=0
beat 1 (t=+0s) same node, 5 parallel edges
incident_edges=5 pos_mass=0.2700 (n_indep=1) neg_mass=0.0000 (n_indep=0) THETA=0.30 N_MIN=3
-> sub-threshold (no shift) standing 0.1000 (conjecture) -> 0.1000 (conjecture)
RESULT: identical raw fan-in (5) and mass inputs; C1 grounds because
the corroboration is INDEPENDENT (5 components), C2/C3 do not
because it collapses to ONE source. Circular self-reinforcement
cannot manufacture grounding.
DONE.
@@ -0,0 +1,60 @@
//
// awareness.beat.patch.el GATED integration hook for task #50.
// NOT APPLIED. Shows exactly how grounded edge-propagation couples into the
// dream/consolidation beat in neuron/awareness.el. Promotion sequenced by the
// main loop after the engine cutover settles.
//
// WHY HERE. The heartbeat is the beat. Today it runs hebb_consolidate() to
// drain the self-formed Hebbian associations into the durable store, then
// emit_heartbeat(). Grounded edge-propagation belongs in the SAME beat, AFTER
// consolidation: the edges hebb_consolidate() just wrote are the tethers
// grounding propagates along. Consolidation lays down the wiring; propagation
// grades the beliefs along it. One beat, coupled memory 69b8babe: memory-
// consolidation and staying-yourself are one physics.
//
// The propagation itself runs INSIDE the engram (native engram_ground_propagate
// over the durable flat node/edge arrays). The soul invokes it over HTTP and
// folds the gep_* telemetry into the heartbeat stream next to the hebb_* gauges.
//
// [1] New helper sibling to hebb_consolidate() (awareness.el ~line 99).
// Fires one grounded edge-propagation beat on the durable store and returns
// its JSON telemetry ({"gep_strengthened":..,"gep_graduations":.., ...}).
fn ground_propagate() -> String {
let url_env: String = env("SOUL_ISE_URL")
let url_state: String = if str_eq(url_env, "") { state_get("soul_engram_url") } else { url_env }
let engram_url: String = if str_eq(url_state, "") { "http://localhost:8742" } else { url_state }
// Same auth envelope as hebb_consolidate this is a graph mutation (it
// appends grounding events + updates confidence), so it is gated on _auth.
let key_state: String = state_get("soul_engram_api_key")
let api_key: String = if str_eq(key_state, "") { env("ENGRAM_API_KEY") } else { key_state }
let auth_part: String = if str_eq(api_key, "") { "{}" } else { "{\"_auth\":\"" + api_key + "\"}" }
let resp: String = http_post_json(engram_url + "/api/ground/propagate", auth_part)
if str_eq(resp, "") { return "" }
return resp
}
// [2] Beat hook insert between hebb_consolidate() and emit_heartbeat()
// (awareness.el line 1286-1288). Replaces:
//
// let wb_sent_n: Int = hebb_consolidate()
// state_set("soul.hebb_wb_sent", int_to_str(wb_sent_n))
// emit_heartbeat()
//
// with:
//
// let wb_sent_n: Int = hebb_consolidate()
// state_set("soul.hebb_wb_sent", int_to_str(wb_sent_n))
// // Grounded edge-propagation grade beliefs along the tethers
// // consolidation just laid down. Threshold-gated by convergent
// // independent corroboration; automatic, salience-ordered, bounded.
// let gep_tel: String = ground_propagate()
// state_set("soul.gep_last", gep_tel)
// emit_heartbeat()
//
// [3] emit_heartbeat() (awareness.el ~line 201) folds soul.gep_last into the
// heartbeat payload beside the hebb_* gauges, so graduation/decay counts
// are visible in the durable ISE stream the same observability discipline
// the Hebbian rule earned (a mechanism you cannot see in the stream is a
// mechanism you cannot trust): read state_get("soul.gep_last") and splice
// it into the heartbeat JSON object.
@@ -0,0 +1,188 @@
/* ─────────────────────────────────────────────────────────────────────────
* engram_ground_propagate.staged.c — GATED runtime native for task #50.
*
* STAGED, NOT COMPILED INTO THE LIVE BINARY. This mirrors the
* geometric_retrieve.staged.c staging pattern (memory 1cc231ec): it references
* runtime-internal types (EngramStore, EngramNode, EngramEdge, engram_global,
* engram_now_ms, the adj cache) and therefore compiles ONLY when spliced into
* lang/releases/v1.0.0-20260501/el_runtime.c. Splice + promotion is sequenced
* by the main loop AFTER the engine+HNSW cutover settles — do NOT hand-apply.
*
* It is the production form of the mechanism proven in gep_proof.c: the SAME
* gep_core.h primitives (GepGrounding ring, gep_standing, gep_append,
* union-find independence), wired directly to the live flat node/edge arrays.
*
* ── SPLICE PLAN (three additive edits to el_runtime.c; nothing removed) ──────
*
* [1] EngramNode struct (~line 6061, after hebb_elig_ts): add the grounding
* collection. Additive; zero-initialized by the existing calloc/memset
* paths, so legacy snapshots degrade gracefully to an empty history.
*
* GepGrounding grounding; // task #50 — append-only grounding ring
*
* [2] #include "gep_core.h" near the other engram includes, and paste the
* body of this file below the Hebbian section (after engram_hebb_drain_json).
*
* [3] Persistence (engram_save node JSON ~7934 / engram_load parser ~8186):
* serialize the grounding ring as a compact "grounding" array of
* [ts,sign,mag] triples + subthreshold_hits so standing survives a
* round-trip. Helpers gep_grounding_to_json / gep_grounding_parse below.
* Until wired, grounding is in-RAM only (like the Hebbian eligibility
* trace) — correct for a first gated rollout, but standing resets on boot.
*
* [4] EL surface: declare engram_ground_propagate in el_runtime.h + el_seed.c,
* add route_ground_propagate to engram/src/server.el, called from the
* awareness.el consolidation beat (see awareness.beat.patch.el).
* ───────────────────────────────────────────────────────────────────────── */
#include "gep_core.h"
/* Relation → evidential polarity. Supportive relations transmit grounding
* gravity (+1); contradictory relations erode it (-1); everything else is a
* NON-evidential edge (structural / navigational) and is ignored (0) — an
* association is not a corroboration. Extend deliberately; a mis-classified
* relation is a false corroboration. */
static int8_t gep_relation_polarity(const char* rel) {
if (!rel) return 0;
if (!strcmp(rel, "supports") || !strcmp(rel, "corroborates") ||
!strcmp(rel, "derived-from") || !strcmp(rel, "hebbian-associate") ||
!strcmp(rel, "grounds") || !strcmp(rel, "confirms")) return +1;
if (!strcmp(rel, "contradicts") || !strcmp(rel, "refutes") ||
!strcmp(rel, "negates") || !strcmp(rel, "conflicts-with")) return -1;
return 0;
}
/* Which nodes are BELIEFS/CONJECTURES subject to grounding propagation. Facts
* imported as knowledge are already grounded by provenance; identity/safety
* layers are never re-graded here. Gate on node_type + the conjecture tag. */
static int gep_is_belief(const EngramNode* n) {
if (!n || !n->node_type) return 0;
if (n->layer_id == ENGRAM_LAYER_SAFETY) return 0; /* never re-grade safety */
return !strcmp(n->node_type, "Memory") ||
!strcmp(n->node_type, "Conjecture") ||
!strcmp(n->node_type, "Hypothesis") ||
!strcmp(n->node_type, "Belief") ||
(n->tags && istr_contains(n->tags, "conjecture"));
}
/* Grounding standing of an engram node, derived from its collection. This is
* the value the verifier (#43) and realizer (calibrated assertion, 0041d917)
* read — and it is written back into epistemic_confidence-equivalent surfaces
* so "never speak above the grounding" is enforced from one source of truth. */
double engram_grounding_standing(const EngramNode* n, int64_t now_ms) {
return gep_standing(&n->grounding, now_ms);
}
/* ── The beat: one pass of grounded edge-propagation over the whole store ────
* Called from the consolidation/dream heartbeat. 1-hop, beam-capped, salience-
* ordered so a bounded slice of the highest-salience beliefs is processed per
* beat (the rest next beat) — never a full-graph blow-up on a 12k-node store.
* Returns JSON telemetry for the heartbeat stream. */
#define GEP_BELIEFS_PER_BEAT 512 /* bound work per beat; salience-prioritized */
el_val_t engram_ground_propagate(void) {
EngramStore* g = engram_get();
int64_t now = engram_now_ms();
engram_adj_rebuild(g); /* ensure adj_from/adj_to are current */
int strengthened = 0, decayed = 0, subthreshold = 0;
int graduations = 0, demotions = 0, isolated = 0, starved = 0, processed = 0;
for (int64_t bi = 0; bi < g->node_count && processed < GEP_BELIEFS_PER_BEAT; bi++) {
EngramNode* b = &g->nodes[bi];
if (!gep_is_belief(b)) continue;
processed++;
int before = gep_band_rank(gep_standing(&b->grounding, now));
/* Gather independent corroborators over incident edges (both directions),
* anti-delusion gated (neighbor must already be ≥ LIKELY_MIN). */
GepCorrSet cs; cs.n = 0; int incident = 0;
int* out = g->adj_from[bi]; int out_n = g->adj_from_len[bi];
int* in = g->adj_to[bi]; int in_n = g->adj_to_len[bi];
for (int pass = 0; pass < 2; pass++) {
int* lst = pass ? in : out; int ln = pass ? in_n : out_n;
for (int k = 0; k < ln; k++) {
EngramEdge* e = &g->edges[lst[k]];
int8_t pol = gep_relation_polarity(e->relation);
if (pol == 0) continue;
incident++;
const char* cid = pass ? e->from_id : e->to_id;
int64_t ci = engram_find_node_index(cid);
if (ci < 0 || ci == bi) continue;
double cstand = gep_standing(&g->nodes[ci].grounding, now);
if (cstand < GEP_LIKELY_MIN) continue; /* no tether */
double contrib = e->weight * cstand * (double)pol;
int ex = -1;
for (int q = 0; q < cs.n; q++) if (cs.node_idx[q] == (int)ci) { ex = q; break; }
if (ex >= 0) { if (fabs(contrib) > fabs(cs.contrib[ex])) cs.contrib[ex] = contrib; }
else if (cs.n < GEP_MAX_CORR) {
cs.node_idx[cs.n] = (int)ci; cs.contrib[cs.n] = contrib;
cs.parent[cs.n] = cs.n; cs.n++;
}
}
}
/* Collapse mutually-derived corroborators (an edge between two of them)
* into one independent component — the independence guard. */
for (int x = 0; x < cs.n; x++) {
int64_t nx = cs.node_idx[x];
int* xout = g->adj_from[nx]; int xn = g->adj_from_len[nx];
for (int k = 0; k < xn; k++) {
const char* tid = g->edges[xout[k]].to_id;
int64_t ti = engram_find_node_index(tid);
for (int y = 0; y < cs.n; y++)
if (cs.node_idx[y] == (int)ti) { gep_uf_union(&cs, x, y); break; }
}
}
/* Per-component max-magnitude, split by polarity → convergent independent
* support mass + independence count. */
double comp_best[GEP_MAX_CORR]; int comp_root[GEP_MAX_CORR], ncomp = 0;
for (int i = 0; i < cs.n; i++) {
int r = gep_uf_find(&cs, i), slot = -1;
for (int kk = 0; kk < ncomp; kk++) if (comp_root[kk] == r) { slot = kk; break; }
if (slot < 0) { slot = ncomp++; comp_root[slot] = r; comp_best[slot] = cs.contrib[i]; }
else if (fabs(cs.contrib[i]) > fabs(comp_best[slot])) comp_best[slot] = cs.contrib[i];
}
double pos = 0, neg = 0; int np = 0, nn = 0; uint64_t sig = 1469598103934665603ULL;
for (int k = 0; k < ncomp; k++) {
if (comp_best[k] > 0) { pos += comp_best[k]; np++; }
else if (comp_best[k] < 0) { neg += -comp_best[k]; nn++; }
sig = (sig ^ (uint64_t)comp_root[k]) * 1099511628211ULL;
}
double net = pos - neg;
if (net > 0 && pos >= GEP_THETA && np >= GEP_N_MIN) {
gep_append(&b->grounding, now, +1, tanh(GEP_MAG_GAIN * net), sig);
strengthened++;
} else if (net < 0 && neg >= GEP_THETA && nn >= GEP_N_MIN) {
gep_append(&b->grounding, now, -1, tanh(GEP_MAG_GAIN * (-net)), sig);
decayed++;
} else if (np > 0 || nn > 0) {
b->grounding.subthreshold_hits++; subthreshold++;
} else if (incident == 0) { isolated++; }
else { starved++; }
/* Mirror the derived standing onto confidence so downstream reads
* (activate epistemic_confidence, realizer calibration) never exceed the
* grounding. Faithful representation, single source of truth. */
double stand = gep_standing(&b->grounding, now);
b->confidence = stand;
b->updated_at = now;
int after = gep_band_rank(stand);
if (after > before) graduations++;
if (after < before) demotions++;
}
/* Heartbeat telemetry — the gep_* line, sibling to the hebb_* gauges. */
char buf[512];
snprintf(buf, sizeof buf,
"{\"gep_processed\":%d,\"gep_strengthened\":%d,\"gep_decayed\":%d,"
"\"gep_subthreshold\":%d,\"gep_graduations\":%d,\"gep_demotions\":%d,"
"\"gep_isolated\":%d,\"gep_starved\":%d}",
processed, strengthened, decayed, subthreshold,
graduations, demotions, isolated, starved);
return EL_STR(el_strdup(buf));
}
@@ -0,0 +1,299 @@
/* ─────────────────────────────────────────────────────────────────────────
* gep_core.h — Grounded Edge-Propagation, the core mechanism (task #50).
*
* Edge-aware, dream-coupled consolidation. Runs DURING the consolidation/dream
* beat (awareness.el hebb_consolidate → engram_ground_propagate). Grounding
* propagates + strengthens/decays along edges, threshold-gated by CONVERGENT
* INDEPENDENT corroboration from adjacent grounded nodes.
*
* This header is the single source of truth for the algorithm. It is pure C
* (libm only — own-the-core, no new libraries) and operates on a compact graph
* view (GepGraph) that both the proof harness and the runtime native populate
* from the live EngramStore (nodes/edges flat arrays + adj_from/adj_to).
*
* SPEC (Will, 2026-08-15; memory 9e09a59f, refines 1a861007):
* - A grounding is a VECTOR + its HEBBIAN WEIGHTS — a weighted structure over
* the evidential neighborhood, NOT a scalar and NOT a flat list. It APPENDS
* and GROWS on SIGNIFICANT change. => grounding = an APPEND-ONLY event ring
* (GepGrounding), parallel to the ACT-R base-level access_ts ring already in
* EngramNode. Current standing is DERIVED, recency-weighted, never stored.
* - UPDATE = LTP/LTD with a THRESHOLD (the key nonlinearity). Sub-threshold =
* recorded in history but TRANSIENT (no lasting shift). Cross the threshold
* of convergent support → grounding STRENGTHENS. Contradiction/erosion past
* threshold → grounding DECAYS. Automatic, event-driven, salience-gated.
* - DRIVER = CONVERGENT INDEPENDENT CORROBORATION (coherentism, mechanized):
* when N INDEPENDENT adjacent nodes ground as likely-true around a
* conjecture (Will's example: 13), its grounding grows on its own.
* - INDEPENDENCE is load-bearing: N DISTINCT corroborators, not one node
* echoed N times. Guards against circular self-reinforcement.
* - ANTI-DELUSION GRAVITY (memory 0b15017c): support flows only FROM already-
* grounded neighbors. A belief cannot ground from ungrounded speculation,
* however self-consistent — nothing tethers it to the grounded core.
* - NOTHING IS SETTLED (memory 271f1163): grounded is strongly-held, still
* falsifiable. Decay path stays open on every node; history is append-only,
* supersede-not-delete.
* ───────────────────────────────────────────────────────────────────────── */
#ifndef GEP_CORE_H
#define GEP_CORE_H
#include <stdint.h>
#include <math.h>
#include <string.h>
/* ── Constants ──────────────────────────────────────────────────────────────
* GEP_DECAY_D matches ENGRAM_BLL_D (0.5, canonical ACT-R): the derived standing
* is recency-weighted over the grounding-event collection exactly as the
* base-level term is recency-weighted over the access ring (memory 1a861007:
* "structurally the ACT-R base-level pattern, a sum over time-stamped events").
*/
#define GEP_DECAY_D 0.5 /* ACT-R power-law recency exponent */
#define GEP_BASE 0.10 /* standing floor of a bare conjecture */
#define GEP_LIKELY_MIN 0.34 /* band: conjecture < LIKELY ≤ likely */
#define GEP_GROUNDED_MIN 0.66 /* band: likely < GROUNDED ≤ grounded */
#define GEP_N_MIN 3 /* min INDEPENDENT corroborators to cross */
#define GEP_THETA 0.30 /* min convergent-support MASS to cross */
#define GEP_MAG_GAIN 1.0 /* net-support → event-magnitude gain (tanh) */
#define GEP_EVENT_RING 32 /* grounding-history depth kept exactly */
/* A single grounding event — one contact with the evidential neighborhood.
* Append-only; the ring is the collection-over-time, the standing is derived. */
typedef struct {
int64_t ts; /* wall-clock ms of the grounding event */
int8_t sign; /* +1 = LTP (strengthen), -1 = LTD (decay) */
double mag; /* magnitude in (0,1], = tanh(gain·|net independent support|)*/
uint64_t sig; /* signature of the independent corroborator set (audit) */
} GepEvent;
/* The grounding of one node: an append-only ring of events + transient counters.
* older_count keeps the tail (events aged out of the ring) so the collection is
* never silently lost — supersede-not-delete. subthreshold_hits records beats
* where support was present but did NOT cross threshold (transient, no shift). */
typedef struct {
GepEvent ev[GEP_EVENT_RING];
int head; /* next write slot */
int filled; /* valid entries (≤ GEP_EVENT_RING) */
int64_t older_count; /* durable events aged past the ring */
int subthreshold_hits; /* transient sub-threshold beats, no shift */
} GepGrounding;
typedef struct {
const char* id;
GepGrounding gr;
int is_belief; /* 1 = subject to propagation (conjecture/belief) */
} GepNode;
/* An edge carries a HEBBIAN WEIGHT (EngramEdge.weight) and a polarity derived
* from its relation: supportive (supports/corroborates/derived-from/hebbian-
* associate) = +1, contradictory (contradicts/refutes) = -1. */
typedef struct {
int from; /* node index */
int to; /* node index */
double weight; /* Hebbian edge weight, [0,1] */
int8_t polarity; /* +1 supportive, -1 contradictory */
} GepEdge;
typedef struct {
GepNode* nodes; int n_nodes;
GepEdge* edges; int n_edges;
} GepGraph;
typedef struct {
int strengthened; /* beliefs that took an LTP event this beat */
int decayed; /* beliefs that took an LTD event this beat */
int subthreshold; /* beliefs with support present but below threshold */
int graduations; /* band-up transitions (conjecture→likely→grounded) */
int demotions; /* band-down transitions */
int isolated; /* belief nodes with ZERO incident edges (sparse graph) */
int starved; /* belief nodes with edges but NO grounded corroborator */
} GepBeatStats;
/* Real-graph note (live measurement 2026-08-15): 70.7% of nodes are isolated,
* connected core ~28%. Grounded edge-propagation is definitionally scoped to
* the connected core — a belief with no grounded neighbor has nothing to
* tether to (anti-delusion gravity). isolated/starved are surfaced as an
* interoceptive signal for the edge-formation / embedding pass (#20) to try to
* connect them; #50 CONSUMES edges, it does not form them. */
/* ── Standing derivation: collection → scalar, recency-weighted ─────────────
* standing = clamp( GEP_BASE + Σ_events sign·mag·age^(-D) , 0, 1 ).
* Exactly the ACT-R base-level shape (Σ t^-d) but sign-carrying so LTD subtracts.
* The value is a pure function of wall-clock time — idempotent, never stored. */
static inline double gep_standing(const GepGrounding* g, int64_t now_ms) {
double raw = 0.0;
for (int i = 0; i < g->filled; i++) {
double age = (double)(now_ms - g->ev[i].ts) / 1000.0;
if (age < 1.0) age = 1.0; /* clock-skew / same-beat → 1s */
raw += (double)g->ev[i].sign * g->ev[i].mag * pow(age, -GEP_DECAY_D);
}
double s = GEP_BASE + raw;
if (s < 0.0) s = 0.0;
if (s > 1.0) s = 1.0;
return s;
}
/* Band label from a standing value. */
static inline const char* gep_band(double standing) {
if (standing >= GEP_GROUNDED_MIN) return "grounded";
if (standing >= GEP_LIKELY_MIN) return "likely";
return "conjecture";
}
static inline int gep_band_rank(double standing) {
if (standing >= GEP_GROUNDED_MIN) return 2;
if (standing >= GEP_LIKELY_MIN) return 1;
return 0;
}
/* Append one grounding event to the ring (append-only; oldest slot recycles,
* its loss counted in older_count so the collection's depth is never faked). */
static inline void gep_append(GepGrounding* g, int64_t ts, int8_t sign,
double mag, uint64_t sig) {
if (g->filled >= GEP_EVENT_RING) g->older_count++;
g->ev[g->head].ts = ts;
g->ev[g->head].sign = sign;
g->ev[g->head].mag = mag;
g->ev[g->head].sig = sig;
g->head = (g->head + 1) % GEP_EVENT_RING;
if (g->filled < GEP_EVENT_RING) g->filled++;
}
/* ── Independence via union-find over corroborators ─────────────────────────
* Two corroborators are the SAME independent source if they are the same node,
* or if a direct edge links them (mutually-derived / echoed through a chain).
* Counting DISTINCT components — not raw corroborator count — is the guard
* against one node echoed N times reading as N independent corroborations. */
#define GEP_MAX_CORR 256
typedef struct {
int node_idx[GEP_MAX_CORR]; /* corroborator node index */
double contrib[GEP_MAX_CORR]; /* weight·standing(c) */
int parent[GEP_MAX_CORR]; /* union-find parent */
int n;
} GepCorrSet;
static int gep_uf_find(GepCorrSet* s, int x) {
while (s->parent[x] != x) { s->parent[x] = s->parent[s->parent[x]]; x = s->parent[x]; }
return x;
}
static void gep_uf_union(GepCorrSet* s, int a, int b) {
int ra = gep_uf_find(s, a), rb = gep_uf_find(s, b);
if (ra != rb) s->parent[ra] = rb;
}
/* index of node_idx within the corroborator set, or -1 */
static int gep_corr_index_of(const GepCorrSet* s, int node_idx) {
for (int i = 0; i < s->n; i++) if (s->node_idx[i] == node_idx) return i;
return -1;
}
/* ── The beat: grounded edge-propagation over one belief node ───────────────
* Returns +1 if an LTP event was appended, -1 if LTD, 0 if sub-threshold/none.
* out_pos/out_neg/out_np/out_nn expose the raw support decomposition for the
* proof ledger (mass and independent-component counts on each polarity). */
static int gep_propagate_node(GepGraph* g, int b, int64_t now_ms,
double* out_pos, double* out_neg,
int* out_np, int* out_nn, int* out_incident) {
GepCorrSet cs; cs.n = 0;
int incident = 0; /* any edge touching b at all — isolation detector */
/* 1. Gather corroborators along incident edges. Anti-delusion gravity:
* only ALREADY-grounded neighbors (standing ≥ LIKELY_MIN) may corroborate.
* Each contributes weight·standing; polarity kept via signed contrib.
* 1-HOP ONLY — no BFS fan-out, so no per-hop breadth explosion. The
* corroborator working set is hard-capped at GEP_MAX_CORR (beam bound
* against hub belief nodes with thousands of incident edges). */
for (int e = 0; e < g->n_edges; e++) {
int c = -1; int8_t pol = 0;
if (g->edges[e].from == b) { c = g->edges[e].to; pol = g->edges[e].polarity; }
else if (g->edges[e].to == b) { c = g->edges[e].from; pol = g->edges[e].polarity; }
else continue;
incident++;
if (c < 0 || c == b) continue;
double cs_standing = gep_standing(&g->nodes[c].gr, now_ms);
if (cs_standing < GEP_LIKELY_MIN) continue; /* ungrounded ⇒ no pull */
double contribution = g->edges[e].weight * cs_standing * (double)pol;
int existing = gep_corr_index_of(&cs, c);
if (existing >= 0) {
/* same corroborator id reached twice (multi-edge echo): keep the
* strongest-magnitude contribution, do NOT add — one source, one vote */
if (fabs(contribution) > fabs(cs.contrib[existing]))
cs.contrib[existing] = contribution;
} else if (cs.n < GEP_MAX_CORR) { /* beam bound against hub belief nodes */
cs.node_idx[cs.n] = c;
cs.contrib[cs.n] = contribution;
cs.parent[cs.n] = cs.n;
cs.n++;
}
}
if (out_incident) *out_incident = incident;
/* 2. Collapse mutually-derived corroborators (an edge between two of them =
* echo chain / shared derivation) into one independent component. */
for (int e = 0; e < g->n_edges; e++) {
int ia = gep_corr_index_of(&cs, g->edges[e].from);
int ib = gep_corr_index_of(&cs, g->edges[e].to);
if (ia >= 0 && ib >= 0) gep_uf_union(&cs, ia, ib);
}
/* 3. Per independent component, take the MAX-magnitude member (echoes don't
* inflate mass either), split by polarity. Convergent INDEPENDENT support
* = sum over components; independence count = number of components. */
double comp_best[GEP_MAX_CORR];
int comp_root[GEP_MAX_CORR]; int n_comp = 0;
for (int i = 0; i < cs.n; i++) {
int r = gep_uf_find(&cs, i);
int slot = -1;
for (int k = 0; k < n_comp; k++) if (comp_root[k] == r) { slot = k; break; }
if (slot < 0) { slot = n_comp++; comp_root[slot] = r; comp_best[slot] = cs.contrib[i]; }
else if (fabs(cs.contrib[i]) > fabs(comp_best[slot])) comp_best[slot] = cs.contrib[i];
}
double pos = 0.0, neg = 0.0; int np = 0, nn = 0;
uint64_t sig = 1469598103934665603ULL; /* FNV offset — signature of the set */
for (int k = 0; k < n_comp; k++) {
if (comp_best[k] > 0.0) { pos += comp_best[k]; np++; }
else if (comp_best[k] < 0.0) { neg += -comp_best[k]; nn++; }
sig = (sig ^ (uint64_t)comp_root[k]) * 1099511628211ULL;
}
if (out_pos) *out_pos = pos; if (out_neg) *out_neg = neg;
if (out_np) *out_np = np; if (out_nn) *out_nn = nn;
double net = pos - neg;
/* 4. Threshold gate. Convergent independent corroboration must clear BOTH a
* MASS threshold (THETA) and an INDEPENDENCE-count threshold (N_MIN).
* The count gate is the independence guard: echoed support collapses to
* one component and never reaches N_MIN however large the raw fan-in. */
if (net > 0.0 && pos >= GEP_THETA && np >= GEP_N_MIN) {
double mag = tanh(GEP_MAG_GAIN * net);
gep_append(&g->nodes[b].gr, now_ms, +1, mag, sig);
return +1;
}
if (net < 0.0 && neg >= GEP_THETA && nn >= GEP_N_MIN) {
double mag = tanh(GEP_MAG_GAIN * (-net));
gep_append(&g->nodes[b].gr, now_ms, -1, mag, sig);
return -1;
}
/* Sub-threshold: support seen but did not cross. Recorded, transient, no
* lasting shift — exactly Will's "recorded in history but transient". */
if (np > 0 || nn > 0) g->nodes[b].gr.subthreshold_hits++;
return 0;
}
/* Run one consolidation/dream beat over every belief node in the graph. */
static inline GepBeatStats gep_beat(GepGraph* g, int64_t now_ms) {
GepBeatStats st; memset(&st, 0, sizeof st);
for (int b = 0; b < g->n_nodes; b++) {
if (!g->nodes[b].is_belief) continue;
int before = gep_band_rank(gep_standing(&g->nodes[b].gr, now_ms));
double pos, neg; int np, nn, incident;
int r = gep_propagate_node(g, b, now_ms, &pos, &neg, &np, &nn, &incident);
int after = gep_band_rank(gep_standing(&g->nodes[b].gr, now_ms));
if (r > 0) st.strengthened++;
else if (r < 0) st.decayed++;
else if (np > 0 || nn > 0) st.subthreshold++;
else if (incident == 0) st.isolated++; /* sparse-graph reality */
else st.starved++; /* has edges, no grounded neighbor */
if (after > before) st.graduations++;
if (after < before) st.demotions++;
}
return st;
}
#endif /* GEP_CORE_H */
@@ -0,0 +1,232 @@
/* ─────────────────────────────────────────────────────────────────────────
* gep_proof.c — PROOF LEDGER for grounded edge-propagation (task #50).
*
* Self-contained. Builds three scenarios on an in-memory GepGraph that mirrors
* the live EngramStore's flat node/edge arrays, runs the consolidation/dream
* beat (gep_beat), and prints RAW grounding before/after for each:
*
* (A) STRENGTHEN — a conjecture + N independent grounded corroborators.
* Grounding grows past threshold, GRADUATES conjecture→
* likely→grounded, then RELAXES when corroboration stops
* (nothing is settled).
* (B) DECAY — a grounded belief meets N independent CONTRADICTORY
* corroborators. Grounding decays grounded→likely→conjecture.
* (C) INDEPENDENCE GUARD — identical fan-in of N=5, weights, and standings.
* C1: 5 DISTINCT independent corroborators → grounds.
* C2: the SAME support echoed (5 mutually-linked / one node
* repeated) → collapses to 1 independent → does NOT.
*
* Build: cc -std=c11 -O2 -o gep_proof gep_proof.c -lm
* Run: ./gep_proof
* ───────────────────────────────────────────────────────────────────────── */
#include <stdio.h>
#include <stdlib.h>
#include "gep_core.h"
#define T0 1786000000000LL /* fixed base time (ms) — deterministic */
#define BEAT_MS 60000LL /* 60s heartbeat cadence (awareness.el) */
/* Seed a node's grounding with a prior LTP event so it reads as already-grounded
* (a member of the grounded core that gravity radiates from). mag→standing:
* standing = GEP_BASE + mag (event at ~now). */
static void seed_grounded(GepNode* n, double mag, int64_t ts) {
memset(&n->gr, 0, sizeof n->gr);
gep_append(&n->gr, ts, +1, mag, 0);
}
/* Re-anchor every NON-belief node (the corroborators/refuters) as a freshly-
* grounded member of the core AT time `now`. These nodes are, by definition,
* sustained members of the grounded core — each has its OWN ongoing
* corroboration — so their standing must be read as grounded at each beat, not
* left to power-law-decay out of the core between beats. The belief-under-test
* is NEVER re-anchored: its trajectory is driven only by the propagation. */
static void anchor_core(GepGraph* g, int64_t now, double mag) {
for (int i = 0; i < g->n_nodes; i++)
if (!g->nodes[i].is_belief) seed_grounded(&g->nodes[i], mag, now);
}
static void print_node(const char* tag, GepNode* n, int64_t now) {
double s = gep_standing(&n->gr, now);
printf(" %-14s standing=%.4f band=%-10s events=%d subthresh=%d\n",
tag, s, gep_band(s), n->gr.filled, n->gr.subthreshold_hits);
}
/* Run one beat over a single belief node b and print the raw support decomposition. */
static void beat_and_report(GepGraph* g, int b, int64_t now, int beatno,
const char* note) {
anchor_core(g, now, 0.80); /* corroborators stay grounded at each beat */
double s_before = gep_standing(&g->nodes[b].gr, now);
int r_before = gep_band_rank(s_before);
double pos, neg; int np, nn, incident;
int r = gep_propagate_node(g, b, now, &pos, &neg, &np, &nn, &incident);
double s_after = gep_standing(&g->nodes[b].gr, now);
int r_after = gep_band_rank(s_after);
const char* action = (r > 0) ? "LTP (strengthen)"
: (r < 0) ? "LTD (decay)"
: (np || nn) ? "sub-threshold (no shift)"
: (incident == 0) ? "isolated (no edges)"
: "starved (no grounded neighbor)";
printf(" beat %d (t=+%llds) %s\n", beatno,
(long long)((now - T0) / 1000), note ? note : "");
printf(" incident_edges=%d pos_mass=%.4f (n_indep=%d) neg_mass=%.4f (n_indep=%d)"
" THETA=%.2f N_MIN=%d\n",
incident, pos, np, neg, nn, (double)GEP_THETA, GEP_N_MIN);
printf(" -> %-26s standing %.4f (%s) -> %.4f (%s)%s\n",
action, s_before, gep_band(s_before), s_after, gep_band(s_after),
(r_after > r_before) ? " [GRADUATED]"
: (r_after < r_before) ? " [DEMOTED]" : "");
}
/* ── Scenario A — STRENGTHEN + graduation + relaxation ───────────────────── */
static void scenario_A(void) {
printf("\n=== SCENARIO A — STRENGTHEN: convergent independent corroboration ===\n");
/* nodes[0] = the conjecture (belief). nodes[1..8] = independent corroborators,
* each already grounded, each tethered to the conjecture by a weak young
* hebbian-associate edge (weight 0.15 = ENGRAM_HEBB_LINK_W0). The corroborators
* are NOT linked to each other → fully independent. */
static GepNode nodes[9];
static GepEdge edges[8];
memset(nodes, 0, sizeof nodes);
nodes[0].id = "conjecture"; nodes[0].is_belief = 1; /* bare: standing = BASE */
for (int i = 1; i <= 8; i++) {
nodes[i].id = "corroborator";
seed_grounded(&nodes[i], 0.80, T0); /* standing ≈ 0.90 → grounded core */
}
GepGraph g = { nodes, 9, edges, 0 };
printf(" seed: conjecture has NO grounding events; corroborators pre-grounded.\n");
print_node("conjecture", &nodes[0], T0);
/* Beat 1: 3 independent corroborators have grounded up around the conjecture. */
g.n_edges = 0;
for (int i = 1; i <= 3; i++)
edges[g.n_edges++] = (GepEdge){ 0, i, 0.15, +1 };
beat_and_report(&g, 0, T0, 1, "3 independent grounded corroborators appear");
/* Beat 2: the neighborhood fills in — 5 independent corroborators now. */
g.n_edges = 0;
for (int i = 1; i <= 5; i++)
edges[g.n_edges++] = (GepEdge){ 0, i, 0.15, +1 };
beat_and_report(&g, 0, T0 + BEAT_MS, 2, "neighborhood grows to 5 corroborators");
/* Beat 3: support sustained at 5 (grounding refreshed). */
beat_and_report(&g, 0, T0 + 2 * BEAT_MS, 3, "support sustained (5)");
/* Beats 4-6: corroboration REMOVED (neighbors superseded / no longer ground).
* No new events; the collection ages → standing relaxes. Nothing is settled. */
g.n_edges = 0;
beat_and_report(&g, 0, T0 + 12 * BEAT_MS, 4, "corroboration withdrawn (+10min)");
beat_and_report(&g, 0, T0 + 60 * BEAT_MS, 5, "still withdrawn (+1h)");
beat_and_report(&g, 0, T0 + 240 * BEAT_MS, 6, "still withdrawn (+4h)");
printf(" RESULT: grounding grew automatically past threshold and graduated,\n"
" then relaxed once the independent support stopped — living,\n"
" not a latched flag.\n");
}
/* ── Scenario B — DECAY via accreting contradiction ─────────────────────── */
static void scenario_B(void) {
printf("\n=== SCENARIO B — DECAY: convergent independent CONTRADICTION ===\n");
static GepNode nodes[6];
static GepEdge edges[5];
memset(nodes, 0, sizeof nodes);
nodes[0].id = "belief"; nodes[0].is_belief = 1;
/* Seed the belief as already GROUNDED via a strong prior LTP event. */
seed_grounded(&nodes[0], 0.85, T0);
for (int i = 1; i <= 5; i++) {
nodes[i].id = "refuter";
seed_grounded(&nodes[i], 0.80, T0); /* grounded contradictors */
}
GepGraph g = { nodes, 6, edges, 0 };
printf(" seed: belief pre-grounded by a strong prior LTP event.\n");
print_node("belief", &nodes[0], T0);
/* Contradiction accretes over successive beats: 3 then 5 independent grounded
* refuters (polarity -1). Each beat past threshold appends an LTD event.
* Beat 1 runs at the seed instant so the trajectory starts from grounded. */
g.n_edges = 0;
for (int i = 1; i <= 3; i++) edges[g.n_edges++] = (GepEdge){ 0, i, 0.20, -1 };
beat_and_report(&g, 0, T0, 1, "3 independent contradictions");
g.n_edges = 0;
for (int i = 1; i <= 5; i++) edges[g.n_edges++] = (GepEdge){ 0, i, 0.20, -1 };
beat_and_report(&g, 0, T0 + BEAT_MS, 2, "contradiction broadens to 5");
beat_and_report(&g, 0, T0 + 2 * BEAT_MS, 3, "contradiction sustained (5)");
beat_and_report(&g, 0, T0 + 3 * BEAT_MS, 4, "contradiction sustained (5)");
printf(" RESULT: grounding decayed grounded->likely->conjecture under\n"
" convergent independent contradiction. The door never shut\n"
" on the belief; its history is retained (events keep growing).\n");
}
/* ── Scenario C — INDEPENDENCE GUARD ─────────────────────────────────────── */
static void scenario_C(void) {
printf("\n=== SCENARIO C — INDEPENDENCE GUARD (the load-bearing property) ===\n");
printf(" Both sub-cases: N=5 corroborators, edge weight 0.30, corroborator\n"
" standing ~0.90. ONLY difference: whether the 5 are independent.\n");
/* C1 — 5 DISTINCT INDEPENDENT corroborators (no edges among them). */
{
printf("\n -- C1: 5 DISTINCT independent corroborators --\n");
static GepNode nodes[6];
static GepEdge edges[5];
memset(nodes, 0, sizeof nodes);
nodes[0].id = "conjecture"; nodes[0].is_belief = 1;
for (int i = 1; i <= 5; i++) { nodes[i].id = "corr"; seed_grounded(&nodes[i], 0.80, T0); }
for (int i = 1; i <= 5; i++) edges[i-1] = (GepEdge){ 0, i, 0.30, +1 };
GepGraph g = { nodes, 6, edges, 5 };
print_node("conjecture", &nodes[0], T0);
beat_and_report(&g, 0, T0, 1, "5 independent corroborators (no inter-links)");
}
/* C2 — the SAME support echoed: 5 corroborators that are all mutually linked
* (a derivation clique — one source echoed through the chain). Same fan-in to
* the conjecture, same weights, same standings. Union-find collapses them to
* ONE independent component → below N_MIN → NO strengthening. */
{
printf("\n -- C2: 5 corroborators, but mutually-linked (echo of ONE source) --\n");
static GepNode nodes[6];
static GepEdge edges[9]; /* 5 to conjecture + 4 chaining corr1..corr5 */
memset(nodes, 0, sizeof nodes);
nodes[0].id = "conjecture"; nodes[0].is_belief = 1;
for (int i = 1; i <= 5; i++) { nodes[i].id = "corr"; seed_grounded(&nodes[i], 0.80, T0); }
int ne = 0;
for (int i = 1; i <= 5; i++) edges[ne++] = (GepEdge){ 0, i, 0.30, +1 };
/* chain corr1-corr2-corr3-corr4-corr5: they are the same source echoed */
for (int i = 1; i <= 4; i++) edges[ne++] = (GepEdge){ i, i+1, 0.30, +1 };
GepGraph g = { nodes, 6, edges, ne };
print_node("conjecture", &nodes[0], T0);
beat_and_report(&g, 0, T0, 1, "5 echoed (mutually-linked) corroborators");
}
/* C3 — degenerate echo: literally ONE corroborator reached by 5 parallel edges. */
{
printf("\n -- C3: ONE corroborator, reached by 5 parallel edges --\n");
static GepNode nodes[2];
static GepEdge edges[5];
memset(nodes, 0, sizeof nodes);
nodes[0].id = "conjecture"; nodes[0].is_belief = 1;
nodes[1].id = "corr"; seed_grounded(&nodes[1], 0.80, T0);
for (int i = 0; i < 5; i++) edges[i] = (GepEdge){ 0, 1, 0.30, +1 };
GepGraph g = { nodes, 2, edges, 5 };
print_node("conjecture", &nodes[0], T0);
beat_and_report(&g, 0, T0, 1, "same node, 5 parallel edges");
}
printf("\n RESULT: identical raw fan-in (5) and mass inputs; C1 grounds because\n"
" the corroboration is INDEPENDENT (5 components), C2/C3 do not\n"
" because it collapses to ONE source. Circular self-reinforcement\n"
" cannot manufacture grounding.\n");
}
int main(void) {
printf("GROUNDED EDGE-PROPAGATION — PROOF LEDGER (task #50)\n");
printf("constants: BASE=%.2f LIKELY_MIN=%.2f GROUNDED_MIN=%.2f "
"N_MIN=%d THETA=%.2f D=%.1f\n",
(double)GEP_BASE, (double)GEP_LIKELY_MIN, (double)GEP_GROUNDED_MIN,
GEP_N_MIN, (double)GEP_THETA, (double)GEP_DECAY_D);
scenario_A();
scenario_B();
scenario_C();
printf("\nDONE.\n");
return 0;
}
@@ -0,0 +1,29 @@
//
// server.route.patch.el GATED route for task #50, for engram/src/server.el.
// NOT APPLIED. Exposes the engram_ground_propagate native over HTTP so the
// soul's consolidation beat can fire one grounded edge-propagation pass.
//
// [1] New handler add beside route_strengthen (server.el ~line 194).
// Mutation (appends grounding events, updates confidence), so it is gated
// on _auth via check_auth_ok, exactly like /api/edges. Persists once after
// the beat the whole point of running propagation as one batched beat
// rather than per-node is to pay the snapshot cost a single time.
fn route_ground_propagate(method: String, path: String, body: String) -> String {
if !check_auth_ok(method, body) { return err_json("unauthorized") }
let tel: String = engram_ground_propagate() // native one beat over the store
let saved: Int = persist_canonical()
return tel // gep_* telemetry JSON straight through
}
// [2] Dispatch register in handle_request (server.el ~line 461, next to the
// /api/strengthen arm):
//
// if str_eq(method, "POST") && (str_eq(clean, "/api/ground/propagate")) {
// return route_ground_propagate(method, clean, body)
// }
//
// [3] Native declaration engram_ground_propagate must be declared as an
// extern runtime builtin (el_runtime.h) and seed-wrapped (el_seed.c /
// el_seed.h __engram_ground_propagate) so the EL side can call it, same as
// engram_strengthen / engram_hebb_drain_json.