feat: implement core architecture modules
Phase 1: Wiki-Link Graph Indexing - WikiLinkParser: extract [[links]] from markdown - WikiLinkGraph: BFS traversal, reachable docs, backlinks - Support relative path resolution (../../../) Phase 2: ScoringPipeline trait (SOLID design) - DocumentScorer trait: single interface for all scorers - GlobalTfIdfScorer, ProjectTfIdfScorer, SemanticScorer - MetadataBoostingScorer (decorator pattern) - ScoringPipeline: orchestrate multiple scorers with RRF fusion - Benefits: add new scorers without modifying existing code Phase 7: RBAC + PolicyProvider trait - PolicyProvider trait: pluggable backends (Vault, Postgres, Redis) - VaultPolicyProvider: load YAML from vault/projects/* and vault/shared/skills/* - MockPolicyProvider: for testing (no I/O) - AccessChecker trait: single-purpose RBAC checks - AccessLevelChecker, RoleChecker, PermissionChecker - AccessDecisionEngine: orchestrate checkers with short-circuit eval - AuditLogger trait: pluggable audit backends Test Fixtures (DRY principle) - OidcClaimsBuilder: fluent API for test data - AccessPolicyBuilder: fluent API for policies - MockPolicyProvider, MockAuditLogger: testing mocks All modules compile and unit tests pass.
This commit is contained in:
@@ -18,6 +18,7 @@ time = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
walkdir = "2.5"
|
||||
sha2 = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
time = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod reference_cycle_guard;
|
||||
pub mod optimizer_sink;
|
||||
pub mod optimizer_metrics;
|
||||
pub mod query_metrics;
|
||||
pub mod wiki_link;
|
||||
|
||||
pub use pi_session::PiSessionSource;
|
||||
pub use claude_transcript::ClaudeTranscriptSource;
|
||||
@@ -18,3 +19,4 @@ pub use query_metrics::{
|
||||
QueryMetrics, QueryMetricsRepository, ProgressSnapshot, MetricsSummary,
|
||||
OptimizationStatus, CompressorMetrics, ContentTypeMetrics,
|
||||
};
|
||||
pub use wiki_link::{WikiLink, WikiLinkParser, WikiLinkGraph, LinkType};
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/// Wiki-Link Graph: Extract and manage [[links]] between documents
|
||||
///
|
||||
/// Phase 1: Wiki-Link Graph Indexing
|
||||
///
|
||||
/// Used to scope queries to project namespaces and enable graph traversal.
|
||||
/// For example: poimen/tools/kubectl.md [[debugging.md]] creates an edge
|
||||
/// from tools/kubectl to debugging (within same project).
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use regex::Regex;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LinkType {
|
||||
Memory, // [[debugging.md]]
|
||||
Skill, // [[SKILL-kubernetes-debugging]]
|
||||
Shared, // [[../../shared/concepts/design-patterns.md]]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WikiLink {
|
||||
pub project: String,
|
||||
pub source_path: String, // e.g., "tools/kubectl.md"
|
||||
pub target_path: String, // e.g., "debugging.md"
|
||||
pub link_type: LinkType,
|
||||
pub resolved_path: Option<String>, // fully qualified
|
||||
}
|
||||
|
||||
/// Parser: Extract [[links]] from markdown
|
||||
pub struct WikiLinkParser;
|
||||
|
||||
impl WikiLinkParser {
|
||||
/// Extract all wiki-links from markdown content
|
||||
pub fn parse_links(content: &str) -> Result<Vec<String>> {
|
||||
let regex = Regex::new(r"\[\[([^\]]+)\]\]")?;
|
||||
let links = regex
|
||||
.captures_iter(content)
|
||||
.map(|cap| cap[1].trim().to_string())
|
||||
.collect();
|
||||
Ok(links)
|
||||
}
|
||||
|
||||
/// Infer link type from target path
|
||||
pub fn infer_link_type(target: &str) -> LinkType {
|
||||
if target.contains("SKILL-") {
|
||||
LinkType::Skill
|
||||
} else if target.contains("shared:") || target.starts_with("../../") {
|
||||
LinkType::Shared
|
||||
} else if target.ends_with(".md") || !target.contains("/") {
|
||||
LinkType::Memory
|
||||
} else {
|
||||
LinkType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve relative path to fully qualified path
|
||||
///
|
||||
/// Examples:
|
||||
/// - "debugging.md" from "tools/kubectl.md" -> "tools/debugging.md"
|
||||
/// - "../concepts/design.md" from "tools/kubectl.md" -> "concepts/design.md"
|
||||
/// - "../../shared/skills/SKILL-*" from "tools/kubectl.md" -> "shared/skills/SKILL-*"
|
||||
pub fn resolve_path(target: &str, source_dir: &Path) -> Result<PathBuf> {
|
||||
// If target starts with shared: prefix, it's absolute
|
||||
if target.starts_with("shared:") {
|
||||
return Ok(PathBuf::from(target.replace("shared:", "shared/")));
|
||||
}
|
||||
|
||||
// If target is just a filename, it's in the same dir as source
|
||||
if !target.contains("/") && !target.contains("..") {
|
||||
return Ok(source_dir.join(target));
|
||||
}
|
||||
|
||||
// Otherwise resolve relative path
|
||||
let resolved = source_dir.parent().unwrap_or_else(|| Path::new("")).join(target);
|
||||
Ok(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
/// Graph Index: Stores and queries wiki-link relationships
|
||||
pub struct WikiLinkGraph {
|
||||
/// Forward links: source -> [targets]
|
||||
forward_links: HashMap<String, Vec<String>>,
|
||||
/// Reverse links: target -> [sources] (for backlinks)
|
||||
backward_links: HashMap<String, Vec<String>>,
|
||||
/// Project scoping
|
||||
project: String,
|
||||
}
|
||||
|
||||
impl WikiLinkGraph {
|
||||
pub fn new(project: &str) -> Self {
|
||||
Self {
|
||||
forward_links: HashMap::new(),
|
||||
backward_links: HashMap::new(),
|
||||
project: project.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a wiki-link edge
|
||||
pub fn add_link(&mut self, source: &str, target: &str) {
|
||||
self.forward_links.entry(source.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(target.to_string());
|
||||
|
||||
self.backward_links.entry(target.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(source.to_string());
|
||||
}
|
||||
|
||||
/// Get all reachable documents from a starting point (BFS)
|
||||
pub fn reachable_docs(&self, start: &str) -> HashSet<String> {
|
||||
let mut visited = HashSet::new();
|
||||
let mut queue = vec![start.to_string()];
|
||||
|
||||
while let Some(current) = queue.pop() {
|
||||
if visited.contains(¤t) {
|
||||
continue;
|
||||
}
|
||||
visited.insert(current.clone());
|
||||
|
||||
if let Some(targets) = self.forward_links.get(¤t) {
|
||||
for target in targets {
|
||||
if !visited.contains(target) {
|
||||
queue.push(target.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visited
|
||||
}
|
||||
|
||||
/// Get backlinks (documents that link to this one)
|
||||
pub fn backlinks(&self, doc: &str) -> Vec<String> {
|
||||
self.backward_links
|
||||
.get(doc)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get forward links (documents this one links to)
|
||||
pub fn forward_links(&self, doc: &str) -> Vec<String> {
|
||||
self.forward_links
|
||||
.get(doc)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_wiki_links() {
|
||||
let content = r#"
|
||||
## Borrowing
|
||||
See [[lifetimes.md]] for more.
|
||||
Also check [[../../shared/skills/SKILL-ownership]]
|
||||
"#;
|
||||
|
||||
let links = WikiLinkParser::parse_links(content).unwrap();
|
||||
assert_eq!(links.len(), 2);
|
||||
assert!(links.contains(&"lifetimes.md".to_string()));
|
||||
assert!(links.contains(&"../../shared/skills/SKILL-ownership".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infer_link_type() {
|
||||
assert_eq!(WikiLinkParser::infer_link_type("debugging.md"), LinkType::Memory);
|
||||
assert_eq!(
|
||||
WikiLinkParser::infer_link_type("SKILL-kubernetes-debug"),
|
||||
LinkType::Skill
|
||||
);
|
||||
assert_eq!(
|
||||
WikiLinkParser::infer_link_type("../../shared/concepts/design.md"),
|
||||
LinkType::Shared
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_path_simple() {
|
||||
let source_dir = Path::new("poimen/tools");
|
||||
let resolved = WikiLinkParser::resolve_path("debugging.md", source_dir).unwrap();
|
||||
assert_eq!(resolved.file_name().unwrap(), "debugging.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_graph_reachable_docs() {
|
||||
let mut graph = WikiLinkGraph::new("poimen");
|
||||
|
||||
graph.add_link("index.md", "tools/kubectl.md");
|
||||
graph.add_link("tools/kubectl.md", "debugging/pod-crashes.md");
|
||||
graph.add_link("debugging/pod-crashes.md", "../memories/ownership.md");
|
||||
|
||||
let reachable = graph.reachable_docs("index.md");
|
||||
assert!(reachable.contains("index.md"));
|
||||
assert!(reachable.contains("tools/kubectl.md"));
|
||||
assert!(reachable.contains("debugging/pod-crashes.md"));
|
||||
assert!(reachable.contains("../memories/ownership.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_graph_backlinks() {
|
||||
let mut graph = WikiLinkGraph::new("poimen");
|
||||
|
||||
graph.add_link("tools/kubectl.md", "debugging.md");
|
||||
graph.add_link("tools/docker.md", "debugging.md");
|
||||
|
||||
let backlinks = graph.backlinks("debugging.md");
|
||||
assert_eq!(backlinks.len(), 2);
|
||||
assert!(backlinks.contains(&"tools/kubectl.md".to_string()));
|
||||
assert!(backlinks.contains(&"tools/docker.md".to_string()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user