diff --git a/ql/.gitea/workflows/ci-dev.yaml b/ql/.gitea/workflows/ci-dev.yaml new file mode 100644 index 0000000..76d9bef --- /dev/null +++ b/ql/.gitea/workflows/ci-dev.yaml @@ -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 ] diff --git a/ql/.gitea/workflows/ci-stage.yaml b/ql/.gitea/workflows/ci-stage.yaml new file mode 100644 index 0000000..4f822ab --- /dev/null +++ b/ql/.gitea/workflows/ci-stage.yaml @@ -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 ] diff --git a/ql/.gitea/workflows/release.yaml b/ql/.gitea/workflows/release.yaml new file mode 100644 index 0000000..3624ee8 --- /dev/null +++ b/ql/.gitea/workflows/release.yaml @@ -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}"'"}}' diff --git a/ql/spec/elql.md b/ql/spec/elql.md new file mode 100644 index 0000000..62c38a3 --- /dev/null +++ b/ql/spec/elql.md @@ -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=&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) +``` diff --git a/ql/studio/studio.el b/ql/studio/studio.el new file mode 100644 index 0000000..b098dac --- /dev/null +++ b/ql/studio/studio.el @@ -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 ") + 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 ") + " — browse nodes by type") + println(" types: Memory, Concept, Event, Entity, Process, InternalState") + println(" " + color_cyan("tier ") + " — browse nodes by tier") + println(" tiers: Working, Episodic, Semantic, Procedural") + println(" " + color_cyan("search ") + " — full-text search") + println(" " + color_cyan("node ") + " — show node detail + neighbors") + println(" " + color_cyan("activate ") + " — spreading activation from seed") + println(" " + color_cyan("edges") + " — browse edges") + println(" " + color_cyan("top") + " — top nodes by salience") + println(" " + color_cyan("export ") + " — 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 ")) + 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("") diff --git a/ql/test/field_test.el b/ql/test/field_test.el new file mode 100644 index 0000000..6003731 --- /dev/null +++ b/ql/test/field_test.el @@ -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() diff --git a/ql/test/language_features_test.el b/ql/test/language_features_test.el new file mode 100644 index 0000000..9291d75 --- /dev/null +++ b/ql/test/language_features_test.el @@ -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") diff --git a/ql/test/llm_test.el b/ql/test/llm_test.el new file mode 100644 index 0000000..b8347f8 --- /dev/null +++ b/ql/test/llm_test.el @@ -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")