Skip to content

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:

  1. Schemas — what is allowed
  2. Forge — what it becomes (deterministic)
  3. Registry — where it is in its lifecycle
  4. Services — what happens over time
  5. 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 contract
  • kind — routing key to the correct handler

Identity & Metadata

Each document includes meta with:

  • title
  • uuid

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 state
  • on_frame_active — core simulation
  • on_frame_aggregation — finalize updates
  • on_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

  1. Parse spec
  2. Compile via Forge
  3. Create service
  4. 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., emits waldo://discovery events, prefers LAN (RFC 1918) addresses over Tailscale/CGNAT
  • ProbeService — periodically GETs /info on each known server, emits waldo://server-status events
  • ConnectionManager — owns the active connection: Idle → Connecting → Connected → Reconnecting → Failed, emits waldo://connection on 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:

  1. LAN (RFC 1918: 10.x, 172.16–31.x, 192.168.x)
  2. Other routable unicast
  3. CGNAT (100.64/10) — Tailscale range, used as fallback
  4. Link-local (169.254.x) — last resort

End-to-End Flow

  1. Write YAML (scenario content in plantangenet-core)
  2. Validate via schema
  3. Compile with Forge
  4. Track in Registry (ForgeRegistry, SimpleRegistry, or RealmStore depending on type)
  5. Run via Services (each frame)
  6. Realm marks itself ready when loaded and bootstrapped
  7. Runner announces on LAN via mDNS; exposes /info
  8. Waldo discovers runner, probes /info, opens connection
  9. Observe/control via KNAT

Each stage builds on guarantees from the previous one.


Concrete Example

A greenhouse area:

  1. Define YAML (kind: simulation.area) with temp/humidity/light
  2. Schema validates
  3. Forge builds fields (heat, moisture) + affordances (grow, tend)
  4. Registry advances to Active
  5. Service updates per tick (temp drifts, humidity stabilizes)
  6. 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.