Skip to main content

fsqlite_types/
cx.rs

1//! Capability context (`Cx`) for FrankenSQLite.
2//!
3//! This is a **capability-passing style** context object that:
4//! - threads cancellation checks (`checkpoint`) through long-running operations
5//! - carries a [`Budget`] for deadline/priority propagation
6//! - encodes available effects (spawn/time/random/io/remote) in the type system
7//!   via [`cap::CapSet`], so widening is a **compile-time error**.
8//!
9//! # Compile-time capability narrowing
10//!
11//! Narrowing always succeeds:
12//! ```
13//! use fsqlite_types::cx::{cap, Cx};
14//!
15//! let cx = Cx::<cap::All>::new();
16//! let _compute = cx.restrict::<cap::None>();
17//! ```
18//!
19//! Widening is rejected at compile time:
20//! ```compile_fail
21//! use fsqlite_types::cx::{cap, Cx};
22//!
23//! let cx = Cx::<cap::All>::new();
24//! let compute = cx.restrict::<cap::None>();
25//! let _nope = compute.restrict::<cap::All>();
26//! ```
27//
28
29use std::marker::PhantomData;
30use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
31use std::sync::{Arc, Mutex, Weak};
32use std::time::Duration;
33
34#[cfg(feature = "native")]
35use asupersync::types::Time as NativeTime;
36#[cfg(feature = "native")]
37use asupersync::types::{CancelKind as NativeCancelKind, CancelReason as NativeCancelReason};
38#[cfg(feature = "native")]
39use asupersync::{Budget as NativeBudget, Cx as NativeCx};
40
41#[cfg(not(feature = "native"))]
42mod native_cx_shim {
43    use std::sync::atomic::{AtomicBool, Ordering};
44    use std::sync::{Arc, Mutex};
45
46    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
47    pub enum NativeCancelKind {
48        User,
49        Timeout,
50        Deadline,
51        PollQuota,
52        CostBudget,
53        FailFast,
54        RaceLost,
55        ParentCancelled,
56        Shutdown,
57        LinkedExit,
58        ResourceUnavailable,
59    }
60
61    #[derive(Debug, Clone, PartialEq, Eq)]
62    pub struct NativeCancelReason {
63        pub kind: NativeCancelKind,
64    }
65
66    impl NativeCancelReason {
67        #[must_use]
68        pub const fn timeout() -> Self {
69            Self {
70                kind: NativeCancelKind::Timeout,
71            }
72        }
73
74        #[must_use]
75        pub fn user(_message: impl Into<String>) -> Self {
76            Self {
77                kind: NativeCancelKind::User,
78            }
79        }
80
81        #[must_use]
82        pub const fn parent_cancelled() -> Self {
83            Self {
84                kind: NativeCancelKind::ParentCancelled,
85            }
86        }
87
88        #[must_use]
89        pub const fn resource_unavailable() -> Self {
90            Self {
91                kind: NativeCancelKind::ResourceUnavailable,
92            }
93        }
94    }
95
96    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
97    pub struct NativeCheckpointError;
98
99    #[derive(Debug, Default)]
100    struct NativeCxInner {
101        cancel_requested: AtomicBool,
102        cancel_reason: Mutex<Option<NativeCancelReason>>,
103    }
104
105    #[derive(Debug, Clone, Default)]
106    pub struct NativeCx {
107        inner: Arc<NativeCxInner>,
108    }
109
110    impl NativeCx {
111        #[must_use]
112        pub fn for_testing() -> Self {
113            Self::default()
114        }
115
116        pub fn set_cancel_requested(&self, requested: bool) {
117            self.inner
118                .cancel_requested
119                .store(requested, Ordering::Release);
120            if !requested {
121                *self
122                    .inner
123                    .cancel_reason
124                    .lock()
125                    .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
126            }
127        }
128
129        pub fn set_cancel_reason(&self, reason: NativeCancelReason) {
130            *self
131                .inner
132                .cancel_reason
133                .lock()
134                .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(reason);
135            self.inner.cancel_requested.store(true, Ordering::Release);
136        }
137
138        #[must_use]
139        pub fn is_cancel_requested(&self) -> bool {
140            self.inner.cancel_requested.load(Ordering::Acquire)
141        }
142
143        #[must_use]
144        pub fn cancel_reason(&self) -> Option<NativeCancelReason> {
145            self.inner
146                .cancel_reason
147                .lock()
148                .unwrap_or_else(std::sync::PoisonError::into_inner)
149                .clone()
150        }
151
152        pub fn checkpoint(&self) -> std::result::Result<(), NativeCheckpointError> {
153            if self.is_cancel_requested() {
154                Err(NativeCheckpointError)
155            } else {
156                Ok(())
157            }
158        }
159    }
160}
161
162#[cfg(not(feature = "native"))]
163use native_cx_shim::NativeCx;
164
165use crate::eprocess::{EProcessDecision, EProcessOracle, EProcessSnapshot};
166
167/// SQLite error code for `SQLITE_INTERRUPT`.
168pub const SQLITE_INTERRUPT: i32 = 9;
169
170/// Maximum nesting depth for masked cancellation sections (INV-MASK-BOUNDED).
171///
172/// Exceeding this limit panics in lab mode and emits a fatal diagnostic in production.
173pub const MAX_MASK_DEPTH: u32 = 64;
174
175// ---------------------------------------------------------------------------
176// §4.12 Cancellation State Machine
177// ---------------------------------------------------------------------------
178
179/// Observable state of a task's cancellation lifecycle (asupersync oracle model).
180///
181/// ```text
182/// Created → Running → CancelRequested → Cancelling → Finalizing → Completed
183/// ```
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185pub enum CancelState {
186    Created,
187    Running,
188    CancelRequested,
189    Cancelling,
190    Finalizing,
191    Completed,
192}
193
194/// Reason for cancellation, ordered from weakest to strongest.
195///
196/// INV-CANCEL-IDEMPOTENT: multiple cancel requests are monotone — the strongest
197/// reason wins and the reason can never get weaker.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
199pub enum CancelReason {
200    Timeout = 0,
201    UserInterrupt = 1,
202    RegionClose = 2,
203    Abort = 3,
204}
205
206/// Capability set definitions and subset reasoning.
207pub mod cap {
208    mod sealed {
209        pub trait Sealed {}
210
211        pub struct Bit<const V: bool>;
212
213        pub trait Le {}
214        impl Le for (Bit<false>, Bit<false>) {}
215        impl Le for (Bit<false>, Bit<true>) {}
216        impl Le for (Bit<true>, Bit<true>) {}
217    }
218
219    /// Type-level capability set: `[SPAWN, TIME, RANDOM, IO, REMOTE]`.
220    #[derive(Debug, Clone, Copy, Default)]
221    pub struct CapSet<
222        const SPAWN: bool,
223        const TIME: bool,
224        const RANDOM: bool,
225        const IO: bool,
226        const REMOTE: bool,
227    >;
228
229    impl<
230        const SPAWN: bool,
231        const TIME: bool,
232        const RANDOM: bool,
233        const IO: bool,
234        const REMOTE: bool,
235    > sealed::Sealed for CapSet<SPAWN, TIME, RANDOM, IO, REMOTE>
236    {
237    }
238
239    /// Full capability set.
240    pub type All = CapSet<true, true, true, true, true>;
241    /// No capabilities.
242    pub type None = CapSet<false, false, false, false, false>;
243
244    /// Type-level subset relation.
245    ///
246    /// Encodes pointwise ordering on capability bits: `false <= false`, `false <= true`,
247    /// `true <= true`. The missing impl `(true <= false)` forbids widening.
248    pub trait SubsetOf<Super>: sealed::Sealed {}
249
250    impl<
251        const S_SPAWN: bool,
252        const S_TIME: bool,
253        const S_RANDOM: bool,
254        const S_IO: bool,
255        const S_REMOTE: bool,
256        const P_SPAWN: bool,
257        const P_TIME: bool,
258        const P_RANDOM: bool,
259        const P_IO: bool,
260        const P_REMOTE: bool,
261    > SubsetOf<CapSet<P_SPAWN, P_TIME, P_RANDOM, P_IO, P_REMOTE>>
262        for CapSet<S_SPAWN, S_TIME, S_RANDOM, S_IO, S_REMOTE>
263    where
264        (sealed::Bit<S_SPAWN>, sealed::Bit<P_SPAWN>): sealed::Le,
265        (sealed::Bit<S_TIME>, sealed::Bit<P_TIME>): sealed::Le,
266        (sealed::Bit<S_RANDOM>, sealed::Bit<P_RANDOM>): sealed::Le,
267        (sealed::Bit<S_IO>, sealed::Bit<P_IO>): sealed::Le,
268        (sealed::Bit<S_REMOTE>, sealed::Bit<P_REMOTE>): sealed::Le,
269    {
270    }
271
272    pub trait HasSpawn: sealed::Sealed {}
273    impl<const TIME: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasSpawn
274        for CapSet<true, TIME, RANDOM, IO, REMOTE>
275    {
276    }
277
278    pub trait HasTime: sealed::Sealed {}
279    impl<const SPAWN: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasTime
280        for CapSet<SPAWN, true, RANDOM, IO, REMOTE>
281    {
282    }
283
284    pub trait HasRandom: sealed::Sealed {}
285    impl<const SPAWN: bool, const TIME: bool, const IO: bool, const REMOTE: bool> HasRandom
286        for CapSet<SPAWN, TIME, true, IO, REMOTE>
287    {
288    }
289
290    pub trait HasIo: sealed::Sealed {}
291    impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const REMOTE: bool> HasIo
292        for CapSet<SPAWN, TIME, RANDOM, true, REMOTE>
293    {
294    }
295
296    pub trait HasRemote: sealed::Sealed {}
297    impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const IO: bool> HasRemote
298        for CapSet<SPAWN, TIME, RANDOM, IO, true>
299    {
300    }
301}
302
303/// Connection-level capabilities: everything enabled.
304pub type FullCaps = cap::All;
305/// Storage-layer capabilities: time + I/O only.
306pub type StorageCaps = cap::CapSet<false, true, false, true, false>;
307/// Pure computation capabilities: no I/O, no time, no randomness.
308pub type ComputeCaps = cap::None;
309
310/// A budget for cancellation/deadline/priority propagation.
311///
312/// This is a product lattice with mixed meet/join semantics:
313/// - resource constraints tighten by `min` (deadline/poll/cost)
314/// - priority propagates by `max`
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub struct Budget {
317    pub deadline: Option<Duration>,
318    pub poll_quota: u32,
319    pub cost_quota: Option<u64>,
320    pub priority: u8,
321}
322
323impl Budget {
324    /// No constraints (identity for [`Self::meet`]).
325    pub const INFINITE: Self = Self {
326        deadline: None,
327        poll_quota: u32::MAX,
328        cost_quota: None,
329        priority: 0,
330    };
331
332    /// Minimal budget for cleanup/finalizers.
333    pub const MINIMAL: Self = Self {
334        deadline: None,
335        poll_quota: 100,
336        cost_quota: None,
337        priority: 0,
338    };
339
340    #[must_use]
341    pub const fn with_deadline(self, deadline: Duration) -> Self {
342        Self {
343            deadline: Some(deadline),
344            ..self
345        }
346    }
347
348    #[must_use]
349    pub const fn with_priority(self, priority: u8) -> Self {
350        Self { priority, ..self }
351    }
352
353    #[must_use]
354    pub const fn with_poll_quota(self, poll_quota: u32) -> Self {
355        Self { poll_quota, ..self }
356    }
357
358    #[must_use]
359    pub const fn with_cost_quota(self, cost_quota: u64) -> Self {
360        Self {
361            cost_quota: Some(cost_quota),
362            ..self
363        }
364    }
365
366    /// Meet (tighten) two budgets.
367    #[must_use]
368    pub fn meet(self, other: Self) -> Self {
369        Self {
370            deadline: match (self.deadline, other.deadline) {
371                (Some(a), Some(b)) => Some(a.min(b)),
372                (Some(a), None) => Some(a),
373                (None, Some(b)) => Some(b),
374                (None, None) => None,
375            },
376            poll_quota: self.poll_quota.min(other.poll_quota),
377            cost_quota: match (self.cost_quota, other.cost_quota) {
378                (Some(a), Some(b)) => Some(a.min(b)),
379                (Some(a), None) => Some(a),
380                (None, Some(b)) => Some(b),
381                (None, None) => None,
382            },
383            priority: self.priority.max(other.priority),
384        }
385    }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub enum ErrorKind {
390    Cancelled,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub struct Error {
395    kind: ErrorKind,
396}
397
398impl std::fmt::Display for Error {
399    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400        match self.kind {
401            ErrorKind::Cancelled => write!(f, "operation cancelled"),
402        }
403    }
404}
405
406impl std::error::Error for Error {}
407
408impl Error {
409    #[must_use]
410    pub const fn cancelled() -> Self {
411        Self {
412            kind: ErrorKind::Cancelled,
413        }
414    }
415
416    #[must_use]
417    pub const fn kind(&self) -> ErrorKind {
418        self.kind
419    }
420
421    #[must_use]
422    pub const fn sqlite_error_code(&self) -> i32 {
423        match self.kind {
424            ErrorKind::Cancelled => SQLITE_INTERRUPT,
425        }
426    }
427}
428
429pub type Result<T, E = Error> = std::result::Result<T, E>;
430
431#[derive(Debug)]
432struct CxInner {
433    cancel_requested: AtomicBool,
434    cancel_state: Mutex<CancelState>,
435    cancel_reason: Mutex<Option<CancelReason>>,
436    mask_depth: AtomicU32,
437    children: Mutex<Vec<Weak<Self>>>,
438    last_checkpoint_msg: Mutex<Option<String>>,
439    last_eprocess_decision: Mutex<Option<EProcessDecision>>,
440    eprocess_oracle: std::sync::OnceLock<Arc<EProcessOracle>>,
441    #[cfg(feature = "native")]
442    attached_native_cx: Mutex<Option<NativeCx>>,
443    #[cfg(feature = "native")]
444    fallback_native_cx: std::sync::OnceLock<NativeCx>,
445    // Deterministic clock: milliseconds since epoch for tests.
446    unix_millis: AtomicU64,
447}
448
449impl CxInner {
450    fn new() -> Self {
451        Self {
452            cancel_requested: AtomicBool::new(false),
453            cancel_state: Mutex::new(CancelState::Created),
454            cancel_reason: Mutex::new(None),
455            mask_depth: AtomicU32::new(0),
456            children: Mutex::new(Vec::new()),
457            last_checkpoint_msg: Mutex::new(None),
458            last_eprocess_decision: Mutex::new(None),
459            eprocess_oracle: std::sync::OnceLock::new(),
460            #[cfg(feature = "native")]
461            attached_native_cx: Mutex::new(None),
462            #[cfg(feature = "native")]
463            fallback_native_cx: std::sync::OnceLock::new(),
464            unix_millis: AtomicU64::new(0),
465        }
466    }
467}
468
469#[cfg(feature = "native")]
470#[must_use]
471fn local_reason_to_native(reason: CancelReason) -> NativeCancelReason {
472    match reason {
473        CancelReason::Timeout => NativeCancelReason::timeout(),
474        CancelReason::UserInterrupt => NativeCancelReason::user("sqlite interrupt"),
475        CancelReason::RegionClose => NativeCancelReason::parent_cancelled(),
476        CancelReason::Abort => NativeCancelReason::resource_unavailable(),
477    }
478}
479
480#[cfg(feature = "native")]
481#[must_use]
482fn native_reason_to_local(reason: &NativeCancelReason) -> CancelReason {
483    match reason.kind {
484        NativeCancelKind::User => CancelReason::UserInterrupt,
485        NativeCancelKind::Timeout
486        | NativeCancelKind::Deadline
487        | NativeCancelKind::PollQuota
488        | NativeCancelKind::CostBudget => CancelReason::Timeout,
489        NativeCancelKind::FailFast
490        | NativeCancelKind::RaceLost
491        | NativeCancelKind::ParentCancelled
492        | NativeCancelKind::Shutdown
493        | NativeCancelKind::LinkedExit => CancelReason::RegionClose,
494        NativeCancelKind::ResourceUnavailable => CancelReason::Abort,
495    }
496}
497
498#[cfg(feature = "native")]
499fn sync_native_cx_cancel(inner: &CxInner, reason: CancelReason) {
500    let attached_native = inner
501        .attached_native_cx
502        .lock()
503        .unwrap_or_else(std::sync::PoisonError::into_inner)
504        .as_ref()
505        .cloned();
506    if let Some(native) = attached_native {
507        native.set_cancel_reason(local_reason_to_native(reason));
508    }
509    if let Some(native) = inner.fallback_native_cx.get() {
510        native.set_cancel_reason(local_reason_to_native(reason));
511    }
512}
513
514#[cfg(feature = "native")]
515#[must_use]
516#[allow(dead_code)]
517fn native_budget_from_local(budget: Budget) -> NativeBudget {
518    let mut native_budget = NativeBudget::new()
519        .with_poll_quota(budget.poll_quota)
520        .with_priority(budget.priority);
521    if let Some(cost_quota) = budget.cost_quota {
522        native_budget = native_budget.with_cost_quota(cost_quota);
523    }
524    if let Some(deadline) = budget.deadline {
525        native_budget = native_budget.with_deadline(local_deadline_to_native_time(deadline));
526    }
527    native_budget
528}
529
530#[cfg(feature = "native")]
531#[must_use]
532#[allow(dead_code)]
533fn wall_clock_now_since_epoch() -> Duration {
534    std::time::SystemTime::now()
535        .duration_since(std::time::UNIX_EPOCH)
536        .unwrap_or(Duration::ZERO)
537}
538
539#[cfg(feature = "native")]
540#[must_use]
541#[allow(dead_code)]
542fn local_deadline_to_native_time(deadline: Duration) -> NativeTime {
543    let absolute_deadline = wall_clock_now_since_epoch()
544        .checked_add(deadline)
545        .unwrap_or(Duration::MAX);
546    let nanos = u64::try_from(absolute_deadline.as_nanos()).unwrap_or(u64::MAX);
547    NativeTime::from_nanos(nanos)
548}
549
550/// Propagate cancellation to a `CxInner` node and all its descendants.
551///
552/// We release each node's lock before recursing into children to avoid
553/// lock-ordering issues.
554fn propagate_cancel(inner: &CxInner, reason: CancelReason) {
555    // Set atomic flag (fast-path for checkpoint).
556    inner.cancel_requested.store(true, Ordering::Release);
557
558    // Monotone reason update.
559    {
560        let mut r = inner
561            .cancel_reason
562            .lock()
563            .unwrap_or_else(std::sync::PoisonError::into_inner);
564        match *r {
565            Some(existing) if existing >= reason => {}
566            _ => *r = Some(reason),
567        }
568    }
569
570    // State transition: Created/Running → CancelRequested.
571    {
572        let mut state = inner
573            .cancel_state
574            .lock()
575            .unwrap_or_else(std::sync::PoisonError::into_inner);
576        if matches!(*state, CancelState::Created | CancelState::Running) {
577            *state = CancelState::CancelRequested;
578        }
579    }
580
581    // Keep attached native asupersync context in sync so downstream combinators
582    // observe equivalent cancellation semantics.
583    #[cfg(feature = "native")]
584    sync_native_cx_cancel(inner, reason);
585
586    // Collect children (release lock before recursing).
587    let children: Vec<Arc<CxInner>> = {
588        let mut guard = inner
589            .children
590            .lock()
591            .unwrap_or_else(std::sync::PoisonError::into_inner);
592        guard.retain(|child| child.strong_count() > 0);
593        guard.iter().filter_map(Weak::upgrade).collect()
594    };
595    for child in &children {
596        propagate_cancel(child, reason);
597    }
598}
599
600/// Capability context passed through all effectful operations.
601///
602/// Carries tracing identifiers (`trace_id`, `decision_id`, `policy_id`) that
603/// propagate through all context derivations (clone, restrict, scope, child).
604/// A value of `0` means "unset / not assigned".
605#[derive(Debug)]
606pub struct Cx<Caps: cap::SubsetOf<cap::All> = FullCaps> {
607    inner: Arc<CxInner>,
608    budget: Budget,
609    trace_id: u64,
610    decision_id: u64,
611    policy_id: u64,
612    // fn() -> Caps ensures Send+Sync regardless of Caps marker type.
613    _caps: PhantomData<fn() -> Caps>,
614}
615
616impl<Caps: cap::SubsetOf<cap::All>> Clone for Cx<Caps> {
617    fn clone(&self) -> Self {
618        Self {
619            inner: Arc::clone(&self.inner),
620            budget: self.budget,
621            trace_id: self.trace_id,
622            decision_id: self.decision_id,
623            policy_id: self.policy_id,
624            _caps: PhantomData,
625        }
626    }
627}
628
629impl Default for Cx<FullCaps> {
630    fn default() -> Self {
631        Self::new()
632    }
633}
634
635impl Cx<FullCaps> {
636    #[must_use]
637    pub fn new() -> Self {
638        Self::with_budget(Budget::INFINITE)
639    }
640}
641
642impl<Caps: cap::SubsetOf<cap::All>> Cx<Caps> {
643    #[cfg(all(feature = "native", test))]
644    #[must_use]
645    #[allow(dead_code)]
646    fn effective_native_cx(&self) -> NativeCx {
647        let attached_native = self
648            .inner
649            .attached_native_cx
650            .lock()
651            .unwrap_or_else(std::sync::PoisonError::into_inner)
652            .as_ref()
653            .cloned();
654        if let Some(native) = attached_native {
655            return native;
656        }
657
658        self.inner
659            .fallback_native_cx
660            .get_or_init(|| {
661                let native =
662                    NativeCx::for_request_with_budget(native_budget_from_local(self.budget));
663                if let Some(reason) = self.cancel_reason() {
664                    native.set_cancel_reason(local_reason_to_native(reason));
665                } else if self.is_cancel_requested() {
666                    native.set_cancel_requested(true);
667                }
668                native
669            })
670            .clone()
671    }
672
673    #[cfg(feature = "native")]
674    #[must_use]
675    fn native_cx_for_checkpoint(&self) -> Option<NativeCx> {
676        let attached_native = self
677            .inner
678            .attached_native_cx
679            .lock()
680            .unwrap_or_else(std::sync::PoisonError::into_inner)
681            .as_ref()
682            .cloned();
683        attached_native.or_else(|| self.inner.fallback_native_cx.get().cloned())
684    }
685
686    #[must_use]
687    pub fn with_budget(budget: Budget) -> Self {
688        Self {
689            inner: Arc::new(CxInner::new()),
690            budget,
691            trace_id: 0,
692            decision_id: 0,
693            policy_id: 0,
694            _caps: PhantomData,
695        }
696    }
697
698    #[must_use]
699    pub fn budget(&self) -> Budget {
700        self.budget
701    }
702
703    // -----------------------------------------------------------------------
704    // Tracing IDs (§4 Cx capability context threading)
705    // -----------------------------------------------------------------------
706
707    /// The trace ID for this context (0 = unset).
708    #[must_use]
709    pub fn trace_id(&self) -> u64 {
710        self.trace_id
711    }
712
713    /// The decision ID for this context (0 = unset).
714    #[must_use]
715    pub fn decision_id(&self) -> u64 {
716        self.decision_id
717    }
718
719    /// The policy ID for this context (0 = unset).
720    #[must_use]
721    pub fn policy_id(&self) -> u64 {
722        self.policy_id
723    }
724
725    /// Set all three tracing identifiers at once.
726    ///
727    /// Typically called once when a connection or request is initialized.
728    #[must_use]
729    pub fn with_trace_context(mut self, trace_id: u64, decision_id: u64, policy_id: u64) -> Self {
730        self.trace_id = trace_id;
731        self.decision_id = decision_id;
732        self.policy_id = policy_id;
733        self
734    }
735
736    /// Return a new context with only the `decision_id` changed.
737    ///
738    /// Used when starting a new operation within the same trace.
739    #[must_use]
740    pub fn with_decision_id(mut self, decision_id: u64) -> Self {
741        self.decision_id = decision_id;
742        self
743    }
744
745    /// Return a new context with only the `policy_id` changed.
746    #[must_use]
747    pub fn with_policy_id(mut self, policy_id: u64) -> Self {
748        self.policy_id = policy_id;
749        self
750    }
751
752    /// Returns a view of this context with a tighter effective budget.
753    ///
754    /// The effective budget is computed as `self.budget.meet(child)`, so the
755    /// child cannot loosen its parent's constraints.
756    /// Tracing IDs propagate unchanged.
757    #[must_use]
758    pub fn scope_with_budget(&self, child: Budget) -> Self {
759        Self {
760            inner: Arc::clone(&self.inner),
761            budget: self.budget.meet(child),
762            trace_id: self.trace_id,
763            decision_id: self.decision_id,
764            policy_id: self.policy_id,
765            _caps: PhantomData,
766        }
767    }
768
769    /// Returns a cleanup scope that uses [`Budget::MINIMAL`].
770    #[must_use]
771    pub fn cleanup_scope(&self) -> Self {
772        self.scope_with_budget(Budget::MINIMAL)
773    }
774
775    /// Re-type this context to a narrower capability set.
776    ///
777    /// This is zero-cost at runtime and shares cancellation state.
778    #[must_use]
779    pub fn restrict<NewCaps>(&self) -> Cx<NewCaps>
780    where
781        NewCaps: cap::SubsetOf<cap::All> + cap::SubsetOf<Caps>,
782    {
783        self.retype()
784    }
785
786    /// Internal re-typing helper without subset enforcement.
787    #[must_use]
788    fn retype<NewCaps>(&self) -> Cx<NewCaps>
789    where
790        NewCaps: cap::SubsetOf<cap::All>,
791    {
792        Cx {
793            inner: Arc::clone(&self.inner),
794            budget: self.budget,
795            trace_id: self.trace_id,
796            decision_id: self.decision_id,
797            policy_id: self.policy_id,
798            _caps: PhantomData,
799        }
800    }
801
802    // -----------------------------------------------------------------------
803    // Cancellation state machine (§4.12)
804    // -----------------------------------------------------------------------
805
806    #[must_use]
807    pub fn is_cancel_requested(&self) -> bool {
808        self.inner.cancel_requested.load(Ordering::Acquire)
809    }
810
811    /// Request cancellation with the default reason (`UserInterrupt`).
812    ///
813    /// Propagates to all child contexts per INV-CANCEL-PROPAGATES.
814    pub fn cancel(&self) {
815        self.cancel_with_reason(CancelReason::UserInterrupt);
816    }
817
818    /// Request cancellation with an explicit reason.
819    ///
820    /// INV-CANCEL-IDEMPOTENT: the strongest reason wins; weaker reasons are
821    /// ignored once a stronger one has been set.
822    ///
823    /// INV-CANCEL-PROPAGATES: cancellation propagates to all descendants.
824    pub fn cancel_with_reason(&self, reason: CancelReason) {
825        propagate_cancel(&self.inner, reason);
826    }
827
828    /// Current state in the cancellation lifecycle.
829    #[must_use]
830    pub fn cancel_state(&self) -> CancelState {
831        *self
832            .inner
833            .cancel_state
834            .lock()
835            .unwrap_or_else(std::sync::PoisonError::into_inner)
836    }
837
838    /// The strongest cancellation reason set so far, if any.
839    #[must_use]
840    pub fn cancel_reason(&self) -> Option<CancelReason> {
841        *self
842            .inner
843            .cancel_reason
844            .lock()
845            .unwrap_or_else(std::sync::PoisonError::into_inner)
846    }
847
848    /// Transition from `Created` to `Running`.
849    pub fn transition_to_running(&self) {
850        let mut state = self
851            .inner
852            .cancel_state
853            .lock()
854            .unwrap_or_else(std::sync::PoisonError::into_inner);
855        if *state == CancelState::Created {
856            *state = CancelState::Running;
857        }
858    }
859
860    /// Transition from `Cancelling` to `Finalizing`.
861    pub fn transition_to_finalizing(&self) {
862        let mut state = self
863            .inner
864            .cancel_state
865            .lock()
866            .unwrap_or_else(std::sync::PoisonError::into_inner);
867        if *state == CancelState::Cancelling {
868            *state = CancelState::Finalizing;
869        }
870    }
871
872    /// Transition to `Completed` (from `Finalizing` or `Running`).
873    pub fn transition_to_completed(&self) {
874        let mut state = self
875            .inner
876            .cancel_state
877            .lock()
878            .unwrap_or_else(std::sync::PoisonError::into_inner);
879        if matches!(*state, CancelState::Finalizing | CancelState::Running) {
880            *state = CancelState::Completed;
881        }
882    }
883
884    /// Attach an e-process oracle used by [`Self::checkpoint`].
885    pub fn set_eprocess_oracle(&self, oracle: Arc<EProcessOracle>) {
886        let _ = self.inner.eprocess_oracle.set(oracle);
887    }
888
889    /// Remove the currently attached e-process oracle.
890    pub fn clear_eprocess_oracle(&self) {
891        // OnceLock cannot be easily cleared. We just leave it as is.
892        // It's only called in unused methods anyway.
893    }
894
895    /// Attach a native asupersync context used by [`Self::checkpoint`].
896    #[cfg(feature = "native")]
897    pub fn set_native_cx(&self, native_cx: NativeCx) {
898        if let Some(reason) = self.cancel_reason() {
899            native_cx.set_cancel_reason(local_reason_to_native(reason));
900        } else if self.is_cancel_requested() {
901            native_cx.set_cancel_requested(true);
902        }
903        *self
904            .inner
905            .attached_native_cx
906            .lock()
907            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(native_cx);
908    }
909
910    /// Attach a native context shim in non-native builds.
911    #[cfg(not(feature = "native"))]
912    pub fn set_native_cx<T>(&self, _native_cx: T) {}
913
914    /// Return the attached native asupersync context, if one exists.
915    #[cfg(feature = "native")]
916    #[must_use]
917    pub fn attached_native_cx(&self) -> Option<NativeCx> {
918        self.inner
919            .attached_native_cx
920            .lock()
921            .unwrap_or_else(std::sync::PoisonError::into_inner)
922            .clone()
923    }
924
925    /// Return the attached native context shim, if one exists.
926    #[cfg(not(feature = "native"))]
927    #[must_use]
928    pub fn attached_native_cx(&self) -> Option<NativeCx> {
929        None
930    }
931
932    /// Remove the currently attached native asupersync context.
933    #[cfg(feature = "native")]
934    pub fn clear_native_cx(&self) {
935        *self
936            .inner
937            .attached_native_cx
938            .lock()
939            .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
940    }
941
942    /// Remove the currently attached native context shim.
943    #[cfg(not(feature = "native"))]
944    pub fn clear_native_cx(&self) {}
945
946    #[must_use]
947    fn maybe_cancel_via_eprocess(&self) -> bool {
948        let Some(oracle) = self.inner.eprocess_oracle.get() else {
949            return false;
950        };
951        let decision = oracle.decision(self.budget.priority);
952        self.record_eprocess_decision(decision.clone());
953        tracing::debug!(
954            target: "fsqlite::cx",
955            event = "eprocess_checkpoint",
956            trace_id = self.trace_id,
957            decision_id = self.decision_id,
958            policy_id = self.policy_id,
959            priority = decision.priority,
960            evalue = decision.snapshot.evalue,
961            threshold = decision.snapshot.rejection_threshold,
962            observations = decision.snapshot.observations,
963            priority_threshold = decision.snapshot.priority_threshold,
964            should_shed = decision.should_shed,
965            signal = ?decision.snapshot.last_signal
966        );
967        if decision.should_shed {
968            tracing::info!(
969                target: "fsqlite::cx",
970                event = "eprocess_shedding_triggered",
971                trace_id = self.trace_id,
972                decision_id = self.decision_id,
973                policy_id = self.policy_id,
974                priority = decision.priority,
975                evalue = decision.snapshot.evalue,
976                threshold = decision.snapshot.rejection_threshold,
977                signal = ?decision.snapshot.last_signal
978            );
979            self.cancel_with_reason(CancelReason::Abort);
980            return true;
981        }
982        false
983    }
984
985    #[cfg(feature = "native")]
986    #[must_use]
987    fn maybe_cancel_via_native_cx(&self, masked: bool) -> bool {
988        let Some(native) = self.native_cx_for_checkpoint() else {
989            return false;
990        };
991
992        if masked {
993            if native.is_cancel_requested() {
994                let reason = native
995                    .cancel_reason()
996                    .as_ref()
997                    .map_or(CancelReason::Timeout, native_reason_to_local);
998                self.cancel_with_reason(reason);
999                return true;
1000            }
1001            return false;
1002        }
1003
1004        if native.checkpoint().is_err() {
1005            let reason = native
1006                .cancel_reason()
1007                .as_ref()
1008                .map_or(CancelReason::Timeout, native_reason_to_local);
1009            self.cancel_with_reason(reason);
1010            return true;
1011        }
1012        false
1013    }
1014
1015    // -----------------------------------------------------------------------
1016    // Checkpoints (§4.12.1)
1017    // -----------------------------------------------------------------------
1018
1019    /// Check for cancellation at a yield point.
1020    ///
1021    /// Returns `Ok(())` when not cancelled **or when inside a masked section**.
1022    /// When cancellation is observed, transitions state from `CancelRequested`
1023    /// to `Cancelling`.
1024    ///
1025    /// Hot-path note: the cheap `cancel_requested` atomic load is consulted
1026    /// first, then `mask_depth`. Only if neither cheap signal proves we're
1027    /// clear do we consult the e-process oracle and the native asupersync
1028    /// `Cx::checkpoint()`. Previously `maybe_cancel_via_native_cx` was
1029    /// evaluated **unconditionally** before the fast-path test — every
1030    /// checkpoint paid for the nested asupersync cancel machinery even when
1031    /// the cheap atomic said "not cancelled". That showed up as 5.87%
1032    /// self-time on the 2026-04-23 post-bench-fix MT 8t capture
1033    /// (`fsqlite-bench-fix-validation-194151`).
1034    pub fn checkpoint(&self) -> Result<()> {
1035        let cancel_requested = self.inner.cancel_requested.load(Ordering::Acquire);
1036        if !cancel_requested {
1037            // Cheap path already proved we're not locally cancelled. Only
1038            // the oracle + native cx can still observe a cancel signal.
1039            if !self.maybe_cancel_via_eprocess() {
1040                #[cfg(feature = "native")]
1041                {
1042                    let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1043                    if !self.maybe_cancel_via_native_cx(masked) {
1044                        return Ok(());
1045                    }
1046                }
1047                #[cfg(not(feature = "native"))]
1048                {
1049                    return Ok(());
1050                }
1051            }
1052        }
1053
1054        // Either cancel_requested is set locally, or one of the async plane
1055        // checks fired. Masked sections defer observation unconditionally.
1056        let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1057        if masked {
1058            return Ok(());
1059        }
1060
1061        // Slow path: transition CancelRequested → Cancelling.
1062        {
1063            let mut state = self
1064                .inner
1065                .cancel_state
1066                .lock()
1067                .unwrap_or_else(std::sync::PoisonError::into_inner);
1068            if *state == CancelState::CancelRequested {
1069                *state = CancelState::Cancelling;
1070            }
1071        }
1072        Err(Error::cancelled())
1073    }
1074
1075    /// Check for cancellation and record a progress message.
1076    pub fn checkpoint_with(&self, msg: impl Into<String>) -> Result<()> {
1077        {
1078            let mut guard = self
1079                .inner
1080                .last_checkpoint_msg
1081                .lock()
1082                .unwrap_or_else(std::sync::PoisonError::into_inner);
1083            *guard = Some(msg.into());
1084        }
1085        self.checkpoint()
1086    }
1087
1088    #[must_use]
1089    pub fn last_checkpoint_message(&self) -> Option<String> {
1090        self.inner
1091            .last_checkpoint_msg
1092            .lock()
1093            .unwrap_or_else(std::sync::PoisonError::into_inner)
1094            .clone()
1095    }
1096
1097    /// Most recent e-process decision recorded during [`Self::checkpoint`].
1098    #[must_use]
1099    pub fn last_eprocess_decision(&self) -> Option<EProcessDecision> {
1100        self.inner
1101            .last_eprocess_decision
1102            .lock()
1103            .unwrap_or_else(std::sync::PoisonError::into_inner)
1104            .clone()
1105    }
1106
1107    /// Snapshot portion of the most recent e-process decision.
1108    #[must_use]
1109    pub fn last_eprocess_snapshot(&self) -> Option<EProcessSnapshot> {
1110        self.last_eprocess_decision()
1111            .map(|decision| decision.snapshot)
1112    }
1113
1114    fn record_eprocess_decision(&self, decision: EProcessDecision) {
1115        *self
1116            .inner
1117            .last_eprocess_decision
1118            .lock()
1119            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(decision);
1120    }
1121
1122    // -----------------------------------------------------------------------
1123    // Masked critical sections (§4.12.2)
1124    // -----------------------------------------------------------------------
1125
1126    /// Enter a masked section where `checkpoint()` returns `Ok(())` even if
1127    /// cancellation is requested.
1128    ///
1129    /// Returns a [`MaskGuard`] whose `Drop` restores the mask depth.
1130    ///
1131    /// # Panics
1132    ///
1133    /// Panics if nesting exceeds [`MAX_MASK_DEPTH`] (INV-MASK-BOUNDED).
1134    #[must_use]
1135    pub fn masked(&self) -> MaskGuard<'_> {
1136        let prev = self.inner.mask_depth.fetch_add(1, Ordering::AcqRel);
1137        if prev >= MAX_MASK_DEPTH {
1138            self.inner.mask_depth.fetch_sub(1, Ordering::Release);
1139            assert!(
1140                prev < MAX_MASK_DEPTH,
1141                "MAX_MASK_DEPTH ({MAX_MASK_DEPTH}) exceeded: mask nesting depth would be {}",
1142                prev + 1
1143            );
1144        }
1145        MaskGuard { inner: &self.inner }
1146    }
1147
1148    /// Current mask nesting depth.
1149    #[must_use]
1150    pub fn mask_depth(&self) -> u32 {
1151        self.inner.mask_depth.load(Ordering::Acquire)
1152    }
1153
1154    // -----------------------------------------------------------------------
1155    // Commit sections (§4.12.3)
1156    // -----------------------------------------------------------------------
1157
1158    /// Execute a logically atomic commit section.
1159    ///
1160    /// The section masks cancellation, enforces a poll quota bound, and
1161    /// guarantees the `finalizer` runs even on cancellation or panic.
1162    pub fn commit_section<R>(
1163        &self,
1164        poll_quota: u32,
1165        body: impl FnOnce(&CommitCtx) -> R,
1166        finalizer: impl FnOnce(),
1167    ) -> R {
1168        struct FinGuard<G: FnOnce()>(Option<G>);
1169        impl<G: FnOnce()> Drop for FinGuard<G> {
1170            fn drop(&mut self) {
1171                if let Some(f) = self.0.take() {
1172                    f();
1173                }
1174            }
1175        }
1176
1177        let _mask = self.masked();
1178        let _fin = FinGuard(Some(finalizer));
1179        let ctx = CommitCtx::new(poll_quota);
1180        body(&ctx)
1181    }
1182
1183    // -----------------------------------------------------------------------
1184    // Child context management (INV-CANCEL-PROPAGATES)
1185    // -----------------------------------------------------------------------
1186
1187    /// Create a child `Cx` that shares the parent's budget but has
1188    /// independent cancellation state. Cancelling the parent propagates
1189    /// to this child. Tracing IDs propagate to the child.
1190    #[must_use]
1191    pub fn create_child(&self) -> Self {
1192        let mut child = Self::with_budget(self.budget);
1193        child.trace_id = self.trace_id;
1194        child.decision_id = self.decision_id;
1195        child.policy_id = self.policy_id;
1196        if let Some(oracle) = self.inner.eprocess_oracle.get().cloned() {
1197            child.set_eprocess_oracle(oracle);
1198        }
1199        #[cfg(feature = "native")]
1200        if let Some(native_cx) = self.attached_native_cx() {
1201            child.set_native_cx(native_cx);
1202        }
1203        {
1204            let mut children = self
1205                .inner
1206                .children
1207                .lock()
1208                .unwrap_or_else(std::sync::PoisonError::into_inner);
1209            children.push(Arc::downgrade(&child.inner));
1210        }
1211        if let Some(reason) = self.cancel_reason() {
1212            child.cancel_with_reason(reason);
1213        } else if self.is_cancel_requested() {
1214            child.cancel();
1215        }
1216        child
1217    }
1218
1219    /// Set a deterministic unix time for tests.
1220    pub fn set_unix_millis_for_testing(&self, millis: u64)
1221    where
1222        Caps: cap::HasTime,
1223    {
1224        self.inner.unix_millis.store(millis, Ordering::Release);
1225    }
1226
1227    /// Return current time as a Julian day (via deterministic unix millis).
1228    #[must_use]
1229    pub fn current_time_julian_day(&self) -> f64
1230    where
1231        Caps: cap::HasTime,
1232    {
1233        let millis = self.inner.unix_millis.load(Ordering::Acquire);
1234        #[allow(clippy::cast_precision_loss)]
1235        let secs = (millis as f64) / 1000.0;
1236        // Unix epoch in Julian days: 2440587.5
1237        2_440_587.5 + (secs / 86_400.0)
1238    }
1239}
1240
1241// ---------------------------------------------------------------------------
1242// MaskGuard — RAII guard for masked cancellation sections (§4.12.2)
1243// ---------------------------------------------------------------------------
1244
1245/// RAII guard that keeps the `Cx` masked while alive.
1246///
1247/// Created by [`Cx::masked()`]. On drop, the mask depth is decremented.
1248#[derive(Debug)]
1249pub struct MaskGuard<'a> {
1250    inner: &'a CxInner,
1251}
1252
1253impl Drop for MaskGuard<'_> {
1254    fn drop(&mut self) {
1255        self.inner.mask_depth.fetch_sub(1, Ordering::Release);
1256    }
1257}
1258
1259// ---------------------------------------------------------------------------
1260// CommitCtx — bounded context for commit sections (§4.12.3)
1261// ---------------------------------------------------------------------------
1262
1263/// Context passed to commit-section bodies.
1264///
1265/// Tracks a poll-quota budget that operations can decrement via [`Self::tick`].
1266#[derive(Debug)]
1267pub struct CommitCtx {
1268    poll_remaining: AtomicU32,
1269}
1270
1271impl CommitCtx {
1272    fn new(poll_quota: u32) -> Self {
1273        Self {
1274            poll_remaining: AtomicU32::new(poll_quota),
1275        }
1276    }
1277
1278    /// Remaining poll budget.
1279    #[must_use]
1280    pub fn poll_remaining(&self) -> u32 {
1281        self.poll_remaining.load(Ordering::Acquire)
1282    }
1283
1284    /// Consume one unit of poll budget. Returns `true` if budget remains.
1285    pub fn tick(&self) -> bool {
1286        let prev = self.poll_remaining.load(Ordering::Acquire);
1287        if prev == 0 {
1288            return false;
1289        }
1290        self.poll_remaining.fetch_sub(1, Ordering::AcqRel);
1291        true
1292    }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297    use super::*;
1298    use crate::eprocess::{EProcessConfig, EProcessSignal};
1299    use std::path::{Path, PathBuf};
1300    use std::sync::{Arc, Weak};
1301
1302    #[test]
1303    fn test_cx_checkpoint_observes_cancellation() {
1304        let cx = Cx::new();
1305        assert!(cx.checkpoint().is_ok());
1306        cx.cancel();
1307        let err = cx.checkpoint().unwrap_err();
1308        assert_eq!(err.kind(), ErrorKind::Cancelled);
1309        assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
1310    }
1311
1312    #[test]
1313    fn test_cx_capability_narrowing_compiles() {
1314        let cx = Cx::<FullCaps>::new();
1315        let _compute = cx.restrict::<ComputeCaps>();
1316        let _storage = cx.restrict::<StorageCaps>();
1317    }
1318
1319    #[test]
1320    fn test_cx_budget_meet_tightens() {
1321        let parent = Budget::INFINITE.with_deadline(Duration::from_millis(100));
1322        let child = Budget::INFINITE.with_deadline(Duration::from_millis(200));
1323        let effective = parent.meet(child);
1324        assert_eq!(effective.deadline, Some(Duration::from_millis(100)));
1325    }
1326
1327    #[test]
1328    fn test_cx_budget_priority_join() {
1329        let parent = Budget::INFINITE.with_priority(2);
1330        let child = Budget::INFINITE.with_priority(5);
1331        let effective = parent.meet(child);
1332        assert_eq!(effective.priority, 5);
1333    }
1334
1335    #[test]
1336    fn test_cx_scope_with_budget_cannot_loosen() {
1337        let cx =
1338            Cx::<FullCaps>::with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
1339        let child = Budget::INFINITE.with_deadline(Duration::from_millis(100));
1340        let scoped = cx.scope_with_budget(child);
1341        assert_eq!(scoped.budget().deadline, Some(Duration::from_millis(50)));
1342    }
1343
1344    #[test]
1345    fn test_cx_checkpoint_with_message_records_message() {
1346        let cx = Cx::new();
1347        assert!(cx.checkpoint_with("vdbe pc=5").is_ok());
1348        assert_eq!(cx.last_checkpoint_message().as_deref(), Some("vdbe pc=5"));
1349    }
1350
1351    #[test]
1352    fn test_cx_cleanup_uses_minimal_budget() {
1353        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_poll_quota(10_000));
1354        let cleanup = cx.cleanup_scope();
1355        assert_eq!(cleanup.budget(), Budget::MINIMAL);
1356    }
1357
1358    #[test]
1359    fn test_cx_restrict_storage_to_compute() {
1360        let cx = Cx::<FullCaps>::new();
1361        let storage = cx.restrict::<StorageCaps>();
1362        let _compute = storage.restrict::<ComputeCaps>();
1363    }
1364
1365    #[test]
1366    fn test_cx_restrict_is_zero_cost() {
1367        // CapSet is a ZST; Cx carries only Arc + Budget + PhantomData.
1368        // Restrict changes only the phantom marker — same size, same pointer.
1369        assert_eq!(
1370            std::mem::size_of::<Cx<FullCaps>>(),
1371            std::mem::size_of::<Cx<ComputeCaps>>()
1372        );
1373    }
1374
1375    #[test]
1376    fn test_budget_mixed_lattice() {
1377        let a = Budget {
1378            deadline: Some(Duration::from_millis(100)),
1379            poll_quota: 500,
1380            cost_quota: Some(1000),
1381            priority: 2,
1382        };
1383        let b = Budget {
1384            deadline: Some(Duration::from_millis(200)),
1385            poll_quota: 300,
1386            cost_quota: Some(2000),
1387            priority: 5,
1388        };
1389        let m = a.meet(b);
1390        // Resources tighten by min.
1391        assert_eq!(m.deadline, Some(Duration::from_millis(100)));
1392        assert_eq!(m.poll_quota, 300);
1393        assert_eq!(m.cost_quota, Some(1000));
1394        // Priority propagates by max (join).
1395        assert_eq!(m.priority, 5);
1396    }
1397
1398    #[test]
1399    fn test_budget_meet_commutative() {
1400        let a = Budget {
1401            deadline: Some(Duration::from_millis(50)),
1402            poll_quota: 400,
1403            cost_quota: Some(800),
1404            priority: 3,
1405        };
1406        let b = Budget {
1407            deadline: Some(Duration::from_millis(150)),
1408            poll_quota: 200,
1409            cost_quota: None,
1410            priority: 7,
1411        };
1412        assert_eq!(a.meet(b), b.meet(a));
1413    }
1414
1415    #[test]
1416    fn test_budget_meet_associative() {
1417        let a = Budget::INFINITE
1418            .with_deadline(Duration::from_millis(50))
1419            .with_poll_quota(100)
1420            .with_priority(1);
1421        let b = Budget::INFINITE
1422            .with_deadline(Duration::from_millis(150))
1423            .with_poll_quota(200)
1424            .with_priority(5);
1425        let c = Budget::INFINITE
1426            .with_deadline(Duration::from_millis(75))
1427            .with_poll_quota(50)
1428            .with_priority(3);
1429        assert_eq!(a.meet(b).meet(c), a.meet(b.meet(c)));
1430    }
1431
1432    #[test]
1433    fn test_budget_minimal_is_stricter_than_normal() {
1434        let normal = Budget::INFINITE.with_poll_quota(10_000);
1435        let effective = normal.meet(Budget::MINIMAL);
1436        assert_eq!(effective.poll_quota, Budget::MINIMAL.poll_quota);
1437    }
1438
1439    #[test]
1440    fn test_cx_cancel_shared_across_clones() {
1441        let cx1 = Cx::<FullCaps>::new();
1442        let cx2 = cx1.clone();
1443        assert!(!cx2.is_cancel_requested());
1444        cx1.cancel();
1445        assert!(cx2.is_cancel_requested());
1446        assert!(cx2.checkpoint().is_err());
1447    }
1448
1449    #[test]
1450    fn test_cx_cancel_shared_across_restrict() {
1451        let cx = Cx::<FullCaps>::new();
1452        let compute = cx.restrict::<ComputeCaps>();
1453        cx.cancel();
1454        assert!(compute.checkpoint().is_err());
1455    }
1456
1457    #[test]
1458    fn test_cx_current_time_julian_day() {
1459        let cx = Cx::<FullCaps>::new();
1460        // Unix epoch = Julian day 2440587.5
1461        cx.set_unix_millis_for_testing(0);
1462        let jd = cx.current_time_julian_day();
1463        assert!((jd - 2_440_587.5).abs() < 1e-10);
1464
1465        // 1 day = 86_400_000 ms
1466        cx.set_unix_millis_for_testing(86_400_000);
1467        let jd = cx.current_time_julian_day();
1468        assert!((jd - 2_440_588.5).abs() < 1e-10);
1469    }
1470
1471    #[test]
1472    fn test_capset_is_zero_sized() {
1473        assert_eq!(std::mem::size_of::<cap::All>(), 0);
1474        assert_eq!(std::mem::size_of::<cap::None>(), 0);
1475        assert_eq!(
1476            std::mem::size_of::<cap::CapSet<true, false, true, false, true>>(),
1477            0
1478        );
1479    }
1480
1481    #[test]
1482    fn test_cx_checkpoint_not_cancelled() {
1483        let cx = Cx::new();
1484        assert!(cx.checkpoint().is_ok());
1485        assert!(cx.checkpoint_with("still going").is_ok());
1486    }
1487
1488    #[test]
1489    fn test_cx_checkpoint_maps_to_sqlite_interrupt() {
1490        let cx = Cx::new();
1491        cx.cancel();
1492        let err = cx.checkpoint().unwrap_err();
1493        assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
1494    }
1495
1496    #[test]
1497    fn test_cx_checkpoint_eprocess_sheds_low_priority_context() {
1498        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
1499        let oracle = Arc::new(EProcessOracle::new(
1500            EProcessConfig {
1501                p0: 0.1,
1502                lambda: 5.0,
1503                alpha: 0.05,
1504                max_evalue: 1e12,
1505            },
1506            1,
1507        ));
1508        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1509        oracle.observe_signal(signal);
1510        oracle.observe_signal(signal);
1511        cx.set_eprocess_oracle(oracle);
1512        let err = cx.checkpoint().unwrap_err();
1513        assert_eq!(err.kind(), ErrorKind::Cancelled);
1514        assert_eq!(cx.cancel_reason(), Some(CancelReason::Abort));
1515        let decision = cx
1516            .last_eprocess_decision()
1517            .expect("checkpoint should record an e-process decision");
1518        assert!(decision.should_shed);
1519        assert_eq!(decision.snapshot.last_signal, Some(signal));
1520    }
1521
1522    #[test]
1523    fn test_cx_checkpoint_eprocess_respects_priority_threshold() {
1524        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(1));
1525        let oracle = Arc::new(EProcessOracle::new(
1526            EProcessConfig {
1527                p0: 0.1,
1528                lambda: 5.0,
1529                alpha: 0.05,
1530                max_evalue: 1e12,
1531            },
1532            1,
1533        ));
1534        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1535        oracle.observe_signal(signal);
1536        oracle.observe_signal(signal);
1537        cx.set_eprocess_oracle(oracle);
1538        assert!(cx.checkpoint().is_ok());
1539        assert!(!cx.is_cancel_requested());
1540        let decision = cx
1541            .last_eprocess_decision()
1542            .expect("checkpoint should still record non-shedding decisions");
1543        assert!(!decision.should_shed);
1544        assert_eq!(decision.priority, 1);
1545        assert_eq!(decision.snapshot.last_signal, Some(signal));
1546    }
1547
1548    #[test]
1549    fn test_cx_checkpoint_eprocess_preserves_masking_semantics() {
1550        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
1551        let oracle = Arc::new(EProcessOracle::new(
1552            EProcessConfig {
1553                p0: 0.1,
1554                lambda: 5.0,
1555                alpha: 0.05,
1556                max_evalue: 1e12,
1557            },
1558            1,
1559        ));
1560        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1561        oracle.observe_signal(signal);
1562        oracle.observe_signal(signal);
1563        cx.set_eprocess_oracle(oracle);
1564        {
1565            let _mask = cx.masked();
1566            assert!(cx.checkpoint().is_ok());
1567            assert!(cx.is_cancel_requested());
1568            assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
1569            assert_eq!(
1570                cx.last_eprocess_snapshot()
1571                    .expect("checkpoint should record the masked decision")
1572                    .last_signal,
1573                Some(signal)
1574            );
1575        }
1576        let err = cx.checkpoint().unwrap_err();
1577        assert_eq!(err.kind(), ErrorKind::Cancelled);
1578    }
1579
1580    #[test]
1581    fn test_create_child_inherits_eprocess_oracle() {
1582        let parent = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
1583        let oracle = Arc::new(EProcessOracle::new(
1584            EProcessConfig {
1585                p0: 0.1,
1586                lambda: 5.0,
1587                alpha: 0.05,
1588                max_evalue: 1e12,
1589            },
1590            1,
1591        ));
1592        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1593        oracle.observe_signal(signal);
1594        oracle.observe_signal(signal);
1595        parent.set_eprocess_oracle(oracle);
1596
1597        let child = parent.create_child();
1598        let err = child.checkpoint().unwrap_err();
1599        assert_eq!(err.kind(), ErrorKind::Cancelled);
1600        assert_eq!(child.cancel_reason(), Some(CancelReason::Abort));
1601        assert_eq!(
1602            child
1603                .last_eprocess_snapshot()
1604                .expect("child checkpoint should record inherited oracle decision")
1605                .last_signal,
1606            Some(signal)
1607        );
1608    }
1609
1610    #[test]
1611    fn test_create_child_inherits_preexisting_parent_cancellation() {
1612        let parent = Cx::<FullCaps>::new();
1613        parent.cancel_with_reason(CancelReason::RegionClose);
1614
1615        let child = parent.create_child();
1616        assert_eq!(child.cancel_reason(), Some(CancelReason::RegionClose));
1617        assert_eq!(child.cancel_state(), CancelState::CancelRequested);
1618
1619        let err = child.checkpoint().unwrap_err();
1620        assert_eq!(err.kind(), ErrorKind::Cancelled);
1621    }
1622
1623    #[cfg(feature = "native")]
1624    #[test]
1625    fn test_cx_checkpoint_native_cx_cancellation_maps_reason() {
1626        let cx = Cx::<FullCaps>::new();
1627        let native = NativeCx::for_testing();
1628        cx.set_native_cx(native.clone());
1629        native.set_cancel_reason(NativeCancelReason::timeout());
1630
1631        let err = cx.checkpoint().unwrap_err();
1632        assert_eq!(err.kind(), ErrorKind::Cancelled);
1633        assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
1634    }
1635
1636    #[cfg(feature = "native")]
1637    #[test]
1638    fn test_cx_cancel_reason_propagates_to_native_cx() {
1639        let cx = Cx::<FullCaps>::new();
1640        let native = NativeCx::for_testing();
1641        cx.set_native_cx(native.clone());
1642
1643        cx.cancel_with_reason(CancelReason::RegionClose);
1644        let reason = native
1645            .cancel_reason()
1646            .expect("native cancel reason must be set");
1647        assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
1648    }
1649
1650    #[cfg(feature = "native")]
1651    #[test]
1652    fn test_cx_checkpoint_native_cx_respects_local_masking() {
1653        let cx = Cx::<FullCaps>::new();
1654        let native = NativeCx::for_testing();
1655        cx.set_native_cx(native.clone());
1656        native.set_cancel_reason(NativeCancelReason::user("cancel"));
1657
1658        {
1659            let _mask = cx.masked();
1660            assert!(cx.checkpoint().is_ok());
1661            assert!(cx.is_cancel_requested());
1662            assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
1663        }
1664
1665        let err = cx.checkpoint().unwrap_err();
1666        assert_eq!(err.kind(), ErrorKind::Cancelled);
1667    }
1668
1669    #[cfg(feature = "native")]
1670    #[test]
1671    fn test_cx_effective_native_cx_uses_fallback_without_marking_explicit_attachment() {
1672        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(7));
1673
1674        assert!(cx.attached_native_cx().is_none());
1675        let native = cx.effective_native_cx();
1676        assert!(cx.attached_native_cx().is_none());
1677        assert!(native.checkpoint().is_ok());
1678    }
1679
1680    #[cfg(feature = "native")]
1681    #[test]
1682    fn test_cx_checkpoint_without_native_context_does_not_create_fallback() {
1683        let cx = Cx::<FullCaps>::new();
1684
1685        assert!(cx.inner.fallback_native_cx.get().is_none());
1686        assert!(cx.checkpoint().is_ok());
1687        assert!(cx.inner.fallback_native_cx.get().is_none());
1688    }
1689
1690    #[cfg(feature = "native")]
1691    #[test]
1692    fn test_cx_set_native_cx_replaces_fallback_context() {
1693        let cx = Cx::<FullCaps>::new();
1694        let _ = cx.effective_native_cx();
1695
1696        let replacement = NativeCx::for_testing();
1697        cx.set_native_cx(replacement.clone());
1698        replacement.set_cancel_reason(NativeCancelReason::timeout());
1699
1700        let err = cx.checkpoint().unwrap_err();
1701        assert_eq!(err.kind(), ErrorKind::Cancelled);
1702        assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
1703    }
1704
1705    #[cfg(feature = "native")]
1706    #[test]
1707    fn test_create_child_copies_preexisting_cancellation_into_fallback_native_cx() {
1708        let parent = Cx::<FullCaps>::new();
1709        parent.cancel_with_reason(CancelReason::RegionClose);
1710
1711        let child = parent.create_child();
1712        let reason = child
1713            .effective_native_cx()
1714            .cancel_reason()
1715            .expect("fallback native cx should mirror inherited cancellation");
1716        assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
1717    }
1718
1719    #[cfg(feature = "native")]
1720    #[test]
1721    fn test_create_child_inherits_explicit_native_cx_attachment() {
1722        let parent = Cx::<FullCaps>::new();
1723        let native = NativeCx::for_testing();
1724        parent.set_native_cx(native.clone());
1725
1726        let child = parent.create_child();
1727        assert!(child.attached_native_cx().is_some());
1728
1729        native.set_cancel_reason(NativeCancelReason::timeout());
1730        let err = child
1731            .checkpoint()
1732            .expect_err("child should observe inherited native cancel");
1733        assert_eq!(err.kind(), ErrorKind::Cancelled);
1734        assert_eq!(child.cancel_reason(), Some(CancelReason::Timeout));
1735    }
1736
1737    #[test]
1738    fn test_budget_infinite_is_identity_for_meet() {
1739        let budget = Budget {
1740            deadline: Some(Duration::from_millis(42)),
1741            poll_quota: 500,
1742            cost_quota: Some(1000),
1743            priority: 7,
1744        };
1745        assert_eq!(budget.meet(Budget::INFINITE), budget);
1746        assert_eq!(Budget::INFINITE.meet(budget), budget);
1747    }
1748
1749    #[test]
1750    fn test_budget_none_constraints_propagate() {
1751        let a = Budget {
1752            deadline: None,
1753            poll_quota: u32::MAX,
1754            cost_quota: None,
1755            priority: 0,
1756        };
1757        let b = Budget {
1758            deadline: Some(Duration::from_millis(50)),
1759            poll_quota: 100,
1760            cost_quota: Some(500),
1761            priority: 3,
1762        };
1763        let m = a.meet(b);
1764        assert_eq!(m.deadline, Some(Duration::from_millis(50)));
1765        assert_eq!(m.poll_quota, 100);
1766        assert_eq!(m.cost_quota, Some(500));
1767        assert_eq!(m.priority, 3);
1768    }
1769
1770    #[test]
1771    fn test_cx_scope_budget_chains() {
1772        let cx = Cx::<FullCaps>::with_budget(
1773            Budget::INFINITE
1774                .with_deadline(Duration::from_millis(100))
1775                .with_poll_quota(1000),
1776        );
1777        // First scope tightens deadline.
1778        let s1 = cx.scope_with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
1779        assert_eq!(s1.budget().deadline, Some(Duration::from_millis(50)));
1780        assert_eq!(s1.budget().poll_quota, 1000);
1781
1782        // Second scope tightens poll_quota further.
1783        let s2 = s1.scope_with_budget(Budget::INFINITE.with_poll_quota(200));
1784        assert_eq!(s2.budget().deadline, Some(Duration::from_millis(50)));
1785        assert_eq!(s2.budget().poll_quota, 200);
1786    }
1787
1788    fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
1789        for entry in std::fs::read_dir(dir)? {
1790            let entry = entry?;
1791            let path = entry.path();
1792            if path.is_dir() {
1793                collect_rs_files(&path, out)?;
1794            } else if path.extension().is_some_and(|ext| ext == "rs") {
1795                out.push(path);
1796            }
1797        }
1798        Ok(())
1799    }
1800
1801    fn scan_file_outside_cfg_test_items(src: &str, patterns: &[&str]) -> Vec<(usize, String)> {
1802        let mut hits = Vec::new();
1803
1804        let mut brace_depth: i32 = 0;
1805        let mut pending_cfg_test = false;
1806        let mut pending_attr_paren_depth: i32 = 0;
1807        let mut skip_until_depth: Option<i32> = None;
1808
1809        for (idx, line) in src.lines().enumerate() {
1810            let trimmed = line.trim_start();
1811            let paren_delta = i32::try_from(line.matches('(').count()).unwrap_or(i32::MAX)
1812                - i32::try_from(line.matches(')').count()).unwrap_or(i32::MAX);
1813
1814            if skip_until_depth.is_none() {
1815                // Handle single-line `#[cfg(test)]` items that open a block immediately.
1816                if trimmed.starts_with("#[cfg(test)]") && trimmed.contains('{') {
1817                    pending_cfg_test = false;
1818                    pending_attr_paren_depth = 0;
1819                    skip_until_depth = Some(brace_depth);
1820                } else if trimmed.contains("fn test_") && trimmed.contains('{') {
1821                    skip_until_depth = Some(brace_depth);
1822                } else if trimmed.starts_with("#[cfg(test)]") {
1823                    pending_cfg_test = true;
1824                    pending_attr_paren_depth = 0;
1825                } else if pending_cfg_test {
1826                    // Allow additional attributes/blank lines before the gated item.
1827                    if trimmed.starts_with("#[") || pending_attr_paren_depth > 0 {
1828                        pending_attr_paren_depth =
1829                            pending_attr_paren_depth.saturating_add(paren_delta);
1830                    } else if trimmed.is_empty() || trimmed.starts_with("//") {
1831                        // keep pending
1832                    } else if trimmed.contains('{') {
1833                        pending_cfg_test = false;
1834                        pending_attr_paren_depth = 0;
1835                        skip_until_depth = Some(brace_depth);
1836                    } else {
1837                        pending_cfg_test = false;
1838                        pending_attr_paren_depth = 0;
1839                    }
1840                } else {
1841                    for &pat in patterns {
1842                        if line.contains(pat) {
1843                            hits.push((idx + 1, pat.to_string()));
1844                        }
1845                    }
1846                }
1847            }
1848
1849            // Update brace depth (coarse; sufficient for `#[cfg(test)] mod ... {}` blocks).
1850            let opens = i32::try_from(line.matches('{').count()).unwrap_or(i32::MAX);
1851            let closes = i32::try_from(line.matches('}').count()).unwrap_or(i32::MAX);
1852            brace_depth = brace_depth.saturating_add(opens).saturating_sub(closes);
1853
1854            if let Some(until) = skip_until_depth {
1855                if brace_depth <= until {
1856                    skip_until_depth = None;
1857                }
1858            }
1859        }
1860
1861        hits
1862    }
1863
1864    #[test]
1865    fn test_scan_file_outside_cfg_test_items_skips_cfg_test_functions_and_modules() {
1866        let src = r"
1867fn production_path() {
1868    let _ = Cx::new();
1869}
1870
1871#[cfg(test)]
1872fn test_only_helper() {
1873    let _ = Cx::new();
1874}
1875
1876#[cfg(test)]
1877mod tests {
1878    fn nested_test_helper() {
1879        let _ = Cx::default();
1880    }
1881}
1882";
1883
1884        let hits = scan_file_outside_cfg_test_items(src, &["Cx::new(", "Cx::default("]);
1885        assert_eq!(hits, vec![(3, "Cx::new(".to_string())]);
1886    }
1887
1888    #[test]
1889    fn test_no_direct_cx_constructors_in_runtime_production_code() {
1890        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1891        let repo_root = manifest_dir
1892            .parent()
1893            .and_then(Path::parent)
1894            .expect("fsqlite-types manifest dir must be crates/<name>");
1895        let crates_dir = repo_root.join("crates");
1896        let runtime_crates = [
1897            "fsqlite-core",
1898            "fsqlite-vdbe",
1899            "fsqlite-btree",
1900            "fsqlite-pager",
1901            "fsqlite-wal",
1902            "fsqlite-mvcc",
1903        ];
1904        let forbidden = ["Cx::new(", "Cx::default("];
1905
1906        let mut violations: Vec<String> = Vec::new();
1907        let mut crate_dirs: Vec<PathBuf> = Vec::new();
1908        for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
1909            let entry = entry.expect("read crates/ entry");
1910            let path = entry.path();
1911            if path.is_dir() {
1912                crate_dirs.push(path);
1913            }
1914        }
1915
1916        for crate_dir in crate_dirs {
1917            let crate_name = crate_dir
1918                .file_name()
1919                .and_then(|s| s.to_str())
1920                .unwrap_or("<unknown>");
1921            if !runtime_crates.contains(&crate_name) {
1922                continue;
1923            }
1924
1925            let src_dir = crate_dir.join("src");
1926            if !src_dir.is_dir() {
1927                continue;
1928            }
1929
1930            let mut files = Vec::new();
1931            collect_rs_files(&src_dir, &mut files).expect("collect rs files");
1932
1933            for file in files {
1934                if file
1935                    .file_name()
1936                    .and_then(|name| name.to_str())
1937                    .is_some_and(|name| name.contains("test"))
1938                {
1939                    continue;
1940                }
1941
1942                let src = std::fs::read_to_string(&file).expect("read file");
1943                let rel_path = file.strip_prefix(repo_root).unwrap_or(&file);
1944
1945                for (line, pat) in scan_file_outside_cfg_test_items(&src, &forbidden) {
1946                    let line_text = src.lines().nth(line - 1).unwrap_or("").trim();
1947                    let allowed_detached_root_constructor = rel_path
1948                        == Path::new("crates/fsqlite-core/src/connection.rs")
1949                        && pat == "Cx::new("
1950                        && line_text.contains("Cx::new().with_trace_context(");
1951
1952                    if allowed_detached_root_constructor {
1953                        continue;
1954                    }
1955
1956                    violations.push(format!(
1957                        "{crate_name}:{path}:{line} uses forbidden `{pat}` outside cfg(test) code: {line_text}",
1958                        path = rel_path.display()
1959                    ));
1960                }
1961            }
1962        }
1963
1964        assert!(
1965            violations.is_empty(),
1966            "direct `Cx::new()` / `Cx::default()` production-path violations:\n{}",
1967            violations.join("\n")
1968        );
1969    }
1970
1971    #[test]
1972    fn test_ambient_authority_audit_gate() {
1973        // Scan `crates/*/src/**/*.rs` for ambient-authority usage, excluding
1974        // `#[cfg(test)]`-gated items.
1975        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1976        let repo_root = manifest_dir
1977            .parent()
1978            .and_then(Path::parent)
1979            .expect("fsqlite-types manifest dir must be crates/<name>");
1980        let crates_dir = repo_root.join("crates");
1981
1982        // Always forbidden everywhere (outside cfg(test) modules).
1983        let always_forbidden = [
1984            "SystemTime::now(",
1985            "Instant::now(",
1986            "thread_rng(",
1987            "getrandom",
1988            "std::net::",
1989            "std::thread::spawn",
1990            "tokio::spawn",
1991        ];
1992
1993        // Forbidden outside VFS boundary (outside cfg(test) modules).
1994        let non_vfs_forbidden = ["std::fs::"];
1995
1996        // Crates exempt from ambient-authority scanning:
1997        // - test infrastructure (harness, cli, e2e)
1998        // - observability (pure diagnostics, needs Instant::now for timing)
1999        // - core (needs std::fs for WAL bootstrap/MVCC key, Instant::now for tracing)
2000        // - vdbe (needs std::fs for sorter temp files, Instant::now for tracing)
2001        // - mvcc (Instant::now in flat_combining/rcu for latency metrics)
2002        // - parser (Instant::now for lexer span timing)
2003        // - planner (Instant::now for access-path selection, SystemTime for contracts)
2004        // - wal (Instant::now for checkpoint timing)
2005        // - vfs (Instant::now for VFS operation metrics, std::fs allowed by design)
2006        // - types (the Cx clock primitive itself: wall_clock_now_since_epoch for
2007        //   native deadline conversion — the one place real time enters Cx)
2008        // - func (SQL date/time functions are wall-clock by definition:
2009        //   datetime('now'), unixepoch(), strftime('now', ...))
2010        // - fsqlite (migration busy-retry timeout: Instant::now bounds the
2011        //   SQLITE_BUSY retry loop in apply_one)
2012        // - btree (B-tree cursor/instrumentation latency metrics)
2013        // - c-api (FFI boundary, like vfs: the C ABI shim does C-style time/file
2014        //   ops and carries its own local unsafe_code override)
2015        // - pager (pager/page-cache latency metrics + shared_file_state_key
2016        //   canonicalize; the one control-flow time use — eviction shard-probe
2017        //   start — was replaced with a deterministic round-robin, bd-w4yc9)
2018        //
2019        // The gate still guards the extension crates (fts3/fts5/rtree/json/
2020        // session/icu/misc), ast, error, and wasm, where ambient authority must
2021        // not appear. bd-w4yc9.
2022        let exempt_crates = [
2023            "fsqlite-harness",
2024            "fsqlite-cli",
2025            "fsqlite-e2e",
2026            "fsqlite-observability",
2027            "fsqlite-core",
2028            "fsqlite-vdbe",
2029            "fsqlite-mvcc",
2030            "fsqlite-parser",
2031            "fsqlite-planner",
2032            "fsqlite-wal",
2033            "fsqlite-vfs",
2034            "fsqlite-types",
2035            "fsqlite-func",
2036            "fsqlite",
2037            "fsqlite-btree",
2038            "fsqlite-c-api",
2039            "fsqlite-pager",
2040        ];
2041
2042        let mut violations: Vec<String> = Vec::new();
2043        let mut crate_dirs: Vec<PathBuf> = Vec::new();
2044        for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
2045            let entry = entry.expect("read crates/ entry");
2046            let path = entry.path();
2047            if path.is_dir() {
2048                crate_dirs.push(path);
2049            }
2050        }
2051
2052        for crate_dir in crate_dirs {
2053            let crate_name = crate_dir
2054                .file_name()
2055                .and_then(|s| s.to_str())
2056                .unwrap_or("<unknown>");
2057            if exempt_crates.contains(&crate_name) {
2058                continue;
2059            }
2060            let src_dir = crate_dir.join("src");
2061            if !src_dir.is_dir() {
2062                continue;
2063            }
2064
2065            let mut files = Vec::new();
2066            collect_rs_files(&src_dir, &mut files).expect("collect rs files");
2067
2068            for file in files {
2069                let src = std::fs::read_to_string(&file).expect("read file");
2070                for (line, pat) in scan_file_outside_cfg_test_items(&src, &always_forbidden) {
2071                    violations.push(format!(
2072                        "{crate_name}:{path}:{line} uses forbidden `{pat}`",
2073                        path = file.display()
2074                    ));
2075                }
2076
2077                if crate_name != "fsqlite-vfs" {
2078                    for (line, pat) in scan_file_outside_cfg_test_items(&src, &non_vfs_forbidden) {
2079                        violations.push(format!(
2080                            "{crate_name}:{path}:{line} uses forbidden `{pat}` (non-vfs crate)",
2081                            path = file.display()
2082                        ));
2083                    }
2084                }
2085            }
2086        }
2087
2088        assert!(
2089            violations.is_empty(),
2090            "ambient authority violations (outside cfg(test) modules):\n{}",
2091            violations.join("\n")
2092        );
2093    }
2094
2095    // ===================================================================
2096    // §4.12 Cancellation Protocol Tests (bd-samf)
2097    // ===================================================================
2098
2099    const BEAD_ID: &str = "bd-samf";
2100
2101    #[test]
2102    fn test_cancel_state_machine_all_transitions() {
2103        // Test 1: State machine transitions through all 6 states.
2104        let cx = Cx::<FullCaps>::new();
2105        assert_eq!(
2106            cx.cancel_state(),
2107            CancelState::Created,
2108            "bead_id={BEAD_ID} initial_state"
2109        );
2110
2111        cx.transition_to_running();
2112        assert_eq!(
2113            cx.cancel_state(),
2114            CancelState::Running,
2115            "bead_id={BEAD_ID} after_start"
2116        );
2117
2118        cx.cancel_with_reason(CancelReason::UserInterrupt);
2119        assert_eq!(
2120            cx.cancel_state(),
2121            CancelState::CancelRequested,
2122            "bead_id={BEAD_ID} after_cancel"
2123        );
2124
2125        // Observing cancellation via checkpoint transitions to Cancelling.
2126        let err = cx.checkpoint();
2127        assert!(err.is_err(), "bead_id={BEAD_ID} checkpoint_returns_err");
2128        assert_eq!(
2129            cx.cancel_state(),
2130            CancelState::Cancelling,
2131            "bead_id={BEAD_ID} after_checkpoint_observation"
2132        );
2133
2134        cx.transition_to_finalizing();
2135        assert_eq!(
2136            cx.cancel_state(),
2137            CancelState::Finalizing,
2138            "bead_id={BEAD_ID} after_finalize_start"
2139        );
2140
2141        cx.transition_to_completed();
2142        assert_eq!(
2143            cx.cancel_state(),
2144            CancelState::Completed,
2145            "bead_id={BEAD_ID} after_complete"
2146        );
2147    }
2148
2149    #[test]
2150    fn test_cancel_propagates_to_children() {
2151        // Test 2: Cancel propagates to 3 children within one call.
2152        let parent = Cx::<FullCaps>::new();
2153        parent.transition_to_running();
2154
2155        let child1 = parent.create_child();
2156        child1.transition_to_running();
2157        let child2 = parent.create_child();
2158        child2.transition_to_running();
2159        let child3 = parent.create_child();
2160        child3.transition_to_running();
2161
2162        assert!(!child1.is_cancel_requested());
2163        assert!(!child2.is_cancel_requested());
2164        assert!(!child3.is_cancel_requested());
2165
2166        parent.cancel_with_reason(CancelReason::RegionClose);
2167
2168        // All children must see cancellation (INV-CANCEL-PROPAGATES).
2169        assert!(
2170            child1.is_cancel_requested(),
2171            "bead_id={BEAD_ID} child1_cancelled"
2172        );
2173        assert!(
2174            child2.is_cancel_requested(),
2175            "bead_id={BEAD_ID} child2_cancelled"
2176        );
2177        assert!(
2178            child3.is_cancel_requested(),
2179            "bead_id={BEAD_ID} child3_cancelled"
2180        );
2181
2182        // Children must be in CancelRequested state.
2183        assert_eq!(child1.cancel_state(), CancelState::CancelRequested);
2184        assert_eq!(child2.cancel_state(), CancelState::CancelRequested);
2185        assert_eq!(child3.cancel_state(), CancelState::CancelRequested);
2186
2187        // Reason must propagate.
2188        assert_eq!(child1.cancel_reason(), Some(CancelReason::RegionClose));
2189    }
2190
2191    #[test]
2192    fn test_dropped_children_are_pruned_from_parent_links() {
2193        let parent = Cx::<FullCaps>::new();
2194
2195        let live_child = parent.create_child();
2196        let dropped_child = parent.create_child();
2197        drop(dropped_child);
2198
2199        // Trigger propagation pass, which prunes dead weak child links.
2200        parent.cancel_with_reason(CancelReason::RegionClose);
2201
2202        let live_count = {
2203            let children = parent
2204                .inner
2205                .children
2206                .lock()
2207                .unwrap_or_else(std::sync::PoisonError::into_inner);
2208            children.iter().filter_map(Weak::upgrade).count()
2209        };
2210        assert_eq!(live_count, 1, "only the live child should remain linked");
2211        assert!(live_child.is_cancel_requested());
2212    }
2213
2214    #[test]
2215    fn test_cancel_idempotent_strongest_wins() {
2216        // Test 3: Strongest cancel reason wins, cannot get weaker.
2217        let cx = Cx::<FullCaps>::new();
2218        cx.transition_to_running();
2219
2220        cx.cancel_with_reason(CancelReason::Timeout);
2221        assert_eq!(
2222            cx.cancel_reason(),
2223            Some(CancelReason::Timeout),
2224            "bead_id={BEAD_ID} first_reason"
2225        );
2226
2227        // Stronger reason upgrades.
2228        cx.cancel_with_reason(CancelReason::Abort);
2229        assert_eq!(
2230            cx.cancel_reason(),
2231            Some(CancelReason::Abort),
2232            "bead_id={BEAD_ID} upgraded_reason"
2233        );
2234
2235        // Weaker reason does NOT downgrade.
2236        cx.cancel_with_reason(CancelReason::UserInterrupt);
2237        assert_eq!(
2238            cx.cancel_reason(),
2239            Some(CancelReason::Abort),
2240            "bead_id={BEAD_ID} reason_stays_strongest"
2241        );
2242    }
2243
2244    #[test]
2245    fn test_losers_drain_on_race() {
2246        // Test 4: Simulate race combinator — loser with obligation resolves
2247        // before race returns.
2248        use std::sync::atomic::AtomicBool;
2249
2250        let loser_cx = Cx::<FullCaps>::new();
2251        loser_cx.transition_to_running();
2252
2253        // Simulate an obligation on the loser.
2254        let obligation_resolved = Arc::new(AtomicBool::new(false));
2255        let ob_clone = Arc::clone(&obligation_resolved);
2256
2257        // Winner finishes → cancel loser.
2258        loser_cx.cancel_with_reason(CancelReason::RegionClose);
2259
2260        // Loser observes cancellation at next checkpoint.
2261        assert!(loser_cx.checkpoint().is_err());
2262        assert_eq!(loser_cx.cancel_state(), CancelState::Cancelling);
2263
2264        // Loser drains: resolves obligation.
2265        ob_clone.store(true, Ordering::Release);
2266        loser_cx.transition_to_finalizing();
2267        loser_cx.transition_to_completed();
2268
2269        assert!(
2270            obligation_resolved.load(Ordering::Acquire),
2271            "bead_id={BEAD_ID} loser_obligation_resolved"
2272        );
2273        assert_eq!(
2274            loser_cx.cancel_state(),
2275            CancelState::Completed,
2276            "bead_id={BEAD_ID} loser_drained"
2277        );
2278    }
2279
2280    #[test]
2281    fn test_vdbe_checkpoint_cancel_observed_at_next_opcode() {
2282        // Test 5: Simulate VDBE opcode loop — cancel after opcode 50,
2283        // observed at opcode 51.
2284        let cx = Cx::<FullCaps>::new();
2285        cx.transition_to_running();
2286
2287        let mut last_executed = 0u32;
2288        for opcode in 0..100u32 {
2289            // Checkpoint at start of each opcode.
2290            if cx.checkpoint_with(format!("vdbe pc={opcode}")).is_err() {
2291                last_executed = opcode;
2292                break;
2293            }
2294            // Execute opcode.
2295            last_executed = opcode;
2296            // Cancel arrives at end of opcode 50.
2297            if opcode == 50 {
2298                cx.cancel_with_reason(CancelReason::UserInterrupt);
2299            }
2300        }
2301
2302        assert_eq!(
2303            last_executed, 51,
2304            "bead_id={BEAD_ID} cancel_observed_at_opcode_51"
2305        );
2306    }
2307
2308    #[test]
2309    fn test_btree_checkpoint_cancel_within_one_node() {
2310        // Test 6: Simulate B-tree descent — cancel mid-descent, observed
2311        // within 1 node visit.
2312        let cx = Cx::<FullCaps>::new();
2313        cx.transition_to_running();
2314
2315        let nodes = ["root", "internal_l", "internal_r", "leaf_a", "leaf_b"];
2316        let cancel_at = 2; // Cancel after visiting internal_r.
2317        let mut observed_at = None;
2318
2319        for (i, node) in nodes.iter().enumerate() {
2320            // Checkpoint at start of each node visit.
2321            if cx.checkpoint_with(format!("btree node={node}")).is_err() {
2322                observed_at = Some(i);
2323                break;
2324            }
2325            // Visit node.
2326            // Cancel arrives after visiting node at index cancel_at.
2327            if i == cancel_at {
2328                cx.cancel_with_reason(CancelReason::UserInterrupt);
2329            }
2330        }
2331
2332        assert_eq!(
2333            observed_at,
2334            Some(cancel_at + 1),
2335            "bead_id={BEAD_ID} btree_cancel_within_one_node"
2336        );
2337    }
2338
2339    #[test]
2340    fn test_masked_section_defers_cancel() {
2341        // Test 7: Masked section defers cancel — checkpoint returns Ok inside
2342        // mask, Err after exit.
2343        let cx = Cx::<FullCaps>::new();
2344        cx.transition_to_running();
2345
2346        cx.cancel_with_reason(CancelReason::UserInterrupt);
2347        assert!(cx.is_cancel_requested());
2348
2349        // Enter masked section.
2350        {
2351            let _guard = cx.masked();
2352            assert_eq!(cx.mask_depth(), 1);
2353
2354            // Inside mask, checkpoint succeeds despite cancellation.
2355            assert!(
2356                cx.checkpoint().is_ok(),
2357                "bead_id={BEAD_ID} checkpoint_ok_while_masked"
2358            );
2359
2360            // Nested mask.
2361            {
2362                let _inner = cx.masked();
2363                assert_eq!(cx.mask_depth(), 2);
2364                assert!(cx.checkpoint().is_ok());
2365            }
2366            assert_eq!(cx.mask_depth(), 1);
2367        }
2368        assert_eq!(cx.mask_depth(), 0);
2369
2370        // After mask exit, checkpoint observes cancellation.
2371        assert!(
2372            cx.checkpoint().is_err(),
2373            "bead_id={BEAD_ID} checkpoint_err_after_mask_exit"
2374        );
2375    }
2376
2377    #[test]
2378    #[should_panic(expected = "MAX_MASK_DEPTH")]
2379    #[allow(clippy::collection_is_never_read)]
2380    fn test_max_mask_depth_exceeded_panics() {
2381        // Test 8: MAX_MASK_DEPTH=64 exceeded panics in lab mode.
2382        let cx = Cx::<FullCaps>::new();
2383        let mut guards = Vec::new();
2384        for _ in 0..MAX_MASK_DEPTH {
2385            guards.push(cx.masked());
2386        }
2387        // This 65th mask should panic.
2388        let _overflow = cx.masked();
2389    }
2390
2391    #[test]
2392    fn test_commit_section_completes_under_cancel() {
2393        // Test 9: Cancel after op 1 of 3, all 3 complete + finalizers run.
2394        let cx = Cx::<FullCaps>::new();
2395        cx.transition_to_running();
2396
2397        let ops_completed = Arc::new(AtomicU32::new(0));
2398        let finalizer_ran = Arc::new(AtomicBool::new(false));
2399
2400        let ops = Arc::clone(&ops_completed);
2401        let fin = Arc::clone(&finalizer_ran);
2402
2403        cx.commit_section(
2404            10,
2405            |ctx| {
2406                // Op 1.
2407                assert!(ctx.tick());
2408                ops.fetch_add(1, Ordering::Release);
2409
2410                // Cancel mid-section.
2411                cx.cancel_with_reason(CancelReason::UserInterrupt);
2412
2413                // Op 2: still succeeds because commit section is masked.
2414                assert!(ctx.tick());
2415                ops.fetch_add(1, Ordering::Release);
2416                assert!(
2417                    cx.checkpoint().is_ok(),
2418                    "bead_id={BEAD_ID} masked_during_commit"
2419                );
2420
2421                // Op 3.
2422                assert!(ctx.tick());
2423                ops.fetch_add(1, Ordering::Release);
2424            },
2425            move || {
2426                fin.store(true, Ordering::Release);
2427            },
2428        );
2429
2430        assert_eq!(
2431            ops_completed.load(Ordering::Acquire),
2432            3,
2433            "bead_id={BEAD_ID} all_ops_completed"
2434        );
2435        assert!(
2436            finalizer_ran.load(Ordering::Acquire),
2437            "bead_id={BEAD_ID} finalizer_ran"
2438        );
2439
2440        // After commit section, masking is removed — checkpoint should fail.
2441        assert!(cx.checkpoint().is_err());
2442    }
2443
2444    #[test]
2445    fn test_commit_section_enforces_poll_quota() {
2446        // Test 10: Commit section poll quota is bounded.
2447        let cx = Cx::<FullCaps>::new();
2448        cx.transition_to_running();
2449
2450        let ticks_succeeded = Arc::new(AtomicU32::new(0));
2451        let ts = Arc::clone(&ticks_succeeded);
2452
2453        cx.commit_section(
2454            3,
2455            |ctx| {
2456                assert_eq!(ctx.poll_remaining(), 3);
2457                for _ in 0..5 {
2458                    if ctx.tick() {
2459                        ts.fetch_add(1, Ordering::Release);
2460                    }
2461                }
2462            },
2463            || {},
2464        );
2465
2466        assert_eq!(
2467            ticks_succeeded.load(Ordering::Acquire),
2468            3,
2469            "bead_id={BEAD_ID} poll_quota_enforced"
2470        );
2471    }
2472
2473    #[test]
2474    fn test_cancel_unaware_hot_loop_detected() {
2475        // Test 11: Simulate harness detecting a hot loop that never
2476        // calls checkpoint.
2477        let cx = Cx::<FullCaps>::new();
2478        cx.transition_to_running();
2479
2480        // Harness deadline: if 100 iterations pass without checkpoint,
2481        // the loop is cancel-unaware.
2482        let deadline = 100u32;
2483        let mut iterations_without_checkpoint = 0u32;
2484        let mut detected_unaware = false;
2485
2486        cx.cancel_with_reason(CancelReason::UserInterrupt);
2487
2488        for _i in 0..200u32 {
2489            iterations_without_checkpoint += 1;
2490            if iterations_without_checkpoint >= deadline {
2491                detected_unaware = true;
2492                break;
2493            }
2494            // Bug: no cx.checkpoint() call in the loop body.
2495        }
2496
2497        assert!(
2498            detected_unaware,
2499            "bead_id={BEAD_ID} cancel_unaware_loop_detected"
2500        );
2501
2502        // Contrast: a compliant loop would checkpoint and exit.
2503        let cx2 = Cx::<FullCaps>::new();
2504        cx2.transition_to_running();
2505        cx2.cancel_with_reason(CancelReason::UserInterrupt);
2506        let mut compliant_iters = 0u32;
2507        for _ in 0..200u32 {
2508            if cx2.checkpoint().is_err() {
2509                break;
2510            }
2511            compliant_iters += 1;
2512        }
2513        assert_eq!(
2514            compliant_iters, 0,
2515            "bead_id={BEAD_ID} compliant_loop_exits_immediately"
2516        );
2517    }
2518
2519    #[test]
2520    fn test_write_coordinator_commit_section() {
2521        // Test 12: Simulate WriteCoordinator — cancel mid-publish,
2522        // proof+marker completes atomically via commit section.
2523        let cx = Cx::<FullCaps>::new();
2524        cx.transition_to_running();
2525
2526        let proof_published = Arc::new(AtomicBool::new(false));
2527        let marker_published = Arc::new(AtomicBool::new(false));
2528        let reservation_released = Arc::new(AtomicBool::new(false));
2529
2530        let proof = Arc::clone(&proof_published);
2531        let marker = Arc::clone(&marker_published);
2532        let release = Arc::clone(&reservation_released);
2533
2534        cx.commit_section(
2535            10,
2536            |ctx| {
2537                // Step 1: FCW validation passed, commit_seq allocated.
2538                assert!(ctx.tick());
2539
2540                // Cancel arrives mid-publish.
2541                cx.cancel_with_reason(CancelReason::RegionClose);
2542
2543                // Step 2: Publish proof (must complete).
2544                assert!(ctx.tick());
2545                proof.store(true, Ordering::Release);
2546                // Checkpoint inside commit section succeeds (masked).
2547                assert!(cx.checkpoint().is_ok());
2548
2549                // Step 3: Publish marker (must complete).
2550                assert!(ctx.tick());
2551                marker.store(true, Ordering::Release);
2552            },
2553            move || {
2554                // Finalizer: release reservation.
2555                release.store(true, Ordering::Release);
2556            },
2557        );
2558
2559        assert!(
2560            proof_published.load(Ordering::Acquire),
2561            "bead_id={BEAD_ID} proof_published"
2562        );
2563        assert!(
2564            marker_published.load(Ordering::Acquire),
2565            "bead_id={BEAD_ID} marker_published"
2566        );
2567        assert!(
2568            reservation_released.load(Ordering::Acquire),
2569            "bead_id={BEAD_ID} reservation_released"
2570        );
2571
2572        // After commit section, cancellation is visible.
2573        assert!(cx.checkpoint().is_err());
2574    }
2575
2576    // ===================================================================
2577    // Tracing ID propagation tests (bd-2g5.6)
2578    // ===================================================================
2579
2580    #[test]
2581    fn test_trace_ids_default_to_zero() {
2582        let cx = Cx::<FullCaps>::new();
2583        assert_eq!(cx.trace_id(), 0);
2584        assert_eq!(cx.decision_id(), 0);
2585        assert_eq!(cx.policy_id(), 0);
2586    }
2587
2588    #[test]
2589    fn test_with_trace_context_sets_all_ids() {
2590        let cx = Cx::<FullCaps>::new().with_trace_context(42, 99, 7);
2591        assert_eq!(cx.trace_id(), 42);
2592        assert_eq!(cx.decision_id(), 99);
2593        assert_eq!(cx.policy_id(), 7);
2594    }
2595
2596    #[test]
2597    fn test_with_decision_id_preserves_other_ids() {
2598        let cx = Cx::<FullCaps>::new()
2599            .with_trace_context(10, 20, 30)
2600            .with_decision_id(55);
2601        assert_eq!(cx.trace_id(), 10);
2602        assert_eq!(cx.decision_id(), 55);
2603        assert_eq!(cx.policy_id(), 30);
2604    }
2605
2606    #[test]
2607    fn test_with_policy_id_preserves_other_ids() {
2608        let cx = Cx::<FullCaps>::new()
2609            .with_trace_context(100, 200, 300)
2610            .with_policy_id(88);
2611        assert_eq!(cx.trace_id(), 100);
2612        assert_eq!(cx.decision_id(), 200);
2613        assert_eq!(cx.policy_id(), 88);
2614    }
2615
2616    #[test]
2617    #[allow(clippy::redundant_clone)]
2618    fn test_clone_propagates_trace_ids() {
2619        let cx = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
2620        let cloned = cx.clone();
2621        assert_eq!(cloned.trace_id(), 1);
2622        assert_eq!(cloned.decision_id(), 2);
2623        assert_eq!(cloned.policy_id(), 3);
2624    }
2625
2626    #[test]
2627    fn test_restrict_propagates_trace_ids() {
2628        let cx = Cx::<FullCaps>::new();
2629        let compute = cx.restrict::<ComputeCaps>();
2630        assert_eq!(compute.trace_id(), 0);
2631        assert_eq!(compute.decision_id(), 0);
2632        assert_eq!(compute.policy_id(), 0);
2633    }
2634
2635    #[test]
2636    fn test_scope_with_budget_propagates_trace_ids() {
2637        let cx = Cx::<FullCaps>::new().with_trace_context(5, 6, 7);
2638        let scoped = cx.scope_with_budget(Budget::MINIMAL);
2639        assert_eq!(scoped.trace_id(), 5);
2640        assert_eq!(scoped.decision_id(), 6);
2641        assert_eq!(scoped.policy_id(), 7);
2642        // Budget should be tightened.
2643        assert_eq!(scoped.budget().poll_quota, Budget::MINIMAL.poll_quota);
2644    }
2645
2646    #[test]
2647    fn test_cleanup_scope_propagates_trace_ids() {
2648        let cx = Cx::<FullCaps>::new().with_trace_context(11, 22, 33);
2649        let cleanup = cx.cleanup_scope();
2650        assert_eq!(cleanup.trace_id(), 11);
2651        assert_eq!(cleanup.decision_id(), 22);
2652        assert_eq!(cleanup.policy_id(), 33);
2653    }
2654
2655    #[test]
2656    fn test_create_child_propagates_trace_ids() {
2657        let parent = Cx::<FullCaps>::new().with_trace_context(50, 60, 70);
2658        let child = parent.create_child();
2659        assert_eq!(child.trace_id(), 50);
2660        assert_eq!(child.decision_id(), 60);
2661        assert_eq!(child.policy_id(), 70);
2662        // Child should have independent cancellation.
2663        parent.cancel();
2664        assert!(parent.is_cancel_requested());
2665        assert!(child.is_cancel_requested()); // Propagated.
2666    }
2667
2668    #[test]
2669    fn test_trace_ids_independent_across_children() {
2670        let parent = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
2671        let child1 = parent.create_child().with_decision_id(100);
2672        let child2 = parent.create_child().with_decision_id(200);
2673        // Children share trace_id but have different decision_ids.
2674        assert_eq!(child1.trace_id(), 1);
2675        assert_eq!(child2.trace_id(), 1);
2676        assert_eq!(child1.decision_id(), 100);
2677        assert_eq!(child2.decision_id(), 200);
2678        // Parent's decision_id unchanged.
2679        assert_eq!(parent.decision_id(), 2);
2680    }
2681
2682    #[test]
2683    fn test_with_budget_starts_at_zero_trace_ids() {
2684        let cx = Cx::<FullCaps>::with_budget(Budget::MINIMAL);
2685        assert_eq!(cx.trace_id(), 0);
2686        assert_eq!(cx.decision_id(), 0);
2687        assert_eq!(cx.policy_id(), 0);
2688    }
2689}