Wanderland

Wanderland Dev — AI Primer

A frame-of-mind primer for AI agents (Claude) working in the wanderland-* repos. Read this at the start of every session. It frames the model, the method, the testing discipline, and the constraints; the section-by-section detail lives in wanderland-dev-ai-reference — pull a section when you need it. Verified against on-disk code (gem 2026.07.127) as of 2026-07-30.

Mantra: Before writing anything, look for what already exists. The most expensive mistake an AI makes here is inventing a thing that's already there under a different name. The shell / http / secret / json_rpc / store adapter kinds exist. The containerized_deployment archetype exists. The iterate_artifacts iterator exists. The chain: wrapper exists. Grep before you invent.

TL;DR — the mental model

Wanderland is a Ruby framework where the unit of work is a boundary: a class with a declared identity, scopes, input/output shape, and a single #call(input). Boundaries reach the outside world only through adapters (http, shell, secret, json_rpc, store, aws, clock, drone) that wrap an external dependency behind a uniform facade with mode-switching (live / replay / mock / dry_run). A site's config.yml declares which adapters to mount and which boundaries to wire into routes (an HTTP path + verb, also a CLI subcommand). For pipeline-shaped work, archetypes (containerized_deployment, infrastructure_deployment, loom_ingest) declare slot orderings that user config fills in. Packs are gems that add adapter kinds, boundaries, and archetypes to the core engine.

Every dispatched boundary produces a signed, chained Crossing (caller identity → boundary identity, type-addressed). The append-only Context log of all crossings for a request is what later slots read — boundaries communicate through the context, not by returning values to each other.

Deep dives: wanderland-dev-ai-reference §Boundaries, §Adapters, §Dispatch & flow control.

Method — data models first

The order of work is fixed. It is the cheapest path and the one that keeps a change honest. When a task gets into the weeds, return to this order.

The data model is the spec. Between the shapes, the scenarios that pin them, the conversation, and the code comments, the implementation is determined. Write the fixture, watch the scenario miss, build the boundary, watch it pass. Start from the data a call must produce, not from the boundary.

Investigation to pack (looking-glass). Not every capability starts as a spec — some start as a question you answered by hand against a live system. Poke the live surface (/inspect/routes, a GraphQL endpoint, kubectl, a drone sortie) until it makes sense; when the same join/filter/verdict recurs, that recurrence wants to be a boundary; prototype it in wanderland-looking-glass (the scratch pack) and use it for real; productionize into the owning domain pack if it earns its keep, delete it if not. Cheap either way. Investigation discovers the shapes; data-models-first is how you build them once you know them.

Core engine at a glance

Three ways in, one seam. --type cli (Commander subcommand per route), --type http (Puma + Rack), and --type test (scenario runner) all funnel through the same Dispatch.invoke:

adapter parses protocol → params
  → resolve route → allocate a fresh append-only Context (:trace:<svc>:req-<uuid>)
  → announce the adapter → walk each slot:
        effective when: guard  →  Boundary.execute  →  context.append(crossing)
  → terminal crossing → formatter → response body

The mental shape is adapter → chain (of slots) → formatters. Key properties:

Detail: wanderland-dev-ai-reference §Dispatch & flow control, §Configuration & routes (the 12-slot boot chain), §Formatters.

How you extend it

Four extension points, in rough order of frequency. The rule is always find what exists first — the registries are global and names collide.

What already exists (grep before inventing):

Extension point What's there
Adapter kinds http, shell, secret, json_rpc, store (core); aws, clock, drone (packs)
Archetypes containerized_deployment, infrastructure_deployment (pipelines); loom_ingest (loom)
Iterators iterate_artifacts (artifacts), iterate_components (component)
Packs aws, pipelines, k8s, cheshire, drones, lantern, loom, sutra

Detail + full boundary/adapter/pack inventories: wanderland-dev-ai-reference §Boundaries, §Adapters, §Archetypes, §Pack catalog. Loom (the markdown-graph engine behind the oculus tools) has its own node: wanderland-dev-loom-pack.

Reading from the input context — pull data with the BoundaryInput accessors, never by hand:

Do not Wanderland::HashUtils.dig_indifferent(input[…], …) or chain input["args"]["x"] — Wanderland/InputAccessByHand flags both. Rule of thumb: a big block of defensive hash-digging means there's a better way somewhere — usually one of these accessors, or a first_of_or across the candidate paths.

Schema seam — validation belongs in input_shape, not #call. find / first_of_or also enforce the field's declared input_shape rule for free: a default: fills in for an absent/blank value, and constraints: (ShapeMatcher rules — e.g. constraints: { matches: '\S' } for non-blank) validate a present value and throw a 400 halt on failure. Purely additive — a path with no rule, or a rule without default/constraints, is a no-op. So declare the argument contract once in the schema and read with input.find, instead of hand-rolling defaults and blank-checks. Pattern: calver's format — optional, default: DEFAULT_FORMAT, constraints: { matches: '\S' }, and a one-line #call.

Testing style

Red→green is the discipline for wanderland-core changes. Failing scenarios precede the code change — new feature: write the spec, watch it fail for the right reason, implement, watch it pass; changing behavior: update the spec that pinned the old behavior, watch it fail, implement, pass. Skipping this is not optional — "or it doesn't count."

Two patterns, used side by side:

Run site scenarios with bundle exec wanderland --type test config.yml. Full ShapeMatcher vocabulary (~30 matchers), the reset/reload harness, the route-mode-only mocks gotcha, and layered dispatch scenarios: wanderland-dev-ai-reference §Scenarios & testing.

Constraints & gotchas

The high-value, expensive mistakes — the ones worth catching before you write code, several of which also flag where the platform still wants UX revision. Long-form field notes (deploy war-stories) live in wanderland-dev-ai-reference §Tips & tricks.

⚠️ WHEN YOU'RE DEALING WITH STORAGE, THINK SQL

Retention, expiry, counts, aggregates, "find the old ones and delete them" — these are database queries, not Ruby loops. Never load a mount into memory to sift it. TTL reaping is DELETE ... WHERE at < cutoff; the only Ruby is the cutoff instant. The database does the finding and the deleting.

Scar tissue (2026-08): a reaper computed age in Ruby by reading every record on the mount (under(prefix)) into an array to find old cells. On a ~258k-row pg store that spiked the pod past its memory limit and OOM-killed it into CrashLoopBackOff. The fix was a one-line set-based delete_stale in SQL — bounded memory, no scan. If your storage code holds rows in Ruby to decide something a WHERE clause could decide, stop and write the query.

The store is one tree — four pointers into it

The crossing store is one append-only tree, rooted at : — the pinch point. It is not several namespaces side by side. It is one address space that describes itself.

A record (a crossing) carries four address pointers plus a payload, and every one of them points into this same tree:

The property that matters: an address is itself a walkable node. A type_addr like :bridges:remark is also a to_addr you can walk to, and the crossings sitting there are what define that type — its handlers, its bridge, its denials. So the ontology is data at addresses: types, sessions, bridges, plans, and the configuration of the system itself are all crossings in the one store, composed by the same walk. That is what data-driven means here — there is no schema off to the side.

A read is a walk up an address to the root, collapsing the layers it collects. The hierarchy sets precedence, and merge operators order sets and lists.

An anti is not special machinery. It is an ordinary crossing whose type sits at the known sibling address :anti:<kind>, the mirror of :types:<kind>. The collapse has one rule: an :anti:X cancels the most-recent :types:X at the same address, last in first out, because type_addr.sub(":anti:", ":types:") maps the sibling back to its twin.

So there is no mount you file a record into, and no second tree for types. There is one tree and four pointers into it. To make a record kind reversible, its type_addr must sit under :types: — where its :anti: twin is defined to look. An edge types as :types:edge:<relation>, so an unlink is an :anti:edge:<relation>. A list item types as :types:rupa, so a remove is an :anti:rupa. A type pointer parked in a data region (:rupa:edge:<relation>) has no anti twin that can find it, so nothing cancels it. When you add a new record kind, place its type under :types:.

Full model: crossings-architecture — the four-address store, the walk, the self-describing ontology. The sūtra pack is the reduced version: the same four-address record and anti-collapse, without the compile, execute, and bridge machinery.

Workspace map

Repo What lives here
wanderland-core engine, dispatch, registries, boot chain, built-in adapter kinds (http/shell/secret/json_rpc/store), YAML resolvers, formatters, scenario runner, introspection routes
wanderland-aws-pack aws kind; ECS/ECR/CFN/S3/SSM boundaries; !SsmLookup/!ListenerPriority resolvers
wanderland-pipelines-pack clock kind; pipeline boundaries + iterators; containerized_deployment / infrastructure_deployment archetypes
wanderland-k8s-pack kustomize/kubectl deploy boundaries (mounts a :kubectl shell adapter)
wanderland-loom-pack the markdown-graph engine behind the oculus tools (wanderland-dev-loom-pack)
wanderland-{lantern,cheshire}-pack server-rendered HTML "grin/cat" component layer
wanderland-drones-pack drone kind; drone-sortie runner + hive registry
wanderland-sutra-pack recursive case/runbook primitive (rūpa / sūtra)
vivarta-jenkins, per-project images (cool-tools, cfn-tools, …) site-specific boundaries + their own config.yml, consuming the packs above as gems

Pack file layout and the entry-file registration pattern: wanderland-dev-ai-reference §Pack catalog.

Pointers