Guide

Diagnostics

Distinguish refusals, typed failures, author defects, and operational failure.

The system distinguishes four outcomes because each one preserves a different boundary. A refusal is a successful decision not to handle a valid stimulus. A typed failure rejects data or an operation described by the public contract. An author defect means authored or host code broke an invariant that was required to be total. Operational failure belongs to engine status and recovery, outside the chart's own state, event, and transition vocabulary.

Preserve refusal as a successful disposition

A valid external event that selects no transition produces an Unhandled fold outcome. The engine reports it to the sender as Refused. The snapshot is unchanged, the sequence does not advance, no successful step is appended, and the running instance remains usable.

Refusal is useful information. It says the event crossed its external boundary but the current configuration assigned it no enabled behavior. Converting that result to a generic exception would make “the chart deliberately did not handle this” indistinguishable from “the chart could not evaluate this.”

Raised internal events use a different delivery contract. When no transition matches, the interpreter drops the raise while continuing the current macrostep. There is no external caller awaiting a refusal result.

Selection details belong on the events and refusal concept page. This page starts with the disposition and follows its evidence to the remedy.

Return typed failures at owned boundaries

Typed failures describe conditions the boundary declares and callers can reason about.

| Failure | Meaning | Failure direction | | --------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | Schema decode failure | Chart validation failures are Schema decode failures with a structured issue tree. | Assembly or validation fails with all discovered issues. | | ReservedEvent | External input attempted to use interpreter-owned vocabulary. | Admission stops before selection. | | MalformedEvent | A known authored tag carried fields rejected by its Schema. | Decoding fails before any binding or chart work. | | MicrostepLimitExceeded | The macrostep would exceed 256 microsteps. | The unfinished macrostep publishes nothing. | | InconsistentConfiguration | Supplied or produced configuration or history violates chart invariants. | The step rejects the inconsistent state instead of publishing it. | | StatechartDone | A sender addressed a completed instance. | No later event is admitted. | | StatechartStopped | A sender addressed a stopped or cancelled instance. | No later event is admitted. |

These values remain separate from storage failures and runtime defects. A Schema failure is not a chart refusal. A terminal-instance error is not an unhandled event. A chart-validation issue is not an engine crash.

Foreign durable values must cross checked decoders before becoming domain values. Unknown or obsolete output shapes fail loudly instead of entering a fallback branch that guesses their meaning. Boundary compatibility must be an explicit contract, not reconstruction from a looser object.

A typed failure is also different from a compile-time authoring diagnostic. A typed failure rejects a value or operation at a declared boundary. An authoring diagnostic rejects the authored chart shape at the site where its obligation was created.

Read named authoring diagnostics as laws

The authoring surface rejects structurally impossible or semantically incomplete charts before they run. Each diagnostic has a stable identity and states the violated law and the corrective direction.

Read a diagnostic in this order:

  • The named interface is the rule statement. TimerDoneNeedsCoActiveListener says that a timer's completion listener must be reachable from its mount.
  • The law field, when present, carries the shared law name. For example, both activity and child channel totality diagnostics use "ChartTotality". The specific interface name still says which obligation failed.
  • The literal message says what fact failed and what to change. At the validation boundary, formatted diagnostic text follows the shape <where>: <Law>: ...; the final part is the remedy, not decoration. The type-level message is the same remedy-bearing text before that location and law prefix is added.
  • The source location points to the authored site of the bug. A type may become visible at Statechart.make, but the remedy belongs in the timer mount, activity invocation, listener, query, or transition that created the obligation. Read the authored site, not engine internals.

The following witness deliberately omits the input producer for a fielded timer. The refusal is under @ts-expect-error so the witness compiler must prove that the error fires and still exit successfully.

import { Schema } from "effect";
import { State, Timer, Transitions } from "@motive/motive";

const Deadline = Timer.relative("Deadline", {
  input: Schema.Struct({ milliseconds: Schema.Finite }),
});

class Waiting extends State.Atomic<Waiting>()("Waiting") {
  static transitions = Transitions.make(this, ({ arm }) => [
    // @ts-expect-error ArmOfFieldedTimerNeedsProducer: required timer input is missing
    arm(Deadline),
  ]);
}
void Waiting;

The returned named type is ArmOfFieldedTimerNeedsProducer<"Deadline">. Its literal message is arm(Deadline) declares required input; supply it as arm(Deadline, (args) => input). The message tells you to build the input at the mount. It does not suggest a cast or a weaker representation.

Totality diagnostics make the same authored-site distinction at assembly. An invoked activity must have a disposition for every channel it can produce. A mounted timer must have a provably co-active Done listener. These are different named laws even though both prevent an unobserved completion.

import { Schema } from "effect";
import { Activity, State, Statechart, Timer, Transitions } from "@motive/motive";

const Charge = Activity.make("Charge", {
  success: Schema.Struct({ receipt: Schema.String }),
  error: Schema.Struct({ reason: Schema.String }),
});
const ChargeAttempt = Charge.as("ChargeAttempt");

class Charging extends State.Atomic<Charging>()("Charging") {
  static transitions = Transitions.make(this, ({ invoke }) => [
    invoke(ChargeAttempt),
  ]);
}

// @ts-expect-error ActivityChannelMustHaveTotalDisposition: Done and Error are unhandled
Statechart.make(Charging);

const Pulse = Timer.relative("Pulse");

class Pulsing extends State.Atomic<Pulsing>()("Pulsing") {
  static transitions = Transitions.make(this, ({ arm }) => [
    arm(Pulse),
  ]);
}

// @ts-expect-error TimerDoneNeedsCoActiveListener: the mount has no Done listener
Statechart.make(Pulsing);

The unsuppressed compiler output names ActivityChannelMustHaveTotalDisposition<"Charging:ChargeAttempt.Done" | "Charging:ChargeAttempt.Error"> and TimerDoneNeedsCoActiveListener<"Pulse", TimerOccurrence<"Pulse", "Pulse", "default">>. Those names point back to Charging and Pulsing, the authored sites that need a remedy.

The legal form is structural. Supply the required timer input and write down the activity and timer dispositions at a state that is co-active with their mounts. A bare completion listener is an explicit successful disposition.

import { Schema } from "effect";
import { Activity, State, Statechart, Timer, Transitions } from "@motive/motive";

const Deadline = Timer.relative("DeadlineRemedy", {
  input: Schema.Struct({ milliseconds: Schema.Finite }),
});

class Waiting extends State.Atomic<Waiting>()("WaitingRemedy") {
  static transitions = Transitions.make(this, ({ arm, on }) => [
    arm(Deadline, () => ({ milliseconds: 1_000 })),
    on(Deadline.Done),
  ]);
}

const Charge = Activity.make("ChargeRemedy", {
  success: Schema.Struct({ receipt: Schema.String }),
  error: Schema.Struct({ reason: Schema.String }),
});
const ChargeAttempt = Charge.as("ChargeAttemptRemedy");

class Charging extends State.Atomic<Charging>()("ChargingRemedy") {
  static transitions = Transitions.make(this, ({ invoke, on }) => [
    invoke(ChargeAttempt),
    on(ChargeAttempt.Done),
    on(ChargeAttempt.Error),
  ]);
}

const WaitingChart = Statechart.make(Waiting);
const ChargingChart = Statechart.make(Charging);
void WaitingChart;
void ChargingChart;

Typical law families include:

  • a final or history state cannot own outgoing behavior;
  • an eventless transition with neither a target nor a guard is rejected, while more elaborate cycles remain subject to the microstep limit;
  • an eligible self or descendant transition is internal by default, and explicit re-entry remains limited to self or descendant targets;
  • an authored completion listener must name a completion source that can actually exist;
  • singular timers, activities, and children must have co-active dispositions for every completion channel they can produce;
  • a closed family must observe aggregate Done and, when fallible, one of Error or MemberError; MemberDone remains optional; and
  • a boundary action declaration must contain at least one action.

The complete named catalog at this grounding is below. The remedy column keeps the message's direction in public language.

| Named diagnostic(s) | Shared law field | Read the rule and remedy | | ------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------- | | ReenterNeedsSelfOrDescendantTarget | ReentryChangesTheSourceDomain | Use reenter() only with a self target or a descendant target. | | BoundaryBranchNeedsActions | Message only | Add at least one action link to entry() or exit(), or remove the arm. | | TriggerUnionNeedsAtLeastTwoMembers | TriggerUnionNeedsAtLeastTwoMembers | Put at least two event schemas in the trigger union. | | EntryPathStateMayBeSuppliedOnce | EntryPathStateMayBeSuppliedOnce | Supply each entry-path state at most once. | | UpdateTargetMustBeProvablyActive | Message fields identify target and source | Update only a state provably active at the authored source. | | StrictReadMustBeProvablyActive | Message fields identify read and source | Use a provable strict read, or Query.option for a genuinely uncertain branch. |

| Named diagnostic(s) | Shared law field | Read the rule and remedy | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | ActivityChannelMustHaveTotalDisposition | ChartTotality | Dispose every terminal channel of every armed activity. | | ChildChannelMustHaveTotalDisposition | ChartTotality | Dispose every terminal channel of every armed child. | | TimerDoneNeedsCoActiveListener | TimerDoneNeedsCoActiveListener | Add a provably co-active on(Timer.Done, ...) listener. | | ActivityFamilySettlementNeedsCoActiveListener | ActivityFamilySettlementNeedsCoActiveListener | Handle or explicitly ignore aggregate Done; for a fallible family, handle or ignore Error or MemberError. | | ActivityFamilyRequiresEntries, ChildrenRequireEntries | ActivityFamilyRequiresEntries for the activity diagnostic; message fields for the child diagnostic | Supply the closed family membership entries at invoke or spawn. | | ChartDefaultOccurrenceIsStaged | ChartDefaultOccurrenceIsStaged | Declare a named chart site with .as("Site") before spawn. | | ArmOfFieldedTimerNeedsProducer | ArmOfFieldedTimerNeedsProducer | Supply the declared timer input at arm. | | InvokeOfFieldedActivityNeedsInput | Message only | Supply the declared activity input at invoke. |

| Named diagnostic(s) | Shared law field | Read the rule and remedy | | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------- | | CompletionPatternMustBeUnambiguous, CompletionPatternMustMatchARegion, CompletionPatternClaimsRegionTwice, CompletionPatternMustNameARegion | CompletionPatternMustNameARegion | Pin a pattern to the correct region. An empty pattern means orElse(). | | FinalHasNoBehavior, HistoryHasNoBehavior | Message only | Remove transitions from final and history states. |

The table deliberately lists the diagnostic names that exist in this authoring surface. The interface name is the rule statement, and the message is the shortest path to a valid authored shape. A shared law groups related obligations; it does not erase the site-specific fact.

The diagnostic should remain structured through tooling. Its identity, law, message, and source location carry more information than a boolean “invalid” or a rewritten string. Editors and documentation can link the named law to a focused explanation without parsing prose.

A diagnostic is not a compatibility fallback. The fix is to make the chart's meaning total or remove the impossible declaration. Wrapping the same shape in an adapter does not discharge the law.

Contain author defects without partial commit

The pure fold may call authored producers and runtime bindings that are assumed to be total at their semantic boundary. If one throws, returns an invalid shape, or otherwise breaks that assumption, the authored or host computation is defective. The event does not become an authored error event.

The engine contains the defect according to where it occurred:

| Defect seat | Engine disposition | Commit guarantee | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | External send fold or pre-commit resolution | The sender fails with a structured step defect. The instance remains usable. | No proposed snapshot, mounted work, plan, or timeline entry is published. | | Machine-scheduled fold or pre-commit resolution | The instance faults after the engine's bounded delivery policy. Sibling instances remain usable. | The faulting step has no partial commit. | | Instantiation | Creation fails as a contained defect. | No observable runtime or instance record is published. | | Post-commit activity or emit enactment | The instance faults without undoing the accepted step. | The committed snapshot, history, and intent remain observable. |

The distinction between pre-commit and post-commit is decisive. Before commit, atomicity requires complete absence of the proposed step. After commit, atomicity requires preserving the facts that recovery must resume. Rolling back a committed snapshot because later enactment defected would erase the durable cause of the operational failure.

An authoring diagnostic is earlier and narrower. It points at a chart declaration that can be corrected before assembly. An author defect is a non-total computation at an authored site or host binding while the chart is being processed. Both are fault-loud, but their remedies belong to different boundaries.

Separate faulted from parked

Durable engines need two operational failure directions because “cannot run” is too weak to guide recovery.

A faulted instance has a valid stored snapshot: the past is trustworthy, but machine-scheduled progress is blocked. Reads can still serve the committed head. External sends may heal or complete the chart. Outbound work that was already committed remains governed by its delivery lifecycle.

A parked instance has an untrustworthy stored past: the snapshot cannot be accepted under the registered chart or durable identity. Author-facing reads and sends reject rather than pretending the value is current truth. Operator inspection must preserve the reason and the evidence needed to decide whether recovery is possible.

Neither status is a chart state, an event, or a transition target. They belong to engine operations and committed inspection. This is the operational failure branch of the four-way split, not a new chart status to handle with an event.

Choose the remedy at the owning boundary

Use the disposition to select the remedy:

  • For a refusal, decide whether the chart should add behavior or the caller should accept non-handling.
  • For a typed failure, correct the value or operation at the boundary that rejected it.
  • For an authoring diagnostic, make the chart satisfy the named law at the authored site identified by the message.
  • For a pre-commit defect, correct the non-total authored or host computation; do not infer that any part of the step happened.
  • For a post-commit defect, begin from the committed snapshot and intent; do not replay from an imagined pre-step state.
  • For a faulted or parked instance, use operational inspection and recovery authority rather than chart events that cannot describe the condition.

The doctrine is fault-loud and information-preserving: each failed boundary retains enough structure to identify what was refused, what was invalid, what defected, and what (if anything) committed.