# Effects-as-data

## Concept

Effects-as-data treats side-effecting operations as values: a function
returns a data structure *describing* the work to do (commands, actions,
intents) rather than performing the work directly. A separate runtime
component interprets the description and executes the effects, keeping
the producing function pure and the side-effect surface inspectable.

## Shape

Every effects-as-data implementation carries these invariants:

- **Function returns descriptions, not effects.** The producing
  function's return value is a data structure (or list of data
  structures) naming the effects to perform. The function itself
  does not perform IO. Same inputs to the function always produce
  the same description; the function is pure in the
  `code-design.md` §Effects sense even though the *system* performs
  effects.
- **Effect data is inspectable.** The returned description is
  walkable, serializable, comparable. Tests assert on the description
  ("did the function decide to send these 3 commands?") without ever
  running the effects. The Composability §Inspectability invariant
  applies: the effect plan is data, not a closure that hides what
  will happen.
- **A runtime executes the descriptions.** A separate runtime layer
  reads the description and dispatches each effect to the appropriate
  performer (HTTP client, file writer, subprocess invoker, time
  source — terms used in the abstract here; language-specific
  performer types belong in the per-language render). The runtime is
  the only impure component; everything else is data.
- **Effect taxonomy is enumerable.** The set of effect kinds the
  system supports is a closed (or registered) catalog. New effect
  kinds are added by extending the catalog and the runtime, not by
  threading new IO calls through existing code paths.
- **Composable as values.** Two effect descriptions concatenate
  (append, sequence, monoid). An empty effect list is admissible —
  it means "do nothing." (Composability invariant 2: identity.)
- **Reversibility-ready (when applicable).** Where the domain
  requires undo or rollback, effect descriptions carry enough
  information that the runtime can synthesize an inverse — or a
  paired "compensation" effect description exists. This is the
  effects-as-data equivalent of the interceptor pattern's
  compensation pair.

## Forecloses

When a function is designed as effects-as-data, the following designs
are explicitly *not* the right choice for the same problem:

- **Direct effect calls in business logic.** A function that calls
  HTTP clients, subprocess invokers, or filesystem writers directly
  inside the decision logic is not effects-as-data — it is performing
  the effect inline. The defining feature of this pattern is the lack
  of inline effect calls; choosing the pattern forecloses inlining
  them. (Per `code-design.md` Rule 10
  `code_design.injectable_seams`, this anti-pattern is already
  forbidden for new code at the within-function level; the
  effects-as-data pattern strengthens the same constraint at the
  function-shape level.)
- **Callback-based effect dispatch.** A function that takes a
  callback parameter and calls `callback(arg)` is not
  effects-as-data — it is hidden coupling via a closure. The
  callback's behavior is opaque; the data version exposes the intent
  as inspectable structure.
- **Interceptor stack for the same domain.** Interceptors and
  effects-as-data both isolate effect-doing from decision-making, but
  via different mechanisms. Interceptors thread a Context through a
  list of nodes; effects-as-data returns a list of effect
  descriptions. Choosing one forecloses the other for the same
  function. (They can compose at different layers — an interceptor's
  enter handler may return an effect-data list — but not within the
  same level of decision-making.)
- **Side effects via context-manager `__enter__` / `__exit__`.** A
  resource-acquisition shape that performs effects in the
  `__enter__` block is not effects-as-data; the effect is happening
  in the act of context-binding rather than being described as data.
  Choosing this pattern means choosing a different abstraction.
- **Inline mutation of shared external state.** A function that
  signals "an effect happened" by writing to a module-level list or
  a database table is not effects-as-data — it is a hidden side
  effect dressed up as data. The data must be the function's return
  value, not an external write.

## Judgment probes

These checks verify that a function actually implements the
effects-as-data pattern:

- **Return-value carries the effect plan.** Read the producing
  function. Does its return value contain the full description of
  the effects to perform? Or does it return `None` / a bare status
  flag while a separate side channel carries the actual work?
- **Function purity audit.** Run the producing function twice with
  the same input. Are the two return values equal (deep equality)
  and is there no observable change to the world between the two
  calls? If the second call produces different output or has any
  observable side effect, the function is not actually pure — an
  effect leaked into the decision layer.
- **Runtime is a separate component.** Locate the code that turns
  the effect descriptions into real effects. Is it a separate
  function, module, class, or service from the producing function?
  If the same function both produces descriptions and immediately
  interprets them, the separation has collapsed and the pattern's
  benefit (testability of decisions independent of effects) is
  unrealized.
- **Effect catalog is enumerable.** Can a reader list the kinds of
  effects the system supports by reading one file or one registry?
  If new effect kinds are introduced ad-hoc by adding new return
  shapes without a central catalog, the inspectability invariant
  weakens over time.
- **Effect plan equality is meaningful.** Can two effect plans be
  compared for equality (`plan_a == plan_b`)? If the effect
  descriptions embed function references, closures, or unhashable
  resources, equality fails — tests cannot assert on planned
  effects, only on observable outcomes after execution.
- **Empty plan is admissible.** Does the system handle an empty
  effect plan (zero effects) cleanly, returning the input state
  unchanged? If empty plans crash or special-case, the monoid
  identity is missing.

## Render: Python

Python's idiomatic effects-as-data rendering is a list of frozen
dataclass instances describing the effects to perform, returned from a
pure planning function, executed by a separate runtime function that
takes the effect-performer injectables (HTTP client, filesystem writer,
clock) as parameters. The frozen dataclass is the value; the runtime is
the only impure component.

```python
from collections.abc import Callable
from dataclasses import dataclass
from functools import cache
from typing import Union

# Effect catalog — closed sum type produced by factory (Rule 5).
@cache
def _effect_classes() -> tuple[type, type, type]:
    @dataclass(frozen=True, slots=True)
    class WriteFile:
        path: str
        content: str

    @dataclass(frozen=True, slots=True)
    class HttpPost:
        url: str
        body: dict

    @dataclass(frozen=True, slots=True)
    class LogMessage:
        level: str
        message: str

    return WriteFile, HttpPost, LogMessage

WriteFile, HttpPost, LogMessage = _effect_classes()
Effect = Union[WriteFile, HttpPost, LogMessage]

# Pure planner: same inputs → same effect list. No IO.
def plan_release_effects(version: str, notes: str) -> list[Effect]:
    if not version:
        return []  # identity: empty plan is admissible
    return [
        WriteFile(path="CHANGELOG.md", content=notes),
        HttpPost(url="https://forge.example/release", body={"v": version}),
        LogMessage(level="info", message=f"planned release {version}"),
    ]

# Runtime: takes injected performers, dispatches each effect.
def run_effects(
    effects: list[Effect],
    *,
    file_writer: Callable[[str, str], None],
    http: object,
    logger: Callable[[str, str], None],
) -> None:
    for effect in effects:
        if isinstance(effect, WriteFile):
            file_writer(effect.path, effect.content)
        elif isinstance(effect, HttpPost):
            http.post(effect.url, json=effect.body)
        elif isinstance(effect, LogMessage):
            logger(effect.level, effect.message)
        else:  # exhaustiveness check — new effect kinds must be registered
            raise ValueError(f"unknown effect: {type(effect).__name__}")

# Tests assert on the plan, not on observed IO:
def test_plan_release_includes_changelog():
    plan = plan_release_effects("1.2.3", "Initial release")
    assert WriteFile(path="CHANGELOG.md", content="Initial release") in plan
```

The project's `_propose_version_bump_edits` / `_apply_version_bump_edits`
pair in `release.py` is this same pattern in dict form — the planner
returns `list[dict]` with `{path, before, after, new_content}` entries
and the applier walks them. Frozen-dataclass rendering above is the
forward-going style per Rule 5; new effect-list catalogs should use the
factory + dataclass shape rather than untyped dicts.

### Probes

- **planner returns a list of catalogued effect types.** The planner's
  return annotation must resolve to `list[Effect]` (or a TypedDict
  union). AST: walk the planner's `Return` nodes; each must construct a
  `List` whose elements are `Call(func=Name(<EffectClass>))` against the
  closed catalog. A `Return` of `None` or a bare effect (not in a list)
  fails Empty-plan-admissible / Composable-as-values.
- **planner is pure.** AST-walk the planner body for calls to known
  effect-doing names (`httpx`, `requests`, `subprocess`, `open`,
  `Path.read_text`, `Path.write_text`, `time.sleep`, `print`,
  `os.environ`). Any such call inside the planner means the planner
  performs IO directly (Forecloses §Direct effect calls).
- **runtime separation.** The producing function and the interpreting
  function are distinct `FunctionDef` nodes. Detect by name convention
  (`plan_*` / `propose_*` for planners; `run_*` / `apply_*` /
  `execute_*` for runtimes) and assert they are not the same function
  and that the planner does not call the runtime.
- **effect-class factory present.** Each `Effect` type must be produced
  by a `@cache`-decorated factory function returning the class (Rule 5
  `code_design.schema_as_functions`). Top-level `@dataclass`-decorated
  effect classes fail this probe.
- **frozen dataclass shape.** Each effect class is `@dataclass(frozen=
  True, slots=True)`. AST: check the decorator's keyword args.
  Mutable effect descriptions break Effect-plan-equality (the
  description is no longer hashable / comparable).
- **runtime exhaustiveness.** The runtime's dispatch dispatches over the
  closed catalog. AST: walk the runtime body for `isinstance(effect,
  X)` chains, collect the X set, compare to the catalog membership
  enumerated from the factory return. Missing-kind dispatch raises a
  `ValueError` (the example above's `else` branch).
- **empty-plan admissibility.** Detect either a unit test that calls
  the runtime with `[]` or an inspection of the runtime body confirming
  the for-loop terminates cleanly on empty input (no `if not effects:
  raise` guard).

### Example

`mcp-server/src/forge_manager_mcp/server/handlers/release.py` —
`_propose_version_bump_edits` returns `list[dict]` describing file
edits; `_apply_version_bump_edits` interprets the list and performs the
writes. The pair is the project's existing effects-as-data shape; the
frozen-dataclass rendering above is the forward-going Rule 5–compliant
upgrade path.

### Anti-patterns

- **Planner that calls `requests.post(...)` and returns a status code.**
  The IO happens in the planner; the return value is not the effect
  description. Violates Function-returns-descriptions and Forecloses
  §Direct effect calls in business logic.
- **Effects-as-callbacks: `def plan(callback): callback(x); callback(y)`.**
  The "effect plan" is the sequence of callback invocations; nothing is
  inspectable, equality is meaningless, tests cannot assert on planned
  work without a mock callback. Violates Forecloses §Callback-based
  effect dispatch and Effect-plan-equality.
- **Module-level mutable effect log: `EFFECTS_LOG = []` updated from
  inside the planner.** The "data" is a side effect on a global, not the
  function's return value. Violates Forecloses §Inline mutation of
  shared external state.

## Render: Go

Go renders effects-as-data via a closed `Effect` interface implemented
by per-effect-kind structs, collected into a slice the producing
function returns. A separate runtime dispatches on concrete type
(typically via a type switch or a registry keyed by `reflect.Type`).
Because the effect descriptions are plain structs with exported
fields, they serialize to JSON and compare by `reflect.DeepEqual` —
the inspectability and equality invariants follow naturally.

```go
package effects

import (
	"context"
	"encoding/json"
	"net/http"
)

// Effect is the closed catalog interface. Each variant is a struct
// with exported fields (no method bodies hiding behavior, no
// closures captured) — the entire description is data.
type Effect interface {
	effectMarker() // sealed-interface trick; unexported method
}

// Effect kinds — one struct per variant. New kinds = new struct +
// new runtime case, both edited together. The taxonomy is enumerable
// by walking the package for types implementing Effect.
type WriteFile struct {
	Path string
	Body []byte
}
type HTTPPost struct {
	URL     string
	Body    []byte
	Headers map[string]string
}
type LogLine struct {
	Level   string
	Message string
}

func (WriteFile) effectMarker() {}
func (HTTPPost) effectMarker()  {}
func (LogLine) effectMarker()   {}

// Plan is the producing function's return value: a slice of effect
// descriptions. The empty plan is admissible (monoid identity).
// Plans concatenate with append.
type Plan []Effect

// Pure producing function — returns a Plan, performs no IO.
// Same inputs always produce the same plan; comparable via
// reflect.DeepEqual; serializable via json.Marshal.
func DecideUserSignup(name, email string) Plan {
	if name == "" {
		return Plan{LogLine{Level: "warn", Message: "empty name"}}
	}
	return Plan{
		WriteFile{Path: "/var/users/" + name, Body: []byte(email)},
		HTTPPost{URL: "https://welcome.example/send",
			Body: []byte(`{"to":"` + email + `"}`)},
		LogLine{Level: "info", Message: "signed up " + name},
	}
}

// Runtime — the only impure component. Dispatches via type switch.
// New effect kinds add a case here and a struct above.
type Runtime struct {
	HTTP *http.Client
	Log  func(level, msg string)
}

func (r *Runtime) Apply(ctx context.Context, plan Plan) error {
	for _, eff := range plan {
		switch e := eff.(type) {
		case WriteFile:
			if err := writeFile(e.Path, e.Body); err != nil {
				return err
			}
		case HTTPPost:
			if err := r.post(ctx, e); err != nil {
				return err
			}
		case LogLine:
			r.Log(e.Level, e.Message)
		}
	}
	return nil
}

// Plans serialize for inspection and replay:
//   data, _ := json.Marshal(plan)
//   var replayed Plan; _ = json.Unmarshal(data, &replayed)
var _ = json.Marshal
```

### Probes

- **Producing function returns Plan type (AST).** The producing
  function's return type must be `Plan` (or `[]Effect`), not `error`
  alone, not a bare status. AST walk: for each function tagged as
  effects-as-data, check `*ast.FuncType.Results.List` has exactly one
  result of the named Plan type.
- **No IO calls in producing function body (AST).** Walk the
  producing function with `ast.Inspect` and flag any `*ast.CallExpr`
  whose selector resolves to a known IO package: `net/http`,
  `database/sql`, `os` (File ops), `io`, `time.Now`, `os.Getenv`,
  `os/exec`. The producing function constructs descriptions; it must
  not perform IO. (Reading `os.Stdin` inside a pure function is a
  hidden side channel; flag.)
- **Effect catalog enumerable.** Walk the package for types
  implementing the `Effect` interface (concrete `effectMarker()`
  receiver types). The set must match a documented catalog (the
  package's exported types alphabetized, or a manually-curated list).
  A struct implementing `Effect` that does not appear in the runtime's
  type switch is a half-registered effect — flag.
- **Runtime dispatch completeness.** Walk the runtime's type-switch
  (or registry) and assert every `Effect`-implementing type has a
  case. Detector: compare the set of `Effect` implementors found by
  AST search against the set of cases in `Runtime.Apply`. Symmetric
  difference > 0 is a bug — either an effect kind is unhandled
  (silent drop) or a stale case exists for a deleted kind.
- **Effect structs are pure data.** Each effect struct's fields must
  be values, not function types (`func()`) or interface types other
  than `Effect` itself (`interface{}`, `any` carrying closures). AST
  probe: walk `*ast.StructType.Fields.List` for each effect type and
  flag fields with `FuncType` or `InterfaceType{Methods: empty}`.
  Function-typed fields break equality and serialization.
- **Plan equality via reflect.DeepEqual.** Write a test that calls
  the producing function twice with the same input and asserts
  `reflect.DeepEqual(planA, planB)` and `json.Marshal(planA) ==
  json.Marshal(planB)`. Failure means the plan contains
  non-deterministic content (map ordering, captured closures,
  pointers to fresh allocations) and the inspectability invariant is
  broken.
- **Empty plan handling.** Test that `runtime.Apply(ctx, Plan{})`
  returns nil error and performs no observable IO. Mocked HTTP /
  filesystem should record zero calls. Failure means the runtime
  special-cases empty plans or the monoid identity is missing.

### Example

`k8s.io/apimachinery`'s patch operations — `JSONPatchOp{Op, Path,
Value}` — are effect descriptions consumed by a separate apply loop;
the description is data, the apply loop is the runtime. The
`kubectl apply` shape (manifest → diff → apply plan → execute) is
effects-as-data at the orchestration layer. Within the stdlib,
`go/ast` itself is structurally similar: the producer (`parser.Parse`)
returns a value tree; the consumer (`go/types`, `go/printer`) walks
it. The producer does no semantic action.

### Anti-patterns

- **Producing function calling `http.Post` directly.** A function
  that "logs that it sent the email" by returning a Plan but also
  actually sending the email inline is not effects-as-data — the
  effect leaked into the decision layer. AST probe must flag this.
- **Effect struct with `func()` field.** An effect like
  `RunCallback struct { Fn func() error }` smuggles behavior inside a
  data envelope; the callback is opaque, unserialized, and
  uncomparable. The effect description must name *what* to do via
  data, not embed a closure that hides it.
- **Producer returning a `chan Effect` instead of a slice.** A
  channel is consumed by reading; reading is a single-shot operation
  that destroys the plan's inspectability. Plans must be slices
  (re-readable, indexable, serializable). Use a slice and let the
  runtime range over it.
- **Runtime as a method on the producing struct.** When the producing
  function and the runtime live on the same struct (`(*Service).Plan`
  and `(*Service).Apply`), the test surface collapses — the
  caller cannot test plan construction without instantiating the
  whole Service. Runtime must be a separate type.

## Render: Ada

In Ada, effects-as-data is rendered as a tagged-type hierarchy
rooted at an abstract `Action` type, with each concrete effect kind
a child type carrying its parameters as record components. The
producing function returns an unbounded vector of class-wide
`Action'Class` values via `Ada.Containers.Indefinite_Vectors`. A
separate `Runtime` package consumes the vector, dispatching via the
abstract primitive `Perform` on each element — Ada's tag-driven
dynamic dispatch *is* the runtime's dispatch mechanism. Action
equality (load-bearing for inspectability) is overridden per child
type via `"="` primitive overriding.

```ada
with Ada.Containers.Indefinite_Vectors;

package Effects is

   --  Root of the effect taxonomy. Concrete effect kinds inherit
   --  and override Perform.
   type Action is abstract tagged null record;

   procedure Perform (A : Action) is abstract;

   function "=" (Left, Right : Action) return Boolean is abstract;

   --  The plan: a sequence of class-wide actions. Inspectable
   --  (length, indexed access), serialisable (each child type
   --  exposes its components), composable (vector concatenation).
   package Plans is new Ada.Containers.Indefinite_Vectors
     (Index_Type   => Positive,
      Element_Type => Action'Class);

   subtype Plan is Plans.Vector;

   Empty_Plan : constant Plan := Plans.Empty_Vector;

end Effects;

package Effects.Examples is

   --  A concrete effect: write a line to a named log channel.
   type Write_Log is new Action with record
      Channel : Natural;
      Message : String (1 .. 80);
   end record;

   overriding procedure Perform (A : Write_Log);

   overriding function "=" (Left, Right : Write_Log) return Boolean is
     (Left.Channel = Right.Channel and then Left.Message = Right.Message);

end Effects.Examples;

package Effects.Runtime is

   --  The runtime is the only impure component. It walks the plan
   --  and dispatches Perform on each element via tag-driven
   --  dynamic dispatch.
   procedure Execute (P : Effects.Plan);

end Effects.Runtime;

package body Effects.Runtime is

   procedure Execute (P : Effects.Plan) is
   begin
      for Cursor in P.Iterate loop
         Effects.Plans.Element (Cursor).Perform;
      end loop;
   end Execute;

end Effects.Runtime;
```

The producing function — for instance `Decide_Releases (State :
Release_State) return Effects.Plan` — has profile
`(in Release_State) -> Plan` with no `with Ada.Text_IO` import: a
mechanical check that the decision function is effect-free.

### Probes

- **Root `Action` type is abstract and tagged.** The base type
  declaration must be `type Action is abstract tagged ...` with at
  least one abstract primitive (`Perform`). A concrete root makes
  effect kinds optional rather than enforced. (libadalang: walk
  `TypeDecl` for `Action`; assert `f_type_def` is a
  `DerivedTypeDef` or `RecordTypeDef` with `f_has_abstract` set.)
- **`Perform` is an abstract primitive on Action.** Locate the
  primitive operations of `Action`; assert one is named `Perform`
  (or per project convention, but always abstract). Each concrete
  child type must override it. (libadalang: walk child `TypeDecl`
  nodes; for each, locate `SubpDecl` with `f_overriding =
  Overriding_Overriding` and name `Perform`.)
- **Plan is a vector / list of `Action'Class`, not concrete
  Action.** The plan container type must be parameterised over the
  class-wide type `Action'Class` (not the root type alone), so the
  vector can hold heterogeneous concrete subtypes. A vector of
  `Action` alone cannot — the tag would be lost.
- **Producing function does not import IO packages.** The decision
  function's compilation unit must not have a `with` clause for
  `Ada.Text_IO`, `Ada.Streams.Stream_IO`, `GNAT.Sockets`, or any
  other IO-class package. The mechanical check is: walk the
  `CompilationUnit` `prelude`; assert no `WithClause` names an IO
  package. (Project-specific allow-list for cases where IO is read-
  only e.g. `Ada.Calendar.Clock` — the producing function reads
  time from a captured Now parameter, not from the clock directly.)
- **Empty plan is exported.** A constant `Empty_Plan : constant
  Plan := <empty vector>;` (or equivalent length-zero literal) must
  appear in the public part. Without it the identity element is
  unreachable from clients.
- **Each concrete effect type overrides `"="`.** Plan equality
  drives test assertions
  (`Decide (input_A) = Build_Expected_Plan ([Write_Log'(...), ...])`).
  If equality is the default record equality, components that are
  access types or unbounded strings fail the inspectability check.
  Walk concrete `Action`-derived types; assert each declares
  `overriding function "="`.
- **Runtime is in a separate package from the producer.** The
  `Execute` (or equivalent) procedure must live in a different
  package unit from any `Decide_*` / producing function. A
  single-package design where the same body declares and immediately
  consumes the plan collapses the decision / runtime separation.

### Example

AdaCore's `Ada.Containers` heterogeneous-vector idiom (storing
`Element_Type'Class` in indefinite vectors) is the canonical
container shape. The GNATCOLL.Command_Lines example —
`Command_Type` hierarchy with a dispatch loop — is the closest
in-tree match for the executor-walks-a-plan idiom.

### Anti-patterns

- **Variant record instead of tagged hierarchy.** Encoding effect
  kinds as a single discriminated record `type Action (Kind :
  Effect_Kind) is record case Kind is when Write_Log => ...; when
  HTTP_Get => ...; end case; end record;` works for closed
  catalogues but is not extensible — adding a new effect kind
  requires editing every site that matches on Kind. The tagged-type
  hierarchy lets new effect packages be added in client code
  without touching the catalogue.
- **Runtime fused into the producing function.** A function
  `Decide_And_Execute (State : Release_State)` that internally
  builds a plan, then immediately calls `Execute` on it before
  returning — the decision and the execution are syntactically
  fused so the decision cannot be tested without the IO.
- **Plan stored in a package-level vector.** A package that
  declares `Pending_Actions : Plan;` at package level and exports
  procedures that append to it, with a separate `Run` that drains
  it — looks like effects-as-data, is hidden global state. The
  plan must be a return value, not a module-level accumulator.

## Render: TypeScript

TypeScript's idiomatic effects-as-data rendering: functions return
command/action objects — plain objects with a required `type` (or
`kind`) tag — and a separate executor interprets them. The producing
function stays pure; the side-effect surface is an inspectable value
(re-frame's effect-map discipline).

```typescript
type Effect =
  | { type: "http-post"; url: string; body: unknown }
  | { type: "write-file"; path: string; text: string };

// Pure planner: describes the work, performs none of it.
const planSync = (dirty: ReadonlyArray<string>): ReadonlyArray<Effect> =>
  dirty.map((path) => ({ type: "write-file", path, text: render(path) }));

// Separate executor: the ONLY place IO happens.
const execute = async (effects: ReadonlyArray<Effect>): Promise<void> => {
  for (const effect of effects) {
    switch (effect.type) {
      case "http-post":  await fetch(effect.url, { method: "POST", body: JSON.stringify(effect.body) }); break;
      case "write-file": await fs.promises.writeFile(effect.path, effect.text); break;
    }
  }
};
```

### Probes

Mechanical (wired in `_TYPESCRIPT_PROBES`):

- **returns_effect_descriptor.** The producing function returns plain
  object(s) carrying a `type:` / `kind:` tag — the effect description.
  A producer that returns no tagged descriptor is not representing
  effects as data.
- **executor_separate.** No direct IO call inside the producer
  (`fetch(`, `fs.`, `axios.`, `localStorage.`) — IO in the producer
  means the interpretation is NOT separate; the planner must stay
  pure (Forecloses §Direct effect calls).

Judgment (prose-only):

- **closed effect vocabulary.** The `Effect` union is the catalog of
  possible effects; adding an effect extends the union, not a call
  site.
- **executor switch is total.** Every union member has an
  interpretation arm (the compiler's exhaustiveness check).

### Example

The SolidJS UI's action dispatchers that build request-descriptor
objects consumed by one fetch executor; the Python
`compose_swimlanes` block (`daemon/swimlanes.py.design.md`) is the
sibling declaration on the Python side.

### Anti-patterns

- **`await fetch(...)` inside the planner.** The planner performed
  the effect instead of describing it — the purity invariant is gone.
- **Effect objects without a discriminant tag.** An untagged object
  cannot be interpreted by a total switch; the vocabulary is open and
  uninspectable.
