Getting Started
This page is the shortest complete path through harmos: declare a dependency, define one change, boot a runtime, commit, read the position that commit reached, and shut down without losing anything. At the end you have a running journal-as-truth application and the four lines you will keep writing.
Every Rust block below is embedded from examples/minischematic/, so what you
read is what compiles. mise run run:minischematic runs the whole example.
Add the Dependency
Section titled “Add the Dependency”An application depends on harmos and names nothing beneath it: every kit in the
workspace is a feature of this crate that opens a module of the same name.
Harmos is not published to a registry, so that dependency is declared by path
or by git revision:
[dependencies]# By path, for a checkout beside your project:harmos = { path = "../harmos/crates/harmos" }
# Or by git, against the remote you cloned from. Pin `rev` rather than tracking# a branch: harmos is pre-1.0 and its public surface still moves.harmos = { git = "<the harmos remote>", rev = "<a commit you tested against>" }Inside this workspace the same dependency is a workspace entry, which is what the schematic example's application crate declares:
# Compiled for the host and nothing else, which is what lets the adapter, the# JSON codec and the guest host sit in one unconditional list. The api crate is# here because `services/` is where an autorouter's request is held to the# transaction it is decoded as.[dependencies]minischematic-api = { path = "../api" }harmos = { workspace = true, features = ["guest"] }jsonl-adapter = { path = "../../jsonl-adapter" }serde.workspace = trueserde_json.workspace = truethiserror.workspace = truetracing.workspace = trueharmos owns the runtime, while serde and thiserror support the state and
its refusals. tracing instruments the entry verbs and nothing else —
installing a subscriber over them is a shell's decision, so
tracing-subscriber is not here. The adapter that reads a recording back is an
ordinary dependency too, because this crate is compiled for the host and only
the host.
Four crates, and the names say which is which.
examples/minischematic/app is the package called minischematic-app: what the
editor is and how a runtime runs it — the state, the catalogs, the identity
and the entry verbs. examples/minischematic/api is minischematic-api: the
editor's contract with its own plugins, and nothing else.
examples/minischematic/cli is minischematic-cli, the shell that chooses a
document and prints a transcript, and examples/minischematic/plugin is
minischematic-plugin, the Wasm guest. The direction is one way and never
bends: api below app below the shell, api below the plugin, and nothing at all
depending on the plugin.
The api crate is small on purpose:
# The editor's serializable contract with its own plugins, and nothing else.[dependencies]serde.workspace = trueHarmos with every default feature off, and serde. That is the whole list,
because a plugin compiles this crate and only this crate — so anything that
opened a file or booted a runtime would be a guest build that cannot be made.
It is a separate crate rather than a module for exactly that reason: a crate
boundary is the only kind a compiler enforces, mise run check:wasm builds it
for wasm32-unknown-unknown on every run, and neither it nor the app crate
carries a single cfg(target_arch) gate.
Choose Your Features
Section titled “Choose Your Features”Every optional capability is a cargo feature, and a default build compiles none of them. Enable what you use and nothing else.
| Crate | Feature | What it adds |
|---|---|---|
harmos | (default) | the journal, streams, work, evidence — no optional dependency |
harmos | artifact | harmos::artifact, the declaration contract both carriers name (Load Artifacts) |
harmos | sidecar | harmos::sidecar, native-process authoring and supervision (Isolate a Sidecar) |
harmos | guest | harmos::guest, the Wasm host loader beside the authoring kit (Load Guests and Sidecars) |
harmos | componentize | the component encoder in harmos::guest, without the host runtime a packaging tool never runs |
harmos | signal | harmos::signal, measurement kernels over plain &[f64] |
harmos | plugin | harmos::plugin, packaging and installation for both carriers (Ship the Runtime) |
harmos | sim | the seeded simulation harness under harmos::sim (Simulate) |
Storage Is a Contract, Not a Crate
Section titled “Storage Is a Contract, Not a Crate”There is no storage row in that table, and no second dependency beneath
harmos. Harmos publishes one order and one fold contract — Storage,
Source, Series, Vault, and the encode boundary beside them — and ships
no engine that implements them. An application that persists writes the adapter it
wants, against ports the runtime already declares, and the runtime never learns
that JSON, or a directory, or a file exists.
You do not start from nothing. examples/jsonl-adapter is a complete, durable,
tested implementation of those ports, published so you can read one end to end
and copy what you need: a JSONL journal copy, a crash-recovery sidecar, a
snapshot producer, a JSON Lines stream capture, and an evidence vault. It is a
worked example rather than a supported dependency — vendor the files you want,
own them, and change the on-disk layout freely
(reference).
Choose an Async Runtime
Section titled “Choose an Async Runtime”Harmos spawns one writer task, so a Tokio runtime has to exist. It enables the Tokio features it needs itself; what your own crate adds is the entrypoint:
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "time"] }The examples use the ordinary #[tokio::main] entrypoint. That keeps the
learning surface focused on harmos and gives hosted work the multi-thread
runtime it may need without an application-specific executor choice.
Define One Change
Section titled “Define One Change”A transaction owns a permanent wire id, a precondition, and an infallible
application after that precondition succeeds. The runtime records it only
after apply completes.
#[harmos::transaction(id = "symbol.place", version = 1, irreversible)]#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]pub struct PlaceSymbol { /// The name the placement takes, for as long as it exists. pub symbol: SymbolId, /// What the drawing calls it. pub reference: String, /// Where it goes. pub at: Point, /// How far it is turned. pub rotation: Rotation,}
impl Transaction<Schematic> for PlaceSymbol { type Error = SchematicError;
fn check(&self, schematic: &Schematic) -> Result<(), Self::Error> { schematic.require_symbol_free(self.symbol) }
fn apply(&self, schematic: &mut Schematic) { schematic.symbols.insert( self.symbol, Symbol { reference: self.reference.clone(), position: self.at, rotation: self.rotation, }, ); }}Name the Application
Section titled “Name the Application”Application names the state, the closed transaction, record, and stream catalogs
carried by this runtime, and the resources its jobs borrow. The types remain
application types.
/// The editor's identity card: this state, changed by this language, narrated/// by these facts, and publishing these streams.#[derive(Clone, Copy, Debug)]pub struct SchematicEditor;
impl harmos::Application for SchematicEditor { type State = Schematic; type Transaction = Transactions; type Record = Records; type Streams = StreamCatalog; type Resources = ();}Boot the Runtime
Section titled “Boot the Runtime”The builder receives the origin state first and the declared resources second.
It freezes the catalogs, restores any attached history, opens the writer, and
returns only when the runtime is ready. The schematic names that declaration
once, as app::builder, and it is deliberately environment-free: it reads no
variable and opens no file, so every way the editor is stood up — live,
memory-only, or simulated — starts from the same three registrations.
/// Declares the canonical editor: its catalogs, and nothing else.////// Infallible and environment-free. It reads no variable, opens no file, and/// chooses no storage, which is what lets [`launch`] and [`simulate`] — and a/// memory-only teaching boot — all start from the same declaration.pub fn builder(origin: impl Into<Origin>, resources: ()) -> Builder<SchematicEditor> { harmos::Runtime::builder(origin, resources) .register(Transactions::catalog()) .register(Records::catalog()) .register(StreamCatalog::catalog())}Commit Once
Section titled “Commit Once”commit accepts the leaf transaction type. The generated conversion places it
in the application catalog, and the returned receipt names its position in the one
history.
let moved = handle .commit(MoveSymbol { symbol: R1, to: Point::new(10, 45), }) .await .expect("R1 is placed"); println!(" [{}] moved R1 to (10, 45)", moved.position);Read the Positioned State
Section titled “Read the Positioned State”A read takes a short closure and returns an owned At<R>. The value is paired
with the journal position the closure observed, so a caller can compare it
directly with the commit receipt.
/// One line of positioned state, as every read hands it back.async fn read(runtime: &Runtime) -> String { let read = runtime .journal .read(|schematic| { let symbols = schematic.symbols.len(); let nets = schematic.nets.len(); let position = schematic.symbol(R1).position; let name = schematic.net(N1).name.to_string();
format!("{symbols} symbols, {nets} net · R1 at {position} · net 1 is {name}") }) .await .expect("reads are open");
format!("read @{} · {}", read.position, read.value)}Stop Without Hanging
Section titled “Stop Without Hanging”stop consumes the runtime and waits for the writer to finish what it
admitted. It can only do that once the last sender is gone — and every
Journal clone holds one. So drop your handles first:
drop(handle); // and every other Journal clonelet settled = runtime.stop().await; // the position every fold reachedGet that wrong and stop does not fail; it waits, for a sender nobody is going
to release. The diagnostic for it is runtime.notices(), which publishes the
number of live Journal clones when the journal join crosses its grace window.
Read that number as an ownership bug in your own code — a longer grace window
does not fix it. Ship the Runtime works the whole exit
order through.
Run It
Section titled “Run It”From the repository root:
mise run run:minischematicThe transcript begins with the memory-only boot, prints each receipt position, and prints the positioned read after the edits. The same command continues through streams, undo, durable facts, shutdown, and recovery; the first two stops are the path above.
Read Model the State for the rules that keep replay honest, then Assemble and Launch for the complete builder contract. Keep Vocabulary open — several words on this page mean something narrower here than elsewhere.