stemedb/applications/aphoria/src/walker/path_mapper.rs
jordan 2b0923f20e feat: Distributed replication foundation (Phase 6A) - HLC, Merkle trees, CRDT stores, sync protocol
- 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>
2026-02-02 19:31:54 -07:00

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"]);
}
}