Registry and call convention for tripartite boundaries. The bridge between engine-specific logic and the shared scenario/game infrastructure.
The scenario system needs to call engine operations without knowing what they are. The crossing engine has walk, collapse, compile. The lantern engine has route-match, middleware-walk, component-render. The game engine, the test runner, and the Emacs integration all need to call these — but they shouldn't be coupled to any specific engine.
A registry. For operation X, call block Y.
At boot time, each engine registers its boundaries:
# Simple proc registration
Wanderland::Boundary.register(:route_match) do |input|
# input is the scenario's `input:` hash
# return value is compared against `expected:`
match_route(input[:method], input[:path], input[:routes])
end
# Class registration (auto-registers via the `boundary` macro)
class RouteMatch
include Wanderland::Boundary
boundary :route_match
def call(input)
trace("boundary:route-match", meta: { path: input[:path] }) do
# routing: walk route table
# grammar: pattern + method check
# execution: emit matched config + params
end
end
end
The scenario/game calls execute — it doesn't know or care what's behind the name:
# Scenario#run does this:
result = Wanderland::Boundary.execute(:route_match, input)
# Which does:
handler = @registry
if handler.is_a?(Class)
handler.new.call(input) # instantiate, call
elsif handler.respond_to?(:call)
handler.call(input) # call proc/lambda directly
end
Wanderland::Boundary.registered # [:route_match, :middleware_walk, ...]
Wanderland::Boundary.lookup(:name) # the handler (class or proc)
Wanderland::Boundary.registry # full map (dup)
Wanderland::Boundary.reset! # clear (for testing)
Including Wanderland::Boundary gives you Wanderland::Logging and Wanderland::Tracing for free. The convention is that each boundary's call method has three phases:
| Phase | What it does | Analogy |
|---|---|---|
| Routing | Collect potential — walk, lookup, gather | Linker resolving symbols |
| Grammar | Constrain what crosses — filter, validate, deny | Type checker, shape matcher |
| Execution | Burn to output — render, write, emit | Code generation, the gradient |
These aren't enforced by the framework. They're a convention that makes boundaries composable and testable. Each phase can be tested independently if needed, but the boundary is the unit of composition.
The YAML scenario says:
operation: route_match
input:
method: GET
path: /node/sprout-api
routes: [...]
expected:
matched: true
params:
slug: sprout-api
Scenario#run calls Boundary.execute(:route_match, input). The result is compared against expected: using ShapeMatcher. Blanks are paths into the result that players fill in during the game.
The scenario doesn't import RouteMatch. It doesn't know what language it's written in. It just knows: this name maps to a callable, and the callable's output should match this shape.
Boundary.execute no longer returns a raw result. It returns a crossing record — the result wrapped with full provenance:
{
"boundary" => "resolve_repo",
"identity" => "dark-lantern",
"from" => nil, # caller identity if present
"requirements" => ["read"],
"capabilities" => ["repo_resolution"],
"result" => { "repo_path" => "..." },
"at" => "2026-04-02T...",
"signature" => "base64..." # if identity has a PKI key
}
The boundary owns the record content. The engine validates shape and appends to Context. If signed, Context verifies the signature before storing — a tampered crossing never enters the context.
Every boundary declares identity, requirements, and capabilities:
boundary :deploy,
requirements: [:deploy, :sign],
capabilities: [:deployment],
identity: deploy_identity,
description: "Deploy to target env"
Boundary.manifest returns all registrations with effective metadata for introspection.