# Railway

## Concept

A railway-oriented computation has two parallel rails: a success rail and
a failure rail. Each computation step either propagates a success value
onto the next success step or short-circuits the entire chain onto the
failure rail, where it skips forward to a single failure-handling site
without further transformation.

## Shape

Every railway implementation carries these invariants:

- **Two-valued result type.** Every step returns a value belonging to a
  tagged-union type with exactly two cases: success (carrying the
  forward-going value) and failure (carrying the error description).
  Names vary by language and library (`Result<T, E>` / `Either<E, T>` /
  `(Ok|Err)` / `Maybe`-with-error), but the shape is fixed: one bit of
  tag + one of two payload variants.
- **Success-path composition.** A step taking the success-rail value
  produces another two-valued result. The chain operator (`bind` /
  `andThen` / `flat_map` / `?` / `try`) automatically propagates failure
  without invoking the next step. The composer is itself the
  abstraction; the steps remain `(success_value) -> Result`.
- **Failure short-circuit, single-site recovery.** Once a step
  produces a failure, no subsequent success-rail step runs. The
  failure value flows past every intermediate step to a single
  failure-handling site (often at the end of the chain, or at an
  explicit recovery operator). Recovery either converts a failure back
  to a success (returning to the success rail) or propagates further.
- **Failure values are nameable, not just thrown.** The failure case
  carries a structured value the caller can inspect — error code,
  message, originating step, context — not just an opaque exception
  bubbling unhandled.
- **Pure step composition.** Step composition is a pure value-level
  operation; the chain itself is a description that runs only when
  evaluated. (Distinct from effects-as-data — railway's "data" is the
  Result value, not a queued action. The step's body may still do
  effect work; the result-typing is what makes the chain railway-shaped.)
- **Identity-step admissible.** A step `(value) -> Success(value)` is
  the railway identity — it composes into any chain without changing
  the result.

## Forecloses

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

- **Exception-based error propagation as the *default* short-circuit.**
  Railway makes failures first-class values. If the design's *ordinary*
  step-to-step short-circuit is "throw an exception and rely on
  `try`/`catch` to skip the rest of the chain", it is not railway — it
  is procedural error handling. The two have very different testability
  and composability stories. (Choosing railway forecloses the
  exception-default style.) This does **not** foreclose a single,
  documented *exception-to-Result adapter* at a fatal boundary: a step
  may lift an upstream failure into a structured, `from`-chained wrapper
  error (`raise WriteError(...) from cause`) to short-circuit a *fatal*
  rail whose contract is "this failure is unrecoverable for the whole
  operation" — provided the raised value is a named, inspectable type
  (not a bare `raise ValueError("oops")`). The blessed adapter is the
  exception-rail rendering described in §Render: Python; the foreclosed
  shape is exceptions used in lieu of the Result/envelope value on the
  *ordinary* (recoverable) rail. See §Render: Python §Probes "no `raise`
  for control flow" for the mechanical distinction.
- **Inline conditional dispatch on each step's result.** A chain that
  reads `if step_result.is_error: return early` between each pair of
  steps is implementing railway by hand without the composer
  abstraction. The hand-written form is brittle to refactor (insertion
  of a new step requires editing every following block).
- **Sentinel-value short-circuit.** Returning `None` or `-1` or a
  magic string to signal failure is not railway — it is sentinel
  encoding. The two-tag type with two payload variants is what makes
  the failure rail observable and the step composition safe.
- **Pipeline-with-Result-shaped values.** A pipeline (per pattern
  pipeline.md) where each step's input and output happens to be a
  `Result` type but the pipeline composer does not short-circuit on
  failure is not railway — it is a pipeline that ignores the failure
  variant. The short-circuit-on-failure semantics is what makes the
  composition railway-shaped, not the Result-typed step boundaries.
- **Interceptor stack with error-aware traversal.** Interceptors have
  their own three-traversal machinery (enter / leave / error) that
  handles failure via the error-handler chain. The two are not
  interchangeable: railway is value-typed and chainable as functions;
  interceptors are state-machine nodes. Choosing one forecloses the
  other for the same problem.

## Judgment probes

These checks verify that a function actually implements the railway
pattern:

- **Tagged-union return type at every step.** Walk the chain steps.
  Does each step return a two-tag union with a success variant and a
  failure variant? If any step returns a bare value (success-only
  type), the railway shape is broken at that step.
- **Composer dispatch present.** Is there a single composition
  operator (named `bind` / `and_then` / `flat_map` / `?` / `chain` /
  language equivalent) that handles the failure short-circuit
  automatically? If composition is done with hand-written conditional
  blocks between every pair of steps, the abstraction is missing —
  the steps may type-check but the pattern's benefit is unrealized.
- **Failure-value inspectability.** Can a caller of the chain inspect
  what went wrong on the failure path — error code, originating step,
  contextual details? If the failure variant carries only an opaque
  string, observability is compromised.
- **Single recovery site (or explicit recovery operators).** Are
  recoveries from failure back to success concentrated at well-defined
  sites — end-of-chain handler, explicit `recover` operator, explicit
  `map_err` for transforming — rather than scattered conditional
  branches between every two steps?
- **Step purity vs effect placement.** Each step is itself a function
  `(success_value) -> Result`. Whether the step body does effect work
  is a separate question (effect work in a step is admissible — the
  step calls an effect function and wraps the outcome in a Result).
  But step bodies must not have *additional* short-circuit behavior
  outside the Result mechanism (no `raise` for control flow, no
  `return None` mid-step).
- **No silent failure swallow.** Is there any step in the chain whose
  body catches a failure and returns a default success value silently?
  If yes, the failure rail is being short-circuited in the wrong
  direction — failure-to-success transitions must be explicit
  recoveries, not silent swallows.

## Render: Python

Python's idiomatic railway rendering is the project's existing `_check`
envelope shape from `forge_manager_mcp/server/handlers/release.py`:
`{"name": str, "ok": bool, "severity": str, "detail": str}`. The `ok`
field is the success/failure tag; `detail` carries the structured failure
message on the failure rail and the success summary on the success rail.
Composition runs via a `bind` helper that short-circuits on `ok=False`.

A pure-Python `Result[T, E]` variant using `typing.Union` of two frozen
dataclasses is the other admissible rendering and is preferable when the
success rail carries a non-dict value the chain needs to type-check.

```python
from collections.abc import Callable
from dataclasses import dataclass
from functools import reduce
from typing import Generic, TypeVar, Union

T = TypeVar("T")
U = TypeVar("U")
E = TypeVar("E")

@dataclass(frozen=True, slots=True)
class Ok(Generic[T]):
    value: T

@dataclass(frozen=True, slots=True)
class Err(Generic[E]):
    error: E
    step: str  # originating step name — inspectability per Failure-value invariant

Result = Union[Ok[T], Err[E]]
Step = Callable[[T], Result]

def bind(result: Result, step: Step) -> Result:
    """Single composition operator. Short-circuits on Err."""
    if isinstance(result, Err):
        return result  # failure rail — skip the step
    return step(result.value)

def chain(initial: Result, steps: list[Step]) -> Result:
    """Compose a railway chain via fold over bind. Identity if steps=[]."""
    return reduce(bind, steps, initial)

# Example: each step returns a Result.
def parse_int(s: str) -> Result:
    try:
        return Ok(int(s))
    except ValueError as exc:
        return Err(error=str(exc), step="parse_int")

def must_be_positive(n: int) -> Result:
    if n <= 0:
        return Err(error=f"expected > 0, got {n}", step="must_be_positive")
    return Ok(n)

def double(n: int) -> Result:
    return Ok(n * 2)

result = chain(Ok("21"), [
    lambda v: parse_int(v),
    must_be_positive,
    double,
])
# result is Ok(value=42); a failing input yields Err with originating step.
```

The project's `_check`-envelope rendering (release.py) is the same shape
in dict form — the success/failure tag is `ok`, the failure detail is a
structured string, and gate functions like `_check_milestone_open_issues
_against_cut` are the steps. The orchestrator's check list is the chain.

A third blessed rendering is the **envelope-with-exception-rail** shape
used by `replication/manager.py` (#645). The success rail is carried by a
named envelope (`WriteResult[T]`) or a failure-rail collection of
structured warnings (`list[MirrorWarning]` — each `MirrorWarning` carries
`platform_id` + `error_type` + `error_message` for inspectability), and a
**fatal** rail short-circuits by raising a structured, `from`-chained
wrapper (`raise ReplicationWriteError(...) from exc`). The raise is *not*
exception-default control flow (§Forecloses); it is the documented
exception-to-Result adapter for a rail whose contract is "the
source-of-truth write failed, so the whole operation is fatal". A step
that returns a bare success value (`T`) with this exception-rail as its
only failure path is railway-shaped: success carries the value, failure
carries the structured raised error.

### Probes

- **two-tag return type.** Each step function's return annotation must
  resolve to `Result`, `Ok | Err`, or the project's envelope-dict
  TypedDict. AST: walk `FunctionDef.returns`; reject bare `int` / `str`
  / `dict` returns from steps registered in the chain.
- **bind/chain composer present.** The composition vehicle is a named
  function (`bind`, `and_then`, `chain`, or the project's envelope
  fold). AST: search the module for at least one `FunctionDef` whose
  body's first statement is an `isinstance(result, Err)` guard returning
  the result unchanged. Absence means hand-rolled if-blocks between
  every pair of steps (Forecloses §Inline conditional dispatch).
- **failure variant carries structured detail.** Walk `Err` (or
  envelope-dict `ok=False`) construction sites. Each site must populate
  at least: an `error` / `detail` field AND a `step` / `name`
  identifying the originating step. A bare `Err("oops")` with no step
  attribution fails Failure-value-inspectability.
- **no silent failure swallow.** AST-walk each step for `try` / `except`
  blocks whose `except` handler returns an `Ok` construction without
  having converted a structured input into a recovery — i.e., catches a
  step's failure and pretends success. Flag any `except` handler whose
  body's final `return` is `Ok(...)` and whose body does not call a
  named recovery function.
- **no `raise` for control flow inside steps.** AST: walk step bodies
  for `Raise` nodes. Each Raise must either re-raise (`raise` alone) or
  be wrapped in a documented "exception-to-Result" adapter call. A bare
  `raise SomeError(...)` inside a step body violates Pure-step
  composition (the chain expects values, not exceptions).
- **single-site recovery.** Count the call sites that match `Ok(...)`
  construction inside an `if isinstance(..., Err)` branch (the
  failure-to-success recovery shape). If recoveries appear at more than
  one site outside a named `recover()` helper, the recovery is scattered
  rather than concentrated.

### Example

`mcp-server/src/forge_manager_mcp/server/handlers/release.py` —
`_check` produces the success/failure envelope, and the gate helpers
(`_check_milestone_open_issues_against_cut`, `_check_no_feature_variance`,
etc.) are railway-shaped: each returns the envelope, and the
orchestrator concatenates them into a check-list that short-circuits at
the policy layer on the first severity=fail entry.

### Anti-patterns

- **Bare `raise ValueError(...)` instead of `Err(...)`.** A step that
  signals failure by raising forces every caller to wrap the chain in
  `try` / `except`. The failure value is unobservable inside the chain;
  composition cannot proceed past the raise without unwinding the stack.
  Violates Forecloses §Exception-based error propagation.
- **`Optional[T]` return with `None` meaning failure.** A step
  returning `T | None` is sentinel-encoded failure: the caller cannot
  inspect what went wrong, only that something did. Violates Forecloses
  §Sentinel-value short-circuit and Failure-value inspectability.
- **Hand-written `if result is None: return None` between every pair of
  steps.** Pipeline-with-Result-shaped values where the composer does
  not short-circuit — each step's caller checks the failure and returns
  early. Violates Forecloses §Pipeline-with-Result-shaped values and is
  brittle to refactor (inserting a step requires editing every
  surrounding block).

## Render: Go

Go renders railway via its native `(T, error)` multi-return idiom.
There is no `Result[T, E]` sum type in the language; the
two-valued result is the pair `(value, nil)` on the success rail and
`(zero, err)` on the failure rail. The composer abstraction is
trickier — Go does not natively short-circuit `(T, error)` pairs the
way Rust's `?` or Haskell's `>>=` does — so railway in Go is rendered
in one of two shapes: (a) hand-written `if err != nil { return zero,
err }` per step, which the language community considers idiomatic but
which fails the "composer dispatch present" probe; or (b) an explicit
`Result[T]` generic carrier with a `Bind` operator, which makes the
composition explicit at the cost of fighting the language. The pattern
prefers (b) for designs that genuinely need railway-style chaining;
(a) is admissible for short chains where the per-step `if err` block
is visually compact.

```go
package railway

// Result carries the two-rail value. Ok holds the success value when
// Err is nil; when Err is non-nil, Ok is the zero value and ignored.
type Result[T any] struct {
	Ok  T
	Err error
}

// Success and Failure are the two-tag constructors. Naming them
// makes the rail explicit at construction sites.
func Success[T any](v T) Result[T]      { return Result[T]{Ok: v} }
func Failure[T any](e error) Result[T]  { return Result[T]{Err: e} }

// Bind is the composer: it propagates failure without invoking step,
// and threads success through. The empty (identity) step is
// `func(v T) Result[T] { return Success(v) }` — composes into any
// chain without changing the result.
func Bind[T, U any](r Result[T], step func(T) Result[U]) Result[U] {
	if r.Err != nil {
		return Failure[U](r.Err)
	}
	return step(r.Ok)
}

// Recover converts a failure back to a success at an explicit site.
// Recovery elsewhere is forbidden; this is the single-site recovery
// invariant.
func Recover[T any](r Result[T], rescue func(error) Result[T]) Result[T] {
	if r.Err == nil {
		return r
	}
	return rescue(r.Err)
}

// MapErr transforms the failure value without changing the rail.
func MapErr[T any](r Result[T], f func(error) error) Result[T] {
	if r.Err == nil {
		return r
	}
	return Failure[T](f(r.Err))
}

// Example: parse, validate, persist.
//
//   r := Bind(
//       Bind(parseInput(raw), validateUser),
//       persistUser,
//   )
//   final := Recover(r, func(e error) Result[User] {
//       log.Print(e); return Success(User{Anonymous: true})
//   })
```

Structured failure values use Go's `errors.As` / `errors.Is`
mechanism — the failure variant is the standard `error` interface,
with concrete types implementing it for inspectability:

```go
type ValidationError struct {
	Field   string
	Step    string
	Message string
}

func (e *ValidationError) Error() string { return e.Message }
```

### Probes

- **Result-type return signature (AST).** Each chain step must declare
  return type `Result[T]` (or the project's named equivalent), not a
  bare `T` and not `(T, error)`. AST walk: for each function passed to
  `Bind`, check `*ast.FuncType.Results.List` resolves to the named
  `Result` type. If a step returns `(T, error)`, the chain has dropped
  to the hand-written conditional form and the composer abstraction is
  missing.
- **Composer dispatch present.** Locate the project's `Bind` (or
  `AndThen`, `Chain`) and verify the chain uses it. Probe: walk a
  chain site and count `Bind` calls vs `if err != nil { return zero,
  err }` blocks between step calls. Multiple manual conditional blocks
  between two step calls = hand-written railway = composer missing.
- **Failure-value inspectability.** Walk Failure construction sites
  (`Failure[T](err)`). The `err` value passed in must be a concrete
  named type implementing `error` (so callers can `errors.As` it), not
  a bare `errors.New("msg")` string-only error. String-only errors
  pass the type check but fail observability.
- **Single recovery site (or explicit recover operators).** Search
  the codebase for `Recover` (or `MapErr`, `RescueWith`) call sites.
  These should appear at chain endpoints or at explicit recovery
  boundaries, not interleaved between every pair of `Bind` calls.
  Interleaved recovery is hand-rolled railway in disguise.
- **No silent failure swallow.** AST walk each step body: flag any
  step that catches an error (Go: a return of `Success(zero)` after
  receiving a non-nil error from an internal call) without surfacing
  it through the Result type. Specifically: a step body containing
  `if err != nil { return Success(defaultValue) }` is a silent
  failure-to-success transition — the failure rail is being
  short-circuited the wrong direction.
- **No panic for control flow.** Step bodies must not `panic` to
  signal failure (with a recover at the chain boundary that converts
  panic to Result). Panic-based control flow defeats the value-typed
  failure invariant. AST walk for `*ast.CallExpr` with `Fun.Name ==
  "panic"` inside step bodies.

### Example

The Go stdlib `errors` package — `errors.Is`, `errors.As`,
`errors.Join` — provides the failure-value inspectability machinery
the railway pattern requires. For a third-party explicit Result type,
`github.com/samber/mo` exposes `Result[T]` with `Bind`-style
composition; `github.com/destel/rill` exposes pipeline-of-results
combinators. The Go community's dominant idiom remains
hand-written `if err != nil`, which renders the pattern partially.

### Anti-patterns

- **`if err != nil { return zero, err }` per step with no composer.**
  Idiomatic Go but fails the composer-dispatch probe. The pattern
  prescribes a `Bind` operator; without it, inserting a new step
  requires editing every following conditional block — the
  refactor-resistance property railway is supposed to provide is
  absent.
- **Returning `(T, error)` from a chain step.** The two-rail shape is
  carried by the `Result[T]` carrier; multi-return shifts the
  short-circuit logic to the caller. AST probe should flag this as
  not-railway even though it is idiomatic Go.
- **`panic` + `recover` for control flow.** A step that panics with a
  typed error and a chain wrapper that recovers and wraps as a
  `Result.Failure` looks like railway but uses exception-shaped
  control flow underneath. The failure value lifecycle is then
  unobservable to static analysis.
- **Sentinel `nil` or `""` to signal failure.** A step returning
  `Success("")` to mean "skip this and treat as failure downstream" is
  sentinel encoding. The Result carrier exists precisely to make the
  failure rail explicit; sentinel values defeat its purpose.

## Render: Ada

Ada's variant records (discriminated records with a `case`
component) are an unusually clean fit for the Result type: the
discriminant is the success / failure tag, and the variant part
holds the payload — the compiler enforces at every read that the
caller has inspected the discriminant before touching the variant.
The `Bind` composer is a generic function over success and failure
payload types; failure short-circuits via a `case Status is` block.
Contracts on the result type catch sentinel-encoding drift: a
`Type_Invariant` asserts the failure branch always carries a
non-default error value.

```ada
generic
   type T is private;
   type E is private;
   Default_E : E;
package Railway is

   type Outcome_Kind is (Ok, Err);

   --  Discriminated record: the compiler refuses to read Value when
   --  Status = Err and refuses to read Error when Status = Ok.
   --  Sentinel encoding (returning a default T with an out-of-band
   --  flag) is structurally impossible.
   type Result (Status : Outcome_Kind := Ok) is record
      case Status is
         when Ok  => Value : T;
         when Err => Error : E;
      end case;
   end record;

   function Success (V : T) return Result is
     (Result'(Status => Ok, Value => V));

   function Failure (Why : E) return Result is
     (Result'(Status => Err, Error => Why));

   --  Bind: success short-circuits past Step on Err, calls Step on Ok.
   --  Step itself is a one-argument function returning Result — pure
   --  composition, no exceptions.
   generic
      with function Step (V : T) return Result;
   function Bind (R : Result) return Result;

   --  Single-site recovery: convert Err back to Ok (or propagate).
   generic
      with function Handler (Err : E) return Result;
   function Recover (R : Result) return Result;

   --  Identity step admissible: Identity (V) = Success (V) trivially.
   function Identity (V : T) return Result is (Success (V));

end Railway;

package body Railway is

   function Bind (R : Result) return Result is
   begin
      case R.Status is
         when Ok  => return Step (R.Value);
         when Err => return R;        --  short-circuit
      end case;
   end Bind;

   function Recover (R : Result) return Result is
   begin
      case R.Status is
         when Ok  => return R;
         when Err => return Handler (R.Error);
      end case;
   end Recover;

end Railway;
```

### Probes

- **Result is a discriminated record with a `case` part.** The
  Result type declaration must be a `type ... (Status : ...)
  is record case Status is ...` form. A type that is a tagged-class
  hierarchy (abstract `Result` with `Ok_Result`, `Err_Result`
  subclasses) is admissible but loses the compiler's variant-access
  check; the discriminated record form is the load-bearing shape.
  (libadalang: walk `TypeDecl` for `Result`; assert
  `f_type_def.f_record_def.f_components.f_components` contains a
  `ComponentDecl` whose declaration is a `Variant_Part`.)
- **Discriminant is an enumeration with exactly two values.** The
  discriminant subtype must be an enumeration `(Ok, Err)` (or
  equivalent two-value enumeration). A Boolean discriminant is
  admissible; a discriminant with three or more values means the
  pattern has drifted into a richer outcome shape and is no longer
  railway.
- **Bind body uses `case` dispatch, not `if`-on-`Status`.** The
  Bind function body must dispatch via `case R.Status is when Ok
  => ... when Err => ...`. An `if R.Status = Ok then ... else ...`
  form is admissible semantically but loses the compiler's
  exhaustiveness check; the case form is what makes adding a third
  discriminant value a compile error.
- **No `raise` inside the Bind or Step bodies.** Walk the
  declarations and bodies in the package; assert no `RaiseStmt` is
  reachable from Bind or from any Step instance. Exception-based
  short-circuit defeats the value-typed failure rail.
- **Identity function declared and returns Success.** The package
  must export a function (or expression function) `Identity` whose
  body is `Success (V)` — the railway monoid identity. Without it
  the identity element is unreachable from clients.
- **Failure constructor populates the Err discriminant.** The
  `Failure` factory's body must return an aggregate with
  `Status => Err` and a non-default `Error` payload. A factory that
  returns `Result'(Status => Ok, Value => <default>)` and signals
  failure via a side channel violates the inspectability invariant.

### Example

The SPARK 2014 examples under
`spark2014/testsuite/gnatprove/tests/optional_*/` use discriminated
records to express Option / Result patterns with provable absence of
run-time errors. AdaCore's `Ada.Containers.Indefinite_Holders` is a
related sum-type-flavoured container worth pointing at.

### Anti-patterns

- **Out-parameter "result" with status flag.** A subprogram
  declared `procedure Step (Input : T; Output : out T; Status : out
  Outcome_Kind)` — the discriminant and the payload have been
  pulled apart into separate out parameters, so the compiler's
  variant-access check is bypassed. Looks idiomatic in Ada, breaks
  the pattern.
- **Exception-shaped chain.** A "railway" where each step's body
  raises a custom exception (`raise Validation_Error`) and the
  outer driver wraps the chain in a single `exception when
  Validation_Error => ...`. The shape is exception-default error
  handling, not railway — same short-circuit semantics, no
  inspectable failure value.
- **Generic `Result` with class-wide `Error : Exception_Occurrence`
  payload.** Encoding the error variant as an
  `Exception_Occurrence` (Ada's reified exception state)
  technically satisfies the variant-record shape, but the error
  is opaque — `Exception_Message (Error)` is the only inspection
  vehicle. Failure inspectability is broken without a structured
  error type.

## Render: TypeScript

TypeScript's idiomatic railway rendering is a discriminated union —
`{ok: true, value} | {ok: false, error}` — with early-return chaining.
The union tag makes the two rails a *type*; the compiler enforces that
every consumer branches before touching `value`. Domain failures ride
the failure rail as values; `throw` is reserved for genuinely fatal,
non-domain conditions.

```typescript
type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const parsePort = (raw: string): Result<number, string> => {
  const n = Number(raw);
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
    return { ok: false, error: `not a port: ${raw}` };
  }
  return { ok: true, value: n };
};

const connect = (raw: string): Result<string, string> => {
  const port = parsePort(raw);
  if (!port.ok) return port;            // short-circuit onto the failure rail
  return { ok: true, value: `daemon:${port.value}` };
};
```

### Probes

Mechanical (wired in `_TYPESCRIPT_PROBES`):

- **discriminated_result_type.** The function's text carries a tagged
  two-rail carrier — an `ok:` / `kind:` discriminant (union type or
  `{ ok: true, ... }` / `{ ok: false, ... }` literals). A bare success
  return with no discriminant is not railway-shaped.
- **no_throw_on_domain_path.** No `throw` inside the chained steps —
  a domain failure returns the failure rail; `throw` for control flow
  collapses the two rails back into an exception channel (Forecloses
  §Raise for control flow).

Judgment (prose-only):

- **single failure-handling site.** Failures skip forward without
  further transformation; intermediate steps do not catch-and-patch.
- **no compensation.** If enter/leave/error unwind is needed, the
  shape is interceptor, not railway.

### Example

The MCP server's `WriteResult`-style envelopes mirror this union in
Python; the SolidJS UI's fetch wrappers returning
`{ ok, data } | { ok: false, error }` are the TS consumers.

### Anti-patterns

- **`throw new Error(...)` for a domain failure.** The failure rail
  exists so the caller branches on data, not on `try/catch`.
- **`ok?: boolean` optional tag.** The discriminant must be required
  and literal-typed — an optional tag re-opens the untyped middle.
