use tokio::fs;
use crate::{
api::RequestContext,
domain::{
RepositoryMapDirectory, RepositoryMapDirectoryChange, RepositoryMapType,
validate_directory_collection,
},
project::{CODESPEC_MAP_FILE_NAME, KNOWLEDGE_MAP_FILE_NAME},
};
use super::{
KnowledgeMapMutationResponse, KnowledgeMapService, KnowledgeMapServiceError,
ensure_owned_directory, now_stamp,
};
impl KnowledgeMapService {
pub async fn add_directory(
&self,
context: &RequestContext,
directory: RepositoryMapDirectory,
) -> Result<KnowledgeMapMutationResponse, KnowledgeMapServiceError> {
let mutation_locks = self.acquire_legacy_aware_mutation_locks().await?;
let legacy_recovery_state = mutation_locks.legacy_recovery_state;
self.recover_legacy_rollback_transition().await?;
self.recover_manifest_backup().await?;
self.recover_legacy_redirect_transition().await?;
if legacy_recovery_state && !fs::try_exists(self.map_path()).await? {
let mut preflight = self.load_for_mutation().await?;
preflight
.map
.ensure_reserved_repository_routes_snapshot(preflight.omitted_through)?;
add_directory_to_collection(
self.map_type,
&mut preflight.directories,
directory.clone(),
)?;
}
self.prepare_legacy_migration().await?;
let mut snapshot = self.load_for_mutation().await?;
if self.map_type == RepositoryMapType::Knowledge {
snapshot
.map
.ensure_reserved_repository_routes_snapshot(snapshot.omitted_through)?;
}
let name = directory.directory.clone();
add_directory_to_collection(self.map_type, &mut snapshot.directories, directory)?;
snapshot.map.record_change(
"directory.add",
format!("Added governed directory '{name}'."),
now_stamp(),
);
self.write_map(&mut snapshot).await?;
Ok(self.mutation_response(
context,
snapshot.map.map_version,
format!("added directory {name}"),
))
}
pub async fn update_directory(
&self,
context: &RequestContext,
change: RepositoryMapDirectoryChange,
) -> Result<KnowledgeMapMutationResponse, KnowledgeMapServiceError> {
let mutation_locks = self.acquire_legacy_aware_mutation_locks().await?;
let legacy_recovery_state = mutation_locks.legacy_recovery_state;
self.recover_legacy_rollback_transition().await?;
self.recover_manifest_backup().await?;
self.recover_legacy_redirect_transition().await?;
if legacy_recovery_state && !fs::try_exists(self.map_path()).await? {
let mut preflight = self.load_for_mutation().await?;
preflight
.map
.ensure_reserved_repository_routes_snapshot(preflight.omitted_through)?;
update_directory_collection(self.map_type, &mut preflight.directories, &change)?;
}
self.prepare_legacy_migration().await?;
let mut snapshot = self.load_for_mutation().await?;
if self.map_type == RepositoryMapType::Knowledge {
snapshot
.map
.ensure_reserved_repository_routes_snapshot(snapshot.omitted_through)?;
}
let name = update_directory_collection(self.map_type, &mut snapshot.directories, &change)?;
snapshot.map.record_change(
"directory.update",
format!("Updated governed directory '{name}'."),
now_stamp(),
);
self.write_map(&mut snapshot).await?;
Ok(self.mutation_response(
context,
snapshot.map.map_version,
format!("updated directory {name}"),
))
}
pub async fn remove_directory(
&self,
context: &RequestContext,
directory: String,
) -> Result<KnowledgeMapMutationResponse, KnowledgeMapServiceError> {
if self
.map_type
.required_directories()
.contains(&directory.as_str())
{
return Err(KnowledgeMapServiceError::InvalidRequest(format!(
"required directory '{directory}' cannot be removed"
)));
}
let mutation_locks = self.acquire_legacy_aware_mutation_locks().await?;
let legacy_recovery_state = mutation_locks.legacy_recovery_state;
self.recover_legacy_rollback_transition().await?;
self.recover_manifest_backup().await?;
self.recover_legacy_redirect_transition().await?;
if legacy_recovery_state && !fs::try_exists(self.map_path()).await? {
let mut preflight = self.load_for_mutation().await?;
preflight
.map
.ensure_reserved_repository_routes_snapshot(preflight.omitted_through)?;
remove_directory_from_collection(
self.map_type,
&mut preflight.directories,
&directory,
)?;
}
self.prepare_legacy_migration().await?;
let mut snapshot = self.load_for_mutation().await?;
if self.map_type == RepositoryMapType::Knowledge {
snapshot
.map
.ensure_reserved_repository_routes_snapshot(snapshot.omitted_through)?;
}
remove_directory_from_collection(self.map_type, &mut snapshot.directories, &directory)?;
snapshot.map.record_change(
"directory.remove",
format!("Removed governed directory '{directory}'."),
now_stamp(),
);
self.write_map(&mut snapshot).await?;
Ok(self.mutation_response(
context,
snapshot.map.map_version,
format!("removed directory {directory}"),
))
}
pub(super) async fn ensure_baseline_files(&self) -> Result<(), KnowledgeMapServiceError> {
let contract = self.repository_root.join(self.contract_dir_name());
ensure_owned_directory(&self.repository_root, &contract).await?;
for directory in self.map_type.required_directories() {
let path = contract.join(directory);
ensure_owned_directory(&self.repository_root, &path).await?;
let readme = path.join("README.md");
if !fs::try_exists(&readme).await? {
fs::write(readme, baseline_readme(self.map_type, directory)).await?;
}
}
Ok(())
}
pub(super) async fn validate_directory_files(&self) -> Result<(), KnowledgeMapServiceError> {
if self.uses_legacy_contract().await? {
return Ok(());
}
let map = self.load_show_view().await?;
validate_directory_collection(self.map_type, &map.directories, true)?;
let repository = fs::canonicalize(&self.repository_root).await?;
for entry in map.directories {
let directory = self
.repository_root
.join(self.map_type.as_str())
.join(&entry.directory);
let metadata = fs::symlink_metadata(&directory).await?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(KnowledgeMapServiceError::UnsafePath(
directory.display().to_string(),
));
}
let canonical = fs::canonicalize(directory).await?;
if !canonical.starts_with(&repository) {
return Err(KnowledgeMapServiceError::UnsafePath(
canonical.display().to_string(),
));
}
for key_file in entry.key_files {
let path = self.repository_root.join(&key_file);
let metadata = fs::symlink_metadata(&path).await?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(KnowledgeMapServiceError::UnsafePath(key_file));
}
if !fs::canonicalize(path).await?.starts_with(&repository) {
return Err(KnowledgeMapServiceError::UnsafePath(key_file));
}
}
}
Ok(())
}
pub(super) async fn validate_cross_map_relations(
&self,
) -> Result<(), KnowledgeMapServiceError> {
let map = self.load_show_view().await?;
let peer_type = match self.map_type {
RepositoryMapType::Knowledge => RepositoryMapType::Codespec,
RepositoryMapType::Codespec => RepositoryMapType::Knowledge,
};
let peer_targets = map
.directories
.iter()
.flat_map(|entry| &entry.relations)
.filter_map(|relation| {
relation
.target
.strip_prefix(&format!("{}:", peer_type.as_str()))
})
.collect::<Vec<_>>();
if peer_targets.is_empty() {
return Ok(());
}
let peer = self.for_type(peer_type).load_show_view().await?;
for target in peer_targets {
if !peer
.directories
.iter()
.any(|entry| entry.directory == target)
{
return Err(KnowledgeMapServiceError::Integrity(format!(
"cross-map directory target '{}:{target}' does not exist",
peer_type.as_str()
)));
}
}
Ok(())
}
}
fn add_directory_to_collection(
map_type: RepositoryMapType,
directories: &mut Vec<RepositoryMapDirectory>,
directory: RepositoryMapDirectory,
) -> Result<(), KnowledgeMapServiceError> {
directory.validate(map_type)?;
if directories
.iter()
.any(|entry| entry.directory.eq_ignore_ascii_case(&directory.directory))
{
return Err(KnowledgeMapServiceError::InvalidRequest(format!(
"directory '{}' already exists",
directory.directory
)));
}
directories.push(directory);
directories.sort_by(|left, right| left.directory.cmp(&right.directory));
validate_directory_collection(map_type, directories, true)?;
Ok(())
}
fn update_directory_collection(
map_type: RepositoryMapType,
directories: &mut [RepositoryMapDirectory],
change: &RepositoryMapDirectoryChange,
) -> Result<String, KnowledgeMapServiceError> {
let name = change.directory.clone();
let entry = directories
.iter_mut()
.find(|entry| entry.directory == name)
.ok_or_else(|| {
KnowledgeMapServiceError::InvalidRequest(format!("directory '{name}' does not exist"))
})?;
if let Some(value) = &change.purpose {
entry.purpose = value.clone();
}
if let Some(value) = &change.content_scope {
entry.content_scope = value.clone();
}
if let Some(value) = &change.key_files {
entry.key_files = value.clone();
}
if let Some(value) = change.load_hint {
entry.load_hint = value;
}
if let Some(value) = &change.relations {
entry.relations = value.clone();
}
if let Some(value) = change.update_rule {
entry.update_rule = value;
}
validate_directory_collection(map_type, directories, true)?;
Ok(name)
}
fn remove_directory_from_collection(
map_type: RepositoryMapType,
directories: &mut Vec<RepositoryMapDirectory>,
directory: &str,
) -> Result<(), KnowledgeMapServiceError> {
let before = directories.len();
directories.retain(|entry| entry.directory != directory);
if before == directories.len() {
return Err(KnowledgeMapServiceError::InvalidRequest(format!(
"directory '{directory}' does not exist"
)));
}
validate_directory_collection(map_type, directories, true)?;
Ok(())
}
fn baseline_readme(map_type: RepositoryMapType, directory: &str) -> String {
let title = directory
.split('-')
.map(|part| {
let mut characters = part.chars();
characters.next().map_or_else(String::new, |first| {
first.to_uppercase().collect::<String>() + characters.as_str()
})
})
.collect::<Vec<_>>()
.join(" ");
let file_name = match map_type {
RepositoryMapType::Knowledge => KNOWLEDGE_MAP_FILE_NAME,
RepositoryMapType::Codespec => CODESPEC_MAP_FILE_NAME,
};
format!(
"# {title}\n\nThis directory is governed by `{}/{file_name}`. Update its map entry through `relay-knowledge map directory` and keep reviewed source material within the declared content scope.\n",
map_type.as_str()
)
}
#[cfg(test)]
#[path = "governance_tests.rs"]
mod tests;