Wanderland

Wanderland Core Template Engine

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.

Resolver Reference

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.

Route

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).

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

Env

Coalesce

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.

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

Dig

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.

Listed for completeness. It is the mechanism by which a deep path like !UserConfig tags.system resolves when tags is itself an execute value.

Architecture

Load and resolve

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:

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.

Phases

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:

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.

Resolver interface

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:

Cache

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.

Error model

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 configuration

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.

Integration with other syntaxes

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.

Adding a new resolver

Site Audit

wanderland.dev

oculus-view: fence: Fence execute bit is disabled. Use force=true to override.