# Event-First Statecharts

Target SDK: `helixwright==1.0.0`, Browser RPC protocol `6`.

Use `StateMachine` for any flow in which the current page may branch, repeat,
hydrate asynchronously, open a frame/popup, show an overlay, recover from an error, or
resume from a partially completed state. A statechart replaces assumptions about the
previous line with classification of the current browser state.

## Runtime cycle

Every transition uses this fixed sequence:

1. classify all state signatures from one `state.snapshot` target/frame/document epoch;
2. open a transition-owned `events.subscribe` cursor before dispatch;
3. run one state handler and require its exact `RuntimeActionEffect`;
4. validate the action's own 22-field receipt and final dispatch phase;
5. evaluate typed outcome signals causally bound to that action;
6. reclassify from a confirming atomic snapshot.

There is no configurable polling/fallback observation path. A stream gap, overflow,
unsupported delivery, or sequence discontinuity triggers explicit resynchronization;
ordinary transitions remain event-first.

## Serializable signatures

`State.match` must be an exact `Signature`, not a callable:

```python
form_signature = hw.Signature(
    name="form",
    url_substr="/checkout",
    require_selectors=["#checkout-form", "#submit"],
    absent_selectors=["#complete", "#server-error"],
    texts=["Checkout"],
    form_fields=["email", "country"],
)
```

Supported identity features are:

- `url_substr`;
- positive `title_substrs` and negative `absent_title_substrs`;
- `require_selectors`;
- `absent_selectors`;
- landmark `texts` (compiled to structured text selectors);
- `form_fields` (compiled to name selectors);
- non-negative weights for URL, title, selectors, absence, text, and fields.

At least one positively weighted feature is required. Feature lists may not contain
empty values, duplicates, or a selector that is both required and absent. Two states
with identical serialized signatures are rejected at construction.

`Signature.to_dict()` and `Signature.from_dict()` are the stable serialization surface.
Use `page.probe.signature(...)` and repeated Trace runs during authoring, then keep only
strong, orthogonal features.

## State definition

```python
def submit(ctx):
    return ctx.scope.locator("#submit").click(
        ready=ctx.ready,
        timeout_ms=15_000,
    )

ready = hw.ReadinessContract(
    all_of=(hw.ReadinessSignal.element_state("#submit", "enabled"),),
    quiet_ms=100,
)

state = hw.State(
    name="form",
    match=form_signature,
    ready=ready,
    handler=submit,
    expect=hw.OutcomeContract(
        success_states=["done"],
        branch_states=["server_error"],
        progress=[hw.signals.DomChanged()],
        failure=[hw.signals.Appeared("#validation-error")],
        no_effect="report",
        timeout_ms=15_000,
    ),
)
```

Rules for a non-terminal state:

- `handler` is required;
- `expect` must be an exact `OutcomeContract` with `require_receipt=True`;
- `ready`, when present, must be an exact `ReadinessContract`;
- the handler must consume `ctx.ready` in its final action and return that action's
  `RuntimeActionEffect`;
- the outcome must declare at least one positive state target or typed signal;
- `no_effect` is `report`; an acknowledged action is not blindly replayed;
- same-state progress remains in the current causal wait and is reclassified without
  replaying the handler.

Terminal states have no handler, readiness, outcome, child, or region activity.

## Passive event-first states

Use `passive=True` only when the browser observes a state change without a dispatched
automation action, such as a managed interstitial clearing itself:

```python
hw.State(
    "interstitial",
    match=hw.Signature(require_selectors=["#interstitial"]),
    passive=True,
    handler=None,
    ready=None,
    expect=hw.OutcomeContract(
        success_states=["form"],
        require_receipt=False,
        no_effect="report",
    ),
)
```

A passive state cannot define a handler or readiness contract and cannot masquerade as
an action. It still opens its own event observer, settles a declared outcome, and
reclassifies under the run deadline. Terminal states cannot also be passive.

## Complete example

```python
import helixwright as hw

def submit(ctx):
    return ctx.scope.get_by_role("button", name="Submit").click(
        ready=ctx.ready,
        timeout_ms=15_000,
    )

machine = hw.StateMachine(
    [
        hw.State(
            "form",
            match=hw.Signature(
                require_selectors=["#form", "#submit"],
                absent_selectors=["#complete"],
            ),
            ready=hw.ReadinessContract(
                all_of=(
                    hw.ReadinessSignal.attribute(
                        "#submit", "data-ready", equals="true"
                    ),
                ),
                stable_frames=3,
            ),
            handler=submit,
            expect=hw.OutcomeContract(
                success_states=["done"],
                progress=[hw.signals.DomChanged()],
                no_effect="report",
            ),
        ),
        hw.State(
            "done",
            match=hw.Signature(require_selectors=["#complete"]),
            terminal=True,
        ),
    ],
    name="submit_flow",
)

result = machine.run(
    page,
    until="done",
    max_transitions=10,
    per_state_timeout_ms=15_000,
    deadline_ms=120_000,
    failure_dir="evidence/state-failures",
)
assert result.reached_until
```

## Outcome signals

State-machine outcomes accept these exact built-in serializable signal types from
`hw.signals`:

- `StateReached`, `UrlContains`, `UrlMatches`;
- `Appeared`, `Disappeared`, `ElementState`, `ValueChanged`, `DomChanged`;
- `Network`, `ConsoleMessage`, `AxAlert`, `DialogOpened`, `DownloadStarted`.

Each signal evaluates to a four-state result: matched, unmatched, unknown, or error.
Unknown/error evidence is not silently converted to false. Arbitrary callback
predicates are not valid state-machine outcome signals because they cannot be bound to
one serializable V6 event/snapshot contract.

`OutcomeVerdict.status` may be `success`, `branch`, `progress`,
`same_state_progress`, `no_effect`, `failure`, `error`, `dispatch_unknown`,
`timeout`, or `unknown`.

## Frame-scoped states

Set `frame` when a state's handler and child machine belong to an iframe/OOPIF:

```python
hw.State(
    "payment",
    match=hw.Signature(require_selectors=["iframe#payment"]),
    frame="iframe#payment",
    ready=payment_ready,
    handler=pay,
    expect=payment_outcome,
)
```

The action executes in an immutable child frame scope. Settlement remains correlated to
the owning transition so frame replacement/removal cannot strand the machine on a
mutable global cursor.

## Child machines and regions

- `children=child_machine` models a compound state. The child runs while the parent
  state is active and returns after reaching its terminal state.
- `regions=(consent_region, notification_region)` models orthogonal concurrent state.
  Regions classify from a shared macrostep epoch and selected transitions run in a
  deterministic order.
- Do not poll an overlay or child application from the parent handler. Model it
  explicitly so its state, action, outcome, and receipt remain visible.

## Same-state progress

A page may stay in the same classifier state while each action produces real progress
(for example, loading one more batch). Declare a typed progress signal:

```python
expect=hw.OutcomeContract(
    progress=[hw.signals.DomChanged("#results")],
    no_effect="report",
    same_state_with_progress="wait_and_reclassify",
)
```

The machine records progress and reclassifies without treating the repeated state as a
new permission to replay an uncertain action. Repetition without causal progress is
bounded by oscillation detection.

## Deadlines and cancellation

`run()` has one absolute `deadline_ms` shared by classification, subscription, action,
children, regions, and outcome settlement. `per_state_timeout_ms` bounds each state,
and a state's own `per_state_timeout_ms` may narrow it. A `threading.Event` supplied as
`cancel_event` cancels the run cooperatively.

Typed failures include:

- `UnknownStateError`;
- `AmbiguousStateError`;
- `StateClassificationError`;
- `TransitionAssertionError`;
- `OscillationError`;
- `MaxTransitionsError`;
- `StateMachineCancelledError`;
- `StateMachineDeadlineError`.

On failure, retain the private Trace, atomic classification, event cursor/sequence,
action effect, receipt, and outcome verdict. Do not restart the browser merely to make
the state machine easier to debug.
