Skip to main content

relay_knowledge/paths/
repository_root.rs

1//! Repository-root discovery for repository-local contracts.
2//!
3//! This module owns path-based discovery for repository-scoped files such as
4//! `.knowledge/knowledge-map.yaml`. Callers provide the starting directory;
5//! process cwd lookup belongs to bootstrap.
6
7use std::{
8    error::Error,
9    fmt, fs, io,
10    path::{Path, PathBuf},
11};
12
13use crate::project::AGENT_CONTRACT_DIR_NAME;
14
15/// Error raised before repository-root discovery can walk ancestors.
16#[derive(Debug)]
17pub enum RepositoryRootDiscoveryError {
18    StartUnavailable { path: PathBuf, source: io::Error },
19    StartNotDirectory { path: PathBuf },
20    MarkerProbeFailed { path: PathBuf, source: io::Error },
21}
22
23impl fmt::Display for RepositoryRootDiscoveryError {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::StartUnavailable { path, source } => {
27                write!(
28                    formatter,
29                    "failed to inspect start directory '{}': {source}",
30                    path.display()
31                )
32            }
33            Self::StartNotDirectory { path } => {
34                write!(
35                    formatter,
36                    "repository root search must start from a directory, got '{}'",
37                    path.display()
38                )
39            }
40            Self::MarkerProbeFailed { path, source } => {
41                write!(
42                    formatter,
43                    "failed to inspect repository marker '{}': {source}",
44                    path.display()
45                )
46            }
47        }
48    }
49}
50
51impl Error for RepositoryRootDiscoveryError {
52    fn source(&self) -> Option<&(dyn Error + 'static)> {
53        match self {
54            Self::StartUnavailable { source, .. } | Self::MarkerProbeFailed { source, .. } => {
55                Some(source)
56            }
57            Self::StartNotDirectory { .. } => None,
58        }
59    }
60}
61
62/// Finds the repository root that owns repository-local knowledge contracts.
63///
64/// Discovery starts at `start` and walks ancestors. A `.git` directory/file or
65/// `.knowledge` directory wins immediately. If neither exists, the nearest
66/// `AGENTS.md` ancestor is used as a compatibility fallback.
67pub fn discover_repository_root(
68    start: &Path,
69) -> Result<Option<PathBuf>, RepositoryRootDiscoveryError> {
70    let metadata =
71        fs::metadata(start).map_err(|source| RepositoryRootDiscoveryError::StartUnavailable {
72            path: start.to_path_buf(),
73            source,
74        })?;
75    if !metadata.is_dir() {
76        return Err(RepositoryRootDiscoveryError::StartNotDirectory {
77            path: start.to_path_buf(),
78        });
79    }
80
81    let mut agents_root = None;
82    for path in start.ancestors() {
83        if marker_exists(path.join(".git"))? || marker_exists(path.join(AGENT_CONTRACT_DIR_NAME))? {
84            return Ok(Some(path.to_path_buf()));
85        }
86        if agents_root.is_none() && marker_exists(path.join("AGENTS.md"))? {
87            agents_root = Some(path.to_path_buf());
88        }
89    }
90
91    Ok(agents_root)
92}
93
94fn marker_exists(path: PathBuf) -> Result<bool, RepositoryRootDiscoveryError> {
95    path.try_exists()
96        .map_err(|source| RepositoryRootDiscoveryError::MarkerProbeFailed { path, source })
97}
98
99#[cfg(test)]
100#[path = "repository_root_tests.rs"]
101mod tests;