Skip to main content

turnframe_understand/
progress.rs

1//! The steps of an understanding as it runs, for a consumer that wants to show them.
2//!
3//! A [`Step`] is published to the turn's [`StepSink`] the moment a task decides
4//! something. It states what was understood, never what happened: nothing here is a
5//! receipt. Each step carries its facts as data and a plain one-line [`Step::describe`];
6//! a consumer that wants prose feeds the facts to its own writer.
7
8use std::sync::{Mutex, PoisonError};
9
10use serde::{Deserialize, Serialize};
11use turnframe_core::ids::{OperationKey, TargetToken, WorkflowKey};
12use turnframe_core::understanding::{ActId, NotUnderstoodReason, UnitId, UnitKind};
13
14/// One thing the understanding decided.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "step", rename_all = "snake_case")]
17#[non_exhaustive]
18pub enum Step {
19    /// The message is being read.
20    Reading {
21        /// How many words it has.
22        words: usize,
23    },
24    /// The message was split into units.
25    Segmented {
26        /// The model's one-line reading of the message.
27        analysis: String,
28        /// Each unit: its id, kind and words.
29        units: Vec<UnitSummary>,
30    },
31    /// The coverage check added units the segmentation missed.
32    Covered {
33        /// The units added.
34        added: Vec<UnitSummary>,
35    },
36    /// A round of the whole-turn check answered.
37    CrossChecked {
38        /// The round, from 1.
39        round: u8,
40        /// What it found.
41        findings: usize,
42    },
43    /// A round of the whole-turn check did not run, or gave no answer to use.
44    CrossCheckSkipped {
45        /// The round, from 1.
46        round: u8,
47        /// Why, as a failure code.
48        code: String,
49    },
50    /// A unit was routed.
51    Routed {
52        /// The unit.
53        unit: UnitId,
54        /// Where it went.
55        to: Routing,
56    },
57    /// An act's record was decided.
58    Located {
59        /// The act.
60        act: ActId,
61        /// Its record.
62        record: Located,
63    },
64    /// An act's arguments were read.
65    Extracted {
66        /// The act.
67        act: ActId,
68        /// Each argument given, as a person reads it.
69        given: Vec<(String, String)>,
70        /// The arguments the user did not give.
71        not_given: Vec<String>,
72    },
73    /// An act was verified against the user's words.
74    Verified {
75        /// The act.
76        act: ActId,
77        /// Whether everything checked out.
78        confirmed: bool,
79        /// The verifier's reason.
80        reason: String,
81        /// The arguments found wanting.
82        at_fault: Vec<String>,
83    },
84    /// An act's values are being read again, with feedback.
85    Repairing {
86        /// The act.
87        act: ActId,
88        /// Why.
89        because: String,
90    },
91    /// The domain checked an act.
92    Checked {
93        /// The act.
94        act: ActId,
95        /// The argument it refused, and why, when it refused one.
96        refused: Option<(String, String)>,
97    },
98    /// A unit produced nothing to act on.
99    NotUnderstood {
100        /// The unit.
101        unit: UnitId,
102        /// Why.
103        reason: NotUnderstoodReason,
104    },
105    /// The understanding is assembled.
106    Assembled {
107        /// Acts ready for the reducer.
108        ready: Vec<ActId>,
109        /// Acts that will ask for a value.
110        asking: Vec<ActId>,
111        /// Acts held back.
112        held: Vec<ActId>,
113        /// Questions found.
114        questions: usize,
115    },
116}
117
118/// A unit, as a step names it.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct UnitSummary {
121    /// Its id.
122    pub id: UnitId,
123    /// What it is.
124    pub kind: UnitKind,
125    /// Its words, verbatim.
126    pub text: String,
127}
128
129/// Where a unit was routed.
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(tag = "kind", rename_all = "snake_case")]
132pub enum Routing {
133    /// An operation.
134    Operation {
135        /// Its key.
136        operation: OperationKey,
137    },
138    /// Starting a workflow.
139    Start {
140        /// The workflow.
141        workflow: WorkflowKey,
142    },
143    /// Nothing on offer does what it asks.
144    Nothing,
145}
146
147/// The record an act was aimed at.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(tag = "kind", rename_all = "snake_case")]
150pub enum Located {
151    /// A record in view, by its label.
152    Record {
153        /// Its token.
154        token: TargetToken,
155        /// Its label.
156        label: String,
157    },
158    /// A record this message creates.
159    New,
160    /// The record an earlier act of this message creates.
161    SameTurn {
162        /// That act.
163        act: ActId,
164    },
165    /// The record of the card on screen.
166    Card,
167    /// A record not in view.
168    NotListed,
169    /// Several records fit.
170    Ambiguous,
171    /// No record.
172    Nothing,
173}
174
175impl Step {
176    /// One plain line saying what was decided.
177    #[must_use]
178    pub fn describe(&self) -> String {
179        match self {
180            Self::Reading { words } => format!("Reading the message ({words} words)."),
181            Self::Segmented { analysis, units } => {
182                let listed: Vec<String> = units.iter().map(UnitSummary::describe).collect();
183                let units = format!("Units: {}.", listed.join("; "));
184                if analysis.is_empty() {
185                    units
186                } else {
187                    format!("{analysis} {units}")
188                }
189            }
190            Self::Covered { added } => {
191                let listed: Vec<String> = added.iter().map(UnitSummary::describe).collect();
192                format!("The check found more: {}.", listed.join("; "))
193            }
194            Self::CrossChecked { round, findings } => match findings {
195                0 => format!("Checking the whole message, round {round}: it all holds."),
196                _ => format!(
197                    "Checking the whole message, round {round}: {findings} thing(s) to read again."
198                ),
199            },
200            Self::CrossCheckSkipped { round, code } => {
201                format!("The whole message was not checked in round {round} ({code}).")
202            }
203            Self::Routed { unit, to } => match to {
204                Routing::Operation { operation } => format!("{unit} asks for {operation}."),
205                Routing::Start { workflow } => format!("{unit} starts a new {workflow}."),
206                Routing::Nothing => format!("{unit} asks for nothing on offer."),
207            },
208            Self::Located { act, record } => match record {
209                Located::Record { label, .. } => format!("{act} is about {label}."),
210                Located::New => format!("{act} creates a new record."),
211                Located::SameTurn { act: earlier } => {
212                    format!("{act} is about the record {earlier} creates.")
213                }
214                Located::Card => format!("{act} is about the card on screen."),
215                Located::NotListed => format!("{act} names a record not in view."),
216                Located::Ambiguous => format!("{act} could be about more than one record."),
217                Located::Nothing => format!("{act} is about no record."),
218            },
219            Self::Extracted {
220                act,
221                given,
222                not_given,
223            } => {
224                let mut parts: Vec<String> = given
225                    .iter()
226                    .map(|(name, value)| format!("{name} = {value}"))
227                    .collect();
228                parts.extend(not_given.iter().map(|name| format!("{name} not given")));
229                if parts.is_empty() {
230                    format!("{act} takes no arguments.")
231                } else {
232                    format!("{act}: {}.", parts.join(", "))
233                }
234            }
235            Self::Verified {
236                act,
237                confirmed,
238                reason,
239                ..
240            } => {
241                if *confirmed {
242                    format!("{act} checks out: {reason}")
243                } else {
244                    format!("{act} does not check out: {reason}")
245                }
246            }
247            Self::Repairing { act, because } => format!("Reading {act} again: {because}"),
248            Self::Checked { act, refused } => match refused {
249                Some((argument, reason)) => format!("{act}: {argument} was refused: {reason}"),
250                None => format!("{act} passes the domain's checks."),
251            },
252            Self::NotUnderstood { unit, reason } => {
253                format!("{unit} was not understood: {}.", reason_text(reason))
254            }
255            Self::Assembled {
256                ready,
257                asking,
258                held,
259                questions,
260            } => format!(
261                "{} ready, {} asking for a value, {} held, {questions} questions.",
262                ready.len(),
263                asking.len(),
264                held.len()
265            ),
266        }
267    }
268}
269
270impl UnitSummary {
271    fn describe(&self) -> String {
272        format!("{} {:?} «{}»", self.id, self.kind, self.text)
273    }
274}
275
276fn reason_text(reason: &NotUnderstoodReason) -> String {
277    match reason {
278        NotUnderstoodReason::NoOperation => "nothing on offer does it".to_owned(),
279        NotUnderstoodReason::Unclear => "the readings disagreed".to_owned(),
280        NotUnderstoodReason::NotRequested => "the user did not ask for it".to_owned(),
281        NotUnderstoodReason::TaskFailed { task, code } => format!("{task} failed ({code})"),
282        NotUnderstoodReason::KeptUnchanged { constraint } => {
283            format!("it would change what {constraint} keeps")
284        }
285        _ => "no reason recorded".to_owned(),
286    }
287}
288
289/// Receives the steps of a turn as they happen.
290pub trait StepSink: Send + Sync {
291    /// Delivers one step. Implementations must not block.
292    fn step(&self, step: Step);
293}
294
295/// A sink that drops every step.
296#[derive(Debug, Clone, Copy, Default)]
297pub struct NoSteps;
298
299impl StepSink for NoSteps {
300    fn step(&self, _step: Step) {}
301}
302
303/// A sink that keeps every step, for tests and for a caller that reads them at the end.
304#[derive(Debug, Default)]
305pub struct RecordedSteps {
306    steps: Mutex<Vec<Step>>,
307}
308
309impl RecordedSteps {
310    /// An empty recorder.
311    #[must_use]
312    pub fn new() -> Self {
313        Self::default()
314    }
315
316    /// Every step so far, in order.
317    #[must_use]
318    pub fn steps(&self) -> Vec<Step> {
319        self.steps
320            .lock()
321            .unwrap_or_else(PoisonError::into_inner)
322            .clone()
323    }
324}
325
326impl StepSink for RecordedSteps {
327    fn step(&self, step: Step) {
328        self.steps
329            .lock()
330            .unwrap_or_else(PoisonError::into_inner)
331            .push(step);
332    }
333}
334
335/// A sink that sends each step down an unbounded channel.
336#[derive(Debug, Clone)]
337pub struct ChannelSteps(tokio::sync::mpsc::UnboundedSender<Step>);
338
339impl ChannelSteps {
340    /// A sink and the receiver it feeds.
341    #[must_use]
342    pub fn channel() -> (Self, tokio::sync::mpsc::UnboundedReceiver<Step>) {
343        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
344        (Self(sender), receiver)
345    }
346}
347
348impl StepSink for ChannelSteps {
349    fn step(&self, step: Step) {
350        let _ = self.0.send(step);
351    }
352}
353
354impl<F: Fn(Step) + Send + Sync> StepSink for F {
355    fn step(&self, step: Step) {
356        self(step);
357    }
358}