Skip to main content

turnframe_core/
response.rs

1//! Ordered response blocks and the narration contract (spec §18).
2//!
3//! The assistant turn is a list of typed blocks persisted exactly as returned.
4//! Model-authored blocks (answers, transitions) sit between server-authored
5//! blocks (receipts, notices, interactions, artifacts) and may only cite facts
6//! the server allowed. [`claim_guard::verify`] is the structural check that no
7//! model-authored block claims an operational outcome without a matching
8//! event-backed receipt block.
9
10use serde::{Deserialize, Serialize};
11
12use crate::case::CaseRef;
13use crate::event::{ArtifactRef, OperationalReceipt, ReceiptSeverity};
14use crate::ids::{
15    AttachmentId, BlockId, ConversationId, EventId, InteractionId, OperationKey, OptionId,
16    QuestionId, ReceiptId, TurnId, WorkflowKey,
17};
18use crate::interaction::{FieldValue, InteractionKind, InteractionStatus, InteractionView};
19use crate::knowledge::Citation;
20use crate::locale::{Locale, LocalizedText};
21use crate::plan::AnswerBasis;
22use crate::reduce::AnswerTask;
23
24crate::ids::string_id! {
25    /// Opaque token that lets a client or operator retrieve the replay record.
26    ReplayToken
27}
28
29/// Severity of a server notice.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum NoticeSeverity {
33    /// Neutral.
34    Info,
35    /// Needs attention.
36    Warning,
37    /// Something failed.
38    Error,
39}
40
41/// Whether a question was answered (spec §19.4).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum AnswerStatus {
45    /// Answered.
46    Answered,
47    /// The assistant needs more information.
48    ClarificationRequested,
49    /// Out of scope or unsupported.
50    Unsupported,
51    /// The required sources were unavailable.
52    SourceUnavailable,
53    /// An answer existed and the guard would not let it through.
54    ///
55    /// Distinct from [`Self::Unsupported`], and the distinction is not
56    /// bookkeeping. *Unsupported* is a statement about the question: it is
57    /// outside what this assistant answers. *Withheld* is a statement about one
58    /// attempt at answering it: the question was in scope, an answer was
59    /// written, and the claim guard refused to publish it. Reporting the second
60    /// as the first tells the user their question was out of scope, which is
61    /// false, and false about them rather than about the system — the one thing
62    /// a guard built for honesty must not produce.
63    Withheld,
64    /// No model wrote an answer this time, after the retries; asking again may work.
65    NotWritten,
66}
67
68/// A model-authored answer to a question (spec §18.2).
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct GeneratedAnswer {
71    /// Stable block id.
72    pub block_id: BlockId,
73    /// The question answered, when it came from the plan.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub question_id: Option<QuestionId>,
76    /// The text.
77    pub text: String,
78    /// State basis used.
79    pub basis: AnswerBasis,
80    /// Outcome of the answer task.
81    pub status: AnswerStatus,
82    /// Facts the text relies on (all must be in the allowed set).
83    #[serde(default)]
84    pub facts_used: Vec<NarratableFact>,
85    /// Citations.
86    #[serde(default)]
87    pub citations: Vec<Citation>,
88    /// Complete value sets the answer carries, resolved to the turn's locale.
89    ///
90    /// Written by the deterministic layer from a workflow's own declaration,
91    /// never by a model, and rendered by the client the way a receipt is. This
92    /// is how "the values this field accepts" reaches a user: as data with the
93    /// workflow's labels, rather than as a sentence that could name a fourth
94    /// thing.
95    #[serde(default, skip_serializing_if = "Vec::is_empty")]
96    pub enumerations: Vec<AnsweredEnumeration>,
97}
98
99/// One complete value set as it reaches the client.
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct AnsweredEnumeration {
102    /// The field or concept these are the values of.
103    pub subject: String,
104    /// A sentence introducing them, when the workflow wrote one.
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub preamble: Option<String>,
107    /// Every value, in the order the workflow declared them.
108    pub values: Vec<AnsweredValue>,
109}
110
111/// One value of an [`AnsweredEnumeration`].
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct AnsweredValue {
114    /// Stable identifier, as the domain stores it.
115    pub id: String,
116    /// What a person calls it, in the turn's locale.
117    pub label: String,
118}
119
120/// A short model-authored transition or acknowledgment.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct GeneratedTransition {
123    /// Stable block id.
124    pub block_id: BlockId,
125    /// The text.
126    pub text: String,
127    /// Facts the text relies on.
128    #[serde(default)]
129    pub facts_used: Vec<NarratableFact>,
130}
131
132/// A receipt block.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub struct ReceiptBlock {
135    /// Stable block id.
136    pub block_id: BlockId,
137    /// The receipt.
138    pub receipt: OperationalReceipt,
139}
140
141/// A server-authored notice (spec §18.2).
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct ServerNotice {
144    /// Stable block id.
145    pub block_id: BlockId,
146    /// Stable code (e.g. `"turnframe.notice.nothing_submitted"`).
147    pub code: String,
148    /// Severity.
149    pub severity: NoticeSeverity,
150    /// Copy.
151    pub text: LocalizedText,
152}
153
154/// An interaction block.
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct InteractionBlock {
157    /// Stable block id.
158    pub block_id: BlockId,
159    /// Client-facing view.
160    pub view: InteractionView,
161}
162
163/// An artifact block.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct ArtifactView {
166    /// Stable block id.
167    pub block_id: BlockId,
168    /// The artifact.
169    pub artifact: ArtifactRef,
170}
171
172/// One ordered block of an assistant turn (spec §18.1).
173///
174/// New block kinds are the obvious extension point, so downstream matches need
175/// a wildcard arm.
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(tag = "kind", rename_all = "snake_case")]
178#[non_exhaustive]
179pub enum ResponseBlock {
180    /// Model-authored answer.
181    Answer(GeneratedAnswer),
182    /// Model-authored transition.
183    Transition(GeneratedTransition),
184    /// Server-authored receipt.
185    Receipt(ReceiptBlock),
186    /// Server-authored notice.
187    Notice(ServerNotice),
188    /// Persisted interaction.
189    Interaction(InteractionBlock),
190    /// Artifact.
191    Artifact(ArtifactView),
192}
193
194impl ResponseBlock {
195    /// The stable block id.
196    #[must_use]
197    pub fn block_id(&self) -> &BlockId {
198        match self {
199            Self::Answer(b) => &b.block_id,
200            Self::Transition(b) => &b.block_id,
201            Self::Receipt(b) => &b.block_id,
202            Self::Notice(b) => &b.block_id,
203            Self::Interaction(b) => &b.block_id,
204            Self::Artifact(b) => &b.block_id,
205        }
206    }
207
208    /// Returns `true` for blocks whose text a model wrote.
209    #[must_use]
210    pub fn is_model_authored(&self) -> bool {
211        matches!(self, Self::Answer(_) | Self::Transition(_))
212    }
213}
214
215/// The persisted assistant turn (spec §18.1).
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct AssistantTurn {
218    /// The user turn answered.
219    pub turn_id: TurnId,
220    /// The conversation.
221    pub conversation_id: ConversationId,
222    /// Ordered blocks.
223    pub blocks: Vec<ResponseBlock>,
224    /// Token to retrieve the replay record.
225    pub replay_token: ReplayToken,
226    /// The cases this turn was about, so the next turn knows what the conversation is
227    /// on. Read back as a fallback for a turn with no subject of its own, never as an
228    /// addition to its own.
229    #[serde(default, skip_serializing_if = "Vec::is_empty")]
230    pub subjects: Vec<crate::case::CaseRef>,
231    /// What this reply asked the user for. The next turn reads it, and only that one.
232    #[serde(default, skip_serializing_if = "Vec::is_empty")]
233    pub expectations: Vec<Expectation>,
234    /// The acts this turn did, so a correction in the next message changes what it says of
235    /// one and keeps the rest. The next turn reads it, and only that one.
236    #[serde(default, skip_serializing_if = "Vec::is_empty")]
237    pub done: Vec<DoneAct>,
238    /// The next steps this reply offered, in order: the next message is read first as
239    /// taking one up, and a surface may show them as choices.
240    #[serde(default, skip_serializing_if = "Vec::is_empty")]
241    pub offers: Vec<Offer>,
242}
243
244/// A step a reply offered: taken up, it runs its operation on its record with the arguments
245/// already known, and asks for the rest.
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub struct Offer {
248    /// The record it acts on; one still to create has an empty case id.
249    pub case_ref: CaseRef,
250    /// The operation it runs.
251    pub operation: crate::ids::OperationKey,
252    /// The words the reply offered it in, in the turn's language.
253    pub words: String,
254    /// The arguments already known, by name.
255    pub arguments: serde_json::Map<String, serde_json::Value>,
256}
257
258/// An act a turn did, with the values it was given.
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260pub struct DoneAct {
261    /// The act, with its arguments.
262    pub act: Box<crate::understanding::UnderstoodAct>,
263    /// The record it changed.
264    pub case_ref: CaseRef,
265}
266
267/// Something a reply asked for, so the next message can be read as its answer (§6.8).
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(tag = "kind", rename_all = "snake_case")]
270#[non_exhaustive]
271pub enum Expectation {
272    /// Values an act needs before it can run.
273    AwaitingValue {
274        /// The act, with the arguments already given.
275        act: Box<crate::understanding::UnderstoodAct>,
276        /// The record it applies to, when it has one.
277        #[serde(default, skip_serializing_if = "Option::is_none")]
278        case_ref: Option<CaseRef>,
279        /// The arguments still to give.
280        missing: Vec<String>,
281    },
282    /// An open obligation of a record, which the reply asked about.
283    AwaitingObligation {
284        /// The record.
285        case_ref: CaseRef,
286        /// The obligation, as a sentence.
287        obligation: String,
288    },
289    /// An open obligation whose workflow named the act that answers it.
290    AwaitingOperation {
291        /// The record.
292        case_ref: CaseRef,
293        /// The obligation, as a sentence.
294        obligation: String,
295        /// The act that answers it.
296        act: crate::flow::ObligationAct,
297    },
298    /// An act of an earlier turn still waiting for a record the user named that did not
299    /// exist, carried until a record registered under that name completes it or the act
300    /// is done another way. Never what the next message answers.
301    StillWaiting {
302        /// The act, with the name it gave the record.
303        act: Box<crate::understanding::UnderstoodAct>,
304        /// The record it applies to, when it has one.
305        #[serde(default, skip_serializing_if = "Option::is_none")]
306        case_ref: Option<CaseRef>,
307        /// The arguments still to give.
308        missing: Vec<String>,
309    },
310}
311
312impl AssistantTurn {
313    /// Ids of all blocks, in order.
314    #[must_use]
315    pub fn block_ids(&self) -> Vec<BlockId> {
316        self.blocks.iter().map(|b| b.block_id().clone()).collect()
317    }
318
319    /// All receipt blocks.
320    pub fn receipts(&self) -> impl Iterator<Item = &OperationalReceipt> {
321        self.blocks.iter().filter_map(|b| match b {
322            ResponseBlock::Receipt(r) => Some(&r.receipt),
323            _ => None,
324        })
325    }
326
327    /// All interaction views.
328    pub fn interactions(&self) -> impl Iterator<Item = &InteractionView> {
329        self.blocks.iter().filter_map(|b| match b {
330            ResponseBlock::Interaction(i) => Some(&i.view),
331            _ => None,
332        })
333    }
334}
335
336/// Voice the narrator should use.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
338#[serde(rename_all = "snake_case")]
339pub enum ToneProfile {
340    /// Plain and neutral.
341    #[default]
342    Neutral,
343    /// Friendly.
344    Warm,
345    /// Formal register.
346    Formal,
347    /// As short as possible.
348    Concise,
349}
350/// Whether a fact is about the turn in hand or about the room it happens in.
351///
352/// Every record open in the account has outstanding fields, and the writer needs to know
353/// them, but only the ones of the case the turn is about are what it asks next. A mark,
354/// not a filter: the writing stage asks for the **first** open obligation, so
355/// [`Self::ThisTurn`] sorts first and the order is load-bearing.
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
357#[serde(rename_all = "snake_case")]
358pub enum FactRelevance {
359    /// About a case the turn engaged, so it is what the reply is answering.
360    ///
361    /// The default, because it is what every fact was taken to be before the
362    /// distinction existed, including in records written then.
363    #[default]
364    ThisTurn,
365    /// True of the account, and not what this turn is about.
366    Background,
367}
368
369impl FactRelevance {
370    /// Whether this is a fact about the turn in hand.
371    #[must_use]
372    pub const fn is_this_turn(self) -> bool {
373        matches!(self, Self::ThisTurn)
374    }
375}
376
377/// How far the first answer to a card got, when a second one arrives.
378///
379/// Four and not two, because each is a different sentence and the difference is
380/// what the user came back for. "I am still working on it" must not invite a
381/// third click; "I already did that" is what somebody who missed the first reply
382/// wants to hear; and a card whose work failed or was closed must not be
383/// described as done.
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
385#[serde(rename_all = "snake_case")]
386#[non_exhaustive]
387pub enum AnswerProgress {
388    /// The commands the first answer authorized are still running.
389    Running,
390    /// They committed. Their events are surfaced as this turn's receipts.
391    Done,
392    /// They failed, so the work the card offered did not happen.
393    Failed,
394    /// The card was closed without doing that work — declined, dismissed,
395    /// invalidated or expired.
396    Closed,
397}
398
399impl AnswerProgress {
400    /// How far a card in `status` got.
401    ///
402    /// Matched exhaustively so a status added later has to be placed here rather
403    /// than falling into whichever arm is nearest.
404    #[must_use]
405    pub const fn of(status: InteractionStatus) -> Self {
406        match status {
407            InteractionStatus::Resolving => Self::Running,
408            InteractionStatus::Resolved => Self::Done,
409            InteractionStatus::Failed => Self::Failed,
410            // A card still `Active` cannot be an already-answered one, but it is
411            // no more "done" than a dismissed one, so it reads as closed rather
412            // than as a claim nothing backs.
413            InteractionStatus::Active
414            | InteractionStatus::Declined
415            | InteractionStatus::Dismissed
416            | InteractionStatus::Invalidated
417            | InteractionStatus::Expired => Self::Closed,
418        }
419    }
420}
421
422/// A fact the narrator may state (spec §18.3). Everything else is forbidden.
423///
424/// New fact kinds are expected, so downstream matches need a wildcard arm.
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
426#[serde(tag = "kind", rename_all = "snake_case")]
427#[non_exhaustive]
428pub enum NarratableFact {
429    /// Something the user can do now, in the workflow's words.
430    OperationAvailable {
431        /// The workflow.
432        workflow: WorkflowKey,
433        /// The operation.
434        operation: OperationKey,
435        /// What it does.
436        summary: String,
437    },
438    /// A record a question is about: its name, where its lifecycle stands, and its
439    /// outcome once complete.
440    Record {
441        /// The case.
442        case_ref: CaseRef,
443        /// The name the user knows it by, when the directory supplied one.
444        #[serde(default, skip_serializing_if = "Option::is_none")]
445        label: Option<String>,
446        /// Its phase, in the workflow's own vocabulary.
447        phase: serde_json::Value,
448        /// Its outcome, when it is complete.
449        #[serde(default, skip_serializing_if = "Option::is_none")]
450        outcome: Option<serde_json::Value>,
451    },
452    /// A committed field value.
453    StateValue {
454        /// The case.
455        case_ref: CaseRef,
456        /// Field path.
457        field: String,
458        /// Value.
459        value: serde_json::Value,
460    },
461    /// A proposed, uncommitted change.
462    ProposedChange {
463        /// The case.
464        case_ref: CaseRef,
465        /// Field path.
466        field: String,
467        /// Current value.
468        #[serde(default, skip_serializing_if = "FieldValue::is_absent")]
469        before: FieldValue,
470        /// Proposed value. [`FieldValue::cleared`] is "the field is being
471        /// emptied", which is not the same statement as "unchanged".
472        #[serde(default, skip_serializing_if = "FieldValue::is_absent")]
473        after: FieldValue,
474    },
475    /// An operational outcome; must be backed by a receipt block.
476    OperationalOutcome {
477        /// The receipt.
478        receipt_id: ReceiptId,
479        /// Events behind it.
480        event_ids: Vec<EventId>,
481        /// Status code of the receipt.
482        status_code: String,
483    },
484    /// A card is available; must be backed by an interaction block.
485    InteractionAvailable {
486        /// The interaction.
487        interaction_id: InteractionId,
488        /// Shape.
489        interaction_kind: InteractionKind,
490    },
491    /// The user declined an option on a card, so the server did nothing.
492    ///
493    /// # Why this is not [`Self::ActRefused`]
494    ///
495    /// The two look alike and read differently, which is the whole point. A
496    /// refusal is the *server* saying no and owes the user a reason. A decline
497    /// is the *user* saying no and owes them an acknowledgement — "all right, I
498    /// haven't; tell me what you'd like to change" rather than "that could not
499    /// be done". Folding them together would hand the narrator one word for two
500    /// situations and it would write the wrong one half the time.
501    ///
502    /// # Why the narrator needs it at all
503    ///
504    /// The deterministic notice already says the instruction was declined, and
505    /// it says it whether or not a model runs. What it cannot do is say it in
506    /// the register of the conversation. Without this fact the narrator sees a
507    /// turn with no plan, no receipts and nothing outstanding, and the only
508    /// sentence that fits an empty brief is a generic offer of help — so the
509    /// user reads a non-sequitur above a canned line, and it is obvious which
510    /// of the two a machine wrote.
511    InstructionDeclined {
512        /// The case the card belonged to.
513        case_ref: CaseRef,
514        /// The card that was answered.
515        interaction_id: InteractionId,
516        /// The option that was chosen.
517        option_id: OptionId,
518        /// What that card asked, in the reader's locale, so the acknowledgement a
519        /// decline owes names what was declined rather than offering to change something.
520        #[serde(default)]
521        question: String,
522        /// The label of the option the user chose, in the reader's locale.
523        #[serde(default, skip_serializing_if = "String::is_empty")]
524        option_label: String,
525    },
526    /// An act this turn prepared and did **not** run, because a card is asking
527    /// the user to authorize it first.
528    ///
529    /// Forbidding the claim that it happened is not enough: a writer told it may not
530    /// assert X, with no fact to say instead, asserts not-X. This is that fact, so the
531    /// true sentence («before I do it, I am asking you») is the one it can write. It is
532    /// emitted wherever the confirmation policy defers a command, since a card raised by
533    /// policy moves no case and no phase or briefing knows it is there.
534    ActAwaitingConfirmation {
535        /// The case the act was aimed at.
536        case_ref: CaseRef,
537        /// The operation that is waiting.
538        operation: String,
539        /// What the workflow calls the thing being confirmed, when it said.
540        #[serde(default, skip_serializing_if = "Option::is_none")]
541        subject: Option<String>,
542    },
543    /// An act the domain refused, and why.
544    ///
545    /// # Why a refusal is not a claim
546    ///
547    /// The claim guard exists to stop a model saying something happened that
548    /// did not. Saying that something did *not* happen, with the reason the
549    /// server itself produced, does not cross that line — it is the honest half
550    /// of the same rule. Without it an assistant forbidden from claiming
551    /// success has no way to report failure either, so it writes about
552    /// something else and the user concludes the write went through.
553    ///
554    /// That is not hypothetical: it is what happens next. A user told nothing
555    /// about a refusal says "I already gave you that", the interpreter reads a
556    /// transcript in which the refusal never occurred, and the complaint gets
557    /// written into the field the refusal was protecting.
558    ActRefused {
559        /// The case the act was aimed at, when its target resolved to one.
560        ///
561        /// Absent when the refusal *is* the target: an act naming a shape the
562        /// operation does not accept, or a card that is not on screen, never
563        /// reached a case at all. The narrator is told about it anyway, because
564        /// the user asked for something and did not get it.
565        #[serde(default, skip_serializing_if = "Option::is_none")]
566        case_ref: Option<CaseRef>,
567        /// The rejection's stable code.
568        code: String,
569        /// The domain's own sentence, in the reader's locale, or empty when the
570        /// rejection carried none.
571        explanation: String,
572    },
573    /// An act the domain accepted, and that changed nothing.
574    ///
575    /// Not a refusal: nothing about it was wrong. The state it asks for is the
576    /// state the case is already in, so the workflow compiled no commands —
577    /// which is a legitimate answer, and the one a singleton workflow relies on
578    /// when its start is proposed a second time.
579    ///
580    /// # Why the narrator needs it
581    ///
582    /// For the reason [`Self::ActRefused`] gives, and it is the same sentence:
583    /// an outcome nobody is told about is read by the user as a success, and by
584    /// the next turn's interpreter as a transcript in which it never happened.
585    /// A refusal has been told for a long time; this one was not, and it leaves
586    /// exactly the same hole — no receipt, no fact, obligations unchanged — so
587    /// the writing stage does the only thing an empty brief allows. It asks for
588    /// something else, or it announces the write.
589    ///
590    /// One turn paid for it whole. The user asked to go on with a draft; the
591    /// plan proposed starting a workflow that was already open; the domain
592    /// compiled nothing, the act left no trace, and the reply invented a datum
593    /// on a document the user had never named.
594    ActChangedNothing {
595        /// The case the act reached.
596        case_ref: CaseRef,
597        /// The operation, as the catalogue names it. Absent for the acts that
598        /// carry none — a start is the common one.
599        #[serde(default, skip_serializing_if = "Option::is_none")]
600        operation: Option<String>,
601        /// The workflow's own sentence saying WHY nothing changed, in the
602        /// reader's locale, or empty when it wrote none.
603        ///
604        /// One workflow compiles nothing for reasons that want opposite
605        /// replies — the singleton whose start was proposed twice, the write
606        /// that tells a field what it already says — and the name of the
607        /// operation does not separate them. A writer given only the name
608        /// invents, and what it reaches for is a refusal: «I cannot do that
609        /// here», to a user who had asked for a correction and now has no idea
610        /// what to say next.
611        ///
612        /// Filled from
613        /// [`WorkflowDefinition::nothing_changed`](crate::flow::WorkflowDefinition::nothing_changed);
614        /// empty is what every workflow said before that channel existed, and
615        /// it defaults on deserialization so records written then still load.
616        #[serde(default, skip_serializing_if = "String::is_empty")]
617        explanation: String,
618    },
619    /// An act that waits for values the user has not given: they are asked for, and
620    /// nothing is written until they are.
621    ValueNeeded {
622        /// The case the act is aimed at, when it resolved to one.
623        #[serde(default, skip_serializing_if = "Option::is_none")]
624        case_ref: Option<CaseRef>,
625        /// The operation.
626        operation: String,
627        /// The arguments asked for, by the labels people use for them.
628        arguments: Vec<String>,
629        /// The domain's explanation, when it refused a value it was given.
630        #[serde(default, skip_serializing_if = "Option::is_none")]
631        reason: Option<String>,
632    },
633    /// Words of the message that produced nothing to act on.
634    NotUnderstood {
635        /// The words, verbatim.
636        words: String,
637    },
638    /// An act held back because another part of the message about the same record
639    /// was not understood.
640    ActHeld {
641        /// The operation.
642        operation: String,
643        /// The words that were not understood.
644        because: String,
645    },
646    /// A workflow this account cannot start yet, and the reason it declared.
647    ///
648    /// Shown what is unavailable, the model rightly proposes no act, so there is no
649    /// refusal to carry the reason: it travels as this fact instead, like an open
650    /// obligation, something that has *not* happened, known from a declaration. It widens
651    /// nothing the narrator may claim: no [`ClaimClass`] covers it and no receipt backs it.
652    WorkflowUnavailable {
653        /// The workflow that cannot be started.
654        workflow: WorkflowKey,
655        /// Why, in the workflow's own words, resolved to the reader's locale.
656        reason: String,
657    },
658    /// The card this turn answered had already been answered.
659    ///
660    /// # The turn this fills
661    ///
662    /// A second click on the same card authorizes nothing: the compare-and-set
663    /// that guarantees it is the part of this worth leaving alone. But the
664    /// record it comes back with used to be dropped, so from that point on
665    /// nothing could tell a second click from a turn with no click at all — and
666    /// a turn with no plan, no receipts and nothing outstanding still calls the
667    /// stage that writes the lead-in, which does what that stage always does
668    /// with an empty brief: it invents.
669    ///
670    /// One such turn told a user to start their invoicing configuration from the
671    /// payment method. It was collected, it was on the card they were looking
672    /// at, and the case's own guidance for that phase said not to start over.
673    ///
674    /// Skipping the stage is not the fix, because the turn would go out silent
675    /// and a second click is somebody who did not understand the first answer.
676    /// The fix is to give the turn something true to say.
677    ///
678    /// # It claims nothing this turn did
679    ///
680    /// The original resolution's events are surfaced beside this as receipts, so
681    /// prose that says the work is done is backed by the events that did it —
682    /// which is what "repeat the result without repeating the effect" means. On
683    /// a resolution still running there are no events yet, and
684    /// [`AnswerProgress::Running`] is the fact that keeps the prose off a claim
685    /// nothing backs.
686    InteractionAlreadyAnswered {
687        /// The card that was clicked again.
688        interaction_id: InteractionId,
689        /// The option chosen the first time, when the record kept one.
690        option_id: Option<OptionId>,
691        /// How far the first answer got.
692        progress: AnswerProgress,
693    },
694    /// A file the turn carried that the model was not shown.
695    ///
696    /// # Why the writing stage has to be told
697    ///
698    /// A turn's attachments reach the model as parts of the request, so a
699    /// question about a document is answered from the document. When one does
700    /// not get there — the application no longer holds it, the budget for one
701    /// request would not take it, no provider in the chain accepts its media
702    /// type — the model has the user's sentence about a file and no file.
703    ///
704    /// A stage asked to answer about something it cannot see answers anyway, and
705    /// an answer about the wrong document is worse than an answer about none. So
706    /// this is a fact and not only a notice: the notice tells the user
707    /// deterministically, and the fact is what stops the prose describing a
708    /// document nobody looked at.
709    ///
710    /// It widens nothing the narrator may claim. A file not shown is not an
711    /// effect, so no [`ClaimClass`] covers it and no receipt could back it.
712    AttachmentNotShown {
713        /// The file, as the turn named it.
714        attachment_id: AttachmentId,
715        /// What the user called it, when the upload carried a name.
716        filename: Option<String>,
717        /// Why it was not shown, in the reader's locale.
718        reason: String,
719    },
720    /// An obligation the case still has open.
721    ///
722    /// # Why this is a fact and not a licence
723    ///
724    /// Everything else here describes something that *happened*. This
725    /// describes something that has not: a value the workflow's own projection
726    /// says is still outstanding. It is on the same side of the design as the
727    /// rest, because it comes from the projection rather than from the model,
728    /// and the projection is a pure function of committed state.
729    ///
730    /// It widens nothing the narrator may claim. An obligation is not an
731    /// effect, so no [`ClaimClass`] covers it and no receipt could back it; the
732    /// claim guard is unmoved. What it changes is whether the assistant can ask
733    /// for the next thing. A flow whose job is to collect several values could
734    /// previously only acknowledge each one and wait, because the stage that
735    /// writes the prose was never told what was still missing.
736    ///
737    /// # It must be the obligations *after* the turn
738    ///
739    /// The projection that framed the interpretation is the one from before
740    /// the commands ran. Narrating from it asks the user for the value they
741    /// have just supplied, which is worse than saying nothing. The runtime
742    /// re-projects the cases a turn changed and narrates from that.
743    ObligationOpen {
744        /// The case, at the revision the obligation was read at.
745        case_ref: CaseRef,
746        /// The obligation, in the workflow's own vocabulary.
747        obligation: serde_json::Value,
748        /// The same thing in words a person would recognise, when the workflow
749        /// says it ([`WorkflowDefinition::obligation_sentence`](crate::flow::WorkflowDefinition::obligation_sentence)).
750        /// The serialized value is the domain's structure, and a stage handed only that
751        /// guesses what to ask for.
752        #[serde(default, skip_serializing_if = "Option::is_none")]
753        sentence: Option<String>,
754        /// Whether this is a case the turn engaged or one that merely exists.
755        ///
756        /// See [`FactRelevance`]. Facts about the turn sort first, because the
757        /// writing stage is told to ask for the first one.
758        #[serde(default)]
759        relevance: FactRelevance,
760    },
761    /// Retrieved knowledge.
762    Knowledge {
763        /// Chunk id.
764        chunk_id: String,
765        /// Source id.
766        source_id: String,
767        /// The text.
768        text: String,
769    },
770}
771
772/// Classes of claims the narrator must not make (spec §18.3).
773///
774/// Deliberately exhaustive, like [`InteractionStatus`]:
775/// the list is a closed vocabulary with an [`ALL`](Self::ALL) table, and a
776/// caller that forbids claims must be forced by the compiler to consider every
777/// one of them.
778#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
779#[serde(rename_all = "snake_case")]
780pub enum ClaimClass {
781    /// "Created".
782    Creation,
783    /// "Updated".
784    Update,
785    /// "Deleted".
786    Deletion,
787    /// "Submitted" / "sent".
788    Submission,
789    /// "Accepted".
790    Acceptance,
791    /// "Delivered".
792    Delivery,
793    /// "Completed".
794    Completion,
795    /// "You can see the card below".
796    InteractionVisibility,
797    /// "I will notify you".
798    FutureNotification,
799}
800
801impl ClaimClass {
802    /// Every class.
803    pub const ALL: [Self; 9] = [
804        Self::Creation,
805        Self::Update,
806        Self::Deletion,
807        Self::Submission,
808        Self::Acceptance,
809        Self::Delivery,
810        Self::Completion,
811        Self::InteractionVisibility,
812        Self::FutureNotification,
813    ];
814}
815
816/// A receipt as summarized for the narrator.
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818pub struct ReceiptSummary {
819    /// The receipt.
820    pub receipt_id: ReceiptId,
821    /// Status code.
822    pub status_code: String,
823    /// Severity.
824    pub severity: ReceiptSeverity,
825    /// Events behind it.
826    pub event_ids: Vec<EventId>,
827    /// The receipt's own title, in the reader's locale.
828    ///
829    /// # Why the narrator is shown the copy and not only the code
830    ///
831    /// A status code says that something happened; it does not say what. A
832    /// narrator given `firm.field_set` and nothing else can only write
833    /// the sentence that is true of any such event — that a datum was noted —
834    /// while the receipt rendered directly beneath the prose says which field
835    /// took which value. The reader gets an acknowledgement that is vaguer than
836    /// the block under it, from the same turn.
837    ///
838    /// This does not widen what the narrator may claim. The receipt is already
839    /// on screen, already passed the claim guard, and is already backed by the
840    /// events it cites; letting the prose above it say the same thing is
841    /// restating a claim the server has made, not making a new one.
842    pub title: String,
843    /// The receipt's own body, in the reader's locale. See [`Self::title`].
844    pub body: String,
845}
846
847impl ReceiptSummary {
848    /// Summarizes `receipt` for a reader of `locale`.
849    ///
850    /// The copy is resolved here rather than carried as
851    /// [`crate::locale::LocalizedText`] so that what the
852    /// narrator reads is exactly the string the reader will see below it. Two
853    /// renderings of one receipt in one turn is the failure this avoids.
854    #[must_use]
855    pub fn of(receipt: &OperationalReceipt, locale: &crate::locale::Locale) -> Self {
856        Self {
857            receipt_id: receipt.receipt_id,
858            status_code: receipt.status_code.clone(),
859            severity: receipt.severity,
860            event_ids: receipt.event_ids.clone(),
861            title: receipt.title.resolve(locale).to_owned(),
862            body: receipt.body.resolve(locale).to_owned(),
863        }
864    }
865}
866
867/// One option as summarized for the narrator.
868#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
869pub struct OptionSummary {
870    /// Option id.
871    pub id: OptionId,
872    /// Resolved label.
873    pub label: String,
874}
875
876/// The next interaction as summarized for the narrator.
877#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
878pub struct InteractionSummary {
879    /// The interaction.
880    pub interaction_id: InteractionId,
881    /// Shape.
882    pub kind: InteractionKind,
883    /// Whether it blocks the case.
884    pub blocking: bool,
885    /// What the card asks, in the reader's locale: the narrator reads the exact string
886    /// the user reads beside it, as it does a receipt's. Resolved here rather than carried
887    /// as [`crate::locale::LocalizedText`], for the reason [`ReceiptSummary::of`] gives.
888    #[serde(default)]
889    pub title: String,
890    /// The card's own body, in the reader's locale. Empty when it has none.
891    /// See [`Self::title`].
892    #[serde(default, skip_serializing_if = "String::is_empty")]
893    pub body: String,
894    /// Options with labels resolved for the turn's locale.
895    pub options: Vec<OptionSummary>,
896}
897
898/// What the runtime asks a narrator model to produce (spec §18.3). The output is
899/// inserted around deterministic blocks, never trusted as the blocks themselves.
900#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
901pub struct NarrationRequest {
902    /// Voice.
903    pub tone: ToneProfile,
904    /// Locale of the user.
905    pub locale: Locale,
906    /// The user's text, for acknowledgment.
907    #[serde(default, skip_serializing_if = "Option::is_none")]
908    pub user_text: Option<String>,
909    /// Questions to answer.
910    ///
911    /// Empty for the transition stage, and that is the point. A transition
912    /// shown a question answers it — that is what a model does with a question
913    /// in its context — so the user read the same explanation twice, once in
914    /// the answer block and once in the acknowledgement above it, in two
915    /// different wordings. Two increasingly explicit paragraphs of prompt did
916    /// not hold, because a rule the model has to remember is not a rule.
917    ///
918    /// The questions belong to the answer stage, which has them, writes one
919    /// block each with its own basis and its own guard, and is the only stage
920    /// that should be answering anything.
921    pub questions: Vec<AnswerTask>,
922    /// How many questions another block of this turn will answer.
923    ///
924    /// What the transition stage gets instead of the questions themselves: it
925    /// may need to know that an answer is coming — to leave room for it, or to
926    /// not ask for something that is about to be explained — and the count says
927    /// that without giving it anything to answer.
928    #[serde(default)]
929    pub pending_questions: usize,
930    /// Facts that may be stated.
931    pub allowed_facts: Vec<NarratableFact>,
932    /// Claims that must not be made.
933    pub forbidden_claim_classes: Vec<ClaimClass>,
934    /// Receipts that will surround the narration.
935    pub surrounding_receipts: Vec<ReceiptSummary>,
936    /// The next card, if any.
937    #[serde(default, skip_serializing_if = "Option::is_none")]
938    pub next_interaction_summary: Option<InteractionSummary>,
939    /// What each workflow in view wants said, in the phase its case is in.
940    ///
941    /// Server-authored, one per case, and empty for a deployment whose
942    /// workflows say nothing. Before this the composer had no briefing at all,
943    /// so everything a domain had to say about its own voice — ask one question
944    /// at a time, do not restate a summary already on screen — reached the only
945    /// stage with a voice not at all.
946    #[serde(default, skip_serializing_if = "Vec::is_empty")]
947    pub guidance: Vec<CaseGuidance>,
948    /// The reply that came just before this turn, when there was one.
949    ///
950    /// # The one input a deictic question points at
951    ///
952    /// The answer stage was built from the tasks, the facts, the forbidden
953    /// classes and the receipts. The conversation was not in it, so a question
954    /// whose subject is **the previous assistant message** reached a stage that
955    /// could not see that message. Asked "in che senso?" about a sentence the
956    /// assistant had just written, it had the three words, a pile of open
957    /// obligations belonging to several records, and nothing about what had been
958    /// asked — and told the user his own message was not specific enough.
959    ///
960    /// One message rather than the window, and only the assistant's. A question
961    /// about the antecedent almost always means the last thing said; the current
962    /// message is already here as [`Self::user_text`]; and handing this stage
963    /// the whole transcript is the shape that produces prose about whatever is
964    /// in front of it, which three earlier rounds were spent undoing.
965    ///
966    /// Absent on the first turn of a conversation, and absent from the
967    /// acknowledgement stage's brief, which has no question to resolve.
968    #[serde(default, skip_serializing_if = "Option::is_none")]
969    pub preceding_reply: Option<String>,
970    /// What a person calls each case the brief mentions, as the directory labels it, so
971    /// the stage that speaks to the user names a record as the user does. Server-authored,
972    /// and only for cases the brief already carries: a label is what something is called,
973    /// not a new subject to bring up.
974    #[serde(default, skip_serializing_if = "Vec::is_empty")]
975    pub case_labels: Vec<CaseLabel>,
976}
977
978/// What a person calls one case.
979#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
980pub struct CaseLabel {
981    /// The case.
982    pub case_ref: crate::case::CaseRef,
983    /// The name the directory supplied for it, as shown to the user.
984    pub label: String,
985}
986
987/// One workflow's guidance for the stage that writes, and the case it is about.
988#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
989pub struct CaseGuidance {
990    /// The case the guidance is about.
991    pub case_ref: crate::case::CaseRef,
992    /// What the workflow wants said in the phase this case is in.
993    pub briefing: String,
994}
995
996/// Structural guard against unbacked operational claims (I16, spec §17.4).
997pub mod claim_guard {
998    use std::collections::{BTreeMap, BTreeSet};
999
1000    use serde::{Deserialize, Serialize};
1001
1002    use super::{AssistantTurn, NarratableFact, ResponseBlock};
1003    use crate::event::ReceiptSeverity;
1004    use crate::ids::{BlockId, InteractionId, ReceiptId};
1005
1006    /// A block claims something the turn does not back.
1007    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
1008    #[serde(tag = "kind", rename_all = "snake_case")]
1009    #[non_exhaustive]
1010    pub enum ClaimViolation {
1011        /// A model-authored block cites an operational outcome without a
1012        /// matching event-backed receipt block.
1013        #[error(
1014            "block {block_id} claims outcome of receipt {receipt_id} without a backing receipt block"
1015        )]
1016        UnbackedOperationalOutcome {
1017            /// The offending block.
1018            block_id: BlockId,
1019            /// The receipt cited.
1020            receipt_id: ReceiptId,
1021        },
1022        /// A success receipt has no event ids.
1023        #[error("receipt block {block_id} ({receipt_id}) claims success without events")]
1024        ReceiptWithoutEvents {
1025            /// The block.
1026            block_id: BlockId,
1027            /// The receipt.
1028            receipt_id: ReceiptId,
1029        },
1030        /// A model-authored block refers to a card that is not in the turn.
1031        #[error("block {block_id} refers to interaction {interaction_id} that is not in the turn")]
1032        InteractionNotInTurn {
1033            /// The offending block.
1034            block_id: BlockId,
1035            /// The interaction cited.
1036            interaction_id: InteractionId,
1037        },
1038        /// Two receipt blocks of one turn claim the same receipt id.
1039        #[error("block {block_id} repeats receipt {receipt_id}")]
1040        DuplicateReceiptId {
1041            /// The second block carrying the id.
1042            block_id: BlockId,
1043            /// The repeated receipt.
1044            receipt_id: ReceiptId,
1045        },
1046    }
1047
1048    /// Verifies that every operational outcome cited by a model-authored block
1049    /// is backed by a receipt block with a superset of its event ids and at
1050    /// least one event, that every success receipt has events, that no receipt
1051    /// id appears twice, and that every card the narration mentions is a block
1052    /// of the same turn.
1053    ///
1054    /// Receipt ids are unique per turn because they are derived from the events
1055    /// they cite ([`ReceiptId::derive`](crate::ids::ReceiptId::derive)). Merging
1056    /// two receipts that share an id would let a claim rest on another
1057    /// receipt's events.
1058    pub fn verify(turn: &AssistantTurn) -> Result<(), ClaimViolation> {
1059        let mut receipts: BTreeMap<ReceiptId, BTreeSet<_>> = BTreeMap::new();
1060        let mut interactions = BTreeSet::new();
1061        for block in &turn.blocks {
1062            match block {
1063                ResponseBlock::Receipt(r) => {
1064                    if r.receipt.severity == ReceiptSeverity::Success
1065                        && r.receipt.event_ids.is_empty()
1066                    {
1067                        return Err(ClaimViolation::ReceiptWithoutEvents {
1068                            block_id: r.block_id.clone(),
1069                            receipt_id: r.receipt.receipt_id,
1070                        });
1071                    }
1072                    if receipts
1073                        .insert(
1074                            r.receipt.receipt_id,
1075                            r.receipt.event_ids.iter().copied().collect(),
1076                        )
1077                        .is_some()
1078                    {
1079                        return Err(ClaimViolation::DuplicateReceiptId {
1080                            block_id: r.block_id.clone(),
1081                            receipt_id: r.receipt.receipt_id,
1082                        });
1083                    }
1084                }
1085                ResponseBlock::Interaction(i) => {
1086                    interactions.insert(i.view.id);
1087                }
1088                _ => {}
1089            }
1090        }
1091        for block in &turn.blocks {
1092            let (block_id, facts) = match block {
1093                ResponseBlock::Answer(a) => (&a.block_id, &a.facts_used),
1094                ResponseBlock::Transition(t) => (&t.block_id, &t.facts_used),
1095                _ => continue,
1096            };
1097            for fact in facts {
1098                match fact {
1099                    NarratableFact::OperationalOutcome {
1100                        receipt_id,
1101                        event_ids,
1102                        ..
1103                    } => {
1104                        let backed = receipts.get(receipt_id).is_some_and(|events| {
1105                            !events.is_empty() && event_ids.iter().all(|e| events.contains(e))
1106                        });
1107                        if !backed {
1108                            return Err(ClaimViolation::UnbackedOperationalOutcome {
1109                                block_id: block_id.clone(),
1110                                receipt_id: *receipt_id,
1111                            });
1112                        }
1113                    }
1114                    NarratableFact::InteractionAvailable { interaction_id, .. }
1115                        if !interactions.contains(interaction_id) =>
1116                    {
1117                        return Err(ClaimViolation::InteractionNotInTurn {
1118                            block_id: block_id.clone(),
1119                            interaction_id: *interaction_id,
1120                        });
1121                    }
1122                    _ => {}
1123                }
1124            }
1125        }
1126        Ok(())
1127    }
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132    use super::*;
1133    use crate::event::ReceiptSeverity;
1134
1135    fn receipt_with_block(block: &str, id: ReceiptId, events: Vec<EventId>) -> ResponseBlock {
1136        ResponseBlock::Receipt(ReceiptBlock {
1137            block_id: BlockId::from(block),
1138            receipt: OperationalReceipt {
1139                receipt_id: id,
1140                event_ids: events,
1141                severity: ReceiptSeverity::Success,
1142                title: "Sent".into(),
1143                body: "Rebooking sent".into(),
1144                status_code: "trip.rebooking_sent".into(),
1145                artifact_refs: vec![],
1146            },
1147        })
1148    }
1149
1150    fn receipt(id: ReceiptId, events: Vec<EventId>) -> ResponseBlock {
1151        receipt_with_block("r1", id, events)
1152    }
1153
1154    fn transition(fact: NarratableFact) -> ResponseBlock {
1155        ResponseBlock::Transition(GeneratedTransition {
1156            block_id: BlockId::from("t1"),
1157            text: "Done".into(),
1158            facts_used: vec![fact],
1159        })
1160    }
1161
1162    fn turn(blocks: Vec<ResponseBlock>) -> AssistantTurn {
1163        AssistantTurn {
1164            turn_id: TurnId::nil(),
1165            conversation_id: ConversationId::nil(),
1166            blocks,
1167            subjects: Vec::new(),
1168            expectations: Vec::new(),
1169            replay_token: ReplayToken::from("rt"),
1170            done: Vec::new(),
1171            offers: Vec::new(),
1172        }
1173    }
1174
1175    #[test]
1176    fn outcome_claims_need_receipt_blocks() {
1177        let rid = ReceiptId::nil();
1178        let eid = EventId::nil();
1179        let fact = NarratableFact::OperationalOutcome {
1180            receipt_id: rid,
1181            event_ids: vec![eid],
1182            status_code: "trip.rebooking_sent".into(),
1183        };
1184        assert!(
1185            claim_guard::verify(&turn(vec![
1186                transition(fact.clone()),
1187                receipt(rid, vec![eid])
1188            ]))
1189            .is_ok()
1190        );
1191        assert!(matches!(
1192            claim_guard::verify(&turn(vec![transition(fact.clone())])),
1193            Err(claim_guard::ClaimViolation::UnbackedOperationalOutcome { .. })
1194        ));
1195        assert!(matches!(
1196            claim_guard::verify(&turn(vec![receipt(rid, vec![])])),
1197            Err(claim_guard::ClaimViolation::ReceiptWithoutEvents { .. })
1198        ));
1199        let other_event = EventId::new();
1200        assert!(matches!(
1201            claim_guard::verify(&turn(vec![
1202                transition(fact),
1203                receipt(rid, vec![other_event])
1204            ])),
1205            Err(claim_guard::ClaimViolation::UnbackedOperationalOutcome { .. })
1206        ));
1207    }
1208
1209    #[test]
1210    fn two_receipts_may_not_share_an_id() {
1211        let rid = ReceiptId::nil();
1212        let mine = EventId::nil();
1213        let other = EventId::new();
1214        let fact = NarratableFact::OperationalOutcome {
1215            receipt_id: rid,
1216            event_ids: vec![other],
1217            status_code: "trip.rebooking_sent".into(),
1218        };
1219        // Without the check, the two receipts' event sets would be merged and
1220        // the claim would rest on the second receipt's event.
1221        let violation = claim_guard::verify(&turn(vec![
1222            transition(fact),
1223            receipt_with_block("r1", rid, vec![mine]),
1224            receipt_with_block("r2", rid, vec![other]),
1225        ]))
1226        .unwrap_err();
1227        assert_eq!(
1228            violation,
1229            claim_guard::ClaimViolation::DuplicateReceiptId {
1230                block_id: BlockId::from("r2"),
1231                receipt_id: rid,
1232            }
1233        );
1234    }
1235
1236    #[test]
1237    fn interaction_claims_need_interaction_blocks() {
1238        let fact = NarratableFact::InteractionAvailable {
1239            interaction_id: InteractionId::nil(),
1240            interaction_kind: InteractionKind::ConfirmCommand,
1241        };
1242        assert!(matches!(
1243            claim_guard::verify(&turn(vec![transition(fact)])),
1244            Err(claim_guard::ClaimViolation::InteractionNotInTurn { .. })
1245        ));
1246    }
1247
1248    #[test]
1249    fn blocks_carry_ids_and_tag() {
1250        let t = turn(vec![transition(NarratableFact::Knowledge {
1251            chunk_id: "c".into(),
1252            source_id: "s".into(),
1253            text: "t".into(),
1254        })]);
1255        assert_eq!(t.block_ids(), vec![BlockId::from("t1")]);
1256        let json = serde_json::to_value(&t.blocks[0]).unwrap();
1257        assert_eq!(json["kind"], "transition");
1258    }
1259}