Skip to main content

WorkflowDefinition

Trait WorkflowDefinition 

Source
pub trait WorkflowDefinition:
    Send
    + Sync
    + 'static {
    type State: Clone + Send + Sync + Serialize + DeserializeOwned + 'static;
    type Phase: Clone + Send + Sync + Serialize + DeserializeOwned + Eq + 'static;
    type Obligation: Clone + Send + Sync + Serialize + DeserializeOwned + Eq + Hash + 'static;
    type Command: Clone + Send + Sync + Serialize + DeserializeOwned + 'static;
    type Event: Clone + Send + Sync + Serialize + DeserializeOwned + 'static;
    type Outcome: Clone + Send + Sync + Serialize + DeserializeOwned + Eq + 'static;

Show 27 methods // Required methods fn key(&self) -> WorkflowKey; fn version(&self) -> WorkflowVersion; fn phase_ownership(&self, phase: &Self::Phase) -> PhaseOwnership; fn project( &self, case_ref: CaseRef, state: Option<&Self::State>, ) -> ViewOf<Self>; fn operations(&self, view: &ViewOf<Self>) -> Vec<OperationSpec>; fn compile_act( &self, state: Option<&Self::State>, view: &ViewOf<Self>, act: &ResolvedAct, ) -> Result<Vec<Self::Command>, DomainRejection>; fn command_policy( &self, state: Option<&Self::State>, command: &Self::Command, ) -> CommandPolicy; fn validate_command( &self, state: Option<&Self::State>, command: &Self::Command, ) -> Result<(), DomainRejection>; fn receipts( &self, events: &[ReceiptEvent<Self::Event>], locale: &Locale, ) -> Vec<OperationalReceipt>; // Provided methods fn narratable_state(&self, state: Option<&Self::State>) -> Vec<StateField> { ... } fn summary(&self) -> Option<String> { ... } fn glossary(&self) -> Vec<GlossaryTerm> { ... } fn noun(&self) -> Option<LocalizedText> { ... } fn briefing(&self, view: &ViewOf<Self>) -> Option<String> { ... } fn obligation_sentence( &self, obligation: &Self::Obligation, ) -> Option<LocalizedText> { ... } fn obligation_act( &self, state: Option<&Self::State>, obligation: &Self::Obligation, ) -> Option<ObligationAct> { ... } fn start_preconditions(&self) -> Vec<StartPrecondition> { ... } fn confirmation_subject( &self, state: Option<&Self::State>, view: &ViewOf<Self>, act: &ResolvedAct, ) -> Option<ConfirmationSubject> { ... } fn start_behaviour(&self) -> StartBehaviour { ... } fn may_open_beside( &self, open: &[ViewOf<Self>], ) -> Result<(), DomainRejection> { ... } fn transition_briefing(&self, view: &ViewOf<Self>) -> Option<String> { ... } fn answer_briefing(&self, view: &ViewOf<Self>) -> Option<String> { ... } fn enumerations(&self, view: &ViewOf<Self>) -> Vec<DomainEnumeration> { ... } fn next_steps(&self, view: &ViewOf<Self>) -> Vec<LocalizedText> { ... } fn artifacts(&self, view: &ViewOf<Self>) -> Vec<ArtifactRef> { ... } fn nothing_changed( &self, state: Option<&Self::State>, act: &ResolvedAct, ) -> Option<LocalizedText> { ... } fn build_interaction( &self, state: Option<&Self::State>, view: &ViewOf<Self>, requirement: &InteractionRequirement, ) -> Result<InteractionSpec, DomainRejection> { ... }
}
Expand description

A workflow: pure projection plus deterministic compilation and policy (spec §8.2).

Required Associated Types§

Source

type State: Clone + Send + Sync + Serialize + DeserializeOwned + 'static

Persisted case state.

Source

type Phase: Clone + Send + Sync + Serialize + DeserializeOwned + Eq + 'static

Lifecycle phase.

Source

type Obligation: Clone + Send + Sync + Serialize + DeserializeOwned + Eq + Hash + 'static

Open obligation, possibly parameterized.

Source

type Command: Clone + Send + Sync + Serialize + DeserializeOwned + 'static

Typed command.

Source

type Event: Clone + Send + Sync + Serialize + DeserializeOwned + 'static

Typed domain event.

Source

type Outcome: Clone + Send + Sync + Serialize + DeserializeOwned + Eq + 'static

Terminal outcome.

Required Methods§

Source

fn key(&self) -> WorkflowKey

Stable key.

Source

fn version(&self) -> WorkflowVersion

Version; must change when projection semantics change (spec §8.4).

Source

fn phase_ownership(&self, phase: &Self::Phase) -> PhaseOwnership

Who must act in a phase. Drives the §8.4 invariants.

Source

fn project( &self, case_ref: CaseRef, state: Option<&Self::State>, ) -> ViewOf<Self>

Pure projection (I2).

An absent state means the case does not exist yet, and never that it no longer does: a case’s identity outlives its content, so removal is a status the state carries and never an absence. Its one use is a case the application’s case directory has offered but that has not been created, which is projected to a pre-draft phase and never to a terminal one. A projector that gives an absent state a terminal phase or an outcome is reported by the state explorer in the test kit.

Source

fn operations(&self, view: &ViewOf<Self>) -> Vec<OperationSpec>

The operations offered in this view, with their arguments, labels and examples.

Source

fn compile_act( &self, state: Option<&Self::State>, view: &ViewOf<Self>, act: &ResolvedAct, ) -> Result<Vec<Self::Command>, DomainRejection>

Compiles a resolved act into typed commands (spec §21.2).

Source

fn command_policy( &self, state: Option<&Self::State>, command: &Self::Command, ) -> CommandPolicy

Policy of a command. Unknown commands must default to CommandPolicy::conservative.

Source

fn validate_command( &self, state: Option<&Self::State>, command: &Self::Command, ) -> Result<(), DomainRejection>

Deterministic validation before execution.

Source

fn receipts( &self, events: &[ReceiptEvent<Self::Event>], locale: &Locale, ) -> Vec<OperationalReceipt>

Renders receipts from committed events (spec §17.3).

The events arrive as ReceiptEvents, not bare payloads, because a receipt must cite the EventIds that authorize its claim (I16): a Success receipt with no event ids is refused by claim_guard::verify. Derive the receipt id with ReceiptId::derive so a replayed turn renders the same receipts.

§The redacted case

ReceiptEvent::Redacted means the event happened and its payload was erased (see the event module). The event is still in the ledger, at its position, with its identity and its type, so a receipt rendered over it is still backed and still passes the claim guard — but it cannot say what changed, and it must not read as though it could. Write copy that is true of an erased event: that this step is on record and its detail is gone. Rendering nothing at all is worse than it looks, because a turn that quietly drops a receipt reads as a turn in which nothing happened.

Provided Methods§

Source

fn narratable_state(&self, state: Option<&Self::State>) -> Vec<StateField>

What this case HOLDS, for the stage that answers questions.

The obligations on the view say what a case still needs; this says what it already has. Without it the answering stage is handed a list of missing fields and no values, and «what did you record as the company name?» comes back as «nothing», truthfully as far as the brief goes, over a record that holds one.

Takes the state rather than the view for the same reason Self::compile_act does: the view is a projection and a projection drops the values.

Declare only what a person may be told back. A value under an obligation is fine — it is theirs, they gave it — and a value the domain holds for its own bookkeeping is not.

The default is empty, which is the previous behaviour.

Source

fn summary(&self) -> Option<String>

One line saying what the workflow is for, shown when a message is split into requests. None, the default, shows the workflow’s key alone.

Source

fn glossary(&self) -> Vec<GlossaryTerm>

Terms this workflow’s users say, and what they mean here.

Source

fn noun(&self) -> Option<LocalizedText>

What one record of this workflow is called, in each language its users speak («traveler», «viaggiatore»), for the sentences the runtime writes about one. None, the default, uses the workflow’s key.

Source

fn briefing(&self, view: &ViewOf<Self>) -> Option<String>

Guidance for understanding a turn about a record in this view. None, the default, is ordinary.

It is instructions, not data: never interpolate text a user wrote. It varies by view, not by record contents, which keeps it reviewable.

Source

fn obligation_sentence( &self, obligation: &Self::Obligation, ) -> Option<LocalizedText>

One obligation, in words a person would recognise.

The stage that writes is handed obligations as the domain’s own serialized values, which a match needs and a sentence does not. Returning None, the default, lets the value travel as it is, and a structured one such as {"fill":{"row":1}} is then guessed at. Say it as the question it is, «row 1 has no A: what is it?»: localized copy for a reader, saying what is missing rather than what to do about it.

Source

fn obligation_act( &self, state: Option<&Self::State>, obligation: &Self::Obligation, ) -> Option<ObligationAct>

The act that answers obligation, and the values it already knows.

Asked «row 1 has no A: what is it?», the user answers «X»: the answer is A, and the row is the obligation’s. With an act named here the reply’s question carries it, so a bare answer completes it. None, the default, leaves the answer to be routed as any other message.

Source

fn start_preconditions(&self) -> Vec<StartPrecondition>

What must already be true of another case before this workflow may be started.

Every precondition must hold, or the workflow’s “no case yet” operations are absent from the catalogue and the model cannot propose starting it. A declaration and not a read: see StartPrecondition, which also says what this shape cannot guarantee.

The default is empty, which requires nothing and changes nothing.

Source

fn confirmation_subject( &self, state: Option<&Self::State>, view: &ViewOf<Self>, act: &ResolvedAct, ) -> Option<ConfirmationSubject>

What a confirmation the policy engine raises is about.

Called when an act compiles to commands that policy says need a click, with the state and the act that produced them — which is everything needed to write “Delete the record for Mario Rossi?” where the engine would otherwise draw its per-kind box. See ConfirmationSubject.

The default is None, which keeps that box exactly as it was.

Source

fn start_behaviour(&self) -> StartBehaviour

What starting this workflow means when a case of it is already open.

See StartBehaviour, which carries the whole argument. The default mints a new case every time, which is what this door did before the declaration existed.

Source

fn may_open_beside(&self, open: &[ViewOf<Self>]) -> Result<(), DomainRejection>

Whether a new case may be opened while open ones are: an operation aimed at a new record, the door StartBehaviour does not govern.

open holds every case of this workflow the turn can address, plus any it already minted, projected with no state. Only the domain knows whether a second one is reasonable (an unfinished draft beside another is not; one waiting to be sent may be), and neither compile_act nor validate_command sees the others. A refusal rejects the act with the domain’s own sentence and the rest of the turn stands. The default admits everything.

Source

fn transition_briefing(&self, view: &ViewOf<Self>) -> Option<String>

What this workflow wants said while it acknowledges the turn and asks for what is still open.

§Why there are two of these and not one

There was one, and it went to both writing stages. The runtime goes to real lengths to keep a question away from this stage — the questions are not in its brief and the words they are made of are cut out of the message it is shown — because a model handed a question answers it, and the user reads the same explanation twice. Then one adopter string reached both stages and put the instruction straight back, and the duplicate came out again.

The guarantee has to be whole or it is not one. So a workflow addresses each stage by name: what to say while acknowledging is not what to say while answering, and a workflow with something for one and nothing for the other says exactly that by leaving the other at None.

This is the stage that asks, so guidance about what to ask for and in what order belongs here.

Like Self::briefing it takes the view and not the state, so the guidance varies exactly as much as the projection does and can never carry a user’s words into a prompt. The composer sees the view projected after the turn committed, so what it is briefed about is the case as it now stands.

Source

fn answer_briefing(&self, view: &ViewOf<Self>) -> Option<String>

What this workflow wants said while it answers a question the user asked.

The other half of Self::transition_briefing, and deliberately a different string: guidance about how to explain a domain concept has no business reaching the stage whose job is to acknowledge and ask.

Source

fn enumerations(&self, view: &ViewOf<Self>) -> Vec<DomainEnumeration>

The complete sets of values this workflow accepts, for the fields where there is one.

§The claim nothing was checking

The claim guard verifies that prose does not say an action happened when it did not. “These are the values this field accepts” is not a claim about an action; it is a claim about the domain, it is exactly as harmful when false, and it went out unchecked. A workflow accepting two legal forms was asked which forms exist and answered with three, the third being a plausible name for nothing. A user who takes that advice types a value the domain will refuse.

A firmer prompt does not fix it. A sentence naming three plausible things is what a language model produces when nothing decides how many there are, and the values already existed in the workflow — as prose in a briefing, which is to say as a suggestion.

§What the runtime does with it

A question whose references name an enumerated subject, and whose basis is general domain knowledge — “which forms are there”, not “which one does this record have” — is answered from this declaration and no model is asked. The values reach the user as structured data carrying the workflow’s own labels, so there is no sentence for a third value to appear in. Guarding prose after the fact was the alternative, and it cannot be done: reading an answer cannot tell an invented value from a real one, which is why the declaration answers instead of checking.

It also tells the runtime that such a question is answerable, which is what stops it being dropped as the assistant’s own next step: the field a flow is collecting is the field a user asks about, and without a declared answer the two are indistinguishable.

The default is empty, which declares nothing and changes nothing.

Source

fn next_steps(&self, view: &ViewOf<Self>) -> Vec<LocalizedText>

What the user may do next once the case owes nothing, each a sentence in the workflow’s words: the reply offers them when the case needs nothing more.

The default is empty, which offers nothing.

Source

fn artifacts(&self, view: &ViewOf<Self>) -> Vec<ArtifactRef>

The documents this case has, for the turn to put in front of the user.

§The type that nothing could fill

ArtifactRef, ArtifactView and OperationalReceipt::artifact_refs all existed, and no workflow could fill any of them. The only hook that came close is Self::receipts, which is handed the events and nothing else — and whether a case has a document is a question about the state those events folded into, not about the events. A list of “line added” cannot answer it, so every implementation ended at an empty vector.

What that cost is concrete: a user was asked to authorise the irreversible transmission of a document they had never seen, because the preview that used to sit in the conversation had nowhere to come from.

§Why the view and not the receipt

A receipt exists only where something committed. A document does not stop existing on a turn that writes nothing — the user asks a question, or refuses a card and the requirement stays down — and on those turns there is no receipt to hang it on. An artifact outlives the turn that produced it, so it is declared from the projection, like everything else that is true of a case rather than of a moment.

§What the runtime does with it

One ResponseBlock::Artifact per declaration, for the cases the turn was about. A case the turn merely loaded does not put a document in the reply, for the same reason its briefing does not: a conversation about one record is not an occasion to show another.

Re-declaring the same artifact after an edit is not a duplicate and is not suppressed. It is the same document at a later revision, the turn order says which is which, and what a surface does with the earlier ones is a rendering decision the runtime has no business taking.

The default is empty, which declares nothing and changes nothing.

Source

fn nothing_changed( &self, state: Option<&Self::State>, act: &ResolvedAct, ) -> Option<LocalizedText>

Why an act that compiled nothing changed nothing, in the reader’s own words.

Called only when compile_act returned no commands at all. That is a legitimate answer — the state the act asks for is the state the case is already in — but the runtime cannot say which of a workflow’s several reasons it was, and the writing stage is handed the operation’s name and nothing else.

§What that costs when nobody implements it

One workflow compiles nothing for two quite different reasons: the singleton whose start was proposed a second time, and the write that tells a field what it already says. Given only «this act changed nothing», a writer invents a reason, and the one it reaches for is a refusal: asked to correct a value and told nothing changed, it answered «I cannot do that here» — which was not true, and left the user with no idea what to say next. The sentence a person needed was «that is already the value; tell me what you want instead», and only the workflow knows it.

This is the same channel DomainRejection gives a refusal, for the same reason: a workflow that wrote a sentence knows more than the runtime does.

§The default

None, which is exactly what every workflow said before this existed — the fact reaches the writing stage naming the operation and no more.

Source

fn build_interaction( &self, state: Option<&Self::State>, view: &ViewOf<Self>, requirement: &InteractionRequirement, ) -> Result<InteractionSpec, DomainRejection>

Turns a requirement of the view into a full interaction spec.

The default uses the requirement’s payload, so a requirement that carries none must be completed here: the engine validates the result (InteractionSpec::validate) and refuses a card nobody could answer.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§