Skip to main content

projectatlas/runtime/
source_observation.rs

1//! Process-local source observation for long-lived verified index reads.
2
3use super::{
4    ScanRuntimePlan, SourceVerificationWork, WatchChangeSet,
5    open_atlas_store_read_only_for_project, open_exact_fresh_atlas_store_for_project_controlled,
6    open_exact_saved_source_matches_index_controlled, publication_input_error,
7    source_changed_during_derivation, source_inspection_error,
8    verify_saved_source_matches_index_controlled,
9};
10use crate::CliError;
11use blake3::Hasher;
12use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
13use projectatlas_core::graph::ProjectInstanceId;
14use projectatlas_core::{CanonicalProjectRoot, IndexGeneration, IndexWorkControl, IndexWorkStage};
15use projectatlas_db::{AtlasStore, CapturedProjectBinding, IndexPublicationState};
16use std::collections::HashMap;
17#[cfg(windows)]
18use std::ffi::{OsStr, OsString};
19use std::fmt;
20use std::fs;
21use std::fs::File;
22use std::hash::Hash;
23use std::io::{Read, Take};
24#[cfg(windows)]
25use std::os::windows::ffi::{OsStrExt, OsStringExt};
26use std::path::{Path, PathBuf};
27use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
28use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, TrySendError, sync_channel};
29use std::sync::{Arc, Mutex};
30use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
31
32/// Maximum independent project bindings observed by one MCP process.
33const SOURCE_OBSERVATION_CAPACITY: usize = 16;
34/// Maximum unprocessed native watcher events retained per project.
35const SOURCE_OBSERVATION_QUEUE_CAPACITY: usize = 1_024;
36/// Fixed attempts used to reconcile an edit that overlaps a query or verification.
37const VERIFIED_READ_ATTEMPTS: usize = 3;
38/// Maximum bytes read from any one external source-selection policy input.
39const MAX_POLICY_INPUT_BYTES: u64 = 16 * 1_024 * 1_024;
40
41/// Whether exact verification may repair a bounded delta before returning a read.
42#[derive(Clone, Copy)]
43enum SourceRepairPolicy {
44    /// Ordinary navigation may reconcile a safe incremental delta.
45    Incremental,
46    /// Refresh no-op admission requires an already current publication.
47    RequireCurrent,
48}
49
50/// Read admission either proves current state or transfers exact discovery to refresh.
51pub(crate) enum SourceReadAdmission<T> {
52    /// The requested snapshot/result is backed by current source evidence.
53    Current(T),
54    /// No mutation occurred; the refresh owner can consume this exact assessment.
55    Reconcile(Box<super::IndexFreshnessAssessment>),
56}
57
58impl<T> SourceReadAdmission<T> {
59    /// Preserve typed guidance for callers that do not own a refresh operation.
60    pub(crate) fn require_current(self) -> Result<T, CliError> {
61        match self {
62            Self::Current(value) => Ok(value),
63            Self::Reconcile(assessment) => Err(CliError::RefreshRequired(Box::new(
64                assessment.delta.map_or_else(
65                    || super::index_policy_refresh_required(&assessment.source.root),
66                    |delta| delta.report,
67                ),
68            ))),
69        }
70    }
71}
72
73impl SourceRepairPolicy {
74    /// Open exact source evidence with the caller's explicit mutation policy.
75    fn open_exact(
76        self,
77        binding: &SourceBinding,
78        control: &IndexWorkControl,
79    ) -> Result<SourceReadAdmission<super::ExactFreshIndexRead>, CliError> {
80        match self {
81            Self::Incremental => open_exact_fresh_atlas_store_for_project_controlled(
82                &binding.database,
83                &binding.root,
84                binding.config.as_deref(),
85                control,
86            )
87            .map(SourceReadAdmission::Current),
88            Self::RequireCurrent => {
89                let (store, _plan, assessment) = super::open_exact_index_assessment(
90                    &binding.database,
91                    &binding.root,
92                    binding.config.as_deref(),
93                    projectatlas_fs::ScanLimits::default(),
94                    control,
95                )?;
96                if assessment.delta.is_some() {
97                    store.finish_index_read_snapshot()?;
98                    Ok(SourceReadAdmission::Reconcile(Box::new(assessment)))
99                } else {
100                    Ok(SourceReadAdmission::Current(super::ExactFreshIndexRead {
101                        store,
102                        work: assessment.work,
103                    }))
104                }
105            }
106        }
107    }
108}
109
110/// Exact process-local evidence attached to one accepted `SQLite` read snapshot.
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub(crate) struct VerifiedReadStamp {
113    /// Nonce distinguishing this server process from every restart.
114    pub(crate) process_nonce: [u8; 16],
115    /// Monotonic verified epoch inside this process and source binding.
116    pub(crate) epoch: u64,
117    /// Durable complete publication generation read by the `SQLite` snapshot.
118    pub(crate) generation: IndexGeneration,
119    /// Durable project identity captured by the same root-bound snapshot.
120    pub(crate) project_instance_id: ProjectInstanceId,
121}
122
123/// Measured freshness work consumed before one result was accepted.
124#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
125pub(crate) struct VerifiedReadWork {
126    /// Exact full-source verifications performed for this accepted call.
127    pub(crate) exact_verifications: u64,
128    /// Repository entries inspected by exact verification.
129    pub(crate) filesystem_entries: u64,
130    /// Repository source bytes hashed by exact verification.
131    pub(crate) filesystem_bytes: u64,
132    /// `SQLite` read statements owned directly by freshness verification.
133    pub(crate) sqlite_read_statements: u64,
134    /// Full indexed-node rows decoded by freshness verification.
135    pub(crate) decoded_nodes: u64,
136    /// Provisional query results discarded after an overlapping invalidation.
137    pub(crate) retries: u64,
138    /// Wall time for verification, bounded query construction, and acceptance.
139    pub(crate) elapsed: Duration,
140    /// Rendered result bytes supplied by the adapter after acceptance.
141    pub(crate) output_bytes: u64,
142}
143
144impl VerifiedReadWork {
145    /// Accumulate one exact source verification into this accepted call's work.
146    fn add_exact(&mut self, work: SourceVerificationWork) {
147        self.exact_verifications = self.exact_verifications.saturating_add(1);
148        self.filesystem_entries = self
149            .filesystem_entries
150            .saturating_add(work.filesystem_entries);
151        self.filesystem_bytes = self.filesystem_bytes.saturating_add(work.filesystem_bytes);
152        self.sqlite_read_statements = self
153            .sqlite_read_statements
154            .saturating_add(work.sqlite_read_statements);
155        self.decoded_nodes = self.decoded_nodes.saturating_add(work.decoded_nodes);
156    }
157}
158
159/// Accepted owned result plus its epoch stamp and measured freshness work.
160#[derive(Clone, Debug, Eq, PartialEq)]
161pub(crate) struct VerifiedReadOutcome<T> {
162    /// Result built entirely inside the accepted `SQLite` snapshot.
163    pub(crate) value: T,
164    /// Process epoch and durable generation that accepted the result.
165    pub(crate) stamp: VerifiedReadStamp,
166    /// Work consumed before the result was accepted.
167    pub(crate) work: VerifiedReadWork,
168}
169
170impl<T> VerifiedReadOutcome<T> {
171    /// Record the already-rendered output size without rescanning or reopening `SQLite`.
172    pub(crate) fn with_output_bytes(mut self, output_bytes: usize) -> Self {
173        self.work.output_bytes = u64::try_from(output_bytes).unwrap_or(u64::MAX);
174        self
175    }
176}
177
178/// Source authority retained through one purpose mutation commit boundary.
179enum MutationSourceWitness {
180    /// Native observation plus the accepted source epoch.
181    Observed {
182        /// Exact source binding and observer continuity retained by this admission.
183        entry: Arc<SourceObservationEntry>,
184        /// Exact durable generation and policy epoch accepted before mutation.
185        epoch: VerifiedSourceEpoch,
186    },
187    /// Exact-per-call compatibility witness when no observer can be admitted.
188    Exact {
189        /// Canonical source, database, and configuration identity.
190        binding: SourceBinding,
191        /// Durable generation and project identity accepted before mutation.
192        stamp: VerifiedReadStamp,
193        /// Scanner contract accepted around exact source verification.
194        contract_fingerprint: String,
195        /// Dynamic source-selection policy accepted around exact verification.
196        policy_witness: String,
197    },
198}
199
200/// Saved-source witness retained through one purpose mutation commit boundary.
201pub(crate) struct VerifiedMutationAdmission {
202    /// Observed or exact-per-call source authority retained by this admission.
203    witness: MutationSourceWitness,
204    /// Cooperative cancellation and work budget shared through commit admission.
205    control: IndexWorkControl,
206}
207
208impl VerifiedMutationAdmission {
209    /// Revalidate exact saved source, policy, observer continuity, and cancellation before commit.
210    pub(crate) fn verify(&self) -> Result<(), CliError> {
211        match &self.witness {
212            MutationSourceWitness::Observed { entry, epoch } => {
213                Self::verify_observed(entry, epoch, &self.control, || {
214                    verify_saved_source_matches_index_controlled(
215                        &entry.binding.database,
216                        &entry.binding.root,
217                        entry.binding.config.as_deref(),
218                        &self.control,
219                    )
220                })
221            }
222            MutationSourceWitness::Exact {
223                binding,
224                stamp,
225                contract_fingerprint,
226                policy_witness,
227            } => Self::verify_exact(
228                binding,
229                stamp,
230                contract_fingerprint,
231                policy_witness,
232                &self.control,
233            ),
234        }
235    }
236
237    /// Revalidate an observer-backed mutation witness around exact saved source.
238    fn verify_observed(
239        entry: &SourceObservationEntry,
240        epoch: &VerifiedSourceEpoch,
241        control: &IndexWorkControl,
242        verify_source: impl FnOnce() -> Result<(), CliError>,
243    ) -> Result<(), CliError> {
244        if let Err(error) = Self::verify_observation(entry, epoch, control) {
245            entry.invalidate_epoch(epoch);
246            return Err(error);
247        }
248        if let Err(error) = verify_source() {
249            if matches!(error, CliError::RefreshRequired(_)) {
250                entry.invalidate_after_proven_source_mismatch()?;
251            } else {
252                entry.invalidate_epoch(epoch);
253            }
254            return Err(error);
255        }
256        if let Err(error) = Self::verify_observation(entry, epoch, control) {
257            entry.invalidate_epoch(epoch);
258            return Err(error);
259        }
260        Ok(())
261    }
262
263    /// Require the retained observer epoch to remain current.
264    fn verify_observation(
265        entry: &SourceObservationEntry,
266        epoch: &VerifiedSourceEpoch,
267        control: &IndexWorkControl,
268    ) -> Result<(), CliError> {
269        match SourceObservationRegistry::accepts_observed_result(entry, epoch, control) {
270            Ok(ObservedAcceptance::Accepted) => Ok(()),
271            Ok(ObservedAcceptance::Superseded | ObservedAcceptance::Invalidated) => {
272                Err(source_changed_during_derivation(&entry.binding.root, "."))
273            }
274            Err(error) => Err(error),
275        }
276    }
277
278    /// Revalidate exact saved source plus durable identity immediately before commit.
279    fn verify_exact(
280        binding: &SourceBinding,
281        stamp: &VerifiedReadStamp,
282        contract_fingerprint: &str,
283        policy_witness: &str,
284        control: &IndexWorkControl,
285    ) -> Result<(), CliError> {
286        let before = exact_source_policy(binding, control)?;
287        if before.0 != contract_fingerprint || before.1 != policy_witness {
288            return Err(source_changed_during_derivation(&binding.root, "."));
289        }
290        let exact = open_exact_saved_source_matches_index_controlled(
291            &binding.database,
292            &binding.root,
293            binding.config.as_deref(),
294            control,
295        )?;
296        let verification = (|| {
297            let publication = exact
298                .store
299                .index_publication()?
300                .ok_or_else(|| source_changed_during_derivation(&binding.root, "."))?;
301            let captured = exact.store.captured_project_binding()?;
302            let after = exact_source_policy(binding, control)?;
303            Ok::<_, CliError>((publication, captured, after))
304        })();
305        let finished = exact.store.finish_index_read_snapshot();
306        let (publication, captured, after) = verification?;
307        finished?;
308        if before == after
309            && after.0 == contract_fingerprint
310            && after.1 == policy_witness
311            && publication.generation == stamp.generation
312            && captured.project_instance_id == stamp.project_instance_id
313        {
314            Ok(())
315        } else {
316            Err(source_changed_during_derivation(&binding.root, "."))
317        }
318    }
319}
320
321/// Acceptance of one observed result against the shared source epoch.
322#[derive(Clone, Copy, Debug, Eq, PartialEq)]
323enum ObservedAcceptance {
324    /// The retained witness is still current.
325    Accepted,
326    /// Another reconciliation replaced the witness without invalid source evidence.
327    Superseded,
328    /// Source, policy, or observation continuity no longer supports the witness.
329    Invalidated,
330}
331
332/// Exact root/database/config identity used to isolate one observer.
333#[derive(Clone, Debug, Eq)]
334struct SourceBinding {
335    /// Canonical source root watched by this observer.
336    root: PathBuf,
337    /// Database excluded from source-change invalidation.
338    database: PathBuf,
339    /// Optional scanner configuration watched with the source root.
340    config: Option<PathBuf>,
341}
342
343impl PartialEq for SourceBinding {
344    fn eq(&self, other: &Self) -> bool {
345        self.root == other.root && self.database == other.database && self.config == other.config
346    }
347}
348
349impl Hash for SourceBinding {
350    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
351        self.root.hash(state);
352        self.database.hash(state);
353        self.config.hash(state);
354    }
355}
356
357impl SourceBinding {
358    /// Configuration directory observed separately when it lies outside the source root.
359    fn external_config_parent(&self) -> Option<&Path> {
360        self.config
361            .as_deref()
362            .filter(|config| !config.starts_with(&self.root))
363            .and_then(Path::parent)
364    }
365
366    /// Resolve one root, database, and optional configuration into a stable binding.
367    fn new(database: &Path, root: &Path, config: Option<&Path>) -> Result<Self, CliError> {
368        let root = root.canonicalize().map_err(|source| CliError::Io {
369            path: root.to_path_buf(),
370            source,
371        })?;
372        let database = absolute_path_from(&root, database);
373        let config = config.map(|path| absolute_path_from(&root, path));
374        Ok(Self {
375            root,
376            database,
377            config,
378        })
379    }
380}
381
382/// One successfully verified source epoch retained only by this process.
383#[derive(Clone, Debug, Eq, PartialEq)]
384struct VerifiedSourceEpoch {
385    /// Accepted process and durable publication identity.
386    stamp: VerifiedReadStamp,
387    /// Latest watcher ingress sequence covered by this epoch.
388    ingress_sequence: u64,
389    /// Source publication contract covered by this epoch.
390    contract_fingerprint: String,
391    /// Exact external source-selection policy covered by this epoch.
392    policy_witness: String,
393}
394
395/// Mutable verified-epoch state serialized for one source binding.
396#[derive(Debug, Default)]
397struct ObservationState {
398    /// Monotonic epoch sequence within the owning observer.
399    next_epoch: u64,
400    /// Last source epoch proven safe for warm reads.
401    verified: Option<VerifiedSourceEpoch>,
402}
403
404/// Concrete watcher and state for one exact source binding.
405struct SourceObservationEntry {
406    /// Exact source, database, and configuration identity.
407    binding: SourceBinding,
408    /// Native watcher kept alive for the entry lifetime.
409    watcher: RecommendedWatcher,
410    /// Bounded receiver whose lock serializes callback publication with drain acknowledgment.
411    receiver: Arc<Mutex<Receiver<Event>>>,
412    /// Monotonic count of relevant events accepted by the callback.
413    ingress_sequence: Arc<AtomicU64>,
414    /// Whether overflow, disconnection, or rescan invalidated event continuity.
415    continuity_lost: Arc<AtomicBool>,
416    /// Serializes exact verification and epoch installation for this binding.
417    reconcile: Mutex<()>,
418    /// Current process-local verified epoch.
419    state: Mutex<ObservationState>,
420    #[cfg(test)]
421    /// Deterministic event ingress used only by owning tests.
422    test_sender: SyncSender<Event>,
423    #[cfg(test)]
424    /// One event injected after an acceptance drain for owning race tests.
425    acceptance_event: Mutex<Option<Event>>,
426    #[cfg(test)]
427    /// Continuity loss injected during the next locked event drain for owning race tests.
428    drain_continuity_invalidations: AtomicU64,
429}
430
431#[cfg(windows)]
432impl Drop for SourceObservationEntry {
433    fn drop(&mut self) {
434        drop(self.watcher.unwatch(&self.binding.root));
435        if let Some(parent) = self.binding.external_config_parent() {
436            drop(self.watcher.unwatch(parent));
437        }
438        // notify's Windows Unwatch and Drop only enqueue actions. Configure waits
439        // for an acknowledgement after preceding cancellation/completion work,
440        // so directory cleanup cannot race an outstanding native notification.
441        drop(self.watcher.configure(notify::Config::default()));
442    }
443}
444
445impl fmt::Debug for SourceObservationEntry {
446    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
447        formatter
448            .debug_struct("SourceObservationEntry")
449            .field("binding", &self.binding)
450            .field(
451                "ingress_sequence",
452                &self.ingress_sequence.load(Ordering::Acquire),
453            )
454            .field(
455                "continuity_lost",
456                &self.continuity_lost.load(Ordering::Acquire),
457            )
458            .finish_non_exhaustive()
459    }
460}
461
462impl SourceObservationEntry {
463    /// Start a bounded native watcher for one source binding.
464    fn start(binding: SourceBinding) -> Result<Self, CliError> {
465        let (sender, receiver) = sync_channel(SOURCE_OBSERVATION_QUEUE_CAPACITY);
466        let receiver = Arc::new(Mutex::new(receiver));
467        #[cfg(test)]
468        let test_sender = sender.clone();
469        let ingress_sequence = Arc::new(AtomicU64::new(0));
470        let continuity_lost = Arc::new(AtomicBool::new(false));
471        let watcher = notify::recommended_watcher(watcher_callback(
472            sender,
473            Arc::clone(&receiver),
474            Arc::clone(&ingress_sequence),
475            Arc::clone(&continuity_lost),
476        ))
477        .map_err(|source| observer_error(&binding.root, &source))?;
478        let mut entry = Self {
479            binding,
480            watcher,
481            receiver,
482            ingress_sequence,
483            continuity_lost,
484            reconcile: Mutex::new(()),
485            state: Mutex::new(ObservationState::default()),
486            #[cfg(test)]
487            test_sender,
488            #[cfg(test)]
489            acceptance_event: Mutex::new(None),
490            #[cfg(test)]
491            drain_continuity_invalidations: AtomicU64::new(0),
492        };
493        entry
494            .watcher
495            .watch(&entry.binding.root, RecursiveMode::Recursive)
496            .map_err(|source| observer_error(&entry.binding.root, &source))?;
497        if let Some(parent) = entry.binding.external_config_parent() {
498            entry
499                .watcher
500                .watch(parent, RecursiveMode::NonRecursive)
501                .map_err(|source| observer_error(&entry.binding.root, &source))?;
502        }
503        Ok(entry)
504    }
505
506    /// Publish one deterministic test event through the production handshake.
507    #[cfg(test)]
508    fn publish_test_event(&self, event: Event) -> Result<(), CliError> {
509        let _receiver = self
510            .receiver
511            .lock()
512            .map_err(|_poisoned| lock_error(&self.binding.root, "source observation receiver"))?;
513        self.test_sender.try_send(event).map_err(|source| {
514            CliError::InvalidInput(format!(
515                "deterministic source observer event injection failed: {source}"
516            ))
517        })?;
518        self.ingress_sequence.fetch_add(1, Ordering::AcqRel);
519        Ok(())
520    }
521
522    /// Discard the current verified epoch after any uncertainty.
523    fn invalidate(&self) {
524        if let Ok(mut state) = self.state.lock() {
525            state.verified = None;
526        }
527    }
528
529    /// Discard only the failed operation's epoch without clearing a successor.
530    fn invalidate_epoch(&self, epoch: &VerifiedSourceEpoch) {
531        if let Ok(mut state) = self.state.lock()
532            && state
533                .verified
534                .as_ref()
535                .is_some_and(|current| current.stamp == epoch.stamp)
536        {
537            state.verified = None;
538        }
539    }
540
541    /// Reject all cached and in-flight evidence after exact source verification fails.
542    fn invalidate_after_proven_source_mismatch(&self) -> Result<(), CliError> {
543        let _receiver = self
544            .receiver
545            .lock()
546            .map_err(|_poisoned| lock_error(&self.binding.root, "source observation receiver"))?;
547        self.continuity_lost.store(true, Ordering::Release);
548        self.invalidate();
549        Ok(())
550    }
551
552    /// Reset continuity and drain events before sampling exact source truth.
553    fn clear_before_exact_verification(&self) -> Result<(), CliError> {
554        let receiver = self
555            .receiver
556            .lock()
557            .map_err(|_poisoned| lock_error(&self.binding.root, "source observation receiver"))?;
558        self.invalidate();
559        self.continuity_lost.store(false, Ordering::Release);
560        loop {
561            match receiver.try_recv() {
562                Ok(_event) => {}
563                Err(TryRecvError::Empty) => return Ok(()),
564                Err(TryRecvError::Disconnected) => {
565                    self.continuity_lost.store(true, Ordering::Release);
566                    return Ok(());
567                }
568            }
569        }
570    }
571
572    /// Return whether any potentially relevant event arrived since exact truth was sampled.
573    fn changed_since_exact_verification(
574        &self,
575        scan_options: &projectatlas_fs::ScanOptions,
576    ) -> Result<bool, CliError> {
577        let receiver = self
578            .receiver
579            .lock()
580            .map_err(|_poisoned| lock_error(&self.binding.root, "source observation receiver"))?;
581        self.drain_source_events(&receiver, scan_options)
582    }
583
584    /// Drain and invalidate atomically with callback publication and epoch installation.
585    fn drain_source_events(
586        &self,
587        receiver: &Receiver<Event>,
588        scan_options: &projectatlas_fs::ScanOptions,
589    ) -> Result<bool, CliError> {
590        #[cfg(test)]
591        if self
592            .drain_continuity_invalidations
593            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
594                remaining.checked_sub(1)
595            })
596            .is_ok()
597        {
598            self.continuity_lost.store(true, Ordering::Release);
599        }
600        if self.continuity_lost.load(Ordering::Acquire) {
601            self.invalidate();
602            return Ok(true);
603        }
604        let mut changes = WatchChangeSet::default();
605        loop {
606            match receiver.try_recv() {
607                Ok(event) => {
608                    let event_changes = observer_event_changes(&self.binding, scan_options, &event);
609                    changes.requires_full_scan |= event_changes.requires_full_scan;
610                    changes.paths.extend(event_changes.paths);
611                    if self
612                        .binding
613                        .config
614                        .as_ref()
615                        .is_some_and(|config| event.paths.iter().any(|path| path == config))
616                    {
617                        changes.requires_full_scan = true;
618                    }
619                }
620                Err(TryRecvError::Empty) => break,
621                Err(TryRecvError::Disconnected) => {
622                    self.continuity_lost.store(true, Ordering::Release);
623                    self.invalidate();
624                    return Ok(true);
625                }
626            }
627        }
628        let changed = changes.requires_full_scan || !changes.paths.is_empty();
629        if changed {
630            // Preserve consumed changes for an exact verification already in flight.
631            self.continuity_lost.store(true, Ordering::Release);
632            self.invalidate();
633        } else {
634            let acknowledged = self.ingress_sequence.load(Ordering::Acquire);
635            let mut state = self
636                .state
637                .lock()
638                .map_err(|_poisoned| lock_error(&self.binding.root, "source observation state"))?;
639            if let Some(epoch) = state.verified.as_mut() {
640                epoch.ingress_sequence = acknowledged;
641            }
642        }
643        Ok(changed)
644    }
645
646    /// Consume events only while the sampled epoch still owns their selection policy.
647    fn drain_epoch_events(
648        &self,
649        receiver: &Receiver<Event>,
650        epoch: &VerifiedSourceEpoch,
651        scan_options: &projectatlas_fs::ScanOptions,
652    ) -> Result<ObservedAcceptance, CliError> {
653        let Some(current) = self.current_epoch()? else {
654            return Ok(ObservedAcceptance::Invalidated);
655        };
656        if current.stamp != epoch.stamp {
657            return Ok(ObservedAcceptance::Superseded);
658        }
659        if self.drain_source_events(receiver, scan_options)? {
660            return Ok(ObservedAcceptance::Invalidated);
661        }
662        Ok(ObservedAcceptance::Accepted)
663    }
664
665    /// Install a new verified epoch after exact source and policy reconciliation.
666    fn install_epoch(
667        &self,
668        process_nonce: [u8; 16],
669        binding: &CapturedProjectBinding,
670        generation: IndexGeneration,
671        ingress_sequence: u64,
672        contract_fingerprint: String,
673        policy_witness: String,
674    ) -> Result<VerifiedSourceEpoch, CliError> {
675        let mut state = self
676            .state
677            .lock()
678            .map_err(|_poisoned| lock_error(&self.binding.root, "source observation state"))?;
679        state.next_epoch = state.next_epoch.saturating_add(1);
680        let epoch = VerifiedSourceEpoch {
681            stamp: VerifiedReadStamp {
682                process_nonce,
683                epoch: state.next_epoch,
684                generation,
685                project_instance_id: binding.project_instance_id,
686            },
687            ingress_sequence,
688            contract_fingerprint,
689            policy_witness,
690        };
691        state.verified = Some(epoch.clone());
692        Ok(epoch)
693    }
694
695    /// Clone the current verified epoch without holding the state lock.
696    fn current_epoch(&self) -> Result<Option<VerifiedSourceEpoch>, CliError> {
697        self.state
698            .lock()
699            .map(|state| state.verified.clone())
700            .map_err(|_poisoned| lock_error(&self.binding.root, "source observation state"))
701    }
702}
703
704/// Classify source events without letting this binding's own `SQLite` files self-invalidate it.
705fn observer_event_changes(
706    binding: &SourceBinding,
707    scan_options: &projectatlas_fs::ScanOptions,
708    event: &Event,
709) -> WatchChangeSet {
710    let mut filtered = event.clone();
711    let metadata_directory = binding.root.join(".projectatlas");
712    filtered.paths.retain(|path| {
713        let candidate = super::absolute_watch_path(&binding.root, path);
714        !same_native_path(&candidate, &metadata_directory)
715            && !is_database_runtime_path(&candidate, &binding.database)
716    });
717    if filtered.paths.is_empty() && !event.need_rescan() {
718        return WatchChangeSet::default();
719    }
720    super::notify_event_changes(&binding.root, scan_options, &filtered)
721}
722
723/// Return whether a path is the database or one of its runtime sidecar files.
724fn is_database_runtime_path(candidate: &Path, database: &Path) -> bool {
725    if same_native_path(candidate, database) {
726        return true;
727    }
728    ["-wal", "-shm", "-journal"].into_iter().any(|suffix| {
729        let mut sidecar = database.as_os_str().to_os_string();
730        sidecar.push(suffix);
731        let sidecar = Path::new(&sidecar);
732        same_native_path(candidate, sidecar) || {
733            #[cfg(windows)]
734            {
735                missing_windows_sidecar_matches(candidate, sidecar, database, suffix)
736            }
737            #[cfg(not(windows))]
738            {
739                false
740            }
741        }
742    })
743}
744
745/// Match a removed sidecar through its existing database leaf on ordinary
746/// case-insensitive Windows directories. The base-file proof keeps a
747/// case-sensitive directory's distinct spelling fail-closed.
748#[cfg(windows)]
749fn missing_windows_sidecar_matches(
750    candidate: &Path,
751    sidecar: &Path,
752    database: &Path,
753    suffix: &str,
754) -> bool {
755    if candidate.exists() || sidecar.exists() {
756        return false;
757    }
758    let (Some(parent), Some(leaf)) = (candidate.parent(), candidate.file_name()) else {
759        return false;
760    };
761    let suffix_units: Vec<u16> = OsStr::new(suffix).encode_wide().collect();
762    let leaf_units: Vec<u16> = leaf.encode_wide().collect();
763    if leaf_units.len() <= suffix_units.len()
764        || !leaf_units[leaf_units.len() - suffix_units.len()..]
765            .iter()
766            .zip(&suffix_units)
767            .all(|(actual, expected)| {
768                let actual = *actual;
769                let expected = *expected;
770                let fold_ascii = |unit: u16| {
771                    if (u16::from(b'A')..=u16::from(b'Z')).contains(&unit) {
772                        unit + (u16::from(b'a') - u16::from(b'A'))
773                    } else {
774                        unit
775                    }
776                };
777                actual == expected || fold_ascii(actual) == fold_ascii(expected)
778            })
779    {
780        return false;
781    }
782    let base = parent.join(OsString::from_wide(
783        &leaf_units[..leaf_units.len() - suffix_units.len()],
784    ));
785    // An unresolved candidate whose base spelling is already exact has no
786    // native evidence that the missing suffix belongs to a case-insensitive
787    // namespace; leave that event observable instead of guessing.
788    base != database && same_native_path(&base, database)
789}
790
791/// Compare native paths without converting identity through display text.
792fn same_native_path(left: &Path, right: &Path) -> bool {
793    if left == right {
794        return true;
795    }
796    let canonical = |path: &Path| -> Option<PathBuf> {
797        if let Ok(path) = fs::canonicalize(path) {
798            return Some(path);
799        }
800        let parent = path.parent()?;
801        let identity = CanonicalProjectRoot::from_path(parent).ok()?;
802        Some(identity.as_path().join(path.file_name()?))
803    };
804    match (canonical(left), canonical(right)) {
805        (Some(left), Some(right)) => left == right,
806        _ => false,
807    }
808}
809
810/// Bounded process-local registry shared by every clone of one MCP server.
811pub(crate) struct SourceObservationRegistry {
812    /// Random identity distinguishing epochs created by this process.
813    process_nonce: [u8; 16],
814    /// Bounded observers keyed by exact source binding.
815    entries: Mutex<HashMap<SourceBinding, Arc<SourceObservationEntry>>>,
816    /// Acceptance invalidations forced by owning mutation-admission tests.
817    #[cfg(test)]
818    mutation_acceptance_invalidations: AtomicU64,
819    /// Preparation invalidations forced by owning mutation-admission tests.
820    #[cfg(test)]
821    preparation_invalidations: AtomicU64,
822}
823
824impl fmt::Debug for SourceObservationRegistry {
825    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
826        let entries = self.entries.lock().map_or(0, |entries| entries.len());
827        formatter
828            .debug_struct("SourceObservationRegistry")
829            .field("entries", &entries)
830            .field("capacity", &SOURCE_OBSERVATION_CAPACITY)
831            .finish_non_exhaustive()
832    }
833}
834
835impl Default for SourceObservationRegistry {
836    fn default() -> Self {
837        Self {
838            process_nonce: process_nonce(),
839            entries: Mutex::new(HashMap::new()),
840            #[cfg(test)]
841            mutation_acceptance_invalidations: AtomicU64::new(0),
842            #[cfg(test)]
843            preparation_invalidations: AtomicU64::new(0),
844        }
845    }
846}
847
848impl SourceObservationRegistry {
849    /// Run one complete query inside a source-epoch and `SQLite`-generation boundary.
850    pub(crate) fn with_verified_read<T, F>(
851        &self,
852        database: &Path,
853        root: &Path,
854        config: Option<&Path>,
855        control: &IndexWorkControl,
856        query: F,
857    ) -> Result<VerifiedReadOutcome<T>, CliError>
858    where
859        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
860    {
861        self.with_read_policy(
862            database,
863            root,
864            config,
865            control,
866            SourceRepairPolicy::Incremental,
867            query,
868        )
869        .and_then(SourceReadAdmission::require_current)
870    }
871
872    /// Verify current source without repairing it, for truthful refresh no-op admission.
873    pub(crate) fn with_verified_current_read<T, F>(
874        &self,
875        database: &Path,
876        root: &Path,
877        config: Option<&Path>,
878        control: &IndexWorkControl,
879        query: F,
880    ) -> Result<SourceReadAdmission<VerifiedReadOutcome<T>>, CliError>
881    where
882        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
883    {
884        self.with_read_policy(
885            database,
886            root,
887            config,
888            control,
889            SourceRepairPolicy::RequireCurrent,
890            query,
891        )
892    }
893
894    /// Share epoch, snapshot and acceptance checks across read mutation policies.
895    fn with_read_policy<T, F>(
896        &self,
897        database: &Path,
898        root: &Path,
899        config: Option<&Path>,
900        control: &IndexWorkControl,
901        repair: SourceRepairPolicy,
902        query: F,
903    ) -> Result<SourceReadAdmission<VerifiedReadOutcome<T>>, CliError>
904    where
905        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
906    {
907        let binding = SourceBinding::new(database, root, config)?;
908        let Some(entry) = self.entry(binding.clone())? else {
909            return self.with_exact_fallback(&binding, control, repair, query);
910        };
911        self.with_observed_read(&entry, control, repair, query)
912    }
913
914    /// Admit a purpose mutation and retain its source witness through commit.
915    pub(crate) fn admit_mutation(
916        &self,
917        database: &Path,
918        root: &Path,
919        config: Option<&Path>,
920        control: &IndexWorkControl,
921    ) -> Result<VerifiedMutationAdmission, CliError> {
922        let binding = SourceBinding::new(database, root, config)?;
923        let Some(entry) = self.entry(binding.clone())? else {
924            return self.admit_exact_mutation(binding, control);
925        };
926        let mut work = VerifiedReadWork::default();
927        for attempt in 0..VERIFIED_READ_ATTEMPTS {
928            if let Err(error) = control.check(IndexWorkStage::Publication) {
929                entry.invalidate();
930                return Err(error.into());
931            }
932            let (store, epoch) =
933                match self.prepare_observed_store(&entry, control, &mut work, attempt == 0) {
934                    Ok(Some(prepared)) => prepared,
935                    Ok(None) => {
936                        entry.invalidate();
937                        return self.admit_exact_mutation(binding, control);
938                    }
939                    Err(error) => {
940                        entry.invalidate();
941                        return Err(error);
942                    }
943                };
944            #[cfg(test)]
945            if self
946                .mutation_acceptance_invalidations
947                .fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
948                    remaining.checked_sub(1)
949                })
950                .is_ok()
951            {
952                entry.continuity_lost.store(true, Ordering::Release);
953            }
954            match Self::accepts_observed_result(&entry, &epoch, control) {
955                Ok(ObservedAcceptance::Accepted) => {
956                    if let Err(error) = store.finish_index_read_snapshot() {
957                        entry.invalidate_epoch(&epoch);
958                        return Err(error.into());
959                    }
960                    return Ok(VerifiedMutationAdmission {
961                        witness: MutationSourceWitness::Observed { entry, epoch },
962                        control: control.clone(),
963                    });
964                }
965                Ok(ObservedAcceptance::Superseded | ObservedAcceptance::Invalidated) => {}
966                Err(error) => {
967                    entry.invalidate_epoch(&epoch);
968                    drop(store.finish_index_read_snapshot());
969                    return Err(error);
970                }
971            }
972            drop(store.finish_index_read_snapshot());
973        }
974        self.admit_exact_mutation(binding, control)
975    }
976
977    /// Admit a mutation through exact source when no native observer is available.
978    fn admit_exact_mutation(
979        &self,
980        binding: SourceBinding,
981        control: &IndexWorkControl,
982    ) -> Result<VerifiedMutationAdmission, CliError> {
983        for _attempt in 0..VERIFIED_READ_ATTEMPTS {
984            control.check(IndexWorkStage::Publication)?;
985            let before = exact_source_policy(&binding, control)?;
986            let exact = open_exact_fresh_atlas_store_for_project_controlled(
987                &binding.database,
988                &binding.root,
989                binding.config.as_deref(),
990                control,
991            )?;
992            let after = match exact_source_policy(&binding, control) {
993                Ok(after) => after,
994                Err(error) => {
995                    drop(exact.store.finish_index_read_snapshot());
996                    return Err(error);
997                }
998            };
999            if before != after {
1000                drop(exact.store.finish_index_read_snapshot());
1001                continue;
1002            }
1003            let stamp = match self.exact_stamp(&exact.store, &binding.root) {
1004                Ok(stamp) => stamp,
1005                Err(error) => {
1006                    drop(exact.store.finish_index_read_snapshot());
1007                    return Err(error);
1008                }
1009            };
1010            exact.store.finish_index_read_snapshot()?;
1011            return Ok(VerifiedMutationAdmission {
1012                witness: MutationSourceWitness::Exact {
1013                    binding,
1014                    stamp,
1015                    contract_fingerprint: after.0,
1016                    policy_witness: after.1,
1017                },
1018                control: control.clone(),
1019            });
1020        }
1021        Err(source_changed_during_derivation(&binding.root, "."))
1022    }
1023
1024    /// Inject one deterministic observer event for owning integration tests.
1025    #[cfg(test)]
1026    pub(crate) fn inject_test_event(
1027        &self,
1028        database: &Path,
1029        root: &Path,
1030        config: Option<&Path>,
1031        event: Event,
1032    ) -> Result<(), CliError> {
1033        let binding = SourceBinding::new(database, root, config)?;
1034        let entry = self
1035            .entries
1036            .lock()
1037            .map_err(|_poisoned| lock_error(&binding.root, "source observation registry"))?
1038            .get(&binding)
1039            .cloned()
1040            .ok_or_else(|| {
1041                CliError::InvalidInput(format!(
1042                    "source observer test entry is missing for '{}'",
1043                    binding.root.display()
1044                ))
1045            })?;
1046        entry.publish_test_event(event)
1047    }
1048
1049    /// Reuse or admit the bounded observer for an exact source binding.
1050    fn entry(
1051        &self,
1052        binding: SourceBinding,
1053    ) -> Result<Option<Arc<SourceObservationEntry>>, CliError> {
1054        let mut entries = self
1055            .entries
1056            .lock()
1057            .map_err(|_poisoned| lock_error(&binding.root, "source observation registry"))?;
1058        if let Some(entry) = entries.get(&binding) {
1059            return Ok(Some(Arc::clone(entry)));
1060        }
1061        if entries.len() >= SOURCE_OBSERVATION_CAPACITY {
1062            return Ok(None);
1063        }
1064        let entry = match SourceObservationEntry::start(binding.clone()) {
1065            Ok(entry) => Arc::new(entry),
1066            Err(_observer_unavailable) => return Ok(None),
1067        };
1068        entries.insert(binding, Arc::clone(&entry));
1069        Ok(Some(entry))
1070    }
1071
1072    /// Retry a query until one observed epoch remains valid through acceptance.
1073    fn with_observed_read<T, F>(
1074        &self,
1075        entry: &SourceObservationEntry,
1076        control: &IndexWorkControl,
1077        repair: SourceRepairPolicy,
1078        mut query: F,
1079    ) -> Result<SourceReadAdmission<VerifiedReadOutcome<T>>, CliError>
1080    where
1081        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
1082    {
1083        let started = Instant::now();
1084        let mut work = VerifiedReadWork::default();
1085        for attempt in 0..VERIFIED_READ_ATTEMPTS {
1086            if let Err(error) = control.check(IndexWorkStage::Publication) {
1087                entry.invalidate();
1088                return Err(error.into());
1089            }
1090            let (store, epoch) = match self
1091                .prepare_observed_store_with_policy(entry, control, &mut work, false, repair)
1092            {
1093                Ok(SourceReadAdmission::Current(Some(prepared))) => prepared,
1094                Ok(SourceReadAdmission::Reconcile(assessment)) => {
1095                    entry.invalidate();
1096                    return Ok(SourceReadAdmission::Reconcile(assessment));
1097                }
1098                Ok(SourceReadAdmission::Current(None)) => {
1099                    entry.invalidate();
1100                    return Err(source_changed_during_derivation(&entry.binding.root, "."));
1101                }
1102                Err(error) => {
1103                    entry.invalidate();
1104                    return Err(error);
1105                }
1106            };
1107            let value = match query(&store, epoch.stamp.clone()) {
1108                Ok(value) => value,
1109                Err(error) => {
1110                    if matches!(error, CliError::IndexWork(_)) {
1111                        entry.invalidate_epoch(&epoch);
1112                    }
1113                    drop(store.finish_index_read_snapshot());
1114                    return Err(error);
1115                }
1116            };
1117            match Self::accepts_observed_result(entry, &epoch, control) {
1118                Ok(ObservedAcceptance::Accepted) => {
1119                    store.finish_index_read_snapshot()?;
1120                    work.elapsed = started.elapsed();
1121                    return Ok(SourceReadAdmission::Current(VerifiedReadOutcome {
1122                        value,
1123                        stamp: epoch.stamp,
1124                        work,
1125                    }));
1126                }
1127                Ok(ObservedAcceptance::Superseded | ObservedAcceptance::Invalidated) => {}
1128                Err(error) => {
1129                    entry.invalidate_epoch(&epoch);
1130                    drop(store.finish_index_read_snapshot());
1131                    return Err(error);
1132                }
1133            }
1134            drop(store.finish_index_read_snapshot());
1135            if attempt + 1 < VERIFIED_READ_ATTEMPTS {
1136                work.retries = work.retries.saturating_add(1);
1137            }
1138        }
1139        Err(source_changed_during_derivation(&entry.binding.root, "."))
1140    }
1141
1142    /// Open a snapshot backed by a current epoch or establish a new exact epoch.
1143    fn prepare_observed_store(
1144        &self,
1145        entry: &SourceObservationEntry,
1146        control: &IndexWorkControl,
1147        work: &mut VerifiedReadWork,
1148        force_exact: bool,
1149    ) -> Result<Option<(AtlasStore, VerifiedSourceEpoch)>, CliError> {
1150        self.prepare_observed_store_with_policy(
1151            entry,
1152            control,
1153            work,
1154            force_exact,
1155            SourceRepairPolicy::Incremental,
1156        )
1157        .and_then(SourceReadAdmission::require_current)
1158    }
1159
1160    /// Establish or reuse an observed epoch with an explicit source-repair policy.
1161    fn prepare_observed_store_with_policy(
1162        &self,
1163        entry: &SourceObservationEntry,
1164        control: &IndexWorkControl,
1165        work: &mut VerifiedReadWork,
1166        force_exact: bool,
1167        repair: SourceRepairPolicy,
1168    ) -> Result<SourceReadAdmission<Option<(AtlasStore, VerifiedSourceEpoch)>>, CliError> {
1169        let _reconcile = entry
1170            .reconcile
1171            .lock()
1172            .map_err(|_poisoned| lock_error(&entry.binding.root, "source reconciliation"))?;
1173        // One-shot refresh must include writes completed before the request. An empty
1174        // callback queue cannot fence events still pending in an asynchronous backend.
1175        // Until a causal backend fence is available, require exact refresh admission.
1176        // Mutation authority also starts from exact saved source under this lock.
1177        if force_exact || matches!(repair, SourceRepairPolicy::RequireCurrent) {
1178            entry.invalidate();
1179        }
1180        let plan = ScanRuntimePlan::for_path_controlled(
1181            entry.binding.config.as_deref(),
1182            &entry.binding.root,
1183            None,
1184            control,
1185        )
1186        .map_err(|source| publication_input_error(&entry.binding.root, source))?;
1187        entry.changed_since_exact_verification(&plan.scan_options)?;
1188        let contract_fingerprint = plan.publication_contract_fingerprint();
1189        let policy_witness = source_policy_witness(&plan, control)?;
1190        if let Some(epoch) = entry.current_epoch()?
1191            && epoch.contract_fingerprint == contract_fingerprint
1192            && epoch.policy_witness == policy_witness
1193            && !entry.continuity_lost.load(Ordering::Acquire)
1194        {
1195            let store = open_atlas_store_read_only_for_project(
1196                &entry.binding.database,
1197                &entry.binding.root,
1198            )?;
1199            let publication = store.index_publication()?.filter(|publication| {
1200                publication.state == IndexPublicationState::Complete
1201                    && publication.contract_fingerprint.as_deref()
1202                        == Some(contract_fingerprint.as_str())
1203            });
1204            let captured = store.captured_project_binding()?;
1205            work.sqlite_read_statements = work.sqlite_read_statements.saturating_add(1);
1206            if publication
1207                .is_some_and(|publication| publication.generation == epoch.stamp.generation)
1208                && captured.project_instance_id == epoch.stamp.project_instance_id
1209            {
1210                return Ok(SourceReadAdmission::Current(Some((store, epoch))));
1211            }
1212            entry.invalidate();
1213            drop(store.finish_index_read_snapshot());
1214        }
1215
1216        for _attempt in 0..VERIFIED_READ_ATTEMPTS {
1217            entry.clear_before_exact_verification()?;
1218            let before_plan = ScanRuntimePlan::for_path_controlled(
1219                entry.binding.config.as_deref(),
1220                &entry.binding.root,
1221                None,
1222                control,
1223            )
1224            .map_err(|source| publication_input_error(&entry.binding.root, source))?;
1225            let before_contract = before_plan.publication_contract_fingerprint();
1226            let before_policy = source_policy_witness(&before_plan, control)?;
1227            let exact = match repair.open_exact(&entry.binding, control)? {
1228                SourceReadAdmission::Current(exact) => exact,
1229                SourceReadAdmission::Reconcile(assessment) => {
1230                    return Ok(SourceReadAdmission::Reconcile(assessment));
1231                }
1232            };
1233            work.add_exact(exact.work);
1234            let after_plan = ScanRuntimePlan::for_path_controlled(
1235                entry.binding.config.as_deref(),
1236                &entry.binding.root,
1237                None,
1238                control,
1239            )
1240            .map_err(|source| publication_input_error(&entry.binding.root, source))?;
1241            let after_contract = after_plan.publication_contract_fingerprint();
1242            let after_policy = source_policy_witness(&after_plan, control)?;
1243            #[cfg(test)]
1244            if self
1245                .preparation_invalidations
1246                .fetch_update(Ordering::AcqRel, Ordering::Acquire, |remaining| {
1247                    remaining.checked_sub(1)
1248                })
1249                .is_ok()
1250            {
1251                entry.invalidate_after_proven_source_mismatch()?;
1252            }
1253            if before_contract != after_contract || before_policy != after_policy {
1254                drop(exact.store.finish_index_read_snapshot());
1255                continue;
1256            }
1257            let publication = exact
1258                .store
1259                .index_publication()?
1260                .ok_or_else(|| source_changed_during_derivation(&entry.binding.root, "."))?;
1261            let captured = exact.store.captured_project_binding()?;
1262            work.sqlite_read_statements = work.sqlite_read_statements.saturating_add(1);
1263            let receiver = entry.receiver.lock().map_err(|_poisoned| {
1264                lock_error(&entry.binding.root, "source observation receiver")
1265            })?;
1266            if entry.drain_source_events(&receiver, &after_plan.scan_options)? {
1267                drop(receiver);
1268                drop(exact.store.finish_index_read_snapshot());
1269                continue;
1270            }
1271            let ingress_sequence = entry.ingress_sequence.load(Ordering::Acquire);
1272            let epoch = entry.install_epoch(
1273                self.process_nonce,
1274                &captured,
1275                publication.generation,
1276                ingress_sequence,
1277                after_contract,
1278                after_policy,
1279            )?;
1280            return Ok(SourceReadAdmission::Current(Some((exact.store, epoch))));
1281        }
1282        Ok(SourceReadAdmission::Current(None))
1283    }
1284
1285    /// Confirm that no source, policy, or observer event invalidated a query result.
1286    fn accepts_observed_result(
1287        entry: &SourceObservationEntry,
1288        epoch: &VerifiedSourceEpoch,
1289        control: &IndexWorkControl,
1290    ) -> Result<ObservedAcceptance, CliError> {
1291        for _attempt in 0..VERIFIED_READ_ATTEMPTS {
1292            control.check(IndexWorkStage::Publication)?;
1293            let Some(sampled) = entry.current_epoch()? else {
1294                return Ok(ObservedAcceptance::Invalidated);
1295            };
1296            let plan = ScanRuntimePlan::for_path_controlled(
1297                entry.binding.config.as_deref(),
1298                &entry.binding.root,
1299                None,
1300                control,
1301            )
1302            .map_err(|source| publication_input_error(&entry.binding.root, source))?;
1303            let contract = plan.publication_contract_fingerprint();
1304            let policy = source_policy_witness(&plan, control)?;
1305            {
1306                let receiver = entry.receiver.lock().map_err(|_poisoned| {
1307                    lock_error(&entry.binding.root, "source observation receiver")
1308                })?;
1309                match entry.drain_epoch_events(&receiver, &sampled, &plan.scan_options)? {
1310                    ObservedAcceptance::Accepted => {}
1311                    ObservedAcceptance::Superseded => continue,
1312                    ObservedAcceptance::Invalidated => return Ok(ObservedAcceptance::Invalidated),
1313                }
1314            }
1315            #[cfg(test)]
1316            if let Some(event) = entry
1317                .acceptance_event
1318                .lock()
1319                .map_err(|_poisoned| lock_error(&entry.binding.root, "acceptance test event"))?
1320                .take()
1321            {
1322                entry.publish_test_event(event)?;
1323            }
1324            let receiver = entry.receiver.lock().map_err(|_poisoned| {
1325                lock_error(&entry.binding.root, "source observation receiver")
1326            })?;
1327            match entry.drain_epoch_events(&receiver, &sampled, &plan.scan_options)? {
1328                ObservedAcceptance::Accepted => {}
1329                ObservedAcceptance::Superseded => continue,
1330                ObservedAcceptance::Invalidated => return Ok(ObservedAcceptance::Invalidated),
1331            }
1332            let mut state = entry
1333                .state
1334                .lock()
1335                .map_err(|_poisoned| lock_error(&entry.binding.root, "source observation state"))?;
1336            let Some(current) = state.verified.as_ref() else {
1337                return Ok(ObservedAcceptance::Invalidated);
1338            };
1339            // A reconciliation during policy sampling requires fresh policy evidence.
1340            if current.stamp != sampled.stamp {
1341                continue;
1342            }
1343            if current.contract_fingerprint != contract || current.policy_witness != policy {
1344                state.verified = None;
1345                return Ok(ObservedAcceptance::Invalidated);
1346            }
1347            if current.stamp != epoch.stamp {
1348                return Ok(ObservedAcceptance::Superseded);
1349            }
1350            if current.ingress_sequence == entry.ingress_sequence.load(Ordering::Acquire) {
1351                return Ok(ObservedAcceptance::Accepted);
1352            }
1353        }
1354        Ok(ObservedAcceptance::Invalidated)
1355    }
1356
1357    /// Exact-per-call compatibility path when a native observer cannot be admitted.
1358    fn with_exact_fallback<T, F>(
1359        &self,
1360        binding: &SourceBinding,
1361        control: &IndexWorkControl,
1362        repair: SourceRepairPolicy,
1363        mut query: F,
1364    ) -> Result<SourceReadAdmission<VerifiedReadOutcome<T>>, CliError>
1365    where
1366        F: FnMut(&AtlasStore, VerifiedReadStamp) -> Result<T, CliError>,
1367    {
1368        let started = Instant::now();
1369        let mut work = VerifiedReadWork::default();
1370        for attempt in 0..VERIFIED_READ_ATTEMPTS {
1371            let exact = match repair.open_exact(binding, control)? {
1372                SourceReadAdmission::Current(exact) => exact,
1373                SourceReadAdmission::Reconcile(assessment) => {
1374                    return Ok(SourceReadAdmission::Reconcile(assessment));
1375                }
1376            };
1377            work.add_exact(exact.work);
1378            let stamp = self.exact_stamp(&exact.store, &binding.root)?;
1379            let value = query(&exact.store, stamp.clone())?;
1380            exact.store.finish_index_read_snapshot()?;
1381
1382            let post = match repair.open_exact(binding, control)? {
1383                SourceReadAdmission::Current(exact) => exact,
1384                SourceReadAdmission::Reconcile(assessment) => {
1385                    return Ok(SourceReadAdmission::Reconcile(assessment));
1386                }
1387            };
1388            work.add_exact(post.work);
1389            let post_publication = post.store.index_publication()?;
1390            let post_binding = post.store.captured_project_binding()?;
1391            post.store.finish_index_read_snapshot()?;
1392            if post_publication.is_some_and(|candidate| {
1393                candidate.generation == stamp.generation
1394                    && post_binding.project_instance_id == stamp.project_instance_id
1395            }) {
1396                work.elapsed = started.elapsed();
1397                return Ok(SourceReadAdmission::Current(VerifiedReadOutcome {
1398                    value,
1399                    stamp,
1400                    work,
1401                }));
1402            }
1403            if attempt + 1 < VERIFIED_READ_ATTEMPTS {
1404                work.retries = work.retries.saturating_add(1);
1405            }
1406        }
1407        Err(source_changed_during_derivation(&binding.root, "."))
1408    }
1409
1410    /// Capture durable identity from one exact current read snapshot.
1411    fn exact_stamp(&self, store: &AtlasStore, root: &Path) -> Result<VerifiedReadStamp, CliError> {
1412        let publication = store
1413            .index_publication()?
1414            .ok_or_else(|| source_changed_during_derivation(root, "."))?;
1415        let captured = store.captured_project_binding()?;
1416        Ok(VerifiedReadStamp {
1417            process_nonce: self.process_nonce,
1418            epoch: 0,
1419            generation: publication.generation,
1420            project_instance_id: captured.project_instance_id,
1421        })
1422    }
1423}
1424
1425/// Convert native watcher callbacks into bounded ingress and continuity state.
1426fn watcher_callback(
1427    sender: SyncSender<Event>,
1428    receiver: Arc<Mutex<Receiver<Event>>>,
1429    ingress_sequence: Arc<AtomicU64>,
1430    continuity_lost: Arc<AtomicBool>,
1431) -> impl FnMut(notify::Result<Event>) + Send + 'static {
1432    move |result| match result {
1433        Ok(event) if matches!(event.kind, EventKind::Access(_)) => {}
1434        result => {
1435            let _receiver = match receiver.lock() {
1436                Ok(receiver) => receiver,
1437                Err(_poisoned) => {
1438                    continuity_lost.store(true, Ordering::Release);
1439                    ingress_sequence.fetch_add(1, Ordering::AcqRel);
1440                    return;
1441                }
1442            };
1443            match result {
1444                Ok(event) => {
1445                    let needs_rescan = event.need_rescan();
1446                    match sender.try_send(event) {
1447                        Ok(()) if !needs_rescan => {}
1448                        Ok(()) | Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
1449                            continuity_lost.store(true, Ordering::Release);
1450                        }
1451                    }
1452                }
1453                Err(_source) => {
1454                    continuity_lost.store(true, Ordering::Release);
1455                }
1456            }
1457            ingress_sequence.fetch_add(1, Ordering::AcqRel);
1458        }
1459    }
1460}
1461
1462/// Hash the bounded external inputs that may change the scanner's source selection.
1463fn source_policy_witness(
1464    plan: &ScanRuntimePlan,
1465    control: &IndexWorkControl,
1466) -> Result<String, CliError> {
1467    let mut hasher = Hasher::new();
1468    hash_field(
1469        &mut hasher,
1470        "contract",
1471        plan.publication_contract_fingerprint().as_bytes(),
1472    );
1473    let root_identity = CanonicalProjectRoot::from_path(&plan.root)
1474        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
1475    let root_identity_bytes = root_identity
1476        .encode()
1477        .map_err(|error| CliError::InvalidInput(error.to_string()))?;
1478    hash_field(&mut hasher, "root", &root_identity_bytes);
1479    for path in source_policy_paths(plan, control)? {
1480        control.check(IndexWorkStage::Publication)?;
1481        hash_field(&mut hasher, "path", path.as_os_str().as_encoded_bytes());
1482        match path.metadata() {
1483            Ok(metadata) if metadata.is_file() => {
1484                let file = File::open(&path).map_err(|source| CliError::Io {
1485                    path: path.clone(),
1486                    source,
1487                })?;
1488                hash_policy_file(&mut hasher, path.as_path(), file, control)?;
1489            }
1490            Ok(metadata) if metadata.is_dir() => {
1491                hash_field(&mut hasher, "state", b"directory");
1492            }
1493            Ok(_metadata) => {
1494                hash_field(&mut hasher, "state", b"other");
1495            }
1496            Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
1497                hash_field(&mut hasher, "state", b"absent");
1498            }
1499            Err(source) => {
1500                return Err(CliError::Io { path, source });
1501            }
1502        }
1503    }
1504    Ok(hasher.finalize().to_hex().to_string())
1505}
1506
1507/// Capture the scanner contract and dynamic policy for one exact source binding.
1508fn exact_source_policy(
1509    binding: &SourceBinding,
1510    control: &IndexWorkControl,
1511) -> Result<(String, String), CliError> {
1512    let plan = ScanRuntimePlan::for_path_controlled(
1513        binding.config.as_deref(),
1514        &binding.root,
1515        None,
1516        control,
1517    )
1518    .map_err(|source| publication_input_error(&binding.root, source))?;
1519    let contract_fingerprint = plan.publication_contract_fingerprint();
1520    let policy_witness = source_policy_witness(&plan, control)?;
1521    Ok((contract_fingerprint, policy_witness))
1522}
1523
1524/// Collect every scanner policy input whose state contributes to source selection.
1525fn source_policy_paths(
1526    plan: &ScanRuntimePlan,
1527    control: &IndexWorkControl,
1528) -> Result<Vec<PathBuf>, CliError> {
1529    let mut paths = projectatlas_fs::source_selection_policy_paths_controlled(&plan.root, control)
1530        .map_err(|source| source_inspection_error(&plan.root, source))?;
1531    if let Some(config) = plan.selected_config_path.as_ref() {
1532        paths.push(config.clone());
1533    } else {
1534        paths.push(plan.root.join(".projectatlas").join("config.toml"));
1535        paths.push(plan.root.join("projectatlas.toml"));
1536    }
1537    paths.sort();
1538    paths.dedup();
1539    Ok(paths)
1540}
1541
1542/// Hash one bounded policy file with cancellation checks between chunks.
1543fn hash_policy_file(
1544    hasher: &mut Hasher,
1545    path: &Path,
1546    file: File,
1547    control: &IndexWorkControl,
1548) -> Result<(), CliError> {
1549    let mut reader = file.take(MAX_POLICY_INPUT_BYTES.saturating_add(1));
1550    let mut buffer = [0_u8; 8_192];
1551    let mut observed = 0_u64;
1552    loop {
1553        control.check(IndexWorkStage::Publication)?;
1554        let read = read_policy_chunk(&mut reader, &mut buffer, path)?;
1555        if read == 0 {
1556            break;
1557        }
1558        observed = observed.saturating_add(u64::try_from(read).unwrap_or(u64::MAX));
1559        if observed > MAX_POLICY_INPUT_BYTES {
1560            return Err(CliError::InvalidInput(format!(
1561                "source-selection policy input '{}' exceeds the {} byte limit",
1562                path.display(),
1563                MAX_POLICY_INPUT_BYTES
1564            )));
1565        }
1566        hasher.update(&buffer[..read]);
1567    }
1568    hash_field(hasher, "state", b"present");
1569    Ok(())
1570}
1571
1572/// Read one policy chunk while retaining the file path in any I/O error.
1573fn read_policy_chunk(
1574    reader: &mut Take<File>,
1575    buffer: &mut [u8],
1576    path: &Path,
1577) -> Result<usize, CliError> {
1578    reader.read(buffer).map_err(|source| CliError::Io {
1579        path: path.to_path_buf(),
1580        source,
1581    })
1582}
1583
1584/// Append one domain-separated field to a policy witness.
1585fn hash_field(hasher: &mut Hasher, name: &str, value: &[u8]) {
1586    hasher.update(name.as_bytes());
1587    hasher.update(&[0]);
1588    hasher.update(value);
1589    hasher.update(&[0xff]);
1590}
1591
1592/// Resolve a possibly relative path against the canonical source root.
1593fn absolute_path_from(root: &Path, path: &Path) -> PathBuf {
1594    if path.is_absolute() {
1595        path.to_path_buf()
1596    } else {
1597        root.join(path)
1598    }
1599}
1600
1601/// Generate one process identity, with a deterministic local entropy fallback.
1602fn process_nonce() -> [u8; 16] {
1603    let mut nonce = [0_u8; 16];
1604    if getrandom::fill(&mut nonce).is_ok() {
1605        return nonce;
1606    }
1607    let mut hasher = Hasher::new();
1608    hasher.update(&std::process::id().to_le_bytes());
1609    let time = SystemTime::now()
1610        .duration_since(UNIX_EPOCH)
1611        .map_or(0, |duration| duration.as_nanos());
1612    hasher.update(&time.to_le_bytes());
1613    nonce.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
1614    nonce
1615}
1616
1617/// Convert watcher startup failure into a root-scoped observer diagnostic.
1618fn observer_error(root: &Path, source: &notify::Error) -> CliError {
1619    CliError::InvalidInput(format!(
1620        "source observation is unavailable for '{}': {source}",
1621        root.display()
1622    ))
1623}
1624
1625/// Build a root-scoped diagnostic for poisoned observer state.
1626fn lock_error(root: &Path, owner: &str) -> CliError {
1627    CliError::InvalidInput(format!(
1628        "{owner} lock is unavailable for project root '{}'",
1629        root.display()
1630    ))
1631}
1632
1633#[cfg(test)]
1634mod tests {
1635    use super::*;
1636    use notify::event::{ModifyKind, RenameMode};
1637    use notify::{EventKind, event::AccessKind};
1638    use projectatlas_core::{IndexCancellation, PurposeSource};
1639    use std::error::Error;
1640    #[cfg(windows)]
1641    use std::process::Command;
1642
1643    /// Create and publish a minimal indexed repository for observer tests.
1644    fn indexed_project(root: &Path) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
1645        fs::create_dir_all(root.join(".projectatlas"))?;
1646        let source = root.join("source.rs");
1647        fs::write(&source, "fn original() {}\n")?;
1648        let database = root.join(".projectatlas").join("projectatlas.db");
1649        let mut store = super::super::open_atlas_store_for_project(&database, root)?;
1650        let plan = ScanRuntimePlan::for_path(None, root, None)?;
1651        super::super::run_scan_pipeline(
1652            &mut store,
1653            &plan,
1654            &super::super::SymbolBuildOptions::new(
1655                super::super::MAX_SYMBOL_FILE_BYTES,
1656                Some(1),
1657                None,
1658            ),
1659        )?;
1660        drop(store);
1661        Ok((database, source))
1662    }
1663
1664    /// Build a bounded non-cancelled control for observer tests.
1665    fn test_control() -> IndexWorkControl {
1666        IndexWorkControl::new(IndexCancellation::new(), Some(Duration::from_secs(30)))
1667    }
1668
1669    /// Return a fallible test failure without panicking inside result-returning tests.
1670    fn require(condition: bool, message: &str) -> Result<(), Box<dyn Error>> {
1671        if condition {
1672            Ok(())
1673        } else {
1674            Err(std::io::Error::other(message).into())
1675        }
1676    }
1677
1678    #[test]
1679    fn native_observer_teardown_releases_root_and_external_config() -> Result<(), Box<dyn Error>> {
1680        let temp = tempfile::tempdir()?;
1681        let root = temp.path().join("repo");
1682        let external = temp.path().join("settings");
1683        fs::create_dir_all(&root)?;
1684        fs::create_dir_all(&external)?;
1685        let config = external.join("config.toml");
1686        fs::write(&config, "")?;
1687        let database = root.join("atlas.db");
1688        let entry =
1689            SourceObservationEntry::start(SourceBinding::new(&database, &root, Some(&config))?)?;
1690        fs::write(root.join("source.rs"), "fn observed() {}")?;
1691        drop(entry);
1692        require(!database.exists(), "observer cleanup created an index")?;
1693        fs::remove_dir_all(&root)?;
1694        fs::remove_dir_all(&external)?;
1695        require(
1696            !root.exists() && !external.exists(),
1697            "watch directories remain",
1698        )
1699    }
1700
1701    #[test]
1702    fn native_observer_failed_registration_releases_admitted_root() -> Result<(), Box<dyn Error>> {
1703        let temp = tempfile::tempdir()?;
1704        let root = temp.path().join("repo");
1705        fs::create_dir_all(&root)?;
1706        let database = root.join("atlas.db");
1707        let config = temp.path().join("missing-parent").join("config.toml");
1708        let binding = SourceBinding::new(&database, &root, Some(&config))?;
1709        let result = SourceObservationEntry::start(binding.clone());
1710        require(result.is_err(), "missing configuration parent was accepted")?;
1711        require(
1712            !database.exists(),
1713            "failed observer startup created an index",
1714        )?;
1715        fs::remove_dir_all(&root)?;
1716        require(!root.exists(), "failed startup retained its root")?;
1717        require(
1718            SourceObservationEntry::start(binding).is_err(),
1719            "missing root was accepted",
1720        )
1721    }
1722
1723    #[test]
1724    fn watcher_callback_is_bounded_and_marks_overflow_and_rescan() {
1725        let (sender, receiver) = sync_channel(1);
1726        let receiver = Arc::new(Mutex::new(receiver));
1727        let sequence = Arc::new(AtomicU64::new(0));
1728        let lost = Arc::new(AtomicBool::new(false));
1729        let mut callback = watcher_callback(
1730            sender,
1731            Arc::clone(&receiver),
1732            Arc::clone(&sequence),
1733            Arc::clone(&lost),
1734        );
1735        callback(Ok(Event::new(EventKind::Modify(ModifyKind::Name(
1736            RenameMode::Any,
1737        )))));
1738        callback(Ok(Event::new(EventKind::Modify(ModifyKind::Any))));
1739
1740        assert_eq!(sequence.load(Ordering::Acquire), 2);
1741        assert!(lost.load(Ordering::Acquire));
1742        assert!(
1743            receiver
1744                .lock()
1745                .is_ok_and(|receiver| receiver.try_recv().is_ok())
1746        );
1747    }
1748
1749    #[test]
1750    fn watcher_callback_ignores_access_events_without_advancing_epoch() {
1751        let (sender, receiver) = sync_channel(1);
1752        let receiver = Arc::new(Mutex::new(receiver));
1753        let sequence = Arc::new(AtomicU64::new(0));
1754        let lost = Arc::new(AtomicBool::new(false));
1755        let mut callback =
1756            watcher_callback(sender, receiver, Arc::clone(&sequence), Arc::clone(&lost));
1757        callback(Ok(Event::new(EventKind::Access(AccessKind::Any))));
1758
1759        assert_eq!(sequence.load(Ordering::Acquire), 0);
1760        assert!(!lost.load(Ordering::Acquire));
1761    }
1762
1763    #[test]
1764    fn current_read_reconciles_exactly_without_repair_or_callback_delivery()
1765    -> Result<(), Box<dyn Error>> {
1766        let temp = tempfile::tempdir()?;
1767        let (database, source) = indexed_project(temp.path())?;
1768        let registry = SourceObservationRegistry::default();
1769        let plan = ScanRuntimePlan::for_path(None, temp.path(), None)?;
1770        let options = super::super::SymbolBuildOptions::new(
1771            super::super::MAX_SYMBOL_FILE_BYTES,
1772            Some(1),
1773            None,
1774        );
1775        let first = registry
1776            .with_verified_current_read(
1777                &database,
1778                temp.path(),
1779                None,
1780                &test_control(),
1781                |store, _stamp| Ok(store.index_publication()?),
1782            )?
1783            .require_current()?;
1784        let second = registry
1785            .with_verified_current_read(
1786                &database,
1787                temp.path(),
1788                None,
1789                &test_control(),
1790                |store, _stamp| Ok(store.index_publication()?),
1791            )?
1792            .require_current()?;
1793        require(
1794            first.value == second.value && first.stamp.generation == second.stamp.generation,
1795            "unchanged admission changed publication",
1796        )?;
1797        require(
1798            second.work.exact_verifications == 1
1799                && second.work.filesystem_entries > 0
1800                && second.work.filesystem_bytes > 0
1801                && second.work.decoded_nodes > 0,
1802            "one-shot admission trusted an unfenced callback queue",
1803        )?;
1804
1805        // A source event is not the only invalidation: an external writer advances generation.
1806        let mut writer = super::super::open_atlas_store_for_project(&database, temp.path())?;
1807        let generation = first.stamp.generation;
1808        super::super::run_scan_pipeline(&mut writer, &plan, &options)?;
1809        let advanced = registry
1810            .with_verified_current_read(
1811                &database,
1812                temp.path(),
1813                None,
1814                &test_control(),
1815                |_store, stamp| Ok(stamp.generation),
1816            )?
1817            .require_current()?;
1818        require(
1819            advanced.value != generation && advanced.work.exact_verifications > 0,
1820            "external publication reused the previous verified epoch",
1821        )?;
1822        let before = writer.index_publication()?;
1823        let previous_text = writer.load_file_text("source.rs")?.map(|row| row.content);
1824        // Simulate an event still pending upstream: keep the accepted epoch but
1825        // detach native ingress before the source write. No callback can expose it.
1826        {
1827            let binding = SourceBinding::new(&database, temp.path(), None)?;
1828            let mut entries = registry.entries.lock().map_err(|_poisoned| {
1829                lock_error(&binding.root, "test source observation registry")
1830            })?;
1831            let entry = entries
1832                .get_mut(&binding)
1833                .and_then(Arc::get_mut)
1834                .ok_or_else(|| std::io::Error::other("test observer is not exclusively owned"))?;
1835            entry.watcher = notify::recommended_watcher(|_event| {})?;
1836        }
1837        fs::write(&source, "fn changed_before_observation() {}\n")?;
1838        let admission = registry.with_verified_current_read(
1839            &database,
1840            temp.path(),
1841            None,
1842            &test_control(),
1843            |_store, _stamp| Ok(()),
1844        )?;
1845        let SourceReadAdmission::Reconcile(assessment) = admission else {
1846            return Err(
1847                std::io::Error::other("changed source did not transfer its discovery").into(),
1848            );
1849        };
1850        require(
1851            writer.index_publication()? == before
1852                && writer.load_file_text("source.rs")?.map(|row| row.content) == previous_text,
1853            "refused no-op admission changed the prior publication",
1854        )?;
1855        let refresh = super::super::refresh_index_from_assessment_controlled(
1856            &mut writer,
1857            &plan,
1858            &options,
1859            *assessment,
1860            &test_control(),
1861        )?;
1862        require(
1863            refresh.symbols.parsed == 1,
1864            "transferred discovery did not publish the affected source",
1865        )?;
1866        require(
1867            writer
1868                .load_file_text("source.rs")?
1869                .is_some_and(|row| row.content == "fn changed_before_observation() {}\n"),
1870            "transferred discovery did not publish current text",
1871        )?;
1872        fs::write(&source, "fn changed_after_handoff() {}\n")?;
1873        let stale = SourceObservationRegistry::default().with_verified_current_read(
1874            &database,
1875            temp.path(),
1876            None,
1877            &test_control(),
1878            |_store, _stamp| Ok(()),
1879        )?;
1880        let SourceReadAdmission::Reconcile(assessment) = stale else {
1881            return Err(std::io::Error::other("second change did not transfer discovery").into());
1882        };
1883        super::super::run_scan_pipeline(&mut writer, &plan, &options)?;
1884        let winner = writer.index_publication()?;
1885        let result = super::super::refresh_index_from_assessment_controlled(
1886            &mut writer,
1887            &plan,
1888            &options,
1889            *assessment,
1890            &test_control(),
1891        );
1892        require(
1893            matches!(
1894                result,
1895                Err(CliError::Db(
1896                    projectatlas_db::DbError::PublicationBaseGenerationChanged { .. }
1897                ))
1898            ),
1899            "handoff discarded captured generation and repeated discovery",
1900        )?;
1901        require(
1902            writer.index_publication()? == winner,
1903            "stale handoff changed the winner",
1904        )?;
1905        Ok(())
1906    }
1907
1908    #[test]
1909    fn verified_epoch_avoids_repeat_tree_and_node_table_work() -> Result<(), Box<dyn Error>> {
1910        let temp = tempfile::tempdir()?;
1911        let (database, _source) = indexed_project(temp.path())?;
1912        let registry = SourceObservationRegistry::default();
1913
1914        let first = registry.with_verified_read(
1915            &database,
1916            temp.path(),
1917            None,
1918            &test_control(),
1919            |store, stamp| Ok((store.overview()?, stamp)),
1920        )?;
1921        require(
1922            (1..=u64::try_from(VERIFIED_READ_ATTEMPTS)?).contains(&first.work.exact_verifications),
1923            "initial read did not perform a bounded exact verification",
1924        )?;
1925        require(
1926            first.work.filesystem_entries > 0,
1927            "initial read did not inspect filesystem entries",
1928        )?;
1929        require(
1930            first.work.decoded_nodes > 0,
1931            "initial read did not decode indexed nodes",
1932        )?;
1933
1934        let second = registry.with_verified_read(
1935            &database,
1936            temp.path(),
1937            None,
1938            &test_control(),
1939            |store, stamp| Ok((store.overview()?, stamp)),
1940        )?;
1941        require(
1942            second.work.exact_verifications == 0,
1943            "warm read unexpectedly repeated exact verification",
1944        )?;
1945        require(
1946            second.work.filesystem_entries == 0,
1947            "warm read unexpectedly inspected filesystem entries",
1948        )?;
1949        require(
1950            second.work.filesystem_bytes == 0,
1951            "warm read unexpectedly hashed source bytes",
1952        )?;
1953        require(
1954            second.work.decoded_nodes == 0,
1955            "warm read unexpectedly decoded the full node table",
1956        )?;
1957        require(
1958            second.work.sqlite_read_statements == 1,
1959            "warm freshness check used an unexpected SQLite statement count",
1960        )?;
1961        require(
1962            first.stamp == second.stamp,
1963            "warm read did not reuse the verified epoch",
1964        )?;
1965        Ok(())
1966    }
1967
1968    #[test]
1969    fn ignore_policy_changes_invalidate_the_epoch_and_refresh_source_truth()
1970    -> Result<(), Box<dyn Error>> {
1971        let temp = tempfile::tempdir()?;
1972        let (database, _source) = indexed_project(temp.path())?;
1973        let registry = SourceObservationRegistry::default();
1974        let first = registry.with_verified_read(
1975            &database,
1976            temp.path(),
1977            None,
1978            &test_control(),
1979            |store, _stamp| Ok(store.load_node_by_path("source.rs")?.is_some()),
1980        )?;
1981        require(first.value, "initial indexed source was missing")?;
1982
1983        fs::write(temp.path().join(".ignore"), "source.rs\n")?;
1984        let ignored = registry.with_verified_read(
1985            &database,
1986            temp.path(),
1987            None,
1988            &test_control(),
1989            |store, _stamp| Ok(store.load_node_by_path("source.rs")?.is_some()),
1990        );
1991
1992        require(
1993            matches!(ignored, Err(CliError::RefreshRequired(_))),
1994            "ignore policy change did not require refresh",
1995        )?;
1996        let binding = SourceBinding::new(&database, temp.path(), None)?;
1997        let entry = registry
1998            .entries
1999            .lock()
2000            .map_err(|_poisoned| std::io::Error::other("registry lock poisoned"))?
2001            .get(&binding)
2002            .cloned()
2003            .ok_or_else(|| std::io::Error::other("observer entry missing"))?;
2004        require(
2005            entry.current_epoch()?.is_none(),
2006            "ignore policy change left a verified epoch installed",
2007        )?;
2008        Ok(())
2009    }
2010
2011    #[test]
2012    fn git_exclude_changes_are_witnessed_with_a_git_directory() -> Result<(), Box<dyn Error>> {
2013        let temp = tempfile::tempdir()?;
2014        let (database, _source) = indexed_project(temp.path())?;
2015        let git = temp.path().join(".git");
2016        let git_info = git.join("info");
2017        fs::create_dir_all(&git_info)?;
2018        fs::create_dir_all(git.join("objects"))?;
2019        fs::create_dir_all(git.join("refs"))?;
2020        fs::write(git.join("HEAD"), "ref: refs/heads/main\n")?;
2021        fs::write(git.join("config"), "[core]\n")?;
2022        fs::write(git_info.join("exclude"), "")?;
2023        let registry = SourceObservationRegistry::default();
2024        let first = registry.with_verified_read(
2025            &database,
2026            temp.path(),
2027            None,
2028            &test_control(),
2029            |store, _stamp| Ok(store.load_node_by_path("source.rs")?.is_some()),
2030        )?;
2031        require(first.value, "initial indexed source was missing")?;
2032
2033        fs::write(git_info.join("exclude"), "source.rs\n")?;
2034        let ignored = registry.with_verified_read(
2035            &database,
2036            temp.path(),
2037            None,
2038            &test_control(),
2039            |store, _stamp| Ok(store.load_node_by_path("source.rs")?.is_some()),
2040        )?;
2041
2042        require(
2043            !ignored.value,
2044            "Git exclude change left the excluded source indexed",
2045        )?;
2046        require(
2047            ignored.work.exact_verifications >= 1,
2048            "Git exclude change reused the stale source epoch",
2049        )?;
2050        require(
2051            ignored.stamp.epoch > first.stamp.epoch,
2052            "Git exclude change did not advance the verified source epoch",
2053        )?;
2054        Ok(())
2055    }
2056
2057    #[cfg(unix)]
2058    #[test]
2059    fn symlinked_git_exclude_target_changes_invalidate_the_epoch() -> Result<(), Box<dyn Error>> {
2060        let temp = tempfile::tempdir()?;
2061        let policy = tempfile::tempdir()?;
2062        let (database, _source) = indexed_project(temp.path())?;
2063        let git = temp.path().join(".git");
2064        let git_info = git.join("info");
2065        let exclude_target = policy.path().join("exclude");
2066        fs::create_dir_all(&git_info)?;
2067        fs::create_dir_all(git.join("objects"))?;
2068        fs::create_dir_all(git.join("refs"))?;
2069        fs::write(git.join("HEAD"), "ref: refs/heads/main\n")?;
2070        fs::write(git.join("config"), "[core]\n")?;
2071        fs::write(&exclude_target, "")?;
2072        std::os::unix::fs::symlink(&exclude_target, git_info.join("exclude"))?;
2073        let registry = SourceObservationRegistry::default();
2074        let first = registry.with_verified_read(
2075            &database,
2076            temp.path(),
2077            None,
2078            &test_control(),
2079            |store, _stamp| Ok(store.load_node_by_path("source.rs")?.is_some()),
2080        )?;
2081        require(first.value, "initial indexed source was missing")?;
2082
2083        fs::write(&exclude_target, "source.rs\n")?;
2084        let ignored = registry.with_verified_read(
2085            &database,
2086            temp.path(),
2087            None,
2088            &test_control(),
2089            |store, _stamp| Ok(store.load_node_by_path("source.rs")?.is_some()),
2090        )?;
2091
2092        require(
2093            !ignored.value,
2094            "symlinked Git exclude target change left the source indexed",
2095        )?;
2096        require(
2097            ignored.work.exact_verifications >= 1,
2098            "symlinked Git exclude target change reused the stale source epoch",
2099        )?;
2100        require(
2101            ignored.stamp.epoch > first.stamp.epoch,
2102            "symlinked Git exclude target change did not advance the verified source epoch",
2103        )?;
2104        Ok(())
2105    }
2106
2107    #[test]
2108    fn mid_query_edit_discards_provisional_result_and_reconciles() -> Result<(), Box<dyn Error>> {
2109        let temp = tempfile::tempdir()?;
2110        let (database, source) = indexed_project(temp.path())?;
2111        let registry = SourceObservationRegistry::default();
2112        let _initial = registry.with_verified_read(
2113            &database,
2114            temp.path(),
2115            None,
2116            &test_control(),
2117            |store, _stamp| Ok(store.overview()?),
2118        )?;
2119        let binding = SourceBinding::new(&database, temp.path(), None)?;
2120        let entry = registry
2121            .entries
2122            .lock()
2123            .map_err(|_poisoned| std::io::Error::other("registry lock poisoned"))?
2124            .get(&binding)
2125            .cloned()
2126            .ok_or_else(|| std::io::Error::other("observer entry missing"))?;
2127        let mut calls = 0_u64;
2128        let revised = "fn revised() {}\n";
2129
2130        let outcome = registry.with_verified_read(
2131            &database,
2132            temp.path(),
2133            None,
2134            &test_control(),
2135            |store, _stamp| {
2136                calls = calls.saturating_add(1);
2137                let hash = store
2138                    .load_node_by_path("source.rs")?
2139                    .and_then(|node| node.node.content_hash)
2140                    .ok_or_else(|| CliError::InvalidInput("source hash missing".to_string()))?;
2141                if calls == 1 {
2142                    fs::write(&source, revised).map_err(|source_error| CliError::Io {
2143                        path: source.clone(),
2144                        source: source_error,
2145                    })?;
2146                    entry.publish_test_event(
2147                        Event::new(EventKind::Modify(ModifyKind::Any)).add_path(source.clone()),
2148                    )?;
2149                }
2150                Ok(hash)
2151            },
2152        )?;
2153
2154        require(calls >= 2, "mid-query edit did not retry the query")?;
2155        require(
2156            outcome.work.retries >= 1,
2157            "mid-query edit was not reported as a retry",
2158        )?;
2159        require(
2160            outcome.work.exact_verifications >= 1,
2161            "mid-query edit did not trigger exact verification",
2162        )?;
2163        require(
2164            outcome.value == blake3::hash(revised.as_bytes()).to_hex().to_string(),
2165            "accepted result did not reflect the revised source",
2166        )?;
2167        Ok(())
2168    }
2169
2170    #[test]
2171    fn observed_acceptance_does_not_wait_for_exact_reconciliation() -> Result<(), Box<dyn Error>> {
2172        let temp = tempfile::tempdir()?;
2173        let (database, _source) = indexed_project(temp.path())?;
2174        let registry = SourceObservationRegistry::default();
2175        let control = test_control();
2176        let entry = registry
2177            .entry(SourceBinding::new(&database, temp.path(), None)?)?
2178            .ok_or_else(|| std::io::Error::other("source observer unavailable"))?;
2179        let (store, epoch) = registry
2180            .prepare_observed_store(&entry, &control, &mut VerifiedReadWork::default(), false)?
2181            .ok_or_else(|| std::io::Error::other("read epoch unavailable"))?;
2182        let reconcile = entry
2183            .reconcile
2184            .lock()
2185            .map_err(|_poisoned| std::io::Error::other("source reconciliation lock poisoned"))?;
2186        let (sender, receiver) = std::sync::mpsc::channel();
2187        let accepted = std::thread::scope(|scope| {
2188            scope.spawn(|| {
2189                let accepted = matches!(
2190                    SourceObservationRegistry::accepts_observed_result(&entry, &epoch, &control),
2191                    Ok(ObservedAcceptance::Accepted)
2192                );
2193                let _sent = sender.send(accepted);
2194            });
2195            let result = receiver.recv_timeout(Duration::from_secs(5));
2196            // Release even on failure so the regression cannot strand the test worker.
2197            drop(reconcile);
2198            result
2199        });
2200        require(
2201            accepted == Ok(true),
2202            "observed acceptance waited for exact reconciliation or rejected valid evidence",
2203        )?;
2204        store.finish_index_read_snapshot()?;
2205        Ok(())
2206    }
2207
2208    #[test]
2209    fn older_read_acceptance_preserves_newer_mutation_witness() -> Result<(), Box<dyn Error>> {
2210        let temp = tempfile::tempdir()?;
2211        let (database, source) = indexed_project(temp.path())?;
2212        let before = fs::read(&source)?;
2213        let registry = SourceObservationRegistry::default();
2214        let control = test_control();
2215        let mut admission = None;
2216        registry.with_verified_read(&database, temp.path(), None, &control, |store, _stamp| {
2217            if admission.is_none() {
2218                admission =
2219                    Some(registry.admit_mutation(&database, temp.path(), None, &control)?);
2220            }
2221            Ok(store.overview()?)
2222        })?;
2223        require(
2224            fs::read(&source)? == before,
2225            "source changed during read admission",
2226        )?;
2227        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2228        let transaction = store.begin_purpose_mutation()?;
2229        store.set_purpose(
2230            "source.rs",
2231            "Accepted concurrent purpose",
2232            PurposeSource::Agent,
2233        )?;
2234        admission
2235            .ok_or_else(|| std::io::Error::other("mutation admission missing"))?
2236            .verify()?;
2237        transaction.commit()?;
2238        require(
2239            store.load_node_by_path("source.rs")?.is_some_and(|node| {
2240                node.purpose.purpose.as_deref() == Some("Accepted concurrent purpose")
2241            }),
2242            "valid concurrent mutation did not commit its purpose",
2243        )?;
2244        Ok(())
2245    }
2246
2247    #[test]
2248    fn superseded_mutation_cleanup_preserves_successor_witness() -> Result<(), Box<dyn Error>> {
2249        let temp = tempfile::tempdir()?;
2250        let (database, _source) = indexed_project(temp.path())?;
2251        let registry = SourceObservationRegistry::default();
2252        let control = test_control();
2253        let previous = registry.admit_mutation(&database, temp.path(), None, &control)?;
2254        let current = registry.admit_mutation(&database, temp.path(), None, &control)?;
2255        require(
2256            matches!(previous.verify(), Err(CliError::RefreshRequired(_))),
2257            "superseded mutation witness was unexpectedly accepted",
2258        )?;
2259        current.verify()?;
2260        Ok(())
2261    }
2262
2263    #[test]
2264    fn exact_source_mismatch_invalidates_successor_without_observer_delivery()
2265    -> Result<(), Box<dyn Error>> {
2266        let temp = tempfile::tempdir()?;
2267        let (database, source) = indexed_project(temp.path())?;
2268        let registry = SourceObservationRegistry::default();
2269        let binding = SourceBinding::new(&database, temp.path(), None)?;
2270        let mut observer = SourceObservationEntry::start(binding.clone())?;
2271        observer.watcher.unwatch(&binding.root)?;
2272        observer.watcher.configure(notify::Config::default())?;
2273        let entry = Arc::new(observer);
2274        registry
2275            .entries
2276            .lock()
2277            .map_err(|_poisoned| std::io::Error::other("observer registry lock poisoned"))?
2278            .insert(binding, Arc::clone(&entry));
2279        let control = test_control();
2280        let previous = registry.admit_mutation(&database, temp.path(), None, &control)?;
2281        let MutationSourceWitness::Observed { epoch, .. } = &previous.witness else {
2282            return Err(std::io::Error::other("observed mutation missing").into());
2283        };
2284        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2285        let before_revision = store.authored_purpose_revision()?;
2286        let transaction = store.begin_purpose_mutation()?;
2287        let mut successor = None;
2288        let result = VerifiedMutationAdmission::verify_observed(&entry, epoch, &control, || {
2289            successor = Some(registry.admit_mutation(&database, temp.path(), None, &control)?);
2290            store.set_purpose("source.rs", "Uncommitted purpose", PurposeSource::Agent)?;
2291            fs::write(&source, "fn changed_after_successor() {}\n").map_err(|error| {
2292                CliError::Io {
2293                    path: source.clone(),
2294                    source: error,
2295                }
2296            })?;
2297            verify_saved_source_matches_index_controlled(&database, temp.path(), None, &control)
2298        });
2299        drop(transaction);
2300        require(
2301            matches!(result, Err(CliError::RefreshRequired(_))),
2302            "exact source mismatch did not reject the previous mutation",
2303        )?;
2304        require(
2305            store.authored_purpose_revision()? == before_revision,
2306            "rejected mutation changed the authored-purpose revision",
2307        )?;
2308        require(
2309            entry.current_epoch()?.is_none(),
2310            "proven saved-source mismatch left successor evidence reusable",
2311        )?;
2312        let successor =
2313            successor.ok_or_else(|| std::io::Error::other("successor admission missing"))?;
2314        require(
2315            matches!(successor.verify(), Err(CliError::RefreshRequired(_))),
2316            "successor survived a proven saved-source mismatch",
2317        )?;
2318        Ok(())
2319    }
2320
2321    #[test]
2322    fn cancelled_exact_verification_preserves_successor_witness() -> Result<(), Box<dyn Error>> {
2323        let temp = tempfile::tempdir()?;
2324        let (database, _source) = indexed_project(temp.path())?;
2325        let registry = SourceObservationRegistry::default();
2326        let cancellation = IndexCancellation::new();
2327        let control = IndexWorkControl::new(cancellation.clone(), Some(Duration::from_secs(30)));
2328        let previous = registry.admit_mutation(&database, temp.path(), None, &control)?;
2329        let MutationSourceWitness::Observed { entry, epoch } = &previous.witness else {
2330            return Err(std::io::Error::other("observed mutation missing").into());
2331        };
2332        let mut successor = None;
2333        let result = VerifiedMutationAdmission::verify_observed(entry, epoch, &control, || {
2334            successor =
2335                Some(registry.admit_mutation(&database, temp.path(), None, &test_control())?);
2336            cancellation.cancel();
2337            verify_saved_source_matches_index_controlled(&database, temp.path(), None, &control)
2338        });
2339        require(
2340            matches!(result, Err(CliError::IndexWork(_))),
2341            "exact verification did not return cancellation",
2342        )?;
2343        successor
2344            .ok_or_else(|| std::io::Error::other("successor admission missing"))?
2345            .verify()?;
2346        Ok(())
2347    }
2348
2349    #[test]
2350    fn stale_read_source_event_invalidates_newer_mutation_and_rolls_back()
2351    -> Result<(), Box<dyn Error>> {
2352        let temp = tempfile::tempdir()?;
2353        let (database, source) = indexed_project(temp.path())?;
2354        let registry = SourceObservationRegistry::default();
2355        let control = test_control();
2356        let entry = registry
2357            .entry(SourceBinding::new(&database, temp.path(), None)?)?
2358            .ok_or_else(|| std::io::Error::other("source observer unavailable"))?;
2359        let (read_store, old_epoch) = registry
2360            .prepare_observed_store(&entry, &control, &mut VerifiedReadWork::default(), false)?
2361            .ok_or_else(|| std::io::Error::other("read epoch unavailable"))?;
2362        let admission = registry.admit_mutation(&database, temp.path(), None, &control)?;
2363        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2364        let before_revision = store.authored_purpose_revision()?;
2365        let before_purpose = store
2366            .load_node_by_path("source.rs")?
2367            .ok_or_else(|| std::io::Error::other("indexed source missing"))?
2368            .purpose;
2369        let transaction = store.begin_purpose_mutation()?;
2370        store.set_purpose(
2371            "source.rs",
2372            "Rejected concurrent purpose",
2373            PurposeSource::Agent,
2374        )?;
2375        let event = Event::new(EventKind::Modify(ModifyKind::Any)).add_path(source);
2376        let previous_options = projectatlas_fs::ScanOptions {
2377            exclude_path_prefixes: vec!["source.rs".to_owned()],
2378            ..projectatlas_fs::ScanOptions::default()
2379        };
2380        let ignored = observer_event_changes(&entry.binding, &previous_options, &event);
2381        require(
2382            ignored.paths.is_empty() && !ignored.requires_full_scan,
2383            "previous policy did not exclude the event in the regression fixture",
2384        )?;
2385        entry.publish_test_event(event)?;
2386        {
2387            let receiver = entry.receiver.lock().map_err(|_poisoned| {
2388                std::io::Error::other("source observation receiver lock poisoned")
2389            })?;
2390            require(
2391                entry.drain_epoch_events(&receiver, &old_epoch, &previous_options)?
2392                    == ObservedAcceptance::Superseded,
2393                "older policy consumed an event belonging to a successor epoch",
2394            )?;
2395        }
2396        require(
2397            SourceObservationRegistry::accepts_observed_result(&entry, &old_epoch, &control)?
2398                == ObservedAcceptance::Invalidated,
2399            "stale reader treated a source event as mere supersession",
2400        )?;
2401        require(
2402            entry.current_epoch()?.is_none(),
2403            "source event left newer evidence installed",
2404        )?;
2405        let plan = ScanRuntimePlan::for_path_controlled(None, temp.path(), None, &control)?;
2406        require(
2407            entry.changed_since_exact_verification(&plan.scan_options)?,
2408            "consumed source event was lost to concurrent exact verification",
2409        )?;
2410        read_store.finish_index_read_snapshot()?;
2411        let verification = admission.verify();
2412        transaction.rollback()?;
2413        require(
2414            matches!(verification, Err(CliError::RefreshRequired(_))),
2415            "consumed source event did not reject the mutation witness",
2416        )?;
2417        require(
2418            store.authored_purpose_revision()? == before_revision
2419                && store
2420                    .load_node_by_path("source.rs")?
2421                    .is_some_and(|node| node.purpose == before_purpose),
2422            "rejected mutation changed authored purpose state",
2423        )?;
2424        Ok(())
2425    }
2426
2427    #[test]
2428    fn mutation_admission_reconciles_saved_source_without_waiting_for_observer_delivery()
2429    -> Result<(), Box<dyn Error>> {
2430        let temp = tempfile::tempdir()?;
2431        let (database, source) = indexed_project(temp.path())?;
2432        let registry = SourceObservationRegistry::default();
2433        let control = test_control();
2434        let _warm = registry.with_verified_read(
2435            &database,
2436            temp.path(),
2437            None,
2438            &control,
2439            |store, _stamp| Ok(store.overview()?),
2440        )?;
2441        let revised = "fn revised_before_admission() {}\n";
2442        fs::write(&source, revised)?;
2443
2444        let admission = registry.admit_mutation(&database, temp.path(), None, &control)?;
2445        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2446        let content_hash = store
2447            .load_node_by_path("source.rs")?
2448            .and_then(|node| node.node.content_hash)
2449            .ok_or_else(|| std::io::Error::other("reconciled source hash missing"))?;
2450        require(
2451            content_hash == blake3::hash(revised.as_bytes()).to_hex().to_string(),
2452            "mutation admission reused the warm source epoch",
2453        )?;
2454        admission.verify()?;
2455        Ok(())
2456    }
2457
2458    #[test]
2459    fn mutation_admission_retries_transient_invalidation_and_falls_back_to_exact_source()
2460    -> Result<(), Box<dyn Error>> {
2461        let temp = tempfile::tempdir()?;
2462        let (database, source) = indexed_project(temp.path())?;
2463        let registry = SourceObservationRegistry::default();
2464        let control = test_control();
2465        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2466        let before_source = fs::read(&source)?;
2467        let before_revision = store.authored_purpose_revision()?;
2468        let before_purpose = store
2469            .load_node_by_path("source.rs")?
2470            .ok_or_else(|| std::io::Error::other("indexed source missing"))?
2471            .purpose;
2472
2473        registry
2474            .mutation_acceptance_invalidations
2475            .store(1, Ordering::Release);
2476        let admission = registry.admit_mutation(&database, temp.path(), None, &control)?;
2477        require(
2478            registry
2479                .mutation_acceptance_invalidations
2480                .load(Ordering::Acquire)
2481                == 0,
2482            "transient mutation invalidation was not consumed",
2483        )?;
2484        admission.verify()?;
2485
2486        registry
2487            .mutation_acceptance_invalidations
2488            .store(u64::try_from(VERIFIED_READ_ATTEMPTS)?, Ordering::Release);
2489        let exact = registry.admit_mutation(&database, temp.path(), None, &control)?;
2490        require(
2491            matches!(&exact.witness, MutationSourceWitness::Exact { .. }),
2492            "persistent mutation invalidation did not fall back to exact source",
2493        )?;
2494        require(
2495            registry
2496                .mutation_acceptance_invalidations
2497                .load(Ordering::Acquire)
2498                == 0,
2499            "mutation invalidation attempts were not bounded",
2500        )?;
2501        exact.verify()?;
2502        require(
2503            fs::read(&source)? == before_source,
2504            "mutation admission changed saved source",
2505        )?;
2506        require(
2507            store.authored_purpose_revision()? == before_revision,
2508            "mutation admission changed authored-purpose revision",
2509        )?;
2510        require(
2511            store
2512                .load_node_by_path("source.rs")?
2513                .is_some_and(|node| node.purpose == before_purpose),
2514            "mutation admission changed the purpose row",
2515        )?;
2516        Ok(())
2517    }
2518
2519    #[test]
2520    fn exact_mismatch_during_preparation_prevents_observed_installation()
2521    -> Result<(), Box<dyn Error>> {
2522        let temp = tempfile::tempdir()?;
2523        let (database, _source) = indexed_project(temp.path())?;
2524        let registry = SourceObservationRegistry::default();
2525        registry
2526            .preparation_invalidations
2527            .store(u64::try_from(VERIFIED_READ_ATTEMPTS)?, Ordering::Release);
2528
2529        let admission = registry.admit_mutation(&database, temp.path(), None, &test_control())?;
2530
2531        require(
2532            matches!(&admission.witness, MutationSourceWitness::Exact { .. }),
2533            "preparation continuity loss did not fall back to exact source",
2534        )?;
2535        require(
2536            registry.preparation_invalidations.load(Ordering::Acquire) == 0,
2537            "preparation invalidation attempts were not bounded",
2538        )?;
2539        admission.verify()?;
2540
2541        registry
2542            .preparation_invalidations
2543            .store(u64::try_from(VERIFIED_READ_ATTEMPTS)?, Ordering::Release);
2544        let read = registry.with_verified_read(
2545            &database,
2546            temp.path(),
2547            None,
2548            &test_control(),
2549            |store, _stamp| Ok(store.overview()?),
2550        );
2551        require(
2552            matches!(read, Err(CliError::RefreshRequired(_))),
2553            "observer read did not retain continuity-loss recovery",
2554        )?;
2555        Ok(())
2556    }
2557
2558    #[test]
2559    fn mutation_verification_redrains_delayed_observer_events() -> Result<(), Box<dyn Error>> {
2560        let temp = tempfile::tempdir()?;
2561        let (database, source) = indexed_project(temp.path())?;
2562        let registry = SourceObservationRegistry::default();
2563        let admission = registry.admit_mutation(&database, temp.path(), None, &test_control())?;
2564        let MutationSourceWitness::Observed { entry, .. } = &admission.witness else {
2565            return Err(
2566                std::io::Error::other("mutation did not retain an observed witness").into(),
2567            );
2568        };
2569        *entry
2570            .acceptance_event
2571            .lock()
2572            .map_err(|_poisoned| std::io::Error::other("acceptance event lock poisoned"))? =
2573            Some(Event::new(EventKind::Modify(ModifyKind::Any)).add_path(database));
2574
2575        admission.verify()?;
2576
2577        require(
2578            entry
2579                .acceptance_event
2580                .lock()
2581                .map_err(|_poisoned| std::io::Error::other("acceptance event lock poisoned"))?
2582                .is_none(),
2583            "delayed irrelevant event was not consumed",
2584        )?;
2585        *entry
2586            .acceptance_event
2587            .lock()
2588            .map_err(|_poisoned| std::io::Error::other("acceptance event lock poisoned"))? =
2589            Some(Event::new(EventKind::Modify(ModifyKind::Any)).add_path(source));
2590        require(
2591            matches!(admission.verify(), Err(CliError::RefreshRequired(_))),
2592            "delayed relevant event was accepted",
2593        )?;
2594        Ok(())
2595    }
2596
2597    #[test]
2598    fn mutation_verification_rechecks_continuity_under_drain_lock() -> Result<(), Box<dyn Error>> {
2599        let temp = tempfile::tempdir()?;
2600        let (database, _source) = indexed_project(temp.path())?;
2601        let registry = SourceObservationRegistry::default();
2602        let admission = registry.admit_mutation(&database, temp.path(), None, &test_control())?;
2603        let MutationSourceWitness::Observed { entry, .. } = &admission.witness else {
2604            return Err(
2605                std::io::Error::other("mutation did not retain an observed witness").into(),
2606            );
2607        };
2608        entry
2609            .drain_continuity_invalidations
2610            .store(1, Ordering::Release);
2611
2612        require(
2613            matches!(admission.verify(), Err(CliError::RefreshRequired(_))),
2614            "continuity loss during drain admission was accepted",
2615        )?;
2616        require(
2617            entry.drain_continuity_invalidations.load(Ordering::Acquire) == 0,
2618            "drain continuity invalidation was not consumed",
2619        )?;
2620        Ok(())
2621    }
2622
2623    #[test]
2624    fn mutation_admission_uses_exact_fallback_when_observer_capacity_is_full()
2625    -> Result<(), Box<dyn Error>> {
2626        let temp = tempfile::tempdir()?;
2627        let (database, source) = indexed_project(temp.path())?;
2628        let git = temp.path().join(".git");
2629        let git_info = git.join("info");
2630        let git_exclude = git_info.join("exclude");
2631        fs::create_dir_all(&git_info)?;
2632        fs::create_dir_all(git.join("objects"))?;
2633        fs::create_dir_all(git.join("refs"))?;
2634        fs::write(git.join("HEAD"), "ref: refs/heads/main\n")?;
2635        fs::write(git.join("config"), "[core]\n")?;
2636        fs::write(&git_exclude, "")?;
2637        let registry = SourceObservationRegistry::default();
2638        let binding = SourceBinding::new(&database, temp.path(), None)?;
2639        let filler = registry
2640            .entry(binding.clone())?
2641            .ok_or_else(|| std::io::Error::other("source observer was unavailable"))?;
2642        {
2643            let mut entries = registry
2644                .entries
2645                .lock()
2646                .map_err(|_poisoned| std::io::Error::other("observer registry was poisoned"))?;
2647            entries.clear();
2648            for index in 0..SOURCE_OBSERVATION_CAPACITY {
2649                let mut filler_binding = binding.clone();
2650                filler_binding.config = Some(temp.path().join(format!("observer-{index}.toml")));
2651                entries.insert(filler_binding, Arc::clone(&filler));
2652            }
2653        }
2654
2655        let control = test_control();
2656        let admission = registry.admit_mutation(&database, temp.path(), None, &control)?;
2657        require(
2658            matches!(&admission.witness, MutationSourceWitness::Exact { .. }),
2659            "full observer registry did not use exact mutation admission",
2660        )?;
2661        let entries = registry
2662            .entries
2663            .lock()
2664            .map_err(|_poisoned| std::io::Error::other("observer registry was poisoned"))?;
2665        require(
2666            entries.len() == SOURCE_OBSERVATION_CAPACITY && !entries.contains_key(&binding),
2667            "exact mutation admission changed the bounded observer registry",
2668        )?;
2669        drop(entries);
2670
2671        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2672        let transaction = store.begin_purpose_mutation()?;
2673        store.set_purpose("source.rs", "Exact fallback purpose", PurposeSource::Agent)?;
2674        admission.verify()?;
2675        transaction.commit()?;
2676        require(
2677            store.load_node_by_path("source.rs")?.is_some_and(|node| {
2678                node.purpose.purpose.as_deref() == Some("Exact fallback purpose")
2679            }),
2680            "exact fallback did not commit an unchanged-source mutation",
2681        )?;
2682
2683        let policy_admission = registry.admit_mutation(&database, temp.path(), None, &control)?;
2684        let before_revision = store.authored_purpose_revision()?;
2685        let before_purpose = store
2686            .load_node_by_path("source.rs")?
2687            .ok_or_else(|| std::io::Error::other("indexed source missing"))?
2688            .purpose;
2689        let transaction = store.begin_purpose_mutation()?;
2690        store.set_purpose("source.rs", "Rejected policy purpose", PurposeSource::Agent)?;
2691        fs::write(&git_exclude, "never-present-policy-only.tmp\n")?;
2692        let verification = policy_admission.verify();
2693        transaction.rollback()?;
2694        require(
2695            matches!(verification, Err(CliError::RefreshRequired(_))),
2696            "exact fallback accepted changed source-selection policy",
2697        )?;
2698        require(
2699            store.authored_purpose_revision()? == before_revision
2700                && store
2701                    .load_node_by_path("source.rs")?
2702                    .is_some_and(|node| node.purpose == before_purpose),
2703            "policy-drift rejection changed authored purpose",
2704        )?;
2705
2706        let replaced = registry.admit_mutation(&database, temp.path(), None, &control)?;
2707        drop(store);
2708        fs::write(&source, "fn concurrently_published() {}\n")?;
2709        let mut publisher = super::super::open_atlas_store_for_project(&database, temp.path())?;
2710        let plan = ScanRuntimePlan::for_path(None, temp.path(), None)?;
2711        super::super::run_scan_pipeline(
2712            &mut publisher,
2713            &plan,
2714            &super::super::SymbolBuildOptions::new(
2715                super::super::MAX_SYMBOL_FILE_BYTES,
2716                Some(1),
2717                None,
2718            ),
2719        )?;
2720        drop(publisher);
2721        verify_saved_source_matches_index_controlled(&database, temp.path(), None, &control)?;
2722        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2723        let before_revision = store.authored_purpose_revision()?;
2724        let before_purpose = store
2725            .load_node_by_path("source.rs")?
2726            .ok_or_else(|| std::io::Error::other("indexed source missing"))?
2727            .purpose;
2728        let transaction = store.begin_purpose_mutation()?;
2729        store.set_purpose(
2730            "source.rs",
2731            "Replaced generation purpose",
2732            PurposeSource::Agent,
2733        )?;
2734        let verification = replaced.verify();
2735        transaction.rollback()?;
2736        require(
2737            matches!(verification, Err(CliError::RefreshRequired(_))),
2738            "exact fallback accepted a replacement publication generation",
2739        )?;
2740        require(
2741            store.authored_purpose_revision()? == before_revision
2742                && store
2743                    .load_node_by_path("source.rs")?
2744                    .is_some_and(|node| node.purpose == before_purpose),
2745            "replacement-generation rejection changed authored purpose",
2746        )?;
2747
2748        let cancellation = IndexCancellation::new();
2749        let canceled_control =
2750            IndexWorkControl::new(cancellation.clone(), Some(Duration::from_secs(30)));
2751        let canceled = registry.admit_mutation(&database, temp.path(), None, &canceled_control)?;
2752        cancellation.cancel();
2753        require(
2754            matches!(canceled.verify(), Err(CliError::IndexWork(_))),
2755            "exact fallback did not retain cancellation through verification",
2756        )?;
2757
2758        let before_revision = store.authored_purpose_revision()?;
2759        let before_purpose = store
2760            .load_node_by_path("source.rs")?
2761            .ok_or_else(|| std::io::Error::other("indexed source missing"))?
2762            .purpose;
2763        let stale = registry.admit_mutation(&database, temp.path(), None, &control)?;
2764        let transaction = store.begin_purpose_mutation()?;
2765        store.set_purpose(
2766            "source.rs",
2767            "Rejected fallback purpose",
2768            PurposeSource::Agent,
2769        )?;
2770        fs::write(&source, "fn changed_after_exact_admission() {}\n")?;
2771        let verification = stale.verify();
2772        transaction.rollback()?;
2773        require(
2774            matches!(verification, Err(CliError::RefreshRequired(_))),
2775            "exact fallback admitted a mutation after saved source changed",
2776        )?;
2777        require(
2778            store.authored_purpose_revision()? == before_revision,
2779            "rejected exact fallback advanced authored-purpose revision",
2780        )?;
2781        require(
2782            store
2783                .load_node_by_path("source.rs")?
2784                .is_some_and(|node| node.purpose == before_purpose),
2785            "rejected exact fallback changed the purpose row",
2786        )?;
2787        Ok(())
2788    }
2789
2790    #[test]
2791    fn post_admission_edit_rolls_back_without_waiting_for_observer_delivery()
2792    -> Result<(), Box<dyn Error>> {
2793        let temp = tempfile::tempdir()?;
2794        let (database, source) = indexed_project(temp.path())?;
2795        let registry = SourceObservationRegistry::default();
2796        let control = test_control();
2797        let admission = registry.admit_mutation(&database, temp.path(), None, &control)?;
2798        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2799        let before = store
2800            .load_node_by_path("source.rs")?
2801            .ok_or_else(|| std::io::Error::other("indexed source missing"))?
2802            .purpose;
2803        let before_revision = store.authored_purpose_revision()?;
2804        let transaction = store.begin_purpose_mutation()?;
2805        store.set_purpose("source.rs", "Stale purpose", PurposeSource::Agent)?;
2806
2807        fs::write(&source, "fn changed_after_admission() {}\n")?;
2808        let verification = admission.verify();
2809        drop(transaction);
2810        require(
2811            matches!(verification, Err(CliError::RefreshRequired(_))),
2812            "post-admission edit did not invalidate the purpose commit",
2813        )?;
2814        require(
2815            store.authored_purpose_revision()? == before_revision,
2816            "rolled-back mutation advanced authored-purpose revision",
2817        )?;
2818        require(
2819            store
2820                .load_node_by_path("source.rs")?
2821                .is_some_and(|node| node.purpose == before),
2822            "rolled-back mutation changed the purpose row",
2823        )?;
2824        Ok(())
2825    }
2826
2827    #[test]
2828    fn post_admission_cancellation_rolls_back_purpose_mutation() -> Result<(), Box<dyn Error>> {
2829        let temp = tempfile::tempdir()?;
2830        let (database, _source) = indexed_project(temp.path())?;
2831        let registry = SourceObservationRegistry::default();
2832        let cancellation = IndexCancellation::new();
2833        let control = IndexWorkControl::new(cancellation.clone(), Some(Duration::from_secs(30)));
2834        let admission = registry.admit_mutation(&database, temp.path(), None, &control)?;
2835        let store = super::super::open_atlas_store_for_project(&database, temp.path())?;
2836        let before_revision = store.authored_purpose_revision()?;
2837        let transaction = store.begin_purpose_mutation()?;
2838        store.set_purpose("source.rs", "Canceled purpose", PurposeSource::Agent)?;
2839
2840        cancellation.cancel();
2841        let verification = admission.verify();
2842        drop(transaction);
2843        require(
2844            matches!(verification, Err(CliError::IndexWork(_))),
2845            "post-admission cancellation did not invalidate the purpose commit",
2846        )?;
2847        require(
2848            store.authored_purpose_revision()? == before_revision,
2849            "canceled mutation advanced authored-purpose revision",
2850        )?;
2851        require(
2852            store
2853                .load_node_by_path("source.rs")?
2854                .is_none_or(|node| node.purpose.purpose.as_deref() != Some("Canceled purpose")),
2855            "canceled mutation changed the purpose row",
2856        )?;
2857        Ok(())
2858    }
2859
2860    #[test]
2861    fn same_native_path_rejects_distinct_unresolved_paths() -> Result<(), Box<dyn Error>> {
2862        let temp = tempfile::tempdir()?;
2863        let left = temp.path().join("missing-left").join("leaf");
2864        let right = temp.path().join("missing-right").join("leaf");
2865
2866        require(
2867            !super::same_native_path(&left, &right),
2868            "unresolved distinct paths were treated as equal",
2869        )?;
2870        Ok(())
2871    }
2872
2873    #[test]
2874    fn same_native_path_matches_paths_below_a_canonicalizable_parent() -> Result<(), Box<dyn Error>>
2875    {
2876        let temp = tempfile::tempdir()?;
2877        let parent = temp.path().join("canonical-parent");
2878        let nested = parent.join("nested");
2879        fs::create_dir(&parent)?;
2880        fs::create_dir(&nested)?;
2881        let left = parent.join("missing");
2882        let right = nested.join("..").join("missing");
2883
2884        require(
2885            super::same_native_path(&left, &right),
2886            "equivalent paths below a canonicalizable parent diverged",
2887        )?;
2888        Ok(())
2889    }
2890
2891    #[cfg(windows)]
2892    #[test]
2893    fn observer_filters_case_variant_database_sidecars_without_rescan() -> Result<(), Box<dyn Error>>
2894    {
2895        let temp = tempfile::tempdir()?;
2896        let root = temp.path().join("MiXeDAtlasRoot");
2897        let metadata = root.join(".projectatlas");
2898        fs::create_dir_all(&metadata)?;
2899        let database = metadata.join("ProjectAtlas.db");
2900        fs::write(&database, b"SQLite format 3\0")?;
2901        let sidecars = ["-wal", "-shm", "-journal"].map(|suffix| {
2902            let mut sidecar = database.as_os_str().to_os_string();
2903            sidecar.push(suffix);
2904            PathBuf::from(sidecar)
2905        });
2906        for sidecar in &sidecars {
2907            fs::write(sidecar, b"SQLite sidecar")?;
2908        }
2909        let source = root.join("source.rs");
2910        fs::write(&source, "fn source() {}\n")?;
2911        let binding = SourceBinding::new(&database, &root, None)?;
2912        let scan_options = projectatlas_fs::ScanOptions::default();
2913        let differently_cased = |path: &Path| -> Result<PathBuf, Box<dyn Error>> {
2914            let path = path
2915                .to_str()
2916                .ok_or_else(|| std::io::Error::other("test path was not UTF-8"))?;
2917            Ok(PathBuf::from(path.to_ascii_uppercase()))
2918        };
2919
2920        for path in std::iter::once(&database).chain(sidecars.iter()) {
2921            let event =
2922                Event::new(EventKind::Modify(ModifyKind::Any)).add_path(differently_cased(path)?);
2923            let changes = super::observer_event_changes(&binding, &scan_options, &event);
2924            require(
2925                !changes.has_changes(),
2926                "case-variant SQLite runtime event triggered a rescan",
2927            )?;
2928        }
2929
2930        for sidecar in &sidecars {
2931            fs::remove_file(sidecar)?;
2932            let event = Event::new(EventKind::Modify(ModifyKind::Any))
2933                .add_path(differently_cased(sidecar)?);
2934            let changes = super::observer_event_changes(&binding, &scan_options, &event);
2935            require(
2936                !changes.has_changes(),
2937                "case-variant removed SQLite sidecar event triggered a rescan",
2938            )?;
2939        }
2940
2941        let event = Event::new(EventKind::Modify(ModifyKind::Any)).add_path(source);
2942        let changes = super::observer_event_changes(&binding, &scan_options, &event);
2943        require(
2944            changes.has_changes(),
2945            "non-runtime source event was incorrectly filtered",
2946        )?;
2947        Ok(())
2948    }
2949
2950    #[cfg(windows)]
2951    #[test]
2952    fn observer_keeps_case_variant_sidecar_event_in_case_sensitive_directory()
2953    -> Result<(), Box<dyn Error>> {
2954        let temp = tempfile::tempdir()?;
2955        let parent = temp.path().join("case-sensitive-observer");
2956        fs::create_dir(&parent)?;
2957        let enabled = Command::new("fsutil")
2958            .args(["file", "SetCaseSensitiveInfo"])
2959            .arg(&parent)
2960            .arg("enable")
2961            .status()
2962            .is_ok_and(|status| status.success());
2963        if !enabled {
2964            return Ok(());
2965        }
2966        let root = parent.join("Root");
2967        let metadata = root.join(".projectatlas");
2968        fs::create_dir_all(&metadata)?;
2969        let database = metadata.join("ProjectAtlas.db");
2970        fs::write(&database, b"SQLite format 3\0")?;
2971        let binding = SourceBinding::new(&database, &root, None)?;
2972        let event_path = metadata.join("PROJECTATLAS.DB-WAL");
2973        let event = Event::new(EventKind::Modify(ModifyKind::Any)).add_path(event_path);
2974        let changes = super::observer_event_changes(
2975            &binding,
2976            &projectatlas_fs::ScanOptions::default(),
2977            &event,
2978        );
2979        require(
2980            changes.has_changes(),
2981            "case-variant sidecar event was suppressed in a case-sensitive directory",
2982        )
2983    }
2984
2985    #[test]
2986    fn cancellation_and_continuity_loss_never_certify_partial_truth() -> Result<(), Box<dyn Error>>
2987    {
2988        let temp = tempfile::tempdir()?;
2989        let (database, _source) = indexed_project(temp.path())?;
2990        let registry = SourceObservationRegistry::default();
2991        let initial = registry.with_verified_read(
2992            &database,
2993            temp.path(),
2994            None,
2995            &test_control(),
2996            |store, _stamp| Ok(store.overview()?),
2997        )?;
2998        require(
2999            initial.work.exact_verifications >= 1,
3000            "initial read did not establish exact truth",
3001        )?;
3002        let binding = SourceBinding::new(&database, temp.path(), None)?;
3003        let entry = registry
3004            .entries
3005            .lock()
3006            .map_err(|_poisoned| std::io::Error::other("registry lock poisoned"))?
3007            .get(&binding)
3008            .cloned()
3009            .ok_or_else(|| std::io::Error::other("observer entry missing"))?;
3010
3011        let cancellation = IndexCancellation::new();
3012        cancellation.cancel();
3013        let canceled_control = IndexWorkControl::new(cancellation, None);
3014        let canceled = registry.with_verified_read(
3015            &database,
3016            temp.path(),
3017            None,
3018            &canceled_control,
3019            |store, _stamp| Ok(store.overview()?),
3020        );
3021        require(
3022            matches!(canceled, Err(CliError::IndexWork(_))),
3023            "cancelled observer read did not return index-work cancellation",
3024        )?;
3025        require(
3026            entry.current_epoch()?.is_none(),
3027            "cancellation left the previous verified epoch installed",
3028        )?;
3029
3030        let recovered = registry.with_verified_read(
3031            &database,
3032            temp.path(),
3033            None,
3034            &test_control(),
3035            |store, _stamp| Ok(store.overview()?),
3036        )?;
3037        require(
3038            recovered.work.exact_verifications >= 1,
3039            "read after cancellation did not establish exact truth",
3040        )?;
3041        entry.continuity_lost.store(true, Ordering::Release);
3042        let reverified = registry.with_verified_read(
3043            &database,
3044            temp.path(),
3045            None,
3046            &test_control(),
3047            |store, _stamp| Ok(store.overview()?),
3048        )?;
3049        require(
3050            reverified.work.exact_verifications >= 1,
3051            "continuity loss did not trigger exact verification",
3052        )?;
3053        Ok(())
3054    }
3055}