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