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 set of resolvers registered when wanderland-core boots. Each entry: what the tag does, the argument shapes it accepts, the resolution phase, the cache lifetime, and a worked example.

!Env

Reads a process environment variable at load time.

# Required — halts if WANDERLAND_HTTP_MODE isn't set.
mode: !Env WANDERLAND_HTTP_MODE

# Optional with default.
mode: !Env { name: WANDERLAND_HTTP_MODE, default: live }

# As input to another resolver.
wanderland_environment: !Env { name: WANDERLAND_ENVIRONMENT, default: local }

!Coalesce

Returns the first non-nil layer from an array of candidates. Mirrors SQL's COALESCE.

The motivating case: !UserConfig returns nil when its key isn't declared, but !Merge halts on a non-Hash layer. Wrap the optional !UserConfig lookup with a hash fallback so the merge always sees a Hash:

args: !Merge
  - template_path: !UserConfig template
    parameters:    !UserConfig parameters
  - !Coalesce
    - !UserConfig provision_args   # may be nil
    - {}                            # empty hash fallback

Or for any "first one that's set" pattern across resolver kinds:

region: !Coalesce
  - !Env { name: AWS_REGION }      # honor explicit env first
  - !UserConfig region              # fall back to config
  - us-east-1                       # last-resort default

!Context

Reads from the chain's request context at dispatch time. The context is the array of crossings prior slots in the chain have produced.

Index forms:

Path Reads
last.<field> Most-recent crossing's result.<field>.
events.<n>.<field> Specific event by index — negative indices supported.
for_boundary.<name>.<field> Most-recent crossing whose boundary == <name>, then result.<field>.
<field> Bare-key shorthand for last.<field>.
# Single-step chain — feed the next boundary the previous result.
chain:
  - boundary: cfn_provisioner
    args: { template_path: cfn/vpc.yml }
  - boundary: publish_cfn_outputs
    args:
      stack_name: !Context last.stack_name

# Multi-step chain — name the boundary the value came from.
chain:
  - boundary: aws_assume_role
    args: { role_arn: ... }
  - boundary: cfn_provisioner
    args:
      template_path: cfn/web.yml
      parameters:
        DeployerSession: !Context for_boundary.aws_assume_role.session_name

!SsmLookup

Lives in wanderland-aws-pack. Listed here because it's the canonical example of a :dispatch-phase resolver that reaches through the runtime adapter registry for a live cloud-side read.

chain:
  - boundary: cfn_provisioner
    args:
      template_path: cfn/web.yml
      parameters:
        VpcId:   !SsmLookup /advaita-core-infra/nonprod/vpc/VpcId
        SubnetA: !SsmLookup
          path:    /advaita-core-infra/nonprod/vpc/SubnetA
          default: subnet-fallback
        WebSg:   !SsmLookup
          path: /advaita-core-infra/nonprod/vpc/WebSg
          mode: verify
        TaskRole: !SsmLookup
          path:     /advaita/nonprod/ecs/TaskRole
          severity: warning

The pattern generalizes — pack-authored resolvers can declare phase :dispatch and reach engine.runtime.adapters.lookup(name) to do any cloud read at the dispatcher's pre-boundary pass. Resolvers that want a boot-time check additionally implement the self.pre_flight(arg, registry, runtime) class hook (see Pre-flight in Architecture).

!Environment

Digs a dotted path under the currently-active environment block of the loading document. Lets one config file carry per-environment values without duplicating the whole tree per env.

wanderland_environment: !Env { name: WANDERLAND_ENVIRONMENT, default: local }

environments:
  default:
    region: us-east-1
  local:
    cluster: vivarta-ci
    subnets: [subnet-localdev-aaa, subnet-localdev-bbb]
  aws-dev:
    cluster: jenkins-dev
    subnets: !Env { name: DEV_SUBNETS, default: ["subnet-1"] }

aws:
  AWS::ECS::Service:
    my-svc:
      cluster: !Environment cluster        # active env wins
      region:  !Environment region         # falls back to environments.default
      subnets: !Environment subnets

!Fixture

Loads a YAML fixture file from a configured search path. Used by the scenario harness, by replay tests, and by any boundary that wants to substitute a recorded payload for a live call.

nodes:       !Fixture mocks/oculus-tags
response:    !Fixture mocks/oculus-tags response          # whitespace shorthand for `at: response`
first_tag:   !Fixture { path: mocks/oculus-tags, at: response.tags.0 }
http_replay: !Fixture cassettes/http/abc123

!Merge

Deep-merges an array of maps left-to-right. Right-most wins for scalars and arrays; hashes recurse.

mocks:
  http:
    - !Merge
      - !Fixture cassettes/http/abc123         # full envelope
      - response:
          status: 503                          # twist one field

!Oculus

Fetches from the Oculus API.

node:    !Oculus wanderland-core                       # full node JSON
content: !Oculus wanderland-core/content               # section peek
nodes:   !Oculus { path: nodes, sort: slug, limit: 5 }
fresh:   !Oculus { path: nodes, nocache: true }        # skip cache

!Patch

Path-addressed overrides on top of a base hash. Where !Merge deep-merges structure, !Patch reaches a specific slot by dot path and writes one value, leaving the rest of the base untouched.

mocks:
  ec2:
    - !Patch
      - !Fixture cassettes/ec2/sha                                                  # base
      - response.data.reservations.0.instances.0.state.name: running                # one field
        response.data.reservations.0.instances.0.state.code: 1

  # Hash form — readable when the base is large.
  ec2_alt:
    - !Patch
      base: !Fixture cassettes/ec2/sha
      set:
        response.data.reservations.0.instances.0.state.name: running

!Reference

Within-file pointer. Digs a dotted path into the loading document and inlines the value at the call site.

grids:
  grocery_walk:
    max_ticks: 10
    cells: { ... }

routes:
  /run:      { grid: !Reference grids.grocery_walk }
  /run/step: { grid: !Reference grids.grocery_walk }

!Task

Fetches from the Task API.

current: !Task current
case:    !Task task-abc-123
cases:   !Task { path: list, is_case: true }

!UserConfig

Reads the user's config YAML — the data the boot archetype consumes when wiring a site.

mounts:    !UserConfig storage.mounts
builders:  !UserConfig pipeline.stages.image.builder.type
all:       !UserConfig                       # entire data hash

!Verify

Opt-in wrapper that flags a value for boot-time pattern-based pre-flight. At dispatch the marker resolves to the inner value verbatim — boundaries downstream see the string as if !Verify were not there. At boot, the marker's pre_flight class hook runs the wrapped value through the global Wanderland::Patterns registry; every matching pattern fires its registered handler.

deployer_role_arn: !Verify arn:aws:iam::789905347053:role/advaita-cfn-deployer-ecs-workloads
cfn_service_role_arn: !Verify arn:aws:iam::789905347053:role/advaita-cfn-service-ecs-workloads

Pack-registered patterns include wanderland-aws-pack's IAM role ARN pattern, which collects wrapped role ARNs and batches one iam:GetRole call per unique role at boot.

Architecture

Two-phase resolution

Load. YAML.safe_load runs with Psych domain types registered for every known tag. Each !Tag produces a marker hash:

{ "_tag" => "Oculus", "_arg" => "wanderland-core" }

After load the tree carries plain YAML values mixed with marker hashes. No fetch has happened.

Resolve. TemplateEngine#resolve_tree walks the tree. At each marker it looks up the resolver class in ResolverRegistry, checks the phase gate, consults the cache, calls fetch_with_context, walks the result (so a resolver can return a tree that contains more markers), and writes the result back into the cache.

The recursive walk-then-resolve pattern lets resolvers compose: !Merge of two !Fixture payloads, !Reference to a block that contains !Env, !Environment falling back to a block whose values are themselves markers.

Phases

A resolver declares its phase with the phase :early | :late | :dispatch directive in the class body. Default is :early.

Phase Resolved when Members
:early At boot, while loading site and archetype config !Env, !Environment, !Reference, !UserConfig
:late At boot, second pass — consumes already-resolved early markers as input !Coalesce, !Fixture, !Merge, !Oculus, !Patch, !Task
:dispatch At the dispatcher, just before a slot's args reach its boundary; the engine has the request context and runtime attached !Context, !SsmLookup (in wanderland-aws-pack), !Verify

The engine is constructed with a phase filter: :early, :late, :dispatch, or :all. Markers whose resolver doesn't belong to the active phase pass through unchanged, so a boot pass leaves dispatch-phase markers intact and the dispatcher's pre-boundary pass resolves them.

:all is the convenience for "everything except :dispatch" — dispatch-phase resolvers need a request context, which is never available at boot, so they're explicitly excluded from :all to keep boot-time resolution deterministic.

Pre-flight

A parallel mechanism that uses the same _tag markers the resolver pipeline reads. After boot's resolver phases run, a pair of boot slots (pre_flight_collect, pre_flight_run) walk the loaded config tree and give every resolver a chance to register a boot-time check before any request runs.

The class hook is Resolver.pre_flight(arg, registry, runtime). Default is a noop — resolvers opt in by overriding. Hooks register callbacks on runtime.pre_flight (a Wanderland::PreFlightRegistry) via register_once(key). Many markers in a config collapse to one callback per key, so the actual cloud-side check fires once with batched inputs.

Callbacks return signals. Types::PRE_FLIGHT_FAILED (under STOP_PREFIX) halts boot with the union of error messages. Types::PRE_FLIGHT_WARNING (under PASS_PREFIX) is observational — collected and reported, boot continues.

Pattern-based participation runs alongside the resolver hook. Wanderland::Patterns.register(regex) { |value, registry, runtime| ... } lets packs declare "any string matching this regex is mine — pre-flight it." The !Verify resolver is the opt-in surface — its pre_flight hook runs the wrapped value through the Patterns registry. Strings not wrapped in !Verify are never pattern-scanned, so an IAM ARN buried in a CFN template body doesn't get checked against the caller's permissions unless the operator explicitly elects to verify it.

Worked example — !SsmLookup registers a callback that calls ssm:GetParameters once with every collected path; wanderland-aws-pack registers an IAM role ARN pattern whose callback batches iam:GetRole over every !Verify-wrapped role.

Resolver interface

class Wanderland::Resolvers::Oculus < Wanderland::Resolver
  tag "Oculus"
  ttl 60
  phase :late

  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 converts !Tag syntax into a marker hash. ttl sets the default cache duration in seconds. phase declares early/late.

Resolvers that need access to the loading tree or to the engine's breadcrumb state override fetch_with_context(arg, engine) instead 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 HTTP 404