merge ci/add-release-workflow into dev

This commit is contained in:
Will Anderson
2026-05-05 00:04:51 -05:00
4 changed files with 926 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
name: ElQL CI — dev
on:
push:
branches:
- dev
pull_request:
branches:
- dev
# ElQL is a set of standalone El programs run against a live Engram server.
# It has no compiled artifact to publish. CI verifies that all .el files
# parse and compile (emit valid C) using elc.
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
- name: Install El SDK from dev registry
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
mkdir -p /usr/local/bin /usr/local/lib/el
LATEST_VERSION=$(gcloud artifacts versions list \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--format="value(name)" 2>/dev/null | sort | tail -1 | sed 's|.*/||' || true)
if [ -n "${LATEST_VERSION}" ]; then
gcloud artifacts generic download \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--version="${LATEST_VERSION}" \
--destination=/usr/local/bin/elc
chmod +x /usr/local/bin/elc
else
echo "Falling back to Gitea latest release..."
curl -fsSL "https://git.neuralplatform.ai/neuron-technologies/el/releases/download/latest/elc" \
-o /usr/local/bin/elc
chmod +x /usr/local/bin/elc
fi
RELEASE_BASE="https://git.neuralplatform.ai/neuron-technologies/el/releases/download/latest"
curl -fsSL "${RELEASE_BASE}/el_runtime.c" -o /usr/local/lib/el/el_runtime.c
curl -fsSL "${RELEASE_BASE}/el_runtime.h" -o /usr/local/lib/el/el_runtime.h
echo "El SDK installed:"; elc --version || true
rm -f /tmp/gcp-key.json
# Compile-check all standalone .el programs (studio, field tests, language tests, llm tests)
- name: Compile-check all El programs
run: |
PASS=0; FAIL=0
for f in studio/studio.el test/field_test.el test/language_features_test.el test/llm_test.el studio.el; do
[ -f "$f" ] || continue
echo -n "Compiling $f ... "
if elc "$f" > /dev/null 2>&1; then
echo "OK"
PASS=$((PASS+1))
else
echo "FAIL"
elc "$f" 2>&1 | head -10
FAIL=$((FAIL+1))
fi
done
echo "Passed: ${PASS}, Failed: ${FAIL}"
[ "${FAIL}" -eq 0 ]
+97
View File
@@ -0,0 +1,97 @@
name: ElQL CI — stage
on:
push:
branches:
- stage
pull_request:
branches:
- stage
# ElQL is a set of standalone El programs run against a live Engram server.
# CI verifies all .el files compile cleanly using the stage-level El SDK.
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Enforce source branch (stage ← dev only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
if [ "${SOURCE}" != "dev" ]; then
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} → stage"
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
- name: Install El SDK from stage registry
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
mkdir -p /usr/local/bin /usr/local/lib/el
LATEST_VERSION=$(gcloud artifacts versions list \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--format="value(name)" 2>/dev/null | sort | tail -1 | sed 's|.*/||' || true)
if [ -n "${LATEST_VERSION}" ]; then
gcloud artifacts generic download \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--version="${LATEST_VERSION}" \
--destination=/usr/local/bin/elc
chmod +x /usr/local/bin/elc
else
echo "Falling back to Gitea latest release..."
curl -fsSL "https://git.neuralplatform.ai/neuron-technologies/el/releases/download/latest/elc" \
-o /usr/local/bin/elc
chmod +x /usr/local/bin/elc
fi
RELEASE_BASE="https://git.neuralplatform.ai/neuron-technologies/el/releases/download/latest"
curl -fsSL "${RELEASE_BASE}/el_runtime.c" -o /usr/local/lib/el/el_runtime.c
curl -fsSL "${RELEASE_BASE}/el_runtime.h" -o /usr/local/lib/el/el_runtime.h
echo "El SDK installed:"; elc --version || true
rm -f /tmp/gcp-key.json
- name: Compile-check all El programs
run: |
PASS=0; FAIL=0
for f in studio/studio.el test/field_test.el test/language_features_test.el test/llm_test.el studio.el; do
[ -f "$f" ] || continue
echo -n "Compiling $f ... "
if elc "$f" > /dev/null 2>&1; then
echo "OK"
PASS=$((PASS+1))
else
echo "FAIL"
elc "$f" 2>&1 | head -10
FAIL=$((FAIL+1))
fi
done
echo "Passed: ${PASS}, Failed: ${FAIL}"
[ "${FAIL}" -eq 0 ]
+115
View File
@@ -0,0 +1,115 @@
name: ElQL Release
on:
push:
branches:
- main
repository_dispatch:
types:
- el-sdk-updated
- engram-updated
# ElQL is a set of standalone El programs run against a live Engram server.
# Release validates that all .el files parse and compile (emit valid C) using
# the prod-level El SDK. No compiled binary is produced or published.
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Enforce source branch (main ← stage only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
if [ "${SOURCE}" != "stage" ]; then
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} → main"
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
# Install El SDK — try GCP Artifact Registry prod first, fall back to Gitea release
- name: Install El SDK
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
RELEASE_BASE: https://git.neuralplatform.ai/neuron-technologies/el/releases/download/latest
run: |
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
mkdir -p /usr/local/bin /usr/local/lib/el
LATEST_VERSION=$(gcloud artifacts versions list \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--format="value(name)" 2>/dev/null | sort | tail -1 | sed 's|.*/||' || true)
if [ -n "${LATEST_VERSION}" ]; then
gcloud artifacts generic download \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el/elc \
--version="${LATEST_VERSION}" \
--destination=/usr/local/bin/elc
chmod +x /usr/local/bin/elc
else
echo "Falling back to Gitea latest release..."
curl -fsSL "${RELEASE_BASE}/elc" -o /usr/local/bin/elc
chmod +x /usr/local/bin/elc
fi
curl -fsSL "${RELEASE_BASE}/el_runtime.c" -o /usr/local/lib/el/el_runtime.c
curl -fsSL "${RELEASE_BASE}/el_runtime.h" -o /usr/local/lib/el/el_runtime.h
echo "El SDK installed:"; elc --version || true
rm -f /tmp/gcp-key.json
# Compile-check all standalone .el programs
- name: Compile-check all El programs
run: |
PASS=0; FAIL=0
for f in studio/studio.el test/field_test.el test/language_features_test.el test/llm_test.el studio.el; do
[ -f "$f" ] || continue
echo -n "Compiling $f ... "
if elc "$f" > /dev/null 2>&1; then
echo "OK"
PASS=$((PASS+1))
else
echo "FAIL"
elc "$f" 2>&1
FAIL=$((FAIL+1))
fi
done
echo "Passed: ${PASS}, Failed: ${FAIL}"
[ "${FAIL}" -eq 0 ]
# Dispatch elql-updated to downstream dependents (none yet — placeholder)
- name: Dispatch elql-updated to dependents
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
run: |
# Add downstream repos here as the dependency graph grows
echo "elql-updated dispatch ready (no downstream targets configured yet)"
# Example:
# curl -sf -X POST \
# -H "Authorization: token ${GITEA_TOKEN}" \
# -H "Content-Type: application/json" \
# "${GITEA_API}/repos/neuron-technologies/some-service/dispatches" \
# -d '{"type":"elql-updated","inputs":{"elql_version":"latest","commit":"'"${GITHUB_SHA}"'"}}'
+625
View File
@@ -0,0 +1,625 @@
# engram-el Specification
Version 1.0.0 — April 30, 2026
---
## Overview
engram-el is the El-native integration layer for the Engram graph engine. It is a collection of El programs that operate on a live Engram server, demonstrating the correct patterns for El programs that use the Engram knowledge graph as their substrate.
The repository contains three components:
| Component | Path | Description |
|-----------|------|-------------|
| Studio | `studio/studio.el` | Full-featured terminal graph explorer |
| Field model | `test/field_test.el` | Hebbian field model proof of concept |
| Language tests | `test/language_features_test.el` | El builtin coverage tests |
| LLM tests | `test/llm_test.el` | LLM builtin smoke tests |
engram-el is not a library. It contains no importable modules and no compilation artifact. It is a set of standalone `.el` programs run directly with `el run-file`.
---
## 1. Architecture
### 1.1 Integration Model
All Engram access uses El's native HTTP builtins over the Engram HTTP API. There is no SDK, no special driver, and no additional compilation step. The integration is:
```
El program → http_get / http_post → Engram HTTP API → JSON response → json_parse
```
This is intentional. The HTTP API is the canonical external interface to Engram. Programs that need tight runtime integration (the Neuron daemon itself) use the lower-level `graph_compile` and `graph_traverse` VM builtins. External tools use HTTP.
### 1.2 Configuration
All programs read server configuration from environment variables at startup:
| Variable | Default | Description |
|----------|---------|-------------|
| `ENGRAM_URL` | `http://localhost:8340` | Engram server base URL |
| `ENGRAM_REPORT` | `/tmp/engram-studio-report.txt` | Studio report output path |
Example:
```bash
# Default local server
el run-file studio/studio.el
# Remote server
ENGRAM_URL=http://engram.example.com el run-file studio/studio.el
# Custom report path
ENGRAM_REPORT=/var/log/studio.txt el run-file studio/studio.el
```
### 1.3 Error Handling Convention
All API wrappers follow this convention: if the response begins with `{"error"`, return empty string. All callers check for empty string and emit a placeholder rather than crashing. Programs run to completion even when the server is unreachable.
```el
fn api_get(path: String) -> String {
let url: String = get_base_url() + path
let resp: String = http_get(url)
if str_starts_with(resp, "{\"error\"") {
return ""
}
return resp
}
```
---
## 2. Studio Application
`studio/studio.el` is a 788-line terminal graph explorer. It connects to Engram, fetches data across all graph dimensions, renders a formatted report to the terminal, and writes a text report to disk.
### 2.1 Sections Rendered
The studio renders these sections sequentially:
| Section | Engram Endpoint | Description |
|---------|----------------|-------------|
| Database Statistics | `GET /api/stats` | Node count, edge count, avg salience, DB size |
| Recent Nodes | `GET /api/nodes?limit=N` | Most recently created nodes |
| Top by Salience | `GET /api/nodes?limit=N&min_salience=0.0` | Highest-salience nodes |
| Memory Nodes | `GET /api/nodes?node_type=Memory&limit=N` | Nodes of type Memory |
| Concept Nodes | `GET /api/nodes?node_type=Concept&limit=N` | Nodes of type Concept |
| Event Nodes | `GET /api/nodes?node_type=Event&limit=N` | Nodes of type Event |
| Entity Nodes | `GET /api/nodes?node_type=Entity&limit=N` | Nodes of type Entity |
| Process Nodes | `GET /api/nodes?node_type=Process&limit=N` | Nodes of type Process |
| Semantic Tier | `GET /api/nodes?tier=Semantic&limit=N` | Nodes in Semantic memory tier |
| Episodic Tier | `GET /api/nodes?tier=Episodic&limit=N` | Nodes in Episodic memory tier |
| Knowledge Browser | `GET /api/nodes?node_type=Concept&limit=50` | Concept nodes as domain anchors |
| Text Search | `GET /api/search?q=<query>&limit=N` | Full-text search results |
| Edge Explorer | `GET /api/edges?limit=N` | Sample of edges with weights |
| Interactive Menu | (local) | Menu of available commands |
### 2.2 Engram Node Fields Consumed
The studio reads these fields from each node JSON object:
| Field | Type | Usage |
|-------|------|-------|
| `id` | String (UUID) | Display (first 8 chars) and API lookups |
| `label` | String | Primary display text |
| `node_type` | String | Type badge and section filtering |
| `tier` | String | Tier badge and section filtering |
| `salience` | Float | Salience bar visualization and sorting |
| `importance` | Float | Node detail display |
| `confidence` | Float | Node detail display |
| `created_at` | Int (ms) | Timestamp display |
| `updated_at` | Int (ms) | Timestamp display |
### 2.3 Node Display
Each node in list views renders as a single row:
```
1. 9685fc7a label text here Memory 0.750
```
Format: `index. short_id label_padded_to_36 type_badge_padded_to_10 salience`
Salience is also rendered as a colored block bar (10 segments) in the Top by Salience section:
- `>= 0.7` — green (`████████░░`)
- `>= 0.4` — yellow
- `< 0.4` — dim
### 2.4 Spreading Activation
The studio includes a spreading activation section (requires a specific seed node ID):
```el
fn show_activation(seed_id: String, limit: Int, report: String) -> String {
let path: String = "/api/activate?seeds=" + seed_id
+ "&limit=" + int_to_str(limit) + "&depth=3"
let json_str: String = api_get(path)
let results_raw: String = json_get_raw(json_str, "results")
let results: List = json_parse(results_raw)
// renders each result: node label, activation_strength, hops
}
```
Each activation result has fields: `node` (nested object), `activation_strength` (Float), `hops` (Int).
### 2.5 Neighbor Traversal
Node detail view fetches BFS neighbors:
```el
let nb_path: String = "/api/neighbors/" + node_id + "?depth=2"
let nb_json: String = api_get(nb_path)
let neighbors: List = json_parse(nb_json)
```
Each neighbor object has fields: `node` (nested object), `edge` (nested object), `hops` (Int). Nested objects are extracted with `json_get_raw`.
### 2.6 Report Export
The studio accumulates a plain-text report string as each section renders. On completion it writes the report to `ENGRAM_REPORT` using `fs_write`:
```el
fn export_report(content: String, path: String) -> String {
let header: String = "ENGRAM DATA STUDIO — Report\n"
+ "Generated: " + time_format(time_now_utc(), "ISO") + "\n"
+ "Server: " + get_base_url() + "\n"
+ repeat_str("=", 60) + "\n"
let ok: Bool = fs_write(path, header + content)
// ...
}
```
`fs_write` returns `Bool` — true on success.
---
## 3. Field Model
`test/field_test.el` is a pure in-memory demonstration of a Hebbian field model. It does not connect to Engram. It uses El arithmetic to simulate the mechanics of the Engram activation engine.
### 3.1 Purpose
The field test documents the mathematical model underlying Engram's spreading activation and confidence-qualified retrieval. It is both a test that the math is correct and a specification of what the numbers mean.
### 3.2 Core Functions
**Proximity (latent semantic gradient):**
```el
fn proximity(ax: Float, ay: Float, bx: Float, by: Float) -> Float {
let d: Float = dist_sq(ax, ay, bx, by)
return 1.0 / (1.0 + d)
}
```
Returns a value in `(0, 1]`. Nodes closer in 2D semantic space have higher proximity.
**Temporal decay:**
```el
fn temporal_decay(age: Float, decay_rate: Float) -> Float {
let d: Float = 1.0 - decay_rate * age
return clamp_f(d, 0.0, 1.0)
}
```
Linear decay from 1.0 at age=0 toward 0.0. Clamped to `[0, 1]`.
**Hebbian weight update:**
```el
fn hebbian_update(weight: Float, act_i: Float, act_j: Float, lr: Float) -> Float {
let delta: Float = lr * act_i * act_j
return clamp_f(weight + delta, 0.0, 1.0)
}
```
When two nodes co-activate, their edge weight increases by `learning_rate × activation_i × activation_j`. Clamped to `[0, 1]`.
**Edge decay (between activations):**
```el
fn edge_decay(weight: Float, decay_rate: Float) -> Float {
return clamp_f(weight * (1.0 - decay_rate), 0.0, 1.0)
}
```
**Path strength:**
```el
fn path_strength(edge_weight: Float, node_age: Float, decay_rate: Float) -> Float {
let td: Float = temporal_decay(node_age, decay_rate)
return edge_weight * td
}
```
Combined confidence qualifier on a retrieval path. Strong edge × recently activated = high path strength.
**Epistemic confidence:**
```el
fn epistemic_confidence(node_confidence: Float, ps: Float) -> Float {
return node_confidence * ps
}
```
Final confidence on a retrieved node: the node's stored confidence multiplied by the path strength through which it was reached.
### 3.3 Simulation Scenario
The test models a clinical scenario with four nodes:
| Node | Label | Semantic position | Temporal coordinate |
|------|-------|-------------------|---------------------|
| A | patient has fever | `(0.2, 0.4)` | `t=100` |
| B | influenza | `(0.3, 0.5)` | `t=90` |
| C | drug interaction warning | `(0.6, 0.3)` | `t=10` (old) |
| D | ibuprofen | `(0.65, 0.3)` | `t=10` (old) |
The semantic axes are: `x` = concrete↔abstract, `y` = negative↔positive valence.
Three co-activation events between A and B (fever/flu diagnosis) strengthen the A→B edge via Hebbian learning. After three events with learning rate 0.3 and full activation strength:
```
w_ab: 0.0 → 0.3 → 0.51 → 0.657
```
C and D are never co-activated with A, so their edges remain at latent proximity only.
**Retrieval:** Activating A (fever) propagates to B with high confidence. C (drug interaction) is reached only via latent proximity gradient — weak signal, old node — producing low epistemic confidence below threshold 0.2. This triggers a "refresh" signal. After the doctor looks up drug interactions, A, C, and D co-activate; edges strengthen; subsequent retrieval finds C and D with high confidence.
The scenario demonstrates: perfect storage, calibrated confidence, and self-correcting retrieval.
---
## 4. Language Feature Tests
`test/language_features_test.el` tests El builtins exercised by engram-el programs. These are functional smoke tests — each prints its result for visual verification.
### 4.1 Arithmetic Operators Tested
| Operator | Example |
|----------|---------|
| `%` (modulo) | `17 % 5``2` |
| `&` (bitwise AND) | `10 & 12``8` |
| `^` (bitwise XOR) | `10 ^ 12``6` |
| `<<` (left shift) | `1 << 3``8` |
| `>>` (right shift) | `16 >> 2``4` |
### 4.2 Math Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `math_sin(f)` | `Float → Float` | Sine |
| `math_cos(f)` | `Float → Float` | Cosine |
| `math_pi()` | `() → Float` | Pi constant |
### 4.3 String Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `str_pad_left(s, width, pad)` | `String, Int, String → String` | Pad to width on left |
| `str_pad_right(s, width, pad)` | `String, Int, String → String` | Pad to width on right |
| `str_format(template, data)` | `String, Map → String` | `{key}` template interpolation |
| `str_len(s)` | `String → Int` | Character count |
| `str_slice(s, start, end)` | `String, Int, Int → String` | Substring by index |
| `str_starts_with(s, prefix)` | `String, String → Bool` | Prefix test |
| `str_char_at(s, i)` | `String, Int → String` | Single character at index |
| `str_char_code(s, i)` | `String, Int → Int` | Unicode code point at index |
| `str_from_char_code(n)` | `Int → String` | Character from code point |
`str_format` uses `{key}` syntax. The data argument is a map literal:
```el
let tmpl = "Hello {name}, you are {age} years old!"
let data = {"name": "Will", "age": "30"}
let formatted = str_format(tmpl, data)
// "Hello Will, you are 30 years old!"
```
### 4.4 Float Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `format_float(f, decimals)` | `Float, Int → String` | Format to N decimal places |
| `float_to_str(f)` | `Float → String` | Default float string |
| `float_to_int(f)` | `Float → Int` | Truncate (not round) |
| `int_to_float(n)` | `Int → Float` | Widen to float |
| `decimal_round(f, decimals)` | `Float, Int → Float` | Round to N decimal places |
### 4.5 Time Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `time_now_utc()` | `() → Int` | Current time as Unix milliseconds |
| `time_format(ts, fmt)` | `Int, String → String` | Format timestamp; `"ISO"` for ISO 8601 |
| `time_to_parts(ts)` | `Int → Map` | Decompose timestamp |
| `time_from_parts(secs, ns, tz)` | `Int, Int, String → Int` | Construct timestamp |
| `time_add(ts, n, unit)` | `Int, Int, String → Int` | Add duration; units: `"day"`, etc. |
| `time_diff(ts1, ts2, unit)` | `Int, Int, String → Int` | Difference in units |
### 4.6 List Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `list_new()` | `() → List` | Empty list |
| `list_len(lst)` | `List → Int` | Element count |
| `list_get(lst, i)` | `List, Int → Any` | Element at index |
| `list_push(lst, v)` | `List, Any → List` | Append element (returns new list) |
| `list_peek_last(lst)` | `List → Any` | Last element without removing |
| `list_range(start, end)` | `Int, Int → List` | Integer range `[start, end)` |
| `list_join(lst, sep)` | `List, String → String` | Join as string with separator |
### 4.7 Stack Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `stack_new()` | `() → Stack` | Empty stack |
| `stack_push(s, v)` | `Stack, Any → Stack` | Push value (returns new stack) |
| `stack_pop(s)` | `Stack → Stack` | Pop top (returns new stack) |
| `stack_peek(s)` | `Stack → Any` | Top element without removing |
### 4.8 Queue Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `queue_new()` | `() → Queue` | Empty queue |
| `queue_enqueue(q, v)` | `Queue, Any → Queue` | Enqueue (returns new queue) |
| `queue_dequeue(q)` | `Queue → Queue` | Dequeue front (returns new queue) |
| `queue_peek(q)` | `Queue → Any` | Front element without removing |
### 4.9 JSON Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `json_parse(s)` | `String → List` or `Map` | Parse JSON array or object |
| `json_stringify(v)` | `Any → String` | Serialize value to JSON |
| `json_get_string(json, key)` | `String, String → String` | Extract string field |
| `json_get_int(json, key)` | `String, String → Int` | Extract integer field |
| `json_get_float(json, key)` | `String, String → Float` | Extract float field |
| `json_get_raw(json, key)` | `String, String → String` | Extract nested object as raw JSON string |
`json_get_raw` is required when a JSON field is itself an object or array that needs to be parsed as a list. Example:
```el
let results_raw: String = json_get_raw(json_str, "results")
let results: List = json_parse(results_raw)
```
### 4.10 Nil Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `is_nil(v)` | `Any → Bool` | True if value is nil |
| `unwrap_or(v, default)` | `Any, Any → Any` | Return `v` if not nil, else `default` |
### 4.11 Type Conversion
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `int_to_str(n)` | `Int → String` | Integer to decimal string |
| `bool_to_str(b)` | `Bool → String` | `"true"` or `"false"` |
| `int_to_float(n)` | `Int → Float` | Widen |
| `float_to_int(f)` | `Float → Int` | Truncate toward zero |
### 4.12 Terminal Color Builtins
Used by the studio for terminal output formatting:
| Builtin | Description |
|---------|-------------|
| `color_bold(s)` | Bold text |
| `color_dim(s)` | Dim/gray text |
| `color_green(s)` | Green text |
| `color_yellow(s)` | Yellow text |
| `color_cyan(s)` | Cyan text |
| `color_red(s)` | Red text |
### 4.13 HTTP Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `http_get(url)` | `String → String` | HTTP GET, returns response body |
| `http_post(url, body)` | `String, String → String` | HTTP POST with body, returns response body |
### 4.14 File System Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `fs_write(path, content)` | `String, String → Bool` | Write string to file; true on success |
### 4.15 Environment and LLM Builtins
| Builtin | Signature | Description |
|---------|-----------|-------------|
| `env(key)` | `String → String` | Read environment variable; empty string if unset |
| `llm_models()` | `() → List` | List available LLM models |
| `llm_call(model, prompt)` | `String, String → String` | Call an LLM; returns response string or error |
`llm_call` requires `ANTHROPIC_API_KEY` in the environment. Returns an error string (not a crash) when the key is missing.
---
## 5. Engram HTTP API Reference
The API surface consumed by engram-el programs:
### 5.1 Endpoints
| Method | Path | Parameters | Returns |
|--------|------|------------|---------|
| `GET` | `/api/stats` | — | `{node_count, edge_count, avg_salience, db_size_bytes}` |
| `GET` | `/api/nodes` | `node_type`, `tier`, `limit`, `min_salience` | JSON array of node objects |
| `GET` | `/api/nodes/{id}` | — | Single node object |
| `GET` | `/api/edges` | `limit` | JSON array of edge objects |
| `GET` | `/api/search` | `q`, `limit` | JSON array of node objects |
| `GET` | `/api/neighbors/{id}` | `depth` | JSON array of `{node, edge, hops}` objects |
| `GET` | `/api/activate` | `seeds`, `limit`, `depth` | `{results: [{node, activation_strength, hops}]}` |
| `POST` | `/api/nodes` | JSON body | Created node object with `id` |
### 5.2 Node Object Schema
```json
{
"id": "uuid-string",
"label": "human-readable label",
"node_type": "Memory|Concept|Event|Entity|Process|InternalState",
"tier": "Working|Episodic|Semantic|Procedural",
"salience": 0.75,
"importance": 0.5,
"confidence": 0.9,
"created_at": 1714500000000,
"updated_at": 1714500000000
}
```
### 5.3 Edge Object Schema
```json
{
"id": "uuid-string",
"from_id": "uuid-string",
"to_id": "uuid-string",
"relation": "relation-type-name",
"weight": 0.8,
"confidence": 0.9
}
```
### 5.4 Activation Result Schema
The `/api/activate` response wraps results in a top-level `results` key:
```json
{
"results": [
{
"node": { /* node object */ },
"activation_strength": 0.62,
"hops": 2
}
]
}
```
Access with `json_get_raw(response, "results")` then `json_parse` the extracted string.
---
## 6. Integration Patterns
### 6.1 Read Nodes
```el
fn get_nodes_of_type(node_type: String, limit: Int) -> List {
let path: String = "/api/nodes?node_type=" + node_type
+ "&limit=" + int_to_str(limit)
let json_str: String = http_get(env("ENGRAM_URL") + path)
if json_str == "" {
return list_new()
}
return json_parse(json_str)
}
```
### 6.2 Write a Node
```el
fn create_node(label: String, content: String, node_type: String, tier: String) -> String {
let body: String = "{\"label\":\"" + label
+ "\",\"content\":\"" + content
+ "\",\"node_type\":\"" + node_type
+ "\",\"tier\":\"" + tier
+ "\",\"importance\":0.5}"
let resp: String = http_post(env("ENGRAM_URL") + "/api/nodes", body)
return json_get_string(resp, "id")
}
```
### 6.3 Text Search
```el
fn search_graph(query: String, limit: Int) -> List {
let path: String = "/api/search?q=" + query + "&limit=" + int_to_str(limit)
let json_str: String = http_get(env("ENGRAM_URL") + path)
if json_str == "" {
return list_new()
}
return json_parse(json_str)
}
```
### 6.4 Spreading Activation
```el
fn activate_from_node(node_id: String, depth: Int, limit: Int) -> List {
let path: String = "/api/activate?seeds=" + node_id
+ "&depth=" + int_to_str(depth)
+ "&limit=" + int_to_str(limit)
let resp_str: String = http_get(env("ENGRAM_URL") + path)
if resp_str == "" {
return list_new()
}
let results_raw: String = json_get_raw(resp_str, "results")
return json_parse(results_raw)
}
```
### 6.5 Extract Nested Objects
When a JSON field is itself an object (e.g., `node` inside a neighbor result), use `json_get_raw` to extract it as a string before reading its fields:
```el
let neighbor_json: String = json_stringify(list_get(neighbors, i))
let node_obj: String = json_get_raw(neighbor_json, "node")
let edge_obj: String = json_get_raw(neighbor_json, "edge")
let label: String = json_get_string(node_obj, "label")
let relation: String = json_get_string(edge_obj, "relation")
```
---
## 7. Design Decisions
### 7.1 Stateless Programs
All engram-el programs are stateless. They read from Engram on each run and write nothing back (the studio is read-only). Exploration tools observe the graph; they do not mutate it.
### 7.2 No Imports
El programs in this repository use zero imports. All capabilities come from El's built-in dispatch layer. This is correct El style for programs that do not need cross-module sharing — El is not a library ecosystem.
### 7.3 Immutable Update Style
Stack, queue, and list operations return new values rather than mutating in place. This is how El builtins are designed:
```el
let s0 = stack_new()
let s1 = stack_push(s0, 10) // s0 is unchanged
let s2 = stack_push(s1, 20)
let top = stack_peek(s2) // 20
let s3 = stack_pop(s2) // s3 has only 10
```
The idiomatic pattern is to shadow the binding with the new value when the old one is no longer needed:
```el
let stack = stack_new()
let stack = stack_push(stack, 10)
let stack = stack_push(stack, 20)
```
### 7.4 String Accumulation Pattern
The studio builds its text report by threading a `String` accumulator through each section function. Each section function receives `report: String` and returns `report + new_content`. This is the correct pattern in El, which has no mutable references:
```el
let report: String = ""
let report: String = show_stats(report)
let report: String = show_recent(20, report)
// ...
export_report(report, report_path)
```