use std::collections::HashMap;
use crate::graph::CodeGraph;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContextRole {
Source,
Target,
Reference,
Comparison,
}
impl ContextRole {
pub fn parse_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"source" => Some(Self::Source),
"target" => Some(Self::Target),
"reference" => Some(Self::Reference),
"comparison" => Some(Self::Comparison),
_ => None,
}
}
pub fn label(&self) -> &str {
match self {
Self::Source => "source",
Self::Target => "target",
Self::Reference => "reference",
Self::Comparison => "comparison",
}
}
}
impl std::fmt::Display for ContextRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug)]
pub struct CodebaseContext {
pub id: String,
pub role: ContextRole,
pub path: String,
pub language: Option<String>,
pub graph: CodeGraph,
}
#[derive(Debug)]
pub struct Workspace {
pub id: String,
pub name: String,
pub contexts: Vec<CodebaseContext>,
pub created_at: u64,
}
#[derive(Debug)]
pub struct CrossContextResult {
pub context_id: String,
pub context_role: ContextRole,
pub matches: Vec<SymbolMatch>,
}
#[derive(Debug)]
pub struct SymbolMatch {
pub unit_id: u64,
pub name: String,
pub qualified_name: String,
pub unit_type: String,
pub file_path: String,
}
#[derive(Debug)]
pub struct Comparison {
pub symbol: String,
pub contexts: Vec<ContextComparison>,
pub semantic_match: f32,
pub structural_diff: Vec<String>,
}
#[derive(Debug)]
pub struct ContextComparison {
pub context_id: String,
pub role: ContextRole,
pub found: bool,
pub unit_type: Option<String>,
pub signature: Option<String>,
pub file_path: Option<String>,
}
#[derive(Debug)]
pub struct CrossReference {
pub symbol: String,
pub found_in: Vec<(String, ContextRole)>,
pub missing_from: Vec<(String, ContextRole)>,
}
#[derive(Debug)]
pub struct WorkspaceManager {
workspaces: HashMap<String, Workspace>,
active: Option<String>,
next_id: u64,
}
impl WorkspaceManager {
pub fn new() -> Self {
Self {
workspaces: HashMap::new(),
active: None,
next_id: 1,
}
}
pub fn create(&mut self, name: &str) -> String {
let id = format!("ws-{}", self.next_id);
self.next_id += 1;
let workspace = Workspace {
id: id.clone(),
name: name.to_string(),
contexts: Vec::new(),
created_at: crate::types::now_micros(),
};
self.workspaces.insert(id.clone(), workspace);
self.active = Some(id.clone());
id
}
pub fn add_context(
&mut self,
workspace_id: &str,
path: &str,
role: ContextRole,
language: Option<String>,
graph: CodeGraph,
) -> Result<String, String> {
let ctx_id = format!("ctx-{}", self.next_id);
self.next_id += 1;
let workspace = self
.workspaces
.get_mut(workspace_id)
.ok_or_else(|| format!("workspace '{}' not found", workspace_id))?;
workspace.contexts.push(CodebaseContext {
id: ctx_id.clone(),
role,
path: path.to_string(),
language,
graph,
});
Ok(ctx_id)
}
pub fn list(&self, workspace_id: &str) -> Result<&Workspace, String> {
self.workspaces
.get(workspace_id)
.ok_or_else(|| format!("workspace '{}' not found", workspace_id))
}
pub fn get_active(&self) -> Option<&str> {
self.active.as_deref()
}
pub fn query_all(
&self,
workspace_id: &str,
query: &str,
) -> Result<Vec<CrossContextResult>, String> {
let workspace = self
.workspaces
.get(workspace_id)
.ok_or_else(|| format!("workspace '{}' not found", workspace_id))?;
let query_lower = query.to_lowercase();
let mut results = Vec::new();
for ctx in &workspace.contexts {
let matches = Self::search_graph(&ctx.graph, &query_lower);
if !matches.is_empty() {
results.push(CrossContextResult {
context_id: ctx.id.clone(),
context_role: ctx.role.clone(),
matches,
});
}
}
Ok(results)
}
pub fn query_context(
&self,
workspace_id: &str,
context_id: &str,
query: &str,
) -> Result<Vec<SymbolMatch>, String> {
let workspace = self
.workspaces
.get(workspace_id)
.ok_or_else(|| format!("workspace '{}' not found", workspace_id))?;
let ctx = workspace
.contexts
.iter()
.find(|c| c.id == context_id)
.ok_or_else(|| {
format!(
"context '{}' not found in workspace '{}'",
context_id, workspace_id
)
})?;
let query_lower = query.to_lowercase();
Ok(Self::search_graph(&ctx.graph, &query_lower))
}
pub fn compare(&self, workspace_id: &str, symbol: &str) -> Result<Comparison, String> {
let workspace = self
.workspaces
.get(workspace_id)
.ok_or_else(|| format!("workspace '{}' not found", workspace_id))?;
let symbol_lower = symbol.to_lowercase();
let mut ctx_comparisons = Vec::new();
let mut structural_diff = Vec::new();
let mut first_sig: Option<String> = None;
let mut first_type: Option<String> = None;
for ctx in &workspace.contexts {
let unit = ctx
.graph
.units()
.iter()
.find(|u| u.name.to_lowercase() == symbol_lower);
match unit {
Some(u) => {
let sig = u.signature.clone();
let utype = u.unit_type.label().to_string();
let fpath = u.file_path.display().to_string();
if let Some(ref first) = first_sig {
if sig.as_deref().unwrap_or("") != first.as_str() {
structural_diff.push(format!(
"signature differs in {}: '{}' vs '{}'",
ctx.id,
sig.as_deref().unwrap_or("<none>"),
first,
));
}
} else {
first_sig = Some(sig.as_deref().unwrap_or("").to_string());
}
if let Some(ref first) = first_type {
if utype != *first {
structural_diff.push(format!(
"type differs in {}: '{}' vs '{}'",
ctx.id, utype, first,
));
}
} else {
first_type = Some(utype.clone());
}
ctx_comparisons.push(ContextComparison {
context_id: ctx.id.clone(),
role: ctx.role.clone(),
found: true,
unit_type: Some(utype),
signature: sig,
file_path: Some(fpath),
});
}
None => {
ctx_comparisons.push(ContextComparison {
context_id: ctx.id.clone(),
role: ctx.role.clone(),
found: false,
unit_type: None,
signature: None,
file_path: None,
});
}
}
}
Ok(Comparison {
symbol: symbol.to_string(),
contexts: ctx_comparisons,
semantic_match: 0.0, structural_diff,
})
}
pub fn cross_reference(
&self,
workspace_id: &str,
symbol: &str,
) -> Result<CrossReference, String> {
let workspace = self
.workspaces
.get(workspace_id)
.ok_or_else(|| format!("workspace '{}' not found", workspace_id))?;
let symbol_lower = symbol.to_lowercase();
let mut found_in = Vec::new();
let mut missing_from = Vec::new();
for ctx in &workspace.contexts {
let exists = ctx
.graph
.units()
.iter()
.any(|u| u.name.to_lowercase() == symbol_lower);
if exists {
found_in.push((ctx.id.clone(), ctx.role.clone()));
} else {
missing_from.push((ctx.id.clone(), ctx.role.clone()));
}
}
Ok(CrossReference {
symbol: symbol.to_string(),
found_in,
missing_from,
})
}
fn search_graph(graph: &CodeGraph, query_lower: &str) -> Vec<SymbolMatch> {
graph
.units()
.iter()
.filter(|u| u.name.to_lowercase().contains(query_lower))
.map(|u| SymbolMatch {
unit_id: u.id,
name: u.name.clone(),
qualified_name: u.qualified_name.clone(),
unit_type: u.unit_type.label().to_string(),
file_path: u.file_path.display().to_string(),
})
.collect()
}
}
impl Default for WorkspaceManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{CodeUnit, CodeUnitType, Language, Span};
use std::path::PathBuf;
fn make_graph(name: &str, sig: Option<&str>) -> CodeGraph {
let mut g = CodeGraph::with_default_dimension();
let mut unit = CodeUnit::new(
CodeUnitType::Function,
Language::Rust,
name.to_string(),
format!("crate::{}", name),
PathBuf::from(format!("src/{}.rs", name)),
Span::new(1, 0, 10, 0),
);
if let Some(s) = sig {
unit.signature = Some(s.to_string());
}
g.add_unit(unit);
g
}
#[test]
fn create_workspace_sets_active() {
let mut mgr = WorkspaceManager::new();
let id = mgr.create("test-ws");
assert_eq!(mgr.get_active(), Some(id.as_str()));
}
#[test]
fn add_context_and_list() {
let mut mgr = WorkspaceManager::new();
let ws = mgr.create("migration");
let ctx = mgr
.add_context(
&ws,
"/src/cpp",
ContextRole::Source,
Some("C++".into()),
make_graph("foo", None),
)
.unwrap();
assert!(ctx.starts_with("ctx-"));
let workspace = mgr.list(&ws).unwrap();
assert_eq!(workspace.contexts.len(), 1);
assert_eq!(workspace.contexts[0].role, ContextRole::Source);
}
#[test]
fn query_all_finds_symbol() {
let mut mgr = WorkspaceManager::new();
let ws = mgr.create("q");
mgr.add_context(
&ws,
"/a",
ContextRole::Source,
None,
make_graph("process", None),
)
.unwrap();
mgr.add_context(
&ws,
"/b",
ContextRole::Target,
None,
make_graph("other", None),
)
.unwrap();
let results = mgr.query_all(&ws, "proc").unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].matches[0].name, "process");
}
#[test]
fn query_context_single() {
let mut mgr = WorkspaceManager::new();
let ws = mgr.create("q2");
let ctx = mgr
.add_context(
&ws,
"/a",
ContextRole::Source,
None,
make_graph("alpha", None),
)
.unwrap();
let matches = mgr.query_context(&ws, &ctx, "alph").unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].name, "alpha");
}
#[test]
fn compare_detects_signature_diff() {
let mut mgr = WorkspaceManager::new();
let ws = mgr.create("cmp");
mgr.add_context(
&ws,
"/a",
ContextRole::Source,
None,
make_graph("foo", Some("(int) -> bool")),
)
.unwrap();
mgr.add_context(
&ws,
"/b",
ContextRole::Target,
None,
make_graph("foo", Some("(i32) -> bool")),
)
.unwrap();
let cmp = mgr.compare(&ws, "foo").unwrap();
assert_eq!(cmp.contexts.len(), 2);
assert!(cmp.contexts[0].found);
assert!(cmp.contexts[1].found);
assert!(!cmp.structural_diff.is_empty());
}
#[test]
fn cross_reference_found_and_missing() {
let mut mgr = WorkspaceManager::new();
let ws = mgr.create("xref");
mgr.add_context(
&ws,
"/a",
ContextRole::Source,
None,
make_graph("bar", None),
)
.unwrap();
mgr.add_context(
&ws,
"/b",
ContextRole::Target,
None,
make_graph("other", None),
)
.unwrap();
let xref = mgr.cross_reference(&ws, "bar").unwrap();
assert_eq!(xref.found_in.len(), 1);
assert_eq!(xref.missing_from.len(), 1);
}
#[test]
fn context_role_roundtrip() {
for label in &["source", "target", "reference", "comparison"] {
let role = ContextRole::parse_str(label).unwrap();
assert_eq!(role.label(), *label);
}
assert!(ContextRole::parse_str("invalid").is_none());
}
#[test]
fn workspace_not_found_error() {
let mgr = WorkspaceManager::new();
assert!(mgr.list("ws-999").is_err());
assert!(mgr.query_all("ws-999", "x").is_err());
}
}