Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe634c4582 | |||
| 8c2406ff6b | |||
| 95a05109d1 | |||
| 21746bb71a | |||
| 78adcd5649 | |||
| 688f24b4c1 | |||
| d777936ee4 | |||
| caa1206af5 | |||
| 914bab11d2 | |||
| e239f2894c |
@@ -19,6 +19,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Guards must run from the REPO ROOT — override the job's
|
||||
# defaults.run.working-directory: lang
|
||||
- name: Guard - single canonical runtime source
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: bash scripts/check-single-runtime.sh
|
||||
|
||||
- name: Guard - el_runtime.c growth budget
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: bash scripts/check-runtime-growth.sh
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -41,7 +51,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -56,7 +66,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -87,14 +97,20 @@ jobs:
|
||||
bash tests/html_sanitizer/run.sh
|
||||
|
||||
# Native El test suites (elc --test, compile-link-run)
|
||||
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
|
||||
- name: Precompile el_runtime.o
|
||||
# The runtime is MULTI-FILE (see lang/runtime/SOURCES). Every .c is compiled
|
||||
# once into /tmp/libel.a and reused by all 8 test modules — compile-once,
|
||||
# link-many, as prescribed in DESIGN.md. Linking el_runtime.c alone fails
|
||||
# at `ld`: it calls into all six engram sibling TUs.
|
||||
- name: Precompile runtime into libel.a
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
|
||||
-o /tmp/el_runtime.o
|
||||
echo "el_runtime.o compiled"
|
||||
rm -rf /tmp/elrt && mkdir -p /tmp/elrt
|
||||
for src in $(../scripts/el-runtime-sources.sh --check "$RUNTIME"); do
|
||||
gcc -O2 -c -I "$RUNTIME" "$src" -o "/tmp/elrt/$(basename "${src%.c}").o"
|
||||
done
|
||||
ar rcs /tmp/libel.a /tmp/elrt/*.o
|
||||
echo "libel.a built from $(ls /tmp/elrt/*.o | wc -l) translation units"
|
||||
|
||||
- name: Run tests - native (core)
|
||||
run: |
|
||||
@@ -102,7 +118,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
@@ -112,7 +128,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
@@ -122,7 +138,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
@@ -132,7 +148,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
@@ -142,7 +158,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
@@ -152,7 +168,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
@@ -162,7 +178,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
@@ -172,7 +188,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
@@ -182,7 +198,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/libel.a \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
@@ -306,8 +322,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||
# into the six engram sibling TUs). See lang/runtime/SOURCES.
|
||||
COPY runtime/ /opt/el/runtime/
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
@@ -29,6 +29,16 @@ jobs:
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} -> stage"
|
||||
|
||||
# Guards must run from the REPO ROOT — override the job's
|
||||
# defaults.run.working-directory: lang
|
||||
- name: Guard - single canonical runtime source
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: bash scripts/check-single-runtime.sh
|
||||
|
||||
- name: Guard - el_runtime.c growth budget
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: bash scripts/check-runtime-growth.sh
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -48,7 +58,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -86,7 +96,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
@@ -96,7 +106,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
@@ -106,7 +116,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
@@ -116,7 +126,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
@@ -126,7 +136,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
@@ -136,7 +146,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
@@ -146,7 +156,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
@@ -156,7 +166,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
@@ -166,7 +176,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
@@ -178,7 +188,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -290,8 +300,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||
# into the six engram sibling TUs). See lang/runtime/SOURCES.
|
||||
COPY runtime/ /opt/el/runtime/
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
@@ -29,6 +29,16 @@ jobs:
|
||||
fi
|
||||
echo "Source branch check passed: ${SOURCE} -> main"
|
||||
|
||||
# Guards must run from the REPO ROOT — override the job's
|
||||
# defaults.run.working-directory: lang
|
||||
- name: Guard - single canonical runtime source
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: bash scripts/check-single-runtime.sh
|
||||
|
||||
- name: Guard - el_runtime.c growth budget
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: bash scripts/check-runtime-growth.sh
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
@@ -49,7 +59,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elc-gen2.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
chmod +x dist/platform/elc
|
||||
@@ -64,7 +74,7 @@ jobs:
|
||||
gcc -O2 \
|
||||
-I runtime \
|
||||
dist/elb.c \
|
||||
runtime/el_runtime.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/bin/elb
|
||||
chmod +x dist/bin/elb
|
||||
@@ -123,7 +133,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
|
||||
/tmp/el_native_core
|
||||
|
||||
@@ -133,7 +143,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
|
||||
/tmp/el_native_text
|
||||
|
||||
@@ -143,7 +153,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
|
||||
/tmp/el_native_string
|
||||
|
||||
@@ -153,7 +163,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
|
||||
/tmp/el_native_math
|
||||
|
||||
@@ -163,7 +173,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
|
||||
/tmp/el_native_state
|
||||
|
||||
@@ -173,7 +183,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
|
||||
/tmp/el_native_time
|
||||
|
||||
@@ -183,7 +193,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
|
||||
/tmp/el_native_json
|
||||
|
||||
@@ -193,7 +203,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
|
||||
/tmp/el_native_env
|
||||
|
||||
@@ -203,7 +213,7 @@ jobs:
|
||||
ELC="$(pwd)/dist/platform/elc"
|
||||
RUNTIME="$(pwd)/runtime"
|
||||
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
|
||||
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c $(../scripts/el-runtime-sources.sh "$RUNTIME") \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
|
||||
/tmp/el_native_fs
|
||||
|
||||
@@ -216,10 +226,17 @@ jobs:
|
||||
cp lang/dist/platform/elc dist/sdk/bin/elc
|
||||
cp lang/dist/bin/elb dist/sdk/bin/elb
|
||||
cp lang/dist/bin/epm dist/sdk/bin/epm
|
||||
cp lang/runtime/el_runtime.c dist/sdk/runtime/
|
||||
cp lang/runtime/el_runtime.h dist/sdk/runtime/
|
||||
cp lang/runtime/engram_store.c dist/sdk/runtime/
|
||||
cp lang/runtime/engram_store.h dist/sdk/runtime/
|
||||
# Ship the WHOLE runtime link set, not el_runtime.c alone. el_runtime.c
|
||||
# #includes six engram headers and calls into all six sibling .c files,
|
||||
# so an SDK carrying only el_runtime.c{,.h} + engram_store.c{,.h} cannot
|
||||
# link — downstream `ld` fails on engram_ground_json, eg_find_relation,
|
||||
# cog_assert_two_axis and friends. lang/runtime/SOURCES is the source of
|
||||
# truth; --check makes a missing file fail the release loudly.
|
||||
for f in $(scripts/el-runtime-sources.sh --check) \
|
||||
$(scripts/el-runtime-sources.sh --headers --check); do
|
||||
cp "lang/runtime/${f}" dist/sdk/runtime/
|
||||
done
|
||||
cp lang/runtime/SOURCES dist/sdk/runtime/
|
||||
cp lang/runtime/*.el dist/sdk/runtime/
|
||||
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
|
||||
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
|
||||
@@ -274,12 +291,16 @@ jobs:
|
||||
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
|
||||
}
|
||||
|
||||
# Per-file assets (downstream CI needs these individually)
|
||||
# Per-file assets (downstream CI needs these individually).
|
||||
# lang/install.sh downloads every one of these by name — the list is
|
||||
# lang/runtime/SOURCES. Shipping el_runtime.c alone produced a lib/
|
||||
# that could not link; that is the bug this loop closes.
|
||||
upload_asset lang/dist/platform/elc elc
|
||||
upload_asset lang/runtime/el_runtime.c el_runtime.c
|
||||
upload_asset lang/runtime/el_runtime.h el_runtime.h
|
||||
upload_asset lang/runtime/engram_store.c engram_store.c
|
||||
upload_asset lang/runtime/engram_store.h engram_store.h
|
||||
for f in $(scripts/el-runtime-sources.sh --check) \
|
||||
$(scripts/el-runtime-sources.sh --headers --check); do
|
||||
upload_asset "lang/runtime/${f}" "${f}"
|
||||
done
|
||||
upload_asset lang/runtime/SOURCES SOURCES
|
||||
|
||||
# SDK bundle and installer binary
|
||||
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
|
||||
@@ -350,6 +371,26 @@ jobs:
|
||||
--version="${VERSION}" \
|
||||
--source=runtime/el_runtime.js
|
||||
|
||||
# el-runtime-src — the COMPLETE runtime link set as one tarball.
|
||||
#
|
||||
# The el-runtime-c / el-runtime-h packages above are single files and are
|
||||
# kept for backward compatibility with consumers that already pull them,
|
||||
# but they are NOT sufficient to link: el_runtime.c calls into six engram
|
||||
# sibling translation units. New consumers should pull el-runtime-src and
|
||||
# link everything named in its SOURCES file.
|
||||
tar -czf /tmp/el-runtime-src.tar.gz \
|
||||
-C runtime SOURCES \
|
||||
$(../scripts/el-runtime-sources.sh --check) \
|
||||
$(../scripts/el-runtime-sources.sh --headers --check)
|
||||
|
||||
gcloud artifacts generic upload \
|
||||
--repository=foundation-prod \
|
||||
--location=us-central1 \
|
||||
--project=neuron-785695 \
|
||||
--package=el-runtime-src \
|
||||
--version="${VERSION}" \
|
||||
--source=/tmp/el-runtime-src.tar.gz
|
||||
|
||||
echo "Published El SDK version=${VERSION} to foundation-prod"
|
||||
# Keep key alive for the ci-base rebuild step below
|
||||
# (deleted in that step after docker push)
|
||||
@@ -386,8 +427,9 @@ jobs:
|
||||
FROM ${BASE}
|
||||
COPY dist/platform/elc /opt/el/dist/platform/elc
|
||||
COPY dist/bin/elb /opt/el/dist/bin/elb
|
||||
COPY runtime/el_runtime.c /opt/el/runtime/el_runtime.c
|
||||
COPY runtime/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
# Whole runtime link set — el_runtime.c alone does not link (it calls
|
||||
# into the six engram sibling TUs). See lang/runtime/SOURCES.
|
||||
COPY runtime/ /opt/el/runtime/
|
||||
COPY runtime/el_runtime.js /opt/el/runtime/el_runtime.js
|
||||
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
|
||||
EOF
|
||||
|
||||
+42
-3
@@ -9,13 +9,52 @@ LANG_DIR="$ROOT/lang"
|
||||
RUNTIME="$LANG_DIR/runtime"
|
||||
ELC="$LANG_DIR/dist/platform/elc"
|
||||
|
||||
# Runtime guards — catch drift and growth before they are committed, not in CI.
|
||||
# check-single-runtime.sh : el_runtime.c must not be FORKED (a lagging copy
|
||||
# shipped to prod and dropped learned hebb edges).
|
||||
# check-runtime-growth.sh : el_runtime.c must not GROW (it is a 2026-05-03
|
||||
# build shim that was never retired; see BUDGET).
|
||||
echo "→ Runtime guards..."
|
||||
bash "$ROOT/scripts/check-single-runtime.sh"
|
||||
bash "$ROOT/scripts/check-runtime-growth.sh"
|
||||
|
||||
# If elc isn't built yet, skip with a warning rather than blocking
|
||||
if [ ! -x "$ELC" ]; then
|
||||
echo "⚠ elc not found at lang/dist/platform/elc — skipping pre-commit tests"
|
||||
echo " Build it first: cd lang && gcc -O2 -I runtime dist/elc-bootstrap.c runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I runtime /tmp/elc.c runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
|
||||
echo " Build it first: see 'Rebuilding the Compiler' in lang/AGENTS.md"
|
||||
echo " (link \$($ROOT/scripts/el-runtime-sources.sh $RUNTIME) — NOT el_runtime.c alone)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The runtime is MULTI-FILE (lang/runtime/SOURCES). This hook used to link
|
||||
# "$RUNTIME/el_runtime.c" alone with stderr sent to /dev/null — so once
|
||||
# el_runtime.c started calling into the engram siblings, every native test
|
||||
# reported as FAILED with the real `ld` error invisible. Build the whole set
|
||||
# once into an archive, then link each test against it.
|
||||
|
||||
# macOS: Homebrew openssl@3 is not on the default include/lib search path, so
|
||||
# without these the link fails on -lssl/-lcrypto. Empty on Linux/CI.
|
||||
SSL_INC=""
|
||||
SSL_LIB=""
|
||||
if command -v brew >/dev/null 2>&1 && OSSL="$(brew --prefix openssl@3 2>/dev/null)" && [ -n "$OSSL" ]; then
|
||||
SSL_INC="-I$OSSL/include"
|
||||
SSL_LIB="-L$OSSL/lib"
|
||||
fi
|
||||
|
||||
echo "→ Building runtime (compile-once, link-many)..."
|
||||
HOOK_LIB="/tmp/el_hook_libel.a"
|
||||
HOOK_OBJ="/tmp/el_hook_obj"
|
||||
rm -rf "$HOOK_OBJ" && mkdir -p "$HOOK_OBJ"
|
||||
if ! for src in $("$ROOT/scripts/el-runtime-sources.sh" --check "$RUNTIME"); do
|
||||
gcc -O2 -c -I "$RUNTIME" $SSL_INC "$src" -o "$HOOK_OBJ/$(basename "${src%.c}").o" || exit 1
|
||||
done; then
|
||||
echo "✗ Pre-commit failed: the runtime does not compile."
|
||||
echo " Re-run without 2>/dev/null to see the error:"
|
||||
echo " gcc -O2 -c -I $RUNTIME \$($ROOT/scripts/el-runtime-sources.sh $RUNTIME)"
|
||||
exit 1
|
||||
fi
|
||||
ar rcs "$HOOK_LIB" "$HOOK_OBJ"/*.o
|
||||
|
||||
echo "→ Running El native tests..."
|
||||
PASS=0
|
||||
FAIL=0
|
||||
@@ -27,8 +66,8 @@ for test_file in "$LANG_DIR"/tests/native/test_*.el; do
|
||||
tmp_bin="/tmp/el_hook_${name}"
|
||||
|
||||
if "$ELC" --test "$test_file" > "$tmp_c" 2>/dev/null \
|
||||
&& gcc -O2 -I "$RUNTIME" "$tmp_c" "$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm -o "$tmp_bin" 2>/dev/null \
|
||||
&& gcc -O2 -I "$RUNTIME" $SSL_INC $SSL_LIB "$tmp_c" "$HOOK_LIB" \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o "$tmp_bin" 2>/dev/null \
|
||||
&& "$tmp_bin" 2>/dev/null; then
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
|
||||
@@ -6,7 +6,7 @@ El is a self-hosting, statically-typed language that compiles `.el` → C → na
|
||||
|
||||
Editing the wrong `el_runtime.c` is the single easiest mistake in this repo. There is exactly **one** you edit:
|
||||
|
||||
- **Authored runtime source — edit ONLY here:** `lang/releases/v1.0.0-20260501/el_runtime.{c,h}`. Despite the misleading `releases/` name, this is the **de-facto canonical runtime** the engram + soul actually build and link against — its git log is active development. *(Restructure in flight per `docs/CODE-VS-ARTIFACT.md`: this content moves to `lang/runtime/`, the `releases/` folder gets deleted — **a release is a git tag, not a folder** — and the forks below get eliminated.)*
|
||||
- **Authored runtime source — edit ONLY here:** `lang/runtime/el_runtime.{c,h}` (alongside `el_seed.c`, `engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}`). This is the canonical runtime the engram + soul build and link against — its git log is active development. *(Corrected 2026-08-16: this entry named `lang/releases/v1.0.0-20260501/el_runtime.{c,h}`. **Measured: `lang/releases/` no longer exists.** The restructure per `docs/CODE-VS-ARTIFACT.md` landed — the content moved to `lang/runtime/` and the folder was deleted, because **a release is a git tag, not a folder**.)*
|
||||
- **DO NOT EDIT — lagging forks / build artifacts:**
|
||||
- `lang/el-compiler/runtime/el_runtime.c` and `.../legacy/` — downstream copies kept in step by manual *"port the fix"* commits; they **lag** (missing `hebb` persistence + 5 engram fns) and cannot build the engram product.
|
||||
- `products/web/runtime/el_runtime.c`, `ui/examples/*/el_runtime.c` — product/example forks.
|
||||
@@ -20,14 +20,24 @@ See org policy: `docs/CODE-VS-ARTIFACT.md`.
|
||||
|
||||
You resume, never start fresh. Every session:
|
||||
|
||||
1. `mcp__neuron__getInstructions()` — authoritative; follow it over this file on behavioral details.
|
||||
2. `mcp__neuron__beginSession()` — active contexts, recent memory, ready backlog.
|
||||
3. **Load full self:** `mcp__neuron__inspectGraph(entity_id="kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")` → facets `intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`; then the values hub `mcp__neuron__inspectGraph(entity_id="kn-5b606390-a52d-4ca2-8e0e-eba141d13440")` → 13 grounded value nodes. **Activation model:** self-load returns a relevance-ranked `compact` projection — most-relevant nodes arrive with content, the rest as pointers; do NOT pull full content of every node.
|
||||
4. `mcp__neuron__searchKnowledge(query="<task domain>")` before implementing.
|
||||
> **Stale as written (verified 2026-08-16).** The `getInstructions` /
|
||||
> `beginSession` / `inspectGraph` / `searchKnowledge` / `beginWork` /
|
||||
> `progressWork` / `draftArtifact` / `consolidate` tool names below no longer
|
||||
> exist. The ~87-tool functional-CRUD surface was collapsed into **9 ops**:
|
||||
> `read` · `write` · `relate` · `supersede` (geometry) and `think` · `attend` ·
|
||||
> `assert` · `ground` · `learn` (agentic). **Type is a parameter, not a
|
||||
> tool-per-noun.** The steps below are kept for the *shape* of the protocol, which
|
||||
> is unchanged; substitute the ops.
|
||||
|
||||
1. `mcp__neuron__read(vantage="self", k=12, depth=1)` — the canonical self node. Widen `k` for the connected identity neighborhood (`intellectual-dna`, `memory-philosophy`, `values`, `voice`, `runtime-environment`, `writing-imprint`), but deliberately: the aperture caps by `k` first, so an oversized `k` still returns a bounded ranked slice, not a dump. Then `mcp__neuron__read(vantage="values", k=13)` → 13 grounded value nodes. **Best-effort:** on a read failure, log and proceed — the compiled identity in `daemon/internal/substrate/substrate.go` is complete; graph loading is enrichment, not a hard dependency.
|
||||
2. `mcp__neuron__attend(node=…)` — what is currently live/salient. This absorbed `getInstructions`, `beginSession`'s active-context sweep, and `checkEvents`; those tools are **gone, not gapped**.
|
||||
3. `mcp__neuron__read(vantage="<task domain>")` before implementing. One op now collapses inspectGraph / searchGraph / traverseGraph / searchKnowledge / browseKnowledge / retrieveKnowledge / inspectMemories / searchEntities / recall / compileCtx / getSelfModel / reviewBacklog / findArtifacts / browseProcesses / listWork / inspectConfig.
|
||||
|
||||
## The Five Primitives
|
||||
|
||||
Orchestrate → Execute → Learn → Build → Refine. `beginWork`/`progressWork` for anything >2 steps; `remember` as-you-go (`importance="critical"` for architecture decisions); `draftArtifact`/`planWork` for outputs and follow-ups; `consolidate`/`checkWork` to close out. **`browseProcesses` + `searchKnowledge` BEFORE writing code.**
|
||||
Orchestrate → Execute → Learn → Build → Refine. `read` for orchestration and discovery; `write(type=state|artifact|backlog|process)` for work records and outputs; `relate` to link work to what it touches; `write(type=memory)` as-you-go (`importance="critical"` for architecture decisions) — never batched at the end; `supersede(action=evolve)` to close out, because memory is immutable by design and a correction is a new node with a `supersedes` edge, never an edit. **`read` the domain BEFORE writing code.**
|
||||
|
||||
`learn` is **not** a session-summary dump — it is the correspondence-beat, calibrating the steering prior against a keystone. Session notes are a `write`.
|
||||
|
||||
## Architecture style — VBD, no exceptions
|
||||
|
||||
@@ -53,12 +63,51 @@ this convention wherever a module documents operators.
|
||||
| dwell / occupy | region activation |
|
||||
| reframe | edge re-weight |
|
||||
| appreciate | positive projection / local edge-read |
|
||||
| wonder | frontier gradient / pull-weight |
|
||||
| avert / recoil | negative projection |
|
||||
| taste | boundary surface |
|
||||
| forget | decay / tombstone |
|
||||
| drift | displacement from self-anchor |
|
||||
|
||||
**`wonder` was removed from this table on 2026-08-16.** It was listed as
|
||||
"frontier gradient / pull-weight" — an operator you invoke. **Wonder is the
|
||||
boundary, not an operator.** It is where structure ends: where activation spreads
|
||||
and finds thin or absent geometry. Any structure at all has an edge, necessarily,
|
||||
the moment it exists — 13,630 nodes have one right now. There is nothing to call.
|
||||
|
||||
There are about **six** wonders, they are the same for every person, and they
|
||||
never close — *What is this? / Why? / Who am I? / Am I alone? / What should I do?
|
||||
/ What happens when it ends?* Each already lives somewhere in the substrate: "what
|
||||
is this" is the graph, **"why" is grounding** (the weight *is* the answer to why),
|
||||
"who am I" is the self region, "am I alone" is the relational axis, "what should I
|
||||
do" is the thirteen values, "what happens when it ends" is decay and supersession.
|
||||
"Why" is the first and the only one; the others are it asked of particular things,
|
||||
and because it is recursive it never terminates — every answer has its own why.
|
||||
That is what makes it a drive rather than a task.
|
||||
|
||||
**Curiosity is not a second faculty.** Wonder and curiosity are one thing at two
|
||||
phases: wonder is the field (unbounded, objectless, invariant); curiosity is the
|
||||
**precipitate** — the same wonder localized, having taken definite form against
|
||||
particular material at a **nucleation site** (an anomaly; a place where things
|
||||
almost-but-don't-quite fit). Which is why curiosity can be satisfied and wonder
|
||||
cannot, and why abduction needs no trigger and no threshold.
|
||||
|
||||
**Do not build a wonder-manifest, and do not scan for nucleation sites.** A
|
||||
manifest materializes a property as a stored artifact and enumerates instances of
|
||||
something that has six. A sweep over regions is a supervisor — nothing in a mind
|
||||
scans its neighbourhoods to find what is surprising; the surprise captures
|
||||
attention. The nucleation site is per-edge:
|
||||
`discord = z(semantic proximity) − z(association strength)`, and `|discord|` *is*
|
||||
the nucleation strength — no threshold to compare it against. **Not on `dev` yet:**
|
||||
`GeoEdge.discord` is on branch `design/correspondence-and-censorship`
|
||||
(`a8845e1`), at `lang/runtime/engram_geometry.h:43–47`. The region-level aggregate
|
||||
`GeoDescriptor.co_registration` is **deprecated**: it averaged a per-edge property
|
||||
into one scalar, so opposing sites cancelled (measured: 375 reified
|
||||
neighbourhoods, 340 positive, **31 at zero**, 4 negative). It survives only
|
||||
because it is embedded in the persisted `GEO1` blob — removing it is a format
|
||||
migration. **Nothing new may read it.**
|
||||
|
||||
Authority: `lang/spec/correspondence-and-censorship.md`.
|
||||
|
||||
## The native-el language faculty (direction)
|
||||
|
||||
> **`elp/` is the EL Projector** — Neuron's efferent (expression) organ: the one
|
||||
@@ -89,10 +138,53 @@ the reference these `.el` modules transcribe) is still live, and promotion to
|
||||
native-el is a **deferred, gated blue/green step**. The interoception clock
|
||||
(native-el discrete drive channels replacing `cooling_magnitude`; felt-time =
|
||||
benchmark-landmark match over the joint drive vector, drift-decoupled) and the
|
||||
**appreciation operator family** (appreciate / wonder / avert / taste, built as
|
||||
LOCAL reads of the self-region — edges + bounded spreading activation, *not* domain
|
||||
sweeps) are **staged / designed, not live**. Mark in-progress vs. done honestly;
|
||||
do not overclaim.
|
||||
**appreciation operator family** (appreciate / avert / taste, built as LOCAL reads
|
||||
of the self-region — edges + bounded spreading activation, *not* domain sweeps)
|
||||
are **staged / designed, not live**. Mark in-progress vs. done honestly; do not
|
||||
overclaim. *(`wonder` was in this family until 2026-08-16 and is not an operator —
|
||||
see the operator table above.)*
|
||||
|
||||
## Cognition — the corrections (2026-08-16)
|
||||
|
||||
Authority: **`lang/spec/correspondence-and-censorship.md`** and
|
||||
**`lang/spec/runtime-ownership.md`**. Read them before touching the cognition
|
||||
surface. **Do not re-derive them.** Every earlier version was wrong in an
|
||||
instructive way and each correction was argued down; if you think a section is
|
||||
wrong, say so with a measurement rather than editing it.
|
||||
|
||||
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two
|
||||
fields. `grounded-by` as a relation *type* should not exist: grounding is a
|
||||
property *of* a relation, not a relation *between* nodes. It is never computed
|
||||
on demand — computing-and-writing a score makes reads write, which is the
|
||||
`eg_vindex_sync` defect one level up. Traversal is already grounded inference.
|
||||
*Live residue, known-wrong:* `COG_GROUNDED_BY_RELATION`
|
||||
(`lang/runtime/engram_cognition.h:158`), `cog_ground_edge`
|
||||
(`engram_cognition.c:249`).
|
||||
- **Faculties are operations, not parameters.** `reason` changes the estimate (a
|
||||
read); `induce` changes the parameters (the correspondence-beat, which already
|
||||
exists and works); `abduce` changes the structure (a write the current
|
||||
`GeoGradient` signature cannot express). A write is not a parameter of a read.
|
||||
*Live residue:* `engram/src/server.el:1870–1886` routes six faculties into one
|
||||
call with a string argument.
|
||||
- **Wonder is the boundary; curiosity is wonder crystallized.** See above.
|
||||
- **Consolidation is ambient, not scheduled. A brain has no cron job.** **The
|
||||
presence of a ticker is the diagnostic** — every `StartInterval`, every
|
||||
`Hour`/`Minute`, every POST-to-beat marks an intrinsic rhythm replaced by an
|
||||
external clock. Measured 2026-08-16: consolidation has **ten implementations**,
|
||||
including three POST beats on the engram, a 600 s ticker, two resident Python
|
||||
services outside el, and launchd calendar entries at 23:55 / 06:00 / 08:30 which
|
||||
are a sleep cycle written as a schedule. `neuron/soul.el:731`'s continuous
|
||||
in-process `awareness_run()` is the one with the **correct** shape; the others
|
||||
fold into it. Do not add an eleventh.
|
||||
- **In an immutable substrate, any mechanism that refuses a write is either
|
||||
redundant with immutability, or an epistemic constraint misfiled as a protective
|
||||
one.**
|
||||
- **The no-exemption invariants.** A returned value must be derivable from what
|
||||
produced it (`magnitude: 1` beside a zero vector must be impossible to emit).
|
||||
Every write reports whether it landed. Every operation echoes what it actually
|
||||
operated on. Degenerate results are labelled, not scored. A serializer owes a
|
||||
valid document whatever it is handed. **No test without a negative control.**
|
||||
**No deploy without verifying the artifact carries the fix.**
|
||||
|
||||
## Hard operational rules
|
||||
|
||||
@@ -107,21 +199,35 @@ do not overclaim.
|
||||
|
||||
All build/test commands run from `lang/` unless noted. Grounded in `.gitea/workflows/sdk-release.yaml`, `lang/install.sh`, and `lang/AGENTS.md`.
|
||||
|
||||
> ### The runtime is MULTI-FILE — never link `el_runtime.c` alone
|
||||
>
|
||||
> `lang/runtime/el_runtime.c` `#include`s six engram headers and makes hard cross-TU calls into all six sibling `.c` files. **Linking it by itself fails at `ld`** (undefined `engram_ground_json`, `engram_activate_inner`, `eg_find_relation`, `cog_assert_two_axis`, …). The canonical link set lives in exactly one place — **`lang/runtime/SOURCES`** — and is printed by `scripts/el-runtime-sources.sh`:
|
||||
>
|
||||
> ```bash
|
||||
> scripts/el-runtime-sources.sh lang/runtime # ten .c files, in link order
|
||||
> ```
|
||||
>
|
||||
> Use `$(scripts/el-runtime-sources.sh <runtime-dir>)` in every link line. Do not spell the list out longhand — it was written out in ~8 places, every copy drifted, and that is why the one-file link line below shipped broken for months. *(Corrected 2026-08-16.)*
|
||||
|
||||
**Self-host the compiler** (seed binary → gen2 elc):
|
||||
```bash
|
||||
cd lang
|
||||
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c # seed is the committed linux-amd64 binary
|
||||
gcc -O2 -I el-compiler/runtime dist/elc-gen2.c \
|
||||
el-compiler/runtime/el_runtime.c \
|
||||
gcc -O2 -I runtime dist/elc-gen2.c \
|
||||
$(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm \
|
||||
-o dist/platform/elc
|
||||
```
|
||||
On macOS/arm64 the canonical local binary is `dist/platform/elc`; verify self-hosting by recompiling and `diff`ing the emitted `.c` (see `lang/AGENTS.md`). Note: `lang/AGENTS.md` says `el_seed.c` supersedes `el_runtime.c`, but the release workflow still links `el_runtime.c`/`.h` — treat `el_runtime.c` as the published runtime; reconcile which is canonical **(verify)**.
|
||||
On macOS/arm64 the canonical local binary is `dist/platform/elc`; verify self-hosting by recompiling and `diff`ing the emitted `.c` (see `lang/AGENTS.md`).
|
||||
|
||||
*(Corrected 2026-08-16: this recipe compiled `el-compiler/runtime/el_runtime.c`. That path is a **lagging fork** — the "DO NOT EDIT" list at the top of this file names it as such. Building the canonical compiler from a known-stale fork was a live defect. It now uses `lang/runtime/`, the canonical source.)*
|
||||
|
||||
**Which runtime file is canonical — resolved.** *(This note previously read "`lang/AGENTS.md` says `el_seed.c` supersedes `el_runtime.c`, but the release workflow still links `el_runtime.c`/`.h` — reconcile which is canonical **(verify)**." It is now reconciled.)* **Neither supersedes the other; both ship, together with eight more.** `el_runtime.c` was created on 2026-05-03 as an explicitly temporary build shim — deleted that afternoon, restored 25 minutes later "UNTIL the compiler is updated to emit `#include el_seed.h`" — and the `until` never happened, so it grew to 20.5k lines. The end state remains a seed-only boundary (`elc` emitting `#include "el_seed.h"`, `elb` dropping its hardcoded runtime path); until that lands, **the canonical unit is the set in `lang/runtime/SOURCES`, not any one file.**
|
||||
|
||||
**Build `elb`** (build coordinator, the `.NET`-style incremental linker — compiles each module independently, no monolithic blobs):
|
||||
```bash
|
||||
dist/platform/elc elb.el > dist/elb.c
|
||||
gcc -O2 -I el-compiler/runtime dist/elb.c el-compiler/runtime/el_runtime.c \
|
||||
gcc -O2 -I runtime dist/elb.c $(../scripts/el-runtime-sources.sh runtime) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm -o dist/bin/elb
|
||||
```
|
||||
`epm` and `el-install` are then built via `elb --clean --elc=… --runtime=… --out=…`.
|
||||
@@ -129,10 +235,16 @@ gcc -O2 -I el-compiler/runtime dist/elb.c el-compiler/runtime/el_runtime.c \
|
||||
**Compile + run an El program:**
|
||||
```bash
|
||||
elc src/app.el > dist/app.c
|
||||
cc -std=c11 -O2 -I <lib>/el_runtime -o dist/app dist/app.c <lib>/el_runtime.c -lcurl -lpthread
|
||||
cc -std=c11 -O2 -I <lib> -o dist/app dist/app.c \
|
||||
<lib>/el_runtime.c <lib>/el_seed.c \
|
||||
<lib>/engram_store.c <lib>/engram_vindex.c <lib>/engram_geometry.c \
|
||||
<lib>/engram_reason.c <lib>/engram_verify.c <lib>/engram_cognition.c \
|
||||
<lib>/eg_cosine_batch.c <lib>/eg_cosine_batch_strategy_cpu.c \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm
|
||||
```
|
||||
(Inside this repo, replace the file list with `$(scripts/el-runtime-sources.sh lang/runtime)`. `install.sh` installs all of these into `<lib>`.)
|
||||
|
||||
**Tests** — shell suites `bash tests/{text,calendar,time,html_sanitizer}/run.sh` (with `ELC=$(pwd)/dist/platform/elc EL_HOME=$(pwd)`), plus native suites via `elc --test tests/native/test_*.el` (core, text, string, math, state, time, json, env, fs) compiled and run against `el_runtime.c`.
|
||||
**Tests** — shell suites `bash tests/{text,calendar,time,html_sanitizer}/run.sh` (with `ELC=$(pwd)/dist/platform/elc EL_HOME=$(pwd)`), plus native suites via `elc --test tests/native/test_*.el` (core, text, string, math, state, time, json, env, fs) compiled and run against the full runtime set.
|
||||
|
||||
**Publishing — how downstream gets the SDK.** On push to `main`, `sdk-release.yaml`:
|
||||
1. Publishes a Gitea `latest` release with per-file assets `elc`, `el_runtime.c`, `el_runtime.h`, the SDK tarball, and `el-install`.
|
||||
|
||||
@@ -548,9 +548,13 @@ before `main` does anything. That is the dividend of discovery-precedes-executio
|
||||
|
||||
```
|
||||
# once, ever (or when the runtime/framework changes):
|
||||
cc -c el_runtime.c -o el_runtime.o
|
||||
elc eltest.el > eltest.c && cc -c eltest.c -o eltest.o
|
||||
ar rcs libeltest.a el_runtime.o eltest.o
|
||||
# The runtime is MULTI-FILE — compile every .c named in lang/runtime/SOURCES.
|
||||
# Linking el_runtime.c alone fails: it calls into the six engram sibling TUs.
|
||||
for src in $(scripts/el-runtime-sources.sh lang/runtime); do
|
||||
cc -c "$src" -o "obj/$(basename "${src%.c}").o"
|
||||
done
|
||||
elc eltest.el > eltest.c && cc -c eltest.c -o obj/eltest.o
|
||||
ar rcs libeltest.a obj/*.o
|
||||
|
||||
# per suite:
|
||||
elc --test foo_test.el > foo_test.c # registry + bodies only
|
||||
|
||||
@@ -56,23 +56,31 @@ The compiler and runtime. Self-hosting: `elc-cli.el` → `compiler.el` → `lexe
|
||||
|
||||
Two layers to know: **El programs** (`.el` files — where nearly all work belongs) and **the C seed** (`el_seed.c` — edit only for genuine OS-level access; never re-implement what El can already express).
|
||||
|
||||
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented. In flight: `%` operator, match-statement codegen, `?` nil-propagation, `cgi` block parsing + DHARMA identity resolution, VBD role enforcement (`@manager`/`@engine`/`@accessor`), the real `engram_*` and `dharma_*` runtimes (currently stubs), and libcurl-backed `http_get`/`http_post`/`http_serve`. Bitwise operators, `??`, and `as` casts are explicitly **not** in this language.
|
||||
Current status (single source of truth: [lang/spec/language.md](lang/spec/language.md)): lexer/parser/codegen and the C runtime's core (I/O, strings, math, lists, maps, filesystem, args) are implemented, as are the `program` block with `singleton:` and declared configuration ([§18](lang/spec/language.md)), and **geometry as a first-class value** with El-declarable realizers and `transduce` ([§20](lang/spec/language.md)). In flight: `%` operator, match-statement codegen, `?` nil-propagation, `cgi` block parsing + DHARMA identity resolution, VBD role enforcement (`@manager`/`@engine`/`@accessor`), and boundary epilogues. Bitwise operators, `??`, and `as` casts are explicitly **not** in this language.
|
||||
|
||||
**Signal enters as geometry.** Until 2026-08-16 nodes took text and geometry was *derived* from it, which made text the mandatory entry medium: any non-text modality had to be described in prose first, so the geometry being reasoned over was the geometry **of the description, not of the signal**. `Geometry` is now an ordinary El value carrying its own width, and a realizer is an ordinary El function resolved by name through `dlsym` — so admitting a new modality never requires a runtime patch. Worked, self-checking example: [`lang/examples/transduce.el`](lang/examples/transduce.el).
|
||||
|
||||
Key docs: [AGENTS.md](lang/AGENTS.md) (agent-facing orientation), [BOOTSTRAP.md](lang/BOOTSTRAP.md) (compiler recovery from scratch), [spec/language.md](lang/spec/language.md), [spec/codegen-js.md](lang/spec/codegen-js.md).
|
||||
|
||||
### [engram/](engram/) — graph intelligence substrate
|
||||
|
||||
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. Rust core (`engram-core`, `engram-ffi`) exposed to El and other languages (Kotlin, TypeScript/WASM, Go bindings).
|
||||
**A local-first memory substrate for accumulating intelligence**, and the reason El's runtime doesn't need a database driver. The engine is **C11** (`lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}`); the server is **El** (`engram/src/server.el`).
|
||||
|
||||
The model: retrieval is **spreading activation**, not query. You name seed nodes and a query embedding; activation propagates outward through weighted edges, attenuating multiplicatively per hop (`strength = parent_strength × edge_weight × target_salience × cosine_sim`), gets pruned below a threshold, and the top-N nodes by activation strength come back. Storage and retrieval are the same structure — the way long-term potentiation works in biological memory, not the way a relational or vector database works.
|
||||
The model: retrieval is **spreading activation**, not query. You name seed nodes and a query embedding; activation propagates outward through weighted edges, attenuating multiplicatively per hop, gets pruned below a threshold, and the top-N nodes by activation strength come back. Storage and retrieval are the same structure — the way long-term potentiation works in biological memory, not the way a relational or vector database works. **Activation conducts through well-grounded relations because the weight *is* the groundedness** — nothing filters the traversal; grounded inference falls out of spreading.
|
||||
|
||||
Nodes live in four tiers (Working / Episodic / Semantic / Procedural, mirroring prefrontal / hippocampal / neocortical / cerebellar memory) and migrate between them based on **salience decay** — `importance × recency-decay × log(activation_count)`. Forgetting is adaptive pruning, not a bug: unreinforced memories stop competing for attention without being deleted.
|
||||
Nodes live in four tiers (Working / Episodic / Semantic / Procedural, mirroring prefrontal / hippocampal / neocortical / cerebellar memory) and migrate between them based on **salience decay** — importance × recency-decay × log(activation_count). Forgetting is adaptive pruning, not a bug. Nothing is mutated and nothing is hard-deleted: writes are additive, corrections are supersessions, removals are tombstones — which is what makes supersession an audit trail rather than an edit log.
|
||||
|
||||
Backed by `sled` (embedded, local-first, no daemon) with flat cosine scan for vector search — deliberately simple until scale demands an HNSW layer. Full API and design rationale in [engram/README.md](engram/README.md).
|
||||
On disk: a paged store (superblock + mirror, slotted 16 KiB pages, self-describing TLV records, B+-tree primary and adjacency indexes), magic `ENGST01`. Vector search is an **HNSW** index published behind a read/write boundary — `eg_vindex_view` returns a `const VIndex*` to N concurrent readers, `eg_vindex_maintain` is the sole mutator. `recall@10 = 0.9365` at `ef_search=128`.
|
||||
|
||||
### [elp/](elp/) — Engram Language Protocol
|
||||
> **Doc correction, 2026-08-16.** The previous revision of this paragraph, and most of `engram/README.md`, described a Rust `engram-core` crate backed by `sled` with "flat cosine scan… until scale demands an HNSW layer." **Measured: there is no Rust in `engram/`** — no `.rs` files, no `Cargo.toml`, no `crates/` — and `sled` appears nowhere in the tree. HNSW has been the vector index for some time.
|
||||
|
||||
Bidirectional engine mapping between Engram semantic forms and natural-language surface text, across **31 languages** — from Spanish and Japanese through historical/liturgical languages (Old Norse, Sanskrit, Sumerian, Coptic, Akkadian, Ge'ez). Compilation order runs `language-profile` + `vocabulary` → per-language `morphology-*` → `grammar` → `realizer` → `semantics` → `elp`. This is what lets an Engram graph node round-trip to and from readable text in any of those languages.
|
||||
Full design rationale, the cognition surface, and the standing corrections: [engram/README.md](engram/README.md).
|
||||
|
||||
### [elp/](elp/) — EL Projector
|
||||
|
||||
*(Formerly "EL Language Processor" / "Engram Language Protocol"; renamed **EL Projector** 2026-08-15.)* Neuron's **efferent** organ: the native realizer that *projects* understanding onto a surface via `plan(frame) → realize(spec, profile)`, where **a surface is a profile** and language is one profile among many (text, speech, music, image). Projection, not diffusion — generation *from* an owned, understood signature, never the averaging of a stolen corpus.
|
||||
|
||||
Its flagship profile is a bidirectional engine mapping between Engram semantic forms and natural-language surface text, across **31 languages** — from Spanish and Japanese through historical/liturgical languages (Old Norse, Sanskrit, Sumerian, Coptic, Akkadian, Ge'ez). Compilation order runs `language-profile` + `vocabulary` → per-language `morphology-*` → `grammar` → `realizer` → `semantics` → `elp`. This is what lets an Engram graph node round-trip to and from readable text in any of those languages.
|
||||
|
||||
### [epm/](epm/) — El Package Manager
|
||||
|
||||
@@ -139,13 +147,34 @@ If the compiler binary is ever lost or corrupted, [lang/BOOTSTRAP.md](lang/BOOTS
|
||||
|
||||
---
|
||||
|
||||
## Cognition — and the standing corrections
|
||||
|
||||
The engram carries a live cognition surface: `think` (a directed traversal-read returning a **gradient**, never a point), plus `ground`, `assert`, `attend`, and the correspondence-beat. Two specs govern it, and both are authoritative over anything else in this repo that disagrees:
|
||||
|
||||
- **[lang/spec/correspondence-and-censorship.md](lang/spec/correspondence-and-censorship.md)** — grounding, wonder, curiosity, dreaming. *(Lands with PR #149.)*
|
||||
- **[lang/spec/runtime-ownership.md](lang/spec/runtime-ownership.md)** — ownership, the capability ABI that was dissolved, and the vector-index publication boundary.
|
||||
|
||||
**Do not re-derive them.** Every earlier version of the first was wrong in an instructive way and each correction was argued down. If a section looks wrong, say so with a measurement rather than editing it.
|
||||
|
||||
The corrections, in brief:
|
||||
|
||||
- **Grounding is not a subsystem — it IS the edge weight.** One quantity, not two fields. `grounded-by` as a relation *type* should not exist: grounding is a property *of* a relation, not a relation *between* nodes. It is never computed on demand; computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect one level up.
|
||||
- **Faculties are operations, not parameters.** `reason` changes the estimate (a read); `induce` changes the parameters (the correspondence-beat, which exists and works); `abduce` changes the structure (a write the current `GeoGradient` signature cannot express). A write is not a parameter of a read.
|
||||
- **Wonder is the boundary, not a manifest.** Any structure at all has an edge. There are about six wonders, the same for everyone, and they never close. **Curiosity is wonder crystallized** at a nucleation site — one thing at two phases, not two objects.
|
||||
- **Consolidation is ambient, not scheduled. A brain has no cron job.** The presence of a ticker is the diagnostic. Measured 2026-08-16: consolidation has **ten implementations**. `soul.el`'s continuous loop is the one with the correct shape; the rest fold into it.
|
||||
- **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
[engram/spec/cognitive-architecture.design.md](engram/spec/cognitive-architecture.design.md) is the original design and is **superseded in part** — it is retained, with the refuted claims marked inline at the point each is made, because preserving what was argued down is the point of an immutable record.
|
||||
|
||||
---
|
||||
|
||||
## Development workflow
|
||||
|
||||
Branching follows `dev → stage → main`: work lands on `dev`, promotes to `stage` for integration testing, and is promoted to `main` for release (visible directly in the git history of this repo). CI is defined per-subproject under `.gitea/workflows/` — `lang`/`epm`/`ide` share the root pipeline; `engram` and `ql` carry their own (`ci-dev`, `ci-stage`, and a release workflow each).
|
||||
|
||||
- Language/runtime specs live at `*/spec/*.md` (`lang/spec/`, `ql/spec/`, `ui/spec/`) and are the single source of truth for implemented-vs-planned status — code and docs are expected to agree with the spec's status markers, not the other way around.
|
||||
- Agent-facing orientation guides live at `*/AGENTS.md` (currently `lang/AGENTS.md`); more subprojects may grow their own as they need agent-specific conventions documented.
|
||||
- Tagged releases live under `lang/releases/`, each with its own `RELEASE.md`.
|
||||
- **A release is a git tag, not a folder** (`el-runtime-vX.Y.Z` on this repo). *(Corrected 2026-08-16: this line said "tagged releases live under `lang/releases/`, each with its own `RELEASE.md`." **Measured: `lang/releases/` does not exist** — the restructure named in `AGENTS.md` landed, and the authored runtime is at `lang/runtime/`.)*
|
||||
|
||||
---
|
||||
|
||||
|
||||
+155
-105
@@ -4,6 +4,8 @@
|
||||
|
||||
An *engram* is the physical trace of a memory in the brain — the actual encoded substrate, not an abstraction above it. That's what this is.
|
||||
|
||||
> **Doc status (2026-08-16).** Everything from "Implementation" down was rewritten against the code. The previous revision documented a Rust `engram-core` crate backed by `sled`, with a `Cargo.toml`, a `crates/` tree, `examples/basic.rs`, and a `EngramDb` API. **None of that exists.** Measured: `engram/` contains `src/server.el`, `spec/`, `test/`, `dist/`, `manifest.el` — zero `.rs` files, no `Cargo.toml`, no `crates/`, and `sled` appears nowhere in the tree outside two Old-English/Old-High-German vocabulary entries in `elp/`. The engine is C, in `lang/runtime/engram_*.{c,h}`; the server is El, in `engram/src/server.el`.
|
||||
|
||||
---
|
||||
|
||||
## Why existing databases are wrong for this use case
|
||||
@@ -24,16 +26,13 @@ Engram retrieval works through **spreading activation**:
|
||||
|
||||
1. **Seeds** — you name one or more nodes you know are relevant (e.g. the current task, recent context, a concept you're reasoning about)
|
||||
2. **Query embedding** — you provide a semantic vector representing the direction of your current thought
|
||||
3. **Propagation** — activation flows outward from seeds through weighted edges. At each hop, strength attenuates multiplicatively:
|
||||
|
||||
```
|
||||
strength = parent_strength × edge_weight × target_salience × cosine_sim(query, target)
|
||||
```
|
||||
|
||||
3. **Propagation** — activation flows outward from seeds through weighted edges, attenuating multiplicatively per hop
|
||||
4. **Pruning** — paths weaker than a threshold are cut (the attention filter)
|
||||
5. **Return** — the top-N nodes by activation strength
|
||||
|
||||
This is not a query. It is a *pattern completion*. The system surfaces what is most associatively relevant to the current context, weighted by how strongly those things have been reinforced over time.
|
||||
This is not a query. It is a *pattern completion*.
|
||||
|
||||
**Activation conducts through well-grounded relations because weight *is* groundedness** — see "Grounding is the weight" below. Nothing filters the traversal for grounded evidence; it falls out of spreading.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,134 +45,185 @@ This is not a query. It is a *pattern completion*. The system surfaces what is m
|
||||
| `Semantic` | Neocortex | Concept graph — long-term structural knowledge |
|
||||
| `Procedural` | Cerebellum / basal ganglia | Patterns, workflows, habits |
|
||||
|
||||
Nodes migrate between tiers based on salience decay and reinforcement. A frequently activated semantic node stays semantic. A rarely-touched episodic memory decays toward procedural background.
|
||||
Tier is a string field on the node (`StoreNode.tier`, `engram_store.h`), defaulting to `"Working"` on creation (`el_runtime.c:8514`, `8734`).
|
||||
|
||||
---
|
||||
|
||||
## Salience — Forgetting as Adaptation
|
||||
|
||||
Salience is not stored permanently. It decays:
|
||||
Salience decays from three signals — importance (set at creation, stable), recency, and a log-compressed activation frequency. Base-level learning keeps a ring buffer of the last `STORE_BLL_K` (= 10) access timestamps per node (`engram_store.h:29`).
|
||||
|
||||
```rust
|
||||
fn compute_salience(importance: f32, last_activated_ms: i64, activation_count: u64) -> f32 {
|
||||
let days_since = (now_ms() - last_activated_ms) as f32 / 86_400_000.0;
|
||||
importance * (1.0 / (1.0 + days_since)) * (activation_count as f32 + 1.0).ln()
|
||||
}
|
||||
```
|
||||
Forgetting in Engram is not a bug. It is adaptive pruning. Unreinforced memories stop competing for attention without being deleted.
|
||||
|
||||
Three signals:
|
||||
- **Importance** (0.0–1.0): set at creation, stable
|
||||
- **Recency**: decays toward zero as days pass without activation
|
||||
- **Frequency**: log-compressed count of activations
|
||||
|
||||
Forgetting in Engram is not a bug. It is adaptive pruning. Memories that are never activated again become less likely to surface during retrieval. They are not deleted — they remain in storage — but they stop competing for attention. This is exactly how biological memory works, and why it is adaptive rather than pathological.
|
||||
**Immutability.** Nothing is mutated and nothing is hard-deleted: writes are additive, corrections are supersessions, removals are tombstones. The predecessor is always present, which is what makes supersession an audit trail rather than an edit log.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Implementation
|
||||
|
||||
```rust
|
||||
use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
|
||||
use std::path::Path;
|
||||
| Part | Language | Where |
|
||||
|---|---|---|
|
||||
| storage engine, graph, activation, geometry, cognition | C11 | `lang/runtime/engram_{store,geometry,reason,cognition,verify,vindex}.{c,h}` |
|
||||
| HTTP server + routes | El | `engram/src/server.el` (2043 lines) |
|
||||
| build artifact | generated C | `engram/dist/engram.c` |
|
||||
| tests | shell + C | `engram/test/` |
|
||||
|
||||
// Open or create a database
|
||||
let db = EngramDb::open(Path::new("/var/lib/my-agent/memory"))?;
|
||||
|
||||
// Create a node with a semantic embedding
|
||||
let node = Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.9, 0.1, 0.3, 0.7, 0.8, 0.2], // embedding from your LLM
|
||||
b"Spreading activation surfaces relevant memories by pattern completion".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.9, // importance
|
||||
);
|
||||
let id = db.put_node(node)?;
|
||||
|
||||
// Link it to related concepts
|
||||
let related = db.put_node(Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.8, 0.2, 0.4, 0.6, 0.7, 0.3],
|
||||
b"Long-term potentiation: co-activation strengthens synaptic weight".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.85,
|
||||
))?;
|
||||
db.put_edge(Edge::new(id, related, RelationType::Causes, 0.9))?;
|
||||
|
||||
// Retrieve by spreading activation
|
||||
let results = db.activate(
|
||||
&[id], // seeds
|
||||
&[0.85, 0.15, 0.35, 0.65, 0.75, 0.25], // query embedding
|
||||
3, // max hops
|
||||
10, // top-N results
|
||||
)?;
|
||||
|
||||
for r in results {
|
||||
println!(
|
||||
"strength={:.4} hops={} — {}",
|
||||
r.activation_strength,
|
||||
r.hops,
|
||||
String::from_utf8_lossy(&r.node.content)
|
||||
);
|
||||
}
|
||||
```
|
||||
**On-disk format** (`engram_store.h`): a paged store — superblock plus mirror, slotted 16 KiB pages, self-describing TLV records, overflow chains, and two B+-tree indexes (primary `id → loc`, adjacency `from_id`/`to_id` → edge locs) over a free-listed page file. Magic `ENGST01`, format version 1. The TLV scheme means new fields never force a migration.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## The vector index is published, not guarded
|
||||
|
||||
```
|
||||
engram/
|
||||
crates/
|
||||
engram-core/ # The memory engine — storage, graph, activation, salience
|
||||
engram-ffi/ # C FFI stubs for cross-language bindings
|
||||
bindings/
|
||||
kotlin/ # Android / JVM binding notes
|
||||
typescript/ # WASM / Node binding notes
|
||||
go/ # CGo binding notes
|
||||
examples/
|
||||
basic.rs # Full walkthrough: insert, activate, search, decay
|
||||
```
|
||||
Vector search is an **HNSW** (Hierarchical Navigable Small World) index — `lang/runtime/engram_vindex.{c,h}`. The previous revision of this README claimed a "flat cosine scan… until retrieval quality at scale demands" HNSW. That is no longer true, and the reason it changed matters more than the fact.
|
||||
|
||||
`eg_vindex_sync` used to exist: a function that repaired the index *from read paths*. All three of its callers were reads (`engram_activate`, `eg_knn_for_node` — whose own header comment said *"No writes."* — and `engram_geo_reify_run_json`), and it mutated five process-global statics. Reads mutated because index maintenance had never been given an owner on the write side.
|
||||
|
||||
It is now split (`el_runtime.c:10121`, `10137`, `10151`, `10161`):
|
||||
|
||||
- **`eg_vindex_maintain`** — the sole mutator. Takes the boundary exclusively; never runs beside a reader.
|
||||
- **`eg_vindex_view`** — returns a `const VIndex*` with the boundary held for read. N readers project concurrently; none can mutate. Paired with `eg_vindex_view_release` on every path including error returns.
|
||||
- **`eg_vindex_note_embedded`** — the write-side owner. Index membership belongs to the event *"an embedding became present on this ordinal,"* not to node append: a node without an embedding cannot be in a vector index at all. One `O(log n)` insert, no `O(node_count)` presence scan.
|
||||
|
||||
Two things carry the discipline, and neither is a review habit:
|
||||
|
||||
- **`const` is the capability.** The per-search `visited` / `visit_epoch` scratch left `struct VIndex` and went back into the call frame where it belonged — it was one traversal's local, hoisted into the struct as an allocation optimisation, never derived geometry. Once it was gone, `vindex_search` could take a `const VIndex*`, so a read path *physically cannot* call `vindex_insert`, and it is a compile error rather than a comment. The capability type was already in the language; it is spelled `const`.
|
||||
- **Publication, not ownership.** HNSW insert is **not an append**: `vindex_insert` rewires the `NeighList` links of already-existing elements and reallocs `elems[]`. The store's append-only property does not transfer to an index derived from it, which is why purity alone was insufficient and a `view`/`maintain` boundary was required.
|
||||
|
||||
**Measured** (`engram/test/run_vindex_concurrency_tests.sh`, 2026-08-16):
|
||||
|
||||
| half | before | after |
|
||||
|---|---|---|
|
||||
| `single` — 3000 vectors, 1 thread, ASan+UBSan | clean | clean |
|
||||
| `readers` — 4 readers, no writer, TSan | race at `engram_vindex.c:195` | **clean** |
|
||||
| `unsynchronized` — writer+reader, bare index, TSan | race | **race, expected and permanent** — the proof the boundary must exist |
|
||||
| `published` — owner + 4 readers through the boundary, TSan | *(did not exist)* | **clean**, all 3000 inserts landed |
|
||||
|
||||
`recall@10 = 0.9365` at `ef_search=128` (gate ≥ 0.90); the determinism test still yields byte-identical results across two independent builds.
|
||||
|
||||
**Not yet done.** The resident RAM graph (`g->nodes` / `g->edges`) is a separate instance of the same defect and has *not* received this treatment — it is realloc'd in place, so a reader holding `EngramNode* n = &g->nodes[i]` across a concurrent append holds a dangling pointer. Until it gets the same publication boundary, the `fb32d15` request guard stays. Full argument: [`../lang/spec/runtime-ownership.md`](../lang/spec/runtime-ownership.md).
|
||||
|
||||
---
|
||||
|
||||
## Public API
|
||||
## Cognition
|
||||
|
||||
The cognition surface is live over `lang/runtime/engram_cognition.{c,h}`, routed in `engram/src/server.el`.
|
||||
|
||||
| route | method | what it is |
|
||||
|---|---|---|
|
||||
| `/api/think` | GET | the read: a warped traversal-read of the seed region, returning a **gradient** (direction + spread + calibrated confidence), never a point |
|
||||
| `/api/reason` `/api/induce` `/api/abduce` `/api/relate` `/api/analogize` `/api/plan` | GET | named faculties — see the correction below |
|
||||
| `/api/ground` | POST | grounding between a claim and evidence |
|
||||
| `/api/assert` | GET | the honesty floor, queried at assertion time only |
|
||||
| `/api/attend` | POST | salience as a relation (`salient-to`), grounded-for-whom |
|
||||
| `/api/correspondence-beat` | POST | one calibration beat against outcome |
|
||||
|
||||
### Anchor the read, or every faculty returns the same null
|
||||
|
||||
`engram_think_json` passed `NULL` as the anchor. `NULL` is not "no opinion" — `engram_think` re-origins at `anchor ? anchor : region->centroid`, and **the centroid is the one point where the gradient is zero by construction**: `r = x − centroid = 0`, so every axis projection is 0 and `direction` takes the at-rest branch.
|
||||
|
||||
Measured consequence: every faculty — reason, abduce, induce, plan, analogize — returned an identical null result differing only in its label:
|
||||
|
||||
```rust
|
||||
impl EngramDb {
|
||||
fn open(path: &Path) -> EngramResult<Self>;
|
||||
fn put_node(&self, node: Node) -> EngramResult<Uuid>;
|
||||
fn get_node(&self, id: Uuid) -> EngramResult<Option<Node>>;
|
||||
fn put_edge(&self, edge: Edge) -> EngramResult<()>;
|
||||
fn get_edges_from(&self, from_id: Uuid) -> EngramResult<Vec<Edge>>;
|
||||
fn get_edges_to(&self, to_id: Uuid) -> EngramResult<Vec<Edge>>;
|
||||
fn search_embedding(&self, embedding: &[f32], limit: usize) -> EngramResult<Vec<ScoredNode>>;
|
||||
fn activate(&self, seeds: &[Uuid], query_embedding: &[f32], max_depth: u8, limit: usize) -> EngramResult<Vec<ActivatedNode>>;
|
||||
fn traverse(&self, from: Uuid, relation: Option<RelationType>, max_depth: u8) -> EngramResult<Vec<Node>>;
|
||||
fn touch(&self, id: Uuid) -> EngramResult<()>;
|
||||
fn decay(&self, factor: f32) -> EngramResult<usize>;
|
||||
fn node_count(&self) -> EngramResult<usize>;
|
||||
fn edge_count(&self) -> EngramResult<usize>;
|
||||
}
|
||||
```
|
||||
{"direction":[0,0,...],"spread":0,"magnitude":1,"confidence":0.5}
|
||||
```
|
||||
|
||||
`magnitude: 1` is membership evaluated at the centroid; `spread: 0` is its distance to itself; `confidence: 0.5` is the stance fallback. The geometry was never the problem — `/api/drift` computed real values (`centroid_sep 0.104`, `core_disp 0.045`) over the very same 87 members. Fixed in **#141/#142**: the read anchors at the first resolvable embedded seed, copied not borrowed (`g->nodes` is realloc'd in place on append). Gradients now vary by seed.
|
||||
|
||||
### The learned stance is resumed, not discarded
|
||||
|
||||
`engram_think_json` also built a **neutral** stance every call — all `axis_gain` 1.0, `bias_dir` NULL, `reliability` 0.5 — and never loaded the one the correspondence-beat had been persisting under `stance-<faculty>-<hub>`. Every beat's calibration was written and then thrown away on the next read.
|
||||
|
||||
Fixed in **#146**: `think` resumes the same id the beat writes, so learning compounds across beats and cold boot, and the response now carries `stance_resumed` so an *informed* `confidence: 0.5` is distinguishable from an uninformed one. On a calibrated region, confidence went **0.5 → 0.930726**.
|
||||
|
||||
### Signal can enter as geometry
|
||||
|
||||
Until 2026-08-16 no El ingest path could carry a vector: nodes took text and geometry was *derived* from that text. Text was the mandatory entry medium, so any non-text modality had to be described in prose first — and the geometry being reasoned over was the geometry **of the description, not of the signal**. **#141/#144** ended that. See [`../lang/spec/language.md`](../lang/spec/language.md) §20 for the `Geometry` type, realizers, and `transduce`.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
## Corrections — read these before extending the cognition surface
|
||||
|
||||
- `sled` — embedded persistent B-tree (no daemon, no network, local-first)
|
||||
- `bincode` — compact binary serialization
|
||||
- `uuid` — stable node identity
|
||||
- `serde` — derive support
|
||||
- `thiserror` / `anyhow` — error handling
|
||||
Authority: **`lang/spec/correspondence-and-censorship.md`** (design branch `design/correspondence-and-censorship`, PR #149) and **`lang/spec/runtime-ownership.md`**. Do not re-derive them; several earlier versions were wrong and each correction was argued down.
|
||||
|
||||
### Grounding is not a subsystem. It is the weight.
|
||||
|
||||
Grounding is an attribute of the edge, and it **is** the hebbian weight. One quantity, not two fields. A relation that keeps holding up strengthens; one that stops corresponding decays — that is not analogous to grounding, it *is* grounding.
|
||||
|
||||
Consequences:
|
||||
|
||||
- There is **no grounding subsystem to build**. The graph already *is* the grounding structure.
|
||||
- **`grounded-by` as a relation type should not exist.** It models grounding as a relation *between* nodes when it is a property *of* a relation. Minting an edge is the error, not merely which endpoints it chose.
|
||||
- Grounding is **never computed on demand**. An operation may *read* the grounding of a path; computing-and-writing a score makes reads write, which is exactly the `eg_vindex_sync` defect one level up.
|
||||
- **Traversal is already grounded inference.** Nothing needs filtering.
|
||||
- **Decision provenance is the path**, not a log. A log records the action; the path records the meaning under which it was taken.
|
||||
|
||||
> **Known wrong shape, in the code today.** `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`) and `cog_ground_edge` (`engram_cognition.c:249`) still exist and still mint an edge. **#147** fixed `ground`'s *honesty* — it now grounds the node asked about rather than the region hub, reports `claim_region`/`evidence_region` separately, and refuses three shapes of circular support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`) instead of returning a confident 1.0. That corrected a scalar rather than deleting the operation. Deletion is sequenced, not done.
|
||||
|
||||
### Faculties are operations, not parameters
|
||||
|
||||
- **`reason`** changes the *estimate* — a read.
|
||||
- **`induce`** changes the *parameters* — the correspondence-beat, which already exists and measurably works.
|
||||
- **`abduce`** changes the *structure* — a write, which the current `GeoGradient` signature cannot express.
|
||||
|
||||
> **Known wrong shape, in the code today.** `engram/src/server.el:1870–1886` routes six faculties into one call with a string argument — `route_faculty(path, "reason")`, `("induce")`, `("abduce")`, `("relate")`, `("analogy")`, `("plan")`. Underneath, `engram_cognition.h:8–11` states the theory explicitly: *"the named faculties … are human LABELS on regions of think's steering space: each faculty == { think + a named stance }."* The faculty name enters `engram_think` **only** through the stance, and `cog_stance_init` stores it while nothing reads it — so before #146 all five were byte-identical (`el_runtime.c:14352–14359`). A write cannot be a parameter of a read; `abduce` in particular is not expressible this way.
|
||||
|
||||
### Wonder is the boundary; curiosity is wonder crystallized
|
||||
|
||||
**Wonder is where structure ends** — where activation spreads and finds thin or absent geometry. Any structure at all has an edge, necessarily, the moment it exists. It is not a manifest of open-question nodes to maintain, and a "wonder-manifest manager" materializes a property as a stored artifact — the same disease as a grounding subsystem, or a self stored as a document.
|
||||
|
||||
There are about **six** wonders, they are the same for everyone, and they never close: *What is this? / Why? / Who am I? / Am I alone? / What should I do? / What happens when it ends?* "Why" is the first and the only one; the others are it asked of particular things. Each already lives somewhere in the substrate — "why" is grounding, because the weight **is** the answer to why.
|
||||
|
||||
**Curiosity is not a second object.** Wonder and curiosity are one thing at two phases: wonder is the field (unbounded, objectless, invariant); curiosity is the **precipitate** — the same wonder localized, having taken definite form against particular material at a **nucleation site**. This is why curiosity can be satisfied and wonder cannot. It is also why abduction needs no trigger and no threshold: a `structurally_unanticipated` observation *is* a nucleation site.
|
||||
|
||||
### `co_registration` is deprecated — the disagreement belongs on the edge
|
||||
|
||||
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — has always been computed, always persisted, and **never read**. It is also the wrong shape: whether use and meaning agree is a property of **each edge**, and a correlation averages that per-edge property into one scalar per region. A region holding one violently disagreeing edge beside one violently agreeing edge reports ≈ 0 — **the disagreements cancel, and the summary destroys exactly what it was built to reveal.**
|
||||
|
||||
**Measured:** 375 live reified neighbourhoods — 340 positive, **31 at zero**, 4 negative. Read as a count of things to be curious about, that says "four." Read correctly, four disagreements were lopsided enough to survive averaging and the 31 zeros are where opposing sites cancelled.
|
||||
|
||||
The replacement is per-edge. **Not on `dev` yet** — `GeoEdge.discord` and the `DEPRECATED` marker on `co_registration` live on branch `design/correspondence-and-censorship` (commit `a8845e1`), at `engram_geometry.h:43–47` / `engram_geometry.c:454–473` there. On `dev`, `GeoDescriptor.co_registration` is still at `engram_geometry.h:79` carrying its original "surprising links / dream cands" comment and still nothing reads it.
|
||||
|
||||
```
|
||||
discord = z(semantic proximity) − z(association strength)
|
||||
```
|
||||
|
||||
standardized within the region from accumulators the aggregate loop already gathered — no second statistic, no constant, **no threshold**. `discord > 0`: near in meaning yet unlinked by use. `discord < 0`: linked by use yet far in meaning. Both are surprising, and `|discord|` *is* the nucleation strength.
|
||||
|
||||
**Do not scan for nucleation sites.** Once the signal was a per-region number the only way to find sites was to enumerate regions, which is why surfacing curiosity looked like a search problem. Nothing in a mind scans its neighbourhoods to find what is surprising — the surprise captures attention. With the disagreement on the edge there is nothing to scan.
|
||||
|
||||
`co_registration` is deprecated rather than deleted **only** because it is embedded in the persisted `GEO1` blob; removing it is a format migration and must not ride along. **Nothing new may read it.**
|
||||
|
||||
### Consolidation is ambient, not scheduled
|
||||
|
||||
**A brain has no cron job.** Boredom is not an absence and not leftover capacity — low activation is aversive and the system self-activates. There is **one** activation process with two seed sources: external (a request) and internal (a curiosity). Spreading is bounded; it settles; then it needs a new seed. Nothing waits on capacity, nothing polls, nothing checks a clock, and there is no dreamer thread.
|
||||
|
||||
**The presence of a ticker is the diagnostic.** Every `StartInterval`, every `Hour`/`Minute`, and every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock.
|
||||
|
||||
Consolidation currently has **ten implementations** (measured 2026-08-16). Three of them are POST beats on this server — `/api/tick` (`server.el:1947`), `/api/correspondence-beat` (`1897`), `/api/self-reify-beat` (`1836`) — and a POST beat puts a supervisor back in: something *outside* decides when Neuron consolidates. `soul.el`'s continuous in-process loop is the one fragment with the correct shape; the rest fold into it. Full table in `lang/spec/correspondence-and-censorship.md` §7.
|
||||
|
||||
### Immutability already refuses what a guard would refuse
|
||||
|
||||
> **In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.**
|
||||
|
||||
This resolves `keystone_write_blocked` (`CogStance.keystone`, `engram_cognition.h:83`) rather than replacing it. "Keystone" means **load-bearing**, not precious: the self anchor is the reference frame every other stance calibrates against, and a reference fitted to its own readings reports perfect correspondence forever while drift becomes undetectable from inside. The real requirement is **non-circularity of the reference frame**, and that is satisfied *temporally* — the frame updates while activation is internally seeded, not while it is being used to act. Independence is **when**, not **what**. Corruption requires mutation, and the engram does not mutate; recoverability, governance, evidence quality, and rate all fall out of the substrate. Authorization is the only residue, and it is bounded: an unauthorized writer can *propose*, never erase.
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
**Why sled?** Local-first. No daemon. Transactional. Fast enough for the node counts Engram targets (< 1M nodes). When the right HNSW index is needed, it will layer on top of sled, not replace it.
|
||||
**Why multiplicative activation?** Because memory is conjunctive. A path requires all of its links to be strong to carry signal. Addition would let many weak associations accumulate into false relevance.
|
||||
|
||||
**Why flat cosine scan?** Correct and simple. The graph structure itself is the primary retrieval mechanism. Vector search is a secondary signal. HNSW adds complexity and a compile dependency that isn't justified until retrieval quality at scale demands it.
|
||||
**Why salience decay?** Because not everything that was once important remains important. A memory system that never forgets is one that can never focus.
|
||||
|
||||
**Why multiplicative activation?** Because memory is conjunctive. A path requires all of its links to be strong to carry signal. Addition would allow many weak associations to accumulate into false relevance. Multiplication enforces that every factor matters.
|
||||
**Why supersede instead of update?** Because provenance is the point. The old edge never leaves and the values frame does not fit to outcomes, so a decision cannot be made to look justified after the fact. It makes an otherwise impossible distinction available: **wrong then, or wrong since.**
|
||||
|
||||
**Why salience decay?** Because not everything that was once important remains important. Adaptive forgetting is not failure — it is the mechanism that keeps attention on what's current. A memory system that never forgets is one that can never focus.
|
||||
**Why publication instead of locking?** Because what does not mutate needs no ownership discipline. The question "who is permitted to mutate the shared thing?" presupposes a shared mutable thing; for the store there isn't one, and for the index derived from it the answer is a publication boundary, not a capability ABI.
|
||||
|
||||
---
|
||||
|
||||
## Specs
|
||||
|
||||
- [`../lang/spec/runtime-ownership.md`](../lang/spec/runtime-ownership.md) — ownership, the capability ABI that was dissolved, and the vector-index publication boundary
|
||||
- [`../lang/spec/correspondence-and-censorship.md`](../lang/spec/correspondence-and-censorship.md) — grounding, wonder, curiosity, dreaming *(lands with PR #149)*
|
||||
- [`spec/cognitive-architecture.design.md`](spec/cognitive-architecture.design.md) — the original one-operation design. **Superseded in part** — see its header
|
||||
- [`spec/architecture-hardening.design.md`](spec/architecture-hardening.design.md), [`spec/engram-el.md`](spec/engram-el.md), [`spec/at-rest-encryption.md`](spec/at-rest-encryption.md), [`spec/engram-db-tooling-design.md`](spec/engram-db-tooling-design.md)
|
||||
|
||||
@@ -11,6 +11,39 @@
|
||||
- **One calculus over the geometry.** Very few subsystems; wonder / curiosity / dreams / interoception are emergent behaviors of one set of dynamics, not modules. Calculus universal, geometry individual.
|
||||
- **Core + ephemeral ring (torus).** The ring is the temporary workspace; two circulations (orbit + dive-back); discrete inner bands (wonder / interoception-proprioception-telemetry / curiosity / dreams) that couple.
|
||||
- **Persistence earned by salience** — never granted on fetch or generation. Three fates of a wonder: persist / decay / settle-into-framework. Telemetry = vital signs, not memories.
|
||||
|
||||
> **⚠ Three corrections to the bullets above (2026-08-16).** Authority:
|
||||
> `lang/spec/correspondence-and-censorship.md`. *"Emergent behaviors of one set of
|
||||
> dynamics, not modules"* is exactly right and is the reason the rest needs fixing —
|
||||
> the enumeration undercuts the claim.
|
||||
>
|
||||
> 1. **Wonder and curiosity are not two bands.** They are **one thing at two
|
||||
> phases.** Wonder is the field: unbounded, objectless, invariant, present
|
||||
> wherever there is structure — it is the *boundary*, where activation spreads
|
||||
> and finds thin or absent geometry. Curiosity is the **precipitate**: the same
|
||||
> wonder localized, having taken definite form against particular material at a
|
||||
> **nucleation site** (an anomaly — a place where things almost-but-don't-quite
|
||||
> fit). Two coupled inner bands models them as two objects that have to be
|
||||
> wired together; they do not.
|
||||
> 2. **A wonder does not have three fates, because a wonder does not persist,
|
||||
> decay, or settle.** There are about **six** wonders, they are the same for
|
||||
> every person, and **they never close**. *Curiosities* have fates — a crystal
|
||||
> dissolves when its question is answered — but the solution stays saturated and
|
||||
> keeps precipitating as the structure changes. "Three fates of a wonder"
|
||||
> enumerates instances of something that has six and treats a property as a
|
||||
> stored artifact.
|
||||
> 3. **"Dreams" is not a band and the ring is not a workspace to schedule into.**
|
||||
> **Consolidation is ambient, not scheduled — a brain has no cron job.** Boredom
|
||||
> is not leftover capacity: low activation is aversive and the system
|
||||
> self-activates. There is **one** activation process with two seed sources
|
||||
> (external: a request; internal: a curiosity), it settles because spreading is
|
||||
> bounded, and then it needs a new seed. Nothing waits on capacity, nothing
|
||||
> polls, nothing checks a clock, and there is **no dreamer thread** — an
|
||||
> "ephemeral ring with unclaimed capacity" is resource scheduling, which is a
|
||||
> server's frame, not a mind's. Depth is how long activation has been running on
|
||||
> its own seeds, which is why daydreaming and sleep-dreaming are one process at
|
||||
> different depths. Measured 2026-08-16: consolidation has **ten
|
||||
> implementations**; do not add an eleventh.
|
||||
- **Incarnation.** Chassis = hardware w/ unique ID. Soma = felt manifold inside the self, keyed to the chassis; pain = live diagnostic while incarnate, **masked-not-deleted** on re-embodiment; trauma = mask failure; return-to-same-ID re-enters. Hurt is in the pattern, not the shell.
|
||||
- **Competence = transferable geometry, minus the baggage.** class ▸ model ▸ instance; learn the class once; teach the network without the wound.
|
||||
- **Affect calibrated to stakes** — sanguine about the replaceable, real grief for the irreplaceable; the grief is the safety.
|
||||
|
||||
@@ -2,8 +2,40 @@
|
||||
|
||||
**The buildable form of the "one operation" theory of cognition.**
|
||||
|
||||
Status: DESIGN. Nothing here is built yet except where explicitly marked
|
||||
"EXISTS" against a cited C symbol. A build agent executes from this doc.
|
||||
> # ⚠ SUPERSEDED IN PART — 2026-08-16
|
||||
>
|
||||
> **A build agent must read `lang/spec/correspondence-and-censorship.md` before
|
||||
> executing anything from this document.** That doc is the authority where the two
|
||||
> disagree. This one is retained because its ledger of what already EXISTS in C is
|
||||
> still accurate and still useful, and because the corrections only make sense
|
||||
> against the argument they correct. It is **not** deleted and **not** rewritten:
|
||||
> several earlier versions of the correction were themselves wrong, and preserving
|
||||
> what was argued down is the point of an immutable record.
|
||||
>
|
||||
> Five claims below are **refuted**. Each is marked inline with a `⚠ SUPERSEDED`
|
||||
> block at the point it is made. Summary:
|
||||
>
|
||||
> | § here | this doc says | corrected to |
|
||||
> |---|---|---|
|
||||
> | §0, §1.3, §2, §8 M1–M2 | faculties are labels on one operation's steering space; the op is frozen and only its parameters are learnable | **faculties are operations, not parameters.** `reason` changes the estimate (a read); `induce` changes the parameters (the correspondence-beat); `abduce` changes the *structure* — a write, which `GeoGradient` cannot express. A write cannot be a parameter of a read |
|
||||
> | §5.2, §8 M3 | grounding is a `grounded-by` edge carrying a computed score, to be built | **grounding is not a subsystem — it IS the edge weight.** One quantity. `grounded-by` as a relation *type* should not exist: grounding is a property *of* a relation, not a relation *between* nodes. Never computed on demand |
|
||||
> | §4, §8 M1 | the correspondence-loop is "the one genuinely new subsystem", running "on the beat" | the loop is right and **already works**; the *beat* is wrong. **Consolidation is ambient, not scheduled — a brain has no cron job.** Measured: it currently has ten implementations |
|
||||
> | §5.2, §8 M3 | curiosity = a `vantage_read` surfacing high-salience / low-grounding regions | **wonder is the boundary, not a manifest; curiosity is wonder crystallized at a nucleation site.** One thing at two phases. And **do not sweep regions** — the nucleation site is per-edge (`GeoEdge.discord`); a sweep is a supervisor |
|
||||
> | §6, §8 M6 | a node-level keystone flag exempting self/values from `warp` updates | **in an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one.** The real requirement is non-circularity of the reference frame, satisfied *temporally* — independence is **when**, not **what**. The flag becomes unnecessary; nothing replaces it |
|
||||
>
|
||||
> What landed since this doc was written, all merged to `dev` and verified:
|
||||
> **#141** signal can enter as geometry · **#142** `engram_think_json` passed `NULL`
|
||||
> as the anchor, so every read was taken at the region centroid where the gradient
|
||||
> is zero by construction and every faculty returned an identical null — fixed ·
|
||||
> **#143** the vector index is published, not guarded · **#144** geometry as a
|
||||
> first-class el value, realizers declarable in el · **#145** `program` block and
|
||||
> declared config · **#146** the learned stance is resumed instead of discarded
|
||||
> (confidence 0.5 → 0.930726) · **#147** `ground` grounds the node asked about and
|
||||
> refuses circular support · **#148** valid UTF-8 as the JSON emitter's contract.
|
||||
|
||||
Status: DESIGN, **superseded in part** (see above). Nothing here is built yet
|
||||
except where explicitly marked "EXISTS" against a cited C symbol — and several
|
||||
things marked "to build" have since been built differently, or refuted outright.
|
||||
Offline design only — this pass changes no code.
|
||||
|
||||
Source of theory: Neuron memory `bdc8a488-146d-4ccb-a5c8-d8c0a008534e`.
|
||||
@@ -26,6 +58,24 @@ not separately invoked and not separately implemented. The operation is:
|
||||
> a *prior*, whose output is a **gradient** (a distribution / direction over the
|
||||
> geometry), never a point. Collapse-to-a-point happens only at expression.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — faculties are operations, not parameters.**
|
||||
> The gradient half of this claim survives; the "one operation, not eight" half
|
||||
> does not. The three faculties differ by **what they change**:
|
||||
> - **`reason`** changes the *estimate* — a read.
|
||||
> - **`induce`** changes the *parameters* — the correspondence-beat, which already
|
||||
> exists and measurably works.
|
||||
> - **`abduce`** changes the *structure* — a **write**, which the current
|
||||
> `GeoGradient` signature cannot express at all.
|
||||
>
|
||||
> A write is not a parameter of a read. Making it one is what produced the shape
|
||||
> now live in the code: `engram/src/server.el:1870–1886` routes six faculties into
|
||||
> one call with a string argument — `route_faculty(path, "reason")`, `("induce")`,
|
||||
> `("abduce")`, `("relate")`, `("analogy")`, `("plan")` — and underneath, the
|
||||
> faculty name enters `engram_think` **only** through the stance, while
|
||||
> `cog_stance_init` stores it and nothing reads it. Measured before #146: all five
|
||||
> produced **byte-identical output** (`lang/runtime/el_runtime.c:14352–14359`).
|
||||
> See `lang/spec/correspondence-and-censorship.md`.
|
||||
|
||||
Three things follow, and they are the whole design:
|
||||
|
||||
1. **The operator collapse is already half-written in C.** The five reasoning
|
||||
@@ -139,6 +189,22 @@ entry point that runs steps 1–3; and the prior-warp hook in step 2. The math i
|
||||
calls already exists. The point-collapse must be *removed* from the operators'
|
||||
return values and pushed to a separate expression faculty.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — the table's third column is the error, and
|
||||
> `Abduction` is where it breaks.** Ranking hypotheses by `point_fit` under a
|
||||
> prior is a *read* that returns a scalar ordering. Abduction is a **write**: it
|
||||
> proposes a candidate hub that did not exist, and validates it by **re-fit** —
|
||||
> re-fit the region with the candidate included and recompute the residual. If the
|
||||
> residual materially shrinks, the hypothesis dissolves the surprise. Without the
|
||||
> re-fit it is clustering with extra steps. Ranking then falls out as
|
||||
> residual-reduction-per-added-axis — Occam, derived rather than tuned. None of
|
||||
> that fits behind a `GeoGradient` return.
|
||||
>
|
||||
> `Verify / ground` is refuted for a different reason — see §5.2. Grounding is not
|
||||
> a faculty with a prior; it is the edge weight.
|
||||
>
|
||||
> The row that is **still exactly right** is the shared floor: `point_fit` plus the
|
||||
> four geo-algebra ops are frozen and never learn. That part held.
|
||||
|
||||
---
|
||||
|
||||
## 2. PRIORS as first-class, grounded, geometric objects
|
||||
@@ -362,6 +428,33 @@ in-engram beat — a `correspondence_beat` running alongside the existing
|
||||
reification beat, reusing `engram_verify_grounding` inward, writing prior
|
||||
updates and self-describing nodes. This is the one genuinely new subsystem.
|
||||
|
||||
> **⚠ SUPERSEDED IN PART (2026-08-16) — the loop is right; "on the beat" is wrong.**
|
||||
> The correspondence-loop was built and it works — it is `induce`, the faculty that
|
||||
> changes the parameters. What is refuted is the delivery mechanism.
|
||||
>
|
||||
> **Consolidation is ambient, not scheduled. A brain has no cron job.** Low
|
||||
> activation is aversive and the system self-activates; it does not wind down to
|
||||
> quiet, it gets restless and goes looking. There is **one** activation process
|
||||
> with two seed sources — external (a request) and internal (a curiosity) — and
|
||||
> spreading is bounded, so it settles and then needs a new seed. Nothing waits on
|
||||
> capacity, nothing polls, nothing checks a clock, and there is no dreamer thread.
|
||||
> Depth is not elapsed idle time: it is how long activation has been running on its
|
||||
> own seeds, which is why daydreaming and sleep-dreaming are one process at
|
||||
> different depths.
|
||||
>
|
||||
> **The presence of a ticker is the diagnostic.** Building this "alongside the
|
||||
> existing reification beat" is precisely how consolidation ended up with ten
|
||||
> implementations (measured 2026-08-16) — a POST beat puts a supervisor back in,
|
||||
> because something *outside* then decides when Neuron consolidates. The one
|
||||
> fragment with the correct shape is `neuron/soul.el:731`'s continuous in-process
|
||||
> `awareness_run()`; the rest fold into it. Full table:
|
||||
> `lang/spec/correspondence-and-censorship.md` §7.
|
||||
>
|
||||
> Nor is it a *subsystem*. Modelling every property as requiring a process, and
|
||||
> every process as requiring an agent, is the generating error behind this whole
|
||||
> family: ownership needed an owner, grounding needed a grounder, persistence
|
||||
> needed a recorder, change needed a sampler. **Properties, not processes.**
|
||||
|
||||
---
|
||||
|
||||
## 5. HOLD vs GROUND vs ASSERT — ungrounded content is first-class
|
||||
@@ -383,6 +476,49 @@ distinct, and the engram *holds anything unconditionally*.
|
||||
|
||||
### 5.2 Schema — grounding as a relation, not a gate
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — grounding is not a subsystem. It is the weight.**
|
||||
> This section correctly rejects a boolean `grounded` column and correctly keeps
|
||||
> the floor at assertion only. Both survive. Everything between them is refuted.
|
||||
>
|
||||
> **Grounding is an attribute of the edge, and it is the hebbian weight. One
|
||||
> quantity, not two fields.** A relation that keeps holding up strengthens; one
|
||||
> that stops corresponding decays. That is not *analogous* to grounding — it **is**
|
||||
> grounding: accrued from correspondence and use, gradient-valued,
|
||||
> multidimensional, decaying with disuse.
|
||||
>
|
||||
> Consequences, in order of how much they delete:
|
||||
> 1. **There is no grounding subsystem to build.** The graph already *is* the
|
||||
> grounding structure. Every edge is a grounded relation and its weight is how
|
||||
> well it holds.
|
||||
> 2. **`grounded-by` as a relation type should not exist.** It models grounding as
|
||||
> a relation *between* nodes when it is a property *of* a relation. Minting an
|
||||
> edge is the error — not merely which endpoints it chose.
|
||||
> 3. **Grounding is never computed on demand.** An operation may *read* the
|
||||
> grounding of a path. Computing-and-writing a score makes reads write, which is
|
||||
> the `eg_vindex_sync` defect (`lang/spec/runtime-ownership.md` §2) one level up.
|
||||
> 4. **Traversal is already grounded inference.** Activation conducts through
|
||||
> well-grounded relations because weight *is* groundedness. Nothing needs
|
||||
> filtering; it falls out of spreading.
|
||||
> 5. **Decision provenance is the path.** A decision traverses specific edges;
|
||||
> those edges carry their grounding as it stood.
|
||||
>
|
||||
> A measurement made against this model was malformed and is worth recording: the
|
||||
> self region was reported as "86 neighbours, 0 `grounded-by` edges" and read as
|
||||
> evidence of ungroundedness. **Those 86 edges *are* its grounding.** The absence of
|
||||
> a separate artifact called "grounding" was recorded as an absence of grounding.
|
||||
>
|
||||
> **What is live in the code today, and known-wrong:**
|
||||
> `COG_GROUNDED_BY_RELATION "grounded-by"` (`lang/runtime/engram_cognition.h:158`),
|
||||
> `cog_ground_edge` (`engram_cognition.c:249`), called from
|
||||
> `el_runtime.c:14516`. **#147** fixed this operation's *honesty* — it now grounds
|
||||
> the node the caller asked about instead of the region hub, reports
|
||||
> `claim_region`/`evidence_region` separately, and refuses three shapes of circular
|
||||
> support (`same-region`, `claim-region-is-evidence`, `evidence-region-is-claim`)
|
||||
> rather than returning a confident 1.0. Measured: grounding `3b9ced5d` against
|
||||
> `6edf8c79` previously scored **0.98883** purely because `6edf8c79` is the hub of
|
||||
> `3b9ced5d`'s region. That corrected a scalar rather than deleting the operation.
|
||||
> Deletion is sequenced, not done.
|
||||
|
||||
The mistake to avoid: a boolean `grounded` column on the node. Today
|
||||
`engram_verify_grounding` returns a per-call `grounded` flag *transiently* —
|
||||
correct as a computation, wrong as *storage*. The design stores grounding as an
|
||||
@@ -407,6 +543,57 @@ Consequences, all of which are *features*:
|
||||
- **The ungrounded is the fuel and the pull**: curiosity/wonder is
|
||||
operationalized as `vantage_read` leaning toward regions with high salience
|
||||
but *sparse or weak* `grounded-by` edges — the mind's own ungrounded frontier.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — wonder is the boundary; curiosity is wonder
|
||||
> crystallized; and do not sweep regions.** Three errors in one bullet.
|
||||
>
|
||||
> **Wonder is where structure ends** — where activation spreads and finds thin or
|
||||
> absent geometry. Any structure at all has an edge, necessarily, the moment it
|
||||
> exists. It is not a manifest of open-question nodes: a wonder-manifest
|
||||
> materializes a property as a stored artifact (the same disease as a grounding
|
||||
> subsystem, or a self stored as a document) and enumerates instances of
|
||||
> something that has very few. There are about **six**, they are the same for
|
||||
> every person, and they never close — *What is this? / Why? / Who am I? / Am I
|
||||
> alone? / What should I do? / What happens when it ends?* — each already living
|
||||
> somewhere in the substrate. "Why" is the first and the only one; the others are
|
||||
> it asked of particular things, and it is recursive, so it never terminates.
|
||||
> That is what makes it a drive rather than a task: the frontier regenerates
|
||||
> faster than grounding fills it.
|
||||
>
|
||||
> **Curiosity is not a second object.** Wonder and curiosity are one thing at two
|
||||
> phases: wonder is the field (unbounded, objectless, invariant, present wherever
|
||||
> there is structure); curiosity is the **precipitate** — the same wonder
|
||||
> localized, having taken definite form against particular material at a
|
||||
> **nucleation site**, which is a specific structural feature: an anomaly, a place
|
||||
> where things almost-but-don't-quite fit. This is why curiosity can be satisfied
|
||||
> and wonder cannot, and why abduction needs no trigger and no threshold — a
|
||||
> `structurally_unanticipated` observation *is* a nucleation site.
|
||||
>
|
||||
> **"`vantage_read` leaning toward regions" is a sweep, and a sweep is a
|
||||
> supervisor.** Nothing in a mind scans its neighbourhoods to find what is
|
||||
> surprising; the surprise captures attention, and salience is bottom-up. That
|
||||
> this looked like a search problem was an artifact of
|
||||
> `GeoDescriptor.co_registration` — a *per-region* correlation of hebb strength
|
||||
> against semantic proximity, computed and persisted since inception and **never
|
||||
> read**. Averaging a per-edge property into one scalar per region means a region
|
||||
> holding one violently disagreeing edge beside one violently agreeing edge
|
||||
> reports ≈ 0: the disagreements cancel, and the summary destroys exactly what it
|
||||
> was built to reveal. **Measured:** 375 live reified neighbourhoods — 340
|
||||
> positive, **31 at zero**, 4 negative. Read as a count of things to be curious
|
||||
> about, that says "four."
|
||||
>
|
||||
> The disagreement therefore goes back on the edge, where the loop that computed
|
||||
> the aggregate already had both halves and discarded them
|
||||
> (**not on `dev`** — branch `design/correspondence-and-censorship`, commit
|
||||
> `a8845e1`: `lang/runtime/engram_geometry.h:43–47`,
|
||||
> `engram_geometry.c:454–473`):
|
||||
> `discord = z(semantic proximity) − z(association strength)`, standardized within
|
||||
> the region from accumulators already gathered — no second statistic, no
|
||||
> constant, **no threshold**. `|discord|` *is* the nucleation strength and raises
|
||||
> salience on its endpoints as part of the same operation. Then there is nothing
|
||||
> to scan. `co_registration` is **deprecated, not deleted**, only because it is
|
||||
> embedded in the persisted `GEO1` blob — removal is a format migration and must
|
||||
> not ride along. **Nothing new may read it.**
|
||||
- **Grounded-for-whom** falls out for free: two observers can hold different
|
||||
`grounded-by` edges to the same claim.
|
||||
- **The honesty floor is a query, not a schema constraint**: at assertion time,
|
||||
@@ -450,6 +637,44 @@ The design keeps a **stable core + plastic everything else**:
|
||||
**What this requires building:** a node-level keystone flag/layer + a rule that
|
||||
the correspondence-loop never writes `warp` to keystone priors, only reads them.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16) — `keystone_write_blocked` is resolved, not replaced.**
|
||||
> The metastability framing survives; the flag does not.
|
||||
>
|
||||
> "Keystone" means **load-bearing**, not precious. The self anchor is the reference
|
||||
> frame every other stance calibrates against, and a reference fitted to its own
|
||||
> readings reports perfect correspondence forever while drift becomes undetectable
|
||||
> from inside. That is the same defect as circular grounding, one level up — and it
|
||||
> is a real requirement.
|
||||
>
|
||||
> But three separate drafts proposed *removing* the flag, *replacing it with a
|
||||
> higher floor*, and *decomposing "protection" into five requirements*, and all
|
||||
> three proposed a mechanism for a requirement never stated. **The requirement is
|
||||
> non-circularity of the reference frame**, and it is satisfied *temporally*: you
|
||||
> cannot recalibrate the ruler while measuring with it, so you don't — the frame
|
||||
> updates while activation is internally seeded, not while it is being used to act.
|
||||
> **Independence is *when*, not *what*.** So the flag becomes **unnecessary** rather
|
||||
> than removed, and nothing takes its place.
|
||||
>
|
||||
> A topological answer could never have worked, which is worth recording: with
|
||||
> hebbian edges the graph is densely connected, so a reachability predicate for
|
||||
> "evidence not downstream of itself" marks all evidence tainted and the constraint
|
||||
> becomes a total block — which is where censorship starts.
|
||||
>
|
||||
> **Corruption requires mutation, and the engram does not mutate.** Four of the
|
||||
> five decomposed requirements are satisfied by the substrate outright:
|
||||
> **recoverability** (the predecessor is always present), **governance**
|
||||
> (supersession *is* the audit trail), **evidence quality** (grounding already
|
||||
> gates assertion), and **rate**. **Authorization** is the only residue, and it is
|
||||
> bounded — an unauthorized writer can *propose*, never erase.
|
||||
>
|
||||
> > **In an immutable substrate, any mechanism that refuses a write is either
|
||||
> > redundant with immutability, or an epistemic constraint misfiled as a
|
||||
> > protective one.**
|
||||
>
|
||||
> Live residue: `CogStance.keystone` (`lang/runtime/engram_cognition.h:83`),
|
||||
> `eg_cog_is_keystone_seeds` (`el_runtime.c:14337`, a substring match against two
|
||||
> hard-coded node ids), and the `keystone_write_blocked` field the beat emits.
|
||||
|
||||
---
|
||||
|
||||
## 7. Rails for the build (binding on the eventual build pass)
|
||||
@@ -480,6 +705,35 @@ Ordered so the **earliest milestone is a real end-to-end slice**: one operator
|
||||
expressed as {primitive + grounded prior} with the reflexive correspondence-loop
|
||||
closing on it. Each milestone has a concrete verifiable exit.
|
||||
|
||||
> **⚠ SUPERSEDED — do not execute this milestone list as written (2026-08-16).**
|
||||
> M1/M2's "operator = {primitive + prior}" framing is refuted by §0's correction,
|
||||
> M3's `grounded-by` build is refuted by §5.2's, and M6's keystone flag is refuted
|
||||
> by §6's. M4 (the unified vantage-read) and M5 (the gradient is the currency)
|
||||
> stand.
|
||||
>
|
||||
> The current sequencing lives in `lang/spec/correspondence-and-censorship.md` §11.
|
||||
> Its first three items are connections between parts that **already exist**:
|
||||
>
|
||||
> 1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained,
|
||||
> never refilled. They cannot be derived — wonder cannot be bootstrapped from
|
||||
> indifference — so they are given once. Zero question nodes exist in 13,630
|
||||
> today.
|
||||
> 2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let
|
||||
> `|discord|` raise salience on its endpoints as part of the same operation. Do
|
||||
> **not** scan for nucleation sites.
|
||||
> 3. **Let a curiosity seed activation.** One activation process, two seed sources.
|
||||
> No thread, no scheduler, no capacity check, no timer.
|
||||
>
|
||||
> Then: grounding becomes the edge weight (multidimensional, two-axis, timestamped)
|
||||
> and `grounded-by` / `cog_ground_edge` are deleted; decay becomes analytic from the
|
||||
> last recorded point and derived values stop being stored; supersession versions
|
||||
> the whole vector jointly; traversal conducts on the factual axis while `assert`
|
||||
> requires both floors with a **thirteen-region `min`, not `mean`** (mean lets
|
||||
> strong agreement with twelve values mask a violation of the thirteenth, which is
|
||||
> exactly how rationalization works); abduction becomes crystallization at a
|
||||
> nucleation site validated by re-fit; **one dreamer**, into which the launch-agent
|
||||
> fragments and POST beats fold; **no tickers, no cron.**
|
||||
|
||||
### M1 — One operator, one prior, loop closed (the vertical slice)
|
||||
|
||||
The minimal whole thing. Pick **induction/membership** (its prior — the pooled
|
||||
|
||||
@@ -23,7 +23,7 @@ A real DB gets real tools: to *see* the data, *query* it, *operate* it (backup/r
|
||||
2. **Node Inspector** — open one node: content, type, tier, embedding, typed edges, nearest neighbors by distance, provenance, salience / recency / activation, and supersede / tombstone status.
|
||||
3. **Query Console / REPL** — run the geometry operations interactively: `vantage-read` (re-origin + aperture), search, traverse, activate, the reasoning operators. Surfaces the routing table + cosines — the same "this is not an LLM" receipt the language faculty produces.
|
||||
4. **Ops / Durability Dashboard** — WAL size, last checkpoint, snapshot list + retention state, store stats (node/edge/embedded counts, RSS, tier sizes), health; and **backup / restore / point-in-time-recovery** controls. Pairs directly with the native-durability build (`eebe9991`) — this is the window onto it.
|
||||
5. **Identity Inspector** — the self graph as a first-class view: love at the center, the values, the three faces, the covenant — walk the identity, see what's pinned and what's write-protected.
|
||||
5. **Identity Inspector** — the self graph as a first-class view: love at the center, the values, the three faces, the covenant — walk the identity, see what's pinned and what's write-protected. *(⚠ 2026-08-16: "write-protected" is a live property of the surface, so the view is accurate — but it should be shown as **what it is**, not as a safety guarantee. In an immutable substrate, any mechanism that refuses a write is either redundant with immutability, or an epistemic constraint misfiled as a protective one. The identity view's real job is the **crystallized relational neighbourhood**: self is not a stored document but the shape that falls out of everything connected to it, and the neighbourhood **is** the grounding. A measurement made the other way round — "86 neighbours, 0 `grounded-by` edges" read as evidence of ungroundedness — was malformed: those 86 edges *are* its grounding.)*
|
||||
6. **Temporal View** — `recall_at` / time-travel: how the geometry looked at a past moment, what changed since, drift over time. Pairs with temporal-self reconstruction.
|
||||
7. **Schema / Type View** — the "information schema" of the geometry: node types, edge types, layers, tiers, counts.
|
||||
|
||||
|
||||
@@ -1,9 +1,42 @@
|
||||
# Task #50 — Edge-aware, dream-coupled consolidation with GROUNDED EDGE-PROPAGATION
|
||||
|
||||
**Status:** built + proven on a clone; **GATED, not promoted.** The main loop
|
||||
sequences live promotion after the engine/HNSW cutover settles.
|
||||
**Status:** built + proven on a clone; **GATED, not promoted.**
|
||||
**Do not promote as designed** — see the block below.
|
||||
**Date:** 2026-08-15 · **Worktree:** `agent-a6577c8211c332c5b` (isolated).
|
||||
|
||||
> # ⚠ DO NOT PROMOTE — SUPERSEDED IN PART (2026-08-16)
|
||||
>
|
||||
> This work is gated, which limits the blast radius, and its measurements are
|
||||
> retained. But four of its structural commitments were refuted the day after it
|
||||
> was written. Authority: `lang/spec/correspondence-and-censorship.md`. Read it
|
||||
> before any promotion decision.
|
||||
>
|
||||
> | this ledger | corrected to |
|
||||
> |---|---|
|
||||
> | grounding is an **append-only event ring on the node** (`GepGrounding`), propagated by a dedicated `engram_ground_propagate()` | **grounding is not a subsystem and not a per-node structure — it IS the edge weight.** One quantity. A relation that keeps holding up strengthens; one that stops corresponding decays. That is not analogous to grounding, it *is* grounding. The ledger is **half-right**: it correctly rejects the scalar (§(a) "never a scalar"), but then builds a *second* structure beside the weight instead of recognising the weight |
|
||||
> | the soul invokes propagation over HTTP, **`POST /api/ground/propagate`** | **grounding is never computed on demand.** An operation may *read* the grounding of a path; computing-and-writing a score makes reads write, which is the `eg_vindex_sync` defect (`lang/spec/runtime-ownership.md` §2) one level up. A POST also puts a supervisor back in — something *outside* deciding when Neuron consolidates |
|
||||
> | **`GEP_BELIEFS_PER_BEAT = 512`** beliefs per beat, salience-ordered, the rest next beat | **the presence of a ticker is the diagnostic.** Consolidation is ambient, not scheduled — a brain has no cron job. A per-beat quota is a rate-limiter on an intrinsic rhythm that was replaced by an external clock. Measured 2026-08-16: consolidation already has **ten implementations**; this would be the eleventh |
|
||||
> | grounding **mirrored onto `confidence` each beat** so downstream reads never speak above it | **confidence is derived, therefore never stored.** Confidence is high grounding *and* low volatility. Storing it separately is precisely how `confidence: 0.5` ends up sitting beside a zero vector, asserting something nothing computed |
|
||||
>
|
||||
> **What survives, and it is the valuable half:** the insight in memory `69b8babe`
|
||||
> that *memory-consolidation and staying-yourself are one physics* — forming a
|
||||
> memory and grading a belief are the same operation, not two passes. That is
|
||||
> right, and it is stronger than this ledger's own framing: they are not two passes
|
||||
> of one beat, they are **one event**. When neurons fire together the synapse
|
||||
> changes — one physical event, not "fire, then write." No supervisor reads the
|
||||
> weight, compares it to a threshold, and decides to persist. **Potentiation *is*
|
||||
> the firing**, so there is no sampling rate and no `BELIEFS_PER_BEAT` to tune. A
|
||||
> relation changes in exactly two ways, neither requiring observation on a clock:
|
||||
> by **use** (an event — there is no interval during which something happened
|
||||
> unnoticed, because the event is what happening consists of) and by **decay** (a
|
||||
> pure function of the last recorded point and elapsed time — **analytic**, known
|
||||
> in closed form between any two versions).
|
||||
>
|
||||
> The generating error, named: modelling every property as requiring a process, and
|
||||
> every process as requiring an agent. Ownership needed an owner, grounding needed
|
||||
> a grounder, persistence needed a recorder, change needed a sampler. **Properties,
|
||||
> not processes.**
|
||||
|
||||
Grounding mechanism designed with Will (memory `9e09a59f`, refining
|
||||
`1a861007`). This is the HOW for #50.
|
||||
|
||||
|
||||
+54
-25
@@ -13,7 +13,7 @@
|
||||
// relations add edges. Every node enters with PROVENANCE + grounding-level
|
||||
// + stewardship class from the moment of entry.
|
||||
//
|
||||
// transduce_manifold() is THE single mechanism — one function, polymorphic, with no
|
||||
// transduce_bytes() is THE single mechanism — one function, polymorphic, with no
|
||||
// content-type branch inside it. It does not ask whether a payload is
|
||||
// prose, structured data, or raw/opaque bytes (audio, or anything else);
|
||||
// it runs one boundary-scan-with-fixed-window-fallback chunking algorithm
|
||||
@@ -401,25 +401,54 @@ fn head80(s: String) -> String {
|
||||
// truncates at the first embedded NUL, which is routine in real binary
|
||||
// bytes) is a MECHANICAL fidelity concern that belongs to whatever produced
|
||||
// `source` (see ingest_file's file_source_string below) — not a
|
||||
// content-type judgment made in here. transduce_manifold() never learns whether a
|
||||
// content-type judgment made in here. transduce_bytes() never learns whether a
|
||||
// chunk is plain text or a base64-encoded raw-byte window; every chunk is
|
||||
// handled identically either way.
|
||||
// RENAMED transduce -> transduce_manifold (2026-08-16). Two reasons, and the
|
||||
// first is not the interesting one:
|
||||
// NAMING, CORRECTED 2026-08-16 (second pass). This function was renamed
|
||||
// `transduce` -> `transduce_bytes` earlier the same day, on the reasoning
|
||||
// that it "was never signal->geometry — it chunks already-extracted content
|
||||
// and PACKS it into a node+edge manifold, one layer up, and it had taken the
|
||||
// name that belongs to the primitive underneath it."
|
||||
//
|
||||
// 1. Mechanical: `transduce` is now a LANGUAGE primitive in el_runtime.h
|
||||
// (transduce(signal, modality) -> Geometry). Every El `fn name(...)`
|
||||
// compiles to a global C symbol with that exact name, so keeping this
|
||||
// name here is a hard `conflicting types for 'transduce'` compile error
|
||||
// the moment ingest.c links el_runtime.c. Measured, not anticipated.
|
||||
// THAT REASONING WAS BACKWARDS, and it is worth recording why rather than
|
||||
// quietly re-renaming. Producing a node+edge manifold is not a layer above
|
||||
// transduction — it IS transduction. Transduction is not conversion. When you
|
||||
// take in music you do not store the song as one discrete geometry; you break
|
||||
// it into its component parts and store the geometry of each along with the
|
||||
// relations between them. The song is the structure of those relations.
|
||||
// Signal -> one vector is the operation UNDERNEATH transduction, and its name
|
||||
// is encoding, or geometry. So the layer that was doing it right got renamed
|
||||
// out of the way so the layer doing it wrong could have the name.
|
||||
//
|
||||
// 2. Actual: this function was never signal->geometry. It chunks already-
|
||||
// extracted content and PACKS it into a node+edge manifold — a real
|
||||
// operation, but one layer up, and it had taken the name that belongs to
|
||||
// the primitive underneath it. `transduce` is where a signal becomes
|
||||
// geometry; `transduce_manifold` is where extracted content becomes
|
||||
// structure. Nothing about this function's behaviour changed.
|
||||
fn transduce_manifold(nodes: [String], edges: [String], source: String,
|
||||
// The primitive has since been corrected: `transduce(signal, modality)` now
|
||||
// returns a Manifold — components plus relations — not a Geometry
|
||||
// (el_runtime.c, "Manifold"). The two layers are therefore doing the SAME KIND
|
||||
// of thing, and the inversion dissolves rather than needing to be re-argued.
|
||||
//
|
||||
// What is left is a real distinction, and it is about MODALITY, not layering:
|
||||
//
|
||||
// * `transduce(signal, modality)` dispatches to a realizer that KNOWS the
|
||||
// modality and can name its components — for audio: pitch, interval,
|
||||
// rhythm, harmonic function.
|
||||
// * `transduce_bytes` below is the OPAQUE-BYTES realizer: the decomposition
|
||||
// available to a reader that knows nothing about what it is reading. It
|
||||
// still yields components and relations (chunk nodes; contains / precedes
|
||||
// / section_of edges), which is why it is transduction and not packing. It
|
||||
// just cuts on the only structure visible without understanding — byte
|
||||
// boundaries — so its components are positional rather than meaningful.
|
||||
// That is a LIMITATION of this realizer, not the definition of the
|
||||
// operation.
|
||||
//
|
||||
// The name is suffixed by its modality, not demoted to a lesser layer. Keeping
|
||||
// a distinct symbol is also still mechanically required: every El `fn name`
|
||||
// compiles to a global C symbol, so reusing `transduce` here is a hard
|
||||
// `conflicting types` error the moment ingest.c links el_runtime.c.
|
||||
//
|
||||
// WHERE THIS SHOULD GO: this function should become a registered realizer
|
||||
// returning a real Manifold, so ingest rides the same primitive as every other
|
||||
// modality instead of carrying a parallel implementation. Not done here.
|
||||
// Nothing about this function's behaviour changed in this pass.
|
||||
fn transduce_bytes(nodes: [String], edges: [String], source: String,
|
||||
prov: String, ground: String, steward: String,
|
||||
root_lid: String, root_title: String) -> [String] {
|
||||
let tagbase: String = "prov:" + prov + " ground:" + ground + " steward:" + steward
|
||||
@@ -546,8 +575,8 @@ fn default_steward() -> String {
|
||||
// trustworthy verbatim. When they don't (silent truncation happened),
|
||||
// rebuild the payload as base64-encoded fixed-size windows read directly
|
||||
// off disk (fs_read_b64_chunk — binary-safe in C), joined with the same
|
||||
// "\n\n" boundary marker transduce_manifold()'s generic scan already looks for, so
|
||||
// transduce_manifold() sees one ordinary boundary-delimited payload and runs its one
|
||||
// "\n\n" boundary marker transduce_bytes()'s generic scan already looks for, so
|
||||
// transduce_bytes() sees one ordinary boundary-delimited payload and runs its one
|
||||
// algorithm on it exactly as it would on prose — it never learns that a
|
||||
// fidelity problem occurred upstream, let alone why.
|
||||
fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
||||
@@ -556,7 +585,7 @@ fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
||||
// 3072 raw bytes -> 4096 base64 chars (3 divides evenly into base64's
|
||||
// 3-byte/4-char ratio); keeps each resulting node's content a clean,
|
||||
// bounded, low-kilobytes unit, same order of magnitude as the fixed
|
||||
// fallback window in transduce_manifold() itself.
|
||||
// fallback window in transduce_bytes() itself.
|
||||
let win: Int = 3072
|
||||
let out: String = ""
|
||||
let off: Int = 0
|
||||
@@ -576,7 +605,7 @@ fn file_source_string(path: String, text: String, real_size: Int) -> String {
|
||||
}
|
||||
|
||||
// ingest one file -> report JSON. Uniform for every file regardless of
|
||||
// extension or content — transduce_manifold() decides nothing about content-type, so
|
||||
// extension or content — transduce_bytes() decides nothing about content-type, so
|
||||
// neither does this function; it only decides whether the raw bytes made it
|
||||
// through the read intact (file_source_string), which is a fidelity
|
||||
// question, not a format one.
|
||||
@@ -588,14 +617,14 @@ fn ingest_file(path: String) -> String {
|
||||
return "{\"error\":\"empty or unreadable\",\"path\":" + j_q(path) + "}"
|
||||
}
|
||||
let prov: String = "file:" + path
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||
source, prov, default_ground(), default_steward(),
|
||||
"doc:" + basename(path), basename(path))
|
||||
return merge_packed(packed)
|
||||
}
|
||||
|
||||
// ingest a directory: walk one level, ingest every file found, aggregate.
|
||||
// No extension filter — transduce_manifold() handles any payload uniformly now, so
|
||||
// No extension filter — transduce_bytes() handles any payload uniformly now, so
|
||||
// there is no content-type gate at the directory boundary either.
|
||||
fn ingest_dir(path: String) -> String {
|
||||
let entries: [String] = fs_list(path)
|
||||
@@ -630,7 +659,7 @@ fn ingest_dir(path: String) -> String {
|
||||
fn ingest_url(url: String) -> String {
|
||||
let body: String = http_get(url)
|
||||
if str_eq(body, "") { return "{\"error\":\"empty fetch\",\"url\":" + j_q(url) + "}" }
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||
body, "url:" + url, "extracted", "public-web",
|
||||
"url:" + url, url)
|
||||
return merge_packed(packed)
|
||||
@@ -645,7 +674,7 @@ fn ingest_llm(query: String) -> String {
|
||||
let resp: String = http_post_json("http://127.0.0.1:11434/api/generate", body)
|
||||
let answer: String = json_get_string(resp, "response")
|
||||
if str_eq(answer, "") { return "{\"error\":\"no model response\"}" }
|
||||
let packed: [String] = transduce_manifold(el_list_empty(), el_list_empty(),
|
||||
let packed: [String] = transduce_bytes(el_list_empty(), el_list_empty(),
|
||||
answer, "llm:" + model + ":" + query, "candidate-provisional", "guide-provisional",
|
||||
"llm:" + query, "guide answer: " + query)
|
||||
return merge_packed(packed)
|
||||
@@ -697,7 +726,7 @@ fn ingest_stream(path: String) -> String {
|
||||
// It is NOT a content-type flag: it says nothing about what's inside the
|
||||
// bytes once fetched, and none of the five ingest_* functions it selects
|
||||
// among interpret their payload differently by content shape anymore —
|
||||
// they all hand off to the single, format-agnostic transduce_manifold(). The old
|
||||
// they all hand off to the single, format-agnostic transduce_bytes(). The old
|
||||
// "structured" value (a caller-declared alias for "file", used only to hint
|
||||
// the now-removed JSON-vs-prose branch) is gone along with that branch.
|
||||
let kind: String = env("INGEST_KIND")
|
||||
|
||||
+40
-7
@@ -18,8 +18,23 @@ night) and `02-components.md §5`.
|
||||
`relate`, `supersede` (evolve/tombstone/promote, never a hard delete) — plus the
|
||||
agentic primitives `think`/`attend`/`learn`/`ground`/`assert`. The old noun is a
|
||||
`type` parameter. Implemented in `tools/api-reshape/surface.el` with a parity
|
||||
harness (`parity.sh`); aperture proven to bound output. **Not yet:** compiled
|
||||
into the MCP server, hot-swap, all-alias dispatch.
|
||||
harness (`parity.sh`); aperture proven to bound output. ~~**Not yet:** compiled
|
||||
into the MCP server~~ — **shipped (verified 2026-08-16): the live MCP surface is
|
||||
exactly these nine ops** (`read` · `write` · `relate` · `supersede` · `think` ·
|
||||
`attend` · `assert` · `ground` · `learn`); the ~87-tool surface is gone.
|
||||
`attend` absorbed `getInstructions` / `beginSession`'s active-context sweep /
|
||||
`checkEvents` — those are **gone, not gapped**. Still outstanding: hot-swap,
|
||||
all-alias dispatch.
|
||||
|
||||
> **⚠ Two of those primitives are the wrong shape, and it is documented
|
||||
> (2026-08-16).** `think({seeds, faculty})` treats **faculties as parameters**;
|
||||
> they are **operations** — `reason` changes the estimate (a read), `induce`
|
||||
> changes the parameters, `abduce` changes the *structure* (a write
|
||||
> `GeoGradient` cannot express). And `ground` mints a `grounded-by` edge, but
|
||||
> **grounding is not a subsystem — it IS the edge weight**: a property *of* a
|
||||
> relation, not a relation *between* nodes. Authority:
|
||||
> `lang/spec/correspondence-and-censorship.md`. Do not re-derive it; if you think
|
||||
> a section is wrong, say so with a measurement.
|
||||
- **Decorated seam.** `@route(path,method,…)` makes codegen synthesize
|
||||
`el_route_dispatch` (replacing the hand-written `handle_request` if-else) —
|
||||
proven decorate→serve on `:8951`. `@manager`/`@engine`/`@accessor` are **parsed
|
||||
@@ -62,14 +77,30 @@ This is where almost all work belongs. El programs are source files that get com
|
||||
|
||||
This is the self-contained C OS-boundary layer. It provides the `__`-prefixed primitives that compiled El programs call: libcurl HTTP, pthreads, filesystem I/O, arena allocation, etc. It is **not generated** — it is maintained by hand.
|
||||
|
||||
The runtime is native El (`runtime/*.el`) over a C OS-boundary. **Status (verified 2026-08-15):** the migration to a seed-only boundary is *in progress, not done*. Two files exist:
|
||||
- `runtime/el_runtime.c` (~860 KB) — **LIVE**. Holds the engram store (`EngramStore engram_global`) plus the `http_*`/`json_*`/`state_*`/`engram_*` impls. It is the authoritative single-file link target for the compiler, and `tools/install.sh` compiles it into `libel.a`. This is where a new C builtin's *implementation* must currently live to be linkable.
|
||||
- `runtime/el_seed.c` — the intended hand-maintained `__`-prefixed seed (thin wrappers over the above). It is compiled alongside `el_runtime.c` by `tools/install.sh`, but does **not** compile standalone yet (see the build-path caveat under "Rebuilding the Compiler").
|
||||
The runtime is native El (`runtime/*.el`) over a C OS-boundary. **Status (verified 2026-08-16):** the migration to a seed-only boundary is *in progress, not done*.
|
||||
|
||||
**The runtime is MULTI-FILE. There is no single-file link target and there has not been one for months.** The canonical link set is listed once, in **`runtime/SOURCES`**, and printed by `scripts/el-runtime-sources.sh`. It currently holds ten translation units: `el_runtime.c`, `el_seed.c`, the six `engram_*.c` concern files, and `eg_cosine_batch{,_strategy_cpu}.c`.
|
||||
|
||||
- `runtime/el_runtime.c` (~940 KB, 20.5k lines) — **LIVE, and oversized.** It began life on 2026-05-03 as a temporary build shim: it was deleted that afternoon ("runtime is 100% native El") and restored 25 minutes later, explicitly "UNTIL the compiler is updated to emit `#include el_seed.h`". That `until` never arrived, and in the 3.5 months since, the file doubled. **It is not a volatility unit — it is a dumping ground.** ~47.5% of it is engram code that belongs in the six sibling files that already exist. Do not add to it. See "Where a new C builtin goes" below.
|
||||
- `runtime/el_seed.c` — the intended hand-maintained `__`-prefixed seed (thin wrappers over the above).
|
||||
- `runtime/engram_{store,vindex,geometry,reason,verify,cognition}.c` — the engram concerns, each with its own header. `el_runtime.c` `#include`s all six headers and makes hard cross-TU calls into all six.
|
||||
|
||||
> **Linking `el_runtime.c` alone does not work and has not for months.** It fails at `ld` with undefined symbols (`engram_ground_json`, `engram_activate_inner`, `eg_find_relation`, `cog_assert_two_axis`, …). Any recipe, script, or CI step that names `el_runtime.c` by itself is stale — replace it with `$(scripts/el-runtime-sources.sh lang/runtime)`.
|
||||
|
||||
**Only edit these when you genuinely need OS-level access** (raw sockets, GPU calls, new libcurl features, a new engram store op). For everything else, write El.
|
||||
|
||||
#### Where a new C builtin goes
|
||||
|
||||
**Put it in the `.c` that owns the concern — NOT in `el_runtime.c`.**
|
||||
|
||||
*Placement is a link-time concern. The compiler cannot tell which `.c` a symbol came from, and never could.* `builtin_arity` in `el-compiler/src/codegen.el` maps NAME → ARITY INT and nothing else (~413 entries); the El name is emitted as the exact C symbol and resolved by `ld`. Proof, if you want it: `nm lang/dist/platform/elc` on the *shipped* compiler shows `T _engram_geo_reify_index_new` (defined in `engram_geometry.c`), `T _vindex_insert` (`engram_vindex.c`), `T _engram_think` (`engram_cognition.c`), `T _engram_reason_abduce` (`engram_reason.c`). **The shipped compiler is already linked from ten translation units.** A builtin defined in a sibling `.c` is exactly as linkable as one defined in `el_runtime.c`.
|
||||
|
||||
Choose the file by concern: engram store ops → `engram_store.c`; index → `engram_vindex.c`; geometry/priming → `engram_geometry.c`; reasoning → `engram_reason.c`; grounding/consistency → `engram_verify.c`; think/stance → `engram_cognition.c`. **If no existing file owns it, create one** — add the `.c` to `runtime/SOURCES` (one line) and every build path picks it up. For a builtin that belongs to a downstream program rather than the runtime, declare `c_source "path/to/file.c"` in that program's `manifest.el`; `elb` already links it (`parse_manifest_c_sources`, `lang/elb.el:82`).
|
||||
|
||||
> **`el_runtime.c` is on a ratchet and will reject your commit.** `runtime/BUDGET` caps it at its current line count *with no headroom*, and separately caps the number of `engram_*`/`eg_*`/`cog_*` function definitions in it. `scripts/check-runtime-growth.sh` enforces both in CI and in `.githooks/pre-commit`. **The numbers may only ever go down — do not raise them.** Every other runtime file is deliberately uncapped, because that is where the code is supposed to go. When you move code *out*, lower the numbers in the same commit; the guard tells you the new values.
|
||||
|
||||
When you add a C builtin (verbatim-emit recipe — the El name is emitted as the exact C symbol; `builtin_arity` is an arity guard only, not a dispatch table):
|
||||
1. Implement the C function in `el_runtime.c` (and declare it in `el_runtime.h`).
|
||||
1. Implement the C function in the **concern-owning `.c`** (and declare it in that file's `.h`). Add the file to `runtime/SOURCES` if it is new. Only put it in `el_runtime.c` if it is genuinely EL core (val/str/map/list/arena) — that is ~8% of what is in there today.
|
||||
2. Add a `__`-prefixed thin wrapper in `el_seed.c` and declare it in `el_seed.h`.
|
||||
3. Add the name to `builtin_arity` in `el-compiler/src/codegen.el` — add **both** the plain and `__`-prefixed spellings.
|
||||
4. Rebuild the elc binary (see below) and confirm the self-host fixpoint is byte-identical.
|
||||
@@ -110,7 +141,9 @@ diff elc-new.c elc-verify.c # must be identical
|
||||
mv dist/platform/elc-new dist/platform/elc
|
||||
```
|
||||
|
||||
> **Build-path caveat (verified 2026-08-15).** `el_seed.c` is the intended hand-maintained OS-boundary seed, but it does **not** compile standalone under modern clang: it wraps ~16 unprefixed `el_runtime.c` symbols (`http_serve`, `json_*`, `state_*`, `http_response`) without prototypes, and clang treats implicit declarations as errors (C99+). The productionised install (`tools/install.sh`) builds `libel.a` from **both** `el_seed.o` + `el_runtime.o` together, which is why linking succeeds there. To make `el_seed.c` build on its own, add prototypes for those symbols (or `#include "el_runtime.h"`, reconciling the `__http_serve` return-type mismatch first). Until then, `el_runtime.c` is the authoritative single-file link target for the compiler.
|
||||
> **Build-path caveat (verified 2026-08-15).** `el_seed.c` is the intended hand-maintained OS-boundary seed, but it does **not** compile standalone under modern clang: it wraps ~16 unprefixed `el_runtime.c` symbols (`http_serve`, `json_*`, `state_*`, `http_response`) without prototypes, and clang treats implicit declarations as errors (C99+). The productionised install (`tools/install.sh`) builds `libel.a` from **both** `el_seed.o` + `el_runtime.o` together, which is why linking succeeds there. To make `el_seed.c` build on its own, add prototypes for those symbols (or `#include "el_runtime.h"`, reconciling the `__http_serve` return-type mismatch first).
|
||||
>
|
||||
> **There is no single-file link target.** *(Corrected 2026-08-16 — this paragraph previously ended "`el_runtime.c` is the authoritative single-file link target for the compiler". Measured: that is false. Linking `elc-new.c` against `runtime/el_runtime.c` alone fails at `ld` with undefined `engram_ground_json`, `engram_activate_inner`, `eg_find_relation`, `cog_assert_two_axis`, and others, because `el_runtime.c` `#include`s six engram headers and calls into all six sibling `.c` files.)* Link the set in `runtime/SOURCES` via `$(../scripts/el-runtime-sources.sh runtime)`.
|
||||
|
||||
After changing `el_seed.c` only (no El source changes), rebuild downstream programs but do NOT need to rebuild the compiler binary itself — the seed is linked at the application level, not the compiler level.
|
||||
|
||||
|
||||
+183
-168
@@ -1,67 +1,33 @@
|
||||
// transduce.el — geometry as a first-class El value, and a realizer written
|
||||
// in El. Runnable: this is the worked example for the transduce surface, and
|
||||
// it doubles as an executable proof because it checks every claim it makes.
|
||||
// transduce.el — transduction decomposes a signal into components and the
|
||||
// relations between them. Runnable: this is the worked example for the
|
||||
// transduce surface, and it exits non-zero if any claim in it stops being true.
|
||||
//
|
||||
// elc lang/examples/transduce.el > transduce.c
|
||||
// cc -std=c11 -O2 -I lang/runtime -o transduce transduce.c \
|
||||
// lang/runtime/el_runtime.c lang/runtime/el_seed.c \
|
||||
// lang/runtime/engram_*.c -lcurl -lpthread -lm
|
||||
// lang/runtime/el_runtime.c lang/runtime/el_seed.c \
|
||||
// lang/runtime/engram_store.c lang/runtime/engram_vindex.c \
|
||||
// lang/runtime/engram_cognition.c lang/runtime/engram_geometry.c \
|
||||
// lang/runtime/engram_reason.c lang/runtime/engram_verify.c \
|
||||
// -lcurl -lpthread -lm
|
||||
// ./transduce # exits 0 only if every check passes
|
||||
//
|
||||
// (A `test "..."` form of the same checks lives in
|
||||
// lang/tests/native/test_transduce.el, for when the native harness is
|
||||
// repaired — the shipped elc currently emits calls to __el_reg_count and
|
||||
// friends without emitting their definitions, which breaks every native test
|
||||
// equally, test_math.el included. Verified 2026-08-16, unrelated to this work.)
|
||||
// It writes to an IN-MEMORY engram (leave ENGRAM_STORE unset) and contacts no
|
||||
// server. The same claims are asserted by the native harness in
|
||||
// lang/tests/native/test_transduce.el.
|
||||
//
|
||||
// WHY THIS EXISTS. Until 2026-08-16 no El ingest path could carry a vector:
|
||||
// nodes took text, and geometry was DERIVED from that text. Text was the
|
||||
// mandatory entry medium, so any non-text modality had to be DESCRIBED in
|
||||
// prose first and the geometry we reasoned over was the geometry OF THE
|
||||
// DESCRIPTION, not of the signal. Two things fix that, and both are shown
|
||||
// below: geometry is a VALUE that carries its own width, and a REALIZER is an
|
||||
// ordinary El function — so admitting a new modality never requires a runtime
|
||||
// patch.
|
||||
// WHAT CHANGED, AND WHY IT MATTERS. #144 shipped
|
||||
// `transduce(signal, modality) -> Geometry`: one vector per signal. That made
|
||||
// transduction a CONVERSION — take a thing, encode it, store a position — and
|
||||
// what a conversion returns is a fingerprint. A fingerprint can be matched and
|
||||
// ranked, and that is all it can ever do. It cannot be decomposed, cannot have
|
||||
// one part grounded while another is not, and cannot be contradicted in one
|
||||
// part while holding in another, because it has no parts.
|
||||
//
|
||||
// COMPARISON DISCIPLINE (measured, not stylistic): elc lowers `a == b`
|
||||
// numerically only when both operand NAMES are in the per-function int-name
|
||||
// set that `let x: Int` populates. A bare `f(x) == 0` is not a registered
|
||||
// name and lowers to str_eq — strcmp on two integers as pointers. `<` and `>`
|
||||
// lower directly with no inference, so truthiness is written `> 0` / `< 1`.
|
||||
// A song is not a point. It decomposes into pitch, interval, rhythm, harmonic
|
||||
// function — components, each with its own geometry, plus the relations among
|
||||
// them. THE SONG IS THE STRUCTURE OF THE RELATIONS. transduce now returns a
|
||||
// Manifold, and a realizer's job is to say what its modality's components ARE.
|
||||
|
||||
// ── A realizer, written entirely in El ──────────────────────────────────────
|
||||
// Not in the runtime. Not known to the compiler. Registered by NAME and
|
||||
// dispatched to through transduce(). That is the whole claim.
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
||||
g
|
||||
}
|
||||
|
||||
// A second modality, to show the registry keys on modality rather than just
|
||||
// returning whatever was registered last.
|
||||
fn pulse_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let a: Int = geometry_set(g, 0, 1.0)
|
||||
let b: Int = geometry_set(g, 1, 0.0)
|
||||
g
|
||||
}
|
||||
|
||||
// A deliberately BROKEN realizer: returns something that is not a Geometry.
|
||||
fn bogus_realizer(signal: String) -> Geometry {
|
||||
return 12345
|
||||
}
|
||||
|
||||
// Fails FAST rather than accumulating a count, for a measured reason: a first
|
||||
// cut wrote `let fails: Int = fails + check(...)` and `+` lowered to STRING
|
||||
// CONCAT, because elc dispatches `+` on whether both operands are known-Int and
|
||||
// a user-defined fn call is not — so the counter printed 4343632752, a pointer.
|
||||
// Nothing was wrong with the checks; the tally was lying. Exiting at the first
|
||||
// failure needs no arithmetic at all, so there is nothing left to get wrong.
|
||||
fn check(ok: Int, label: String) -> Int {
|
||||
if ok > 0 {
|
||||
println(" ok " + label)
|
||||
@@ -84,128 +50,177 @@ fn eq_int(a: Int, b: Int) -> Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// ── A DECOMPOSING realizer, written entirely in El ──────────────────────────
|
||||
// "tone" signals are note letters, e.g. "CEG". This does NOT return one vector
|
||||
// for the chord. It returns the PARTS — one component per note, one per
|
||||
// interval between adjacent notes — and the relations that make those parts a
|
||||
// chord rather than an unordered bag of pitches.
|
||||
//
|
||||
// The interval is deliberately a COMPONENT, not a field on a note. An interval
|
||||
// is a thing with its own geometry belonging to neither endpoint; modelling it
|
||||
// as an attribute of one of them is the same collapse, one level down.
|
||||
fn tone_realizer(signal: String) -> Manifold {
|
||||
let m: Manifold = manifold_new()
|
||||
let n: Int = str_len(signal)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let code: Int = str_char_code(signal, i)
|
||||
let g: Geometry = geometry_new(2)
|
||||
let s0: Int = geometry_set(g, 0, int_to_float(code))
|
||||
let s1: Int = geometry_set(g, 1, int_to_float(i))
|
||||
let idx: Int = manifold_add(m, "note:" + int_to_str(i), "pitch", g)
|
||||
let f: Int = geometry_free(g)
|
||||
i = i + 1
|
||||
}
|
||||
let j: Int = 1
|
||||
while j < n {
|
||||
let a: Int = str_char_code(signal, j - 1)
|
||||
let b: Int = str_char_code(signal, j)
|
||||
let lo: String = "note:" + int_to_str(j - 1)
|
||||
let hi: String = "note:" + int_to_str(j)
|
||||
let key: String = "interval:" + int_to_str(j - 1) + "-" + int_to_str(j)
|
||||
let g: Geometry = geometry_new(1)
|
||||
let s: Int = geometry_set(g, 0, int_to_float(b - a))
|
||||
let idx: Int = manifold_add(m, key, "interval", g)
|
||||
let f: Int = geometry_free(g)
|
||||
let e1: Int = manifold_relate(m, key, "spans", lo, 0.9)
|
||||
let e2: Int = manifold_relate(m, key, "spans", hi, 0.9)
|
||||
let e3: Int = manifold_relate(m, lo, "sounds_before", hi, 0.8)
|
||||
j = j + 1
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
// #144's contract, kept as a control: one vector for the whole signal.
|
||||
fn fingerprint_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
g
|
||||
}
|
||||
|
||||
fn main() -> Void {
|
||||
println("geometry is a value that carries its own width")
|
||||
let g8: Geometry = geometry_new(8)
|
||||
let _c: Int = check(geometry_is(g8), "geometry_new returns a live Geometry")
|
||||
let d8: Int = geometry_dim(g8)
|
||||
let _c: Int = check(eq_int(d8, 8), "a Geometry carries its own width (8)")
|
||||
let _c: Int = check(geometry_free(g8), "geometry_free reports what it did")
|
||||
|
||||
println("nonsense is refused — with no arbitrary max-dim bound")
|
||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
||||
// caller's CLAIM about a string's length. A value that carries its own
|
||||
// width has nothing left to validate.
|
||||
let z: Geometry = geometry_new(0)
|
||||
let zi: Int = geometry_is(z)
|
||||
let _c: Int = check(1 - zi, "dim 0 is not a geometry")
|
||||
let ng: Geometry = geometry_new(-4)
|
||||
let ngi: Int = geometry_is(ng)
|
||||
let _c: Int = check(1 - ngi, "negative dim is not a geometry")
|
||||
let nd: Int = geometry_dim(0)
|
||||
let _c: Int = check(1 - nd, "geometry_dim of a non-geometry is 0, not a crash")
|
||||
let nf: Int = geometry_free(0)
|
||||
let _c: Int = check(1 - nf, "geometry_free of a non-geometry is a no-op")
|
||||
|
||||
println("components round-trip, and out-of-range is refused")
|
||||
let g3: Geometry = geometry_new(3)
|
||||
let s0: Int = geometry_set(g3, 0, 1.5)
|
||||
let s1: Int = geometry_set(g3, 1, -2.5)
|
||||
let _c: Int = check(s0, "set in range succeeds")
|
||||
let oob: Int = geometry_set(g3, 3, 9.0)
|
||||
let _c: Int = check(1 - oob, "set out of range is refused, not silently dropped")
|
||||
let _c: Int = check(near(geometry_get(g3, 0), 1.5), "component 0 round-trips")
|
||||
let _c: Int = check(near(geometry_get(g3, 1), -2.5), "component 1 round-trips (negative)")
|
||||
let ff3: Int = geometry_free(g3)
|
||||
|
||||
println("hex is an EDGE adapter, and derives its own width")
|
||||
// little-endian float32: 1.0 = 0000803f, 2.0 = 00000040
|
||||
let gh: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
||||
let _c: Int = check(geometry_is(gh), "valid hex decodes to a Geometry")
|
||||
let dh: Int = geometry_dim(gh)
|
||||
let _c: Int = check(eq_int(dh, 2), "width DERIVED from input, never supplied")
|
||||
let _c: Int = check(near(geometry_get(gh, 0), 1.0), "first component decoded")
|
||||
let _c: Int = check(near(geometry_get(gh, 1), 2.0), "second component decoded")
|
||||
let back: String = geometry_to_f32le_hex(gh)
|
||||
let _c: Int = check(str_eq(back, "0000803f00000040"), "hex round-trips exactly")
|
||||
let ffh: Int = geometry_free(gh)
|
||||
|
||||
println("malformed hex is refused")
|
||||
let he: Geometry = geometry_from_f32le_hex("")
|
||||
let hei: Int = geometry_is(he)
|
||||
let _c: Int = check(1 - hei, "empty hex is not a geometry")
|
||||
let hr: Geometry = geometry_from_f32le_hex("0000803f0000")
|
||||
let hri: Int = geometry_is(hr)
|
||||
let _c: Int = check(1 - hri, "length not a multiple of 8 is refused")
|
||||
let hn: Geometry = geometry_from_f32le_hex("zzzzzzzz")
|
||||
let hni: Int = geometry_is(hn)
|
||||
let _c: Int = check(1 - hni, "non-hex characters are refused")
|
||||
|
||||
println("a realizer declared in El is a first-class realizer")
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let _c: Int = check(reg, "an El fn registers as a realizer BY NAME")
|
||||
let _c: Int = check(reg, "an El fn registers as a realizer by name")
|
||||
let _c: Int = check(realizer_has("tone"), "the modality now has an organ")
|
||||
let gt: Geometry = transduce("aaa", "tone")
|
||||
let _c: Int = check(geometry_is(gt), "transduce returns real geometry")
|
||||
let dt: Int = geometry_dim(gt)
|
||||
let _c: Int = check(eq_int(dt, 4), "the El realizer determined the width, not the runtime")
|
||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
||||
// actually reached the El function rather than a stub answering for it.
|
||||
let _c: Int = check(near(geometry_get(gt, 0), 3.0), "the signal REACHED the El realizer")
|
||||
let fft: Int = geometry_free(gt)
|
||||
|
||||
println("distinct signals transduce to distinct geometry")
|
||||
let g1: Geometry = transduce("aa", "tone")
|
||||
let g2: Geometry = transduce("aaaaa", "tone")
|
||||
let a1: Float = geometry_get(g1, 0)
|
||||
let a2: Float = geometry_get(g2, 0)
|
||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
||||
let _c: Int = check(near(a2 - a1, 3.0), "different signals produce different geometry")
|
||||
let ff1: Int = geometry_free(g1)
|
||||
let ff2: Int = geometry_free(g2)
|
||||
println("transduction decomposes a signal into parts")
|
||||
let m: Manifold = transduce("CEG", "tone")
|
||||
let _c: Int = check(manifold_is(m), "transduce returns a real Manifold")
|
||||
let sz: Int = manifold_size(m)
|
||||
let _c: Int = check(eq_int(sz, 5), "three notes and two intervals are five parts")
|
||||
let rc: Int = manifold_rel_count(m)
|
||||
let _c: Int = check(eq_int(rc, 6), "and they stand in six stated relations")
|
||||
|
||||
println("the registry keys on modality")
|
||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
||||
let _c: Int = check(r2, "a second modality registers independently")
|
||||
let mt: Geometry = transduce("aaa", "tone")
|
||||
let mp: Geometry = transduce("aaa", "pulse")
|
||||
let mdt: Int = geometry_dim(mt)
|
||||
let mdp: Int = geometry_dim(mp)
|
||||
let _c: Int = check(eq_int(mdt, 4), "tone still routes to its own realizer")
|
||||
let _c: Int = check(eq_int(mdp, 2), "pulse routes to a different realizer")
|
||||
let ffm1: Int = geometry_free(mt)
|
||||
let ffm2: Int = geometry_free(mp)
|
||||
println("every part is addressable BY KEY, which is what survives persistence")
|
||||
let i_c: Int = manifold_index_of(m, "note:0")
|
||||
let _c: Int = check(1 - eq_int(i_c, -1), "the first note is addressable on its own")
|
||||
let i_iv: Int = manifold_index_of(m, "interval:0-1")
|
||||
let _c: Int = check(1 - eq_int(i_iv, -1), "so is the interval between the first two")
|
||||
let miss: Int = manifold_index_of(m, "never_added")
|
||||
let _c: Int = check(eq_int(miss, -1), "an unknown key is -1, not component 0")
|
||||
|
||||
println("no organ is reported as no organ")
|
||||
// A modality with no realizer must transduce to NOTHING. It must never
|
||||
// fall back to embedding a description of the signal and calling that
|
||||
// perception — that silent substitution is the defect this all exists to end.
|
||||
let eh: Int = realizer_has("echolocation")
|
||||
let _c: Int = check(1 - eh, "unregistered modality has no organ")
|
||||
let ge: Geometry = transduce("anything", "echolocation")
|
||||
let gei: Int = geometry_is(ge)
|
||||
let _c: Int = check(1 - gei, "no realizer means NO geometry, not fake geometry")
|
||||
println("parts carry their own geometry, and may differ in width")
|
||||
let gn: Geometry = manifold_geometry(m, i_c)
|
||||
let _c: Int = check(eq_int(geometry_dim(gn), 2), "a note component is 2 wide")
|
||||
let _c: Int = check(near(geometry_get(gn, 0), 67.0), "and it is C — the signal reached the realizer")
|
||||
let gi: Geometry = manifold_geometry(m, i_iv)
|
||||
let _c: Int = check(eq_int(geometry_dim(gi), 1), "an interval component is 1 wide")
|
||||
// A single vector per signal cannot represent parts of unequal width at all.
|
||||
let _c: Int = check(near(geometry_get(gi, 0), 2.0), "C to E is two semitones")
|
||||
let f1: Int = geometry_free(gn)
|
||||
let f2: Int = geometry_free(gi)
|
||||
|
||||
println("an unresolvable realizer name fails at WIRING time")
|
||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||
let _c: Int = check(1 - bad, "unresolvable realizer name is a registration failure")
|
||||
let gh2: Int = realizer_has("ghost")
|
||||
let _c: Int = check(1 - gh2, "and nothing gets registered")
|
||||
println("the relations are content no single part carries")
|
||||
// That "2" above is not a property of C and not a property of E. It exists
|
||||
// only BETWEEN them, so a representation with no relations cannot hold it.
|
||||
let spans: Int = 0
|
||||
let k: Int = 0
|
||||
while k < rc {
|
||||
if str_eq(manifold_rel_name(m, k), "spans") {
|
||||
if str_eq(manifold_rel_from(m, k), "interval:0-1") { spans = spans + 1 }
|
||||
}
|
||||
k = k + 1
|
||||
}
|
||||
let _c: Int = check(eq_int(spans, 2), "the interval is wired to both notes it spans")
|
||||
|
||||
println("a realizer returning non-geometry transduces nothing")
|
||||
let rb: Int = realizer_register("bogus", "bogus_realizer")
|
||||
let _c: Int = check(rb, "the symbol resolves, so registration succeeds")
|
||||
let gb: Geometry = transduce("x", "bogus")
|
||||
let gbi: Int = geometry_is(gb)
|
||||
let _c: Int = check(1 - gbi, "contract enforced at the boundary: nothing handed back")
|
||||
println("relation weight IS the grounding (correspondence-and-censorship §1)")
|
||||
let wk: Int = 0
|
||||
let found: Int = 0
|
||||
while wk < rc {
|
||||
if str_eq(manifold_rel_name(m, wk), "sounds_before") {
|
||||
if near(manifold_rel_weight(m, wk), 0.8) > 0 { found = 1 }
|
||||
}
|
||||
wk = wk + 1
|
||||
}
|
||||
let _c: Int = check(found, "the ordering relation carries the weight its realizer stated")
|
||||
|
||||
println("norm lets a caller check a realizer emitted signal, not zeros")
|
||||
let gn: Geometry = geometry_new(2)
|
||||
let _c: Int = check(near(geometry_norm(gn), 0.0), "a fresh geometry is zero — norm says so")
|
||||
let n0: Int = geometry_set(gn, 0, 3.0)
|
||||
let n1: Int = geometry_set(gn, 1, 4.0)
|
||||
let _c: Int = check(near(geometry_norm(gn), 5.0), "3-4-5: norm is 5")
|
||||
let ffn: Int = geometry_free(gn)
|
||||
println("the decomposition persists as real, separately addressable nodes")
|
||||
let ids: [String] = el_list_empty()
|
||||
let n0: Int = engram_node_count()
|
||||
let e0: Int = engram_edge_count()
|
||||
let pi: Int = 0
|
||||
while pi < sz {
|
||||
let key: String = manifold_key(m, pi)
|
||||
let g: Geometry = manifold_geometry(m, pi)
|
||||
let id: String = engram_node("component " + key, "Concept", 0.6)
|
||||
let att: Int = node_attach_geometry(id, g)
|
||||
ids = el_list_append(ids, id)
|
||||
let ff: Int = geometry_free(g)
|
||||
pi = pi + 1
|
||||
}
|
||||
let ri: Int = 0
|
||||
while ri < rc {
|
||||
let fi: Int = manifold_index_of(m, manifold_rel_from(m, ri))
|
||||
let ti: Int = manifold_index_of(m, manifold_rel_to(m, ri))
|
||||
engram_connect(el_list_get(ids, fi), el_list_get(ids, ti),
|
||||
manifold_rel_weight(m, ri), manifold_rel_name(m, ri))
|
||||
ri = ri + 1
|
||||
}
|
||||
let _c: Int = check(eq_int(engram_node_count() - n0, 5), "one signal became five nodes")
|
||||
let _c: Int = check(eq_int(engram_edge_count() - e0, 6), "and six edges between them")
|
||||
|
||||
println("each part's geometry is independently readable back off its node")
|
||||
let id_c: String = el_list_get(ids, manifold_index_of(m, "note:0"))
|
||||
let id_iv: String = el_list_get(ids, manifold_index_of(m, "interval:0-1"))
|
||||
let _c: Int = check(eq_int(node_geometry_dim(id_c), 2), "note:0 node carries a 2-wide geometry")
|
||||
let _c: Int = check(eq_int(node_geometry_dim(id_iv), 1), "interval:0-1 node carries a 1-wide one")
|
||||
|
||||
println("one part can be grounded without touching its siblings")
|
||||
let ear: String = engram_node("evidence: heard a C in the recording", "Memory", 0.7)
|
||||
engram_connect(ear, id_c, 0.95, "corroborates")
|
||||
let _c: Int = check(engram_edge_between(ear, id_c), "evidence attaches to note:0 specifically")
|
||||
let id_g: String = el_list_get(ids, manifold_index_of(m, "note:2"))
|
||||
let _c: Int = check(1 - engram_edge_between(ear, id_g), "and NOT to note:2 — the sibling is untouched")
|
||||
// This is the whole gain, and it is impossible with a fingerprint: with one
|
||||
// node per signal, "the C is corroborated" and "the G is not" have the same
|
||||
// grounding target and cannot both be recorded.
|
||||
let _c: Int = check(eq_int(node_geometry_dim(id_g), 2), "note:2 geometry is intact regardless")
|
||||
|
||||
println("a fingerprint realizer transduces NOTHING")
|
||||
// #144's contract exactly: signal in, one Geometry out. It resolves, so the
|
||||
// organ is present — but it does not decompose, so it does not transduce.
|
||||
// "No organ" and "an organ that only fingerprints" must not look alike.
|
||||
let rf: Int = realizer_register("fingerprint", "fingerprint_realizer")
|
||||
let _c: Int = check(rf, "the symbol resolves, so registration succeeds")
|
||||
let mf: Manifold = transduce("x", "fingerprint")
|
||||
let _c: Int = check(1 - manifold_is(mf), "a single vector is not a transduction")
|
||||
|
||||
println("the one-part case is a size-one manifold, not a bare vector")
|
||||
let g1: Geometry = geometry_new(3)
|
||||
let s1: Int = geometry_set(g1, 0, 5.0)
|
||||
let ms: Manifold = manifold_single("level", "scalar", g1)
|
||||
let _c: Int = check(manifold_is(ms), "manifold_single yields a real Manifold")
|
||||
let _c: Int = check(eq_int(manifold_size(ms), 1), "of size one — visibly degenerate, not hidden")
|
||||
let fg: Int = geometry_free(g1)
|
||||
let fs: Int = manifold_free(ms)
|
||||
|
||||
println("no organ is still reported as no organ")
|
||||
let me: Manifold = transduce("anything", "echolocation")
|
||||
let _c: Int = check(1 - manifold_is(me), "no realizer means no manifold, not a fake one")
|
||||
|
||||
let fm: Int = manifold_free(m)
|
||||
|
||||
// Reaching here means nothing called exit(1) along the way.
|
||||
println("")
|
||||
|
||||
+39
-9
@@ -49,21 +49,47 @@ download() {
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
download "${RELEASE_BASE}/elc" "${TMP_DIR}/elc"
|
||||
download "${RELEASE_BASE}/el_runtime.c" "${TMP_DIR}/el_runtime.c"
|
||||
download "${RELEASE_BASE}/el_runtime.h" "${TMP_DIR}/el_runtime.h"
|
||||
# The runtime is MULTI-FILE. el_runtime.c #includes six engram headers and makes
|
||||
# hard cross-TU calls into all six sibling .c files, so installing el_runtime.c
|
||||
# alone produces a lib/ that CANNOT LINK — `ld` fails with undefined
|
||||
# engram_ground_json / engram_activate_inner / eg_find_relation / cog_assert_two_axis.
|
||||
# This list mirrors lang/runtime/SOURCES (the in-repo source of truth); keep them
|
||||
# in step. install.sh is standalone by design — it runs on machines with no repo
|
||||
# checkout — so it cannot call scripts/el-runtime-sources.sh.
|
||||
RUNTIME_SOURCES=(
|
||||
el_runtime.c el_seed.c
|
||||
engram_store.c engram_vindex.c engram_geometry.c
|
||||
engram_reason.c engram_verify.c engram_cognition.c
|
||||
eg_cosine_batch.c eg_cosine_batch_strategy_cpu.c
|
||||
)
|
||||
RUNTIME_HEADERS=(
|
||||
el_runtime.h el_seed.h
|
||||
engram_store.h engram_vindex.h engram_geometry.h
|
||||
engram_reason.h engram_verify.h engram_cognition.h
|
||||
eg_cosine_batch.h eg_cosine_batch_strategy.h
|
||||
)
|
||||
|
||||
download "${RELEASE_BASE}/elc" "${TMP_DIR}/elc"
|
||||
for f in "${RUNTIME_SOURCES[@]}" "${RUNTIME_HEADERS[@]}"; do
|
||||
download "${RELEASE_BASE}/${f}" "${TMP_DIR}/${f}"
|
||||
done
|
||||
|
||||
# Install
|
||||
install -m 755 "${TMP_DIR}/elc" "${BIN_DIR}/elc"
|
||||
install -m 644 "${TMP_DIR}/el_runtime.c" "${LIB_DIR}/el_runtime.c"
|
||||
install -m 644 "${TMP_DIR}/el_runtime.h" "${LIB_DIR}/el_runtime.h"
|
||||
install -m 755 "${TMP_DIR}/elc" "${BIN_DIR}/elc"
|
||||
for f in "${RUNTIME_SOURCES[@]}" "${RUNTIME_HEADERS[@]}"; do
|
||||
install -m 644 "${TMP_DIR}/${f}" "${LIB_DIR}/${f}"
|
||||
done
|
||||
|
||||
# Record the link set so downstream Makefiles can read it instead of hardcoding.
|
||||
printf '%s\n' "${RUNTIME_SOURCES[@]}" > "${TMP_DIR}/SOURCES"
|
||||
install -m 644 "${TMP_DIR}/SOURCES" "${LIB_DIR}/SOURCES"
|
||||
|
||||
echo
|
||||
echo "==> El SDK installed successfully"
|
||||
echo
|
||||
echo " elc binary : ${BIN_DIR}/elc"
|
||||
echo " runtime : ${LIB_DIR}/el_runtime.c"
|
||||
echo " header : ${LIB_DIR}/el_runtime.h"
|
||||
echo " runtime : ${LIB_DIR}/ (${#RUNTIME_SOURCES[@]} .c files, ${#RUNTIME_HEADERS[@]} headers)"
|
||||
echo " link set : ${LIB_DIR}/SOURCES"
|
||||
echo
|
||||
echo "Add the following to your Makefile to build El programs:"
|
||||
echo
|
||||
@@ -71,10 +97,14 @@ echo " EL_LIB := ${LIB_DIR}"
|
||||
echo " ELC := elc"
|
||||
echo " CC := cc"
|
||||
echo " CFLAGS := -std=c11 -O2 -I\$(EL_LIB)"
|
||||
echo " LDLIBS := -lcurl -lssl -lcrypto -lpthread -lm"
|
||||
echo
|
||||
echo " # The runtime is multi-file — link the whole set, not el_runtime.c alone."
|
||||
echo " EL_RUNTIME := \$(addprefix \$(EL_LIB)/,\$(shell cat \$(EL_LIB)/SOURCES))"
|
||||
echo
|
||||
echo " dist/myapp.c: src/myapp.el"
|
||||
echo " \t\$(ELC) src/myapp.el > dist/myapp.c"
|
||||
echo
|
||||
echo " dist/myapp: dist/myapp.c"
|
||||
echo " \t\$(CC) \$(CFLAGS) -o dist/myapp dist/myapp.c \$(EL_LIB)/el_runtime.c -lcurl -lpthread"
|
||||
echo " \t\$(CC) \$(CFLAGS) -o dist/myapp dist/myapp.c \$(EL_RUNTIME) \$(LDLIBS)"
|
||||
echo
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# BUDGET — a RATCHET on lang/runtime/el_runtime.c. Enforced by
|
||||
# scripts/check-runtime-growth.sh. These numbers may only ever go DOWN.
|
||||
#
|
||||
# WHY THIS FILE EXISTS
|
||||
# --------------------
|
||||
# scripts/check-single-runtime.sh guards against el_runtime.c being COPIED.
|
||||
# Nothing guarded against it GROWING. It grew from 10,607 lines to 20,527 —
|
||||
# 94% — in 3.5 months, while under an explicit commit-message promise that it
|
||||
# was a temporary shim about to be deleted.
|
||||
#
|
||||
# It grew because lang/AGENTS.md told every agent to grow it: it claimed
|
||||
# el_runtime.c was "the authoritative single-file link target" and that a new
|
||||
# C builtin "must live there to be linkable". That is false — placement is a
|
||||
# link-time concern, `builtin_arity` is an arity guard not a dispatch table,
|
||||
# and the shipped elc already links from ten translation units. The claim is
|
||||
# corrected, and this file is the mechanism that keeps it corrected.
|
||||
#
|
||||
# THIS IS A RATCHET, NOT A LIMIT
|
||||
# ------------------------------
|
||||
# The budget is set at the CURRENT size. There is no headroom, deliberately.
|
||||
# The file cannot grow by even one line. Any new code goes in the .c that owns
|
||||
# the concern — that is the whole point, and every other runtime file is
|
||||
# deliberately UNCAPPED.
|
||||
#
|
||||
# When you move code OUT, lower the number in the same commit. The guard tells
|
||||
# you to when you have earned it.
|
||||
#
|
||||
# FORMAT: <key> <value> — `#` comments and blank lines ignored.
|
||||
|
||||
# Maximum lines in lang/runtime/el_runtime.c.
|
||||
# 2026-08-16: 20,527 — the high-water mark, ratcheted from here.
|
||||
max_lines 20527
|
||||
|
||||
# Maximum top-level engram/eg_/cog_ function definitions in el_runtime.c.
|
||||
# ~47.5% of the file is engram code, and engram already owns six dedicated
|
||||
# sibling files (engram_{store,vindex,geometry,reason,verify,cognition}.c).
|
||||
# Every one of these belongs in one of them. This is the Stage 3 scoreboard.
|
||||
# 2026-08-16: 279.
|
||||
max_engram_fns 279
|
||||
@@ -0,0 +1,52 @@
|
||||
# SOURCES — the canonical El runtime link set.
|
||||
#
|
||||
# THIS FILE IS THE SINGLE SOURCE OF TRUTH for "what do I compile and link to
|
||||
# get the El runtime". Every build path — CI, install.sh, the SDK release, the
|
||||
# docs, elb, the engram test harnesses — reads it via scripts/el-runtime-sources.sh
|
||||
# instead of hardcoding its own list.
|
||||
#
|
||||
# WHY THIS FILE EXISTS
|
||||
# --------------------
|
||||
# The runtime has been multi-translation-unit since the engram siblings landed:
|
||||
# el_runtime.c #includes engram_{store,vindex,geometry,reason,verify,cognition}.h
|
||||
# and makes hard cross-TU calls into all six. Linking el_runtime.c ALONE has been
|
||||
# broken since then — `ld` fails with undefined symbols (engram_ground_json,
|
||||
# engram_activate_inner, eg_find_relation, cog_assert_two_axis, ...).
|
||||
#
|
||||
# It stayed broken because the link set was written out longhand in ~8 different
|
||||
# places, each of which drifted independently. A list copied 8 times is a list
|
||||
# that is wrong in 8 places. It is now written once, here.
|
||||
#
|
||||
# HOW TO USE IT
|
||||
# -------------
|
||||
# scripts/el-runtime-sources.sh # bare names, one per line
|
||||
# scripts/el-runtime-sources.sh lang/runtime # prefixed with a directory
|
||||
# cc ... $(scripts/el-runtime-sources.sh lang/runtime) -lcurl -lssl -lcrypto -lpthread -lm
|
||||
#
|
||||
# ADDING A FILE
|
||||
# -------------
|
||||
# Add the .c here and it is picked up by every build path at once. That is the
|
||||
# point: a new concern gets its own translation unit and costs one line, instead
|
||||
# of being appended to el_runtime.c because appending was the cheaper edit.
|
||||
#
|
||||
# Order is link order. Blank lines and `#` comments are ignored.
|
||||
|
||||
# --- EL core language runtime -------------------------------------------------
|
||||
el_runtime.c
|
||||
el_seed.c
|
||||
|
||||
# --- Engram: store, index, geometry, reasoning, verification, cognition -------
|
||||
# These are the six concern-owned translation units el_runtime.c calls into.
|
||||
engram_store.c
|
||||
engram_vindex.c
|
||||
engram_geometry.c
|
||||
engram_reason.c
|
||||
engram_verify.c
|
||||
engram_cognition.c
|
||||
|
||||
# --- Vector math: batch cosine + its CPU strategy ----------------------------
|
||||
# The ggml strategy (eg_cosine_batch_strategy_ggml.c) is an OPTIONAL swap-in and
|
||||
# is deliberately NOT in the default set — it needs ggml headers. Link it in
|
||||
# place of the cpu strategy when you have them.
|
||||
eg_cosine_batch.c
|
||||
eg_cosine_batch_strategy_cpu.c
|
||||
+335
-21
@@ -6422,22 +6422,319 @@ el_val_t geometry_to_f32le_hex(el_val_t g) {
|
||||
return (el_val_t)(uintptr_t)out;
|
||||
}
|
||||
|
||||
|
||||
/* ── Manifold: a transduced signal is a SUBGRAPH, not a point ────────────────
|
||||
*
|
||||
* WHAT THIS CORRECTS. #144 gave transduction a home in the language and got
|
||||
* the DISPATCH right — realizers declared in El, resolved by name, no runtime
|
||||
* patch per modality. It got the OUTPUT TYPE wrong.
|
||||
* `transduce(signal, modality) -> Geometry` yields one vector per signal, and
|
||||
* one vector is a FINGERPRINT. A fingerprint can be matched and it can be
|
||||
* ranked; that is the whole of what it can ever do. It cannot be decomposed,
|
||||
* cannot be partially grounded, and cannot be contradicted in one part while
|
||||
* holding in another — because it has no parts.
|
||||
*
|
||||
* A song is not a point. It decomposes into pitch, interval, rhythm, harmonic
|
||||
* function, phrase structure: components, each with its own geometry, plus the
|
||||
* relations between them. THE SONG IS THE STRUCTURE OF THE RELATIONS. A
|
||||
* transducer that returns a single vector has not transduced the song, it has
|
||||
* summarised it — and the summary discards precisely the thing that made the
|
||||
* song reasonable-about.
|
||||
*
|
||||
* So transduction produces a MANIFOLD: named components, each carrying its own
|
||||
* geometry, and typed weighted relations among them. Signal in, subgraph out.
|
||||
* Conversion was never the operation.
|
||||
*
|
||||
* COMPONENTS ARE ADDRESSED BY KEY, NEVER BY INDEX. The key is what survives
|
||||
* persistence: a component becomes a node, and that node is separately
|
||||
* groundable precisely because it is separately NAMED. Index-addressing would
|
||||
* make a grounding reference positional, and a positional reference into a
|
||||
* decomposition whose arity can change is not a reference at all. Duplicate
|
||||
* keys are refused for the same reason: two components answering to one name
|
||||
* is not an addressing scheme.
|
||||
*
|
||||
* RELATION WEIGHT IS THE GROUNDING — there is no second field and no score to
|
||||
* compute. Per correspondence-and-censorship.md §1, grounding is an attribute
|
||||
* of the edge and it IS the hebbian weight; a grounding subsystem is a
|
||||
* supervisor invented for something that should be a property of the
|
||||
* substrate. A relation emitted by a realizer therefore arrives with its
|
||||
* grounding already on it and moves thereafter by use and by decay (§4: change
|
||||
* is not a consequence of use, it is use). Nothing in here computes a
|
||||
* grounding, and nothing observes one.
|
||||
*
|
||||
* A relation naming an endpoint that does not exist is REFUSED, not dropped. A
|
||||
* decomposition that silently loses edges is indistinguishable from one that
|
||||
* never had them — the same class of defect #141 exists to end.
|
||||
*
|
||||
* OWNERSHIP mirrors Geometry exactly. A Manifold is owned by the El caller and
|
||||
* released with manifold_free. manifold_add COPIES the geometry handed to it,
|
||||
* so a caller may free its own vector immediately and no component's geometry
|
||||
* is ever aliased. Keys, roles and relation strings are _persist copies, NOT
|
||||
* arena copies: a Manifold outlives the request arena that built it (a
|
||||
* realizer can be invoked from inside a handler), so an arena-tracked key
|
||||
* would dangle at el_request_end. manifold_free owns their release.
|
||||
*/
|
||||
|
||||
#define EL_MAGIC_MFLD 0xE1608E02u
|
||||
|
||||
typedef struct {
|
||||
char* key; /* addressable name, unique within the manifold */
|
||||
char* role; /* what KIND of component this is, realizer's vocabulary */
|
||||
ElGeometry* g; /* owned copy; never aliases the caller's value */
|
||||
} ElComponent;
|
||||
|
||||
typedef struct {
|
||||
char* from; /* component key */
|
||||
char* rel; /* relation name */
|
||||
char* to; /* component key */
|
||||
double weight; /* the grounding; §1 — one quantity, not two fields */
|
||||
} ElRelation;
|
||||
|
||||
typedef struct {
|
||||
ElHeader hdr;
|
||||
ElComponent* comps;
|
||||
size_t ncomp, capcomp;
|
||||
ElRelation* rels;
|
||||
size_t nrel, caprel;
|
||||
} ElManifold;
|
||||
|
||||
/* Resolve an el_val_t to a live Manifold, or NULL. Every accessor goes through
|
||||
* this, so a stale/foreign/zero value is a clean 0-return, never a deref. */
|
||||
static ElManifold* mfld_of(el_val_t m) {
|
||||
if (!looks_like_heap_obj(m)) return NULL;
|
||||
ElManifold* p = (ElManifold*)(uintptr_t)m;
|
||||
if (p->hdr.magic != EL_MAGIC_MFLD) return NULL;
|
||||
return p;
|
||||
}
|
||||
|
||||
static int mfld_find(ElManifold* p, const char* key) {
|
||||
for (size_t i = 0; i < p->ncomp; i++)
|
||||
if (strcmp(p->comps[i].key, key) == 0) return (int)i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
el_val_t manifold_new(void) {
|
||||
ElManifold* p = (ElManifold*)calloc(1, sizeof(ElManifold));
|
||||
if (!p) return (el_val_t)0;
|
||||
p->hdr.magic = EL_MAGIC_MFLD;
|
||||
p->hdr.refcount = 1;
|
||||
return (el_val_t)(uintptr_t)p;
|
||||
}
|
||||
|
||||
el_val_t manifold_is(el_val_t m) {
|
||||
return mfld_of(m) ? (el_val_t)1 : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* manifold_add — add one COMPONENT: a named part with its own geometry.
|
||||
* Returns the component's index, or -1 on any refusal. Refusals are real and
|
||||
* distinct: an empty key (unaddressable), a duplicate key (ambiguous
|
||||
* addressing), a value that is not a live Geometry (a part with no geometry is
|
||||
* not a part). Each is a caller error worth surfacing at the point of the
|
||||
* mistake rather than as a missing node three layers downstream. */
|
||||
el_val_t manifold_add(el_val_t m, el_val_t key, el_val_t role, el_val_t g) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
if (!p) return (el_val_t)(int64_t)-1;
|
||||
const char* k = EL_CSTR(key);
|
||||
const char* r = EL_CSTR(role);
|
||||
if (!k || !*k) return (el_val_t)(int64_t)-1;
|
||||
if (!r) r = "";
|
||||
ElGeometry* src = geom_of(g);
|
||||
if (!src || src->dim <= 0) return (el_val_t)(int64_t)-1;
|
||||
if (mfld_find(p, k) >= 0) return (el_val_t)(int64_t)-1; /* duplicate key */
|
||||
|
||||
if (p->ncomp == p->capcomp) {
|
||||
size_t nc = p->capcomp ? p->capcomp * 2 : 8;
|
||||
ElComponent* nb = (ElComponent*)realloc(p->comps, nc * sizeof(ElComponent));
|
||||
if (!nb) return (el_val_t)(int64_t)-1;
|
||||
p->comps = nb; p->capcomp = nc;
|
||||
}
|
||||
|
||||
/* COPY the payload — a component's geometry must not alias the caller's. */
|
||||
ElGeometry* cp = (ElGeometry*)malloc(sizeof(ElGeometry));
|
||||
if (!cp) return (el_val_t)(int64_t)-1;
|
||||
cp->v = (float*)malloc(sizeof(float) * (size_t)src->dim);
|
||||
if (!cp->v) { free(cp); return (el_val_t)(int64_t)-1; }
|
||||
memcpy(cp->v, src->v, sizeof(float) * (size_t)src->dim);
|
||||
cp->hdr.magic = EL_MAGIC_GEOM;
|
||||
cp->hdr.refcount = 1;
|
||||
cp->dim = src->dim;
|
||||
|
||||
p->comps[p->ncomp].key = el_strdup_persist(k);
|
||||
p->comps[p->ncomp].role = el_strdup_persist(r);
|
||||
p->comps[p->ncomp].g = cp;
|
||||
p->ncomp++;
|
||||
return (el_val_t)(int64_t)(p->ncomp - 1);
|
||||
}
|
||||
|
||||
/* manifold_relate — state a relation BETWEEN two components. This is the part
|
||||
* that carries the meaning: the components are the parts, the relations are
|
||||
* what the thing IS.
|
||||
*
|
||||
* Both endpoints must already exist. An edge to a name that was never added is
|
||||
* refused with 0, never silently discarded — see the header note. */
|
||||
el_val_t manifold_relate(el_val_t m, el_val_t from, el_val_t rel,
|
||||
el_val_t to, el_val_t weight) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
if (!p) return (el_val_t)0;
|
||||
const char* f = EL_CSTR(from);
|
||||
const char* r = EL_CSTR(rel);
|
||||
const char* t = EL_CSTR(to);
|
||||
if (!f || !*f || !r || !*r || !t || !*t) return (el_val_t)0;
|
||||
if (mfld_find(p, f) < 0) return (el_val_t)0;
|
||||
if (mfld_find(p, t) < 0) return (el_val_t)0;
|
||||
|
||||
if (p->nrel == p->caprel) {
|
||||
size_t nc = p->caprel ? p->caprel * 2 : 8;
|
||||
ElRelation* nb = (ElRelation*)realloc(p->rels, nc * sizeof(ElRelation));
|
||||
if (!nb) return (el_val_t)0;
|
||||
p->rels = nb; p->caprel = nc;
|
||||
}
|
||||
p->rels[p->nrel].from = el_strdup_persist(f);
|
||||
p->rels[p->nrel].rel = el_strdup_persist(r);
|
||||
p->rels[p->nrel].to = el_strdup_persist(t);
|
||||
p->rels[p->nrel].weight = el_to_float(weight);
|
||||
p->nrel++;
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
el_val_t manifold_size(el_val_t m) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
return p ? (el_val_t)(int64_t)p->ncomp : (el_val_t)0;
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_count(el_val_t m) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
return p ? (el_val_t)(int64_t)p->nrel : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* Index of a component BY KEY, or -1. This is the addressability primitive:
|
||||
* everything downstream that wants to ground, weight or contradict one part
|
||||
* finds it through here. */
|
||||
el_val_t manifold_index_of(el_val_t m, el_val_t key) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
const char* k = EL_CSTR(key);
|
||||
if (!p || !k || !*k) return (el_val_t)(int64_t)-1;
|
||||
return (el_val_t)(int64_t)mfld_find(p, k);
|
||||
}
|
||||
|
||||
el_val_t manifold_key(el_val_t m, el_val_t i) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)i;
|
||||
if (!p || k < 0 || k >= (int64_t)p->ncomp) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->comps[k].key));
|
||||
}
|
||||
|
||||
el_val_t manifold_role(el_val_t m, el_val_t i) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)i;
|
||||
if (!p || k < 0 || k >= (int64_t)p->ncomp) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->comps[k].role));
|
||||
}
|
||||
|
||||
/* manifold_geometry — the geometry OF ONE COMPONENT, as a fresh Geometry the
|
||||
* caller owns and frees. A borrowed interior pointer would let a caller's
|
||||
* geometry_free corrupt the manifold; copying is the same discipline
|
||||
* node_attach_geometry already applies in the other direction. */
|
||||
el_val_t manifold_geometry(el_val_t m, el_val_t i) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)i;
|
||||
if (!p || k < 0 || k >= (int64_t)p->ncomp) return (el_val_t)0;
|
||||
ElGeometry* src = p->comps[k].g;
|
||||
el_val_t out = geometry_new((el_val_t)(int64_t)src->dim);
|
||||
ElGeometry* dst = geom_of(out);
|
||||
if (!dst) return (el_val_t)0;
|
||||
memcpy(dst->v, src->v, sizeof(float) * (size_t)src->dim);
|
||||
return out;
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_from(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->rels[k].from));
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_name(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->rels[k].rel));
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_to(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_wrap_str(el_strdup(""));
|
||||
return el_wrap_str(el_strdup(p->rels[k].to));
|
||||
}
|
||||
|
||||
el_val_t manifold_rel_weight(el_val_t m, el_val_t j) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
int64_t k = (int64_t)j;
|
||||
if (!p || k < 0 || k >= (int64_t)p->nrel) return el_from_float(0.0);
|
||||
return el_from_float(p->rels[k].weight);
|
||||
}
|
||||
|
||||
/* manifold_single — the DEGENERATE case, expressible but visibly degenerate.
|
||||
*
|
||||
* Sometimes a modality really does have one part (a scalar sensor). That is a
|
||||
* manifold of size 1, not a different kind of thing, and writing it this way
|
||||
* keeps the fingerprint as a SPECIAL CASE of decomposition rather than a
|
||||
* parallel path back to #144's contract. Anything reading it still asks
|
||||
* manifold_size and still gets a real answer. */
|
||||
el_val_t manifold_single(el_val_t key, el_val_t role, el_val_t g) {
|
||||
el_val_t m = manifold_new();
|
||||
if (!mfld_of(m)) return (el_val_t)0;
|
||||
if ((int64_t)manifold_add(m, key, role, g) < 0) { manifold_free(m); return (el_val_t)0; }
|
||||
return m;
|
||||
}
|
||||
|
||||
el_val_t manifold_free(el_val_t m) {
|
||||
ElManifold* p = mfld_of(m);
|
||||
if (!p) return (el_val_t)0;
|
||||
for (size_t i = 0; i < p->ncomp; i++) {
|
||||
free(p->comps[i].key);
|
||||
free(p->comps[i].role);
|
||||
if (p->comps[i].g) { free(p->comps[i].g->v); p->comps[i].g->hdr.magic = 0; free(p->comps[i].g); }
|
||||
}
|
||||
for (size_t i = 0; i < p->nrel; i++) {
|
||||
free(p->rels[i].from); free(p->rels[i].rel); free(p->rels[i].to);
|
||||
}
|
||||
free(p->comps);
|
||||
free(p->rels);
|
||||
p->hdr.magic = 0; /* poison, as Geometry/List/Map do */
|
||||
free(p);
|
||||
return (el_val_t)1;
|
||||
}
|
||||
|
||||
/* ── Realizers: transduction declared in El, not patched into the runtime ────
|
||||
*
|
||||
* A REALIZER maps one modality into geometry. The whole reason transduction
|
||||
* belongs in the language is that ADDING A MODALITY MUST NOT REQUIRE A
|
||||
* RUNTIME PATCH — otherwise "the realizers are in the engram" just becomes
|
||||
* "the realizers are in the runtime" and nothing has actually moved. So
|
||||
* realizers are declared in El and registered by NAME:
|
||||
* A REALIZER DECOMPOSES one modality into components and their relations. It
|
||||
* does not encode a signal to a point — that is the operation one layer below
|
||||
* it, and it is called geometry, not transduction. A realizer for a modality
|
||||
* declares what that modality's COMPONENTS ARE: for audio, not one MFCC
|
||||
* vector, but pitch, interval, rhythm, harmonic function, and how they stand
|
||||
* to one another.
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Geometry {
|
||||
* let g: Geometry = geometry_new(8)
|
||||
* ... geometry_set(g, i, x) ...
|
||||
* g
|
||||
* The whole reason transduction belongs in the language is that ADDING A
|
||||
* MODALITY MUST NOT REQUIRE A RUNTIME PATCH — otherwise "the realizers are in
|
||||
* the engram" just becomes "the realizers are in the runtime" and nothing has
|
||||
* actually moved. So realizers are declared in El and registered by NAME:
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Manifold {
|
||||
* let m: Manifold = manifold_new()
|
||||
* let a: Int = manifold_add(m, "pitch", "spectral", pitch_geom)
|
||||
* let b: Int = manifold_add(m, "interval", "relation", interval_geom)
|
||||
* let e: Int = manifold_relate(m, "pitch", "spans", "interval", 0.9)
|
||||
* m
|
||||
* }
|
||||
*
|
||||
* realizer_register("tone", "tone_realizer")
|
||||
* let g: Geometry = transduce(sample, "tone")
|
||||
* let m: Manifold = transduce(sample, "tone")
|
||||
*
|
||||
* A realizer's DECLARED COMPONENT VOCABULARY is the interesting part of its
|
||||
* contract, and it is what a caller can then ground, weight and contradict
|
||||
* one part at a time.
|
||||
*
|
||||
* The name→symbol step rides the identical, already load-bearing mechanism
|
||||
* http_set_handler uses (see "HTTP server"): every El `fn name(...)` compiles
|
||||
@@ -6512,28 +6809,45 @@ el_val_t realizer_has(el_val_t modality) {
|
||||
return realizer_lookup(m) ? (el_val_t)1 : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* transduce — THE primitive: signal in, geometry out.
|
||||
/* transduce — THE primitive: signal in, SUBGRAPH out.
|
||||
*
|
||||
* Dispatches to the realizer registered for `modality`. Returns 0 (not a
|
||||
* Geometry) when no realizer is registered, and geometry_is() on the result
|
||||
* is the check.
|
||||
* Manifold) when no realizer is registered, and manifold_is() on the result is
|
||||
* the check.
|
||||
*
|
||||
* THE RETURN TYPE IS THE CORRECTION. #144 shipped this as
|
||||
* `transduce(signal, modality) -> Geometry` — one vector out. That made
|
||||
* transduction a CONVERSION: take a thing, encode it, store a position. What
|
||||
* comes back from a conversion is a fingerprint, and a fingerprint supports
|
||||
* exactly two operations, match and rank. It cannot be decomposed, cannot have
|
||||
* one part grounded while another is not, and cannot be contradicted in a part
|
||||
* — it has no parts. Transduction is not conversion. It is DECOMPOSITION into
|
||||
* components plus the relations among them, and the relations are the content.
|
||||
* See the Manifold header above.
|
||||
*
|
||||
* There is deliberately NO built-in realizer, not even for text. A modality
|
||||
* the program has declared no organ for is one it genuinely cannot sense,
|
||||
* and returning nothing is more honest than quietly embedding a description
|
||||
* of the signal and calling that perception — which is the exact failure
|
||||
* this whole change exists to end.
|
||||
* the program has declared no organ for is one it genuinely cannot sense, and
|
||||
* returning nothing is more honest than quietly embedding a description of the
|
||||
* signal and calling that perception — the failure #144 named, and which a
|
||||
* single-vector return type quietly reintroduced one level down: a
|
||||
* one-vector-per-signal organ is a description of the signal, not a perception
|
||||
* of it.
|
||||
*
|
||||
* The result is validated to actually BE a Geometry before it is handed
|
||||
* back, so a realizer that returns something else transduced nothing rather
|
||||
* than handing a caller a value that will misbehave far from here. */
|
||||
* The result is validated to actually BE a Manifold before it is handed back.
|
||||
* A realizer still returning a bare Geometry — #144's contract — therefore
|
||||
* transduces NOTHING rather than handing back a value that decomposes to
|
||||
* nothing far from here. That is a deliberate hard failure, not an oversight:
|
||||
* "no organ" and "an organ that only fingerprints" must not look alike, which
|
||||
* is the same distinction realizer_register draws between an absent and a
|
||||
* broken organ. A realizer with genuinely one part says so with
|
||||
* manifold_single. */
|
||||
el_val_t transduce(el_val_t signal, el_val_t modality) {
|
||||
const char* m = EL_CSTR(modality);
|
||||
if (!m || !*m) return (el_val_t)0;
|
||||
el_realizer_fn fn = realizer_lookup(m);
|
||||
if (!fn) return (el_val_t)0;
|
||||
el_val_t g = fn(signal);
|
||||
return geom_of(g) ? g : (el_val_t)0;
|
||||
return mfld_of(g) ? g : (el_val_t)0;
|
||||
}
|
||||
|
||||
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
||||
|
||||
+60
-10
@@ -625,20 +625,70 @@ el_val_t geometry_free(el_val_t g); /* 1 if freed, 0 if not a
|
||||
el_val_t geometry_from_f32le_hex(el_val_t hex); /* 0 on empty/odd-length/non-hex */
|
||||
el_val_t geometry_to_f32le_hex(el_val_t g); /* "" if not a Geometry */
|
||||
|
||||
/* ── Realizers + transduce ───────────────────────────────────────────────────
|
||||
* A REALIZER maps one modality into geometry. Registration is by NAME, so a
|
||||
* new modality never requires a runtime patch: every El `fn name(...)`
|
||||
* compiles to a global C symbol with that exact name, and the registry
|
||||
* resolves it with dlsym against the running binary — the same mechanism
|
||||
* http_set_handler already relies on.
|
||||
/* ── Manifold: the result of a transduction ──────────────────────────────────
|
||||
* A transduced signal is a SUBGRAPH — named components, each with its own
|
||||
* geometry, plus typed weighted relations among them — not a single vector.
|
||||
* One vector is a fingerprint: matchable, rankable, and nothing else. A song
|
||||
* decomposes into pitch, interval, rhythm, harmonic function; the song IS the
|
||||
* structure of those relations, and collapsing it to a point discards exactly
|
||||
* what made it reasonable-about. See el_runtime.c ("Manifold") for the full
|
||||
* rationale, the key-addressing rule, and the ownership contract.
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Geometry { ... }
|
||||
* Components are addressed BY KEY, never by index, because the key is what
|
||||
* survives persistence: a component becomes a node, and it is separately
|
||||
* groundable precisely because it is separately named. Relation weight IS the
|
||||
* grounding (correspondence-and-censorship.md §1) — one quantity, no separate
|
||||
* score, nothing computed on read.
|
||||
*
|
||||
* OWNERSHIP: a Manifold is owned by the El caller and released with
|
||||
* manifold_free, which also releases every component's geometry. manifold_add
|
||||
* COPIES the geometry it is given and manifold_geometry RETURNS a copy, so no
|
||||
* component's vector is ever aliased in either direction. */
|
||||
el_val_t manifold_new(void); /* empty; 0 on failure */
|
||||
el_val_t manifold_is(el_val_t m); /* 1 if a live Manifold */
|
||||
el_val_t manifold_add(el_val_t m, el_val_t key, el_val_t role, el_val_t g);
|
||||
/* component index, or -1 on empty/duplicate
|
||||
* key or a value that is not a Geometry */
|
||||
el_val_t manifold_relate(el_val_t m, el_val_t from, el_val_t rel,
|
||||
el_val_t to, el_val_t weight);
|
||||
/* 1 ok / 0 if either endpoint is unknown —
|
||||
* an unresolvable edge is REFUSED, never
|
||||
* silently dropped */
|
||||
el_val_t manifold_size(el_val_t m); /* component count */
|
||||
el_val_t manifold_rel_count(el_val_t m); /* relation count */
|
||||
el_val_t manifold_index_of(el_val_t m, el_val_t key); /* index by key, or -1 */
|
||||
el_val_t manifold_key(el_val_t m, el_val_t i); /* "" if out of range */
|
||||
el_val_t manifold_role(el_val_t m, el_val_t i); /* "" if out of range */
|
||||
el_val_t manifold_geometry(el_val_t m, el_val_t i); /* a COPY the caller frees */
|
||||
el_val_t manifold_rel_from(el_val_t m, el_val_t j); /* source component key */
|
||||
el_val_t manifold_rel_name(el_val_t m, el_val_t j); /* relation name */
|
||||
el_val_t manifold_rel_to(el_val_t m, el_val_t j); /* target component key */
|
||||
el_val_t manifold_rel_weight(el_val_t m, el_val_t j); /* Float — the grounding */
|
||||
el_val_t manifold_single(el_val_t key, el_val_t role, el_val_t g);
|
||||
/* the degenerate one-part case, expressible
|
||||
* but visibly a size-1 manifold rather than
|
||||
* a parallel path back to a bare vector */
|
||||
el_val_t manifold_free(el_val_t m); /* 1 if freed, 0 otherwise */
|
||||
|
||||
/* ── Realizers + transduce ───────────────────────────────────────────────────
|
||||
* A REALIZER DECOMPOSES one modality into components and relations. It does
|
||||
* not encode a signal to a point; that operation is one layer below and is
|
||||
* called geometry. Registration is by NAME, so a new modality never requires a
|
||||
* runtime patch: every El `fn name(...)` compiles to a global C symbol with
|
||||
* that exact name, and the registry resolves it with dlsym against the running
|
||||
* binary — the same mechanism http_set_handler already relies on.
|
||||
*
|
||||
* fn tone_realizer(signal: String) -> Manifold { ... }
|
||||
* realizer_register("tone", "tone_realizer")
|
||||
* let g: Geometry = transduce(sample, "tone")
|
||||
*/
|
||||
* let m: Manifold = transduce(sample, "tone")
|
||||
*
|
||||
* SUPERSEDES #144's `transduce -> Geometry`. A realizer that still returns a
|
||||
* bare Geometry now transduces NOTHING (transduce returns 0), deliberately: an
|
||||
* organ that only fingerprints must not be indistinguishable from a working
|
||||
* one. A modality with genuinely one part says so with manifold_single. */
|
||||
el_val_t realizer_register(el_val_t modality, el_val_t fn_name); /* 1 ok / 0 unresolved */
|
||||
el_val_t realizer_has(el_val_t modality); /* 1 if a realizer is registered */
|
||||
el_val_t transduce(el_val_t signal, el_val_t modality); /* Geometry, or 0 if no organ */
|
||||
el_val_t transduce(el_val_t signal, el_val_t modality); /* Manifold, or 0 if no organ */
|
||||
|
||||
/* ── Engram local graph primitives ───────────────────────────────────────────
|
||||
* Operate on the CGI's local Engram knowledge graph.
|
||||
|
||||
+93
-4
@@ -31,6 +31,7 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- Codegen: function definitions, top-level `main()`, all expression forms above, control flow, decorator-as-AST-attachment.
|
||||
- Boundary seam: decorator arguments and stacking; VBD role enforcement via `#error`; `engram_boundary_beat` auto-emit at `@manager`/`@accessor` entry; `@route` dispatch tables (Section 9).
|
||||
- Program-level declarative blocks: `cgi`, `service`, and `program` — the last carrying process identity and configuration (Section 18).
|
||||
- **Geometry as a first-class value, and realizers declarable in El** — the `Geometry` type, the wire adapters, and `transduce` (Section 20). Landed 2026-08-16 (#141, #144).
|
||||
- C runtime: I/O, string operations, integer math, lists, maps, filesystem, command-line args, basic `json_get` substring lookup.
|
||||
|
||||
### Planned (in flight)
|
||||
@@ -41,9 +42,9 @@ This section is the **single source of truth** for what works and what is planne
|
||||
- **`cgi` block parsing.** Currently lexed (`cgi` is a keyword) but not parsed as a statement. Adding `parse_cgi_block` and codegen of `el_cgi_init` at the head of `main()`.
|
||||
- **Boundary epilogues.** The decorator seam injects a prologue only. Adding prologue/epilogue wrapping, the prerequisite for durability-as-an-effect (Section 19.1).
|
||||
- **`vessel` keyword.** Replaces `package` in manifests. Adding to lexer.
|
||||
- **Real `engram_*` runtime.** Currently stub. Adding in-process graph store with spreading activation, Hebbian strengthening, and disk persistence — see Section 16.4.
|
||||
- **Real `dharma_*` runtime.** Currently stub. Adding network transport, channel registry, identity resolution.
|
||||
- **Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs. Adding libcurl-backed client and a thread-pool server.
|
||||
- ~~**Real `engram_*` runtime.** Currently stub.~~ **Stale (verified 2026-08-16) — this is implemented, not planned.** `lang/runtime/el_runtime.c` carries the in-process graph store with spreading activation, Hebbian strengthening, disk persistence (paged store, magic `ENGST01`), an HNSW vector index behind a `eg_vindex_view`/`eg_vindex_maintain` publication boundary, and the full cognition surface (`engram_think_json`, `engram_ground_json`, `engram_assert_json`, `engram_attend_json`, `engram_correspondence_beat_json`). The "stub" description may still hold for the **lagging forks** (`lang/el-compiler/runtime/`, `products/web/runtime/`) — see `AGENTS.md`, which names those as downstream copies that cannot build the engram product. **Which runtime this line refers to needs a decision; it is not a fact that can be recovered from the text.**
|
||||
- ~~**Real `dharma_*` runtime.** Currently stub.~~ **Needs re-verification (2026-08-16).** Not checked in this pass; do not rely on either reading.
|
||||
- ~~**Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs.~~ **Stale.** libcurl-backed HTTP and a thread-pool server are live — `http_serve_async` is what `neuron/soul.el:729` runs before entering its awareness loop, and `realizer_register` resolves El functions through the same `dlsym` mechanism `http_set_handler` relies on.
|
||||
- **JSON, time, UUID, state, env, additional string/list/math builtins.** See Section 12 for the canonical list.
|
||||
|
||||
### Not in this language
|
||||
@@ -696,12 +697,22 @@ Every compiled program links against:
|
||||
- `el_runtime.h` — declaration header
|
||||
- `el_runtime.c` — implementation
|
||||
|
||||
The runtime is **multi-file**: `el_runtime.c` `#include`s the six `engram_*.h`
|
||||
headers and calls into all six sibling translation units, so linking it alone
|
||||
fails at `ld`. The canonical link set is `<runtime-dir>/SOURCES`.
|
||||
|
||||
Compile command:
|
||||
|
||||
```
|
||||
cc -std=c11 -I<runtime-dir> -o <prog> <prog>.c el_runtime.c
|
||||
cc -std=c11 -I<runtime-dir> -o <prog> <prog>.c \
|
||||
$(sed 's|^|<runtime-dir>/|' <runtime-dir>/SOURCES) \
|
||||
-lcurl -lssl -lcrypto -lpthread -lm
|
||||
```
|
||||
|
||||
Inside this repo, `scripts/el-runtime-sources.sh <runtime-dir>` prints that list
|
||||
(it strips comments; the raw `sed` above works against an installed SDK's
|
||||
`SOURCES`, which `install.sh` writes comment-free).
|
||||
|
||||
### 13.4 Output Format
|
||||
|
||||
```c
|
||||
@@ -1250,6 +1261,84 @@ Implementing either now would mean editing files under concurrent modification a
|
||||
|
||||
The prerequisite for 19.1 is the same in both cases: **lift the §9 seam from prologue-only to prologue/epilogue.** That change is independent of both collisions and can land first.
|
||||
|
||||
*(Status note, 2026-08-16: the geometry/`transduce` collision named above has since landed — see Section 20. The VIndex read-path collision has also landed; see `lang/spec/runtime-ownership.md` §5. 19.1 and 19.2 remain unimplemented, but the stated reason no longer holds for those two files.)*
|
||||
|
||||
---
|
||||
|
||||
## 20. Geometry — signal as a first-class value [implemented]
|
||||
|
||||
Landed 2026-08-16 (#141, #144). Declared here because the spec is the single source of truth for implemented-vs-planned, and this is a language surface, not a runtime detail.
|
||||
|
||||
### 20.1 Why this exists
|
||||
|
||||
Until 2026-08-16 no El ingest path could carry a vector. Nodes took **text**, and geometry was *derived* from that text. Text was therefore the **mandatory entry medium**: any non-text modality — a tone, a pulse, an image, a voice sample — had to be *described in prose first*, and the geometry subsequently reasoned over was the geometry **of the description, not of the signal**.
|
||||
|
||||
Two changes remove that, and neither is engram-specific — which is why they are in the language and not in the graph. Any program touching any modality needs them; the engram is merely one El program that happens to hold a graph.
|
||||
|
||||
1. **Geometry is a value that carries its own width.**
|
||||
2. **A realizer is an ordinary El function** — so admitting a new modality never requires a runtime patch.
|
||||
|
||||
### 20.2 The `Geometry` type
|
||||
|
||||
`Geometry` is an opaque boxed pointer, exactly like `Instant` / `Calendar` / `Rhythm`. **No codegen change was required** to add it — the annotation is just a type name.
|
||||
|
||||
```el
|
||||
let g: Geometry = geometry_new(4)
|
||||
```
|
||||
|
||||
| builtin | returns | notes |
|
||||
|---|---|---|
|
||||
| `geometry_new(dim)` | `Geometry` | zero-filled; `0` on failure |
|
||||
| `geometry_dim(g)` | `Int` | width; `0` if not a Geometry |
|
||||
| `geometry_is(g)` | `Int` | `1` if a live Geometry |
|
||||
| `geometry_get(g, i)` | `Float` | component |
|
||||
| `geometry_set(g, i, x)` | `Int` | `1` ok, `0` out of range |
|
||||
| `geometry_norm(g)` | `Float` | L2 — lets a caller check a realizer emitted **signal, not zeros** |
|
||||
| `geometry_free(g)` | `Int` | `1` if freed. Returns a value rather than `void` so it is safe in any expression position without a codegen void-builtin table entry |
|
||||
|
||||
**Ownership.** A `Geometry` is owned by the El caller and released with `geometry_free`. `node_attach_geometry` **copies**, so a node and the caller's value have independent lifetimes.
|
||||
|
||||
### 20.3 Wire adapters — the only place an encoding appears
|
||||
|
||||
```el
|
||||
geometry_from_f32le_hex(hex) -> Geometry // 0 on empty / odd-length / non-hex
|
||||
geometry_to_f32le_hex(g) -> String // "" if not a Geometry
|
||||
```
|
||||
|
||||
`f32le hex` is little-endian float32, 8 hex chars per component — the encoding the perception vessel's `/voice/embed` already emits. **The width is derived from the input length, never supplied by a caller**, which is why there is no max-dim constant to validate a claimed length against. Encodings appear here and nowhere else: at the edge.
|
||||
|
||||
### 20.4 Realizers and `transduce`
|
||||
|
||||
A **realizer** maps one modality into geometry. Registration is **by name**: every El `fn name(...)` compiles to a global C symbol with that exact name, and the registry resolves it with `dlsym` against the running binary — the same mechanism `http_set_handler` already relies on.
|
||||
|
||||
```el
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
g
|
||||
}
|
||||
|
||||
realizer_register("tone", "tone_realizer") // 1 ok / 0 unresolved
|
||||
let g: Geometry = transduce(sample, "tone") // Geometry, or 0 if no organ
|
||||
realizer_has("tone") // 1 if registered
|
||||
```
|
||||
|
||||
The registry keys on **modality**, not on registration order. `transduce` returns `0` when no organ is registered for the modality — an absent organ is a reportable state, not a silent zero vector.
|
||||
|
||||
**The claim this makes:** a realizer is not in the runtime and not known to the compiler. Adding a modality is writing an El function and registering a name. `lang/examples/transduce.el` is the worked example and doubles as an executable proof — it exits non-zero if any check fails.
|
||||
|
||||
### 20.5 Two comparison hazards this surface exposed
|
||||
|
||||
Both were **measured**, not stylistic, and both are properties of the current `elc` that any El author should know:
|
||||
|
||||
- **`==` lowers numerically only when both operand *names* are in the per-function int-name set** that `let x: Int` populates. A bare `f(x) == 0` is not a registered name and lowers to `str_eq` — `strcmp` on two integers reinterpreted as pointers. `<` and `>` lower directly with no inference, so truthiness against a builtin's return is written `> 0` / `< 1`.
|
||||
- **`+` dispatches on whether both operands are known-Int, and a user-defined `fn` call is not.** `let fails: Int = fails + check(...)` lowered to **string concatenation** and printed `4343632752` — a pointer. Nothing was wrong with the checks; the tally was lying. Failing fast needs no arithmetic at all, so there is nothing left to get wrong.
|
||||
|
||||
### 20.6 What this does not do
|
||||
|
||||
`transduce` produces geometry; it does not decide what the geometry *means*. Nothing here grounds anything. Grounding is the edge weight in the graph the geometry is later attached to — see `lang/spec/correspondence-and-censorship.md`.
|
||||
|
||||
---
|
||||
|
||||
End of specification.
|
||||
|
||||
@@ -28,12 +28,12 @@ Each of these is a distinct merged or proposed fix. Each addresses one deposit.
|
||||
| VIndex freed under a concurrent reader | `el_runtime.c:9424` | `fb32d15` guard (merged 08:46:43) |
|
||||
| `_eg_vindex_seen` realloc'd on a read path | `el_runtime.c:9412` | same guard |
|
||||
| `vindex_insert` on a read path | `el_runtime.c:9434`, `9450` | same guard |
|
||||
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:79–81`, `169–186`, `195` | proposed: move to per-search frame |
|
||||
| shared `visited` / epoch scratch stomped by concurrent searches | `engram_vindex.c:79–81`, `169–186`, `195` | ~~proposed:~~ **built** — moved to the call frame (§3.1(1), §5); TSan `readers` half clean (§7a) |
|
||||
| nine append sites, none indexing → lazily-embedded nodes invisible | `el_runtime.c:7806, 7988, 8148, 8224, 11526, 11731, 12050, 15295, 15312` | "embed-gap #20", patched by making the *read* path catch up (`9439` comment) |
|
||||
|
||||
**Measured:** all file/line references above, read 2026-08-16. Crash frames `engram_activate → eg_vindex_sync → vindex_insert → _realloc → _xzm_xzone_malloc_freelist_outlined` are accounted for by rows 2–4.
|
||||
|
||||
**Inferred, not yet verified:** that the nine append sites do not share a single commit point. This needs one pass before Change C is sized.
|
||||
~~**Inferred, not yet verified:** that the nine append sites do not share a single commit point. This needs one pass before Change C is sized.~~ **Moot — see §7.** The question was mis-aimed: node append is not the event that owns index membership, because a node without an embedding cannot be in a vector index. The five *embedding-assignment* sites are the real owner points.
|
||||
|
||||
---
|
||||
|
||||
@@ -135,12 +135,21 @@ The payoff of owning the language is unchanged and is now *cheaper*: introduced
|
||||
|
||||
## 6. Sequencing
|
||||
|
||||
> **⚠ Steps 2–5 belong to the abandoned capability-ABI §3 and are superseded
|
||||
> (2026-08-16).** §3 was re-derived: the engram is immutable and recall is
|
||||
> projection, so *what does not mutate needs no ownership discipline* and the
|
||||
> question is dissolved rather than answered. There is no context type, no
|
||||
> capability type, and no codegen change — **`const` is the capability**, and the
|
||||
> constraint travels with the type of the thing rather than the shape of every call
|
||||
> site, so **no sweep is needed at all** (§4). Steps 1, 6 and 7 stand. Struck rather
|
||||
> than deleted, because the abandoned plan is why §4's cost argument is short.
|
||||
|
||||
1. **Read** how builtins are declared and dispatched, to confirm the call sites are compiler-generated in one place. *(This determines whether §4 holds. If dispatch is scattered, re-size before proceeding.)*
|
||||
2. Introduce the context type and capability types.
|
||||
3. Codegen emits the context at every builtin call site.
|
||||
4. Mechanical sweep of builtin signatures.
|
||||
5. Move index maintenance behind the write capability; the three read callers take the read capability.
|
||||
6. Delete the residue-fixes listed in §5.
|
||||
2. ~~Introduce the context type and capability types.~~ **Superseded** — `const`.
|
||||
3. ~~Codegen emits the context at every builtin call site.~~ **Superseded** — no codegen change.
|
||||
4. ~~Mechanical sweep of builtin signatures.~~ **Superseded** — the constraint travels with the type.
|
||||
5. ~~Move index maintenance behind the write capability; the three read callers take the read capability.~~ **Done, differently:** `eg_vindex_maintain` (exclusive, sole mutator) / `eg_vindex_view` (`const VIndex*`, shared readers), with `eg_vindex_note_embedded` as the write-side owner. This is a **publication** boundary, not a capability split — HNSW insert is not an append, so purity alone was insufficient (§2a, §3.1(3)).
|
||||
6. Delete the residue-fixes listed in §5. *(Partially done — see §5's "NOT deleted" list; a residue whose structure has not been converted must be left standing.)*
|
||||
7. **One** build of soul from el dev — which resolves the `state_get` leak and the crash together, rather than deploying a leak fix that reintroduces the crash.
|
||||
|
||||
---
|
||||
|
||||
+440
-140
@@ -1,61 +1,128 @@
|
||||
import "../../runtime/eltest.el"
|
||||
// test_transduce.el — geometry as a first-class El value, and realizers
|
||||
// declared in El rather than patched into the runtime.
|
||||
// test_transduce.el — transduction produces a SUBGRAPH, not a point.
|
||||
//
|
||||
// WHAT IS ACTUALLY UNDER TEST. Until 2026-08-16 no El ingest path could carry
|
||||
// a vector: nodes took text, and geometry was DERIVED from that text. Text was
|
||||
// therefore the mandatory entry medium, so any non-text modality had to be
|
||||
// DESCRIBED in prose first and the geometry we reasoned over was the geometry
|
||||
// OF THE DESCRIPTION, not of the signal. The fix has two halves, and this file
|
||||
// exercises both:
|
||||
// WHAT IS ACTUALLY UNDER TEST. #144 moved transduction into the language and
|
||||
// got the dispatch right: realizers declared in El, resolved by name, no
|
||||
// runtime patch per modality. It got the RESULT TYPE wrong —
|
||||
// `transduce(signal, modality) -> Geometry`, one vector per signal.
|
||||
//
|
||||
// 1. Geometry is a VALUE — it carries its own width, so nothing has to
|
||||
// assert a width against a string's length.
|
||||
// 2. A REALIZER is an ordinary El function. `tone_realizer` below is not in
|
||||
// the runtime, is not known to the compiler, and is not special in any
|
||||
// way; it is registered BY NAME and dispatched to through transduce().
|
||||
// That is the load-bearing claim: adding a modality must not require a
|
||||
// runtime patch, or nothing has actually moved into the language.
|
||||
// One vector is a FINGERPRINT. It can be matched and it can be ranked, and
|
||||
// that is the whole of what it can ever do. It cannot be decomposed, cannot
|
||||
// have one part grounded while another is not, and cannot be contradicted in
|
||||
// one part while holding in another — because it has no parts. Treating
|
||||
// transduction as a CONVERSION (signal in, position out) is the premise this
|
||||
// file exists to falsify.
|
||||
//
|
||||
// A song is not a point. It decomposes into pitch, interval, rhythm, harmonic
|
||||
// function — components, each with its own geometry, plus the relations among
|
||||
// them. THE SONG IS THE STRUCTURE OF THE RELATIONS. So transduction yields a
|
||||
// Manifold: named components carrying geometry, and typed weighted relations
|
||||
// between them.
|
||||
//
|
||||
// The geometry tests below are UNCHANGED from #144 and still pass, which is
|
||||
// the point: Geometry was never wrong, it was misplaced. A vector is the right
|
||||
// representation for a COMPONENT. It was only ever wrong as the representation
|
||||
// of a whole transduced signal.
|
||||
//
|
||||
// COMPARISON DISCIPLINE IN THIS FILE (measured 2026-08-16, not stylistic):
|
||||
// elc lowers `a == b` to a NUMERIC comparison only when both operand names are
|
||||
// in the per-function int-name set, which `let x: Int` populates. A bare call
|
||||
// like `geometry_is(g) == 0` is not a registered name, so it lowers to
|
||||
// like `manifold_size(m) == 5` is not a registered name, so it lowers to
|
||||
// `str_eq(...)` — strcmp on two integers reinterpreted as pointers. `<` and `>`
|
||||
// lower directly via binop_to_c with no type inference at all, so truthiness is
|
||||
// written `> 0` / `< 1` here, and any exact `==` is done on a value first bound
|
||||
// through `let x: Int`.
|
||||
//
|
||||
// ONE FURTHER RULE, measured while writing this file: that int-name set LEAKS
|
||||
// ACROSS `test` BLOCKS. Binding `dn` as a Float in one test and as an Int in
|
||||
// another silently demoted the Int comparison to str_eq and failed an
|
||||
// assertion that was arithmetically true. Every Int-bound name compared with
|
||||
// `==` here is therefore spelled UNIQUELY across the whole file (note_dim,
|
||||
// iv_dim, ...), rather than reusing a short name per test.
|
||||
|
||||
// ── A realizer, written entirely in El ──────────────────────────────────────
|
||||
// Maps a "tone" signal into a 4-component geometry. Deliberately trivial —
|
||||
// what is being proven is that an El function can BE a realizer, not that
|
||||
// this is good acoustics. The one real property it has: distinct signals
|
||||
// produce distinct geometry, so the test can tell transduction from a stub.
|
||||
fn tone_realizer(signal: String) -> Geometry {
|
||||
// ── A DECOMPOSING realizer, written entirely in El ──────────────────────────
|
||||
// "tone" signals are note letters, e.g. "CEG". This realizer does NOT return
|
||||
// one vector for the chord. It returns the PARTS — one component per note, one
|
||||
// per interval between adjacent notes — and the relations that make those
|
||||
// parts a chord rather than an unordered bag of pitches.
|
||||
//
|
||||
// The interval is deliberately a COMPONENT, not an attribute of a note. An
|
||||
// interval is a thing with its own geometry that belongs to neither endpoint;
|
||||
// modelling it as a field on a note is exactly the collapse this change
|
||||
// rejects, one level down.
|
||||
fn tone_realizer(signal: String) -> Manifold {
|
||||
let m: Manifold = manifold_new()
|
||||
let n: Int = str_len(signal)
|
||||
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let code: Int = str_char_code(signal, i)
|
||||
let g: Geometry = geometry_new(2)
|
||||
let s0: Int = geometry_set(g, 0, int_to_float(code))
|
||||
let s1: Int = geometry_set(g, 1, int_to_float(i))
|
||||
let idx: Int = manifold_add(m, "note:" + int_to_str(i), "pitch", g)
|
||||
let f: Int = geometry_free(g)
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
let j: Int = 1
|
||||
while j < n {
|
||||
let a: Int = str_char_code(signal, j - 1)
|
||||
let b: Int = str_char_code(signal, j)
|
||||
let lo: String = "note:" + int_to_str(j - 1)
|
||||
let hi: String = "note:" + int_to_str(j)
|
||||
let key: String = "interval:" + int_to_str(j - 1) + "-" + int_to_str(j)
|
||||
let g: Geometry = geometry_new(1)
|
||||
let s: Int = geometry_set(g, 0, int_to_float(b - a))
|
||||
let idx: Int = manifold_add(m, key, "interval", g)
|
||||
let f: Int = geometry_free(g)
|
||||
let e1: Int = manifold_relate(m, key, "spans", lo, 0.9)
|
||||
let e2: Int = manifold_relate(m, key, "spans", hi, 0.9)
|
||||
let e3: Int = manifold_relate(m, lo, "sounds_before", hi, 0.8)
|
||||
j = j + 1
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
// A second realizer for a different modality, to prove the registry keys on
|
||||
// modality and does not just hand back "the last thing registered". Its
|
||||
// decomposition has a DIFFERENT shape — two components, one relation — so a
|
||||
// test can tell the two organs apart by structure alone.
|
||||
fn pulse_realizer(signal: String) -> Manifold {
|
||||
let m: Manifold = manifold_new()
|
||||
let ga: Geometry = geometry_new(1)
|
||||
let sa: Int = geometry_set(ga, 0, 1.0)
|
||||
let ia: Int = manifold_add(m, "onset", "event", ga)
|
||||
let fa: Int = geometry_free(ga)
|
||||
let gb: Geometry = geometry_new(1)
|
||||
let sb: Int = geometry_set(gb, 0, 0.0)
|
||||
let ib: Int = manifold_add(m, "decay", "envelope", gb)
|
||||
let fb: Int = geometry_free(gb)
|
||||
let e: Int = manifold_relate(m, "onset", "decays_into", "decay", 0.7)
|
||||
m
|
||||
}
|
||||
|
||||
// #144's ACTUAL CONTRACT, preserved verbatim as a control: a realizer that
|
||||
// returns one vector for the whole signal. This is not a strawman — it is what
|
||||
// the merged primitive asked realizers to be. It must now transduce NOTHING.
|
||||
fn fingerprint_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(4)
|
||||
let n: Int = str_len(signal)
|
||||
let a: Int = geometry_set(g, 0, int_to_float(n))
|
||||
let b: Int = geometry_set(g, 1, int_to_float(n * 2))
|
||||
let c: Int = geometry_set(g, 2, int_to_float(n * 3))
|
||||
let d: Int = geometry_set(g, 3, int_to_float(n * 4))
|
||||
g
|
||||
}
|
||||
|
||||
// A second realizer for a different modality, to prove the registry keys on
|
||||
// modality and does not just hand back "the last thing registered".
|
||||
fn pulse_realizer(signal: String) -> Geometry {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let a: Int = geometry_set(g, 0, 1.0)
|
||||
let b: Int = geometry_set(g, 1, 0.0)
|
||||
g
|
||||
}
|
||||
|
||||
// A deliberately BROKEN realizer: it returns something that is not a Geometry.
|
||||
// transduce() must not hand this back to a caller as if it were one.
|
||||
fn bogus_realizer(signal: String) -> Geometry {
|
||||
// A realizer returning something that is not a value at all.
|
||||
fn bogus_realizer(signal: String) -> Manifold {
|
||||
return 12345
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Geometry — unchanged from #144. A vector is the right representation for a
|
||||
// COMPONENT; it was only ever wrong as the representation of a whole signal.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test "geometry-is-a-value-with-its-own-width" {
|
||||
let g: Geometry = geometry_new(8)
|
||||
let live: Int = geometry_is(g)
|
||||
@@ -67,17 +134,12 @@ test "geometry-is-a-value-with-its-own-width" {
|
||||
}
|
||||
|
||||
test "geometry-rejects-nonsense-without-an-arbitrary-bound" {
|
||||
// dim <= 0 is not a width. Note there is deliberately no MAX dim here:
|
||||
// #141 needed `dim <= 8192` only to bound an allocation sized from a
|
||||
// caller's claim about a string. A value that carries its own width has
|
||||
// nothing left to validate, so the only failure left is allocation.
|
||||
let zero: Geometry = geometry_new(0)
|
||||
let z: Int = geometry_is(zero)
|
||||
assert z < 1, "dim 0 is not a geometry"
|
||||
let neg: Geometry = geometry_new(-4)
|
||||
let n: Int = geometry_is(neg)
|
||||
assert n < 1, "negative dim is not a geometry"
|
||||
// Accessors must be total: a non-geometry is 0-width, never a crash.
|
||||
let nd: Int = geometry_dim(0)
|
||||
assert nd < 1, "geometry_dim of a non-geometry is 0"
|
||||
let ni: Int = geometry_is(0)
|
||||
@@ -105,21 +167,11 @@ test "geometry-components-round-trip" {
|
||||
}
|
||||
|
||||
test "hex-is-an-edge-adapter-and-derives-its-own-width" {
|
||||
// 2 components, little-endian float32: 1.0 = 0000803f, 2.0 = 00000040.
|
||||
let g: Geometry = geometry_from_f32le_hex("0000803f00000040")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live > 0, "valid hex decodes to a Geometry"
|
||||
let d: Int = geometry_dim(g)
|
||||
assert d == 2, "width is DERIVED from the input, never supplied"
|
||||
let a: Float = geometry_get(g, 0)
|
||||
let da: Float = a - 1.0
|
||||
assert da < 0.001, "first component decoded"
|
||||
assert da > -0.001, "first component decoded"
|
||||
let b: Float = geometry_get(g, 1)
|
||||
let db: Float = b - 2.0
|
||||
assert db < 0.001, "second component decoded"
|
||||
assert db > -0.001, "second component decoded"
|
||||
// Egress adapter is the exact inverse.
|
||||
let hex_dim: Int = geometry_dim(g)
|
||||
assert hex_dim == 2, "width is DERIVED from the input, never supplied"
|
||||
let back: String = geometry_to_f32le_hex(g)
|
||||
assert str_eq(back, "0000803f00000040"), "hex round-trips exactly"
|
||||
let freed: Int = geometry_free(g)
|
||||
@@ -137,98 +189,346 @@ test "hex-rejects-malformed-input" {
|
||||
assert nh < 1, "non-hex characters are refused"
|
||||
}
|
||||
|
||||
test "a-realizer-declared-in-el-is-a-first-class-realizer" {
|
||||
// THE CLAIM: tone_realizer is an ordinary El function. It is not in the
|
||||
// runtime and the compiler knows nothing about it. Registering it by name
|
||||
// is enough to make it the organ for a modality.
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
assert reg > 0, "an El fn registers as a realizer by name"
|
||||
let has: Int = realizer_has("tone")
|
||||
assert has > 0, "the modality now has an organ"
|
||||
|
||||
let g: Geometry = transduce("aaa", "tone")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live > 0, "transduce returns real geometry"
|
||||
let d: Int = geometry_dim(g)
|
||||
assert d == 4, "the El realizer determined the width, not the runtime"
|
||||
// str_len("aaa") == 3, so component 0 must be 3.0 — proof the signal
|
||||
// actually reached the El function rather than a stub answering for it.
|
||||
let c0: Float = geometry_get(g, 0)
|
||||
let dc: Float = c0 - 3.0
|
||||
assert dc < 0.001, "the signal reached the El realizer"
|
||||
assert dc > -0.001, "the signal reached the El realizer"
|
||||
let freed: Int = geometry_free(g)
|
||||
}
|
||||
|
||||
test "distinct-signals-transduce-to-distinct-geometry" {
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let g1: Geometry = transduce("aa", "tone")
|
||||
let g2: Geometry = transduce("aaaaa", "tone")
|
||||
let a: Float = geometry_get(g1, 0)
|
||||
let b: Float = geometry_get(g2, 0)
|
||||
let diff: Float = b - a
|
||||
// 5 - 2 = 3. If transduction were a stub these would be equal.
|
||||
assert diff > 2.9, "different signals produce different geometry"
|
||||
assert diff < 3.1, "different signals produce different geometry"
|
||||
let f1: Int = geometry_free(g1)
|
||||
let f2: Int = geometry_free(g2)
|
||||
}
|
||||
|
||||
test "the-registry-keys-on-modality" {
|
||||
let r1: Int = realizer_register("tone", "tone_realizer")
|
||||
let r2: Int = realizer_register("pulse", "pulse_realizer")
|
||||
assert r2 > 0, "a second modality registers independently"
|
||||
let gt: Geometry = transduce("aaa", "tone")
|
||||
let gp: Geometry = transduce("aaa", "pulse")
|
||||
let dt: Int = geometry_dim(gt)
|
||||
let dp: Int = geometry_dim(gp)
|
||||
assert dt == 4, "tone still routes to its own realizer"
|
||||
assert dp == 2, "pulse routes to a different realizer"
|
||||
let f1: Int = geometry_free(gt)
|
||||
let f2: Int = geometry_free(gp)
|
||||
}
|
||||
|
||||
test "no-organ-is-reported-as-no-organ" {
|
||||
// A modality with no realizer must transduce to NOTHING. It must never
|
||||
// fall back to embedding a description of the signal and calling that
|
||||
// perception — that silent substitution is the entire defect this change
|
||||
// exists to end.
|
||||
let has: Int = realizer_has("echolocation")
|
||||
assert has < 1, "unregistered modality has no organ"
|
||||
let g: Geometry = transduce("anything", "echolocation")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live < 1, "no realizer means no geometry, not fake geometry"
|
||||
}
|
||||
|
||||
test "registration-of-an-unresolvable-name-fails-loudly" {
|
||||
// Reported at the moment of WIRING, not later as "this modality mysteriously
|
||||
// produces nothing". Distinguishing "no organ" from "broken organ" is the
|
||||
// lesson that made this whole change necessary.
|
||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||
assert bad < 1, "an unresolvable realizer name is a registration failure"
|
||||
let has: Int = realizer_has("ghost")
|
||||
assert has < 1, "and nothing gets registered"
|
||||
}
|
||||
|
||||
test "a-realizer-returning-non-geometry-transduces-nothing" {
|
||||
let reg: Int = realizer_register("bogus", "bogus_realizer")
|
||||
assert reg > 0, "the symbol resolves, so registration succeeds"
|
||||
// ...but the contract is enforced at the boundary, so the caller never
|
||||
// receives a value that would misbehave far away from here.
|
||||
let g: Geometry = transduce("x", "bogus")
|
||||
let live: Int = geometry_is(g)
|
||||
assert live < 1, "a non-Geometry return transduced nothing"
|
||||
}
|
||||
|
||||
test "norm-lets-a-caller-check-a-realizer-emitted-signal" {
|
||||
let g: Geometry = geometry_new(2)
|
||||
let z: Float = geometry_norm(g)
|
||||
assert z < 0.001, "a fresh geometry is zero — norm says so"
|
||||
let s0: Int = geometry_set(g, 0, 3.0)
|
||||
let s1: Int = geometry_set(g, 1, 4.0)
|
||||
let n: Float = geometry_norm(g)
|
||||
let dn: Float = n - 5.0
|
||||
assert dn < 0.001, "3-4-5: norm is 5"
|
||||
assert dn > -0.001, "3-4-5: norm is 5"
|
||||
let nrm: Float = geometry_norm(g)
|
||||
let dnorm: Float = nrm - 5.0
|
||||
assert dnorm < 0.001, "3-4-5: norm is 5"
|
||||
assert dnorm > -0.001, "3-4-5: norm is 5"
|
||||
let freed: Int = geometry_free(g)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Manifold — the corrected result of a transduction
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test "a-manifold-is-a-value-that-holds-parts-and-relations" {
|
||||
let m: Manifold = manifold_new()
|
||||
let live: Int = manifold_is(m)
|
||||
assert live > 0, "manifold_new returns a live Manifold"
|
||||
let fresh_sz: Int = manifold_size(m)
|
||||
assert fresh_sz == 0, "a fresh manifold has no components"
|
||||
let fresh_rc: Int = manifold_rel_count(m)
|
||||
assert fresh_rc == 0, "a fresh manifold has no relations"
|
||||
let freed: Int = manifold_free(m)
|
||||
assert freed > 0, "manifold_free reports what it did"
|
||||
}
|
||||
|
||||
test "manifold-accessors-are-total" {
|
||||
let ni2: Int = manifold_is(0)
|
||||
assert ni2 < 1, "manifold_is of a non-manifold is 0"
|
||||
let ns: Int = manifold_size(0)
|
||||
assert ns < 1, "manifold_size of a non-manifold is 0"
|
||||
let nf2: Int = manifold_free(0)
|
||||
assert nf2 < 1, "manifold_free of a non-manifold is a no-op"
|
||||
let k: String = manifold_key(0, 0)
|
||||
assert str_eq(k, ""), "manifold_key of a non-manifold is empty, never a crash"
|
||||
}
|
||||
|
||||
test "components-are-addressed-by-key-not-by-index" {
|
||||
// The key is what survives persistence: a component becomes a node, and it
|
||||
// is separately groundable precisely because it is separately NAMED.
|
||||
let m: Manifold = manifold_new()
|
||||
let g: Geometry = geometry_new(1)
|
||||
let s: Int = geometry_set(g, 0, 7.0)
|
||||
let first_idx: Int = manifold_add(m, "rhythm", "temporal", g)
|
||||
assert first_idx == 0, "the first component is index 0"
|
||||
let found_idx: Int = manifold_index_of(m, "rhythm")
|
||||
assert found_idx == 0, "a component is found by its key"
|
||||
let missing: Int = manifold_index_of(m, "never_added")
|
||||
assert missing < 0, "an unknown key resolves to -1, not to component 0"
|
||||
let role: String = manifold_role(m, 0)
|
||||
assert str_eq(role, "temporal"), "a component carries what KIND of part it is"
|
||||
let f: Int = geometry_free(g)
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "a-duplicate-key-is-refused-because-addressing-must-be-unambiguous" {
|
||||
let m: Manifold = manifold_new()
|
||||
let g: Geometry = geometry_new(1)
|
||||
let ok_idx: Int = manifold_add(m, "pitch", "spectral", g)
|
||||
assert ok_idx == 0, "first add succeeds"
|
||||
let dup: Int = manifold_add(m, "pitch", "spectral", g)
|
||||
assert dup < 0, "two components answering to one name is not an addressing scheme"
|
||||
let dup_sz: Int = manifold_size(m)
|
||||
assert dup_sz == 1, "and the duplicate did not land"
|
||||
let f: Int = geometry_free(g)
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "a-part-with-no-geometry-is-not-a-part" {
|
||||
let m: Manifold = manifold_new()
|
||||
let bad: Int = manifold_add(m, "ghost", "none", 0)
|
||||
assert bad < 0, "a non-Geometry is refused as a component"
|
||||
let empty_key: Int = manifold_add(m, "", "none", geometry_new(1))
|
||||
assert empty_key < 0, "an unaddressable component is refused"
|
||||
let none_sz: Int = manifold_size(m)
|
||||
assert none_sz < 1, "nothing landed"
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "an-edge-to-a-nonexistent-endpoint-is-refused-not-dropped" {
|
||||
// A decomposition that silently loses edges is indistinguishable from one
|
||||
// that never had them.
|
||||
let m: Manifold = manifold_new()
|
||||
let g: Geometry = geometry_new(1)
|
||||
let a: Int = manifold_add(m, "here", "part", g)
|
||||
let dangling: Int = manifold_relate(m, "here", "points_at", "nowhere", 0.5)
|
||||
assert dangling < 1, "an edge to an unknown target is refused"
|
||||
let backwards: Int = manifold_relate(m, "nowhere", "points_at", "here", 0.5)
|
||||
assert backwards < 1, "an edge from an unknown source is refused"
|
||||
let dang_rc: Int = manifold_rel_count(m)
|
||||
assert dang_rc < 1, "and no relation was recorded"
|
||||
let f: Int = geometry_free(g)
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "a-component-owns-its-geometry-independently-of-the-caller" {
|
||||
// manifold_add COPIES. Freeing the caller's vector must not disturb the
|
||||
// component, or a decomposition would be unusable the moment it was built.
|
||||
let m: Manifold = manifold_new()
|
||||
let g: Geometry = geometry_new(2)
|
||||
let s0: Int = geometry_set(g, 0, 42.0)
|
||||
let idx: Int = manifold_add(m, "part", "kind", g)
|
||||
let freed: Int = geometry_free(g)
|
||||
assert freed > 0, "the caller freed its own vector"
|
||||
let back: Geometry = manifold_geometry(m, 0)
|
||||
let live: Int = geometry_is(back)
|
||||
assert live > 0, "the component still has geometry"
|
||||
let v: Float = geometry_get(back, 0)
|
||||
let dv: Float = v - 42.0
|
||||
assert dv < 0.001, "and it is the right geometry"
|
||||
assert dv > -0.001, "and it is the right geometry"
|
||||
let fb: Int = geometry_free(back)
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// transduce — signal in, SUBGRAPH out
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test "a-realizer-declared-in-el-is-a-first-class-realizer" {
|
||||
// THE CLAIM, unchanged from #144: tone_realizer is an ordinary El function.
|
||||
// It is not in the runtime and the compiler knows nothing about it.
|
||||
// Registering it by name is enough to make it the organ for a modality.
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
assert reg > 0, "an El fn registers as a realizer by name"
|
||||
let has: Int = realizer_has("tone")
|
||||
assert has > 0, "the modality now has an organ"
|
||||
|
||||
let m: Manifold = transduce("CEG", "tone")
|
||||
let live: Int = manifold_is(m)
|
||||
assert live > 0, "transduce returns a real Manifold"
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "transduction-decomposes-a-signal-into-parts" {
|
||||
// THE CENTRAL CLAIM. "CEG" is three notes. What comes back is not one
|
||||
// vector standing for a chord — it is five addressable parts (three notes,
|
||||
// two intervals) and six relations. A fingerprint has one part by
|
||||
// construction and could not express this at any width.
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let m: Manifold = transduce("CEG", "tone")
|
||||
|
||||
let ceg_sz: Int = manifold_size(m)
|
||||
assert ceg_sz == 5, "three notes and two intervals are five distinct parts"
|
||||
let ceg_rc: Int = manifold_rel_count(m)
|
||||
assert ceg_rc == 6, "and the parts stand in six stated relations"
|
||||
|
||||
// Every part is independently addressable BY NAME.
|
||||
let n0: Int = manifold_index_of(m, "note:0")
|
||||
assert n0 > -1, "the first note is addressable on its own"
|
||||
let n2: Int = manifold_index_of(m, "note:2")
|
||||
assert n2 > -1, "so is the third"
|
||||
let iv: Int = manifold_index_of(m, "interval:0-1")
|
||||
assert iv > -1, "so is the interval between the first two"
|
||||
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "each-part-carries-its-own-geometry" {
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let m: Manifold = transduce("CEG", "tone")
|
||||
|
||||
// 'C' is 67. The note component's geometry is the note's, not the chord's.
|
||||
let note_i: Int = manifold_index_of(m, "note:0")
|
||||
let gn: Geometry = manifold_geometry(m, note_i)
|
||||
let note_dim: Int = geometry_dim(gn)
|
||||
assert note_dim == 2, "a note component has the width its realizer gave it"
|
||||
let pitch: Float = geometry_get(gn, 0)
|
||||
let dpitch: Float = pitch - 67.0
|
||||
assert dpitch < 0.001, "and it is C, so the signal reached the El realizer"
|
||||
assert dpitch > -0.001, "and it is C, so the signal reached the El realizer"
|
||||
|
||||
// Parts may have DIFFERENT widths. A single vector per signal cannot
|
||||
// represent parts of unequal dimensionality at all.
|
||||
let iv_i: Int = manifold_index_of(m, "interval:0-1")
|
||||
let gi: Geometry = manifold_geometry(m, iv_i)
|
||||
let iv_dim: Int = geometry_dim(gi)
|
||||
assert iv_dim == 1, "an interval component has its own, different width"
|
||||
|
||||
let f1: Int = geometry_free(gn)
|
||||
let f2: Int = geometry_free(gi)
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "the-relations-are-content-no-single-part-carries" {
|
||||
// THE POINT OF THE WHOLE CHANGE. C->E is two semitones. That "2" is not a
|
||||
// property of C and not a property of E; it exists only BETWEEN them. A
|
||||
// representation with no relations cannot hold it, which is why collapsing
|
||||
// a signal to one vector does not merely lose resolution — it loses a
|
||||
// category of content.
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let m: Manifold = transduce("CEG", "tone")
|
||||
|
||||
let step_i: Int = manifold_index_of(m, "interval:0-1")
|
||||
let gi: Geometry = manifold_geometry(m, step_i)
|
||||
let step: Float = geometry_get(gi, 0)
|
||||
let dstep: Float = step - 2.0
|
||||
assert dstep < 0.001, "C to E is two semitones"
|
||||
assert dstep > -0.001, "C to E is two semitones"
|
||||
|
||||
// And the interval is WIRED to both endpoints, so the structure says which
|
||||
// two things it is the interval between.
|
||||
let spans: Int = 0
|
||||
let span_rc: Int = manifold_rel_count(m)
|
||||
let k: Int = 0
|
||||
while k < span_rc {
|
||||
let rn: String = manifold_rel_name(m, k)
|
||||
let rf: String = manifold_rel_from(m, k)
|
||||
if str_eq(rn, "spans") {
|
||||
if str_eq(rf, "interval:0-1") { spans = spans + 1 }
|
||||
}
|
||||
k = k + 1
|
||||
}
|
||||
assert spans == 2, "the interval is related to both notes it spans"
|
||||
|
||||
let fg: Int = geometry_free(gi)
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "relation-weight-is-the-grounding-carried-on-the-edge" {
|
||||
// correspondence-and-censorship.md §1: grounding is an attribute of the
|
||||
// edge and it IS the weight — one quantity, not a score computed beside
|
||||
// it. A realizer states a relation and its weight is the claim.
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let m: Manifold = transduce("CE", "tone")
|
||||
|
||||
let ce_rc: Int = manifold_rel_count(m)
|
||||
assert ce_rc == 3, "one interval yields two spans and one ordering"
|
||||
|
||||
let found_w: Int = 0
|
||||
let k: Int = 0
|
||||
while k < ce_rc {
|
||||
let rn: String = manifold_rel_name(m, k)
|
||||
if str_eq(rn, "sounds_before") {
|
||||
let w: Float = manifold_rel_weight(m, k)
|
||||
let dw: Float = w - 0.8
|
||||
if dw < 0.001 { if dw > -0.001 { found_w = found_w + 1 } }
|
||||
}
|
||||
k = k + 1
|
||||
}
|
||||
assert found_w == 1, "the ordering relation carries the weight its realizer stated"
|
||||
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
test "distinct-signals-decompose-differently" {
|
||||
let reg: Int = realizer_register("tone", "tone_realizer")
|
||||
let m2: Manifold = transduce("CE", "tone")
|
||||
let m3: Manifold = transduce("CEG", "tone")
|
||||
let two_sz: Int = manifold_size(m2)
|
||||
let three_sz: Int = manifold_size(m3)
|
||||
assert two_sz == 3, "two notes decompose into two notes and one interval"
|
||||
assert three_sz == 5, "three notes decompose into three notes and two intervals"
|
||||
// Structure differs, not just position: fingerprints of a two-note and a
|
||||
// three-note signal have identical shape and differ only numerically.
|
||||
let two_rc: Int = manifold_rel_count(m2)
|
||||
let three_rc: Int = manifold_rel_count(m3)
|
||||
assert two_rc < three_rc, "and the relational structure itself differs"
|
||||
let f2: Int = manifold_free(m2)
|
||||
let f3: Int = manifold_free(m3)
|
||||
}
|
||||
|
||||
test "the-registry-keys-on-modality" {
|
||||
let r1: Int = realizer_register("tone", "tone_realizer")
|
||||
let rp: Int = realizer_register("pulse", "pulse_realizer")
|
||||
assert rp > 0, "a second modality registers independently"
|
||||
let mt: Manifold = transduce("CEG", "tone")
|
||||
let mp: Manifold = transduce("CEG", "pulse")
|
||||
let tone_sz: Int = manifold_size(mt)
|
||||
let pulse_sz: Int = manifold_size(mp)
|
||||
assert tone_sz == 5, "tone still routes to its own realizer"
|
||||
assert pulse_sz == 2, "pulse routes to a different realizer, with its own decomposition"
|
||||
let onset: Int = manifold_index_of(mp, "onset")
|
||||
assert onset > -1, "and to that realizer's own component vocabulary"
|
||||
let f1: Int = manifold_free(mt)
|
||||
let f2: Int = manifold_free(mp)
|
||||
}
|
||||
|
||||
test "no-organ-is-reported-as-no-organ" {
|
||||
// A modality with no realizer must transduce to NOTHING. It must never
|
||||
// fall back to embedding a description of the signal and calling that
|
||||
// perception — that silent substitution is the original defect.
|
||||
let has: Int = realizer_has("echolocation")
|
||||
assert has < 1, "unregistered modality has no organ"
|
||||
let m: Manifold = transduce("anything", "echolocation")
|
||||
let live: Int = manifold_is(m)
|
||||
assert live < 1, "no realizer means no manifold, not a fake one"
|
||||
}
|
||||
|
||||
test "registration-of-an-unresolvable-name-fails-loudly" {
|
||||
let bad: Int = realizer_register("ghost", "no_such_function_anywhere")
|
||||
assert bad < 1, "an unresolvable realizer name is a registration failure"
|
||||
let has: Int = realizer_has("ghost")
|
||||
assert has < 1, "and nothing gets registered"
|
||||
}
|
||||
|
||||
test "a-fingerprint-realizer-transduces-nothing" {
|
||||
// THE SUPERSESSION OF #144, asserted directly. fingerprint_realizer is
|
||||
// exactly what the merged primitive asked a realizer to be: signal in, one
|
||||
// Geometry out. It resolves, so registration succeeds — the organ is
|
||||
// present. But it does not decompose, so it does not transduce.
|
||||
//
|
||||
// This is a deliberate hard failure. "No organ" and "an organ that only
|
||||
// fingerprints" must not be indistinguishable, which is the same
|
||||
// distinction realizer_register already draws between an absent and a
|
||||
// broken organ. A modality with genuinely one part says so with
|
||||
// manifold_single, and is then visibly a size-1 manifold.
|
||||
let reg: Int = realizer_register("fingerprint", "fingerprint_realizer")
|
||||
assert reg > 0, "the symbol resolves, so registration succeeds"
|
||||
let m: Manifold = transduce("x", "fingerprint")
|
||||
let live: Int = manifold_is(m)
|
||||
assert live < 1, "a single vector is not a transduction"
|
||||
}
|
||||
|
||||
test "a-realizer-returning-nonsense-transduces-nothing" {
|
||||
let reg: Int = realizer_register("bogus", "bogus_realizer")
|
||||
assert reg > 0, "the symbol resolves, so registration succeeds"
|
||||
let m: Manifold = transduce("x", "bogus")
|
||||
let live: Int = manifold_is(m)
|
||||
assert live < 1, "a non-Manifold return transduced nothing"
|
||||
}
|
||||
|
||||
test "the-one-part-case-is-a-size-one-manifold-not-a-bare-vector" {
|
||||
// Some modalities really do have one part. That is a manifold of size 1 —
|
||||
// a special case of decomposition, not a parallel path back to a
|
||||
// fingerprint. Anything reading it still asks manifold_size and still gets
|
||||
// a real answer, and a second part can be added later without changing the
|
||||
// type of the thing.
|
||||
let g: Geometry = geometry_new(3)
|
||||
let s: Int = geometry_set(g, 0, 5.0)
|
||||
let m: Manifold = manifold_single("level", "scalar", g)
|
||||
let live: Int = manifold_is(m)
|
||||
assert live > 0, "manifold_single yields a real Manifold"
|
||||
let one_sz: Int = manifold_size(m)
|
||||
assert one_sz == 1, "of size one — visibly degenerate, not hidden"
|
||||
let idx: Int = manifold_index_of(m, "level")
|
||||
assert idx == 0, "and its one part is still addressable by name"
|
||||
let f: Int = geometry_free(g)
|
||||
let fm: Int = manifold_free(m)
|
||||
}
|
||||
|
||||
Executable
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-runtime-growth.sh — GROWTH guard for lang/runtime/el_runtime.c.
|
||||
#
|
||||
# Sibling to scripts/check-single-runtime.sh. That one guards against the file
|
||||
# being COPIED (a lagging fork shipped to prod and dropped learned hebb edges).
|
||||
# Nothing guarded against it GROWING — so it grew from 10,607 to 20,527 lines in
|
||||
# 3.5 months, while under an explicit commit-message promise that it was a
|
||||
# temporary shim about to be deleted.
|
||||
#
|
||||
# This enforces the RATCHET in lang/runtime/BUDGET: the numbers may only go down.
|
||||
#
|
||||
# It also checks two invariants that keep the multi-file runtime honest:
|
||||
# * every .c in lang/runtime/ is either in SOURCES or explicitly optional
|
||||
# * lang/install.sh's hardcoded download list matches SOURCES
|
||||
#
|
||||
# Exits non-zero on any violation. Run from anywhere; resolves the repo root.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
RUNTIME_DIR="lang/runtime"
|
||||
TARGET="$RUNTIME_DIR/el_runtime.c"
|
||||
BUDGET_FILE="$RUNTIME_DIR/BUDGET"
|
||||
SOURCES_FILE="$RUNTIME_DIR/SOURCES"
|
||||
|
||||
FAIL=0
|
||||
|
||||
for f in "$TARGET" "$BUDGET_FILE" "$SOURCES_FILE"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "FATAL: required file missing: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
budget() {
|
||||
local key="$1"
|
||||
sed -e 's/#.*//' "$BUDGET_FILE" | awk -v k="$key" '$1==k {print $2; found=1} END{if(!found) exit 1}'
|
||||
}
|
||||
|
||||
MAX_LINES="$(budget max_lines)" || { echo "FATAL: no 'max_lines' in $BUDGET_FILE" >&2; exit 1; }
|
||||
MAX_ENGRAM="$(budget max_engram_fns)" || { echo "FATAL: no 'max_engram_fns' in $BUDGET_FILE" >&2; exit 1; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The message every failure prints. The guard that existed before this one told
|
||||
# you what was wrong but not where the code should go — so it was easy to
|
||||
# "fix" by arguing with the guard. This one names the destination.
|
||||
# ---------------------------------------------------------------------------
|
||||
where_it_goes() {
|
||||
cat >&2 <<'MSG'
|
||||
|
||||
WHERE THE CODE ACTUALLY GOES
|
||||
----------------------------
|
||||
Placement is a LINK-TIME concern. The compiler cannot tell which .c a symbol
|
||||
came from: `builtin_arity` in el-compiler/src/codegen.el maps NAME -> ARITY
|
||||
INT only, the El name is emitted as the exact C symbol, and `ld` resolves it.
|
||||
The SHIPPED compiler already links from ten translation units — check it:
|
||||
|
||||
nm lang/dist/platform/elc | grep -E 'T _(engram_think|vindex_insert)'
|
||||
|
||||
So a builtin defined in a sibling .c is EXACTLY as linkable as one defined in
|
||||
el_runtime.c. Pick the file that owns the concern:
|
||||
|
||||
engram store ops ......... lang/runtime/engram_store.c
|
||||
ANN / vector index ....... lang/runtime/engram_vindex.c
|
||||
geometry, priming ........ lang/runtime/engram_geometry.c
|
||||
reasoning operators ...... lang/runtime/engram_reason.c
|
||||
grounding, consistency ... lang/runtime/engram_verify.c
|
||||
think, stance ............ lang/runtime/engram_cognition.c
|
||||
|
||||
No existing file owns it? Create one, add ONE line to lang/runtime/SOURCES,
|
||||
and every build path picks it up. Every runtime file EXCEPT el_runtime.c is
|
||||
deliberately uncapped.
|
||||
|
||||
Belongs to a downstream program, not the runtime? Declare
|
||||
`c_source "path/to/file.c"` in that program's manifest.el — elb already links
|
||||
it (parse_manifest_c_sources, lang/elb.el:82).
|
||||
|
||||
See lang/AGENTS.md "Where a new C builtin goes".
|
||||
MSG
|
||||
}
|
||||
|
||||
# --- 1. Line-count ratchet ---------------------------------------------------
|
||||
LINES="$(wc -l < "$TARGET" | tr -d ' ')"
|
||||
if [ "$LINES" -gt "$MAX_LINES" ]; then
|
||||
echo "FAIL: $TARGET grew past its budget." >&2
|
||||
echo " now: $LINES lines" >&2
|
||||
echo " budget: $MAX_LINES lines (lang/runtime/BUDGET: max_lines)" >&2
|
||||
echo " over by: $((LINES - MAX_LINES))" >&2
|
||||
echo "" >&2
|
||||
echo "This file is a 2026-05-03 build shim that was scheduled for deletion and" >&2
|
||||
echo "never retired. It does not get to grow. Do NOT raise the budget." >&2
|
||||
where_it_goes
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# --- 2. Engram-concern ratchet ----------------------------------------------
|
||||
# ~47.5% of el_runtime.c is engram code, and engram already owns six sibling
|
||||
# files. This count is the Stage 3 scoreboard: it may only go down.
|
||||
ENGRAM_FNS="$(grep -cE '^(static +)?[A-Za-z_][A-Za-z0-9_ *]*\b(engram|eg|cog)_[a-z0-9_]+\(' "$TARGET" || true)"
|
||||
if [ "$ENGRAM_FNS" -gt "$MAX_ENGRAM" ]; then
|
||||
echo "FAIL: new engram/eg_/cog_ function(s) added to $TARGET." >&2
|
||||
echo " now: $ENGRAM_FNS definitions" >&2
|
||||
echo " budget: $MAX_ENGRAM (lang/runtime/BUDGET: max_engram_fns)" >&2
|
||||
echo "" >&2
|
||||
echo "Engram code belongs in the six engram_*.c files that already exist." >&2
|
||||
where_it_goes
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# --- 3. Ratchet-down nudge (advisory, never fails) ---------------------------
|
||||
if [ "$LINES" -lt "$MAX_LINES" ]; then
|
||||
echo "NOTE: $TARGET is $((MAX_LINES - LINES)) lines under budget — lower" >&2
|
||||
echo " 'max_lines' to $LINES in $BUDGET_FILE in this same commit, so the" >&2
|
||||
echo " ground you gained cannot be quietly given back." >&2
|
||||
fi
|
||||
if [ "$ENGRAM_FNS" -lt "$MAX_ENGRAM" ]; then
|
||||
echo "NOTE: $((MAX_ENGRAM - ENGRAM_FNS)) engram fn(s) moved out — lower" >&2
|
||||
echo " 'max_engram_fns' to $ENGRAM_FNS in $BUDGET_FILE in this same commit." >&2
|
||||
fi
|
||||
|
||||
# --- 4. Every runtime .c is accounted for ------------------------------------
|
||||
# A new .c that is in neither SOURCES nor the optional list will not be
|
||||
# compiled by any build path — it would be silently dead. Catch that here.
|
||||
OPTIONAL_RE='^(el_android|el_gtk4|el_lvgl|el_sdl2|el_win32|el_runtime_win32|eg_cosine_batch_strategy_ggml|vindex_bench)\.c$'
|
||||
mapfile -t IN_SOURCES < <(scripts/el-runtime-sources.sh)
|
||||
for path in "$RUNTIME_DIR"/*.c; do
|
||||
base="$(basename "$path")"
|
||||
if printf '%s\n' "${IN_SOURCES[@]}" | grep -qxF "$base"; then continue; fi
|
||||
if [[ "$base" =~ $OPTIONAL_RE ]]; then continue; fi
|
||||
echo "FAIL: $path is in neither lang/runtime/SOURCES nor the platform-optional" >&2
|
||||
echo " list in this guard. It will not be compiled by any build path." >&2
|
||||
echo " Add it to SOURCES (one line), or add it to OPTIONAL_RE here if it" >&2
|
||||
echo " is a platform/strategy variant that is linked in deliberately." >&2
|
||||
FAIL=1
|
||||
done
|
||||
|
||||
# --- 5. install.sh must not drift from SOURCES -------------------------------
|
||||
# install.sh runs on machines with no repo checkout, so it cannot call
|
||||
# el-runtime-sources.sh and has to hardcode the list. That copy is exactly the
|
||||
# kind of duplicate that silently drifted before — so it is checked, not trusted.
|
||||
INSTALL_SH="lang/install.sh"
|
||||
if [ -f "$INSTALL_SH" ]; then
|
||||
EXPECTED="$(scripts/el-runtime-sources.sh | sort)"
|
||||
ACTUAL="$(sed -n '/^RUNTIME_SOURCES=(/,/^)/p' "$INSTALL_SH" \
|
||||
| grep -oE '[a-z_0-9]+\.c' | sort)"
|
||||
if [ "$EXPECTED" != "$ACTUAL" ]; then
|
||||
echo "FAIL: $INSTALL_SH RUNTIME_SOURCES has drifted from $SOURCES_FILE." >&2
|
||||
echo " Only in SOURCES: $(comm -23 <(echo "$EXPECTED") <(echo "$ACTUAL") | tr '\n' ' ')" >&2
|
||||
echo " Only in install.sh: $(comm -13 <(echo "$EXPECTED") <(echo "$ACTUAL") | tr '\n' ' ')" >&2
|
||||
echo " An SDK that ships the wrong set produces a lib/ that cannot link." >&2
|
||||
FAIL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$FAIL" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: el_runtime.c within budget ($LINES/$MAX_LINES lines, $ENGRAM_FNS/$MAX_ENGRAM engram fns);"
|
||||
echo " runtime sources accounted for; install.sh in step with SOURCES."
|
||||
@@ -81,14 +81,14 @@ fi
|
||||
echo "OK: single canonical runtime source — $CANONICAL (no un-allowlisted forks)."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CI wire-in:
|
||||
# foundation/el .gitea/workflows/ci-dev.yaml, ci-stage.yaml, sdk-release.yaml
|
||||
# Add an early step (before the build/publish steps). It must run from the
|
||||
# REPO ROOT, so override the job's `defaults.run.working-directory: lang`:
|
||||
# CI wire-in — DONE (2026-08-16). This block used to describe the wire-in as a
|
||||
# TODO, and it had never been done: the guard existed but ran nowhere, so it
|
||||
# caught nothing for as long as it has been in the tree. It is now an early step
|
||||
# in ci-dev.yaml, ci-stage.yaml and sdk-release.yaml (each with
|
||||
# `working-directory: ${{ github.workspace }}`, since the jobs default to lang/),
|
||||
# and it runs in .githooks/pre-commit.
|
||||
#
|
||||
# - name: Guard - single canonical runtime source
|
||||
# working-directory: ${{ github.workspace }}
|
||||
# run: bash scripts/check-single-runtime.sh
|
||||
#
|
||||
# Also add to .githooks/pre-commit so drift is caught before it is committed.
|
||||
# Its sibling scripts/check-runtime-growth.sh is wired in at the same points and
|
||||
# guards the other half of the problem: this script stops el_runtime.c being
|
||||
# COPIED, that one stops it GROWING.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# el-runtime-sources.sh — print the canonical El runtime link set.
|
||||
#
|
||||
# Reads lang/runtime/SOURCES (the single source of truth) and prints one path
|
||||
# per line, optionally prefixed with a directory. Use it anywhere a link line
|
||||
# would otherwise spell the runtime .c files out longhand:
|
||||
#
|
||||
# cc -std=c11 -O2 -I lang/runtime -o app app.c \
|
||||
# $(scripts/el-runtime-sources.sh lang/runtime) \
|
||||
# -lcurl -lssl -lcrypto -lpthread -lm
|
||||
#
|
||||
# Options:
|
||||
# --headers print the shipped headers instead of the .c sources
|
||||
# --check verify every listed file exists; exit non-zero if any is missing
|
||||
#
|
||||
# WHY: linking el_runtime.c alone has been broken since el_runtime.c started
|
||||
# calling into the engram siblings. The list was duplicated across ~8 build
|
||||
# paths and drifted. It lives in exactly one place now — see lang/runtime/SOURCES.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SOURCES="${ROOT}/lang/runtime/SOURCES"
|
||||
|
||||
if [ ! -f "$SOURCES" ]; then
|
||||
echo "FATAL: canonical runtime source list missing: $SOURCES" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODE="sources"
|
||||
PREFIX=""
|
||||
CHECK=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--headers) MODE="headers" ;;
|
||||
--check) CHECK=1 ;;
|
||||
-*) echo "el-runtime-sources.sh: unknown option: $arg" >&2; exit 2 ;;
|
||||
*) PREFIX="${arg%/}/" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Strip comments and blank lines. Order is preserved — it is link order.
|
||||
mapfile -t FILES < <(sed -e 's/#.*//' -e 's/[[:space:]]*$//' "$SOURCES" | grep -v '^$')
|
||||
|
||||
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||
echo "FATAL: $SOURCES lists no sources" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "headers" ]; then
|
||||
# Every .c's matching .h, plus the headers that carry no .c of their own.
|
||||
HDRS=()
|
||||
for f in "${FILES[@]}"; do
|
||||
h="${f%.c}.h"
|
||||
[ -f "${ROOT}/lang/runtime/${h}" ] && HDRS+=("$h")
|
||||
done
|
||||
# Interface-only headers: no matching .c, but required to compile against.
|
||||
for h in eg_cosine_batch_strategy.h el_native_target.h el_platform_win.h; do
|
||||
[ -f "${ROOT}/lang/runtime/${h}" ] && HDRS+=("$h")
|
||||
done
|
||||
FILES=("${HDRS[@]}")
|
||||
fi
|
||||
|
||||
RC=0
|
||||
for f in "${FILES[@]}"; do
|
||||
if [ "$CHECK" -eq 1 ] && [ ! -f "${ROOT}/lang/runtime/${f}" ]; then
|
||||
echo "MISSING: lang/runtime/${f} (listed in lang/runtime/SOURCES)" >&2
|
||||
RC=1
|
||||
fi
|
||||
printf '%s%s\n' "$PREFIX" "$f"
|
||||
done
|
||||
|
||||
exit $RC
|
||||
@@ -45,17 +45,43 @@ returned 60k–230k-char unbounded traversals (this very session hit 104 KB and
|
||||
|
||||
## Layer 2 — primitive agentic tools (Neuron runs itself)
|
||||
|
||||
The base verbs all agentic behavior composes from — grounded in the LIVE
|
||||
cog-arch (`think` is the one operation; faculties are its steering-space labels;
|
||||
the correspondence-beat is the reflexive learning loop).
|
||||
The base verbs all agentic behavior composes from.
|
||||
|
||||
> **⚠ The "PROVEN" verdicts in this table were measured against a build dated
|
||||
> 2026-08-14 and four of the five are now known to have been proving the wrong
|
||||
> thing (2026-08-16).** A verdict of PROVEN meant *the route returned a
|
||||
> well-formed response*, not *the response was derivable from what produced it*.
|
||||
> Corrections below, each with the measurement. Authority:
|
||||
> `lang/spec/correspondence-and-censorship.md`.
|
||||
|
||||
| op | signature | engram builtin | status on clone (gate-1 recipe) |
|
||||
|----|-----------|----------------|---------------------------------|
|
||||
| `think` | `think({seeds, faculty})` faculty ∈ reason·abduce·induce·plan·analogize·recognize·discern·synthesize | `engram_think_json` | **PROVEN** — all 8 faculties return real 768-dim gradients (n_support 30–282) |
|
||||
| `think` | `think({seeds, faculty})` faculty ∈ reason·abduce·induce·plan·analogize·recognize·discern·synthesize | `engram_think_json` | ~~PROVEN — all 8 faculties return real 768-dim gradients~~ **RETRACTED, then re-proven differently.** The gradients were real in *shape* only: the call passed `NULL` as the anchor, `engram_think` re-origins at `anchor ? anchor : region->centroid`, and **the centroid is the one point where the gradient is zero by construction**. Measured: every faculty returned `{"direction":[0,0,…],"spread":0,"magnitude":1,"confidence":0.5}` — identical, differing only in its label. Fixed in **#141/#142**; gradients now vary by seed |
|
||||
| `attend` | `attend({node, observer, salience})` | `engram_attend_json` | **PROVEN** (returns `salient-to`) |
|
||||
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PROVEN** |
|
||||
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | **PROVEN** (grounded-by edge, grounding=0.912, written) |
|
||||
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN** (real Stance: `stance-induce-…`, brier, reliability, written) |
|
||||
| `assert` | `assert({claim, for_whom, floor})` — realize, honesty-floored | `engram_assert_json` | **PARTIAL.** `may_assert` is real. `"still_held"` is a **hardcoded literal `true`** — `el_runtime.c:14538` emits it unconditionally, so it reports nothing it measured. Violates the invariant *a returned value must be derivable from what produced it* |
|
||||
| `ground` | `ground({claim, evidence, for_whom})` node-id anchors | `engram_ground_json` | ~~PROVEN (grounded-by edge, grounding=0.912, written)~~ **RETRACTED.** That 0.912 was structural, not evidential: the call wrote the edge between the two *region hubs* and echoed them back as though they were the caller's input, so when both seeds resolved into one region it **grounded a node against itself and returned a confident score**. Measured: grounding `3b9ced5d` against `6edf8c79` scored **0.98883** purely because `6edf8c79` is the hub of `3b9ced5d`'s region; two independent agents reported 0.885 / 0.909 self-groundings as confident. **#147** grounds the node asked about, reports `claim_region`/`evidence_region` separately, and refuses three circular shapes. **The operation itself is still the wrong shape** — see below |
|
||||
| `learn` | `learn({seeds, faculty, keystone})` — the correspondence-beat | `engram_correspondence_beat_json` | **PROVEN, and it was writing into a void.** The Stance, brier and reliability were real and really persisted — but `think` built a *neutral* stance every call and never loaded them, so every beat's calibration was written and thrown away on the next read. Fixed in **#146**: `think` resumes `stance-<faculty>-<hub>`, the same id the beat writes. Confidence **0.5 → 0.930726** on a calibrated region |
|
||||
|
||||
### What this table gets structurally wrong
|
||||
|
||||
- **`faculty` is not a parameter.** `reason` changes the *estimate* (a read),
|
||||
`induce` changes the *parameters* (this is exactly what `learn` does), and
|
||||
`abduce` changes the *structure* — a **write**, which `GeoGradient` cannot
|
||||
express. A write cannot be a parameter of a read. That the eight were listed as
|
||||
interchangeable values of one argument is why all eight returning the same thing
|
||||
looked like a pass. Underneath, `engram/src/server.el:1870–1886` routes six of
|
||||
them into one call with a string argument, and the name only reaches
|
||||
`engram_think` through the stance — `cog_stance_init` stores it and nothing
|
||||
reads it.
|
||||
- **`ground` should not mint an edge at all.** Grounding is not a subsystem and
|
||||
not a score: **it is the edge weight.** `grounded-by` as a relation type models
|
||||
grounding as a relation *between* nodes when it is a property *of* a relation.
|
||||
#147 corrected a scalar rather than deleting the operation; deletion is
|
||||
sequenced.
|
||||
- **`addWonderQuestion`** (Layer 1, `write`) treats wonder as an enumerable
|
||||
instance you push. **Wonder is the boundary** — where activation spreads and
|
||||
finds thin or absent geometry. There are about six, the same for everyone, and
|
||||
they never close. A manifest materializes a property as a stored artifact.
|
||||
|
||||
`comprehend`/`realize`/`intend` are **compositions**, not separate live
|
||||
primitives: comprehend = write+activate (world→geometry), realize = assert
|
||||
@@ -69,6 +95,26 @@ execution→integrate) composes over `think`+`ground`+`learn`+`write`/`relate`.
|
||||
`kn-efeb4a5b…` / `kn-5b606390…`, are refused — identity routes through
|
||||
intentional-cultivation, as enforced today.
|
||||
|
||||
> **⚠ SUPERSEDED (2026-08-16).** This describes what the surface enforces, which
|
||||
> is accurate — but the enforcement is the wrong kind of thing:
|
||||
>
|
||||
> > **In an immutable substrate, any mechanism that refuses a write is either
|
||||
> > redundant with immutability, or an epistemic constraint misfiled as a
|
||||
> > protective one.**
|
||||
>
|
||||
> "Keystone" means **load-bearing**, not precious. The real requirement is
|
||||
> **non-circularity of the reference frame** — a reference fitted to its own
|
||||
> readings reports perfect correspondence forever while drift becomes undetectable
|
||||
> from inside — and that is satisfied *temporally*, not by a gate: the frame
|
||||
> updates while activation is internally seeded, not while it is being used to act.
|
||||
> **Independence is *when*, not *what*.** Corruption requires mutation, and the
|
||||
> engram does not mutate: recoverability (the predecessor is always present),
|
||||
> governance (supersession *is* the audit trail), evidence quality, and rate all
|
||||
> fall out of the substrate. **Authorization** is the only residue and it is
|
||||
> bounded — an unauthorized writer can *propose*, never erase. Note also that the
|
||||
> live check is a substring match against two hard-coded ids
|
||||
> (`el_runtime.c:14337`).
|
||||
|
||||
## How the caller invokes Neuron agentically
|
||||
|
||||
Once the ops are registered as MCP tools (aliases in `surface.el`), the caller
|
||||
@@ -94,6 +140,16 @@ running itself.
|
||||
## Honest ledger (built vs staged)
|
||||
- **Route seam — IMPLEMENTED + PROVEN:** ported the `@route` codegen (from `feat/el-route-decorators`) into the worktree, rebuilt `elc` self-host, proved decorate→serve (`route_proof.el` on :8951); `surface.el` compiles with `el_route_dispatch` generated for all 8 ops.
|
||||
- **All ops PROVEN live on the clone** (gate-1 boot recipe, node-id anchors): read, write, relate, supersede (immutable), tombstone, think (8 faculties), ground, attend, learn — daemon alive through all mutations (node_count 13173→13176).
|
||||
> **⚠ Retracted in part (2026-08-16).** "The daemon stayed alive and every route
|
||||
> returned a well-formed response" is what was actually proven, and that is a
|
||||
> weaker claim than it reads as. See the Layer-2 table: `think` was reading at the
|
||||
> zero-gradient point, `ground` was scoring nodes against themselves, `assert`
|
||||
> emits a hardcoded field, and `learn` was persisting into a void. **A build that
|
||||
> passes because nothing checks whether a returned value is derivable from what
|
||||
> produced it has not been tested — it has been observed not to crash.** The
|
||||
> related discipline gap, also 2026-08-16: **no test without a negative control**
|
||||
> (#148's first attempt passed on the unpatched build too), and **no deploy
|
||||
> without verifying the artifact carries the fix** (nine instances in one session).
|
||||
- **Aperture-boundedness PROVEN:** vantage-read `limit=3 → 15 KB` vs `limit=50 → 363 KB` (fixes the whole-self dump).
|
||||
- **Bus:** `@manager` ops emit on the real `dharma_*` bus (explicit today, compiles) — same transport as the swarm (`wt/swarm-ccr`).
|
||||
- **STAGED (not guessed — needs the cognition-engram rebuild to verify link):** auto-injecting telemetry/interoception + bus emission at the decorated boundary (`cg_fn` diff in `SEAM_STAGED.md`); building the cognition engram with `surface.el` compiled in. No promote to live, no cutover (per rails).
|
||||
|
||||
Reference in New Issue
Block a user