Skip to content

Commit 3a7996c

Browse files
author
Alex
committed
Merge remote-tracking branch 'origin/main'
2 parents 07a6e65 + 8b3778f commit 3a7996c

6 files changed

Lines changed: 569 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/terraphim_dsm/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "terraphim_dsm"
3+
version.workspace = true
4+
edition.workspace = true
5+
description = "Semantic module grouping using Terraphim knowledge graphs (sentrux companion tool)"
6+
license = "MIT"
7+
repository = "https://github.com/terraphim/terraphim-ai"
8+
9+
[[bin]]
10+
name = "terraphim_dsm"
11+
path = "src/main.rs"
12+
13+
[dependencies]
14+
clap = { version = "4", features = ["derive"] }
15+
serde = { version = "1", features = ["derive"] }
16+
serde_json = "1"
17+
walkdir = "2"
18+
anyhow = "1"
19+
tracing = "0.1"
20+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
21+
log = "0.4"
22+
dirs = "5"
23+
24+
[dev-dependencies]
25+
tempfile = "3"
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
use anyhow::Result;
2+
use std::collections::HashMap;
3+
use std::path::PathBuf;
4+
use walkdir::WalkDir;
5+
6+
/// Knowledge Graph integration for semantic module labeling
7+
pub struct KnowledgeGraph {
8+
/// Map of term -> domain concept
9+
concepts: HashMap<String, Concept>,
10+
/// KG source directory
11+
#[allow(dead_code)]
12+
kg_path: PathBuf,
13+
}
14+
15+
#[derive(Debug, Clone)]
16+
pub struct Concept {
17+
pub name: String,
18+
#[allow(dead_code)]
19+
pub description: String,
20+
pub synonyms: Vec<String>,
21+
#[allow(dead_code)]
22+
pub related_concepts: Vec<String>,
23+
#[allow(dead_code)]
24+
pub category: String,
25+
}
26+
27+
impl KnowledgeGraph {
28+
pub fn new(kg_path: PathBuf) -> Self {
29+
Self {
30+
concepts: HashMap::new(),
31+
kg_path,
32+
}
33+
}
34+
35+
pub fn concept_count(&self) -> usize {
36+
self.concepts.len()
37+
}
38+
39+
/// Load knowledge graph from ~/.config/terraphim/kg/
40+
pub fn load_default() -> Result<Self> {
41+
let kg_path = dirs::home_dir()
42+
.unwrap_or_default()
43+
.join(".config/terraphim/kg");
44+
45+
let mut kg = Self::new(kg_path.clone());
46+
kg.load_from_directory(&kg_path)?;
47+
Ok(kg)
48+
}
49+
50+
/// Load all markdown files from KG directory
51+
pub fn load_from_directory(&mut self, path: &PathBuf) -> Result<()> {
52+
if !path.exists() {
53+
return Ok(());
54+
}
55+
56+
for entry in WalkDir::new(path)
57+
.follow_links(false)
58+
.into_iter()
59+
.filter_map(|e| e.ok())
60+
{
61+
let path = entry.path();
62+
if path.extension().map_or(false, |ext| ext == "md") {
63+
if let Ok(content) = std::fs::read_to_string(path) {
64+
let concept = self.parse_concept_file(path, &content);
65+
self.concepts.insert(concept.name.clone(), concept);
66+
}
67+
}
68+
}
69+
70+
Ok(())
71+
}
72+
73+
fn parse_concept_file(&self, path: &std::path::Path, content: &str) -> Concept {
74+
let name = path
75+
.file_stem()
76+
.unwrap_or_default()
77+
.to_string_lossy()
78+
.to_string();
79+
80+
let mut description = String::new();
81+
let mut synonyms = Vec::new();
82+
let mut related = Vec::new();
83+
let mut category = "general".to_string();
84+
85+
for line in content.lines() {
86+
let line = line.trim();
87+
88+
if line.starts_with("synonyms::") {
89+
synonyms = line
90+
.trim_start_matches("synonyms::")
91+
.split(',')
92+
.map(|s| s.trim().to_lowercase())
93+
.collect();
94+
} else if line.starts_with("## Related Concepts") {
95+
category = "related".to_string();
96+
} else if line.starts_with("-") && category == "related" {
97+
related.push(line.trim_start_matches("-").trim().to_string());
98+
} else if !line.is_empty() && !line.starts_with("#") {
99+
description.push_str(line);
100+
description.push(' ');
101+
}
102+
}
103+
104+
Concept {
105+
name: name.clone(),
106+
description: description.trim().to_string(),
107+
synonyms,
108+
related_concepts: related,
109+
category,
110+
}
111+
}
112+
113+
/// Match a module path against KG concepts
114+
pub fn match_module(&self, module_path: &str) -> Vec<&Concept> {
115+
let mut matches = Vec::new();
116+
let module_lower = module_path.to_lowercase();
117+
118+
for concept in self.concepts.values() {
119+
// Check if concept name appears in module path
120+
if module_lower.contains(&concept.name.to_lowercase()) {
121+
matches.push(concept);
122+
continue;
123+
}
124+
125+
// Check synonyms
126+
for synonym in &concept.synonyms {
127+
if module_lower.contains(synonym) {
128+
matches.push(concept);
129+
break;
130+
}
131+
}
132+
}
133+
134+
matches
135+
}
136+
137+
/// Get domain category for a module
138+
pub fn get_module_category(&self, module_path: &str) -> String {
139+
let matches = self.match_module(module_path);
140+
141+
if matches.is_empty() {
142+
return "uncategorized".to_string();
143+
}
144+
145+
// Return the first matched concept's name as category
146+
matches[0].name.clone()
147+
}
148+
149+
/// Group modules by domain concept
150+
pub fn group_by_concept(&self, modules: &[String]) -> HashMap<String, Vec<String>> {
151+
let mut groups: HashMap<String, Vec<String>> = HashMap::new();
152+
153+
for module in modules {
154+
let category = self.get_module_category(module);
155+
groups.entry(category).or_default().push(module.clone());
156+
}
157+
158+
groups
159+
}
160+
161+
/// Check if two modules are semantically related
162+
#[allow(dead_code)]
163+
pub fn are_related(&self, module_a: &str, module_b: &str) -> bool {
164+
let concepts_a = self.match_module(module_a);
165+
let concepts_b = self.match_module(module_b);
166+
167+
// Check if they share any concepts
168+
for ca in &concepts_a {
169+
for cb in &concepts_b {
170+
if ca.name == cb.name {
171+
return true;
172+
}
173+
// Check related concepts
174+
if ca.related_concepts.contains(&cb.name) {
175+
return true;
176+
}
177+
}
178+
}
179+
180+
false
181+
}
182+
}
183+
184+
#[cfg(test)]
185+
mod tests {
186+
use super::*;
187+
use std::io::Write;
188+
use tempfile::TempDir;
189+
190+
#[test]
191+
fn test_knowledge_graph_loading() {
192+
let temp_dir = TempDir::new().unwrap();
193+
let kg_dir = temp_dir.path().join("kg");
194+
std::fs::create_dir(&kg_dir).unwrap();
195+
196+
// Create a test concept file
197+
let mut concept_file = std::fs::File::create(kg_dir.join("Authentication.md")).unwrap();
198+
writeln!(concept_file, "# Authentication").unwrap();
199+
writeln!(concept_file, "").unwrap();
200+
writeln!(concept_file, "Authentication and authorization concepts").unwrap();
201+
writeln!(concept_file, "").unwrap();
202+
writeln!(concept_file, "synonyms:: auth, login, identity").unwrap();
203+
writeln!(concept_file, "").unwrap();
204+
writeln!(concept_file, "## Related Concepts").unwrap();
205+
writeln!(concept_file, "- Security").unwrap();
206+
writeln!(concept_file, "- Identity").unwrap();
207+
208+
let mut kg = KnowledgeGraph::new(kg_dir.clone());
209+
kg.load_from_directory(&kg_dir).unwrap();
210+
211+
assert_eq!(kg.concepts.len(), 1);
212+
assert!(kg.concepts.contains_key("Authentication"));
213+
}
214+
215+
#[test]
216+
fn test_module_matching() {
217+
let temp_dir = TempDir::new().unwrap();
218+
let kg_dir = temp_dir.path().join("kg");
219+
std::fs::create_dir(&kg_dir).unwrap();
220+
221+
let mut concept_file = std::fs::File::create(kg_dir.join("Authentication.md")).unwrap();
222+
writeln!(concept_file, "# Authentication").unwrap();
223+
writeln!(concept_file, "synonyms:: auth, login").unwrap();
224+
225+
let mut kg = KnowledgeGraph::new(kg_dir.clone());
226+
kg.load_from_directory(&kg_dir).unwrap();
227+
228+
let matches = kg.match_module("terraphim_service::auth_handler");
229+
assert_eq!(matches.len(), 1);
230+
231+
let matches = kg.match_module("terraphim_service::login");
232+
assert_eq!(matches.len(), 1);
233+
234+
let matches = kg.match_module("terraphim_service::unrelated");
235+
assert_eq!(matches.len(), 0);
236+
}
237+
238+
#[test]
239+
fn test_semantic_relationship() {
240+
let temp_dir = TempDir::new().unwrap();
241+
let kg_dir = temp_dir.path().join("kg");
242+
std::fs::create_dir(&kg_dir).unwrap();
243+
244+
let mut auth_file = std::fs::File::create(kg_dir.join("Authentication.md")).unwrap();
245+
writeln!(auth_file, "# Authentication").unwrap();
246+
writeln!(auth_file, "synonyms:: auth").unwrap();
247+
writeln!(auth_file, "## Related Concepts").unwrap();
248+
writeln!(auth_file, "- Security").unwrap();
249+
250+
let mut sec_file = std::fs::File::create(kg_dir.join("Security.md")).unwrap();
251+
writeln!(sec_file, "# Security").unwrap();
252+
writeln!(sec_file, "synonyms:: security").unwrap();
253+
254+
let mut kg = KnowledgeGraph::new(kg_dir.clone());
255+
kg.load_from_directory(&kg_dir).unwrap();
256+
257+
assert!(kg.are_related("auth_module", "security_handler"));
258+
assert!(!kg.are_related("auth_module", "ui_component"));
259+
}
260+
}

0 commit comments

Comments
 (0)