The declared shape of a boundary's input. lib/wanderland/envelope.rb catalogues every framework key in every stage of input — what type it is, which writer puts it there, whether it's optional. The same catalogue is queryable at /inspect/framework-schema, enforced at write time by Envelope.build, and consulted by BoundaryInput to decide which keys bypass per-boundary input_shape validation.
A boundary's input is the per-call envelope built by the framework before #call(input) runs. Three stages, each with its own writer:
:request — built by Dispatch.build_input. The chain dispatcher adds context per slot; args per slot when the slot declares them.:boot — built by Runtime#boot_from_config while walking the archetype.:scenario — built by Wanderland::Scenario#run.Each stage's keys are declared as Entry records.
Entry = Struct.new(
:key, :type, :stage, :written_by, :optional, :description,
keyword_init: true
)
Fields:
key — the hash key as it appears in input.type — human-readable type ("Wanderland::Runtime", "Hash", "String").stage — :request, :boot, or :scenario.written_by — the file:method that populates the key. nil for keys callers populate themselves (identity).optional — true when the writer can skip the key.description — one-line role.:requestEleven keys. Built by Dispatch.build_input; context and args added by the chain dispatcher.
| key | type | written_by | optional |
|---|---|---|---|
runtime |
Wanderland::Runtime |
Dispatch.build_input |
no |
config |
Hash |
Dispatch.build_input |
no |
params |
Hash |
Dispatch.build_input |
no |
query |
Hash |
Dispatch.build_input |
no |
headers |
Hash |
Dispatch.build_input |
no |
path |
String |
Dispatch.build_input |
no |
route |
Hash |
Dispatch.build_input |
no |
adapter |
String |
Dispatch.invoke |
yes |
context |
Wanderland::Context |
Dispatcher::Chain#execute |
yes |
args |
Hash |
Dispatcher::Chain#execute |
yes |
identity |
caller-provided | — | yes |
:bootFour keys. Built by Runtime#boot_from_config per archetype slot.
| key | type | written_by | optional |
|---|---|---|---|
runtime |
Wanderland::Runtime |
Runtime#boot_from_config |
no |
config_dir |
String |
Runtime#boot_from_config |
no |
context |
Wanderland::Context |
Runtime#boot_from_config |
no |
args |
Hash |
Runtime#boot_from_config |
no |
:scenarioFour keys. Built by Wanderland::Scenario#run.
| key | type | written_by | optional |
|---|---|---|---|
runtime |
Wanderland::Runtime |
Wanderland::Scenario#run |
no |
context |
Wanderland::Context |
Wanderland::Scenario#run |
no |
params |
Hash |
Wanderland::Scenario#run |
yes |
headers |
Hash |
Wanderland::Scenario#run |
yes |
Wanderland::Envelope.schema(:request) # => [Entry, Entry, ...]
Wanderland::Envelope.keys(:request) # => ["runtime", "config", ...]
Wanderland::Envelope.required(:request) # => keys minus optional
Wanderland::Envelope.framework_keys # => union across all stages
Wanderland::Envelope.entry(:request, "runtime")
Wanderland::Envelope.to_h # => stages hash for JSON
framework_keys is the source BoundaryInput reads when deciding which keys bypass the per-boundary input_shape check.
Envelope.buildConstructs an envelope for a stage. The block yields a Builder; set and build enforce the schema.
input = Wanderland::Envelope.build(:request) do |env|
env.set("runtime", runtime)
env.set("config", runtime.config&.domain || {})
env.set("params", params)
env.set("query", params)
env.set("headers", headers)
env.set("path", path || route[:path])
env.set("route", route[:spec] || {})
env.set("adapter", adapter.to_s) if adapter
end
Two failure modes raise Envelope::SchemaError:
set rejects keys not in the stage's schema. Error names the legal key list.build rejects envelopes without every non-optional key. Error names the missing list./inspect/framework-schemaTwo routes mounted in Engine::CORE_ROUTES.
GET /inspect/framework-schemaFull catalogue, grouped by stage.
{
"stages": {
"request": [{ "key": "runtime", "type": "Wanderland::Runtime", ... }, ...],
"boot": [...],
"scenario": [...]
}
}
GET /inspect/framework-schema/:stageOne stage's entries.
{
"stage": "request",
"entries": [
{
"key": "runtime",
"type": "Wanderland::Runtime",
"stage": "request",
"written_by": "Dispatch.build_input",
"optional": null,
"description": "Booted runtime — config, registries, engine."
},
...
]
}
Unknown stage returns 404 with the legal list:
{
"error": "unknown stage: \"frob\"",
"available": ["request", "boot", "scenario"]
}
:early tag resolution in user configConfig.load runs an :early-phase TemplateEngine pass over user config. :early tags (!Env, !UserConfig) resolve at config-load time. :late tags (!Fixture, !Oculus, !Task) survive untouched and resolve at request time when boundaries reach them.
strict_input: !Env { name: WANDERLAND_STRICT_INPUT, default: false }
strict_inputWanderland::BoundaryInput raises Wanderland::BoundaryInput::UndefinedInputError on access to undeclared non-framework keys when runtime.strict_input? is true. Default false; undeclared keys fall back to the raw hash.
Three sources, in precedence order:
Wanderland.boot("config.yml", strict_input: true)
strict_input: true
strict_input: !Env { name: WANDERLAND_STRICT_INPUT, default: false }
The kwarg on Wanderland.boot wins. Config falls back. Default off.
The flag is read once at request entry by Wanderland::Boundary.execute_registration and rides per invocation in BoundaryInput.
input_shape and output_shapeEach boundary declares its read surface and write surface in its registration.
boundary :echo,
input_shape: { "params" => true },
output_shape: { "echoed" => true },
...
Three shapes for the chain that runs every request.
Both shapes declared. The boundary owns the read surface and the write surface.
boundary :echo,
input_shape: { "params" => true },
output_shape: { "echoed" => true }
boundary :json_formatter,
input_shape: { "target" => true },
output_shape: { "body" => true, "content_type" => true }
echo and the four formatters (json_formatter, html_formatter, markdown_formatter, text_formatter) declare both.
input_shape: {} (only framework keys read). output_shape: nil because the boundary re-emits the prior crossing's payload with one augmentation key, and prior keys vary per route.
boundary :verify_route,
capabilities: [:verify, :passthrough],
input_shape: {}
boundary :trace_emit,
capabilities: [:trace, :passthrough],
input_shape: {}
verify_route adds _verify. trace_emit adds _trace. format dispatches to a formatter and stamps body, content_type, formatter_used. seal adds _seal.
The :passthrough capability flag declares the dynamic-output contract.
input_shape: {}. output_shape declares both success and halt payload keys.
boundary :enforce_denials,
input_shape: {},
output_shape: {
"ok" => true,
"status" => true,
"error" => true,
"failed_requirement" => true
}
Flow control covers the halt mechanics.
Each key => true in a shape is the lazy floor: present, any value. The same field accepts the full ShapeMatcher vocabulary — matches, contains, keys, gte/lte, count, first, any, not, empty, prefix, plus the nested-hash form. Tightening:
input_shape: { "params" => true }
input_shape: { "params" => { "message" => true } }
input_shape: { "params" => { "message" => { matches: ".+" } } }
The introspection surface then carries enough to synthesise JSON Schema for two artefacts:
config.yml — the archetype's slot sequence with each slot's args: resolved through !UserConfig thunks composes into a schema for the user's config file.input_shape translates directly to a JSON Schema fragment per route.The synthesiser reads through /inspect/framework-schema and /inspect/boundary/:name. Both endpoints expose the full manifest as JSON; a generator fetches once and emits .json files an editor can validate against.
lib/wanderland/envelope.rb — schema declarations, Builder, module surfacelib/wanderland/boundary_input.rb — strict mode, framework-key fast pathlib/wanderland/boundary_output.rb — output_shape enforcementlib/wanderland/boundaries/introspection/inspect_framework_schema.rb — endpointlib/wanderland/boundaries/{verify,debug,formatters,identity}/*.rb — chain boundary shape declarationswanderland.dev