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    /// Another writer holds the run's advisory writer lease
126    /// ([`crate::WriterLease`], 07 §3.3 rule 5): a live segment is driving
127    /// the run and this process must not append a second walk.
128    #[error("run {run_id} has a live writer (its writer lease is held by another process)")]
129    WriterBusy {
130        /// The run whose lease is held.
131        run_id: String,
132    },
133    /// `begin_run` was called with a run id that already exists.
134    #[error("run {0} already exists")]
135    DuplicateRun(String),
136    /// The run has no materialized checkpoint row yet (no events appended).
137    #[error("run {0} has no materialized checkpoint")]
138    NoCheckpoint(String),
139    /// A fold error (see [`FoldError`] for the rollback semantics on the
140    /// write path).
141    #[error(transparent)]
142    Fold(#[from] FoldError),
143    /// The checkpoint row lags the log head — rule 2 of 07 §3.3 (same-
144    /// transaction materialization) was violated; a store-layer bug.
145    #[error(
146        "checkpoint for run {run_id} is stale: materialized at seq \
147         {materialized_seq}, log head is {log_seq}"
148    )]
149    StaleCheckpoint {
150        /// The run whose checkpoint is stale.
151        run_id: String,
152        /// `checkpoint.log_seq` as stored.
153        materialized_seq: u64,
154        /// The actual `MAX(seq)` of the run's log.
155        log_seq: u64,
156    },
157    /// The materialized view differs from the full-log refold — I1's
158    /// runtime self-check tripped; a store-layer bug (07 §3.3).
159    #[error(
160        "materialized checkpoint for run {run_id} (log_seq {log_seq}) differs from the rebuilt fold"
161    )]
162    CheckpointMismatch {
163        /// The run whose checkpoint mismatches.
164        run_id: String,
165        /// The `log_seq` the stored view claims.
166        log_seq: u64,
167        /// Canonical JSON of the stored view.
168        materialized: String,
169        /// Canonical JSON of the rebuilt view.
170        rebuilt: String,
171    },
172    /// The `run.status` column differs from the folded status.
173    #[error("run {run_id} status '{stored}' differs from folded status '{folded}'")]
174    StatusMismatch {
175        /// The run whose status mismatches.
176        run_id: String,
177        /// `run.status` as stored.
178        stored: String,
179        /// Status produced by the fold.
180        folded: String,
181    },
182    /// A stored row failed to parse back into its typed shape.
183    #[error("corrupt stored data for run {run_id}: {reason}")]
184    Corrupt {
185        /// The run whose stored data is corrupt.
186        run_id: String,
187        /// What failed to parse.
188        reason: String,
189    },
190    /// [`crate::Store::submit_human_response`] refused the response.
191    /// Typed and side-effect free: a rejected response never becomes a
192    /// `humanResponded` event (06 §4.3 — bad data does not enter the
193    /// ledger).
194    #[error("human response for request {request_id} of run {run_id} rejected: {reason}")]
195    HumanResponseRejected {
196        /// The run the response targeted.
197        run_id: String,
198        /// The request the response tried to pair with.
199        request_id: String,
200        /// Why the arbitration refused it.
201        reason: HumanResponseRejection,
202    },
203    /// A locate/dossier query referenced a step instance the ledger never
204    /// entered (spine §9: locate resolves recorded instances only).
205    #[error("run {run_id} has no step instance at '{path}'")]
206    UnknownStepInstance {
207        /// The queried run.
208        run_id: String,
209        /// The canonical path (or bare step id) that failed to resolve.
210        path: String,
211    },
212    /// A bare step id matched several instances (iterations/hook entries);
213    /// the caller must pick one canonical path.
214    #[error("step '{step}' of run {run_id} is ambiguous; candidates: {}", candidates.join(", "))]
215    AmbiguousStep {
216        /// The queried run.
217        run_id: String,
218        /// The bare step id.
219        step: String,
220        /// Canonical strings of every matching instance.
221        candidates: Vec<String>,
222    },
223    /// `PRAGMA journal_mode = WAL` did not take at open (e.g. a network or
224    /// read-only filesystem): the durability contract (actionIntent fsync
225    /// under `synchronous=FULL` in WAL) cannot be honoured, so the store
226    /// refuses to open rather than run silently in another journal mode.
227    #[error(
228        "store at {root} could not enable WAL journaling; sqlite answered journal_mode '{mode}'"
229    )]
230    JournalModeNotWal {
231        /// The store root that failed to open.
232        root: String,
233        /// The journal mode sqlite actually reported.
234        mode: String,
235    },
236    /// A canonical run-path string failed to parse (spine §9 grammar).
237    #[error("run path '{input}' does not parse: {message}")]
238    BadRunPath {
239        /// The offending input.
240        input: String,
241        /// Parser message with offset context.
242        message: String,
243    },
244}
245
246/// The closed rejection vocabulary of the human-response arbitration
247/// (06 §4.3: `unknownRequest | alreadyResponded | deadlineExceeded |
248/// schemaViolation`, plus the lazily-settled leftover).
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub enum HumanResponseRejection {
251    /// No `humanRequested` event carries this request id.
252    UnknownRequest,
253    /// A final response is already paired (first response wins; a
254    /// supervision `suspend` answer is non-final and does not pair).
255    AlreadyResponded,
256    /// The response arrived after the request's absolute deadline, judged
257    /// by the store-receipt clock — the only timeout judge (06 §4.3 rule
258    /// 2). Lazy settlement of the expired request stays the runner's job;
259    /// the arbitration only refuses the late response.
260    DeadlineExpired {
261        /// The request's absolute deadline (ms since epoch).
262        deadline_at_ms: u64,
263        /// When the store received the response (ms since epoch).
264        received_at_ms: u64,
265    },
266    /// The request is no longer pending (its step was already settled).
267    Settled,
268    /// The response payload does not match the shape the request's
269    /// purpose/mode demands (includes `outputSchema` violations of
270    /// `provideInput` and out-of-vocabulary decisions).
271    InvalidShape {
272        /// What exactly is wrong, human-readable.
273        reason: String,
274    },
275}
276
277impl fmt::Display for HumanResponseRejection {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        match self {
280            HumanResponseRejection::UnknownRequest => write!(f, "unknown request"),
281            HumanResponseRejection::AlreadyResponded => {
282                write!(f, "already responded (first response wins)")
283            }
284            HumanResponseRejection::DeadlineExpired {
285                deadline_at_ms,
286                received_at_ms,
287            } => write!(
288                f,
289                "deadline expired (deadlineAtMs {deadline_at_ms}, received at {received_at_ms})"
290            ),
291            HumanResponseRejection::Settled => {
292                write!(f, "the request is no longer pending (already settled)")
293            }
294            HumanResponseRejection::InvalidShape { reason } => {
295                write!(f, "invalid response shape: {reason}")
296            }
297        }
298    }
299}