- Add Hybrid Logical Clock (HLC) for causality tracking across nodes - Implement Merkle tree for efficient diff/sync with BLAKE3 hashing - Add CRDT-aware stores for assertions and votes with vector clocks - Create stemedb-sync crate with anti-entropy and gossip protocols - Add stemedb-rpc crate with gRPC sync service (proto definitions) - Implement SupersessionChain for tracking assertion lifecycles - Add Aphoria application for code analysis/reporting - Add battery11 replication test scaffolding - Fix .gitignore to exclude nested target directories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
197 lines
6.8 KiB
Rust
197 lines
6.8 KiB
Rust
//! Path mapping from file paths to ConceptPath segments.
|
|
#![allow(dead_code)]
|
|
|
|
use std::path::Path;
|
|
|
|
use crate::config::AphoriaConfig;
|
|
use crate::types::Language;
|
|
|
|
/// Maps file paths to ConceptPath segments.
|
|
pub struct PathMapper {
|
|
/// Project name.
|
|
project_name: String,
|
|
}
|
|
|
|
impl PathMapper {
|
|
/// Create a new path mapper for a project.
|
|
pub fn new(root: &Path, config: &AphoriaConfig) -> Self {
|
|
let project_name =
|
|
config.project.name.clone().or_else(|| detect_project_name(root)).unwrap_or_else(
|
|
|| root.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
|
|
);
|
|
|
|
Self { project_name }
|
|
}
|
|
|
|
/// Convert a relative file path to ConceptPath segments.
|
|
///
|
|
/// Language-specific stripping rules remove boilerplate directories.
|
|
pub fn to_segments(&self, relative_path: &str, language: Language) -> Vec<String> {
|
|
let mut segments = Vec::new();
|
|
|
|
// Add language prefix
|
|
let lang_prefix = match language {
|
|
Language::Rust | Language::CargoManifest => "rust",
|
|
Language::Go | Language::GoMod => "go",
|
|
Language::Python | Language::PythonManifest => "python",
|
|
Language::TypeScript => "typescript",
|
|
Language::JavaScript | Language::NpmManifest => "javascript",
|
|
Language::Yaml | Language::Toml | Language::Json | Language::Dotenv => "config",
|
|
Language::Docker => "docker",
|
|
Language::Unknown => "unknown",
|
|
};
|
|
segments.push(lang_prefix.to_string());
|
|
|
|
// Add project name
|
|
segments.push(self.project_name.clone());
|
|
|
|
// Process path components
|
|
let path = Path::new(relative_path);
|
|
let components: Vec<&str> =
|
|
path.components().filter_map(|c| c.as_os_str().to_str()).collect();
|
|
|
|
// Apply language-specific stripping
|
|
let stripped = strip_boilerplate(&components, language);
|
|
|
|
// Remove file extension from last component
|
|
if let Some((last, rest)) = stripped.split_last() {
|
|
for component in rest {
|
|
segments.push((*component).to_string());
|
|
}
|
|
// Strip extension
|
|
let stem = Path::new(last).file_stem().and_then(|s| s.to_str()).unwrap_or(last);
|
|
segments.push(stem.to_string());
|
|
}
|
|
|
|
segments
|
|
}
|
|
}
|
|
|
|
/// Strip boilerplate directories based on language conventions.
|
|
///
|
|
/// Removes common structural directories that don't add semantic meaning:
|
|
/// - Rust: `src/`, `crates/`
|
|
/// - Go: `cmd/`, `internal/`, `pkg/`
|
|
/// - Python: `src/`, `lib/`
|
|
/// - JS/TS: `src/`, `lib/`
|
|
fn strip_boilerplate<'a>(components: &'a [&'a str], language: Language) -> Vec<&'a str> {
|
|
let skip_dirs: &[&str] = match language {
|
|
Language::Rust | Language::CargoManifest => &["src", "crates"],
|
|
Language::Go | Language::GoMod => &["cmd", "internal", "pkg"],
|
|
Language::Python | Language::PythonManifest => &["src", "lib"],
|
|
Language::TypeScript | Language::JavaScript | Language::NpmManifest => &["src", "lib"],
|
|
_ => &[],
|
|
};
|
|
|
|
components.iter().filter(|c| !skip_dirs.contains(c)).copied().collect()
|
|
}
|
|
|
|
/// Detect project name from manifest files.
|
|
fn detect_project_name(root: &Path) -> Option<String> {
|
|
// Try Cargo.toml
|
|
if let Ok(content) = std::fs::read_to_string(root.join("Cargo.toml")) {
|
|
if let Ok(parsed) = content.parse::<toml::Table>() {
|
|
if let Some(package) = parsed.get("package").and_then(|p| p.as_table()) {
|
|
if let Some(name) = package.get("name").and_then(|n| n.as_str()) {
|
|
return Some(name.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try go.mod
|
|
if let Ok(content) = std::fs::read_to_string(root.join("go.mod")) {
|
|
for line in content.lines() {
|
|
if line.starts_with("module ") {
|
|
let module = line.trim_start_matches("module ").trim();
|
|
// Extract last segment of module path
|
|
return Some(module.rsplit('/').next().unwrap_or(module).to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try package.json
|
|
if let Ok(content) = std::fs::read_to_string(root.join("package.json")) {
|
|
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content) {
|
|
if let Some(name) = parsed.get("name").and_then(|n| n.as_str()) {
|
|
return Some(name.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn test_rust_path_mapping() {
|
|
let dir = TempDir::new().expect("create temp dir");
|
|
let config = AphoriaConfig {
|
|
project: crate::config::ProjectConfig {
|
|
name: Some("citadeldb".to_string()),
|
|
language: None,
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
let mapper = PathMapper::new(dir.path(), &config);
|
|
let segments = mapper.to_segments("crates/citadeldb/src/auth/jwt.rs", Language::Rust);
|
|
|
|
assert_eq!(segments, vec!["rust", "citadeldb", "citadeldb", "auth", "jwt"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_go_path_mapping() {
|
|
let dir = TempDir::new().expect("create temp dir");
|
|
let config = AphoriaConfig {
|
|
project: crate::config::ProjectConfig {
|
|
name: Some("myapp".to_string()),
|
|
language: None,
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
let mapper = PathMapper::new(dir.path(), &config);
|
|
let segments = mapper.to_segments("internal/auth/jwt/validator.go", Language::Go);
|
|
|
|
assert_eq!(segments, vec!["go", "myapp", "auth", "jwt", "validator"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_path_mapping() {
|
|
let dir = TempDir::new().expect("create temp dir");
|
|
let config = AphoriaConfig {
|
|
project: crate::config::ProjectConfig {
|
|
name: Some("myapp".to_string()),
|
|
language: None,
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
let mapper = PathMapper::new(dir.path(), &config);
|
|
let segments = mapper.to_segments("config/production.yaml", Language::Yaml);
|
|
|
|
assert_eq!(segments, vec!["config", "myapp", "config", "production"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_strip_boilerplate() {
|
|
let components = vec!["src", "auth", "jwt.rs"];
|
|
let result = strip_boilerplate(&components, Language::Rust);
|
|
assert_eq!(result, vec!["auth", "jwt.rs"]);
|
|
|
|
// Multiple boilerplate dirs (crates/xxx/src/)
|
|
let components = vec!["crates", "mylib", "src", "auth", "jwt.rs"];
|
|
let result = strip_boilerplate(&components, Language::Rust);
|
|
assert_eq!(result, vec!["mylib", "auth", "jwt.rs"]);
|
|
|
|
let components = vec!["internal", "auth", "jwt", "validator.go"];
|
|
let result = strip_boilerplate(&components, Language::Go);
|
|
assert_eq!(result, vec!["auth", "jwt", "validator.go"]);
|
|
}
|
|
}
|