Skip to content

Commit d6b0154

Browse files
authored
fix: preserve learned patterns and source analysis semantics (#54)
Co-authored-by: Mark Stuart <mstuart@users.noreply.github.com>
1 parent 77ea9d3 commit d6b0154

4 files changed

Lines changed: 78 additions & 24 deletions

File tree

‎src/git/decision_parser.rs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,23 @@ impl DecisionParser {
3939
pub fn parse_message(&self, message: &str) -> Vec<Decision> {
4040
let mut decisions = Vec::new();
4141

42+
let normalized = message.to_lowercase();
4243
// Check if message contains decision keywords
4344
let has_decision = self
4445
.decision_keywords
4546
.iter()
46-
.any(|re| re.is_match(&message.to_lowercase()));
47+
.any(|re| re.is_match(&normalized));
4748

4849
if !has_decision {
4950
return decisions;
5051
}
5152

5253
// Extract decision type
53-
let decision_type = if message.contains("migrat") {
54+
let decision_type = if normalized.contains("migrat") {
5455
"migration"
55-
} else if message.contains("arch:") || message.contains("architecture") {
56+
} else if normalized.contains("arch:") || normalized.contains("architecture") {
5657
"architecture"
57-
} else if message.contains("refactor:") {
58+
} else if normalized.contains("refactor:") {
5859
"refactoring"
5960
} else {
6061
"general"
@@ -86,7 +87,7 @@ impl DecisionParser {
8687

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

9192
if let Some(caps) = from_to_re.captures(message) {
9293
let from = caps.get(1).map(|m| m.as_str().to_string());

‎src/graph/analyzer.rs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ fn source_aliases(file_path: &str) -> Vec<String> {
5151
) {
5252
aliases.push(path.with_extension("").to_string_lossy().into_owned());
5353
}
54-
if matches!(extension, Some("ts" | "tsx" | "js"))
55-
&& path.file_stem().and_then(|stem| stem.to_str()) == Some("index")
54+
if (matches!(extension, Some("ts" | "tsx" | "js"))
55+
&& path.file_stem().and_then(|stem| stem.to_str()) == Some("index"))
56+
|| (extension == Some("rs")
57+
&& path.file_stem().and_then(|stem| stem.to_str()) == Some("mod"))
5658
{
5759
if let Some(parent) = path.parent() {
5860
aliases.push(parent.to_string_lossy().into_owned());
@@ -168,7 +170,7 @@ impl DependencyGraph {
168170
return self.node_map.get(&with_ext).copied();
169171
}
170172
}
171-
for ext in &["/index.ts", "/index.js", "/index.tsx"] {
173+
for ext in &["/index.ts", "/index.js", "/index.tsx", "/mod.rs"] {
172174
let with_index = format!("{}{}", resolved, ext);
173175
if self.source_paths.contains(&with_index) {
174176
return self.node_map.get(&with_index).copied();
@@ -315,7 +317,8 @@ impl DependencyGraph {
315317
return normalize_path(&joined.to_string_lossy());
316318
}
317319

318-
import_path.to_string()
320+
let joined = source_dir.join(source_stem).join(import_path);
321+
normalize_path(&joined.to_string_lossy())
319322
}
320323

321324
/// What does this file depend on? (direct)

‎src/sessions/patterns.rs‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,11 @@ impl PatternLibrary {
4545

4646
/// Record an observation of a pattern key.
4747
pub fn observe(&mut self, key: &str) {
48-
*self.observations.entry(key.to_string()).or_insert(0) += 1;
49-
}
50-
51-
/// Record an observation with an example.
52-
pub fn observe_with_example(&mut self, key: &str, example: &str) {
53-
self.observe(key);
54-
55-
let count = *self.observations.get(key).unwrap_or(&1);
48+
let count = self.observations.entry(key.to_string()).or_insert(0);
49+
*count += 1;
50+
let count = *count;
5651
let confidence = Self::calc_confidence(count);
57-
5852
if let Some(pattern) = self.patterns.iter_mut().find(|p| p.key == key) {
59-
if pattern.examples.len() < 10 {
60-
pattern.examples.push(example.to_string());
61-
}
6253
pattern.observation_count = count;
6354
pattern.confidence = confidence;
6455
} else {
@@ -67,13 +58,23 @@ impl PatternLibrary {
6758
pattern_type,
6859
key: key.to_string(),
6960
description,
70-
observation_count: 1,
71-
confidence: 0.1,
72-
examples: vec![example.to_string()],
61+
observation_count: count,
62+
confidence,
63+
examples: Vec::new(),
7364
});
7465
}
7566
}
7667

68+
/// Record an observation with an example.
69+
pub fn observe_with_example(&mut self, key: &str, example: &str) {
70+
self.observe(key);
71+
if let Some(pattern) = self.patterns.iter_mut().find(|p| p.key == key) {
72+
if pattern.examples.len() < 10 {
73+
pattern.examples.push(example.to_string());
74+
}
75+
}
76+
}
77+
7778
fn classify_pattern(&self, key: &str) -> (PatternType, String) {
7879
if key.starts_with("naming:") {
7980
(

‎tests/nov2_regressions.rs‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use code_memory::git::decision_parser::DecisionParser;
2+
use code_memory::graph::analyzer::DependencyGraph;
3+
use code_memory::sessions::PatternLibrary;
4+
5+
#[test]
6+
fn migration_keywords_are_case_insensitive_without_losing_technology_case() {
7+
let decisions = DecisionParser::new()
8+
.parse_message("Decided to Migrate FROM REST TO GraphQL\nKeep typed queries.");
9+
assert_eq!(decisions.len(), 1);
10+
assert_eq!(decisions[0].decision_type, "migration");
11+
assert_eq!(decisions[0].from.as_deref(), Some("REST"));
12+
assert_eq!(decisions[0].to, "GraphQL");
13+
}
14+
15+
#[test]
16+
fn plain_observations_are_visible_to_pattern_queries() {
17+
let mut library = PatternLibrary::new();
18+
for _ in 0..6 {
19+
library.observe("naming:rust:snake_case");
20+
}
21+
let patterns = library.confident_patterns(0.5);
22+
assert_eq!(patterns.len(), 1);
23+
assert_eq!(patterns[0].observation_count, 6);
24+
assert!(patterns[0].examples.is_empty());
25+
library.observe_with_example("naming:rust:snake_case", "fn hello_world()");
26+
assert_eq!(library.all_patterns()[0].observation_count, 7);
27+
assert_eq!(library.all_patterns()[0].examples, ["fn hello_world()"]);
28+
}
29+
30+
#[test]
31+
fn rust_child_modules_resolve_in_flat_and_directory_layouts() {
32+
for (parent, child) in [
33+
("src/foo.rs", "src/foo/child.rs"),
34+
("src/foo/mod.rs", "src/foo/child.rs"),
35+
("src/lib.rs", "src/child/mod.rs"),
36+
] {
37+
for child_first in [false, true] {
38+
let mut graph = DependencyGraph::new();
39+
if child_first {
40+
graph.add_file(child, "pub fn helper() {}\n");
41+
}
42+
graph.add_file(parent, "mod child;\n");
43+
if !child_first {
44+
graph.add_file(child, "pub fn helper() {}\n");
45+
}
46+
assert_eq!(graph.depends_on(parent), [child], "{parent} -> {child}");
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)