From 6e1a338eb607b1740d5d3bd2e95b8e3d6adebb56 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sat, 2 May 2026 10:24:08 -0500 Subject: [PATCH 1/5] uncommitted state captured before pushing to Gitea --- ql/spec/elql.md | 625 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 625 insertions(+) create mode 100644 ql/spec/elql.md 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) +``` From cab85096082c06144739f1e3b4a95d778ca4e4de Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 08:55:34 -0500 Subject: [PATCH 2/5] add gitflow CI for dev/stage/prod environments --- ql/.gitea/workflows/ci-dev.yaml | 89 +++++++++++++++++++++++++++++++ ql/.gitea/workflows/ci-stage.yaml | 87 ++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 ql/.gitea/workflows/ci-dev.yaml create mode 100644 ql/.gitea/workflows/ci-stage.yaml 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..734a59a --- /dev/null +++ b/ql/.gitea/workflows/ci-stage.yaml @@ -0,0 +1,87 @@ +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: 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 ] From 2eebd1322163c5bbe37a9539e696b7b521d67357 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 19:32:23 -0500 Subject: [PATCH 3/5] Add release workflow listening to el-sdk-updated and engram-updated Triggers on push to main plus repository_dispatch for both el-sdk-updated and engram-updated. Installs El SDK from foundation-prod, compile-checks all standalone .el programs, and includes elql-updated dispatch placeholder for future downstream consumers. --- ql/.gitea/workflows/release.yaml | 105 +++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 ql/.gitea/workflows/release.yaml diff --git a/ql/.gitea/workflows/release.yaml b/ql/.gitea/workflows/release.yaml new file mode 100644 index 0000000..30b9d45 --- /dev/null +++ b/ql/.gitea/workflows/release.yaml @@ -0,0 +1,105 @@ +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: 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}"'"}}' From c704e531022238d3561666be43fa2745038402df Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 19:34:45 -0500 Subject: [PATCH 4/5] =?UTF-8?q?enforce=20source=20branch=20in=20CI:=20stag?= =?UTF-8?q?e=E2=86=90dev,=20main=E2=86=90stage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ql/.gitea/workflows/ci-stage.yaml | 10 ++++++++++ ql/.gitea/workflows/release.yaml | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/ql/.gitea/workflows/ci-stage.yaml b/ql/.gitea/workflows/ci-stage.yaml index 734a59a..4f822ab 100644 --- a/ql/.gitea/workflows/ci-stage.yaml +++ b/ql/.gitea/workflows/ci-stage.yaml @@ -19,6 +19,16 @@ jobs: - 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 diff --git a/ql/.gitea/workflows/release.yaml b/ql/.gitea/workflows/release.yaml index 30b9d45..3624ee8 100644 --- a/ql/.gitea/workflows/release.yaml +++ b/ql/.gitea/workflows/release.yaml @@ -21,6 +21,16 @@ jobs: - 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 From 4e79edbe813de0214fbd7fc401c79f02bbbef656 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 4 May 2026 20:17:16 -0500 Subject: [PATCH 5/5] ci: retrigger after ci-base image rebuild