1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "step", rename_all = "snake_case")]
17#[non_exhaustive]
18pub enum Step {
19 Reading {
21 words: usize,
23 },
24 Segmented {
26 analysis: String,
28 units: Vec<UnitSummary>,
30 },
31 Covered {
33 added: Vec<UnitSummary>,
35 },
36 CrossChecked {
38 round: u8,
40 findings: usize,
42 },
43 CrossCheckSkipped {
45 round: u8,
47 code: String,
49 },
50 Routed {
52 unit: UnitId,
54 to: Routing,
56 },
57 Located {
59 act: ActId,
61 record: Located,
63 },
64 Extracted {
66 act: ActId,
68 given: Vec<(String, String)>,
70 not_given: Vec<String>,
72 },
73 Verified {
75 act: ActId,
77 confirmed: bool,
79 reason: String,
81 at_fault: Vec<String>,
83 },
84 Repairing {
86 act: ActId,
88 because: String,
90 },
91 Checked {
93 act: ActId,
95 refused: Option<(String, String)>,
97 },
98 NotUnderstood {
100 unit: UnitId,
102 reason: NotUnderstoodReason,
104 },
105 Assembled {
107 ready: Vec<ActId>,
109 asking: Vec<ActId>,
111 held: Vec<ActId>,
113 questions: usize,
115 },
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct UnitSummary {
121 pub id: UnitId,
123 pub kind: UnitKind,
125 pub text: String,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(tag = "kind", rename_all = "snake_case")]
132pub enum Routing {
133 Operation {
135 operation: OperationKey,
137 },
138 Start {
140 workflow: WorkflowKey,
142 },
143 Nothing,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(tag = "kind", rename_all = "snake_case")]
150pub enum Located {
151 Record {
153 token: TargetToken,
155 label: String,
157 },
158 New,
160 SameTurn {
162 act: ActId,
164 },
165 Card,
167 NotListed,
169 Ambiguous,
171 Nothing,
173}
174
175impl Step {
176 #[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
289pub trait StepSink: Send + Sync {
291 fn step(&self, step: Step);
293}
294
295#[derive(Debug, Clone, Copy, Default)]
297pub struct NoSteps;
298
299impl StepSink for NoSteps {
300 fn step(&self, _step: Step) {}
301}
302
303#[derive(Debug, Default)]
305pub struct RecordedSteps {
306 steps: Mutex<Vec<Step>>,
307}
308
309impl RecordedSteps {
310 #[must_use]
312 pub fn new() -> Self {
313 Self::default()
314 }
315
316 #[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#[derive(Debug, Clone)]
337pub struct ChannelSteps(tokio::sync::mpsc::UnboundedSender<Step>);
338
339impl ChannelSteps {
340 #[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}