A YAML tag-based resolution engine. Custom tags (!Env, !Reference, !Oculus, !Task, !Fixture, !Merge, !Patch, !Environment, !UserConfig) become marker hashes at parse time and resolve through a registry-dispatched, phase-gated, memoizing-cache pipeline. The YAML is a function from environment + fixtures + remote data to a fully-realized config tree.
Lives in wanderland-core. Available to every engine, archetype, and scenario harness in the project.
The resolvers registered when wanderland-core boots. The table lists every resolver with its phase, its cache lifetime, and what it resolves. The subsections below carry the argument shapes and worked examples.
Legend — Phase: compile (:early), link (:link), execute (:dispatch). TTL: -1 never expires within the cache lifetime, 0 skips the cache, and a positive integer is a lifetime in seconds.
| Tag | Phase | TTL | Resolves |
|---|---|---|---|
!Coalesce |
compile | -1 | Returns the first concrete non-nil layer. Defers over an unfired marker. |
!ConfigPath |
compile | -1 | Returns the path of the loaded site config file. |
!Env |
compile | -1 | Reads an environment variable, with an optional default. |
!Environment |
compile | 0 | Reads a value from the active environment block. |
!Fixture |
compile | -1 | Loads a file from disk into the tree. |
!Merge |
compile | -1 | Merges a list of map layers. |
!Oculus |
compile | 60 | Pulls a node, or one section of a node, from the Oculus graph. |
!Patch |
compile | -1 | Applies a structural patch to a map. |
!Reference |
compile | 0 | Reads a value at another path in the same tree. |
!UserConfig |
compile | -1 | Reads a value from the route-scoped config. |
!Route |
link | -1 | Reads a value the route supplied under its args: map. |
!Context |
execute | 0 | Reads a value from the chain's request context. |
!Derive |
execute | -1 | Runs a derive flow and returns the branch a condition selects. |
!Dig |
execute | -1 | Completes a deep path across a later-phase marker. Internal. |
!MappedList |
execute | 0 | Maps each item of a list through a per-item format. |
!Now |
execute | -1 | Formats the current time with a strftime pattern. |
!Param |
execute | 0 | Reads a request parameter or a path capture. |
!Slug |
execute | -1 | Reduces a string to a DNS-label-safe token. |
!Split |
execute | -1 | Splits a string on a delimiter. |
!Sub |
execute | 0 | Fills {name} placeholders in a format string from a values map. |
!Task |
execute | 0 | Reads a detective case from the task API. |
!Verify |
execute | 0 | Marks a value for boot-time pre-flight, and returns the value. |
!SsmLookup is a wanderland-aws-pack resolver. It reads an SSM parameter at the execute phase.
Reads a value the current route supplied under its args: map. A pipeline declares the holes a route fills with !Route <key>. boot_mount_archetype sets Route.data to the route's args before it resolves that route's archetype, so each !Route marker reads route.args.<key>. The args: map keeps these inputs clear of the framework route keys (pipeline, method, name).
:link. A !Route value is static once the route mounts. It binds and bakes at the link pass, the one pass that holds Route.data. A compile pass runs over the whole config before any route mounts, so a compile !Route would bake to nil. The mount restores Route.data to empty after the link pass, so an execute !Route would read an empty table.-1 — route args are static once mounted.String — a dotted path into the route's args (system, template_path).Hash — path: (required).HashUtils.dig_resolving, so a !Route can sit behind a dig or hold a nested value.nil. A pre_flight check turns a required missing hole into a boot halt.pipelines:
core-infra:
stages:
deploy:
provisioner:
args: { template_path: !Route template_path }
metadata:
tags: { system: !Route system }
routes:
/core-infra-route53-bddev:
pipeline: core-infra
args:
system: route53
template_path: stacks/r53.yml
Returns the first concrete non-nil layer from an array of candidates. It follows SQL COALESCE, and it adds one rule for the phase model: it defers over a layer that is still a marker.
:early (compile). It runs at each pass and decides only when it can.-1.Array of layers. A non-Array argument wraps to a one-item array.!Coalesce defers. It returns its own marker for a later pass, and resolve_marker holds that marker rather than resolve it again.false, 0, and "" are present values.The deferral keeps a value fallback correct across phases. A !Route hole with an !Environment fallback binds at the link pass, past the earlier compile pass:
environment: !Coalesce
- !Route args.environment
- !Environment environment
The other common case is a hash fallback that keeps !Merge total. !Merge halts on a nil layer, so wrap an optional !UserConfig with an empty-hash fallback:
args: !Merge
- template_path: !UserConfig template
parameters: !UserConfig parameters
- !Coalesce
- !UserConfig provision_args # nil when the key is absent
- {} # empty hash fallback
Internal. The engine mints !Dig on its own. It mints a Dig marker (via HashUtils.dig_resolving) when a path walk crosses a marker that cannot resolve at the current phase. An example is !UserConfig a.b.c where a is a !Context that resolves only at execute. The marker carries the partly-resolved base and the rest of the path. It completes the dig once the base goes concrete at its own phase.
:dispatch (execute). A base that survives the mint is a later-phase marker by construction. The execute pass holds the Dig as a marker through the earlier passes and fires it when the base resolves. A dig across a compile marker like !Merge mints no Dig at all, because dig_resolving resolves it inline.-1.Listed for completeness. It is the mechanism by which a deep path like !UserConfig tags.system resolves when tags is itself an execute value.
Resolution has two steps: load, then resolve.
Load. YAML.safe_load runs with Psych domain types registered for every known tag. Each !Tag becomes a marker hash:
{ "_tag" => "Oculus", "_arg" => "wanderland-core" }
After load the tree holds plain YAML values mixed with marker hashes. No fetch has run.
Resolve. TemplateEngine#resolve_tree walks the tree. At each marker it finds the resolver class in ResolverRegistry and checks the phase gate. It reads the cache, calls fetch_with_context, walks the result, and writes the result to the cache. The walk of the result lets a resolver return a tree that holds more markers.
The unwind resolves a marker from the bottom up. resolve_tree walks depth-first. It resolves a marker's arguments before the marker itself. An inner tag reaches its value before the outer tag reads it. The execute pass fires every marker, so a marker that reaches a read resolves at that read.
A !Sub over two !Context reads shows the bottom-up order:
image_tag: !Sub
format: "{version}-{sha}"
values:
version: !Context for_boundary.chain.version
sha: !Context for_boundary.chain.build.short_sha
At the read the engine resolves each !Context against the request context first. Then !Sub fills the format string from the two concrete values. The composed tag holds two resolved strings.
A !Coalesce over an unfired marker shows the deferral. !Coalesce returns the first concrete non-nil layer. A layer that is still a marker is a resolver whose phase has not fired. !Coalesce walks its layers in priority order:
!Coalesce defers. It returns its own marker, and a later pass retries.resolve_marker reads a returned same-tag marker as a deferral. It holds the marker for the next pass. So a value fallback lands at the right pass:
environment: !Coalesce
- !Route args.environment # link phase: nil when the route omits the arg
- !Environment environment # compile phase: the deployment's environment
At the compile pass !Route has not fired. !Coalesce defers and keeps both layers. At the link pass !Route resolves to nil, and !Coalesce returns the !Environment value.
The resolve step runs as a toolchain of three phases: compile, link, and execute. A resolver declares its phase with phase :early | :link | :dispatch in the class body. The default is :early. The three phases nest — compile is a subset of link, and link is a subset of execute.
| Phase | Reads at | Role | Members |
|---|---|---|---|
:early (compile) |
Config.load, and archetype mount |
Structural composition. The pass builds the config tree and reads no route data. | !Coalesce, !ConfigPath, !Env, !Environment, !Fixture, !Merge, !Oculus, !Patch, !Reference, !UserConfig |
:link (link) |
Archetype mount, where the pass holds Route.data |
Per-route symbol bind. The pass resolves the value holes a route supplies under its args: map. It bakes each hole into the compiled chain. |
!Route |
:dispatch (execute) |
The dispatcher, before a slot's args reach its boundary | Dynamic reads. The pass holds the request context and the runtime, so it resolves live values. | !Context, !Derive, !Dig, !MappedList, !Now, !Param, !Slug, !Split, !Sub, !Task, !Verify |
The engine takes a pass phase, and a marker fires only when its resolver phase falls at or before the pass:
Route.data.Config.load runs one compile pass over the whole site config. boot_mount_archetype runs a link pass over each route's archetype, where the pass holds Route.data and UserConfig.data. The dispatcher runs an execute pass over a slot's args before the boundary reads them.
A !Route value is static once the route mounts. It binds and bakes at the link pass. The mount restores Route.data to empty after the pass, so an execute read would find Route.data empty. A resolver whose value is a live read — the request context, a request parameter, a remote lookup — belongs at execute.
class Wanderland::Resolvers::Oculus < Wanderland::Resolver
tag "Oculus"
ttl 60
phase :early
def fetch(arg)
# arg is a String or Hash from YAML.
# Return any Ruby value — Hash, Array, scalar.
end
end
tag registers the class with ResolverRegistry and installs a Psych domain type that turns !Tag syntax into a marker hash. ttl sets the default cache duration in seconds. phase declares compile (:early), link (:link), or execute (:dispatch).
Resolvers that need the loading tree or the engine breadcrumb state override fetch_with_context(arg, engine) in place of fetch. The engine, passed as the second argument, exposes:
engine.root — the top-level loaded tree.engine.resolve_subtree(value) — resolve a captured value.engine.reference_breadcrumbs — the shared cycle-detection stack, used by !Reference and !Environment.ResolverCache keys entries on "tag:canonical_arg". The canonical arg drops ttl and nocache keys and sorts hash arguments so two YAML spellings of the same call share a cache slot.
| TTL value | Meaning |
|---|---|
nil or 0 |
Ephemeral — not stored. Every load fetches anew. |
| Positive integer | Seconds. Entry expires at Time.now + ttl. |
-1 |
Forever — never expires within the cache's lifetime. |
nocache: true on the hash form of an argument bypasses both read and write.
Cache stats (hits, misses, size, keys) ride alongside the resolved tree for scenario verification via TemplateEngine#resolve_with_stats.
Resolvers raise subclasses of Wanderland::Resolver::ResolutionError when a config-level mistake makes a value unresolvable: missing reference path, circular reference, missing required env var, missing environment block. The engine catches the base class and substitutes a marker envelope at the failed node:
{ "_resolution_error" => true, "error" => "<message>" }
Consumer boundaries scan the resolved tree for _resolution_error: true and surface it as a halt response. Missing fixtures and HTTP failures use the looser _error envelope so the consumer can choose between halting and substituting a fallback.
Site YAML wires the API-backed resolvers (!Oculus, !Task) to their providers:
providers:
oculus:
backend: https://i.loss.dev
auth: { user: x, pass: y }
tasks:
backend: https://i.loss.dev
auth: { user: x, pass: y }
The boot archetype maps these into class-level config on Resolvers::Oculus and Resolvers::Task before any late-phase pass runs.
| Syntax | Mechanism | Scope |
|---|---|---|
!Tag arg |
Resolver + cache | External data fetch, file load, cross-tree reference |
$namespace.path |
Variable resolve | Walk the resolved tree, no resolver dispatch |
{attribute} |
Attribute resolve | Iteration-local — bind a single record's field |
!Tag brings data in. $ and {} reference data already in the tree. Each tier addresses its scope.
lib/wanderland/resolvers/<name>.rb under Wanderland::Resolvers.Wanderland::Resolver.tag "<Name>", ttl <seconds>, and phase :early | :link | :dispatch.fetch(arg), or fetch_with_context(arg, engine) when the resolver needs engine.root, engine.resolve_subtree, or the breadcrumb trail.ResolutionError subclass for any config-level mistake the resolver can detect, so the engine surfaces it through the _resolution_error envelope.lib/wanderland/resolvers.rb so the Psych domain type registers at load.spec/scenarios/template_engine/ that exercises the happy path, each argument shape, and each halt condition.@oculus:wanderland-core-template-engine annotation in the file header, and a new row in the resolver table above.wanderland.dev