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