ADR-0002: Error branches are required config properties

Status: accepted, 2026-08-31

Context

Task A01 requires that "an unwired error branch is a type error". Two obvious implementations exist, and they interact badly with a different non-negotiable.

A fluent builder (flow.play(...).then(...).onError(...)) can enforce wiring at the type level only through phantom types or builder-state threading, where each call returns a different type carrying what has been wired so far.

That collides with the codegen requirement: "Codegen output must be idiomatic, diffable TypeScript that a human would plausibly have written." State-threaded fluent chains are not what a human writes, and they are painful to emit and to diff.

Decision

Blocks are classes taking a single config object. Error branches are required properties on that object.

new TransferContactToQueue({
          id: "transfer",
          next: "hang-up",
          onQueueAtCapacity: "announce-busy",
          onError: "apologize",
        });
        

Mutually exclusive parameters use discriminated unions, so supplying both text and ssml, or both queue and agent, is also a compile-time error.

Consequences

Omitting a required property is a genuine tsc error, verified in both directions in packages/core/src/synth.test.ts: removing a @ts-expect-error produces TS2345, and an unnecessary one produces TS2578, so the assertions cannot rot into no-ops.

Codegen emits plain object literals, which diff cleanly line by line.

The escape hatch is untyped by necessity. GenericBlock models actions we do not know, so we cannot know their error sets either. The error-branches lint rule is the backstop there.

Blocks map one-to-one onto Actions for the same reason. A block that expanded into several Actions could not be reliably recognized on the way back, which would break the round-trip invariant. "Transfer to queue" is therefore two blocks, UpdateContactTargetQueue and TransferContactToQueue, because that is what Connect actually models.