Wanderland

Wanderland Dev — Best Practices (code smells & corrections)

Companion to wanderland-dev-ai-primer. The custom cops in the shared rubocop config (wanderland-core/lib/rubocop/cop/wanderland/, shipped to every repo via inherit_gem: wanderland-core) are the machine-enforced subset; this guide is the fuller catalog — the smells to look for, how to correct them, the locators, and the judgment calls a cop can't make.

Read/refresh this before a code-smell sweep. It's the content reload: all the things to look for and how to fix them, a more detailed version of what the hooks encode. Extend it as new smells surface.

Positive frame — reach for what exists

BoundaryInput owns the accessors. Before hand-rolling, use them:

Smell catalog

Each entry: Look for / Why / Fix / Locator.

1. Indifferent ||-subscript — hash[a] || hash[b]

2. Redundant || {} guard on dig_indifferent

3. Hand-plucking from the input context

4. Multi-source fallback plucking → first_of_or

5. Reimplementing a core helper

6. Argument validation hand-coded in #call → input_shape schema

7. Partial execution — validate before you mutate

8. Dual-form DSL a boundary already subsumes → collapse + shim

9. Reaching runtime.config.domain in a worker boundary (#5's cop-blind corollary)

10. Secret handed to a shell adapter as an argv element

The heuristic (when no cop fits)

A big block of defensive hash-digging means there's a better way somewhere.

Usually an accessor, a first_of_or, or a schema rule. If you're writing x.is_a?(Hash) ? x[...] : nil, a ladder of ||, or a hand-rolled default, stop and look for the seam.

The cops (machine-enforced subset)

Live in wanderland-core/lib/rubocop/cop/wanderland/, listed in lib/rubocop/wanderland_config.yml, shipped to every repo via inherit_gem: wanderland-core.

To add a cop: drop it under cop/wanderland/, add its require + config block in wanderland_config.yml, rebuild/publish core, then bundle update wanderland-core in each repo. Test it against realistic boundary code (where input is an lvar param), not a sample where input parses as a bare method call — see smell #3's gotcha.

Rollout loop (per repo)

Status so far: wanderland-pipelines-pack (iterators + calver), wanderland-aws-pack and wanderland-k8s-pack (verified clean).

Adapter streaming & log levels — prefer stream: true over :debug

Shell adapters (docker, mvn, git, … — all the shell adapter kind bound to a command:) share one generic run surface: run(*args, cwd:, env:, stdin:, stream: false). stream: is not command-specific — it's the shell adapter's own keyword (adapters/shell.rb), backed by Open3.

So to watch a long shell (build, deploy) on the standard output stream without turning on :debug, pass stream: true on the adapter call. That's the lever for "stop using :debug for adapter shells." Reference: docker_build and compile_jar (mvn.run(..., stream: true)).

Traces (boundary entry/exit) are a separate surface — logged at Wanderland.logger.debug(reg.name) (the :stream: tag is shells only). Getting traces onto stdout without :debug is a log-level / dedicated-sink change, not stream: — a distinct piece of work.

Readiness waits — poll + compare, don't pad with a fixed delay_s

When a step waits for an external system to become ready (a hot-reload endpoint, a rolled deployment, a synced configmap), the readiness mechanism is the poll: fire the request, compare the status to the allowed set, pass → move on; retry at interval_s up to max_attempts for the not-yet-ready case. A large fixed delay_s up front is not readiness — it's a blind guess that runs on every invocation whether or not the wait is needed, and it defeats the retry loop that's sitting right next to it.

Scar tissue (2026-08, jcasc hot-reload): the reload http_poll carried delay_s: 75. The endpoint (/configuration-as-code/reload) returns 302 the instant it's hit regardless of content, so there's nothing to poll for — someone padded a 75s pre-delay instead of trusting the retry. On an unchanged configmap apply it burned the full 75s and then succeeded on attempt 1: pure dead time. Right-sizing: keep a small buffer only for the genuine physical lag it covers — here, the kubelet syncing a changed configmap onto the pod's disk before JCasC re-reads it, ~20s is plenty — and let the retry loop be the readiness wait. Rule of thumb: a delay_s longer than interval_s is a smell. Ask what you're actually waiting for; usually it's a poll you haven't written, or a buffer several times bigger than the lag. (And if the wait is only needed on a changed apply, gate it on kubectl_apply reporting a change — the Dispatcher::GuardView { args, context } slot guard makes that a one-liner; see wanderland-dev-ai-primer §Constraints & gotchas.)