Plantangenet: From Schema to Running World
Introduction
This is written for new engineers joining the system.
This document explains how data moves through Plantangenet—from static YAML files to live, running simulation.
If the Regions & Areas doc explains what exists, this explains how it comes to life.
Pipeline (one line):
Files → Graph → Objects → Simulation → Observability
Each stage removes ambiguity and adds commitment. By the end, the system is executing—not describing—reality.
Two important distinctions before diving in:
- Scenario — a content definition: YAML files in plantangenet-core that describe areas, regions, creatures, quests, and scripts. Scenarios are data, not runtime state.
- Realm — a runtime instance: the live execution context a runner hosts when it loads a scenario. Realm identity (who can access it, what roles exist, readiness state) is managed by the runner, not by content files.
Big Picture
Five stages, each with a single job:
- Schemas — what is allowed
- Forge — what it becomes (deterministic)
- Registry — where it is in its lifecycle
- Services — what happens over time
- KNAT — what you can see/control
Nothing should leak between stages. That separation is what keeps the system predictable.
We’ll walk them in order.
Four-Tier Pattern: Forgeable + Atomic
+-----------+
| ManagedItem| (atom crate: id, type, to_atom, from_atom)
+-----------+
|
+-------------------+
| ItemRegistry<T> | (atom crate: sync HashMap CRUD + iter)
+-------------------+
|
+-----------+ +---------------------+
| ForgeRecord<T> | | SharedRegistry<T> |
| (lifecycle) | | (simple CRUD) |
+-----------+ +---------------------+
| |
| |
+----------------------------+ +---------------------+
| ForgeRegistry<T> | | SimpleRegistry<T> |
| Pending->Generating->... | | (animal species) |
| (runner: per-kind) | | (runner: in-memory) |
+----------------------------+ +---------------------+
| |
(optional redb) (no persistence)
|
+---------------------+
| RealmStore<T> | (realm identity types only)
| ForgeRegistry + redb| flush_to_redb() / load_from_redb()
+---------------------+
1. Schemas — The Source of Truth
Schemas define what can exist. If it’s not expressible in a schema, it doesn’t exist in the system.
What a Schema Does
- Defines structure
- Validates data
- Assigns identity
All YAML must pass schema validation before entering the pipeline.
Standard Format
apiVersion: net.plantange/v1
kind: simulation.area
apiVersion— versioned contractkind— routing key to the correct handler
Identity & Metadata
Each document includes meta with:
titleuuid
This identity persists through compile, registry, and runtime.
Lifecycle (Schemas)
- Canonical — wired to runtime
- Migrating — being shaped
- Reference — defined, not active
- Legacy — deprecated
2. Forge — Deterministic Compilation
Forge turns validated data into concrete runtime artifacts. It is domain-agnostic (no knowledge of Areas/Regions).
Inputs → Outputs
- Input: parsed spec + seed
- Output: compiled artifacts
Graph Model
Everything becomes a graph:
- Nodes = entities
- Edges = dependencies
This encodes what depends on what instead of how to run it.
Generators
Deterministic transforms over the graph:
- derive values (e.g., from seed)
- expand simple specs into richer structures
- fill defaults
Determinism
Same input + same seed = same output
If results differ, it’s a bug.
3. Registry — Lifecycle Management
After compilation, objects enter the Registry, which tracks readiness.
Lifecycle (Objects)
- Pending → Generating → Compiled → Registered → Active
Forward-only. No backtracking.
What’s Stored
- Compiled artifacts
- Current state
- Identity (
forge_id)
Why It Exists
- Services don’t guess readiness
- Fewer race conditions
- Easier debugging
Clear boundary: preparing data vs running data.
4. Services — The Simulation Engine
Services execute behavior over time. Objects don’t run themselves.
Core idea: Services run in a structured loop every frame.
Frame Phases
provision()— one-time async setup
Per frame:
on_frame_init— prepare/advance stateon_frame_active— core simulationon_frame_aggregation— finalize updateson_frame_cleanup— wrap-up
All services follow the same phase order to keep the world consistent.
Responsibilities
- Advance lifecycle states
- Execute logic
- Apply updates deterministically
Execution order is controlled via priorities.
5. Kind Handlers — The Bridge
Kind Handlers connect schemas to runtime behavior.
Given this YAML, what do we do with it?
Responsibilities
- Parse spec
- Compile via Forge
- Create service
- Project to KNAT
Mental Model
- Schema = shape
- Handler = meaning
Without handlers, you get valid graphs that do nothing.
6. Realm — Runtime Identity and Readiness
A realm is the live instance a runner is currently hosting. It is not the scenario YAML; it is the runtime context that owns identity, readiness, and access control.
Lifecycle States
- absent — runner is alive, no realm loaded yet
- loading — realm initialization in progress
- ready — realm loaded and serving
- degraded — runner alive, realm partially unavailable
- failed — realm load failed terminally
These states are canonical. Every part of the system (runner, Waldo, ops tooling) uses the same vocabulary.
The /info Endpoint
The runner exposes GET /info with no authentication required. It is the first thing
a client queries before opening any connection:
{
"name": "My Server",
"version": "0.2.0",
"realm": "downbeat",
"lifecycle": "ready",
"ready": true,
"dev_mode": false,
"status": "running"
}
lifecycle is the canonical realm state. ready is a convenience boolean (true only
when lifecycle is ready). Clients use these to distinguish "server unreachable" from
"server starting" from "realm ready."
Realm Identity Bootstrap
Realm identity (principals, roles, policies) is seeded idempotently from a bootstrap
config on each startup. If no principals exist in production mode, the realm refuses to
mark itself ready — a locked-out realm cannot go live silently. Identity is stored in
realm.redb via RealmStore<T> (see Registry section above).
7. KNAT — Observability Layer
KNAT exposes the running system to tools and operators.
What You Can Do
- Read state
- Invoke actions
- Subscribe to signals
Model
Everything is a node in a tree:
- path
- type (Integer, Enum, Object, Signal)
- capabilities (read/write/invoke/subscribe)
Example
/runner/kinds/simulation.area/area_123/state
/runner/kinds/simulation.area/area_123/environment/field_count
/runner/kinds/simulation.area/area_123/signals/spawn
Purpose
- Internal system stays complex and deterministic
- External surface stays simple and stable
KNAT is the control surface.
8. Client Discovery — Finding a Realm
Waldo (the desktop client) finds runner instances on the LAN without configuration.
Runner Side: mDNS Announcement
On startup, the runner registers a DNS-SD service:
- type:
_plantangenet._tcp.local. - TXT record:
name,version - port: the HTTP bind port
On SIGTERM, the runner sends a DNS goodbye packet (TTL=0) for immediate removal.
Waldo Side: Discovery and Connection
Waldo runs three background services in its Rust (Tauri) layer:
- DiscoveryService — browses for
_plantangenet._tcp.local., emitswaldo://discoveryevents, prefers LAN (RFC 1918) addresses over Tailscale/CGNAT - ProbeService — periodically GETs
/infoon each known server, emitswaldo://server-statusevents - ConnectionManager — owns the active connection:
Idle → Connecting → Connected → Reconnecting → Failed, emitswaldo://connectionon every state transition
The frontend (TypeScript panels) subscribes to these events and renders. It never opens network connections directly; all network IO lives in the Rust layer.
IP Address Priority
When a server announces multiple addresses, Waldo selects in this order:
- LAN (RFC 1918: 10.x, 172.16–31.x, 192.168.x)
- Other routable unicast
- CGNAT (100.64/10) — Tailscale range, used as fallback
- Link-local (169.254.x) — last resort
End-to-End Flow
- Write YAML (scenario content in plantangenet-core)
- Validate via schema
- Compile with Forge
- Track in Registry (ForgeRegistry, SimpleRegistry, or RealmStore depending on type)
- Run via Services (each frame)
- Realm marks itself ready when loaded and bootstrapped
- Runner announces on LAN via mDNS; exposes
/info - Waldo discovers runner, probes
/info, opens connection - Observe/control via KNAT
Each stage builds on guarantees from the previous one.
Concrete Example
A greenhouse area:
- Define YAML (
kind: simulation.area) with temp/humidity/light - Schema validates
- Forge builds fields (heat, moisture) + affordances (grow, tend)
- Registry advances to Active
- Service updates per tick (temp drifts, humidity stabilizes)
- KNAT exposes current values and signals
Over time:
- Conditions change
- Inputs affect behavior
- Observers can react
Same pattern applies everywhere.
Mental Model
- Schemas → possibility (what can exist)
- Forge → structure (deterministic compilation)
- Registry → readiness (lifecycle tracking, three tiers)
- Services → change (frame loop execution)
- Realm → identity (runtime instance, who can access it)
- KNAT → visibility (control surface)
- Discovery → presence (clients find running realms)
Short form:
Define → Build → Track → Run → Host → Observe → Connect
Two easy mistakes:
- Confusing scenario (content YAML) with realm (live runtime instance). Scenario is the blueprint; realm is the building.
- Mixing ForgeRegistry (lifecycle-tracked kinds) with SimpleRegistry (reference data) or RealmStore (durable identity). Wrong tier means either missing lifecycle guarantees or unnecessary overhead.
If you're confused, you're probably mixing stages or tiers.
Where to Go Next
- Trace one YAML through the pipeline
- Read a Kind Handler
- Watch KNAT while the system runs
If it still feels unclear after that, the abstraction likely needs improvement—not you.