18 KiB
el-ui Framework Specification
Version 0.1.0 — April 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?
| Framework | Reactivity model |
|---|---|
| React | Virtual DOM diffing |
| Vue | Dependency tracking (Proxy-based) |
| Svelte | Compile-time analysis |
| el-ui | Spreading activation over a 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.
1. Activation-Based Reactivity
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 identitytype—'state','route', or user-definedname— the state variable namecontent— the current valueimportance— a salience weight[0, 1], boosted when the node is activated
Nodes are connected by edges with a weight [0, 1] and a relation label.
When state changes (via setState()), spreading activation runs from the changed node:
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.
1.2 Why Multiplication, Not Addition
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 Pruning
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 Importance Boost
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.
2. Component Definition Syntax
Components are defined in .el files (the same extension as el source). A component is a specialized el module.
2.1 Structure
component ComponentName {
props {
// optional prop declarations
}
state {
// optional state declarations
}
fn methodName(param: Type) -> ReturnType {
// method body
}
template {
// HTML template
}
}
All four sections (props, state, methods, template) are optional. Components with no template render an empty string.
2.2 Props
Props are inputs from the parent component. They are read-only inside the component.
props {
label: String // required
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?.
2.3 State
State declarations create nodes in the component's Engram graph.
state {
count: Int = 0
label: String = "hello"
active: Bool = false
}
Each state variable becomes:
- A node in
this._graph(seeded at construction time) - A reactive binding: changing the value via
setState()triggers activation
2.4 Methods
Methods are fn definitions inside the component body. They have access to the component's current state and can call setState().
fn increment() -> Void {
count = count + 1
}
State assignments in method bodies (count = count + 1) are compiled to setState('count', count + 1) calls.
3. Template Syntax
3.1 Interpolation
Embed any expression with {expr}:
<h1>{count}</h1>
<p>{"Hello, " + name + "!"}</p>
<span>{active ? "on" : "off"}</span>
3.2 Event Binding
Bind DOM events with on:event={handler}:
<button on:click={() => count = count + 1}>+</button>
<input on:input={(e) => value = e.target.value} />
<div on:mouseenter={() => 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. The renderer binds handlers at mount time and rebinds after each patch.
3.3 Dynamic Attributes
<div class={active ? "btn-active" : "btn"}></div>
<a href={currentUrl}></a>
3.4 Static Attributes
<div class="container"></div>
<input type="text" />
3.5 Boolean Attributes
<button disabled={isDisabled}>Submit</button>
<input checked={isChecked} />
Boolean attributes emit the attribute name if the expression is truthy, nothing if falsy.
3.6 Component Usage
Components are invoked by their name (uppercase first letter). Props are passed as attributes:
<Button label="Click me" variant="primary" onClick={() => handleClick()} />
<Counter />
<UserCard name={currentUser.name} />
Lowercase tags are HTML elements. Uppercase tags are el-ui components.
3.7 Conditional Rendering — {#if}
{#if condition}
<div>shown when true</div>
{/if}
{#if loggedIn}
<Dashboard />
{:else}
<Login />
{/if}
The condition is any JavaScript-compatible boolean expression.
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.
3.9 Semantic Activation — {#activate} (The Novel Construct)
{#activate "recent premium subscribers" as results}
<UserCard name={results.name} />
{/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:
- Searches the graph for nodes whose content or name matches the query.
- Sorts results by their activation score (importance × text match score).
- Renders the template body once for each result, binding the result as
results.
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.
4. The 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).
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
const nodeId = graph.seed({ type: 'state', name: 'count', content: 0, importance: 0.5 });
Returns a UUID string that permanently identifies the node.
4.4 Connecting Nodes
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).
4.5 Subscribing to Activation
const unsubscribe = graph.subscribe(nodeId, (node) => {
console.log('node activated:', node.content);
});
// Later:
unsubscribe();
4.6 Semantic Search
const results = graph.search("recent items", "state");
// Returns array of { id, name, content, importance, score } sorted by score
5. The {#activate} Construct — Semantic State Queries
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:
users.filter(u => u.isPremium && u.lastActive > threshold)
This is structural — it knows the shape of your data.
el-ui's {#activate} is semantic — it knows the meaning of your query:
{#activate "premium users with recent activity" as users}
The activation engine finds nodes that semantically match the query, regardless of their exact structure.
5.2 Compilation
{#activate "query" as results} compiles to:
${(this._graph.search("query") || []).map((results) => `...template...`).join('')}
5.3 Future: Full Semantic Activation
In a future version connected to an Engram database, {#activate} will:
- Embed the query string into the same vector space as node embeddings.
- Seed activation at the closest semantic nodes.
- Spread outward via the graph's weighted edges.
- 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.
6.2 Usage
import { Router, mount } from './el-ui.js';
import { Home, About, NotFound } from './app.js';
const graph = component._graph;
const router = new Router(graph, {
'/': Home,
'/about': About,
'*': NotFound,
});
// 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
- Exact match:
/aboutmatches/about - Prefix match (longest wins):
/aboutmatches/about/team - Wildcard:
'*'catches all unmatched paths
7. Plugin API
7.1 Custom Node Types
Extend the graph with application-specific node types:
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',
});
7.2 Custom Activation Sources
Trigger activation from any node, not just state updates:
import { spreadActivation } from './el-ui.js';
// Run a custom activation query
const results = spreadActivation(graph, [myNodeId], {
maxDepth: 4,
limit: 10,
pruneThreshold: 0.05,
});
results.forEach(({ nodeId, strength, node }) => {
console.log(`${node.name}: ${strength.toFixed(3)}`);
});
7.3 Component Lifecycle Extension
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,
});
}
onMount() {
// Side effects after first render
fetch('/api/data')
.then(r => r.json())
.then(data => {
this._graph.update(this._externalData, data);
});
}
render() {
const data = this._graph.get(this._externalData)?.content;
return data ? `<div>${data.title}</div>` : `<div>Loading...</div>`;
}
}
8. Compilation Pipeline
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:
component Name {— component declarationprops { ... }— prop definitions (optional)state { ... }— state definitions (optional)fn method(...) -> Type { ... }— methods (optional, multiple)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:
- Emits a class extending
Componentfrom the runtime. - In the constructor: seeds state nodes into
this._graph, subscribes to activation events. - Emits
setState(), which callsthis._graph.update()to trigger activation. - Emits a
render()method returning a template literal string. - Translates state assignments in methods:
count = count + 1→this.setState('count', count + 1). - Translates template interpolations:
{count}→${count}. - Emits event handlers as
data-el-{event}attributes for the renderer to bind.
8.5 CLI
# 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
9. Production Build — Quantum-Sealed via engram-crypto
The production build pipeline follows the el sealed artifact format:
# 1. Compile .el to .js
el-ui-compiler App.el -o app.js --target prod
# 2. Seal the JavaScript bundle
ENGRAM_SEAL_KEY=my-deploy-key el seal app.js -o app.sealed
The sealed artifact is an ENGRAM01 sealed bundle (same format as the el production target):
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 }
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.
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.
9.1 The {#activate} Query is Protected
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.
10. Runtime Size Target
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.