Skip to main content

trellis_core/
trace.rs

1use crate::{
2    AuditEntry, ClearReason, CollectionDiffTrace, InvariantResultTrace, OutputFrameKind, OutputKey,
3    RebaselineReason, ResourceCommand, ResourceKey, ScopeId, ScopeLifecycleTrace,
4    StagedInputChange, TransactionId, TransactionPhase, TransactionResult,
5};
6use crate::{NodeId, Revision};
7
8/// Deterministic payload-free projection of a committed transaction result.
9#[derive(Clone, Debug, Eq, PartialEq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct TransactionTrace {
12    /// Committed transaction id.
13    pub transaction_id: TransactionId,
14    /// Graph revision after commit.
15    pub revision: Revision,
16    /// Staged input writes in stable node-id order.
17    pub staged_input_changes: Vec<StagedInputChange>,
18    /// Input nodes changed by this transaction.
19    pub changed_inputs: Vec<NodeId>,
20    /// Initial dirty roots in stable node-id order.
21    pub dirty_roots: Vec<NodeId>,
22    /// Derived nodes recomputed in deterministic topological order.
23    pub recomputed_derived_nodes: Vec<NodeId>,
24    /// Derived nodes changed by this transaction.
25    pub changed_derived_nodes: Vec<NodeId>,
26    /// Collection nodes recomputed in deterministic topological order.
27    pub recomputed_collection_nodes: Vec<NodeId>,
28    /// Collection nodes changed by this transaction.
29    pub changed_collection_nodes: Vec<NodeId>,
30    /// Payload-neutral collection diff summaries.
31    pub collection_diffs: Vec<CollectionDiffTrace>,
32    /// Resource command identity and operation trace.
33    pub resource_commands: Vec<ResourceCommandTrace>,
34    /// Output frame identity and kind trace.
35    pub output_frames: Vec<OutputFrameTrace>,
36    /// Scope lifecycle events emitted by the transaction.
37    pub scope_events: Vec<ScopeLifecycleTrace>,
38    /// Audit log emitted by the transaction.
39    pub audit_log: Vec<AuditEntry>,
40    /// Phase trace emitted by the transaction.
41    pub phase_trace: Vec<TransactionPhase>,
42    /// Optional invariant results layered by testing support.
43    pub invariant_results: Vec<InvariantResultTrace>,
44}
45
46impl TransactionTrace {
47    /// Builds a deterministic trace from a transaction result.
48    pub fn from_result<C, O>(result: &TransactionResult<C, O>) -> Self {
49        Self {
50            transaction_id: result.transaction_id,
51            revision: result.revision,
52            staged_input_changes: result.staged_input_changes.clone(),
53            changed_inputs: result.changed_inputs.clone(),
54            dirty_roots: result.dirty_roots.clone(),
55            recomputed_derived_nodes: result.recomputed_derived_nodes.clone(),
56            changed_derived_nodes: result.changed_derived_nodes.clone(),
57            recomputed_collection_nodes: result.recomputed_collection_nodes.clone(),
58            changed_collection_nodes: result.changed_collection_nodes.clone(),
59            collection_diffs: result.collection_diffs.clone(),
60            resource_commands: result
61                .resource_plan
62                .commands()
63                .iter()
64                .map(ResourceCommandTrace::from_command)
65                .collect(),
66            output_frames: result
67                .output_frames
68                .iter()
69                .map(|frame| OutputFrameTrace {
70                    output_key: frame.output_key,
71                    scope: frame.scope,
72                    transaction_id: frame.transaction_id,
73                    revision: frame.revision,
74                    kind: OutputFrameKindTrace::from_kind(&frame.kind),
75                })
76                .collect(),
77            scope_events: result.scope_events.clone(),
78            audit_log: result.audit_log.clone(),
79            phase_trace: result.phase_trace.clone(),
80            invariant_results: result.invariant_results.clone(),
81        }
82    }
83}
84
85/// Payload-free resource command trace.
86#[derive(Clone, Debug, Eq, PartialEq)]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88pub struct ResourceCommandTrace {
89    /// Resource identity.
90    pub key: ResourceKey,
91    /// Scope associated with the command.
92    pub scope: ScopeId,
93    /// Command operation.
94    pub kind: ResourceCommandKind,
95    /// Host-facing transition policy requested by the command.
96    pub transition: ResourceTransitionPolicy,
97}
98
99impl ResourceCommandTrace {
100    fn from_command<C>(command: &ResourceCommand<C>) -> Self {
101        Self {
102            key: command.key().clone(),
103            scope: command.scope(),
104            kind: ResourceCommandKind::from_command(command),
105            transition: ResourceTransitionPolicy::from_command(command),
106        }
107    }
108}
109
110/// Resource command operation without application payload.
111#[derive(Copy, Clone, Debug, Eq, PartialEq)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113pub enum ResourceCommandKind {
114    /// Open a resource.
115    Open,
116    /// Close a resource.
117    Close,
118    /// Replace a resource.
119    Replace,
120    /// Refresh a resource.
121    Refresh,
122}
123
124impl ResourceCommandKind {
125    pub(crate) fn from_command<C>(command: &ResourceCommand<C>) -> Self {
126        match command {
127            ResourceCommand::Open { .. } => Self::Open,
128            ResourceCommand::Close { .. } => Self::Close,
129            ResourceCommand::Replace { .. } => Self::Replace,
130            ResourceCommand::Refresh { .. } => Self::Refresh,
131        }
132    }
133}
134
135/// Structural resource transition policy without application payload.
136#[derive(Copy, Clone, Debug, Eq, PartialEq)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub enum ResourceTransitionPolicy {
139    /// Open a resource that is not currently owned.
140    Open,
141    /// Close a resource after final ownership is removed.
142    Close,
143    /// Replace a live resource through a host-native replace operation.
144    ReplaceAtomically,
145    /// Refresh an already live resource.
146    Refresh,
147    /// No lifecycle transition is requested.
148    Noop,
149}
150
151impl ResourceTransitionPolicy {
152    pub(crate) fn from_command<C>(command: &ResourceCommand<C>) -> Self {
153        match command {
154            ResourceCommand::Open { .. } => Self::Open,
155            ResourceCommand::Close { .. } => Self::Close,
156            ResourceCommand::Replace { .. } => Self::ReplaceAtomically,
157            ResourceCommand::Refresh { .. } => Self::Refresh,
158        }
159    }
160}
161
162/// Payload-free output frame trace.
163#[derive(Clone, Debug, Eq, PartialEq)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
165pub struct OutputFrameTrace {
166    /// Output identity.
167    pub output_key: OutputKey,
168    /// Scope that owns the output.
169    pub scope: ScopeId,
170    /// Transaction that emitted the frame.
171    pub transaction_id: TransactionId,
172    /// Revision carried by the frame.
173    pub revision: Revision,
174    /// Frame kind without materialized payload.
175    pub kind: OutputFrameKindTrace,
176}
177
178/// Output frame kind without materialized payload.
179#[derive(Copy, Clone, Debug, Eq, PartialEq)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181pub enum OutputFrameKindTrace {
182    /// Baseline frame.
183    Baseline,
184    /// Delta frame.
185    Delta,
186    /// Clear frame with reason.
187    Clear(ClearReason),
188    /// Rebaseline frame with reason.
189    Rebaseline(RebaselineReason),
190}
191
192impl OutputFrameKindTrace {
193    pub(crate) fn from_kind<O>(kind: &OutputFrameKind<O>) -> Self {
194        match kind {
195            OutputFrameKind::Baseline(_) => Self::Baseline,
196            OutputFrameKind::Delta(_) => Self::Delta,
197            OutputFrameKind::Clear(reason) => Self::Clear(*reason),
198            OutputFrameKind::Rebaseline(_, reason) => Self::Rebaseline(*reason),
199        }
200    }
201}
202
203impl<C, O> TransactionResult<C, O> {
204    /// Returns a deterministic payload-free projection of this result.
205    pub fn trace(&self) -> TransactionTrace {
206        TransactionTrace::from_result(self)
207    }
208}
209
210/// Difference between two replay trace sequences.
211#[derive(Clone, Debug, Eq, PartialEq)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213pub struct TraceMismatch {
214    /// Expected transaction traces.
215    pub expected: Vec<TransactionTrace>,
216    /// Actual transaction traces.
217    pub actual: Vec<TransactionTrace>,
218}
219
220/// Compares two deterministic transaction trace sequences.
221pub fn assert_transaction_traces_match(
222    expected: &[TransactionTrace],
223    actual: &[TransactionTrace],
224) -> Result<(), TraceMismatch> {
225    if expected == actual {
226        Ok(())
227    } else {
228        Err(TraceMismatch {
229            expected: expected.to_vec(),
230            actual: actual.to_vec(),
231        })
232    }
233}