spec ·interaction-model

The interaction model.

The interaction model defines what an interaction is, how Refs name locations, how Interactions compose into programs. Language-agnostic, implementation-agnostic. A specification anyone can implement.

Names, and what to do with them.

A program that touches the world needs two things: names for what it touches, and descriptions of what to do with them. The model calls the names Refs and the descriptions Interactions. Every element of a program is one or the other.

ref

A name for a location.

The only atom that touches Context. A pointer to a KV slot, a DB row, a file, an in-memory cell, a UI widget. Carries the address, not the value at the address. Every read and every write in a program passes through a Ref — no side channels.

interaction

A description of what to do.

Everything a program does with Refs and with other Interactions: read, write, compute, branch, iterate, compose. Programs are trees of Interactions. Fabrics execute them against real backends.

Observability and mutation.

A program exists to mutate Context. Context changes exactly one way: a Ref in WRITE position carries a value into it. No Ref in WRITE, no landed mutation.

Two paths make an atom observable.

An atom is observable in its place iff it lands a mutation itself (own WRITE via a Ref), or it yields a value consumed by an ancestor whose chain eventually lands one.

Crossing the two paths gives four cells. Three are live — the atom kinds fall out of them. One is dead by construction and excluded from the model.

observability
matrix · 6 locmtx
mutates: no mutates: yes
yields: yes (a) Query (c) Action
yields: no (d) invalid (b) Command
# a, b, c are the three live kinds. d is dead by construction:
# it neither mutates nor yields, so nothing observes it.

query

Yields, does not mutate.

Pure value producers: arithmetic, comparison, projection, iteration over values. A subtree may still mutate via Action descendants.

command

Mutates, yields nothing.

The pure mutator. Writes through one or more Ref children; that is the only output. Store, Copy, Append, Delete.

action

Mutates and yields.

One atomic step that writes and returns a value. Pop, Swap, INSERT…RETURNING, POST /resource → id. Splitting into Command-then-Query would lose atomicity.

Cardinality, and the kinds it produces.

The second dimension. Where mutation asks does this touch Context, cardinality asks how many values does it yield. Crossed with mutation, the two dimensions produce the full atom vocabulary.

scalar

One value.

The default. Refs are scalar (one address, one value). ScalarQuery, ScalarAction.

stream

0..N values.

Multi-value producers. StreamQuery (Map, Filter, ItemsOf) and StreamAction (Drain, DELETE…RETURNING).

void

Nothing.

No yield. Command and Flow — their observability comes from writing or from composing writers, not from returning a value.

transparent

Forwards the body.

Span wraps any atom and forwards the body’s yield in the same shape. Snapshot, Transaction, Retry, TryCatch.

The atom kinds, at a glance.

Eight concrete kinds fall out of crossing the two dimensions and adding the two composition atoms (Flow, Span).

ref

Address atom.

SCALAR. The only path to Context. Examples: user_ref, config["theme"], counter.

scalarquery / streamquery

Value producers.

Add, Eq, ref.fetch() (scalar). Map, Filter, ItemsOf (stream).

command

Pure mutator.

VOID. Writes and returns nothing. Store, Copy, Append, Delete.

scalaraction / streamaction

Mutate-and-yield.

Pop, Swap, Create (scalar). Drain, DeleteReturning (stream).

flow

Composition of mutators.

VOID. Its body slot needs a mutator, not a value. Sequential, Parallel, IfDo, ForEachDo, WhileDo.

span

Cross-cutting wrapper.

Transparent. Snapshotting, transactions, retry, fallback. Snapshot, Transaction, Retry, TryCatch.

One program, one tree.

A counter that ticks every second and a dashboard widget that mirrors it — the same example the README opens with, drawn as the tree the model actually is. No host-language syntax.

The tree is the program.

Every node is an atom. Every edge is a slot. The kinds on the right explain what each node contributes: a Flow composes mutators, a Command writes through a Ref, a StreamQuery emits values on change.

A Context binds each Ref to the Fabric that owns it — Counter.val to kv, Dashboard.count to the browser. Swap the Context, the tree does not change.

counter.tree
tree · 12 loctree
program
├─ Parallel # Flow · strategy
│ ├─ ForeverDo # Flow · control
│ │ └─ Sequential # Flow · strategy
│ │ ├─ Increment(Counter.val) # Command → Ref(WRITE)
│ │ └─ Delay(1.0) # Query
│ └─ ReactForever # Flow · control
│ ├─ Counter.val.on_change # StreamQuery
│ └─ Set(Dashboard.count, Counter.val) # Command → Ref(WRITE)
└─ context
├─ Counter.val → kv fabric
└─ Dashboard.count → ui fabric

Try Nu.

One command gets you the wheel with every fabric. Then follow the movies tutorial to build a real app in an afternoon.

01 Install

pip install "nustack-py[all]"

02 Run the demo

nu demo movies

03 Build your app

Browse examples

Like what you see?

The project is young. Star it, join the room, watch what we ship next.