turnframe-core 0.1.0

Pure types and the deterministic Flow Map workflow projector for Turnframe
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
//! What a turn was understood to say, assembled by code from small model tasks.
//!
//! An [`Understanding`] is the reducer's input: every act with an [`ActId`], its target,
//! its parsed arguments and the words that state them, and the questions, constraints,
//! card answer and disputes of the same message. Nothing in it is model-facing; the
//! tasks that produced it have their own schemas.

use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

use crate::hash::{Digest, HashError, canonical_digest};
use crate::ids::{OperationKey, OptionId, TargetToken, WorkflowKey};
use crate::plan::AnswerBasis;

/// One unit of a message: `u1`. The segmentation's units are numbered from 1 in message order,
/// and units coverage adds follow them.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UnitId(pub u16);

impl fmt::Display for UnitId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "u{}", self.0)
    }
}

/// One act of a unit: `u2.a1`. Command ids, card keys and minted case ids derive from it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ActId {
    /// The unit that asked for it.
    pub unit: UnitId,
    /// Its position among the unit's acts, from 1.
    pub act: u16,
}

impl ActId {
    /// The `act`-th act of `unit`.
    #[must_use]
    pub const fn new(unit: UnitId, act: u16) -> Self {
        Self { unit, act }
    }
}

impl fmt::Display for ActId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}.a{}", self.unit, self.act)
    }
}

/// An act identifier that is not of the form `u<n>.a<m>`.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("`{0}` is not an act identifier")]
pub struct ActIdError(pub String);

impl FromStr for ActId {
    type Err = ActIdError;

    fn from_str(text: &str) -> Result<Self, Self::Err> {
        let invalid = || ActIdError(text.to_owned());
        let (unit, act) = text.split_once(".a").ok_or_else(invalid)?;
        let unit = unit.strip_prefix('u').ok_or_else(invalid)?;
        Ok(Self {
            unit: UnitId(unit.parse().map_err(|_| invalid())?),
            act: act.parse().map_err(|_| invalid())?,
        })
    }
}

impl Serialize for ActId {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.collect_str(self)
    }
}

impl<'de> Deserialize<'de> for ActId {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let text = String::deserialize(deserializer)?;
        text.parse().map_err(serde::de::Error::custom)
    }
}

/// Which message some words are in: the turn's own, or one of the transcript window shown.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MessageRef {
    /// The message being understood.
    Current,
    /// The transcript window's message at this index, oldest first.
    Earlier {
        /// Index into the window.
        index: usize,
    },
}

/// Words of a message: word indices, inclusive, and the byte range they cover.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WordRange {
    /// First word.
    pub first: usize,
    /// Last word.
    pub last: usize,
    /// Byte offset of the first word's start.
    pub start: usize,
    /// Byte offset just past the last word.
    pub end: usize,
}

/// Words and the message they are in: the evidence for a value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Excerpt {
    /// The message.
    pub message: MessageRef,
    /// The words.
    pub words: WordRange,
}

/// What a unit is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum UnitKind {
    /// Asks for something to be done.
    Request,
    /// Asks something.
    Question,
    /// A condition on the whole turn.
    Constraint,
    /// Changes something asked for earlier.
    Correction,
    /// Withdraws something asked for earlier.
    Cancel,
    /// Answers the card on screen.
    CardAnswer,
    /// Contests something the assistant reported doing.
    Dispute,
    /// Gives a value the assistant asked for.
    ProvidesValue,
    /// Greets, thanks, or says nothing that asks for anything.
    Chitchat,
}

/// Which task found a unit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FoundBy {
    /// The segmentation of the message.
    Segment,
    /// The check for requests the segmentation missed.
    Coverage,
    /// The check of the whole turn.
    CrossCheck,
}

/// One unit of the message.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Unit {
    /// Its identifier.
    pub id: UnitId,
    /// What it is.
    pub kind: UnitKind,
    /// Its words in the current message.
    pub words: WordRange,
    /// The workflow it is about, when one was named or implied.
    pub workflow: Option<WorkflowKey>,
    /// Which task found it.
    pub found_by: FoundBy,
}

/// What an act does.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ActAction {
    /// Applies an operation.
    Apply {
        /// The operation.
        operation: OperationKey,
    },
    /// Starts a new case of a workflow.
    Start {
        /// The workflow.
        workflow: WorkflowKey,
    },
}

/// The record an act applies to.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ActTarget {
    /// A record in view, by its token.
    Record {
        /// The token.
        token: TargetToken,
    },
    /// A record this act creates.
    New {
        /// Its workflow.
        workflow: WorkflowKey,
    },
    /// The record an earlier act of the same turn creates.
    SameTurn {
        /// That act.
        act: ActId,
    },
    /// The record of the card on screen.
    Card,
    /// A record the user named that is not in view; the application looks it up.
    NotListed {
        /// Its workflow.
        workflow: WorkflowKey,
        /// The words naming it, when the user used any.
        words: Option<WordRange>,
    },
    /// Several records fit; the user is asked which.
    Ambiguous {
        /// The records that fit.
        candidates: Vec<TargetToken>,
    },
    /// The operation applies to no record.
    Nothing,
}

/// A record an argument names.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RecordValue {
    /// A record in view.
    Record {
        /// Its token.
        token: TargetToken,
    },
    /// The record an earlier act of the same turn creates.
    SameTurn {
        /// That act.
        act: ActId,
    },
    /// A record the user named that is not in view; the runtime looks it up.
    Named {
        /// The workflow it belongs to.
        workflow: WorkflowKey,
        /// The words that name it.
        named: String,
    },
}

/// An argument's value, parsed and evaluated by code.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum ArgumentValue {
    /// A value in the operation's own schema: text, a date, an amount, an enum value.
    Json(serde_json::Value),
    /// A record, resolved by the runtime into the operation's representation.
    Record(RecordValue),
}

/// An argument and the words that state it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnderstoodArgument {
    /// The value.
    pub value: ArgumentValue,
    /// The words it comes from; absent for a value carried over from an earlier turn.
    pub excerpt: Option<Excerpt>,
}

/// Whether an act may proceed to the reducer as it is.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ActStatus {
    /// Complete and verified.
    Ready,
    /// Missing, unstated or rejected arguments; the user is asked, nothing is written.
    NeedsValue {
        /// The arguments to ask for.
        arguments: Vec<String>,
        /// The domain's explanation, when it rejected a value.
        reason: Option<String>,
    },
    /// Another unit aimed at the same record was not understood.
    Held {
        /// That unit.
        because: UnitId,
    },
}

/// One act the turn asks for.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnderstoodAct {
    /// Its identifier.
    pub id: ActId,
    /// What it does.
    pub action: ActAction,
    /// What it applies to.
    pub target: ActTarget,
    /// Its arguments, by name.
    pub arguments: BTreeMap<String, UnderstoodArgument>,
    /// The words of the unit that asked for it.
    pub words: WordRange,
    /// Acts of the same turn it needs first.
    pub depends_on: Vec<ActId>,
    /// Whether it may proceed.
    pub status: ActStatus,
}

impl UnderstoodAct {
    /// The operation it applies, when it applies one.
    #[must_use]
    pub const fn operation(&self) -> Option<&OperationKey> {
        match &self.action {
            ActAction::Apply { operation } => Some(operation),
            ActAction::Start { .. } => None,
        }
    }

    /// Snake-case name of what it does: `apply_operation` or `start_workflow`.
    #[must_use]
    pub const fn kind_name(&self) -> &'static str {
        match self.action {
            ActAction::Apply { .. } => "apply_operation",
            ActAction::Start { .. } => "start_workflow",
        }
    }
}

/// An act a later unit of the same message replaced or withdrew.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Superseded {
    /// The act.
    pub act: ActId,
    /// What it asked for, kept because the act itself is gone.
    pub action: ActAction,
    /// The correction or cancellation.
    pub by: UnitId,
}

/// A question the user asked.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnderstoodQuestion {
    /// Its unit.
    pub unit: UnitId,
    /// Its words.
    pub words: WordRange,
    /// The workflow it is about.
    pub workflow: Option<WorkflowKey>,
    /// The record it is about.
    pub record: Option<TargetToken>,
    /// The declared subjects it asks about.
    pub subjects: Vec<String>,
    /// Which state answers it.
    pub basis: AnswerBasis,
    /// What kind of thing it asks.
    #[serde(default)]
    pub topic: QuestionTopic,
    /// Whether it follows up the assistant's last message.
    pub continues_previous: bool,
}

/// What kind of thing a question asks.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum QuestionTopic {
    /// What a record holds, or where it stands.
    #[default]
    RecordState,
    /// Which values a field accepts.
    AcceptedValues,
    /// What the user can do here, or whether something can be done.
    Capabilities,
    /// Anything else the domain knows.
    Knowledge,
}

/// A condition the user placed on the whole turn.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ConstraintKind {
    /// Submit nothing.
    DoNotSubmit,
    /// Delete nothing.
    DoNotDelete,
    /// Keep everything a draft.
    DraftOnly,
    /// Ask before applying anything.
    AskBeforeApplying,
    /// Apply only if a condition holds; its words are the condition.
    ApplyOnlyIf,
    /// No external effects.
    NoExternalEffects,
    /// Leave what its words name as it is: an act that would change it runs nothing.
    KeepUnchanged,
}

/// A constraint and its words.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TurnConstraint {
    /// Its unit.
    pub unit: UnitId,
    /// Which constraint.
    pub kind: ConstraintKind,
    /// Its words.
    pub words: WordRange,
}

/// A typed answer to the card on screen.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CardAnswer {
    /// Its unit.
    pub unit: UnitId,
    /// The option chosen.
    pub option: OptionId,
    /// Its words.
    pub words: WordRange,
}

/// Something the assistant reported that the user contests.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Dispute {
    /// Its unit.
    pub unit: UnitId,
    /// Its words.
    pub words: WordRange,
    /// The receipt contested, by the key it was shown under.
    pub receipt: Option<String>,
}

/// Why a unit was not understood.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum NotUnderstoodReason {
    /// No operation on offer does what it asks.
    NoOperation,
    /// Its answers disagreed and the profile asks instead of choosing.
    Unclear,
    /// The verifier found the act was not asked for.
    NotRequested,
    /// It would change what a keep-unchanged constraint keeps.
    KeptUnchanged {
        /// The constraint's unit.
        constraint: UnitId,
    },
    /// A task failed after its repairs and escalation.
    TaskFailed {
        /// The task.
        task: String,
        /// Its failure code.
        code: String,
    },
}

/// A unit that produced nothing to act on, and why.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotUnderstood {
    /// The unit.
    pub unit: UnitId,
    /// Its words.
    pub words: WordRange,
    /// Why.
    pub reason: NotUnderstoodReason,
}

/// Why a whole message could not be read, so no act of it runs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Unreadable {
    /// The segmentation failed.
    Segmentation {
        /// Its failure code.
        code: String,
    },
    /// A constraint was found by coverage, so its kind is unknown.
    LostConstraint,
}

/// Everything a turn was understood to say.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Understanding {
    /// The units, in message order.
    pub units: Vec<Unit>,
    /// The acts, in message order.
    pub acts: Vec<UnderstoodAct>,
    /// Acts replaced or withdrawn by a later unit.
    pub superseded: Vec<Superseded>,
    /// The questions.
    pub questions: Vec<UnderstoodQuestion>,
    /// The constraints.
    pub constraints: Vec<TurnConstraint>,
    /// The typed answer to the card on screen.
    pub card_answer: Option<CardAnswer>,
    /// The disputes.
    pub disputes: Vec<Dispute>,
    /// Units that produced nothing to act on.
    pub not_understood: Vec<NotUnderstood>,
    /// Set when the message could not be read at all; then nothing else is.
    pub unreadable: Option<Unreadable>,
}

impl Understanding {
    /// A message that could not be read: no acts, no questions, only the reason.
    #[must_use]
    pub fn unreadable(reason: Unreadable) -> Self {
        Self {
            unreadable: Some(reason),
            ..Self::default()
        }
    }

    /// The act with this identifier.
    #[must_use]
    pub fn act(&self, id: ActId) -> Option<&UnderstoodAct> {
        self.acts.iter().find(|act| act.id == id)
    }

    /// Canonical digest, so a replay can tell the same understanding from another.
    ///
    /// # Errors
    ///
    /// [`HashError`] when a value cannot be written as canonical JSON.
    pub fn hash(&self) -> Result<Digest, HashError> {
        canonical_digest(self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn act_identifiers_read_and_parse_as_unit_and_position() {
        let id = ActId::new(UnitId(2), 1);
        assert_eq!(id.to_string(), "u2.a1");
        assert_eq!("u2.a1".parse::<ActId>().unwrap(), id);
        assert!("u2".parse::<ActId>().is_err());
        assert!("x2.a1".parse::<ActId>().is_err());
        let json = serde_json::to_value(id).unwrap();
        assert_eq!(json, serde_json::json!("u2.a1"));
        assert_eq!(serde_json::from_value::<ActId>(json).unwrap(), id);
    }
}