Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ exclude = [
"desktop/src-tauri",
# Planned future use, not needed for workspace builds
"crates/terraphim_build_args",
"crates/terraphim-markdown-parser",
# Unused haystack providers (kept for future integration)
"crates/haystack_atlassian",
"crates/haystack_discourse",
Expand Down
76 changes: 76 additions & 0 deletions crates/terraphim-markdown-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,52 @@ use ulid::Ulid;

pub const TERRAPHIM_BLOCK_ID_PREFIX: &str = "terraphim:block-id:";

/// Extract the first H1 heading from markdown content using AST parsing.
///
/// Returns the heading text with original case preserved, or `None` if no
/// `# Heading` is found. Only matches depth-1 headings (`#`, not `##`).
pub fn extract_first_heading(content: &str) -> Option<String> {
let ast = markdown::to_mdast(content, &ParseOptions::gfm()).ok()?;
find_first_h1(&ast)
}

/// Walk the AST to find the first depth-1 heading and collect its text content.
fn find_first_h1(node: &Node) -> Option<String> {
match node {
Node::Heading(h) if h.depth == 1 => {
let text = collect_text_content(&h.children);
if text.is_empty() { None } else { Some(text) }
}
_ => {
if let Some(children) = children(node) {
for child in children {
if let Some(heading) = find_first_h1(child) {
return Some(heading);
}
}
}
None
}
}
}

/// Recursively collect all text content from AST nodes.
fn collect_text_content(nodes: &[Node]) -> String {
let mut text = String::new();
for node in nodes {
match node {
Node::Text(t) => text.push_str(&t.value),
Node::InlineCode(c) => text.push_str(&c.value),
other => {
if let Some(children) = children(other) {
text.push_str(&collect_text_content(children));
}
}
}
}
text
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockKind {
Paragraph,
Expand Down Expand Up @@ -555,4 +601,34 @@ mod tests {
let normalized = normalize_markdown(input).unwrap();
assert!(normalized.blocks.len() >= 2);
}

#[test]
fn extract_first_heading_h1() {
let input = "# Bun Package Manager\n\nsynonyms:: npm, yarn\n";
assert_eq!(
extract_first_heading(input),
Some("Bun Package Manager".to_string())
);
}

#[test]
fn extract_first_heading_skips_h2() {
let input = "## Not This\n\n# This One\n";
assert_eq!(extract_first_heading(input), Some("This One".to_string()));
}

#[test]
fn extract_first_heading_none_when_absent() {
let input = "Just some text\n\n## Only H2\n";
assert_eq!(extract_first_heading(input), None);
}

#[test]
fn extract_first_heading_with_inline_code() {
let input = "# The `bun` Runtime\n";
assert_eq!(
extract_first_heading(input),
Some("The bun Runtime".to_string())
);
}
}
14 changes: 9 additions & 5 deletions crates/terraphim_agent/src/shared_learning/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ use thiserror::Error;
use tokio::sync::RwLock;
use tracing::{debug, info};

use crate::shared_learning::types::{
LearningSource, SharedLearning, TrustLevel,
};
use crate::shared_learning::types::{LearningSource, SharedLearning, TrustLevel};

#[derive(Error, Debug)]
pub enum StoreError {
Expand Down Expand Up @@ -262,7 +260,10 @@ impl SharedLearningStore {

if let Some((existing_id, score)) = best_match {
if score >= self.config.similarity_threshold {
debug!("Merging with existing learning {} (score={:.3})", existing_id, score);
debug!(
"Merging with existing learning {} (score={:.3})",
existing_id, score
);
self.merge_learning(&existing_id, &learning).await?;
return Ok(StoreResult::Merged(existing_id));
}
Expand Down Expand Up @@ -626,7 +627,10 @@ mod tests {

store.insert(learning).await.unwrap();

let suggestions = store.suggest("git push problems", "test-agent", 5).await.unwrap();
let suggestions = store
.suggest("git push problems", "test-agent", 5)
.await
.unwrap();
assert!(!suggestions.is_empty());
assert_eq!(suggestions[0].title, "Git Push Error");
}
Expand Down
42 changes: 25 additions & 17 deletions crates/terraphim_agent/src/shared_learning/wiki_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,18 @@ impl GiteaWikiClient {
if exists {
// Update existing page
self.update_wiki_page(&page_name, &content).await?;
info!("Updated wiki page for learning {}: {}", learning.id, page_name);
info!(
"Updated wiki page for learning {}: {}",
learning.id, page_name
);
Ok(SyncResult::Updated(page_name))
} else {
// Create new page
self.create_wiki_page(&page_name, &content).await?;
info!("Created wiki page for learning {}: {}", learning.id, page_name);
info!(
"Created wiki page for learning {}: {}",
learning.id, page_name
);
Ok(SyncResult::Created(page_name))
}
}
Expand All @@ -155,7 +161,9 @@ impl GiteaWikiClient {
page_name,
])
.output()
.map_err(|e| WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e)))?;
.map_err(|e| {
WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e))
})?;

if output.status.success() {
Ok(true)
Expand All @@ -170,11 +178,7 @@ impl GiteaWikiClient {
}

/// Create a new wiki page
async fn create_wiki_page(
&self,
page_name: &str,
content: &str,
) -> Result<(), WikiSyncError> {
async fn create_wiki_page(&self, page_name: &str, content: &str) -> Result<(), WikiSyncError> {
let output = Command::new(&self.config.robot_path)
.env("GITEA_URL", &self.config.gitea_url)
.env("GITEA_TOKEN", &self.config.token)
Expand All @@ -192,7 +196,9 @@ impl GiteaWikiClient {
&format!("Add shared learning: {}", page_name),
])
.output()
.map_err(|e| WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e)))?;
.map_err(|e| {
WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e))
})?;

if output.status.success() {
Ok(())
Expand All @@ -207,11 +213,7 @@ impl GiteaWikiClient {
}

/// Update an existing wiki page
async fn update_wiki_page(
&self,
page_name: &str,
content: &str,
) -> Result<(), WikiSyncError> {
async fn update_wiki_page(&self, page_name: &str, content: &str) -> Result<(), WikiSyncError> {
let output = Command::new(&self.config.robot_path)
.env("GITEA_URL", &self.config.gitea_url)
.env("GITEA_TOKEN", &self.config.token)
Expand All @@ -229,7 +231,9 @@ impl GiteaWikiClient {
&format!("Update shared learning: {}", page_name),
])
.output()
.map_err(|e| WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e)))?;
.map_err(|e| {
WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e))
})?;

if output.status.success() {
Ok(())
Expand Down Expand Up @@ -258,7 +262,9 @@ impl GiteaWikiClient {
page_name,
])
.output()
.map_err(|e| WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e)))?;
.map_err(|e| {
WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e))
})?;

if output.status.success() {
info!("Deleted wiki page: {}", page_name);
Expand Down Expand Up @@ -299,7 +305,9 @@ impl GiteaWikiClient {
&self.config.repo,
])
.output()
.map_err(|e| WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e)))?;
.map_err(|e| {
WikiSyncError::GiteaRobot(format!("Failed to execute gitea-robot: {}", e))
})?;

if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
Expand Down
1 change: 1 addition & 0 deletions crates/terraphim_automata/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ readme = "README.md"

[dependencies]
terraphim_types = { path = "../terraphim_types", version = "1.0.0" }
terraphim-markdown-parser = { path = "../terraphim-markdown-parser", version = "1.0.0" }

ahash = { version = "0.8.6", features = ["serde"] }
aho-corasick = "1.0.2"
Expand Down
12 changes: 9 additions & 3 deletions crates/terraphim_automata/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,17 @@ fn concept_from_path(path: PathBuf) -> Result<ConceptWithDisplay> {
let stem = path.file_stem().ok_or(BuilderError::Indexation(format!(
"No file stem in path {path:?}"
)))?;
let original_name = stem.to_string_lossy().to_string();
let concept = Concept::from(original_name.clone());
let stem_name = stem.to_string_lossy().to_string();

// Use heading from markdown directives (parsed when the file is first read).
// Falls back to file stem if directives are unavailable for this path.
let display_name = crate::markdown_directives::extract_heading_from_path(&path)
.unwrap_or_else(|| stem_name.clone());

let concept = Concept::from(stem_name);
Ok(ConceptWithDisplay {
concept,
display_name: original_name,
display_name,
})
}

Expand Down
Loading
Loading