Wanderland

Input and Output Shapes

A boundary in wanderland-core declares what it reads and what it writes. Those declarations sit on the boundary :name, ... DSL line as input_shape: and output_shape: keyword arguments. The framework reads them three ways: at runtime to enforce the contract, at introspection time to publish the contract, and at documentation time to render it.

This doc covers what each shape declares, what's enforced when, what bypasses declaration, and how to turn strict input access on.

input_shape

The keys a boundary reads from its input that aren't part of the framework envelope. Framework keys (runtime, args, params, headers, config, route, query, path, adapter, identity, config_dir, context) are always available — they're declared once globally in lib/wanderland/envelope.rb and queryable at /inspect/framework-schema/:stage. A boundary that reads only those keys declares an empty shape:

class Echo
  include Wanderland::Boundary

  boundary :echo,
    capabilities: [:echo],
    description: "Echo input params back as result",
    input_shape: {
      "params" => true
    },
    output_shape: {
      "echoed" => true
    }

  def call(input)
    Signal.ok(echoed: input.params["message"])
  end
end

The value true means "any value." Listing "params" => true even though params is a framework key is documentation: this boundary cares about params. The framework-key bypass would let the boundary access input["params"] without declaring it, but the doc surface is richer when reads are named.

A boundary that reads non-framework keys declares them:

boundary :archetype_inspect,
  capabilities: [:inspect],
  description: "Inspect resolved archetype slots",
  input_shape: {
    "archetype" => true,
    "user_config" => true
  }

Without those declarations, strict mode rejects the reads.

Strict input access

Two access patterns through Wanderland::BoundaryInput:

When strict input is off (the default), reading an undeclared, non-framework key falls back to the raw input hash. When strict input is on, reading the same key raises Wanderland::BoundaryInput::UndefinedInputError. The error names the offending boundary so a stack trace points at the wrong reader.

Strict mode is per-runtime, not per-boundary. It catches drift across the whole site at once.

How to enable strict input

Two ways. The explicit kwarg wins over the config field.

The kwarg on Wanderland.boot:

runtime = Wanderland.boot("config.yml", strict_input: true)

The strict_input field in the site's config.yml, optionally read through !Env so the same config can flip strict via environment:

service: my-service
port: 9295

strict_input: !Env { name: WANDERLAND_STRICT_INPUT, default: false }

routes:
  /hello: { method: get, boundary: echo, name: hello }

The !Env resolver records every variable name it consumes; runtime.capture_env subtracts that set from the snapshot allowlist. Setting WANDERLAND_STRICT_INPUT=true flips strict mode without polluting context_seeds counts in tests that assert on env_snapshot size — the var was consumed as engine input, not projected ambient state.

output_shape

The keys a boundary writes in its return Signal's payload, across every branch (happy path and any halt/error/denied paths). Each key is declared as "key" => true:

boundary :health,
  capabilities: [:health],
  description: "Load balancer health ping",
  input_shape: {},
  output_shape: {
    "status" => true,
    "service" => true,
    "timestamp" => true
  }

BoundaryOutput.enforce_and_validate! rejects writes to undeclared keys at runtime — there is no opt-in flag for output enforcement. Declaring output_shape: engages it. Boundaries that haven't declared one get no enforcement.

Framework signal-shape bypass

Halt, error, and denied signals share a payload tuple — {status, error} for halt, {status, error, cause} for error, {status, error, missing} for denied. Those four keys (status, error, cause, missing) are declared once globally in lib/wanderland/shapes/signals.rb and bypass per-boundary output_shape. A boundary that returns Signal.halt(status: 403, error: "denied") doesn't have to redeclare those keys.

The bypass is conservative: it only kicks in for keys with no rule in the boundary's output_shape. A boundary that explicitly declares "status" => true still gets per-boundary enforcement on that key. The framework shape is the floor, not the ceiling. A boundary with business semantics for status (health returns status: "ok" / "degraded") declares it explicitly; the same key bypass also covers the halt-path status: 403 if the boundary ever halts.

Passthrough boundaries

Some boundaries re-emit the prior crossing's payload with an augmentation rather than originating their own result. trace_emit adds _trace, verify_route adds _verify, seal adds _seal, persist_context adds _persisted. Their output keys vary per route — declaring a static shape would falsely reject upstream values. They omit output_shape: and rely on the no-shape default (no enforcement).

A passthrough boundary's intent is documented inline:

boundary :verify_route,
  capabilities: [:verify, :passthrough],
  description: "Per-request shape probe via X-Wanderland-Verify",
  when_shape: { "always" => true },
  input_shape: {}
  # output_shape is intentionally nil — verify_route is a
  # passthrough that re-emits the prior crossing's payload with
  # an `_verify` augmentation. The prior keys vary per route, so
  # an exhaustive output_shape isn't expressible. The :passthrough
  # capability declares the dynamic behavior.

The :passthrough capability is the wire-level signal of the same fact.

Wanderland::Shapes — pluggable shape registry

Wanderland::Shapes is a registry of shape modules. Each module responds to:

Modules register themselves at load time:

module Wanderland
  module Shapes
    module Signals
      HALT   = { "status" => true, "error" => true }.freeze
      ERROR  = { "status" => true, "error" => true, "cause" => true }.freeze
      DENIED = { "status" => true, "error" => true, "missing" => true }.freeze

      module_function

      def framework_keys
        @framework_keys ||= (HALT.keys | ERROR.keys | DENIED.keys).freeze
      end

      def schemas
        [
          { type_addr: Types::HALT,   payload: HALT },
          { type_addr: Types::ERROR,  payload: ERROR },
          { type_addr: Types::DENIED, payload: DENIED }
        ]
      end
    end

    register(Signals)
  end
end

Wanderland::Shapes.framework_keys returns the union across registered modules. BoundaryOutput consults that union for the bypass. New shape modules — for example, a future Wanderland::Shapes::ContextSeeds declaring the standard payload of seed crossings — register the same way and the bypass set grows.

What gets enforced when

Direction When declared When undeclared, framework key When undeclared, non-framework key
Input read validated against rule always passes (framework bypass) passes when strict_input: false; raises UndefinedInputError when strict_input: true
Output write validated against rule passes (framework bypass via Wanderland::Shapes) raises NoMethodError when output_shape is declared on the boundary; passes when output_shape is nil

Input strictness is per-runtime and opt-in. Output strictness is per-boundary and engages the moment a non-nil output_shape is declared.

What this delivers

Every boundary self-describes its full I/O contract. The runtime enforces drift between declaration and behavior — boundaries can't silently read a key they didn't declare, can't silently write a key they didn't promise. /inspect/boundary/:name returns the contract verbatim. The doc generator reads that catalog and renders boundary chapters; the framework chapters come from /inspect/framework-schema (input) and the Wanderland::Shapes registry (output signal forms).

The journal entry for a boundary is now mechanically derivable:

Every field is grounded in code that the framework will reject any deviation from. The journal is the spec.

Slugs

Mechanism Slug
Envelope schema for inputs wanderland-core.envelope-schema
BoundaryInput shape access wanderland-core-introspection.boundary-input
BoundaryOutput shape access wanderland-core-introspection.boundary-output
Framework shape registry wanderland-core.shapes
Shape module for halt/error/denied wanderland-core.shapes-signals