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