Wanderland

Wanderland Core Guide: Patterns

The primitives everything else composes on. A boundary is the call convention. An archetype is the program. Injections fold onto chains at boot. Shape-matcher verifies structure. The template engine resolves YAML thunks. Overlay operators compose storage records. Scenarios turn YAML into test cases. The game engine turns those same scenarios into a learning UI. None of these are features — they're mechanisms the rest of the system bends to do its job.

The big picture

Boundary             the call convention — one input hash in, one crossing out
  │
  ├─ composed into ─→  Chain       an ordered list of boundary slots, walked by the dispatcher
  │
  ├─ declared via ─→   DSL          identity, requirements, capabilities, when_shape, serves
  │
  └─ assembled by ─→   Archetype    a YAML slot sequence with !UserConfig thunks
                           │
                           ├─ modified by ─→ Injections   boot-time fold over every chain
                           │
                           └─ verified by ─→ ShapeMatcher expected-shape checking

Template Engine      tag-dispatched YAML resolution with memoizing cache
Overlay Operators    CRDT-friendly composition of storage records
Scenario + Game      YAML scenarios doubled as tests and learning exercises

Shape of a boundary (the one you'll write most)

class MyBoundary
  include Wanderland::Boundary

  IDENTITY = Wanderland::Identity.new(
    id: "boundary:my_boundary",
    name: "MyBoundary",
    roles: [:boundary],
    type: :service,
    scopes: [:read, :transform]
  ).freeze

  boundary :my_boundary,
    identity:     IDENTITY,
    requirements: [:read],                                       # scopes the caller must hold
    capabilities: [:transform],                                  # what this boundary claims
    description:  "Transforms incoming shape into outgoing",
    when_shape:   { type_addr: { prefix: ":types:" } },          # default guard
    serves:       "application/json"                             # optional — formatter registration

  def call(input)
    # input has: params, query, path, headers, config, route, runtime, context, args
    # return a Hash → becomes crossing.result
    # or return a Wanderland::Signal for explicit type_addr control
    { "transformed" => transform(input["params"]) }
  end
end

Including Wanderland::Boundary gives you Logging and Tracing for free. The boundary :name, ... declaration registers the class so Boundary.execute(:my_boundary, input) finds it. All metadata is declarative — no register! calls, no side-effecting initialize.

Shape of an archetype

An archetype is a YAML file shipped with the gem. It declares a slot sequence for one kind of execution. !UserConfig thunks in slot args get hydrated from the user's config.yml at boot.

# lib/wanderland/archetypes/engine.yml — the HTTP service archetype
name: engine
slots:
  - name: boundaries
    boundary: boot_load_boundaries
    args: { target: !UserConfig boundary_path }

  - name: resolvers
    boundary: boot_configure_resolvers
    args:
      config:
        resolvers: !UserConfig resolvers

  - name: storage
    boundary: boot_mount_storage
    args:
      config:
        storage: !UserConfig storage

  - name: core_injections
    boundary: register_injections
    args:
      injections:
        - { boundary: enforce_denials, position: interleave }
        - { boundary: trace_emit,      position: last }
        - { boundary: format,          position: last }

  - name: user_injections
    boundary: register_injections
    args:
      injections: !UserConfig injections

  - name: routes
    boundary: boot_mount_routes
    args: {}

The engine ships archetypes for engine, scenario, cli, and test. The terminal slot determines runtime shape: long-running service, one-shot validation, one-shot command.

Shape of a chain injection

Boot-time modifications to every compiled chain. Six positions cover every insertion pattern.

injections:
  # Prepend once at head
  - boundary: request_id_stamp
    position: first

  # Append once at tail (format and trace_emit use this)
  - boundary: audit_writer
    position: last

  # Insert before every user slot (enforce_denials uses this)
  - boundary: auth_check
    position: interleave

  # Interleave conditionally — ShapeMatcher shape over the slot's own data
  - boundary: latency_timer
    position:
      interleave:
        boundary: { matches: "^(auth|repo)_" }

  # Before or after every occurrence of a named boundary
  - boundary: cache_probe
    position: { before: expensive_fetch }
  - boundary: cache_store
    position: { after: expensive_fetch }

Declaration order is final placement order: two position: last injections in sequence mean the second one ends up after the first. Interleave sees only the slots present when it runs — framework enforce_denials interleaves just the user slots because it's declared before trace_emit and format.

Shape of a shape match

ShapeMatcher walks actual and expected in parallel. Plain values compare directly; matcher keywords dispatch to registered strategies.

expected:
  repos:
    count: 3                    # collection size matcher
    first:                      # first-element matcher (recurses)
      name: kremis
    any:                        # some-element matcher
      grammars:
        contains: ruby
  version:
    matches: "^v\\d+\\.\\d+"    # regex matcher
  errors:
    empty: true                 # emptiness matcher
  status: ok                    # plain field comparison

Built-in matchers: count, gte/lte/gt/lt, matches, contains/includes/excludes, first/last, any, keys/has_key, not, empty. Custom matchers register by name and receive (actual, operand, path, failures, matcher) — the last argument lets custom matchers recurse.

Shape of a template tag

The template engine loads YAML with custom tag handlers. Each !Tag becomes a Thunk at load time; Thunk#resolve runs the fetch lazily with a memoizing TTL-keyed cache.

# Scalar — bare path
node: !Oculus wanderland-core

# Scalar with subpath
content: !Oculus wanderland-core/content

# Mapping form with params + cache control
nodes: !Oculus
  path: nodes
  sort: slug
  limit: 5
  ttl: 300

# Skip cache this one time
live: !Task
  path: current
  nocache: true

# Fixture files for testing
mock: !Fixture mocks/oculus-nodes

Write a new resolver by subclassing Wanderland::Resolver, calling tag to register, and implementing fetch(arg). Everything else — caching, TTL, the two-phase walk — is the framework's.

Shape of an overlay operation

A seed record plus a stream of operations at the same to_addr. The overlay reads all records, applies operations in at order, returns the composed state. Operations are signed crossings like anything else.

# Map operations — k/v data
{ op: $set,          path: "result.shape", value: "star" }
{ op: $merge,        path: "result",       value: { extra: true } }
{ op: $inc,          path: "result.score", value: 1 }
{ op: $min | $max,   path: "result.best",  value: 5 }

# Stream operations — ordered sequences
{ op: $append,       path: "result.items", value: ["new"] }
{ op: $prepend,      path: "result.items", value: ["first"] }
{ op: $union,        path: "result.tags",  value: ["new"] }
{ op: $intersection, path: "result.allow", value: ["a", "b"] }

# Structural
type_addr :types:snip        # remove key at path
type_addr :types:splice      # positional array edit { path, index, delete, insert }
type_addr :types:antiparticle  # cancel a prior record by (at, type)
type_addr :types:annotate    # additive metadata

Most map operations are commutative CRDTs (two interceptors writing different paths produce the same result regardless of order). Stream splice is positional and non-commutative — use it sparingly where OT-grade resolution isn't needed.

Shape of a scenario

One YAML file serves as an RSpec test, a game level, and a documentation fixture.

name: "parameter extraction from path"
description: |
  The route pattern /node/:slug has one dynamic segment.
  When the request path is /node/sprout-api, :slug captures "sprout-api".

level:
  world: 1
  number: 2
  hint: "Dynamic segments start with : and capture the path value"

operation: route_match

input:
  method: GET
  path: /node/sprout-api
  routes:
    - pattern: /node/:slug
      layout: node-view

expected:
  matched: true
  params:
    slug: sprout-api

blanks:
  slug_value:
    path: params.slug
    answer: sprout-api

Scenario#run calls Boundary.execute(operation, input). Scenario#verify runs ShapeMatcher against expected. blanks: are paths the game hides and asks the player to fill. The scenario doesn't know whether it's being run by a test runner, a game engine, or an emacs buffer — same YAML, three modes.

Best practices

Declarative first, imperative never. Identity, requirements, capabilities, when_shape, and serves: all live on the boundary DSL. Don't add register! calls, after_boot hooks, or initialize-time wiring. If the framework doesn't have the declaration you need, add it to the DSL — don't route around it.

Let the archetype slot decide. Anything a site needs to hand to the engine at boot (storage mounts, trigger configs, denials, injections, resolvers) flows through a dedicated archetype slot via !UserConfig. New boot-time concerns become one more slot, not one more code path.

Injections are boot-time, when: is request-time. Chain structure is decided once, when the route compiles. Conditional execution is decided at each step, against the walker's current context. Don't try to make an injection skip itself — give its boundary the right when_shape: and let the walker handle it.

ShapeMatcher matchers compose. A custom matcher receives the ShapeMatcher instance as its last argument, so you can call matcher.match(inner_actual, inner_expected, inner_path, failures) to recurse. The built-ins like first: and any: use this to nest.

Use the template engine instead of YAML-then-regex. If site config references external data (oculus nodes, API responses, fixtures), declare a resolver with tag and fetch, then let !Tag do the work. The cache, TTL semantics, nocache override, and param-resolution fall out of the framework.

Overlay by append, read by overlay. Mutation is never in-place. Write a poke/snip/splice record at the same to_addr as the seed, let overlay(addr) compose the current state. Concurrent writers are safe on commutative operators (map + union/append).

Write scenarios, not unit tests, for boundary behaviour. A scenario with input: + expected: covers the same ground as an rspec it block but also lives as a fixture, a game level, and documentation. Unit tests that only exercise the boundary's class internals stay inline; anything about shape and contract goes to a scenario.

Contents

Site Audit

wanderland.dev

oculus-view: fence: fence execute HTTP 404