Wanderland

Wanderland Core: Interceptors

Boundaries at the seams. Interceptors run before and/or after every Boundary.execute call, gated by run_level. They are the observability, validation, and debugging layer.

Design

An interceptor is a boundary with two extra properties: position (before/after/both) and run_level. The engine wraps every Boundary.execute with interceptor calls.

module Wanderland
  module Interceptor
    Registration = Struct.new(
      :name, :boundary, :position, :run_level,
      :core,  # true = cannot be removed or overridden
      keyword_init: true
    )
  end
end

Positions

Run Levels

The engine maintains a current run_level set. Interceptors fire if their run_level is in the set.

Core Interceptors (built-in, not overridable)

Like the health triptych routes, core interceptors ship with the gem and cannot be removed or replaced. Sites add interceptors on top.

result_validator (after, always)

Validates that boundary results do not use reserved ShapeMatcher keywords as top-level keys. Prevents the count collision where a result key shadows a matcher keyword.

Reserved keywords: count, gte, lte, gt, lt, matches, contains, includes, excludes, any, keys, has_key, not, empty, first, last.

If a boundary result uses a reserved keyword at the top level, the interceptor flags it with a warning (not a halt — backwards compatibility). Future: configurable to halt.

class ResultValidator
  include Wanderland::Boundary
  boundary :result_validator,
    capabilities: [:validate]

  RESERVED = %w[
    count gte lte gt lt matches contains includes excludes
    any keys has_key not empty first last
  ].freeze

  def call(input)
    result = input["crossing"]["result"]
    return { "valid" => true } unless result.is_a?(Hash)

    collisions = result.keys & RESERVED
    if collisions.any?
      Wanderland.logger.warn("Interceptor") {
        "Boundary '#{input['crossing']['boundary']}' result uses reserved " \
        "ShapeMatcher keywords as keys: #{collisions.join(', ')}. " \
        "These will shadow matcher operations in expected shapes."
      }
    end

    { "valid" => collisions.empty?, "collisions" => collisions }
  end
end

trace_recorder (after, always)

Records every crossing to the tracer. Already happens in Boundary.execute via Wanderland.tracer.record. Moving it to an interceptor makes it explicit and configurable.

Site Interceptors (extensible)

Registered in config YAML, same as triggers:

interceptors:
  - boundary: audit_logger
    position: after
    run_level: always

  - boundary: debug_inspector
    position: before
    run_level: debug

  - boundary: dashboard_feed
    position: after
    run_level: monitor

  - boundary: request_timer
    position: both
    run_level: always

Execution Flow

Boundary.execute(:my_boundary, input)
  |
  v
run before-interceptors (filtered by run_level)
  |
  v
execute the boundary
  |
  v
run after-interceptors (filtered by run_level)
  |
  v
return crossing

Before-interceptors receive { "boundary" => name, "input" => input }. They can modify input or halt.

After-interceptors receive { "boundary" => name, "crossing" => crossing }. They can flag, log, or modify the crossing result.

Core vs Site

Same pattern as Engine::CORE_ROUTES — enforced defaults, extensible by sites.

Static Analysis (RuboCop Cop)

Complement the runtime validator with a static lint check:

# A custom RuboCop cop that flags hash literals containing
# reserved ShapeMatcher keywords as keys in boundary result hashes.
# Catches the collision at development time, not runtime.

Both layers: runtime interceptor catches it in production, static cop catches it in development. Belt and suspenders.

Conditional Interceptors (future)

Gate interceptors on context shape, not just run_level. ShapeMatcher against a time/context shape:

interceptors:
  - boundary: dashboard_feed
    position: after
    when:
      day_of_week: wednesday
    run_level: monitor

Same when syntax as triggers. The interceptor fires when the context matches AND the run_level is active. This is the "feed to a dashboard on wednesdays" pattern.

Relationship to Triggers

Interceptors and triggers are complementary:

Triggers are event-driven. Interceptors are aspect-oriented. Both are just boundaries with metadata about when they fire.

Implementation Path