Skip to main content

pointlock_store/
fold.rs

1//! Deterministic RunLog → [`CheckpointView`] folding.
2//!
3//! `Checkpoint = deterministic fold of the RunLog` (spine §6.1); this module
4//! is that fold, exposed as the pure function [`fold_checkpoint`] so the
5//! rebuild channel (`pointlock inspect --rebuild-checkpoint`, 07 §3.3) and
6//! the write-path materialization share one implementation and can be
7//! equality-checked against each other.
8//!
9//! ## Fold inputs
10//!
11//! The 17-event union does not carry the root flow id or the provider
12//! binding, so the fold takes a [`RunMeta`] (the `run` table row written by
13//! [`crate::Store::begin_run`]) alongside the ordered events. Both inputs
14//! are immutable after `begin_run`, keeping the fold a pure function of
15//! durable state.
16//!
17//! ## Coverage rules (iron rule: explicit, never silent)
18//!
19//! Every one of the 17 event types is matched explicitly below. Events that
20//! do not change the view are handled as *documented no-ops*, not wildcard
21//! arms. Every [`StepRecord`] field has an event carrier (spine §6.1 M1
22//! note — no placeholders remain):
23//!
24//! - `StepRecord.effectHash` / `judgeHash` / `resolvedInputs`: harvested
25//!   from the `stepEntered` payload.
26//! - `StepRecord.output`: harvested from the `stepExited` payload; a call
27//!   step whose exit carries no output keeps the callee outputs harvested
28//!   from `callFramePopped`.
29//! - `CallFrame.nextIndex`: a *body cursor* — advanced only when the
30//!   exited step is a direct body child of the innermost frame (nested
31//!   container children and iteration instances do not move it; M2).
32//! - `CallFrame.iterStack`: reconstructed from the open container spans —
33//!   an in-flight span whose successor extends it with an `iteration`
34//!   path frame is a live foreach; the `as` name comes from the
35//!   container's `stepEntered` snapshot (`{ items, as }`, the runner's
36//!   foreach carrier). No carrier ⇒ no IterState (never fabricated).
37//! - `CallFrame.vars` stays empty in the fold: `let` products have no
38//!   dedicated event carrier (the `stepEntered` snapshot of a let step
39//!   *is* the bindings object, but the fold is kind-agnostic); the runner
40//!   re-seeds scope from the records on resume (documented divergence,
41//!   pending the handler wave).
42//! - `binding.sessionLineage` / `binding.eventCursor`: copied verbatim from
43//!   [`RunMeta`]; no event advances the cursor or appends a session
44//!   generation yet (M1 scope).
45//! - `runResumed.alignmentReport` stays log-resident; the fold does not
46//!   re-base completed records.
47
48use pointlock_ir::{
49    ActChannel, ActionExecution, ActionName, ActionOutcome, ActionOutcomeKind,
50    AssertionOutcomeRecord, AttemptRecord, BindingState, CallFrame, CheckpointView, ErrorClass,
51    EvidenceRef, ExecutionMode, FlowId, Frontier, Hash, HumanPending, HumanPurpose, IterState,
52    ObservationRecord, PathFrame, PendingIntent, RunLogEvent, RunLogPayload, RunPath, StepId,
53    StepRecord, StepState, StepVerdict, Verdict, VerdictStatus,
54};
55use serde_json::Value;
56use std::collections::BTreeMap;
57
58use crate::error::FoldError;
59
60/// Immutable per-run metadata (the `run` table row): the fold input the
61/// 17-event union does not carry — root flow id, provider binding seed, and
62/// the identity fields also present in `runStarted`.
63#[derive(Debug, Clone, PartialEq)]
64pub struct RunMeta {
65    /// The run's id.
66    pub run_id: String,
67    /// The root flow's id (source of the root [`CallFrame`]'s `flowId`;
68    /// `runStarted` does not carry it).
69    pub flow_id: FlowId,
70    /// Content hash of the executing IR.
71    pub ir_hash: Hash,
72    /// Digest of the bound capability lockfile.
73    pub lockfile_digest: Hash,
74    /// The run's input parameters.
75    pub params_snapshot: Value,
76    /// Provider binding seed (M0: copied into the view verbatim; no event
77    /// advances the cursor yet — M0-C).
78    pub binding: BindingState,
79    /// Run creation timestamp (ms since epoch); informational.
80    pub created_at_ms: u64,
81}
82
83/// Run lifecycle status — the `run.status` column's closed four-value set
84/// (07 §3.3 DDL CHECK constraint).
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum RunStatus {
87    /// The run is executing (also the `begin_run` seed value).
88    Running,
89    /// The run was suspended (`runSuspended`).
90    Suspended,
91    /// A human interaction is pending (`humanRequested`; covers both step
92    /// and supervision purposes — the discriminator lives in
93    /// `humanPending.purpose`, not at run level; R13).
94    AwaitingHuman,
95    /// The run finished (`runFinished`).
96    Finished,
97}
98
99impl RunStatus {
100    /// The stored string form (matches the DDL CHECK constraint verbatim).
101    pub fn as_str(&self) -> &'static str {
102        match self {
103            RunStatus::Running => "running",
104            RunStatus::Suspended => "suspended",
105            RunStatus::AwaitingHuman => "awaitingHuman",
106            RunStatus::Finished => "finished",
107        }
108    }
109
110    /// Parses the stored string form.
111    pub fn parse(value: &str) -> Option<Self> {
112        match value {
113            "running" => Some(RunStatus::Running),
114            "suspended" => Some(RunStatus::Suspended),
115            "awaitingHuman" => Some(RunStatus::AwaitingHuman),
116            "finished" => Some(RunStatus::Finished),
117            _ => None,
118        }
119    }
120}
121
122/// Result of a fold: the materializable view plus the run status the same
123/// event sequence implies (kept together so the write path and the rebuild
124/// self-check share one transition function).
125#[derive(Debug, Clone, PartialEq)]
126pub struct FoldedRun {
127    /// The deterministic checkpoint view.
128    pub view: CheckpointView,
129    /// The run status after the last event.
130    pub status: RunStatus,
131}
132
133/// Folds an ordered event sequence into a [`CheckpointView`] + run status.
134///
135/// Pure and deterministic: same `(meta, events)` in, same [`FoldedRun`]
136/// out. With zero events it returns the seeded pre-start view (empty frame
137/// stack, `pending` frontier at the root flow path). Structural violations
138/// return [`FoldError`] — see the module docs for the coverage rules.
139pub fn fold_checkpoint(meta: &RunMeta, events: &[RunLogEvent]) -> Result<FoldedRun, FoldError> {
140    Ok(fold_state(meta, events)?.finish())
141}
142
143/// Folds the full ledger into the terminal [`FoldState`] (not yet
144/// collapsed to a view). `Store::append_event` caches it per run so the
145/// next append folds exactly one event instead of the whole ledger —
146/// the single-writer invariant (I1) makes the carried state exact, and
147/// `verify_checkpoint` remains the from-scratch cross-check.
148pub(crate) fn fold_state(meta: &RunMeta, events: &[RunLogEvent]) -> Result<FoldState, FoldError> {
149    let mut state = FoldState::seed(meta);
150    let mut prev_seq: Option<u64> = None;
151    for event in events {
152        if event.run_id != meta.run_id {
153            return Err(FoldError::RunIdMismatch {
154                seq: event.seq,
155                expected: meta.run_id.clone(),
156                actual: event.run_id.clone(),
157            });
158        }
159        if let Some(prev) = prev_seq
160            && event.seq <= prev
161        {
162            return Err(FoldError::NonMonotonicSeq {
163                prev,
164                seq: event.seq,
165            });
166        }
167        prev_seq = Some(event.seq);
168        state.apply(event)?;
169    }
170    Ok(state)
171}
172
173/// Scratch record of the step currently being assembled between
174/// `stepEntered` and `stepExited`. Kept as a stack: a `call` step stays
175/// in flight while its callee's steps enter and exit above it.
176#[derive(Debug, Clone)]
177struct InFlightStep {
178    run_path: RunPath,
179    step_id: StepId,
180    effect_hash: Hash,
181    judge_hash: Hash,
182    resolved_inputs: Value,
183    attempts: Vec<AttemptRecord>,
184    /// Callee outputs harvested from `callFramePopped` (call steps); the
185    /// `stepExited` payload's own output takes precedence when present.
186    call_outputs: Option<Value>,
187    observations: Vec<ObservationRecord>,
188    evidence: Vec<EvidenceRef>,
189    assertion_outcomes: Vec<AssertionOutcomeRecord>,
190    verdict: Option<StepVerdict>,
191}
192
193/// (chainIndex, channel, actionName) of one `actionIntent` (item ②).
194type DispatchIdentity = (Option<u32>, Option<ActChannel>, Option<ActionName>);
195
196#[derive(Debug, Clone)]
197pub(crate) struct FoldState {
198    view: CheckpointView,
199    status: RunStatus,
200    root_flow_id: FlowId,
201    started: bool,
202    in_flight: Vec<InFlightStep>,
203    /// callId → the intent's dispatch identity (item ②): in-memory
204    /// carrier from `actionIntent` to the settling `attemptRecord`;
205    /// never persisted (the durable shapes stay unchanged).
206    intent_dispatch: BTreeMap<String, DispatchIdentity>,
207    /// The run-path prefix of each live frame (parallel to `view.frames`):
208    /// the root flow path, then each pushed call frame's event path. The
209    /// direct-body-child test for `nextIndex` needs it.
210    frame_paths: Vec<RunPath>,
211}
212
213impl FoldState {
214    fn seed(meta: &RunMeta) -> Self {
215        let root_path: RunPath = vec![PathFrame::Flow {
216            flow_id: meta.flow_id.clone(),
217            ir_hash: meta.ir_hash.clone(),
218        }];
219        FoldState {
220            view: CheckpointView {
221                run_id: meta.run_id.clone(),
222                ir_hash: meta.ir_hash.clone(),
223                lockfile_digest: meta.lockfile_digest.clone(),
224                params_snapshot: meta.params_snapshot.clone(),
225                binding: meta.binding.clone(),
226                completed: Vec::new(),
227                frames: Vec::new(),
228                frontier: Frontier {
229                    run_path: root_path,
230                    state: StepState::Pending,
231                    pending_intent: None,
232                },
233                human_pending: None,
234            },
235            status: RunStatus::Running,
236            root_flow_id: meta.flow_id.clone(),
237            started: false,
238            in_flight: Vec::new(),
239            intent_dispatch: BTreeMap::new(),
240            frame_paths: Vec::new(),
241        }
242    }
243
244    pub(crate) fn finish(mut self) -> FoldedRun {
245        // Live foreach reconstruction (07 §3.2 iterStack): an in-flight
246        // span whose successor's run path extends it with an `iteration`
247        // frame is a live foreach round; the `as` name comes from the
248        // container's stepEntered snapshot ({ items, as }). No carrier ⇒
249        // no IterState (never fabricated).
250        for frame in &mut self.view.frames {
251            frame.iter_stack.clear();
252        }
253        for pair in self.in_flight.windows(2) {
254            let (parent, child) = (&pair[0], &pair[1]);
255            if child.run_path.len() <= parent.run_path.len() {
256                continue;
257            }
258            let extends = parent
259                .run_path
260                .iter()
261                .zip(child.run_path.iter())
262                .all(|(a, b)| same_site(a, b));
263            let Some(PathFrame::Iteration { index, key }) =
264                child.run_path.get(parent.run_path.len())
265            else {
266                continue;
267            };
268            let Some(var) = parent
269                .resolved_inputs
270                .get("as")
271                .and_then(Value::as_str)
272                .filter(|_| extends)
273            else {
274                continue;
275            };
276            // The IterState belongs to the innermost frame whose prefix
277            // covers the foreach span.
278            let owner = self.frame_paths.iter().rposition(|prefix| {
279                parent.run_path.len() >= prefix.len()
280                    && prefix
281                        .iter()
282                        .zip(parent.run_path.iter())
283                        .all(|(a, b)| same_site(a, b))
284            });
285            if let Some(owner) = owner
286                && owner < self.view.frames.len()
287            {
288                self.view.frames[owner].iter_stack.push(IterState {
289                    var: var.to_owned(),
290                    index: *index,
291                    key: key.clone(),
292                });
293            }
294        }
295        FoldedRun {
296            view: self.view,
297            status: self.status,
298        }
299    }
300
301    /// Applies one event. All 17 payload variants are matched explicitly
302    /// (`callFramePushed` twice — its `rebase` discriminant selects between
303    /// opening a frame and re-entering one); no-op arms are documented as
304    /// such (M0 iron rule — nothing is silently ignored via a wildcard).
305    pub(crate) fn apply(&mut self, event: &RunLogEvent) -> Result<(), FoldError> {
306        let seq = event.seq;
307        if !self.started && !matches!(event.payload, RunLogPayload::RunStarted { .. }) {
308            return Err(FoldError::EventBeforeRunStarted {
309                seq,
310                event_type: event.payload.event_type(),
311            });
312        }
313        match &event.payload {
314            RunLogPayload::RunStarted {
315                ir_hash,
316                lockfile_digest,
317                params_snapshot,
318                // Per-segment supervision policy is log-resident audit data
319                // (spine §6.9): CheckpointView has no field for it.
320                supervise_policy: _,
321            } => {
322                if self.started {
323                    return Err(FoldError::DuplicateRunStarted { seq });
324                }
325                self.started = true;
326                // The log is the truth: adopt the payload's identity fields
327                // (begin_run writes the same values into the run row).
328                self.view.ir_hash = ir_hash.clone();
329                self.view.lockfile_digest = lockfile_digest.clone();
330                self.view.params_snapshot = params_snapshot.clone();
331                // Root call frame: flowId comes from RunMeta (the payload
332                // does not carry it), inputs are the params snapshot
333                // (07 §3.2: the root frame references paramsSnapshot).
334                self.view.frames.push(CallFrame {
335                    flow_id: self.root_flow_id.clone(),
336                    ir_hash: ir_hash.clone(),
337                    call_step_id: None,
338                    inputs_snapshot: params_snapshot.clone(),
339                    vars: Default::default(),
340                    iter_stack: Vec::new(),
341                    next_index: 0,
342                });
343                self.view.frontier = Frontier {
344                    run_path: vec![PathFrame::Flow {
345                        flow_id: self.root_flow_id.clone(),
346                        ir_hash: ir_hash.clone(),
347                    }],
348                    state: StepState::Pending,
349                    pending_intent: None,
350                };
351                self.frame_paths.push(vec![PathFrame::Flow {
352                    flow_id: self.root_flow_id.clone(),
353                    ir_hash: ir_hash.clone(),
354                }]);
355                self.status = RunStatus::Running;
356            }
357            RunLogPayload::StepEntered {
358                step_id,
359                effect_hash,
360                judge_hash,
361                resolved_inputs,
362            } => {
363                self.in_flight.push(InFlightStep {
364                    run_path: event.run_path.clone(),
365                    step_id: step_id.clone(),
366                    effect_hash: effect_hash.clone(),
367                    judge_hash: judge_hash.clone(),
368                    resolved_inputs: resolved_inputs.clone(),
369                    attempts: Vec::new(),
370                    call_outputs: None,
371                    observations: Vec::new(),
372                    evidence: Vec::new(),
373                    assertion_outcomes: Vec::new(),
374                    verdict: None,
375                });
376                self.view.frontier = Frontier {
377                    run_path: event.run_path.clone(),
378                    state: StepState::Ready,
379                    pending_intent: None,
380                };
381            }
382            RunLogPayload::PreflightProbed { outcomes } => {
383                // The probe outcomes stay log-resident (they are probes,
384                // not the step's assert-phase outcomes), but the frontier's
385                // STATE materializes (spine §6.2 / §6.6): a passed probe
386                // set leaves the step `probing` (the act overwrites it with
387                // `acting` moments later — the window is only visible when
388                // the run stops in it), and a missed one leaves it
389                // `drifted`, which is exactly what a checkpoint suspended
390                // on drift must say — 「resume probe failed; awaiting
391                // onResumeDrift disposition」 was unobservable before this
392                // arm wrote it.
393                //
394                // An EMPTY outcome list is the `unprobed` mark (07 §4.2
395                // rule 1), a note that nothing was checked — not a phase
396                // transition; the state stays whatever it was.
397                if !outcomes.is_empty() {
398                    let missed = outcomes
399                        .iter()
400                        .any(|outcome| outcome.result != VerdictStatus::Pass);
401                    self.view.frontier.state = if missed {
402                        StepState::Drifted
403                    } else {
404                        StepState::Probing
405                    };
406                }
407            }
408            RunLogPayload::ActionIntent {
409                call_id,
410                args_snapshot,
411                chain_index,
412                channel,
413                action_name,
414            } => {
415                // Fold-internal intent→settle carrier (2026-07-18
416                // incorporation, item ②): the durable PendingIntent shape
417                // stays unchanged (I1 on existing stores); the identity
418                // fields ride in memory keyed by callId, deterministic
419                // from events (the full-refold fallback reproduces it).
420                self.intent_dispatch.insert(
421                    call_id.clone(),
422                    (*chain_index, *channel, action_name.clone()),
423                );
424                // The crash-window key (07 §3.1): frontier records the
425                // hanging intent until the matching actionSettled.
426                self.view.frontier.state = StepState::Acting;
427                self.view.frontier.pending_intent = Some(PendingIntent {
428                    call_id: call_id.clone(),
429                    args_snapshot: args_snapshot.clone(),
430                });
431            }
432            RunLogPayload::ActionSettled { call_id, outcome } => {
433                let step =
434                    self.in_flight
435                        .last_mut()
436                        .ok_or_else(|| FoldError::EventOutsideStep {
437                            seq,
438                            event_type: event.payload.event_type(),
439                        })?;
440                let dispatch = self.intent_dispatch.remove(call_id).unwrap_or_default();
441                step.attempts
442                    .push(attempt_record(call_id, outcome, dispatch));
443                // Clear the pending intent this terminal settles. A
444                // non-matching callId leaves the intent in place (a runner
445                // discipline breach worth surfacing at reconcile time, not
446                // papering over here).
447                if self
448                    .view
449                    .frontier
450                    .pending_intent
451                    .as_ref()
452                    .is_some_and(|intent| intent.call_id == *call_id)
453                {
454                    self.view.frontier.pending_intent = None;
455                }
456                self.view.frontier.state = StepState::Settling;
457            }
458            RunLogPayload::ObservationRecorded { observation } => {
459                let step =
460                    self.in_flight
461                        .last_mut()
462                        .ok_or_else(|| FoldError::EventOutsideStep {
463                            seq,
464                            event_type: event.payload.event_type(),
465                        })?;
466                if let Some(screenshot) = &observation.screenshot {
467                    step.evidence.push(screenshot.clone());
468                }
469                if let Some(ui_snapshot) = &observation.ui_snapshot {
470                    step.evidence.push(ui_snapshot.clone());
471                }
472                step.observations.push(observation.clone());
473                self.view.frontier.state = StepState::Observing;
474            }
475            RunLogPayload::AssertionEvaluated { outcome } => {
476                let step =
477                    self.in_flight
478                        .last_mut()
479                        .ok_or_else(|| FoldError::EventOutsideStep {
480                            seq,
481                            event_type: event.payload.event_type(),
482                        })?;
483                step.assertion_outcomes.push(outcome.clone());
484                self.view.frontier.state = StepState::Asserting;
485            }
486            RunLogPayload::VerdictRecorded {
487                verdict,
488                localized,
489                localization_gaps: _,
490                remote_archival_error: _,
491            } => {
492                // The judgment's localized manifest merges into the
493                // record's evidence (item ③, 2026-07-18): dedup key
494                // (sha256, asset.id), first occurrence wins — the same
495                // rule the dossier applies, so the two surfaces can
496                // never diverge on one ledger. Gaps stay log-resident
497                // (the dossier reads them from the event; the checkpoint
498                // carries successes only).
499                //
500                // The target is chosen by the event's OWN run path, never by
501                // "is anything in flight". A crash-opened span leaves an
502                // in-flight step that has nothing to do with an offline
503                // re-judgement written against a completed record, and
504                // attaching the verdict to it would silently overwrite a
505                // different step's judgment — the ledger would then say the
506                // crashed step was judged and the re-judged one was not.
507                // Every live `verdictRecorded` is appended at its own step's
508                // path (the same path its `stepEntered` used), so path
509                // equality selects exactly the target `last_mut()` used to
510                // select in every live case.
511                let in_flight_at_path = self
512                    .in_flight
513                    .iter()
514                    .rposition(|step| same_instance(&step.run_path, &event.run_path));
515                if let Some(index) = in_flight_at_path {
516                    let step = &mut self.in_flight[index];
517                    merge_evidence(&mut step.evidence, localized);
518                    step.verdict = Some(project_verdict(verdict));
519                    self.view.frontier.state = StepState::Judged;
520                } else if let Some(record) = self
521                    .view
522                    .completed
523                    .iter_mut()
524                    .rev()
525                    .find(|record| same_instance(&record.run_path, &event.run_path))
526                {
527                    // Offline re-judgement (judgeDirty, spine §6.7-A): the
528                    // log gets a *new* verdictRecorded with `supersedes`;
529                    // the fold re-projects the completed record. Rejudge
530                    // manifests are empty by construction (nothing is
531                    // localized offline) — the merge is a no-op there,
532                    // and the arm treats the field identically in both
533                    // branches so incremental and full refolds agree.
534                    merge_evidence(&mut record.evidence, localized);
535                    record.verdict = Some(project_verdict(verdict));
536                } else {
537                    return Err(FoldError::VerdictWithoutTarget { seq });
538                }
539            }
540            RunLogPayload::StepExited {
541                state,
542                output,
543                localized,
544                ..
545            } => {
546                let mut step = self
547                    .in_flight
548                    .pop()
549                    .ok_or(FoldError::StepExitedWithoutEntry { seq })?;
550                // An unverified exit's manifest merges here (item ③
551                // review fix) — same rule as the verdict-borne one.
552                merge_evidence(&mut step.evidence, localized);
553                // A terminal exit of the awaiting step settles its pending
554                // request without a response — the lazy timeout settlement
555                // (verdict unknown) and the aborted disposition both take
556                // this path (06 §5.3).
557                if self
558                    .view
559                    .human_pending
560                    .as_ref()
561                    .is_some_and(|pending| pending.run_path == event.run_path)
562                {
563                    self.view.human_pending = None;
564                }
565                // Every terminal exit leaves a record (judged / skipped /
566                // blocked / aborted alike): completion order == exit order.
567                self.view.completed.push(StepRecord {
568                    run_path: step.run_path,
569                    step_id: step.step_id,
570                    // Harvested from the stepEntered carrier (spine §6.1
571                    // M1 note).
572                    effect_hash: step.effect_hash,
573                    judge_hash: step.judge_hash,
574                    attempts: step.attempts,
575                    resolved_inputs: step.resolved_inputs,
576                    // The exit's projected output wins; a call step whose
577                    // exit carries none keeps the callee outputs from
578                    // callFramePopped.
579                    output: output.clone().or(step.call_outputs),
580                    observations: step.observations,
581                    evidence: step.evidence,
582                    assertion_outcomes: step.assertion_outcomes,
583                    verdict: step.verdict,
584                });
585                // Advance the innermost frame's *body* cursor — only when
586                // the exited step is a direct body child of that frame
587                // (nested container children and iteration instances do
588                // not move it; M2). While a callee runs, the innermost
589                // frame *is* the callee frame, so this lands on the right
590                // frame for nested exits too. Frame identity is compared
591                // site-wise (hash-insensitive) so a cross-IR resume
592                // segment keeps advancing the same frame.
593                let frame = self
594                    .view
595                    .frames
596                    .last_mut()
597                    .ok_or(FoldError::NoActiveFrame { seq })?;
598                if self
599                    .frame_paths
600                    .last()
601                    .is_some_and(|prefix| direct_body_child(prefix, &event.run_path))
602                {
603                    frame.next_index += 1;
604                }
605                self.view.frontier = Frontier {
606                    run_path: event.run_path.clone(),
607                    state: *state,
608                    pending_intent: None,
609                };
610            }
611            RunLogPayload::CallFramePushed {
612                frame,
613                rebase: false,
614            } => {
615                self.view.frames.push(frame.clone());
616                self.frame_paths.push(event.run_path.clone());
617            }
618            RunLogPayload::CallFramePushed {
619                frame,
620                rebase: true,
621            } => {
622                // A live-frame re-entry under a repaired callee (07 §5.2
623                // case (a)) — NOT a new stack level. The addressed level is
624                // the event path's `call` depth, not the innermost frame: a
625                // resume walks back in from the root, so an outer frame is
626                // re-entered while the inner ones it once opened are still
627                // on the stack. Cross-IR safe by construction — the count
628                // reads the path's shape, never its hashes.
629                let level = event
630                    .run_path
631                    .iter()
632                    .filter(|frame| matches!(frame, PathFrame::Call { .. }))
633                    .count();
634                let depth = self.view.frames.len();
635                let Some(open) = self.view.frames.get_mut(level) else {
636                    return Err(FoldError::RebaseWithoutFrame { seq, level, depth });
637                };
638                // ONLY the callee pin moves. `inputsSnapshot` above all
639                // stays put: a live frame's snapshot is never re-evaluated
640                // for a new IR (07 §5.2 corollary / §4.6), and the descent
641                // was licensed precisely because the `inputs` expressions
642                // did not change — so the archived values ARE the ones the
643                // new IR would produce. The body cursor, iteration stack
644                // and vars describe where the frame *is*, which a repaired
645                // callee does not move either.
646                open.ir_hash = frame.ir_hash.clone();
647                // `frame_paths` is left alone: it is only ever compared
648                // through `same_site`, which is hash-insensitive precisely
649                // so a cross-IR resume keeps matching the same site.
650            }
651            RunLogPayload::CallFramePopped { outputs } => {
652                if self.view.frames.len() <= 1 {
653                    return Err(FoldError::PoppedRootFrame { seq });
654                }
655                self.view.frames.pop();
656                self.frame_paths.pop();
657                // The innermost in-flight step is the host call step (its
658                // callee's steps have all exited); the callee's outputs
659                // are the call step's output. Handler-repair frames have
660                // no host call step — nothing in flight, nothing to fill.
661                if let Some(step) = self.in_flight.last_mut() {
662                    step.call_outputs = outputs.clone();
663                }
664            }
665            RunLogPayload::HandlerTriggered {
666                hook: _,
667                trigger: _,
668                disposition: _,
669            } => {
670                // Documented no-op: handler firing is audit data. The hook
671                // trace materializes through the `hook` frames of
672                // subsequent events' run paths, not as a view field.
673            }
674            RunLogPayload::HumanRequested {
675                request_id,
676                purpose,
677                mode,
678                prompt,
679                // The presented evidence/values and the response contract
680                // stay log-resident; HumanPending does not carry them
681                // (spine §6.6, 06 §4.3 reads them back from the event).
682                presents: _,
683                decisions: _,
684                output_schema: _,
685                deadline_at_ms,
686            } => {
687                self.view.human_pending = Some(HumanPending {
688                    run_path: event.run_path.clone(),
689                    request_id: request_id.clone(),
690                    purpose: *purpose,
691                    mode: *mode,
692                    prompt: prompt.clone(),
693                    deadline_at_ms: *deadline_at_ms,
694                });
695                self.view.frontier.state = StepState::AwaitingHuman;
696                self.status = RunStatus::AwaitingHuman;
697            }
698            RunLogPayload::HumanResponded {
699                request_id,
700                purpose,
701                response,
702                actor: _,
703            } => {
704                // Lazy settlement (spine §6.8): a response must pair the
705                // pending request; the arbitration result itself
706                // (response/actor) stays log-resident.
707                let paired = self
708                    .view
709                    .human_pending
710                    .as_ref()
711                    .is_some_and(|pending| pending.request_id == *request_id);
712                if !paired {
713                    return Err(FoldError::UnpairedHumanResponse {
714                        seq,
715                        request_id: request_id.clone(),
716                    });
717                }
718                // A supervision `suspend` answer is non-final (spine §6.9):
719                // the request stays pending across segments and the run
720                // keeps awaiting a proceed/abort ruling.
721                let retains = *purpose == HumanPurpose::Supervision
722                    && response.get("decision").and_then(Value::as_str) == Some("suspend");
723                if retains {
724                    self.status = RunStatus::AwaitingHuman;
725                } else {
726                    self.view.human_pending = None;
727                    // The frontier step state stays as-is: the follow-up
728                    // event (actionIntent on supervision-proceed,
729                    // verdictRecorded / stepExited on a human step) moves
730                    // it.
731                    self.status = RunStatus::Running;
732                }
733            }
734            RunLogPayload::RunSuspended { .. } => {
735                // Run-level status only; the frontier keeps its last
736                // step-level state so resume knows where the step stood.
737                // A suspension while a human request is pending keeps the
738                // run self-describing as awaitingHuman (spine §6.8: the
739                // wait is a legal suspend point).
740                self.status = if self.view.human_pending.is_some() {
741                    RunStatus::AwaitingHuman
742                } else {
743                    RunStatus::Suspended
744                };
745            }
746            RunLogPayload::RunResumed {
747                // Log-resident in M0: the fold does not re-base completed
748                // records from the alignment report (module docs; M0-C).
749                alignment_report: _,
750                // Per-segment policy, log-resident (as for runStarted).
751                supervise_policy: _,
752                event_cursor,
753            } => {
754                self.status = RunStatus::Running;
755                // 07 §4.5 (incorporated 2026-07-18): a cursor-bearing
756                // resume extends the lineage and reseeds the watermark.
757                // A cursor-less resume (old ledgers) changes nothing —
758                // the view names exactly what was recorded, never an
759                // invented generation (principle 4). Only new-binary
760                // ledgers carry the field, so stored views and refolds
761                // agree on every pre-incorporation store (I1).
762                if let Some(cursor) = event_cursor {
763                    if self.view.binding.session_lineage.last() != Some(&cursor.session_id) {
764                        self.view
765                            .binding
766                            .session_lineage
767                            .push(cursor.session_id.clone());
768                    }
769                    self.view.binding.event_cursor = cursor.clone();
770                }
771            }
772            RunLogPayload::RunFinished {
773                verdict: _,
774                remote_archival_error: _,
775            } => {
776                // The folded flow verdict stays log-resident; the view has
777                // no field for it (reports read it from the log).
778                self.status = RunStatus::Finished;
779            }
780        }
781        Ok(())
782    }
783}
784
785/// Whether `path` addresses a direct body child of the frame rooted at
786/// `prefix`: exactly one extra frame, and that frame is a step or a call
787/// (iteration instances and nested container children are not body
788/// children).
789fn direct_body_child(prefix: &RunPath, path: &RunPath) -> bool {
790    path.len() == prefix.len() + 1
791        && prefix.iter().zip(path.iter()).all(|(a, b)| same_site(a, b))
792        && matches!(
793            path.last(),
794            Some(PathFrame::Step { .. }) | Some(PathFrame::Call { .. })
795        )
796}
797
798/// Whether two run paths address the SAME step instance.
799///
800/// Hash-insensitive by way of [`same_site`], because a cross-IR resume
801/// rewrites the flow and callee hashes of a path whose sites are unchanged:
802/// a step whose span was opened by the crashed segment carries the OLD
803/// flow's hashes, while the events the resume appends carry the new ones.
804/// Both branches of the `verdictRecorded` arm use this one notion — if they
805/// disagreed, a path could match neither and a legitimate verdict would
806/// fold to `VerdictWithoutTarget`.
807fn same_instance(a: &[PathFrame], b: &[PathFrame]) -> bool {
808    a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| same_site(x, y))
809}
810
811/// Site-wise path-frame identity: hash-insensitive (a cross-IR resume
812/// changes the flow/callee hashes of the same site), position-sensitive.
813fn same_site(a: &PathFrame, b: &PathFrame) -> bool {
814    match (a, b) {
815        (PathFrame::Flow { flow_id: a, .. }, PathFrame::Flow { flow_id: b, .. }) => a == b,
816        (PathFrame::Step { step_id: a }, PathFrame::Step { step_id: b }) => a == b,
817        (
818            PathFrame::Call {
819                step_id: a,
820                callee_flow_id: af,
821                ..
822            },
823            PathFrame::Call {
824                step_id: b,
825                callee_flow_id: bf,
826                ..
827            },
828        ) => a == b && af == bf,
829        (
830            PathFrame::Iteration { index: a, key: ak },
831            PathFrame::Iteration { index: b, key: bk },
832        ) => a == b && ak == bk,
833        (a, b) => a == b,
834    }
835}
836
837/// Merges a judgment's localized manifest into a record's evidence:
838/// dedup key (sha256, asset.id), first occurrence wins (item ③ — one
839/// rule for checkpoint and dossier).
840fn merge_evidence(evidence: &mut Vec<EvidenceRef>, localized: &[EvidenceRef]) {
841    for entry in localized {
842        let duplicate = evidence
843            .iter()
844            .any(|existing| existing.sha256 == entry.sha256 && existing.asset.id == entry.asset.id);
845        if !duplicate {
846            evidence.push(entry.clone());
847        }
848    }
849}
850
851/// Projects a four-way terminal into the durable [`AttemptRecord`]
852/// (spine §6.6): discriminant + best-effort classification. The full
853/// outcome stays in the `actionSettled` payload.
854fn attempt_record(
855    call_id: &str,
856    outcome: &ActionOutcome,
857    dispatch: DispatchIdentity,
858) -> AttemptRecord {
859    let kind = match outcome {
860        ActionOutcome::Succeeded { .. } => ActionOutcomeKind::Succeeded,
861        ActionOutcome::Failed { .. } => ActionOutcomeKind::Failed,
862        ActionOutcome::Cancelled { .. } => ActionOutcomeKind::Cancelled,
863        ActionOutcome::TimedOut { .. } => ActionOutcomeKind::TimedOut,
864    };
865    // Best-effort M0 classification: ErrorInfo.code is an open string the
866    // provider adapter maps onto the closed ErrorClass; when the code
867    // already spells a class verbatim we adopt it, otherwise None (a
868    // dedicated carrier is M0-C).
869    let error_class = match outcome {
870        ActionOutcome::Succeeded { .. } => None,
871        ActionOutcome::Failed { error }
872        | ActionOutcome::Cancelled { error }
873        | ActionOutcome::TimedOut { error } => parse_error_class(&error.code),
874    };
875    let (execution_mode, fallback_reason) = match outcome {
876        ActionOutcome::Succeeded { result } => match &result.execution {
877            Some(ActionExecution::NativeSemantic { .. }) => {
878                (Some(ExecutionMode::NativeSemantic), None)
879            }
880            Some(ActionExecution::WebSemantic { .. }) => (Some(ExecutionMode::WebSemantic), None),
881            Some(ActionExecution::CoordinateFallback {
882                fallback_reason, ..
883            }) => (
884                Some(ExecutionMode::CoordinateFallback),
885                Some(*fallback_reason),
886            ),
887            None => (None, None),
888        },
889        _ => (None, None),
890    };
891    let (chain_index, channel, action_name) = dispatch;
892    AttemptRecord {
893        call_id: call_id.to_owned(),
894        outcome: kind,
895        error_class,
896        execution_mode,
897        fallback_reason,
898        chain_index,
899        channel,
900        action_name,
901    }
902}
903
904fn parse_error_class(code: &str) -> Option<ErrorClass> {
905    serde_json::from_value(Value::String(code.to_owned())).ok()
906}
907
908/// Projects a folded [`Verdict`] onto the durable per-step [`StepVerdict`]
909/// (spine §6.6: summary/evidence stay in the `verdictRecorded` payload).
910fn project_verdict(verdict: &Verdict) -> StepVerdict {
911    StepVerdict {
912        status: verdict.status,
913        degraded: verdict.degraded,
914        supersedes: verdict.supersedes.clone(),
915    }
916}
917
918#[cfg(test)]
919mod tests {
920    use pointlock_ir::{BindingState, EventCursor, SupervisePolicy};
921    use serde_json::json;
922
923    use super::*;
924
925    fn hash(fill: char) -> Hash {
926        Hash::new(format!("sha256:{}", fill.to_string().repeat(64))).expect("valid hash")
927    }
928
929    fn meta() -> RunMeta {
930        RunMeta {
931            run_id: "run-1".to_owned(),
932            flow_id: FlowId::new("checkout").expect("valid flow id"),
933            ir_hash: hash('a'),
934            lockfile_digest: hash('b'),
935            params_snapshot: json!({"user": "alice"}),
936            binding: BindingState {
937                device_id: "dev-1".to_owned(),
938                session_lineage: vec!["s-1".to_owned()],
939                event_cursor: EventCursor {
940                    session_id: "s-1".to_owned(),
941                    last_sequence: 0,
942                },
943            },
944            created_at_ms: 1,
945        }
946    }
947
948    fn event(seq: u64, run_path: RunPath, payload: RunLogPayload) -> RunLogEvent {
949        RunLogEvent {
950            run_id: "run-1".to_owned(),
951            seq,
952            at_ms: 1_000 + seq,
953            run_path,
954            payload,
955        }
956    }
957
958    fn run_started() -> RunLogPayload {
959        RunLogPayload::RunStarted {
960            ir_hash: hash('a'),
961            lockfile_digest: hash('b'),
962            params_snapshot: json!({"user": "alice"}),
963            supervise_policy: Some(SupervisePolicy::Mutating),
964        }
965    }
966
967    #[test]
968    fn zero_events_fold_to_the_seeded_pre_start_view() {
969        let folded = fold_checkpoint(&meta(), &[]).expect("fold");
970        assert!(folded.view.frames.is_empty());
971        assert_eq!(folded.view.frontier.state, StepState::Pending);
972        assert_eq!(folded.status, RunStatus::Running);
973    }
974
975    #[test]
976    fn run_started_initializes_the_root_frame_from_meta_flow_id() {
977        let folded = fold_checkpoint(&meta(), &[event(1, vec![], run_started())]).expect("fold");
978        assert_eq!(folded.view.frames.len(), 1);
979        assert_eq!(folded.view.frames[0].flow_id.as_str(), "checkout");
980        assert_eq!(
981            folded.view.frames[0].inputs_snapshot,
982            json!({"user": "alice"})
983        );
984        assert_eq!(folded.view.frames[0].next_index, 0);
985    }
986
987    #[test]
988    fn events_before_run_started_are_rejected() {
989        let err = fold_checkpoint(
990            &meta(),
991            &[event(
992                1,
993                vec![],
994                RunLogPayload::StepEntered {
995                    step_id: StepId::new("login").expect("valid step id"),
996                    effect_hash: hash('c'),
997                    judge_hash: hash('d'),
998                    resolved_inputs: json!({}),
999                },
1000            )],
1001        )
1002        .expect_err("must reject");
1003        assert_eq!(
1004            err,
1005            FoldError::EventBeforeRunStarted {
1006                seq: 1,
1007                event_type: "stepEntered"
1008            }
1009        );
1010    }
1011
1012    #[test]
1013    fn duplicate_run_started_is_rejected() {
1014        let err = fold_checkpoint(
1015            &meta(),
1016            &[
1017                event(1, vec![], run_started()),
1018                event(2, vec![], run_started()),
1019            ],
1020        )
1021        .expect_err("must reject");
1022        assert_eq!(err, FoldError::DuplicateRunStarted { seq: 2 });
1023    }
1024
1025    #[test]
1026    fn non_monotonic_seq_is_rejected() {
1027        let err = fold_checkpoint(
1028            &meta(),
1029            &[
1030                event(1, vec![], run_started()),
1031                event(
1032                    1,
1033                    vec![],
1034                    RunLogPayload::RunSuspended {
1035                        provider_state_summary: None,
1036                        reason: None,
1037                    },
1038                ),
1039            ],
1040        )
1041        .expect_err("must reject");
1042        assert_eq!(err, FoldError::NonMonotonicSeq { prev: 1, seq: 1 });
1043    }
1044
1045    #[test]
1046    fn step_exited_without_entry_is_rejected() {
1047        let err = fold_checkpoint(
1048            &meta(),
1049            &[
1050                event(1, vec![], run_started()),
1051                event(
1052                    2,
1053                    vec![],
1054                    RunLogPayload::StepExited {
1055                        provider_state_summary: None,
1056                        state: StepState::Judged,
1057                        output: None,
1058                        localized: Vec::new(),
1059                        localization_gaps: Vec::new(),
1060                    },
1061                ),
1062            ],
1063        )
1064        .expect_err("must reject");
1065        assert_eq!(err, FoldError::StepExitedWithoutEntry { seq: 2 });
1066    }
1067
1068    #[test]
1069    fn step_record_fields_are_harvested_from_the_carrier_events() {
1070        let step_path: RunPath = vec![PathFrame::Step {
1071            step_id: StepId::new("login").expect("valid step id"),
1072        }];
1073        let folded = fold_checkpoint(
1074            &meta(),
1075            &[
1076                event(1, vec![], run_started()),
1077                event(
1078                    2,
1079                    step_path.clone(),
1080                    RunLogPayload::StepEntered {
1081                        step_id: StepId::new("login").expect("valid step id"),
1082                        effect_hash: hash('c'),
1083                        judge_hash: hash('d'),
1084                        resolved_inputs: json!({"element": {"identifier": "loginButton"}}),
1085                    },
1086                ),
1087                event(
1088                    3,
1089                    step_path.clone(),
1090                    RunLogPayload::StepExited {
1091                        provider_state_summary: None,
1092                        state: StepState::Judged,
1093                        output: Some(json!({"ok": true})),
1094                        localized: Vec::new(),
1095                        localization_gaps: Vec::new(),
1096                    },
1097                ),
1098            ],
1099        )
1100        .expect("fold");
1101        let record = &folded.view.completed[0];
1102        assert_eq!(record.effect_hash, hash('c'));
1103        assert_eq!(record.judge_hash, hash('d'));
1104        assert_eq!(
1105            record.resolved_inputs,
1106            json!({"element": {"identifier": "loginButton"}})
1107        );
1108        assert_eq!(record.output, Some(json!({"ok": true})));
1109    }
1110
1111    #[test]
1112    fn popping_the_root_frame_is_rejected() {
1113        let err = fold_checkpoint(
1114            &meta(),
1115            &[
1116                event(1, vec![], run_started()),
1117                event(2, vec![], RunLogPayload::CallFramePopped { outputs: None }),
1118            ],
1119        )
1120        .expect_err("must reject");
1121        assert_eq!(err, FoldError::PoppedRootFrame { seq: 2 });
1122    }
1123
1124    #[test]
1125    fn unpaired_human_response_is_rejected() {
1126        let err = fold_checkpoint(
1127            &meta(),
1128            &[
1129                event(1, vec![], run_started()),
1130                event(
1131                    2,
1132                    vec![],
1133                    RunLogPayload::HumanResponded {
1134                        request_id: "req-ghost".to_owned(),
1135                        purpose: pointlock_ir::HumanPurpose::Step,
1136                        response: json!({}),
1137                        actor: "cli:tester".to_owned(),
1138                    },
1139                ),
1140            ],
1141        )
1142        .expect_err("must reject");
1143        assert_eq!(
1144            err,
1145            FoldError::UnpairedHumanResponse {
1146                seq: 2,
1147                request_id: "req-ghost".to_owned()
1148            }
1149        );
1150    }
1151
1152    fn human_requested(request_id: &str, purpose: HumanPurpose) -> RunLogPayload {
1153        RunLogPayload::HumanRequested {
1154            request_id: request_id.to_owned(),
1155            purpose,
1156            mode: match purpose {
1157                HumanPurpose::Step => Some(pointlock_ir::HumanMode::Confirm),
1158                HumanPurpose::Supervision => None,
1159            },
1160            prompt: "Decide".to_owned(),
1161            presents: json!([]),
1162            decisions: None,
1163            output_schema: None,
1164            deadline_at_ms: match purpose {
1165                HumanPurpose::Step => Some(9_000),
1166                HumanPurpose::Supervision => None,
1167            },
1168        }
1169    }
1170
1171    #[test]
1172    fn supervision_suspend_answer_keeps_the_request_pending() {
1173        let step_path: RunPath = vec![PathFrame::Step {
1174            step_id: StepId::new("pay").expect("valid step id"),
1175        }];
1176        let folded = fold_checkpoint(
1177            &meta(),
1178            &[
1179                event(1, vec![], run_started()),
1180                event(
1181                    2,
1182                    step_path.clone(),
1183                    human_requested("req-1", HumanPurpose::Supervision),
1184                ),
1185                event(
1186                    3,
1187                    step_path.clone(),
1188                    RunLogPayload::HumanResponded {
1189                        request_id: "req-1".to_owned(),
1190                        purpose: HumanPurpose::Supervision,
1191                        response: json!({"decision": "suspend"}),
1192                        actor: "cli:tester".to_owned(),
1193                    },
1194                ),
1195                // The suspend ruling parks the run; the request survives.
1196                event(
1197                    4,
1198                    vec![],
1199                    RunLogPayload::RunSuspended {
1200                        provider_state_summary: None,
1201                        reason: None,
1202                    },
1203                ),
1204            ],
1205        )
1206        .expect("fold");
1207        let pending = folded.view.human_pending.expect("request stays pending");
1208        assert_eq!(pending.request_id, "req-1");
1209        assert_eq!(folded.status, RunStatus::AwaitingHuman);
1210
1211        // A later final ruling still pairs and settles the wait.
1212        let folded = fold_checkpoint(
1213            &meta(),
1214            &[
1215                event(1, vec![], run_started()),
1216                event(
1217                    2,
1218                    step_path.clone(),
1219                    human_requested("req-1", HumanPurpose::Supervision),
1220                ),
1221                event(
1222                    3,
1223                    step_path.clone(),
1224                    RunLogPayload::HumanResponded {
1225                        request_id: "req-1".to_owned(),
1226                        purpose: HumanPurpose::Supervision,
1227                        response: json!({"decision": "suspend"}),
1228                        actor: "cli:tester".to_owned(),
1229                    },
1230                ),
1231                event(
1232                    4,
1233                    step_path,
1234                    RunLogPayload::HumanResponded {
1235                        request_id: "req-1".to_owned(),
1236                        purpose: HumanPurpose::Supervision,
1237                        response: json!({"decision": "proceed"}),
1238                        actor: "cli:tester".to_owned(),
1239                    },
1240                ),
1241            ],
1242        )
1243        .expect("fold");
1244        assert!(folded.view.human_pending.is_none());
1245        assert_eq!(folded.status, RunStatus::Running);
1246    }
1247
1248    #[test]
1249    fn run_suspended_while_a_request_is_pending_stays_awaiting_human() {
1250        let step_path: RunPath = vec![PathFrame::Step {
1251            step_id: StepId::new("ask").expect("valid step id"),
1252        }];
1253        let folded = fold_checkpoint(
1254            &meta(),
1255            &[
1256                event(1, vec![], run_started()),
1257                event(2, step_path, human_requested("req-2", HumanPurpose::Step)),
1258                event(
1259                    3,
1260                    vec![],
1261                    RunLogPayload::RunSuspended {
1262                        provider_state_summary: None,
1263                        reason: None,
1264                    },
1265                ),
1266            ],
1267        )
1268        .expect("fold");
1269        assert_eq!(folded.status, RunStatus::AwaitingHuman);
1270        let pending = folded.view.human_pending.expect("pending");
1271        assert_eq!(pending.deadline_at_ms, Some(9_000));
1272        assert_eq!(pending.mode, Some(pointlock_ir::HumanMode::Confirm));
1273    }
1274
1275    #[test]
1276    fn step_exit_settles_the_pending_request_without_a_response() {
1277        // The lazy timeout settlement shape: the awaiting step exits
1278        // (verdict unknown) with no humanResponded on the ledger.
1279        let step_path: RunPath = vec![PathFrame::Step {
1280            step_id: StepId::new("ask").expect("valid step id"),
1281        }];
1282        let folded = fold_checkpoint(
1283            &meta(),
1284            &[
1285                event(1, vec![], run_started()),
1286                event(
1287                    2,
1288                    step_path.clone(),
1289                    RunLogPayload::StepEntered {
1290                        step_id: StepId::new("ask").expect("valid step id"),
1291                        effect_hash: hash('c'),
1292                        judge_hash: hash('d'),
1293                        resolved_inputs: json!({"presents": []}),
1294                    },
1295                ),
1296                event(
1297                    3,
1298                    step_path.clone(),
1299                    human_requested("req-3", HumanPurpose::Step),
1300                ),
1301                event(
1302                    4,
1303                    vec![],
1304                    RunLogPayload::RunSuspended {
1305                        provider_state_summary: None,
1306                        reason: None,
1307                    },
1308                ),
1309                event(
1310                    5,
1311                    vec![],
1312                    RunLogPayload::RunResumed {
1313                        alignment_report: pointlock_ir::AlignmentReport {
1314                            entries: vec![],
1315                            resume_point: None,
1316                            requires_confirmation: vec![],
1317                        },
1318                        supervise_policy: None,
1319                        event_cursor: None,
1320                    },
1321                ),
1322                event(
1323                    6,
1324                    step_path,
1325                    RunLogPayload::StepExited {
1326                        provider_state_summary: None,
1327                        state: StepState::Judged,
1328                        output: None,
1329                        localized: Vec::new(),
1330                        localization_gaps: Vec::new(),
1331                    },
1332                ),
1333            ],
1334        )
1335        .expect("fold");
1336        assert!(folded.view.human_pending.is_none());
1337        assert_eq!(folded.status, RunStatus::Running);
1338    }
1339
1340    #[test]
1341    fn error_class_is_adopted_only_when_the_code_spells_a_class() {
1342        assert_eq!(
1343            parse_error_class("action_failed_final"),
1344            Some(ErrorClass::ActionFailedFinal)
1345        );
1346        assert_eq!(parse_error_class("SOME_DAEMON_CODE"), None);
1347    }
1348}