Skip to main content

pointlock_store/
error.rs

1//! Error types of the store: fold-level (pure, structural), store-level
2//! (SQLite / IO / serialization plus the fold errors they wrap), and the
3//! typed rejections of the human-response arbitration (06 §4.3).
4
5use std::fmt;
6
7/// A structural violation detected while folding a RunLog into a
8/// [`pointlock_ir::CheckpointView`].
9///
10/// The fold is deliberately *not* lenient about impossible sequences
11/// (M0 iron rule: events the fold does not understand or cannot anchor are
12/// surfaced, never silently ignored) — a fold error inside
13/// [`crate::Store::append_event`] rolls the whole append back, so a log that
14/// cannot fold is never persisted.
15#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
16pub enum FoldError {
17    /// An event in the fold input belongs to a different run.
18    #[error("event at seq {seq} belongs to run {actual}, fold input is for run {expected}")]
19    RunIdMismatch {
20        /// Sequence number of the offending event.
21        seq: u64,
22        /// The run the fold input is for.
23        expected: String,
24        /// The run the event claims.
25        actual: String,
26    },
27    /// Event sequence numbers must be strictly increasing.
28    #[error("event seq {seq} does not increase monotonically (previous {prev})")]
29    NonMonotonicSeq {
30        /// The previous sequence number.
31        prev: u64,
32        /// The offending sequence number.
33        seq: u64,
34    },
35    /// Any event other than `runStarted` arrived before `runStarted`.
36    #[error("event {event_type} at seq {seq} precedes runStarted")]
37    EventBeforeRunStarted {
38        /// Sequence number of the offending event.
39        seq: u64,
40        /// Wire discriminant of the offending event.
41        event_type: &'static str,
42    },
43    /// A second `runStarted` was appended (later segments start with
44    /// `runResumed`).
45    #[error("duplicate runStarted at seq {seq} (later segments start with runResumed)")]
46    DuplicateRunStarted {
47        /// Sequence number of the offending event.
48        seq: u64,
49    },
50    /// A step-scoped event arrived with no in-flight step to anchor to.
51    #[error("{event_type} at seq {seq} has no in-flight step (missing stepEntered)")]
52    EventOutsideStep {
53        /// Sequence number of the offending event.
54        seq: u64,
55        /// Wire discriminant of the offending event.
56        event_type: &'static str,
57    },
58    /// `stepExited` arrived without a matching `stepEntered`.
59    #[error("stepExited at seq {seq} without a matching stepEntered")]
60    StepExitedWithoutEntry {
61        /// Sequence number of the offending event.
62        seq: u64,
63    },
64    /// `verdictRecorded` targets neither the in-flight step nor any
65    /// completed record at its run path.
66    #[error("verdictRecorded at seq {seq} targets neither the in-flight step nor a completed step")]
67    VerdictWithoutTarget {
68        /// Sequence number of the offending event.
69        seq: u64,
70    },
71    /// `callFramePopped` would pop the root flow frame (or an empty stack).
72    #[error("callFramePopped at seq {seq} would pop the root frame")]
73    PoppedRootFrame {
74        /// Sequence number of the offending event.
75        seq: u64,
76    },
77    /// `stepExited` with no active call frame to advance (structurally
78    /// impossible after a well-formed `runStarted`).
79    #[error("stepExited at seq {seq} with no active call frame")]
80    NoActiveFrame {
81        /// Sequence number of the offending event.
82        seq: u64,
83    },
84    /// A `callFramePushed` marked `rebase` names a stack level that is not
85    /// open (07 §5.2: a rebase re-enters an already-open frame; it never
86    /// creates one).
87    #[error(
88        "callFramePushed(rebase) at seq {seq} targets frame level {level}, stack depth is {depth}"
89    )]
90    RebaseWithoutFrame {
91        /// Sequence number of the offending event.
92        seq: u64,
93        /// The stack level the event's run path addresses.
94        level: usize,
95        /// The number of frames currently open.
96        depth: usize,
97    },
98    /// `humanResponded` pairs no pending request.
99    #[error("humanResponded at seq {seq} pairs no pending request (requestId {request_id})")]
100    UnpairedHumanResponse {
101        /// Sequence number of the offending event.
102        seq: u64,
103        /// The unpaired request id.
104        request_id: String,
105    },
106}
107
108/// Store-level error: SQLite / filesystem / serialization failures, fold
109/// errors surfaced through the write path, and the self-check verdicts of
110/// [`crate::Store::verify_checkpoint`].
111#[derive(Debug, thiserror::Error)]
112pub enum StoreError {
113    /// SQLite error.
114    #[error("sqlite error: {0}")]
115    Sqlite(#[from] rusqlite::Error),
116    /// Filesystem error (evidence area, store directory).
117    #[error("i/o error: {0}")]
118    Io(#[from] std::io::Error),
119    /// JSON (de)serialization error.
120    #[error("serialization error: {0}")]
121    Serde(#[from] serde_json::Error),
122    /// The referenced run does not exist.
123    #[error("unknown run {0}")]
124    UnknownRun(String),
125    /// `begin_run` was called with a run id that already exists.
126    #[error("run {0} already exists")]
127    DuplicateRun(String),
128    /// The run has no materialized checkpoint row yet (no events appended).
129    #[error("run {0} has no materialized checkpoint")]
130    NoCheckpoint(String),
131    /// A fold error (see [`FoldError`] for the rollback semantics on the
132    /// write path).
133    #[error(transparent)]
134    Fold(#[from] FoldError),
135    /// The checkpoint row lags the log head — rule 2 of 07 §3.3 (same-
136    /// transaction materialization) was violated; a store-layer bug.
137    #[error(
138        "checkpoint for run {run_id} is stale: materialized at seq \
139         {materialized_seq}, log head is {log_seq}"
140    )]
141    StaleCheckpoint {
142        /// The run whose checkpoint is stale.
143        run_id: String,
144        /// `checkpoint.log_seq` as stored.
145        materialized_seq: u64,
146        /// The actual `MAX(seq)` of the run's log.
147        log_seq: u64,
148    },
149    /// The materialized view differs from the full-log refold — I1's
150    /// runtime self-check tripped; a store-layer bug (07 §3.3).
151    #[error(
152        "materialized checkpoint for run {run_id} (log_seq {log_seq}) differs from the rebuilt fold"
153    )]
154    CheckpointMismatch {
155        /// The run whose checkpoint mismatches.
156        run_id: String,
157        /// The `log_seq` the stored view claims.
158        log_seq: u64,
159        /// Canonical JSON of the stored view.
160        materialized: String,
161        /// Canonical JSON of the rebuilt view.
162        rebuilt: String,
163    },
164    /// The `run.status` column differs from the folded status.
165    #[error("run {run_id} status '{stored}' differs from folded status '{folded}'")]
166    StatusMismatch {
167        /// The run whose status mismatches.
168        run_id: String,
169        /// `run.status` as stored.
170        stored: String,
171        /// Status produced by the fold.
172        folded: String,
173    },
174    /// A stored row failed to parse back into its typed shape.
175    #[error("corrupt stored data for run {run_id}: {reason}")]
176    Corrupt {
177        /// The run whose stored data is corrupt.
178        run_id: String,
179        /// What failed to parse.
180        reason: String,
181    },
182    /// [`crate::Store::submit_human_response`] refused the response.
183    /// Typed and side-effect free: a rejected response never becomes a
184    /// `humanResponded` event (06 §4.3 — bad data does not enter the
185    /// ledger).
186    #[error("human response for request {request_id} of run {run_id} rejected: {reason}")]
187    HumanResponseRejected {
188        /// The run the response targeted.
189        run_id: String,
190        /// The request the response tried to pair with.
191        request_id: String,
192        /// Why the arbitration refused it.
193        reason: HumanResponseRejection,
194    },
195    /// A locate/dossier query referenced a step instance the ledger never
196    /// entered (spine §9: locate resolves recorded instances only).
197    #[error("run {run_id} has no step instance at '{path}'")]
198    UnknownStepInstance {
199        /// The queried run.
200        run_id: String,
201        /// The canonical path (or bare step id) that failed to resolve.
202        path: String,
203    },
204    /// A bare step id matched several instances (iterations/hook entries);
205    /// the caller must pick one canonical path.
206    #[error("step '{step}' of run {run_id} is ambiguous; candidates: {}", candidates.join(", "))]
207    AmbiguousStep {
208        /// The queried run.
209        run_id: String,
210        /// The bare step id.
211        step: String,
212        /// Canonical strings of every matching instance.
213        candidates: Vec<String>,
214    },
215    /// A canonical run-path string failed to parse (spine §9 grammar).
216    #[error("run path '{input}' does not parse: {message}")]
217    BadRunPath {
218        /// The offending input.
219        input: String,
220        /// Parser message with offset context.
221        message: String,
222    },
223}
224
225/// The closed rejection vocabulary of the human-response arbitration
226/// (06 §4.3: `unknownRequest | alreadyResponded | deadlineExceeded |
227/// schemaViolation`, plus the lazily-settled leftover).
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum HumanResponseRejection {
230    /// No `humanRequested` event carries this request id.
231    UnknownRequest,
232    /// A final response is already paired (first response wins; a
233    /// supervision `suspend` answer is non-final and does not pair).
234    AlreadyResponded,
235    /// The response arrived after the request's absolute deadline, judged
236    /// by the store-receipt clock — the only timeout judge (06 §4.3 rule
237    /// 2). Lazy settlement of the expired request stays the runner's job;
238    /// the arbitration only refuses the late response.
239    DeadlineExpired {
240        /// The request's absolute deadline (ms since epoch).
241        deadline_at_ms: u64,
242        /// When the store received the response (ms since epoch).
243        received_at_ms: u64,
244    },
245    /// The request is no longer pending (its step was already settled).
246    Settled,
247    /// The response payload does not match the shape the request's
248    /// purpose/mode demands (includes `outputSchema` violations of
249    /// `provideInput` and out-of-vocabulary decisions).
250    InvalidShape {
251        /// What exactly is wrong, human-readable.
252        reason: String,
253    },
254}
255
256impl fmt::Display for HumanResponseRejection {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match self {
259            HumanResponseRejection::UnknownRequest => write!(f, "unknown request"),
260            HumanResponseRejection::AlreadyResponded => {
261                write!(f, "already responded (first response wins)")
262            }
263            HumanResponseRejection::DeadlineExpired {
264                deadline_at_ms,
265                received_at_ms,
266            } => write!(
267                f,
268                "deadline expired (deadlineAtMs {deadline_at_ms}, received at {received_at_ms})"
269            ),
270            HumanResponseRejection::Settled => {
271                write!(f, "the request is no longer pending (already settled)")
272            }
273            HumanResponseRejection::InvalidShape { reason } => {
274                write!(f, "invalid response shape: {reason}")
275            }
276        }
277    }
278}