Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f3ddb8b8d |
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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()))
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()))
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 1286–1288):
|
||||
|
||||
```
|
||||
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 ⬇ |
|
||||
| 3–4 | 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.
|
||||
Reference in New Issue
Block a user