cadrumo.application.flows.engine module

The pure flow engine: immutable state plus typed transitions.

The engine is the single flow authority. It owns every flow semantic — visibility, navigation, answer commitment, staleness, deferral, reset, restart — as pure functions over an immutable FlowState. Frontends (full-screen TUI, line mode, scripted driver, non-interactive flags) project the state and dispatch intents; they contain zero flow logic, so every interaction surface is consistent by construction.

Two invariants are load-bearing:

  • Staleness, never silent deletion. Changing an answer whose value gates later pages marks the dependent answers STALE (they surface on the review projection and block submission) rather than deleting them. Shrinking a repeating group likewise marks the orphaned instance answers stale.

  • Deferral never resolves silently. A COMPARE_SELECT page answered with the reserved defer token stays DEFERRED until the operator revisits it; submission refuses while any deferral remains.

Visibility is recomputed from the canonical answer map on every transition; nothing is cached across transitions.

SECTION_VERDICT_PREFIX

Key prefix under which section-exit verdicts land in FlowState.verdicts.

class VisiblePage(**data)[source]

Bases: BaseModel

One entry in the currently-visible page sequence.

key is the canonical answer-map key: the bare page id for a top-level page, <group>#<index>.<page> for a repeating-group instance page.

Parameters:

data (Any)

key: str
page: FlowPage
section_id: str
group_id: str | None
instance_index: int | None
class FlowState(**data)[source]

Bases: BaseModel

Immutable engine state; every transition returns a new instance.

answers holds canonical tokens keyed by visible-page key. verdicts holds the failing validation verdicts currently blocking a page (keyed by page key) or a section exit (keyed section:<id>). stale holds page keys whose committed answer outlived a gating change. instance_counts holds the live instance count per repeating group, seeded from the group’s count_from answer and adjustable from review.

Immutability convention: frozen=True blocks field reassignment; the dict-typed fields are additionally never mutated in place — every transition builds fresh containers and returns a model_copy. Callers must follow the same convention.

Parameters:

data (Any)

flow_id: str
mode: FlowMode
answers: dict[str, str]
verdicts: dict[str, tuple[ValidationVerdict, ...]]
stale: frozenset[str]
instance_counts: dict[str, int]
cursor: str | None
visited: tuple[str, ...]
start_flow(definition, *, mode)[source]

Return the initial state with the cursor on the first visible page.

Return type:

FlowState

Parameters:
visible_sequence(definition, state)[source]

Compute the currently-visible page sequence from the answer map.

Repeating groups expand to their live instance count (the count_from answer unless review adjusted it). A gated page is visible when any of its clauses holds against the answers; clause targets resolve instance-locally first so a group page may gate on a sibling within the same instance.

Return type:

tuple[VisiblePage, ...]

Parameters:
answer(definition, state, page_key, raw)[source]

Commit raw to the page at page_key and return the new state.

A failing validation leaves the previous answer in place and records the verdicts under page_key so the frontend renders them in place. A passing commit stores the canonical token, clears the page’s verdicts, marks dependents of a changed gating answer stale, and refreshes the owning repeating group’s instance count when the page is a declared count source.

Return type:

FlowState

Parameters:
next_page(definition, state)[source]

Advance the cursor to the next visible page.

Crossing out of a section runs that section’s exit validators; any failure records the verdicts under section:<id> and leaves the cursor in place, blocking forward navigation with a rendered, typed result. Moving backwards is never gated.

Return type:

FlowState

Parameters:
jump_to(definition, state, page_key)[source]

Set the cursor onto any currently-visible page (the review jump).

Return type:

FlowState

Parameters:
reset_page(definition, state, page_key)[source]

Clear one page’s answer back to undeclared and mark its dependents stale.

The committed answer is removed (the page’s descriptor default is a render-time prefill, not a committed answer), the page’s verdicts and staleness clear, and — exactly as with a changed gating answer — every answered dependent of the cleared page is marked stale rather than deleted.

Return type:

FlowState

Parameters:
restart_flow(definition, state)[source]

Wipe the whole state and return to the first page (confirmed upstream).

The confirmation is a frontend responsibility; the engine transition itself is unconditional.

Return type:

FlowState

Parameters:
page_status(state, page_key)[source]

Return the review status of one page key.

Return type:

PageStatus

Parameters:
first_unanswered_key(definition, state)[source]

Return the key of the first visible entry the state has not answered.

Lives beside the visibility walk it depends on, because the answer changes with visibility: an entry hidden by a prior answer is not unanswered, it is not asked. Two callers each carried a copy of this loop.

Return type:

str | None

Parameters: