feat: rename crates/ → vessels/ + add El ports per sub-vessel

Belated rename commit for foundation/el-ui — was missed in the
workspace-wide crates→vessels pass earlier today. Same structural
intent as the rename in the other repos: 'crates' is the Rust word,
'vessel' is El's, and the directory rename is the marker that this
slot holds an El buildable unit even if its current contents are
still Rust pending port.

Plus the El ports themselves — manifest.el + src/main.el per sub-
vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout,
el-platform, el-publish, el-secrets, el-services, el-style, el-ui-
compiler). The ui-compiler is a stub: elc only emits C right now;
generating browser-target JS/Wasm is the biggest open language gap
and gets its own project. Until then, el-ui-compiler emits a JS
module that throws elc.backend_missing so callers fail loudly.
Cross-repo path dependencies in Cargo.toml updated to vessels/.
This commit is contained in:
Will Anderson
2026-04-30 18:18:39 -05:00
parent f09803c317
commit f4abfe6fdc
138 changed files with 5445 additions and 900 deletions
+425 -302
View File
@@ -1,76 +1,98 @@
# el-ui Framework Specification
Version 0.1.0 — April 2026
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 answers the same question differently: **what re-renders when state changes?**
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) |
| Svelte | Compile-time analysis |
| **el-ui** | **Spreading activation over a state graph** |
| 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: treating component state as a graph of related nodes, and using the same spreading activation algorithm as the Engram knowledge engine to determine what to update. Reactivity is not declared, tracked, or diffed — it is activated and propagated, exactly as associative memory works in biological neural networks.
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
## 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:
- `id` — UUID, the node's identity
- `type``'state'`, `'route'`, or user-defined
- `name` — the state variable name
- `content` — the current value
- `importance` — a salience weight `[0, 1]`, boosted when the node is activated
| 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 **edges** with a `weight` `[0, 1]` and a `relation` label.
Nodes are connected by directed **edges** with:
When state changes (via `setState()`), spreading activation runs from the changed node:
| 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 is multiplicative, matching the Engram core engine (`engram-core/src/activation.rs`). Every factor must be non-trivial for a path to propagate — weak edges, dormant nodes, and irrelevant connections die immediately. Components subscribed to nodes in the resulting **activation surface** update. Everything else stays still.
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:
### 1.2 Why Multiplication, Not Addition
```
strength = parent_strength × edge.weight × target.importance × cosine_sim(query, target)
```
Addition allows many weak signals to accumulate into false relevance — an observation from the Engram architecture. The brain's associative memory is conjunctive: a path requires ALL its links to be strong. Multiplication enforces this. If any factor is near zero, the path dies.
### 1.3 Why Multiplication, Not Addition
### 1.3 Pruning
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.
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.
### 1.4 Pruning Threshold
### 1.4 Importance Boost
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.
When a node is updated, its importance increases by 0.1 (capped at 1.0). Recently-changed state is more salient — it activates more easily in future propagation. This mirrors long-term potentiation: frequently-used pathways become lower-resistance.
### 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 (the same extension as el source). A component is a specialized el module.
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 Structure
### 2.1 Component Structure
```
component ComponentName {
props {
// optional prop declarations
// prop declarations
}
state {
// optional state declarations
// state declarations
}
fn methodName(param: Type) -> ReturnType {
@@ -83,26 +105,30 @@ component ComponentName {
}
```
All four sections (`props`, `state`, methods, `template`) are optional. Components with no template render an empty string.
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 inputs from the parent component. They are read-only inside the component.
Props are read-only inputs from the parent component.
```
props {
label: String // required
variant: String = "primary" // optional, with default
label: String // required prop
variant: String = "primary" // optional with default
disabled: Bool = false // boolean with default
onClick: Fn() -> Void // function prop
}
```
Prop types: `String`, `Int`, `Float`, `Bool`, `Fn(...) -> T`, `[T]`, `T?`.
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.
State declarations create nodes in the component's Engram graph. Every state variable requires an initial value.
```
state {
@@ -113,20 +139,23 @@ state {
```
Each state variable becomes:
1. A node in `this._graph` (seeded at construction time)
2. A reactive binding: changing the value via `setState()` triggers activation
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. They have access to the component's current state and can call `setState()`.
Methods are `fn` definitions inside the component body. State assignments in method bodies compile to `setState()` calls.
```
fn increment() -> Void {
count = count + 1
count = count + 1 // compiles to: __self.setState('count', count + 1)
}
```
State assignments in method bodies (`count = count + 1`) are compiled to `setState('count', count + 1)` calls.
The generated JavaScript method exposes all state variables as local `const` bindings before executing the body. The variable `__self` refers to the component instance.
---
@@ -134,7 +163,7 @@ State assignments in method bodies (`count = count + 1`) are compiled to `setSta
### 3.1 Interpolation
Embed any expression with `{expr}`:
Any expression embedded in `{expr}`:
```
<h1>{count}</h1>
@@ -144,7 +173,7 @@ Embed any expression with `{expr}`:
### 3.2 Event Binding
Bind DOM events with `on:event={handler}`:
DOM events bound with `on:event={handler}`:
```
<button on:click={() => count = count + 1}>+</button>
@@ -154,7 +183,7 @@ Bind DOM events with `on:event={handler}`:
Supported events: `click`, `input`, `change`, `mouseenter`, `mouseleave`, `keydown`, `keyup`, `keypress`, `focus`, `blur`, `submit`, `mousedown`, `mouseup`, `dblclick`, `contextmenu`.
The compiler emits `data-el-{event}` attributes. The renderer binds handlers at mount time and rebinds after each patch.
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
@@ -172,32 +201,30 @@ The compiler emits `data-el-{event}` attributes. The renderer binds handlers at
### 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.
```
<button disabled={isDisabled}>Submit</button>
<input checked={isChecked} />
```
Boolean attributes emit the attribute name if the expression is truthy, nothing if falsy.
A standalone attribute with no value (e.g., `<button disabled>`) is also parsed and treated as `BoolAttr { expr: "true" }`.
### 3.6 Component Usage
### 3.6 Component Invocation
Components are invoked by their name (uppercase first letter). Props are passed as attributes:
Uppercase-initial tags are el-ui components; lowercase tags are HTML elements:
```
<Button label="Click me" variant="primary" onClick={() => handleClick()} />
<Counter />
<Counter initialValue={5} />
<UserCard name={currentUser.name} />
```
Lowercase tags are HTML elements. Uppercase tags are el-ui components.
The compiler emits `${__self._child(ComponentClass, { prop: value })}` for component references. Child components share the parent's activation graph via `_child()`.
### 3.7 Conditional Rendering — `{#if}`
```
{#if condition}
<div>shown when true</div>
{/if}
{#if loggedIn}
<Dashboard />
{:else}
@@ -205,23 +232,19 @@ Lowercase tags are HTML elements. Uppercase tags are el-ui components.
{/if}
```
The condition is any JavaScript-compatible boolean expression.
Compiles to a ternary template literal expression: `${(condition) ? `...` : `...`}`.
### 3.8 List Rendering — `{#each}`
```
{#each items as item}
<li>{item}</li>
{/each}
{#each users as user}
<UserCard name={user.name} />
{/each}
```
The items expression must evaluate to an array.
The `items` expression must evaluate to an array. Compiles to `${(items).map((user) => `...`).join('')}`.
### 3.9 Semantic Activation — `{#activate}` (The Novel Construct)
### 3.9 Semantic Activation — `{#activate}` (Novel Construct)
```
{#activate "recent premium subscribers" as results}
@@ -229,350 +252,450 @@ The items expression must evaluate to an array.
{/activate}
```
This is el-ui's distinguishing feature. `{#activate}` runs a semantic query against the application's state graph at render time. The query string is a natural language description. The runtime:
`{#activate}` runs a semantic query against the component's state graph at render time. The query string is a natural language description. The runtime:
1. Searches the graph for nodes whose content or name matches the query.
2. Sorts results by their activation score (importance × text match score).
3. Renders the template body once for each result, binding the result as `results`.
1. Calls `this._graph.search("query")` which performs a string-based substring match on node `name` and `content` fields.
2. Sorts results by score (combination of name/content match and node importance).
3. Renders the template body once for each result, binding the result as the variable name.
In v0.1, the search is string-based. In future versions, it will use embedding vectors and cosine similarity — the same semantic search engine as the Engram knowledge database. This means state queries become conceptual rather than structural: instead of `items.filter(i => i.type === 'premium')`, you write `{#activate "premium subscribers"}` and the activation surface finds semantically matching nodes.
Compiles to:
```javascript
${((this._graph.search("query")) || []).map((results) => `...template...`).join('')}
```
In v0.1, the search is string-based (substring match on `name` and `content.toString()`). In future versions connected to an Engram database, `{#activate}` will use embedding vectors and cosine similarity.
**Note on business logic protection:** The spec previously described AES-256-GCM encryption of `{#activate}` query strings in sealed production bundles. This feature is **not implemented** — there is no `--target prod` flag in the current CLI and no `el seal` command.
---
## 4. The State Graph
## 4. State Graph
### 4.1 Graph Structure
Each component instance owns an Engram graph (`this._graph`). State nodes are seeded at construction time. The graph is shared between a component and its children (they operate on the same activation surface).
Each component instance owns an Engram graph (`this._graph`). State nodes are seeded at construction. The graph is shared between a component and its children via `_child()`they operate on the same activation surface.
### 4.2 Node Types
| Type | Description |
|------|-------------|
| `'state'` | Component state variable |
| `'route'` | Router path node |
| User-defined | Any custom node type via `this._graph.seed()` |
### 4.3 Seeding Nodes
### 4.2 State Node Creation
```javascript
const nodeId = graph.seed({ type: 'state', name: 'count', content: 0, importance: 0.5 });
const nodeId = graph.seed({ type: 'state', name: 'count', content: 0 });
// importance defaults to 0.5 if omitted
```
Returns a UUID string that permanently identifies the node.
### 4.4 Connecting Nodes
### 4.3 Edge Creation
```javascript
graph.connect(fromId, toId, { weight: 0.8, relation: 'derived' });
```
Connects two nodes with a directed edge. Higher weight = stronger activation path. Use this to encode semantic relationships between state (e.g., a derived value has an edge from its source).
Connects two nodes with a directed edge. Higher weight = stronger activation path. Default weight is `1.0`, default relation is `'related'`.
### 4.5 Subscribing to Activation
### 4.4 Node Retrieval
```javascript
const node = graph.get(nodeId);
// Returns: { id, type, name, content, importance, edges: [edgeId, ...] }
```
Note: the runtime method is `graph.get(id)`, not `graph.getNode(id)`.
### 4.5 State Update
```javascript
this.setState('count', newValue);
// or directly:
this._graph.update(nodeId, newValue);
```
`setState` looks up the node by state variable name, calls `this._graph.update()` which:
1. Updates `content`.
2. Increments `importance` by 0.1 (capped at 1.0).
3. Runs spreading activation from the changed node.
4. Notifies all subscribers in the activation surface.
5. Notifies direct subscribers on the changed node.
### 4.6 Activation Subscription
```javascript
const unsubscribe = graph.subscribe(nodeId, (node) => {
console.log('node activated:', node.content);
// called whenever this node enters the activation surface
});
// Later:
unsubscribe();
unsubscribe(); // stop listening
```
### 4.6 Semantic Search
Components subscribe to their state nodes in the constructor. When spreading activation reaches a subscribed node, the subscriber callback fires and the component re-renders via `this._renderer.patch()`.
### 4.7 Semantic Search
```javascript
const results = graph.search("recent items", "state");
// Returns array of { id, name, content, importance, score } sorted by score
// Returns: [{ id, type, name, content, importance, score }] sorted by score
// nodeType argument is optional — filters by node.type if provided
```
In v0.1, search is string-based: `name.includes(query)` or `content.toString().includes(query)`. Score is computed as `(nameMatch ? 0.6 : 0) + (contentMatch ? 0.4 : 0)` multiplied by `node.importance`.
### 4.8 Graph Dump (Debug)
```javascript
const allNodes = graph.dump();
// Returns: array of all node objects — useful for DevTools / debugging
```
---
## 5. The `{#activate}` Construct — Semantic State Queries
## 5. Spreading Activation Implementation (In-Browser)
### 5.1 Philosophy
Traditional reactivity systems force you to structure your state queries around your storage schema. If you need "all premium users with recent activity", you write:
The in-browser spreading activation algorithm in `graph.js` (and standalone utilities in `activation.js`) mirrors `engram-core/src/activation.rs`:
```javascript
users.filter(u => u.isPremium && u.lastActive > threshold)
// Within graph.activate(seedId, maxDepth = 3, pruneThreshold = 0.01)
// Returns: Set<string> of activated node IDs
while (queue.length > 0) {
// Best-first: process highest-strength candidate
const { id, strength, depth } = bestInQueue();
if (depth >= maxDepth) continue;
for (const edgeId of node.edges) {
const edge = this.edges.get(edgeId);
const target = this.nodes.get(edge.to);
// Multiplicative formula
const targetStrength = strength * edge.weight * Math.max(0, target.importance);
if (targetStrength <= pruneThreshold) continue;
// Winner-take-most
if (targetStrength > prevBest) { /* enqueue */ }
}
}
```
This is structural — it knows the shape of your data.
**Key parameters:**
- `maxDepth` default: **3** (not 4)
- `pruneThreshold` default: **0.01**
- `limit` default: **20** (applies to `spreadActivation()` utility; `graph.activate()` returns a Set with no limit)
el-ui's `{#activate}` is semantic — it knows the *meaning* of your query:
**Correspondence with engram-core:**
- Same best-first BFS traversal
- Same multiplicative strength formula
- Same pruning threshold semantics (0.01)
- Same winner-take-most rule (strongest path to each node wins)
- Same depth limit semantics
```
{#activate "premium users with recent activity" as users}
```
The in-browser graph omits the `cosine_sim` factor because embedding vectors are not yet available in the browser.
The activation engine finds nodes that semantically match the query, regardless of their exact structure.
### 5.2 Compilation
`{#activate "query" as results}` compiles to:
### 5.1 Standalone Activation Utilities (`activation.js`)
```javascript
${(this._graph.search("query") || []).map((results) => `...template...`).join('')}
import { spreadActivation, activationStrength, reachableNodes, PRUNE_THRESHOLD } from './el-ui.js';
// Multi-seed activation — returns sorted array of reached nodes (excluding seeds)
const reached = spreadActivation(graph, [seedId1, seedId2], { maxDepth: 3, limit: 20 });
// Returns: [{ nodeId, strength, hops, node }]
// Point-to-point strength — returns 0 if no path within maxDepth
const strength = activationStrength(graph, fromId, toId, maxDepth);
// All reachable nodes from a seed (includes seed) — returns Set<string>
const reachable = reachableNodes(graph, seedId, maxDepth);
```
### 5.3 Future: Full Semantic Activation
In a future version connected to an Engram database, `{#activate}` will:
1. Embed the query string into the same vector space as node embeddings.
2. Seed activation at the closest semantic nodes.
3. Spread outward via the graph's weighted edges.
4. Return all nodes above the activation threshold.
This produces a truly associative query: finding nodes that are *semantically related* to the query, not just textually matching.
---
## 6. Router — Graph-Based Routing
### 6.1 Overview
Routes are nodes in the application graph. Navigation activates the target route node. Components subscribed to routing update automatically via spreading activation.
Routes are nodes in the application graph of type `'route'`. Navigation activates the target route node. Components subscribed to routing update automatically via spreading activation.
### 6.2 Usage
Route nodes start with `importance: 0.3`. Navigating to a route boosts its importance by `0.2` (capped at 1.0), modeling browser history salience.
```javascript
import { Router, mount } from './el-ui.js';
import { Home, About, NotFound } from './app.js';
### 6.2 Route Node Connections
const graph = component._graph;
const router = new Router(graph, {
'/': Home,
'/about': About,
'*': NotFound,
});
The router connects parent routes to child routes via edges (weight: 0.8, relation: `'subroute'`). Navigating to `/about/team` activates the `/about` route node as well — activation spreads upward through the subroute chain.
// Navigate programmatically
router.navigate('/about');
// Get current component
const CurrentPage = router.currentComponent();
// Subscribe to route changes
const unsub = router.subscribe((path) => console.log('navigated to', path));
```
### 6.3 Route Node Connections
The router connects parent routes to child routes via edges (weight: 0.8, relation: `'subroute'`). Navigating to `/about/team` activates the `/about` route node as well (activation spreads upward through the subroute chain).
### 6.4 Path Matching
### 6.3 Path Matching
1. Exact match: `/about` matches `/about`
2. Prefix match (longest wins): `/about` matches `/about/team`
3. Wildcard: `'*'` catches all unmatched paths
### 6.4 API
```javascript
const router = new Router(graph, {
'/': HomeComponent,
'/about': AboutComponent,
'*': NotFoundComponent,
});
router.navigate('/about');
router.navigate('/about', true); // replaceState instead of pushState
const CurrentPage = router.currentComponent();
const unsub = router.subscribe((path) => console.log('navigated to', path));
const href = router.href('/about'); // returns the path string (identity in v0.1)
```
The router also listens for `popstate` events (browser back/forward navigation) and activates the appropriate route node automatically.
---
## 7. Plugin API
## 7. Compilation Pipeline
### 7.1 Custom Node Types
### 7.1 Overview
Extend the graph with application-specific node types:
```
source.el → [Lexer] → [Parser] → [Codegen] → output
```
The output format depends on the compilation target (see §7.4).
### 7.2 Lexer
The lexer (`src/lexer.rs`) is a single-pass O(n) tokenizer. It is context-sensitive: when it encounters the `template` keyword followed by `{`, it switches to template mode, producing template-specific tokens:
| Token | Description |
|-------|-------------|
| `HashIdent(kw)` | `{#if}`, `{#each}`, `{#activate}` — block open |
| `SlashIdent(kw)` | `{/if}`, `{/each}`, `{/activate}` — block close |
| `ColonIdent(kw)` | `{:else}` — block continuation |
| `OnColon(event)` | `on:click`, `on:input` — event binding |
| `SelfClose` | `/>` |
| `CloseTag(name)` | `</div>` |
| `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<Component>`. 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 16
- `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');
// Add a custom "user session" node
const sessionNodeId = component._graph.seed({
type: 'session',
name: 'currentUser',
content: { id: '123', name: 'Alice' },
importance: 0.9,
});
// Connect it to a state node for reactive updates
component._graph.connect(sessionNodeId, component._stateNodes['userId'], {
weight: 1.0,
relation: 'owns',
});
const component = mount(App, '#app', { optionalProp: 'value' });
// mount() returns the live Component instance
```
### 7.2 Custom Activation Sources
`mount` instantiates the root component, creates a `Renderer`, calls `renderer.mount()` which calls `render()`, sets `root.innerHTML`, binds events, then calls `onMount()`.
Trigger activation from any node, not just state updates:
### 8.4 DOM Patching Strategy (v0.1)
```javascript
import { spreadActivation } from './el-ui.js';
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.
// Run a custom activation query
const results = spreadActivation(graph, [myNodeId], {
maxDepth: 4,
limit: 10,
pruneThreshold: 0.05,
});
After patching, the renderer attempts to restore focus to the previously focused element by `id`.
results.forEach(({ nodeId, strength, node }) => {
console.log(`${node.name}: ${strength.toFixed(3)}`);
});
```
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.
### 7.3 Component Lifecycle Extension
---
```javascript
class MyComponent extends Component {
constructor(props) {
super(props);
// Seed custom nodes
this._externalData = this._graph.seed({
type: 'external',
name: 'fetchedData',
content: null,
importance: 0.7,
});
}
## 9. Platform Backend Architecture (`el-platform`)
onMount() {
// Side effects after first render
fetch('/api/data')
.then(r => r.json())
.then(data => {
this._graph.update(this._externalData, data);
});
}
The `el-platform` crate defines the `PlatformBackend` trait that all rendering backends implement:
render() {
const data = this._graph.get(this._externalData)?.content;
return data ? `<div>${data.title}</div>` : `<div>Loading...</div>`;
}
```rust
pub trait PlatformBackend: Send + Sync {
fn name(&self) -> &'static str;
fn create_element(&self, tag: &str) -> PlatformResult<PlatformNode>;
fn create_text(&self, content: &str) -> PlatformResult<PlatformNode>;
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<String>;
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.
## 8. Compilation Pipeline
Platform is selected via `manifest.el`:
### 8.1 Overview
```
source.el → [lexer] → [parser] → [codegen] → output.js
```
### 8.2 Lexer (`crates/el-ui-compiler/src/lexer.rs`)
The lexer is a single-pass, O(n) tokenizer. It is context-sensitive: when it encounters the `template` keyword followed by `{`, it switches to template mode, producing template-specific tokens:
| Token | Description |
|-------|-------------|
| `HashIdent(kw)` | `{#if}`, `{#each}`, `{#activate}` |
| `SlashIdent(kw)` | `{/if}`, `{/each}`, `{/activate}` |
| `ColonIdent(kw)` | `{:else}` |
| `OnColon(event)` | `on:click`, `on:input`, etc. |
| `SelfClose` | `/>` |
| `CloseTag(name)` | `</div>` |
| `RawText(s)` | Inline expression text, text content |
### 8.3 Parser (`crates/el-ui-compiler/src/parser.rs`)
Hand-written recursive descent. Produces `Vec<Component>`. 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)
5. `template { ... }` — template tree (optional)
The template is parsed as a tree of `TemplateNode` values. HTML elements, component references, interpolations, and block directives are all represented as nodes in this tree.
### 8.4 Code Generator (`crates/el-ui-compiler/src/codegen.rs`)
The code generator transforms the AST into a JavaScript ES2022 module. For each component:
1. Emits a class extending `Component` from the runtime.
2. In the constructor: seeds state nodes into `this._graph`, subscribes to activation events.
3. Emits `setState()`, which calls `this._graph.update()` to trigger activation.
4. Emits a `render()` method returning a template literal string.
5. Translates state assignments in methods: `count = count + 1``this.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.5 CLI
```bash
# Compile a single .el file
el-ui-compiler App.el -o app.js
# Default output: same name, .js extension
el-ui-compiler App.el # produces App.js
```toml
[platform]
target = "web" # web | server | ios | android | macos | linux | windows
ssr = true
```
---
## 9. Production Build — Quantum-Sealed via engram-crypto
## 10. Comparison to Other Frameworks
The production build pipeline follows the el sealed artifact format:
### 10.1 vs. React
```bash
# 1. Compile .el to .js
el-ui-compiler App.el -o app.js --target prod
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.
# 2. Seal the JavaScript bundle
ENGRAM_SEAL_KEY=my-deploy-key el seal app.js -o app.sealed
```
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.
The sealed artifact is an `ENGRAM01` sealed bundle (same format as the el production target):
### 10.2 vs. Vue
```
Offset Size Field
────── ────── ─────────────────────────────────────────
0 8 Magic: b"ENGRAM01"
8 2 Format version: u16 big-endian (currently 1)
10 * JSON: { algorithm_id, signature, encapsulated_key, nonce, ciphertext }
```
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.
AES-256-GCM encryption. The key is derived from the deployment binding (environment variable, machine fingerprint, or none). Without the key, the bundle is indistinguishable from random bytes. No static analysis tool can extract the application logic, queries, or API keys from a sealed bundle.
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.
**Why "quantum-sealed":** AES-256 is quantum-resistant at 128-bit quantum security (Grover's algorithm provides only a quadratic speedup). The `algorithm_id` field is forward-compatible with ML-KEM when it stabilizes.
### 10.3 vs. Svelte
### 9.1 The `{#activate}` Query is Protected
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.
A key benefit of production sealing: `{#activate}` query strings are embedded in the compiled bundle and encrypted with the application logic. Proprietary semantic queries (which encode business logic about how your application understands its data) are invisible to competitors who decompile your application.
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.
---
## 10. Runtime Size Target
## 11. Versioning Roadmap
The `dist/el-ui.js` runtime targets **under 15KB minified and gzipped**. As of v0.1:
| Module | Purpose | ~Size |
|--------|---------|-------|
| `graph.js` | Engram graph (nodes, edges, activation, search, subscribe) | ~3KB |
| `activation.js` | Standalone activation utilities | ~1.5KB |
| `renderer.js` | DOM patching, event binding | ~2KB |
| `router.js` | Graph-based routing | ~1.5KB |
| `index.js` | Component base class, `mount()`, re-exports | ~1KB |
Total: ~9KB source, ~4KB minified+gzipped (estimated).
---
## 11. Versioning and Compatibility
el-ui follows semantic versioning.
- **v0.1.x** — Initial release. Full re-render on state change. String-based `{#activate}` search.
- **v0.2.x** — Targeted DOM patching (patch only nodes in the activation surface). `{#activate}` with embedding-based semantic search.
- **v0.3.x** — ML-KEM sealed artifacts. Engram database integration for compile-time semantic type checking of `{#activate}` queries.
- **v1.0.0** — Stable API. Full production sealing. LSP integration with spreading activation autocomplete.
---
## 12. Relationship to el
el-ui `.el` files share the `.el` extension with el source files. Components are specialized el modules — in a future version, an `.el` file can mix component definitions with el type definitions, constants, and utility functions in a single compilation unit.
The spreading activation algorithm in `graph.js` and `activation.js` faithfully mirrors `engram-core/src/activation.rs`:
- Same BFS-based traversal
- Same multiplicative strength formula
- Same pruning threshold semantics
- Same winner-take-most rule (strongest path to each node wins)
The in-browser graph is a lightweight implementation without Engram's full embedding vector machinery. In production, a WASM-compiled Engram core can replace the JavaScript graph entirely, enabling true semantic activation with cosine similarity over embedding vectors.
| 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. |