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
11 changes: 6 additions & 5 deletions src/git/decision_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,23 @@ impl DecisionParser {
pub fn parse_message(&self, message: &str) -> Vec<Decision> {
let mut decisions = Vec::new();

let normalized = message.to_lowercase();
// Check if message contains decision keywords
let has_decision = self
.decision_keywords
.iter()
.any(|re| re.is_match(&message.to_lowercase()));
.any(|re| re.is_match(&normalized));

if !has_decision {
return decisions;
}

// Extract decision type
let decision_type = if message.contains("migrat") {
let decision_type = if normalized.contains("migrat") {
"migration"
} else if message.contains("arch:") || message.contains("architecture") {
} else if normalized.contains("arch:") || normalized.contains("architecture") {
"architecture"
} else if message.contains("refactor:") {
} else if normalized.contains("refactor:") {
"refactoring"
} else {
"general"
Expand Down Expand Up @@ -86,7 +87,7 @@ impl DecisionParser {

fn extract_migration_pair(message: &str) -> (Option<String>, String) {
// Pattern: "from X to Y" or "migrate from X to Y"
let from_to_re = Regex::new(r"from\s+(\w+)\s+to\s+(\w+)").unwrap();
let from_to_re = Regex::new(r"(?i)from\s+(\w+)\s+to\s+(\w+)").unwrap();

if let Some(caps) = from_to_re.captures(message) {
let from = caps.get(1).map(|m| m.as_str().to_string());
Expand Down
11 changes: 7 additions & 4 deletions src/graph/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ fn source_aliases(file_path: &str) -> Vec<String> {
) {
aliases.push(path.with_extension("").to_string_lossy().into_owned());
}
if matches!(extension, Some("ts" | "tsx" | "js"))
&& path.file_stem().and_then(|stem| stem.to_str()) == Some("index")
if (matches!(extension, Some("ts" | "tsx" | "js"))
&& path.file_stem().and_then(|stem| stem.to_str()) == Some("index"))
|| (extension == Some("rs")
&& path.file_stem().and_then(|stem| stem.to_str()) == Some("mod"))
{
if let Some(parent) = path.parent() {
aliases.push(parent.to_string_lossy().into_owned());
Expand Down Expand Up @@ -168,7 +170,7 @@ impl DependencyGraph {
return self.node_map.get(&with_ext).copied();
}
}
for ext in &["/index.ts", "/index.js", "/index.tsx"] {
for ext in &["/index.ts", "/index.js", "/index.tsx", "/mod.rs"] {
let with_index = format!("{}{}", resolved, ext);
if self.source_paths.contains(&with_index) {
return self.node_map.get(&with_index).copied();
Expand Down Expand Up @@ -315,7 +317,8 @@ impl DependencyGraph {
return normalize_path(&joined.to_string_lossy());
}

import_path.to_string()
let joined = source_dir.join(source_stem).join(import_path);
normalize_path(&joined.to_string_lossy())
}

/// What does this file depend on? (direct)
Expand Down
31 changes: 16 additions & 15 deletions src/sessions/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,11 @@ impl PatternLibrary {

/// Record an observation of a pattern key.
pub fn observe(&mut self, key: &str) {
*self.observations.entry(key.to_string()).or_insert(0) += 1;
}

/// Record an observation with an example.
pub fn observe_with_example(&mut self, key: &str, example: &str) {
self.observe(key);

let count = *self.observations.get(key).unwrap_or(&1);
let count = self.observations.entry(key.to_string()).or_insert(0);
*count += 1;
let count = *count;
let confidence = Self::calc_confidence(count);

if let Some(pattern) = self.patterns.iter_mut().find(|p| p.key == key) {
if pattern.examples.len() < 10 {
pattern.examples.push(example.to_string());
}
pattern.observation_count = count;
pattern.confidence = confidence;
} else {
Expand All @@ -67,13 +58,23 @@ impl PatternLibrary {
pattern_type,
key: key.to_string(),
description,
observation_count: 1,
confidence: 0.1,
examples: vec![example.to_string()],
observation_count: count,
confidence,
examples: Vec::new(),
});
}
}

/// Record an observation with an example.
pub fn observe_with_example(&mut self, key: &str, example: &str) {
self.observe(key);
if let Some(pattern) = self.patterns.iter_mut().find(|p| p.key == key) {
if pattern.examples.len() < 10 {
pattern.examples.push(example.to_string());
}
}
}

fn classify_pattern(&self, key: &str) -> (PatternType, String) {
if key.starts_with("naming:") {
(
Expand Down
49 changes: 49 additions & 0 deletions tests/nov2_regressions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use code_memory::git::decision_parser::DecisionParser;
use code_memory::graph::analyzer::DependencyGraph;
use code_memory::sessions::PatternLibrary;

#[test]
fn migration_keywords_are_case_insensitive_without_losing_technology_case() {
let decisions = DecisionParser::new()
.parse_message("Decided to Migrate FROM REST TO GraphQL\nKeep typed queries.");
assert_eq!(decisions.len(), 1);
assert_eq!(decisions[0].decision_type, "migration");
assert_eq!(decisions[0].from.as_deref(), Some("REST"));
assert_eq!(decisions[0].to, "GraphQL");
}

#[test]
fn plain_observations_are_visible_to_pattern_queries() {
let mut library = PatternLibrary::new();
for _ in 0..6 {
library.observe("naming:rust:snake_case");
}
let patterns = library.confident_patterns(0.5);
assert_eq!(patterns.len(), 1);
assert_eq!(patterns[0].observation_count, 6);
assert!(patterns[0].examples.is_empty());
library.observe_with_example("naming:rust:snake_case", "fn hello_world()");
assert_eq!(library.all_patterns()[0].observation_count, 7);
assert_eq!(library.all_patterns()[0].examples, ["fn hello_world()"]);
}

#[test]
fn rust_child_modules_resolve_in_flat_and_directory_layouts() {
for (parent, child) in [
("src/foo.rs", "src/foo/child.rs"),
("src/foo/mod.rs", "src/foo/child.rs"),
("src/lib.rs", "src/child/mod.rs"),
] {
for child_first in [false, true] {
let mut graph = DependencyGraph::new();
if child_first {
graph.add_file(child, "pub fn helper() {}\n");
}
graph.add_file(parent, "mod child;\n");
if !child_first {
graph.add_file(child, "pub fn helper() {}\n");
}
assert_eq!(graph.depends_on(parent), [child], "{parent} -> {child}");
}
}
}
Loading