monorepo: fold elql into ql/ (history preserved)
This commit is contained in:
@@ -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 ]
|
||||
@@ -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 ]
|
||||
@@ -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
@@ -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)
|
||||
```
|
||||
@@ -0,0 +1,787 @@
|
||||
// ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
// ║ ENGRAM DATA STUDIO — Native El Application ║
|
||||
// ║ Full-featured terminal explorer for the Engram knowledge graph ║
|
||||
// ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
//
|
||||
// Usage: el run-file studio.el
|
||||
// ENGRAM_URL=http://host:port el run-file studio.el
|
||||
//
|
||||
// Requires: Engram server running (default: http://localhost:8340)
|
||||
|
||||
// ── Configuration ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_base_url() -> String {
|
||||
let e: String = env("ENGRAM_URL")
|
||||
if e == "" {
|
||||
return "http://localhost:8340"
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
fn get_report_path() -> String {
|
||||
let e: String = env("ENGRAM_REPORT")
|
||||
if e == "" {
|
||||
return "/tmp/engram-studio-report.txt"
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// ── Box-drawing helpers ────────────────────────────────────────────────────────
|
||||
|
||||
fn repeat_str(s: String, n: Int) -> String {
|
||||
let result: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let result: String = result + s
|
||||
let i: Int = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn hline(width: Int) -> String {
|
||||
return repeat_str("─", width)
|
||||
}
|
||||
|
||||
fn box_top(width: Int) -> String {
|
||||
return "┌" + hline(width) + "┐"
|
||||
}
|
||||
|
||||
fn box_bot(width: Int) -> String {
|
||||
return "└" + hline(width) + "┘"
|
||||
}
|
||||
|
||||
fn box_row(content: String, width: Int) -> String {
|
||||
let padded: String = str_pad_right(content, width, " ")
|
||||
return "│" + padded + "│"
|
||||
}
|
||||
|
||||
fn double_top(width: Int) -> String {
|
||||
return "╔" + repeat_str("═", width) + "╗"
|
||||
}
|
||||
|
||||
fn double_bot(width: Int) -> String {
|
||||
return "╚" + repeat_str("═", width) + "╝"
|
||||
}
|
||||
|
||||
fn double_row(content: String, width: Int) -> String {
|
||||
let padded: String = str_pad_right(content, width, " ")
|
||||
return "║" + padded + "║"
|
||||
}
|
||||
|
||||
fn section_header(title: String) -> String {
|
||||
println("")
|
||||
println(color_bold(box_top(62)))
|
||||
println(color_bold(box_row(" " + title, 62)))
|
||||
println(color_bold(box_bot(62)))
|
||||
return ""
|
||||
}
|
||||
|
||||
fn divider() -> String {
|
||||
println(color_dim(" " + repeat_str("·", 58)))
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── API access ────────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn api_post(path: String, body: String) -> String {
|
||||
let url: String = get_base_url() + path
|
||||
let resp: String = http_post(url, body)
|
||||
if str_starts_with(resp, "{\"error\"") {
|
||||
return ""
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// ── Safe JSON field extractors ─────────────────────────────────────────────────
|
||||
// These handle both raw JSON strings and parsed Value::Struct objects.
|
||||
|
||||
fn safe_str(node: String, key: String) -> String {
|
||||
return json_get_string(node, key)
|
||||
}
|
||||
|
||||
fn safe_int(node: String, key: String) -> Int {
|
||||
return json_get_int(node, key)
|
||||
}
|
||||
|
||||
fn safe_float(node: String, key: String) -> Float {
|
||||
return json_get_float(node, key)
|
||||
}
|
||||
|
||||
// ── Formatting helpers ────────────────────────────────────────────────────────
|
||||
|
||||
fn short_id(full_id: String) -> String {
|
||||
if str_len(full_id) >= 8 {
|
||||
return str_slice(full_id, 0, 8)
|
||||
}
|
||||
return full_id
|
||||
}
|
||||
|
||||
fn format_bytes(n: Int) -> String {
|
||||
if n < 1024 {
|
||||
return int_to_str(n) + " B"
|
||||
}
|
||||
if n < 1048576 {
|
||||
let kb: Int = n / 1024
|
||||
return int_to_str(kb) + " KB"
|
||||
}
|
||||
let mb: Int = n / 1048576
|
||||
return int_to_str(mb) + " MB"
|
||||
}
|
||||
|
||||
fn format_timestamp(ms: Int) -> String {
|
||||
if ms <= 0 {
|
||||
return "—"
|
||||
}
|
||||
let secs: Int = ms / 1000
|
||||
let ts: Int = time_from_parts(secs, 0, "UTC")
|
||||
return time_format(ts, "ISO")
|
||||
}
|
||||
|
||||
fn content_preview(content: String, max_len: Int) -> String {
|
||||
let n: Int = str_len(content)
|
||||
if n == 0 {
|
||||
return color_dim("(no content)")
|
||||
}
|
||||
if n <= max_len {
|
||||
return content
|
||||
}
|
||||
return str_slice(content, 0, max_len) + color_dim("…")
|
||||
}
|
||||
|
||||
fn salience_bar(salience: Float) -> String {
|
||||
let pct: Float = salience * 10.0
|
||||
let filled: Int = float_to_int(pct)
|
||||
let bar: String = repeat_str("█", filled) + repeat_str("░", 10 - filled)
|
||||
if salience >= 0.7 {
|
||||
return color_green(bar)
|
||||
}
|
||||
if salience >= 0.4 {
|
||||
return color_yellow(bar)
|
||||
}
|
||||
return color_dim(bar)
|
||||
}
|
||||
|
||||
fn tier_badge(tier: String) -> String {
|
||||
if tier == "Semantic" {
|
||||
return color_cyan("[Semantic ]")
|
||||
}
|
||||
if tier == "Episodic" {
|
||||
return color_yellow("[Episodic ]")
|
||||
}
|
||||
if tier == "Working" {
|
||||
return color_green("[Working ]")
|
||||
}
|
||||
if tier == "Procedural" {
|
||||
return color_bold("[Procedural]")
|
||||
}
|
||||
return "[" + str_pad_right(tier, 10, " ") + "]"
|
||||
}
|
||||
|
||||
fn type_badge(node_type: String) -> String {
|
||||
if node_type == "Memory" {
|
||||
return color_cyan("Memory ")
|
||||
}
|
||||
if node_type == "Concept" {
|
||||
return color_green("Concept ")
|
||||
}
|
||||
if node_type == "Event" {
|
||||
return color_yellow("Event ")
|
||||
}
|
||||
if node_type == "Entity" {
|
||||
return color_bold("Entity ")
|
||||
}
|
||||
if node_type == "Process" {
|
||||
return color_cyan("Process ")
|
||||
}
|
||||
if node_type == "InternalState" {
|
||||
return color_dim("IntState ")
|
||||
}
|
||||
return str_pad_right(node_type, 10, " ")
|
||||
}
|
||||
|
||||
// ── Node renderer ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn render_node_row(node_json: String, idx: Int) -> String {
|
||||
let id: String = short_id(safe_str(node_json, "id"))
|
||||
let label: String = safe_str(node_json, "label")
|
||||
let node_type: String = safe_str(node_json, "node_type")
|
||||
let tier: String = safe_str(node_json, "tier")
|
||||
let salience: Float = safe_float(node_json, "salience")
|
||||
|
||||
let label_col: String = str_pad_right(label, 36, " ")
|
||||
let sal_str: String = format_float(salience, 3)
|
||||
|
||||
let num: String = str_pad_left(int_to_str(idx + 1), 3, " ") + ". "
|
||||
return " " + num + color_dim(id) + " " + label_col + " " + type_badge(node_type) + " " + sal_str
|
||||
}
|
||||
|
||||
fn render_node_detail(node_json: String) -> String {
|
||||
let id: String = safe_str(node_json, "id")
|
||||
let label: String = safe_str(node_json, "label")
|
||||
let node_type: String = safe_str(node_json, "node_type")
|
||||
let tier: String = safe_str(node_json, "tier")
|
||||
let salience: Float = safe_float(node_json, "salience")
|
||||
let importance: Float = safe_float(node_json, "importance")
|
||||
let confidence: Float = safe_float(node_json, "confidence")
|
||||
let created: Int = safe_int(node_json, "created_at")
|
||||
let updated: Int = safe_int(node_json, "updated_at")
|
||||
|
||||
println(" " + color_bold("ID: ") + id)
|
||||
println(" " + color_bold("Label: ") + color_cyan(label))
|
||||
println(" " + color_bold("Type: ") + node_type)
|
||||
println(" " + color_bold("Tier: ") + tier_badge(tier))
|
||||
println(" " + color_bold("Salience: ") + salience_bar(salience) + " " + format_float(salience, 4))
|
||||
println(" " + color_bold("Importance: ") + format_float(importance, 4))
|
||||
println(" " + color_bold("Confidence: ") + format_float(confidence, 4))
|
||||
println(" " + color_bold("Created: ") + color_dim(format_timestamp(created)))
|
||||
println(" " + color_bold("Updated: ") + color_dim(format_timestamp(updated)))
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Section: Stats Dashboard ──────────────────────────────────────────────────
|
||||
|
||||
fn show_stats(report: String) -> String {
|
||||
section_header("Database Statistics")
|
||||
let stats_json: String = api_get("/api/stats")
|
||||
if stats_json == "" {
|
||||
println(" " + color_red("Error: could not reach Engram server"))
|
||||
println(" Make sure the server is running: engram-server --data-dir <path>")
|
||||
return report
|
||||
}
|
||||
|
||||
let node_count: Int = safe_int(stats_json, "node_count")
|
||||
let edge_count: Int = safe_int(stats_json, "edge_count")
|
||||
let avg_sal: Float = safe_float(stats_json, "avg_salience")
|
||||
let db_bytes: Int = safe_int(stats_json, "db_size_bytes")
|
||||
|
||||
println(" " + color_bold("Nodes: ") + color_cyan(int_to_str(node_count)))
|
||||
println(" " + color_bold("Edges: ") + color_cyan(int_to_str(edge_count)))
|
||||
println(" " + color_bold("Avg Salience: ") + format_float(avg_sal, 4))
|
||||
println(" " + color_bold("DB Size: ") + color_dim(format_bytes(db_bytes)))
|
||||
|
||||
let new_report: String = report
|
||||
+ "\n=== Database Statistics ===\n"
|
||||
+ "Nodes: " + int_to_str(node_count) + "\n"
|
||||
+ "Edges: " + int_to_str(edge_count) + "\n"
|
||||
+ "Avg Salience: " + format_float(avg_sal, 4) + "\n"
|
||||
+ "DB Size: " + format_bytes(db_bytes) + "\n"
|
||||
return new_report
|
||||
}
|
||||
|
||||
// ── Section: Node Browser ─────────────────────────────────────────────────────
|
||||
|
||||
fn show_nodes(node_type: String, limit: Int, report: String) -> String {
|
||||
let title: String = "Nodes — type: " + node_type
|
||||
section_header(title)
|
||||
|
||||
let path: String = "/api/nodes?node_type=" + node_type + "&limit=" + int_to_str(limit)
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No nodes found or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
let nodes: List = json_parse(json_str)
|
||||
let n: Int = list_len(nodes)
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(no " + node_type + " nodes)"))
|
||||
return report
|
||||
}
|
||||
|
||||
println(" " + color_dim("Showing " + int_to_str(n) + " nodes:"))
|
||||
println("")
|
||||
|
||||
let header: String = " " + str_pad_right(" # ID Label", 55, " ") + " Type Salience"
|
||||
println(color_dim(header))
|
||||
println(color_dim(" " + repeat_str("─", 70)))
|
||||
|
||||
let i: Int = 0
|
||||
let report_section: String = "\n=== " + node_type + " Nodes ===\n"
|
||||
while i < n {
|
||||
let node: String = json_stringify(list_get(nodes, i))
|
||||
println(render_node_row(node, i))
|
||||
let label: String = safe_str(node, "label")
|
||||
let id: String = short_id(safe_str(node, "id"))
|
||||
let sal: Float = safe_float(node, "salience")
|
||||
let report_section: String = report_section + int_to_str(i + 1) + ". [" + id + "] " + label + " (sal=" + format_float(sal, 3) + ")\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Recent Nodes ─────────────────────────────────────────────────────
|
||||
|
||||
fn show_recent(limit: Int, report: String) -> String {
|
||||
section_header("Recent Nodes (last " + int_to_str(limit) + ")")
|
||||
let path: String = "/api/nodes?limit=" + int_to_str(limit)
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No nodes or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
let nodes: List = json_parse(json_str)
|
||||
let n: Int = list_len(nodes)
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(database is empty)"))
|
||||
return report
|
||||
}
|
||||
|
||||
println(" " + color_dim("Showing " + int_to_str(n) + " most recent nodes:"))
|
||||
println("")
|
||||
|
||||
let header: String = " " + str_pad_right(" # ID Label", 55, " ") + " Type Salience"
|
||||
println(color_dim(header))
|
||||
println(color_dim(" " + repeat_str("─", 70)))
|
||||
|
||||
let i: Int = 0
|
||||
let report_section: String = "\n=== Recent Nodes ===\n"
|
||||
while i < n {
|
||||
let node: String = json_stringify(list_get(nodes, i))
|
||||
println(render_node_row(node, i))
|
||||
let label: String = safe_str(node, "label")
|
||||
let id: String = short_id(safe_str(node, "id"))
|
||||
let report_section: String = report_section + int_to_str(i + 1) + ". [" + id + "] " + label + "\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Top Salient Nodes ────────────────────────────────────────────────
|
||||
|
||||
fn show_top_salient(limit: Int, report: String) -> String {
|
||||
section_header("Top " + int_to_str(limit) + " by Salience")
|
||||
let path: String = "/api/nodes?limit=" + int_to_str(limit) + "&min_salience=0.0"
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No nodes or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
let nodes: List = json_parse(json_str)
|
||||
let n: Int = list_len(nodes)
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(no nodes)"))
|
||||
return report
|
||||
}
|
||||
|
||||
println(" " + color_dim("Salience ranking:"))
|
||||
println("")
|
||||
|
||||
let report_section: String = "\n=== Top Salient Nodes ===\n"
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let node: String = json_stringify(list_get(nodes, i))
|
||||
let id: String = short_id(safe_str(node, "id"))
|
||||
let label: String = safe_str(node, "label")
|
||||
let sal: Float = safe_float(node, "salience")
|
||||
let tier: String = safe_str(node, "tier")
|
||||
let bar: String = salience_bar(sal)
|
||||
let rank: String = str_pad_left(int_to_str(i + 1), 2, " ")
|
||||
println(" " + rank + ". " + bar + " " + format_float(sal, 3) + " " + color_dim(id) + " " + str_pad_right(label, 35, " ") + " " + color_dim(tier))
|
||||
let report_section: String = report_section + rank + ". " + format_float(sal, 3) + " [" + id + "] " + label + " " + tier + "\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Node Detail ──────────────────────────────────────────────────────
|
||||
|
||||
fn show_node_detail(node_id: String, report: String) -> String {
|
||||
section_header("Node Detail — " + str_slice(node_id, 0, 8) + "...")
|
||||
let path: String = "/api/nodes/" + node_id
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_red("Node not found: " + node_id))
|
||||
return report
|
||||
}
|
||||
|
||||
render_node_detail(json_str)
|
||||
|
||||
// Show neighbors
|
||||
let nb_path: String = "/api/neighbors/" + node_id + "?depth=2"
|
||||
let nb_json: String = api_get(nb_path)
|
||||
if nb_json != "" {
|
||||
let neighbors: List = json_parse(nb_json)
|
||||
let nb_n: Int = list_len(neighbors)
|
||||
if nb_n > 0 {
|
||||
println("")
|
||||
println(" " + color_bold("Neighbors (" + int_to_str(nb_n) + "):"))
|
||||
let j: Int = 0
|
||||
while j < nb_n {
|
||||
let nb: String = json_stringify(list_get(neighbors, j))
|
||||
// Use json_get_raw to extract nested objects as JSON strings
|
||||
let nb_node: String = json_get_raw(nb, "node")
|
||||
let nb_edge: String = json_get_raw(nb, "edge")
|
||||
let hops: Int = safe_int(nb, "hops")
|
||||
|
||||
let nb_label: String = safe_str(nb_node, "label")
|
||||
let nb_id: String = short_id(safe_str(nb_node, "id"))
|
||||
let hop_str: String = str_pad_left(int_to_str(hops), 2, " ")
|
||||
let relation: String = safe_str(nb_edge, "relation")
|
||||
|
||||
println(" hop " + hop_str + " " + color_dim(nb_id) + " " + color_cyan(relation) + " → " + nb_label)
|
||||
let j: Int = j + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return report + "\n=== Node Detail: " + node_id + " ===\n" + "Fetched node details\n"
|
||||
}
|
||||
|
||||
// ── Section: Search ───────────────────────────────────────────────────────────
|
||||
|
||||
fn show_search(query: String, limit: Int, report: String) -> String {
|
||||
section_header("Search: \"" + query + "\"")
|
||||
let path: String = "/api/search?q=" + query + "&limit=" + int_to_str(limit)
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No results or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
let nodes: List = json_parse(json_str)
|
||||
let n: Int = list_len(nodes)
|
||||
println(" " + color_bold("Found " + int_to_str(n) + " results:"))
|
||||
println("")
|
||||
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(no matches)"))
|
||||
return report + "\n=== Search: " + query + " ===\nNo results\n"
|
||||
}
|
||||
|
||||
let report_section: String = "\n=== Search: " + query + " ===\n" + int_to_str(n) + " results:\n"
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let node: String = json_stringify(list_get(nodes, i))
|
||||
println(render_node_row(node, i))
|
||||
let label: String = safe_str(node, "label")
|
||||
let report_section: String = report_section + int_to_str(i + 1) + ". " + label + "\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Tier Browser ─────────────────────────────────────────────────────
|
||||
|
||||
fn show_nodes_by_tier(tier: String, limit: Int, report: String) -> String {
|
||||
let title: String = "Nodes — tier: " + tier
|
||||
section_header(title)
|
||||
|
||||
let path: String = "/api/nodes?tier=" + tier + "&limit=" + int_to_str(limit)
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No nodes or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
let nodes: List = json_parse(json_str)
|
||||
let n: Int = list_len(nodes)
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(no " + tier + " tier nodes)"))
|
||||
return report
|
||||
}
|
||||
|
||||
println(" " + tier_badge(tier) + " " + color_dim("Showing " + int_to_str(n) + " nodes"))
|
||||
println("")
|
||||
|
||||
let i: Int = 0
|
||||
let report_section: String = "\n=== " + tier + " Tier Nodes ===\n"
|
||||
while i < n {
|
||||
let node: String = json_stringify(list_get(nodes, i))
|
||||
println(render_node_row(node, i))
|
||||
let label: String = safe_str(node, "label")
|
||||
let id: String = short_id(safe_str(node, "id"))
|
||||
let report_section: String = report_section + "[" + id + "] " + label + "\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Spreading Activation ────────────────────────────────────────────
|
||||
|
||||
fn show_activation(seed_id: String, limit: Int, report: String) -> String {
|
||||
section_header("Spreading Activation — seed: " + str_slice(seed_id, 0, 8) + "...")
|
||||
let path: String = "/api/activate?seeds=" + seed_id + "&limit=" + int_to_str(limit) + "&depth=3"
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No activation results or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
// Use json_get_raw to get the "results" array as a JSON string, then parse it
|
||||
let results_raw: String = json_get_raw(json_str, "results")
|
||||
let results: List = json_parse(results_raw)
|
||||
let n: Int = list_len(results)
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(no spreading activation results — check seed ID)"))
|
||||
return report
|
||||
}
|
||||
|
||||
println(" " + color_bold("Activated " + int_to_str(n) + " nodes:"))
|
||||
println("")
|
||||
|
||||
let header: String = " " + str_pad_right(" # ID Label", 52, " ") + " Strength Hops"
|
||||
println(color_dim(header))
|
||||
println(color_dim(" " + repeat_str("─", 68)))
|
||||
|
||||
let report_section: String = "\n=== Activation from " + str_slice(seed_id, 0, 8) + " ===\n"
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let result: String = json_stringify(list_get(results, i))
|
||||
// Use json_get_raw to extract nested node object as a JSON string
|
||||
let node: String = json_get_raw(result, "node")
|
||||
let strength: Float = safe_float(result, "activation_strength")
|
||||
let hops: Int = safe_int(result, "hops")
|
||||
|
||||
let id: String = short_id(safe_str(node, "id"))
|
||||
let label: String = safe_str(node, "label")
|
||||
|
||||
let rank: String = str_pad_left(int_to_str(i + 1), 3, " ")
|
||||
let strength_bar: String = salience_bar(strength)
|
||||
println(" " + rank + ". " + color_dim(id) + " " + str_pad_right(label, 36, " ") + " " + format_float(strength, 3) + " " + color_dim("hops:" + int_to_str(hops)))
|
||||
let report_section: String = report_section + rank + ". [" + id + "] " + label + " strength=" + format_float(strength, 3) + " hops=" + int_to_str(hops) + "\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Edge Statistics ──────────────────────────────────────────────────
|
||||
|
||||
fn show_edges(limit: Int, report: String) -> String {
|
||||
section_header("Edge Explorer (sample of " + int_to_str(limit) + ")")
|
||||
let path: String = "/api/edges?limit=" + int_to_str(limit)
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No edges or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
let edges: List = json_parse(json_str)
|
||||
let n: Int = list_len(edges)
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(no edges in database)"))
|
||||
return report
|
||||
}
|
||||
|
||||
println(" " + color_dim("Showing " + int_to_str(n) + " edges:"))
|
||||
println("")
|
||||
|
||||
let header: String = " " + str_pad_right(" # From → To Relation", 56, " ") + " Weight"
|
||||
println(color_dim(header))
|
||||
println(color_dim(" " + repeat_str("─", 68)))
|
||||
|
||||
let report_section: String = "\n=== Edges ===\n"
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let edge: String = json_stringify(list_get(edges, i))
|
||||
let from_id: String = short_id(safe_str(edge, "from_id"))
|
||||
let to_id: String = short_id(safe_str(edge, "to_id"))
|
||||
let relation: String = safe_str(edge, "relation")
|
||||
let weight: Float = safe_float(edge, "weight")
|
||||
let rank: String = str_pad_left(int_to_str(i + 1), 3, " ")
|
||||
|
||||
println(" " + rank + ". " + color_dim(from_id) + " " + color_cyan("→") + " " + color_dim(to_id) + " " + color_green(str_pad_right(relation, 20, " ")) + " " + format_float(weight, 3))
|
||||
let report_section: String = report_section + from_id + " -[" + relation + "]-> " + to_id + " w=" + format_float(weight, 3) + "\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Knowledge Browser (Concept nodes grouped by tag prefix) ──────────
|
||||
|
||||
fn show_knowledge_browser(report: String) -> String {
|
||||
section_header("Knowledge Browser — Concept Nodes")
|
||||
let path: String = "/api/nodes?node_type=Concept&limit=50"
|
||||
let json_str: String = api_get(path)
|
||||
if json_str == "" {
|
||||
println(" " + color_dim("No concept nodes or server unreachable"))
|
||||
return report
|
||||
}
|
||||
|
||||
let nodes: List = json_parse(json_str)
|
||||
let n: Int = list_len(nodes)
|
||||
if n == 0 {
|
||||
println(" " + color_dim("(no Concept nodes — these are your domain knowledge anchors)"))
|
||||
return report
|
||||
}
|
||||
|
||||
println(" " + color_bold(int_to_str(n) + " concept nodes found:"))
|
||||
println("")
|
||||
|
||||
let report_section: String = "\n=== Knowledge Browser ===\n"
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let node: String = json_stringify(list_get(nodes, i))
|
||||
let id: String = short_id(safe_str(node, "id"))
|
||||
let label: String = safe_str(node, "label")
|
||||
let tier: String = safe_str(node, "tier")
|
||||
let sal: Float = safe_float(node, "salience")
|
||||
println(" " + color_green("◆") + " " + str_pad_right(label, 40, " ") + " " + color_dim(id) + " " + color_dim(tier) + " " + format_float(sal, 3))
|
||||
let report_section: String = report_section + "◆ " + label + " [" + id + "] " + tier + "\n"
|
||||
let i: Int = i + 1
|
||||
}
|
||||
|
||||
return report + report_section
|
||||
}
|
||||
|
||||
// ── Section: Mode Simulation (interactive mode preview) ──────────────────────
|
||||
|
||||
fn show_mode_menu() -> String {
|
||||
let _ = section_header("Interactive Mode Preview")
|
||||
println(" " + color_bold("Available commands (when running interactively):"))
|
||||
println("")
|
||||
println(" " + color_cyan("stats") + " — show database statistics")
|
||||
println(" " + color_cyan("browse <type>") + " — browse nodes by type")
|
||||
println(" types: Memory, Concept, Event, Entity, Process, InternalState")
|
||||
println(" " + color_cyan("tier <tier>") + " — browse nodes by tier")
|
||||
println(" tiers: Working, Episodic, Semantic, Procedural")
|
||||
println(" " + color_cyan("search <query>") + " — full-text search")
|
||||
println(" " + color_cyan("node <id>") + " — show node detail + neighbors")
|
||||
println(" " + color_cyan("activate <id>") + " — spreading activation from seed")
|
||||
println(" " + color_cyan("edges") + " — browse edges")
|
||||
println(" " + color_cyan("top") + " — top nodes by salience")
|
||||
println(" " + color_cyan("export <path>") + " — save text report to file")
|
||||
println(" " + color_cyan("help") + " — show this menu")
|
||||
println(" " + color_cyan("quit") + " — exit studio")
|
||||
println("")
|
||||
println(" " + color_dim("Tip: set ENGRAM_URL=http://host:port to connect to a different server"))
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Export report ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn export_report(content: String, path: String) -> String {
|
||||
let _ = section_header("Report Export")
|
||||
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)
|
||||
if ok {
|
||||
println(" " + color_green("✓") + " Report saved to: " + color_bold(path))
|
||||
println(" " + color_dim("Size: " + format_bytes(str_len(header + content))))
|
||||
} else {
|
||||
println(" " + color_red("✗") + " Failed to write report to: " + path)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── Connectivity check ────────────────────────────────────────────────────────
|
||||
|
||||
fn check_connection() -> Bool {
|
||||
let resp: String = api_get("/api/stats")
|
||||
return resp != ""
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ── MAIN STUDIO ENTRY POINT ──────────────────────────────────────────────────
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
let base_url: String = get_base_url()
|
||||
let WIDTH: Int = 64
|
||||
|
||||
// ── Banner ────────────────────────────────────────────────────────────────────
|
||||
println("")
|
||||
println(color_bold(double_top(WIDTH)))
|
||||
println(color_bold(double_row(" ███████╗███╗ ██╗ ██████╗ ██████╗ █████╗ ███╗ ███╗", WIDTH)))
|
||||
println(color_bold(double_row(" ██╔════╝████╗ ██║██╔════╝ ██╔══██╗██╔══██╗████╗ ████║", WIDTH)))
|
||||
println(color_bold(double_row(" █████╗ ██╔██╗ ██║██║ ███╗██████╔╝███████║██╔████╔██║", WIDTH)))
|
||||
println(color_bold(double_row(" ██╔══╝ ██║╚██╗██║██║ ██║██╔══██╗██╔══██║██║╚██╔╝██║", WIDTH)))
|
||||
println(color_bold(double_row(" ███████╗██║ ╚████║╚██████╔╝██║ ██║██║ ██║██║ ╚═╝ ██║", WIDTH)))
|
||||
println(color_bold(double_row(" ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝", WIDTH)))
|
||||
println(color_bold(double_row("", WIDTH)))
|
||||
println(color_bold(double_row(" DATA STUDIO — Native El Application", WIDTH)))
|
||||
println(color_bold(double_row("", WIDTH)))
|
||||
println(color_bold(double_bot(WIDTH)))
|
||||
println("")
|
||||
println(" " + color_bold("Server: ") + color_cyan(base_url))
|
||||
println(" " + color_bold("Version: ") + color_dim("El-native · 10-loop pass"))
|
||||
println("")
|
||||
|
||||
// ── Connection check ─────────────────────────────────────────────────────────
|
||||
let connected: Bool = check_connection()
|
||||
if connected {
|
||||
println(" " + color_green("●") + " " + color_bold("Connection established"))
|
||||
} else {
|
||||
println(" " + color_red("●") + " " + color_bold("Server unreachable"))
|
||||
println(" " + color_dim("Start the server: ENGRAM_BIND=0.0.0.0:8340 engram-server --data-dir <path>"))
|
||||
println(" " + color_dim("Or set ENGRAM_URL environment variable"))
|
||||
println("")
|
||||
println(" " + color_yellow("Continuing in offline mode — all sections will show placeholders"))
|
||||
}
|
||||
|
||||
// ── Run all dashboard sections ─────────────────────────────────────────────────
|
||||
|
||||
let report: String = ""
|
||||
|
||||
// Section 1: Stats
|
||||
let report: String = show_stats(report)
|
||||
|
||||
// Section 2: Recent nodes
|
||||
let report: String = show_recent(20, report)
|
||||
|
||||
// Section 3: Top salient
|
||||
let report: String = show_top_salient(10, report)
|
||||
|
||||
// Section 4: By node type — Memory
|
||||
let report: String = show_nodes("Memory", 10, report)
|
||||
|
||||
// Section 5: By node type — Concept
|
||||
let report: String = show_nodes("Concept", 10, report)
|
||||
|
||||
// Section 6: By node type — Event
|
||||
let report: String = show_nodes("Event", 8, report)
|
||||
|
||||
// Section 7: By node type — Entity
|
||||
let report: String = show_nodes("Entity", 8, report)
|
||||
|
||||
// Section 8: By node type — Process
|
||||
let report: String = show_nodes("Process", 8, report)
|
||||
|
||||
// Section 9: Tier — Semantic
|
||||
let report: String = show_nodes_by_tier("Semantic", 10, report)
|
||||
|
||||
// Section 10: Tier — Episodic
|
||||
let report: String = show_nodes_by_tier("Episodic", 10, report)
|
||||
|
||||
// Section 11: Knowledge browser (Concept nodes)
|
||||
let report: String = show_knowledge_browser(report)
|
||||
|
||||
// Section 12: Text search samples
|
||||
let report: String = show_search("memory", 10, report)
|
||||
let report: String = show_search("concept", 8, report)
|
||||
|
||||
// Section 13: Edge explorer
|
||||
let report: String = show_edges(15, report)
|
||||
|
||||
// Section 14: Interactive mode preview
|
||||
show_mode_menu()
|
||||
|
||||
// ── Export report ──────────────────────────────────────────────────────────────
|
||||
let report_path: String = get_report_path()
|
||||
export_report(report, report_path)
|
||||
|
||||
// ── Footer ────────────────────────────────────────────────────────────────────
|
||||
println("")
|
||||
println(color_bold(" " + repeat_str("═", 60)))
|
||||
println(" " + color_bold("Engram Data Studio") + color_dim(" — session complete"))
|
||||
println(" " + color_dim(time_format(time_now_utc(), "ISO")))
|
||||
println(color_bold(" " + repeat_str("═", 60)))
|
||||
println("")
|
||||
@@ -0,0 +1,243 @@
|
||||
// field_test.el — Proof of concept: Hebbian field model
|
||||
//
|
||||
// Tests the core mechanics:
|
||||
// 1. Nodes with 2D semantic positions and temporal coordinates
|
||||
// 2. Edges with weights that grow via co-activation (Hebb's rule)
|
||||
// 3. Spreading activation with path strength
|
||||
// 4. Temporal decay in path strength
|
||||
// 5. Epistemic confidence = node confidence * path strength
|
||||
//
|
||||
// No storage — pure in-memory demonstration using print output.
|
||||
|
||||
// ── Math helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn clamp_f(v: Float, lo: Float, hi: Float) -> Float {
|
||||
if v < lo {
|
||||
return lo
|
||||
}
|
||||
if v > hi {
|
||||
return hi
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// L2 distance squared — no sqrt needed for proximity comparison
|
||||
fn dist_sq(ax: Float, ay: Float, bx: Float, by: Float) -> Float {
|
||||
let dx: Float = ax - bx
|
||||
let dy: Float = ay - by
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
// Proximity score: 1 / (1 + dist_sq) — range (0, 1], closer = higher
|
||||
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)
|
||||
}
|
||||
|
||||
// Linear temporal decay: 1.0 at t=0, decaying toward 0
|
||||
// decay_rate: fraction lost per time unit
|
||||
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)
|
||||
}
|
||||
|
||||
// ── Hebbian learning ──────────────────────────────────────────────────────────
|
||||
|
||||
// New edge weight after co-activation event
|
||||
// w_new = clamp(w + lr * a_i * a_j, 0, 1)
|
||||
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)
|
||||
}
|
||||
|
||||
// Edge decay between activations
|
||||
// w_new = w * (1 - decay_rate)
|
||||
fn edge_decay(weight: Float, decay_rate: Float) -> Float {
|
||||
return clamp_f(weight * (1.0 - decay_rate), 0.0, 1.0)
|
||||
}
|
||||
|
||||
// ── Path strength ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Path strength = edge_weight * temporal_decay(node_age)
|
||||
// This is the confidence qualifier on a retrieval result
|
||||
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
|
||||
}
|
||||
|
||||
// Epistemic confidence = node_confidence * path_strength
|
||||
fn epistemic_confidence(node_confidence: Float, ps: Float) -> Float {
|
||||
return node_confidence * ps
|
||||
}
|
||||
|
||||
// ── Simulation ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn run_test() -> String {
|
||||
println("=== Engram Field Model — Proof of Concept ===")
|
||||
println("")
|
||||
|
||||
// ── Nodes ──
|
||||
// Each node: id, label, semantic position (x, y), temporal coordinate, confidence
|
||||
// Semantic space: 2D for demonstration
|
||||
// x=0..1: concrete←→abstract
|
||||
// y=0..1: negative←→positive valence
|
||||
|
||||
println("── Nodes ──────────────────────────────────────")
|
||||
println("A: 'patient has fever' pos=(0.2, 0.4) t=100")
|
||||
println("B: 'influenza' pos=(0.3, 0.5) t=90")
|
||||
println("C: 'drug interaction warning' pos=(0.6, 0.3) t=10 (old, unactivated)")
|
||||
println("D: 'ibuprofen' pos=(0.65, 0.3) t=10 (old, unactivated)")
|
||||
println("")
|
||||
|
||||
// Semantic positions
|
||||
let ax: Float = 0.2
|
||||
let ay: Float = 0.4
|
||||
let bx: Float = 0.3
|
||||
let by: Float = 0.5
|
||||
let cx: Float = 0.6
|
||||
let cy: Float = 0.3
|
||||
let dx_pos: Float = 0.65
|
||||
let dy_pos: Float = 0.3
|
||||
|
||||
// Initial proximity (latent gradient — implicit from positions)
|
||||
let prox_ab: Float = proximity(ax, ay, bx, by)
|
||||
let prox_ac: Float = proximity(ax, ay, cx, cy)
|
||||
let prox_cd: Float = proximity(cx, cy, dx_pos, dy_pos)
|
||||
|
||||
println("── Initial latent gradients (proximity) ───────")
|
||||
println("A↔B proximity: " + float_to_str(prox_ab) + " (semantically close — fever/flu)")
|
||||
println("A↔C proximity: " + float_to_str(prox_ac) + " (semantically distant)")
|
||||
println("C↔D proximity: " + float_to_str(prox_cd) + " (close — drug interaction/ibuprofen)")
|
||||
println("")
|
||||
|
||||
// ── Initial edge weights (all start near zero — no co-activation yet) ──
|
||||
let w_ab: Float = 0.0
|
||||
let w_ac: Float = 0.0
|
||||
let w_cd: Float = 0.0
|
||||
|
||||
println("── Initial edge weights ────────────────────────")
|
||||
println("A→B: " + float_to_str(w_ab) + " (no co-activation yet)")
|
||||
println("A→C: " + float_to_str(w_ac))
|
||||
println("C→D: " + float_to_str(w_cd))
|
||||
println("")
|
||||
|
||||
// ── Co-activation events ──
|
||||
// The doctor CGI processes a patient with fever.
|
||||
// A (fever) and B (influenza) co-activate — the diagnosis fires both.
|
||||
// Learning rate: 0.3
|
||||
|
||||
let lr: Float = 0.3
|
||||
let act_strength: Float = 1.0 // full activation
|
||||
|
||||
println("── Co-activation event 1: patient with fever → flu diagnosis ──")
|
||||
let w_ab_1: Float = hebbian_update(w_ab, act_strength, act_strength, lr)
|
||||
println("A and B fire together (strength=1.0)")
|
||||
println("A→B weight: " + float_to_str(w_ab) + " → " + float_to_str(w_ab_1))
|
||||
println("")
|
||||
|
||||
println("── Co-activation event 2: another flu case ──")
|
||||
let w_ab_2: Float = hebbian_update(w_ab_1, act_strength, act_strength, lr)
|
||||
println("A and B fire together again")
|
||||
println("A→B weight: " + float_to_str(w_ab_1) + " → " + float_to_str(w_ab_2))
|
||||
println("")
|
||||
|
||||
println("── Co-activation event 3: third flu case ──")
|
||||
let w_ab_3: Float = hebbian_update(w_ab_2, act_strength, act_strength, lr)
|
||||
println("A and B fire together again")
|
||||
println("A→B weight: " + float_to_str(w_ab_2) + " → " + float_to_str(w_ab_3))
|
||||
println("")
|
||||
|
||||
// C and D have never been activated in this context — weights stay near zero
|
||||
// (In practice, proximity means a latent gradient exists, but no learned edge yet)
|
||||
|
||||
// ── Query: activate A (fever), what spreads? ──
|
||||
println("── Query: activate A (fever) ───────────────────")
|
||||
println("Spreading activation from A...")
|
||||
println("")
|
||||
|
||||
// Decay rates
|
||||
let temporal_decay_rate: Float = 0.005 // per time unit
|
||||
let current_t: Int = 200
|
||||
|
||||
// Node ages (current_t - node_t)
|
||||
let age_b: Float = 110.0 // t=90, current=200
|
||||
let age_c: Float = 190.0 // t=10, current=200
|
||||
let age_d: Float = 190.0 // t=10, current=200
|
||||
|
||||
// Path strength to B: strong edge, recently relevant
|
||||
let ps_b: Float = path_strength(w_ab_3, age_b, temporal_decay_rate)
|
||||
let conf_b: Float = epistemic_confidence(0.9, ps_b)
|
||||
|
||||
// Path strength to C: no learned edge — only latent proximity gradient
|
||||
// Spreading activation from A can weakly reach C via proximity alone
|
||||
let latent_ac: Float = prox_ac * 0.1 // proximity contributes small initial signal
|
||||
let ps_c: Float = path_strength(latent_ac, age_c, temporal_decay_rate)
|
||||
let conf_c: Float = epistemic_confidence(0.9, ps_c)
|
||||
|
||||
// Path strength to D via C: chain of weak signals
|
||||
let ps_d: Float = path_strength(latent_ac * prox_cd * 0.1, age_d, temporal_decay_rate)
|
||||
let conf_d: Float = epistemic_confidence(0.9, ps_d)
|
||||
|
||||
println("Result: B (influenza)")
|
||||
println(" Edge weight: " + float_to_str(w_ab_3))
|
||||
println(" Node age: " + float_to_str(age_b) + " time units")
|
||||
println(" Temporal decay: " + float_to_str(temporal_decay(age_b, temporal_decay_rate)))
|
||||
println(" Path strength: " + float_to_str(ps_b))
|
||||
println(" Confidence: " + float_to_str(conf_b))
|
||||
println(" → STRONG: B surfaces with high confidence")
|
||||
println("")
|
||||
|
||||
println("Result: C (drug interaction warning)")
|
||||
println(" Edge weight: " + float_to_str(latent_ac) + " (latent only — never co-activated)")
|
||||
println(" Node age: " + float_to_str(age_c) + " time units")
|
||||
println(" Temporal decay: " + float_to_str(temporal_decay(age_c, temporal_decay_rate)))
|
||||
println(" Path strength: " + float_to_str(ps_c))
|
||||
println(" Confidence: " + float_to_str(conf_c))
|
||||
println(" → WEAK: C is distant and old — attenuated path triggers refresh signal")
|
||||
println("")
|
||||
|
||||
println("Result: D (ibuprofen) via C")
|
||||
println(" Path strength: " + float_to_str(ps_d))
|
||||
println(" Confidence: " + float_to_str(conf_d))
|
||||
println(" → VERY WEAK: chain of attenuated edges")
|
||||
println("")
|
||||
|
||||
// ── Refresh trigger ──
|
||||
println("── Attenuation trigger ─────────────────────────")
|
||||
let threshold: Float = 0.2
|
||||
println("Confidence threshold: " + float_to_str(threshold))
|
||||
if conf_c < threshold {
|
||||
println("C is below threshold (" + float_to_str(conf_c) + " < " + float_to_str(threshold) + ")")
|
||||
println("→ Trigger: 'drug interaction warning node found but path is weak.")
|
||||
println(" Last activated " + float_to_str(age_c) + " time units ago.")
|
||||
println(" Recommend fetching current information before acting.'")
|
||||
}
|
||||
println("")
|
||||
|
||||
// ── After refresh: C and D co-activate with A ──
|
||||
println("── Refresh: doctor looks up drug interactions ──")
|
||||
println("A, C, D all co-activate during research")
|
||||
let w_ac_new: Float = hebbian_update(0.0, act_strength, act_strength, lr)
|
||||
let w_cd_new: Float = hebbian_update(0.0, act_strength, act_strength, lr)
|
||||
let age_c_new: Float = 0.0 // just activated
|
||||
let age_d_new: Float = 0.0
|
||||
|
||||
let ps_c_new: Float = path_strength(w_ac_new, age_c_new, temporal_decay_rate)
|
||||
let conf_c_new: Float = epistemic_confidence(0.9, ps_c_new)
|
||||
|
||||
println("A→C weight after refresh: " + float_to_str(w_ac_new))
|
||||
println("C→D weight after refresh: " + float_to_str(w_cd_new))
|
||||
println("C confidence after refresh: " + float_to_str(conf_c_new))
|
||||
println("→ Edges strengthened. Drug interaction now reachable with high confidence.")
|
||||
println("")
|
||||
|
||||
println("=== Result ===")
|
||||
println("Perfect memory: all nodes present throughout.")
|
||||
println("Calibrated confidence: B (flu) high, C/D (drug interaction) low until refreshed.")
|
||||
println("Self-correcting: attenuation triggered fetch, fetch strengthened edges.")
|
||||
println("The field works.")
|
||||
|
||||
return "ok"
|
||||
}
|
||||
|
||||
let result: String = run_test()
|
||||
@@ -0,0 +1,103 @@
|
||||
// language_features_test.el — tests for new operators and builtins
|
||||
|
||||
// 1. Modulo operator
|
||||
let m = 17 % 5
|
||||
println(int_to_str(m))
|
||||
|
||||
// 2. Bitwise ops
|
||||
let flags_and = 10 & 12
|
||||
let flags_xor = 10 ^ 12
|
||||
let flags_shl = 1 << 3
|
||||
let flags_shr = 16 >> 2
|
||||
println(int_to_str(flags_and))
|
||||
println(int_to_str(flags_xor))
|
||||
println(int_to_str(flags_shl))
|
||||
println(int_to_str(flags_shr))
|
||||
|
||||
// 3. Math trig
|
||||
let sin0 = math_sin(0.0)
|
||||
let cos0 = math_cos(0.0)
|
||||
let pi_val = math_pi()
|
||||
println(float_to_str(sin0))
|
||||
println(float_to_str(cos0))
|
||||
println(float_to_str(pi_val))
|
||||
|
||||
// 4. String padding
|
||||
let padded = str_pad_left("42", 5, "0")
|
||||
println(padded)
|
||||
|
||||
let padded_r = str_pad_right("hi", 5, ".")
|
||||
println(padded_r)
|
||||
|
||||
// 5. str_format
|
||||
let tmpl = "Hello {name}, you are {age} years old!"
|
||||
let data = {"name": "Will", "age": "30"}
|
||||
let formatted = str_format(tmpl, data)
|
||||
println(formatted)
|
||||
|
||||
// 6. format_float
|
||||
let ff = format_float(3.14159, 2)
|
||||
println(ff)
|
||||
|
||||
// 7. Time
|
||||
let ts = time_now_utc()
|
||||
let parts = time_to_parts(ts)
|
||||
let iso_str = time_format(ts, "ISO")
|
||||
println(iso_str)
|
||||
|
||||
// 8. Time add/diff
|
||||
let ts2 = time_add(ts, 7, "day")
|
||||
let diff = time_diff(ts, ts2, "day")
|
||||
println(int_to_str(diff))
|
||||
|
||||
// 9. List range
|
||||
let rng = list_range(0, 5)
|
||||
println(list_join(rng, ","))
|
||||
|
||||
// 10. Stack push/pop
|
||||
let s0 = stack_new()
|
||||
let s1 = stack_push(s0, 10)
|
||||
let s2 = stack_push(s1, 20)
|
||||
let top = stack_peek(s2)
|
||||
let s3 = stack_pop(s2)
|
||||
println(int_to_str(top))
|
||||
|
||||
// 11. Queue enqueue/dequeue
|
||||
let q0 = queue_new()
|
||||
let q1 = queue_enqueue(q0, "first")
|
||||
let q2 = queue_enqueue(q1, "second")
|
||||
let front = queue_peek(q2)
|
||||
let q3 = queue_dequeue(q2)
|
||||
println(front)
|
||||
|
||||
// 12. Decimal round
|
||||
let rounded = decimal_round(3.14159, 2)
|
||||
println(float_to_str(rounded))
|
||||
|
||||
// 13. int_to_float / float_to_int
|
||||
let fi = int_to_float(42)
|
||||
let if_ = float_to_int(3.9)
|
||||
println(float_to_str(fi))
|
||||
println(int_to_str(if_))
|
||||
|
||||
// 14. is_nil / unwrap_or
|
||||
let n = unwrap_or(is_nil(42), false)
|
||||
println(bool_to_str(n))
|
||||
|
||||
// 15. list_push, list_pop, list_range
|
||||
let lst = list_new()
|
||||
let lst2 = list_push(lst, 100)
|
||||
let lst3 = list_push(lst2, 200)
|
||||
let last_elem = list_peek_last(lst3)
|
||||
println(int_to_str(last_elem))
|
||||
|
||||
// 16. Char ops
|
||||
let char_a = str_char_at("hello", 1)
|
||||
let code_e = str_char_code("hello", 1)
|
||||
let ch_from = str_from_char_code(65)
|
||||
println(char_a)
|
||||
println(int_to_str(code_e))
|
||||
println(ch_from)
|
||||
|
||||
// Done
|
||||
println("all tests complete")
|
||||
@@ -0,0 +1,10 @@
|
||||
// Test LLM native functions (with ANTHROPIC_API_KEY in env)
|
||||
let models: List = llm_models()
|
||||
println("Available models: " + int_to_str(list_len(models)))
|
||||
|
||||
// This test skips actual API calls since they need env vars
|
||||
// Just verify the functions exist and don't crash on missing keys
|
||||
let missing_key_result: String = llm_call("claude", "ping")
|
||||
println("llm_call returned (may be error without API key): " + str_slice(missing_key_result, 0, 50))
|
||||
|
||||
println("LLM function registration: OK")
|
||||
Reference in New Issue
Block a user