Wanderland

Wanderland Core: Boundary

Registry and call convention for tripartite boundaries. The bridge between engine-specific logic and the shared scenario/game infrastructure.

The Problem

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.

The Solution

A registry. For operation X, call block Y.

Registration

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

Execution

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

Introspection

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)

Tripartite Convention

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.

How Scenarios Use It

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.

Crossing Records (2026-04-02)

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.

Registration Metadata

Every boundary declares identity, requirements, and capabilities:

Boundary.manifest returns all registrations with effective metadata for introspection.