# el-ui Framework Specification
Version 1.1.0 — April 30, 2026
---
## Overview
el-ui is a frontend framework where state is an Engram graph and reactivity is spreading activation. Every framework must answer the same question: what re-renders when state changes? el-ui's answer is: whatever the spreading activation algorithm determines is relevant.
| Framework | Reactivity model |
|-----------|-----------------|
| React | Virtual DOM diffing |
| Vue | Dependency tracking (Proxy-based reactive primitives) |
| Svelte | Compile-time static analysis |
| Solid | Fine-grained signal-based subscriptions |
| **el-ui** | **Spreading activation over a typed state graph** |
The core insight: component state is represented as a graph of related nodes, and the same spreading activation algorithm used by the Engram knowledge engine determines which components need to update. Reactivity is not declared, tracked, or diffed — it is activated and propagated, exactly as associative memory works in biological neural networks.
---
## 1. Activation-Based Reactivity Model
### 1.1 The Model
Every piece of state in an el-ui application is a **node** in an in-browser Engram graph. Nodes have:
| Field | Type | Description |
|-------|------|-------------|
| `id` | String (UUID) | Node's permanent identity |
| `type` | String | `'state'`, `'route'`, or user-defined |
| `name` | String | State variable name |
| `content` | Any | Current value |
| `importance` | Float [0, 1] | Salience weight; boosted on update |
Nodes are connected by directed **edges** with:
| Field | Type | Description |
|-------|------|-------------|
| `weight` | Float [0, 1] | Connection strength |
| `relation` | String | Semantic label of the relationship |
### 1.2 Activation Formula
When state changes via `setState()`, spreading activation runs from the changed node through the graph:
```
strength = parent_strength × edge.weight × target.importance
```
This formula is identical to the Engram core spreading activation formula, with the semantic similarity factor (cosine_sim) omitted because the in-browser graph does not yet carry embedding vectors. In a future version connected to the Engram server, the full four-factor formula applies:
```
strength = parent_strength × edge.weight × target.importance × cosine_sim(query, target)
```
### 1.3 Why Multiplication, Not Addition
Addition allows many weak signals to accumulate into false relevance. The brain's associative memory is conjunctive: an activated path requires ALL its links to be strong. Multiplication enforces this. If any factor is near zero — weak edge, dormant node, or irrelevant connection — the path dies immediately. This is not a performance optimization; it is a semantic property of associative retrieval.
### 1.4 Pruning Threshold
Paths with activation strength below `PRUNE_THRESHOLD` (default: `0.01`) are cut. This prevents exponential blowup in large state graphs and models the brain's attention filter. All nodes not reached above the threshold are not considered for re-render.
### 1.5 Importance Boost (Long-Term Potentiation Analog)
When a state node is updated, its `importance` increases by 0.1 (capped at 1.0):
```
node.importance = Math.min(1.0, node.importance + 0.1)
```
Recently-changed state becomes more salient — it activates more easily in future propagation. This mirrors long-term potentiation: frequently-used pathways become lower-resistance. State that changes often gains importance; state that never changes fades toward background salience.
### 1.6 Activation Surface
The **activation surface** is the set of nodes reached during a spreading activation pass, i.e., all nodes with `strength > PRUNE_THRESHOLD`. Components subscribed to any node in the activation surface receive update notifications and re-render.
---
## 2. Component Definition Syntax
Components are defined in `.el` files (shared extension with the El programming language). A component is a specialized el module with four optional sections.
### 2.1 Component Structure
```
component ComponentName {
props {
// prop declarations
}
state {
// state declarations
}
fn methodName(param: Type) -> ReturnType {
// method body
}
template {
// HTML template
}
}
```
All four sections are optional. Components with no template render an empty string. Multiple `fn` blocks are allowed; multiple `props` or `state` blocks are not (only the last one is used).
### 2.2 Props
Props are read-only inputs from the parent component.
```
props {
label: String // required prop
variant: String = "primary" // optional with default
disabled: Bool = false // boolean with default
onClick: Fn() -> Void // function prop
}
```
Supported prop types: `String`, `Int`, `Float`, `Bool`, `Fn(...) -> T`.
**Note:** Array (`[T]`) and optional (`T?`) type syntax is not currently parsed by the compiler. The parser reads a single identifier as the type name.
Props are accessed in generated code as `this._props_{name}` (not `this.props.name`). In method and template bodies, the compiler exposes each prop as a local constant named `{name}`.
### 2.3 State
State declarations create nodes in the component's Engram graph. Every state variable requires an initial value.
```
state {
count: Int = 0
label: String = "hello"
active: Bool = false
}
```
Each state variable becomes:
1. A node in `this._graph` (seeded at construction time with `type: 'state'`), tracked by ID in `this._stateNodes`.
2. A mirrored value in `this._state` for direct read access.
3. A reactive binding: changing the value via `setState()` triggers activation and re-render.
In method and template bodies, the compiler exposes each state variable as a local constant named `{name}` (reading from `this._state`).
### 2.4 Methods
Methods are `fn` definitions inside the component body. State assignments in method bodies compile to `setState()` calls.
```
fn increment() -> Void {
count = count + 1 // compiles to: __self.setState('count', count + 1)
}
```
The generated JavaScript method exposes all state variables as local `const` bindings before executing the body. The variable `__self` refers to the component instance.
---
## 3. Template Syntax
### 3.1 Interpolation
Any expression embedded in `{expr}`:
```
{count}
{"Hello, " + name + "!"}
{active ? "on" : "off"}
```
### 3.2 Event Binding
DOM events bound with `on:event={handler}`:
```
value = e.target.value} />
hovered = true} on:mouseleave={() => hovered = false} />
```
Supported events: `click`, `input`, `change`, `mouseenter`, `mouseleave`, `keydown`, `keyup`, `keypress`, `focus`, `blur`, `submit`, `mousedown`, `mouseup`, `dblclick`, `contextmenu`.
The compiler emits `data-el-{event}` attributes on elements. The renderer binds handlers at mount time using `new Function(...)` evaluated in the component's scope (`__self`), and rebinds after each patch.
### 3.3 Dynamic Attributes
```
```
### 3.4 Static Attributes
```
```
### 3.5 Boolean Attributes
The parser recognises a fixed set of boolean attribute names: `disabled`, `checked`, `readonly`, `required`, `multiple`, `selected`. These emit the attribute name when the expression is truthy, nothing when falsy.
```
```
A standalone attribute with no value (e.g., `
` |
| `RawText(s)` | Inline expression text, text content |
Non-template code tokens include keywords (`component`, `props`, `state`, `fn`, `template`, `if`, `else`, `return`), identifiers, literals (`String`, `Int`, `Float`, `Bool`), operators, and punctuation.
### 7.3 Parser
Hand-written recursive descent parser. Produces `Vec`. Each component is parsed as:
1. `component Name {` — component declaration
2. `props { ... }` — prop definitions (optional)
3. `state { ... }` — state definitions (optional)
4. `fn method(...) -> Type { ... }` — methods (optional, multiple allowed)
5. `template { ... }` — template tree (optional)
The template parses as a tree of `TemplateNode` values:
| Variant | Description |
|---------|-------------|
| `Element { tag, attrs, children }` | HTML element with attributes and children |
| `Component { name, props }` | Uppercase-initial component reference |
| `Text(s)` | Literal text content |
| `Interpolation(expr)` | `{expr}` — expression interpolation |
| `If { condition, then, else_ }` | `{#if}...{:else}...{/if}` |
| `Each { items, item_name, children }` | `{#each items as item}...{/each}` |
| `Activate { query, result_name, children }` | `{#activate "query" as name}...{/activate}` |
Method bodies are captured as raw source text and passed through to the code generator with simple string transformations.
**Unknown `{#blockname}` tags** produce a parse error (`unknown block tag: #name`).
### 7.4 Code Generator — Multi-Target
The code generator (`src/codegen.rs`) supports three compilation targets:
#### `CodegenTarget::Web` (default) — Legacy JavaScript
Emits an ES2022 JavaScript module using the el-ui JS runtime (`graph.js`, `renderer.js`, `router.js`, `index.js`). This is the primary target for browser-based applications today.
For each component:
1. Emits a class extending `Component` from the runtime.
2. Constructor: seeds state nodes into `this._graph`, sets prop values as `this._props_{name}`, subscribes to activation events.
3. Emits `setState(name, value)` which calls `this._graph.update()` to trigger activation.
4. Emits `render()` returning a template literal string.
5. Translates state assignments: `count = count + 1` → `__self.setState('count', count + 1)`.
6. Translates template interpolations: `{count}` → `${count }`.
7. Emits event handlers as `data-el-{event}` attributes for the renderer to bind.
8. Emits `${__self._child(ComponentClass, props)}` for component references.
#### `CodegenTarget::Server` — Rust SSR
Emits a Rust module where each component is a struct implementing `render_to_html(&self) -> String` via `el_platform::ServerBackend`.
**Note:** The `build_node_tree()` implementation in this target is a stub — it returns a placeholder `PlatformNode::element("div")`. Full template-to-PlatformNode codegen is not yet implemented for the server target.
#### `CodegenTarget::Native(Platform)` — Rust Native / WASM
Emits a Rust module where each component builds a `PlatformNode` tree via the semantic primitive layer and mounts it via the `el_platform` backend for the given platform.
Supported platforms:
| Platform | Backend | Output format |
|----------|---------|---------------|
| `Platform::Web` | `WebBackend` (web-sys) | Rust compiled to WASM — NOT JavaScript |
| `Platform::Ios` | `IosBackend` | Rust (objc2-ui-kit) |
| `Platform::Android` | `AndroidBackend` | Kotlin (Jetpack Compose via build.rs) |
| `Platform::Macos` | `MacosBackend` | Rust (objc2-app-kit) |
| `Platform::Linux` | `LinuxBackend` | Rust (gtk4-rs) |
| `Platform::Windows` | `WindowsBackend` | Rust (windows-rs / WinUI 3) |
**Note:** The `build_node_tree()` implementation in all native targets is currently a stub returning `PlatformNode::element("div")`. Full AST-to-semantic-primitive lowering is not yet wired in — the semantic module and per-platform codegen functions exist and are correct, but the compiler does not yet call them from the AST walk.
### 7.5 Semantic Primitive Layer (`semantic.rs`)
The semantic layer is the bridge between the template AST and platform-specific code generation. It defines EBD-driven concepts — *what* something is, not *how* it looks:
**AppearanceConcept** — what a control is:
- `Action` — primary action trigger (blue/filled on most platforms)
- `Destructive` — dangerous/irreversible action (red tint)
- `Secondary` — subdued / less-prominent action
- `Navigation` — link-like / back-navigation element
- `Informational` — read-only display element
- `Structural` — layout container with no interactive meaning
**LayoutConcept** — how children are arranged:
- `Stack` — vertical (Column / VStack / GtkBox vertical)
- `Row` — horizontal (Row / HStack / GtkBox horizontal)
- `Grid` — two-dimensional grid
- `Overlay` — z-axis layering (ZStack / GtkOverlay)
- `Scroll` — scrollable container
**TextConcept** — the semantic role of text:
- `Heading { level: u8 }` — heading at level 1–6
- `Body` — default body text
- `Caption` — small secondary text
- `Label` — control label
- `Code` — monospaced code text
**SemanticPrimitive** — the EBD building blocks:
- `Button { label, appearance, on_press }` — interactive button
- `Text { content, concept }` — text display
- `Container { children, layout }` — layout container
- `Input { binding, hint, appearance }` — text input bound to state
- `Image { src, alt }` — image element
- `Toggle { binding, label }` — checkbox/switch bound to boolean state
- `List { items_binding, item_name, item_template }` — homogeneous list
Appearance is inferred from explicit `appearance="..."` attributes, CSS class names, or HTML tag heuristics via `infer_appearance()`. Layout and text concepts are inferred from tag names via `infer_layout_concept()` and `infer_text_concept()`.
### 7.6 CLI
```bash
el-ui-compiler App.el # compiles to App.js (Web target, default)
el-ui-compiler App.el -o app.js # explicit output path
```
The CLI always uses `CodegenTarget::Web`. Target selection (`--target server`, `--target ios`, etc.) is available as a `CodegenTarget` enum in the library API but is not exposed as a CLI flag in the current implementation.
---
## 8. Runtime Architecture
### 8.1 Module Structure
| Module | Purpose |
|--------|---------|
| `graph.js` | Engram graph: nodes, edges, activation, search, subscribe, dump |
| `activation.js` | Standalone activation utilities: `spreadActivation`, `activationStrength`, `reachableNodes` |
| `renderer.js` | DOM patching, event binding |
| `router.js` | Graph-based routing |
| `index.js` | `Component` base class, `mount()`, re-exports |
### 8.2 Component Lifecycle
| Lifecycle event | Description |
|-----------------|-------------|
| `constructor(props)` | Seeds state graph, subscribes to activation |
| `onMount()` | Called after first render and DOM attachment |
| `render()` | Returns HTML string; called on activation |
**Note:** `onDestroy()` is documented in prior drafts but is not implemented in the base `Component` class or the `Renderer`. It will not be called by the runtime.
### 8.3 Mounting
```javascript
import { mount } from './el-ui.js';
import { App } from './app.js';
const component = mount(App, '#app', { optionalProp: 'value' });
// mount() returns the live Component instance
```
`mount` instantiates the root component, creates a `Renderer`, calls `renderer.mount()` which calls `render()`, sets `root.innerHTML`, binds events, then calls `onMount()`.
### 8.4 DOM Patching Strategy (v0.1)
The renderer uses full string re-render on every state change: `root.innerHTML = component.render()`. The activated node set argument to `patch()` is accepted but unused — targeted patching is planned for v0.2.
After patching, the renderer attempts to restore focus to the previously focused element by `id`.
Event handlers are rebound after every patch by scanning the new DOM for `data-el-{event}` attributes. Handlers are compiled via `new Function('__self', ...)` — this requires a permissive Content Security Policy in v0.1.
---
## 9. Platform Backend Architecture (`el-platform`)
The `el-platform` crate defines the `PlatformBackend` trait that all rendering backends implement:
```rust
pub trait PlatformBackend: Send + Sync {
fn name(&self) -> &'static str;
fn create_element(&self, tag: &str) -> PlatformResult;
fn create_text(&self, content: &str) -> PlatformResult;
fn set_attribute(&self, node: &mut PlatformNode, name: &str, value: &str) -> PlatformResult<()>;
fn remove_attribute(&self, node: &mut PlatformNode, name: &str) -> PlatformResult<()>;
fn append_child(&self, parent: &mut PlatformNode, child: PlatformNode) -> PlatformResult<()>;
fn remove_child(&self, parent: &mut PlatformNode, child_index: usize) -> PlatformResult<()>;
fn replace_child(&self, parent: &mut PlatformNode, index: usize, new_child: PlatformNode) -> PlatformResult<()>;
fn bind_event(&self, node: &mut PlatformNode, event: &str, handler: EventHandler) -> PlatformResult<()>;
fn render_to_string(&self, node: &PlatformNode) -> PlatformResult;
fn mount(&self, root: PlatformNode, container_id: &str) -> PlatformResult<()>;
fn patch(&self, old: &PlatformNode, new: &PlatformNode) -> PlatformResult<()>;
fn supports_ssr(&self) -> bool { false }
}
```
The `ServerBackend` is the only backend where `supports_ssr()` returns `true` and `render_to_string()` produces real HTML output. All other backends are platform-native.
Platform is selected via `manifest.el`:
```toml
[platform]
target = "web" # web | server | ios | android | macos | linux | windows
ssr = true
```
---
## 10. Comparison to Other Frameworks
### 10.1 vs. React
React uses virtual DOM diffing: on every render, it constructs a virtual tree and diffs it against the previous tree to identify minimal DOM mutations. The unit of re-render is the component; re-render triggers are driven by `setState`, `useReducer`, or context changes.
el-ui does not compute a virtual DOM. The spreading activation pass determines the re-render set from the graph topology. State relationships that exist in the graph are automatically propagated; relationships that do not exist are not. There is no need to declare dependencies, use `useMemo`, or prevent unnecessary renders with `React.memo` — the graph structure encodes the dependency information directly.
### 10.2 vs. Vue
Vue uses a Proxy-based reactive system: state values are wrapped in reactive proxies, and reads from these proxies during render are tracked as dependencies. When a reactive value changes, all components that read it during their last render are re-rendered.
el-ui's graph model is explicit: relationships between state nodes are declared via graph edges, not inferred from read tracking. This makes dependencies inspectable and modifiable at runtime. It also enables semantic queries (`{#activate}`) that find relevant state by meaning rather than by structural reference.
### 10.3 vs. Svelte
Svelte uses compile-time analysis to determine which parts of the DOM update when which state changes. The compiler emits targeted DOM mutations. There is no runtime overhead for a virtual DOM or a reactive system.
el-ui uses a runtime spreading activation pass. This is more expensive than Svelte's compile-time approach for small, known-static state graphs. The advantage is dynamic state topology: new nodes and edges can be added at runtime without recompilation, enabling state graphs that evolve with program execution.
### 10.4 Core Differentiator
No prior framework uses Hebbian spreading activation as the re-render decision mechanism. The multiplicative activation formula, the self-seeded activation from a root node, the importance-boost on state change (LTP analog), and the semantic `{#activate}` query construct have no analogues in any shipping frontend framework as of April 2026.
---
## 11. Versioning Roadmap
| Version | Key changes |
|---------|-------------|
| v0.1.x | Current. Full re-render on state change. String-based `{#activate}` search. JS (Web) target only via CLI. Native targets available via library API but `build_node_tree()` is stub. |
| v0.2.x | Targeted DOM patching (only nodes in activation surface). `{#activate}` with embedding-based semantic search. CLI `--target` flag for native targets. Full AST→semantic→PlatformNode lowering. |
| v0.3.x | WASM-compiled Engram core replacing JavaScript graph. Sealed artifact support. `onDestroy()` lifecycle. |
| v1.0.0 | Stable API. Full production sealing. LSP integration with spreading activation autocomplete. |