1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
//! Integration tests for improved responsibility clustering (Spec 192)
//!
//! Validates that the new clustering module achieves <5% unclustered rate
//! and integrates properly with god object detection.
use debtmap::extraction::adapters::god_object::analyze_god_objects;
use debtmap::extraction::UnifiedFileExtractor;
use std::path::Path;
/// Test that clustering achieves <5% unclustered rate on god_object/detector.rs
///
/// Spec 192 requires: clustering should achieve ≤5% unclustered rate for large files
/// with 20+ methods to demonstrate effective behavioral decomposition.
/// Spec 262: Updated to use detector.rs after recommender.rs was removed.
#[test]
fn test_clustering_on_god_object_detector() {
let source_code = std::fs::read_to_string("src/organization/god_object/detector.rs")
.expect("Failed to read detector.rs");
let path = Path::new("src/organization/god_object/detector.rs");
let extracted = UnifiedFileExtractor::extract(path, &source_code)
.expect("Failed to extract recommender.rs");
let analyses = analyze_god_objects(path, &extracted);
// If no god objects detected, the test passes
if analyses.is_empty() {
println!("No god objects detected - test passes");
return;
}
let analysis = &analyses[0];
// Count total methods in all splits
let total_split_methods: usize = analysis
.recommended_splits
.iter()
.map(|s| s.method_count)
.sum();
// If there are no recommended splits, the file might not be classified as a god object
// In that case, the test passes (no unclustered methods)
if total_split_methods == 0 {
println!("No splits recommended - file not classified as god object");
return;
}
// Count methods in "unclustered" or "utilities" categories (indicates poor clustering)
let unclustered_methods: usize = analysis
.recommended_splits
.iter()
.filter(|s| {
let name_lower = s.suggested_name.to_lowercase();
let resp_lower = s.responsibility.to_lowercase();
name_lower.contains("utilities")
|| name_lower.contains("unclustered")
|| name_lower.contains("misc")
|| resp_lower.contains("utilities")
|| resp_lower.contains("unclustered")
|| resp_lower.contains("misc")
|| s.cluster_quality
.as_ref()
.map(|q| !q.is_acceptable())
.unwrap_or(false)
})
.map(|s| s.method_count)
.sum();
let unclustered_rate = if total_split_methods > 0 {
(unclustered_methods as f64) / (total_split_methods as f64)
} else {
0.0
};
println!("Clustering results for god_object/recommender.rs:");
println!(" Total methods in splits: {}", total_split_methods);
println!(" Unclustered methods: {}", unclustered_methods);
println!(" Unclustered rate: {:.1}%", unclustered_rate * 100.0);
println!(
" Number of clusters: {}",
analysis.recommended_splits.len()
);
// Print cluster quality details
for split in &analysis.recommended_splits {
if let Some(quality) = &split.cluster_quality {
println!(
" - {} ({} methods): coherence={:.2}, separation={:.2}, silhouette={:.2}",
split.suggested_name,
split.method_count,
quality.internal_coherence,
quality.external_separation,
quality.silhouette_score
);
} else {
println!(
" - {} ({} methods): no quality metrics",
split.suggested_name, split.method_count
);
}
}
// REQUIREMENT: <5% unclustered rate (Spec 192)
// Only enforce this if splits were recommended
if total_split_methods > 0 {
assert!(
unclustered_rate < 0.05,
"Unclustered rate {:.1}% exceeds 5% threshold. \
Expected high-quality clustering with coherent behavioral groups.",
unclustered_rate * 100.0
);
}
// REQUIREMENT: At least 2 distinct clusters (no single mega-cluster)
// Only enforce if the file is actually a god object requiring splits
if analysis.recommended_splits.len() == 1 {
let split = &analysis.recommended_splits[0];
// If there's only 1 cluster and it's "unclassified", the file might not be a god object
let name_lower = split.suggested_name.to_lowercase();
if name_lower.contains("unclassified") || name_lower.contains("module") {
println!(
"File not classified as god object (single unclassified cluster) - test passes"
);
return;
}
}
// If we get here, we expect multiple clusters
if total_split_methods > 0 {
assert!(
analysis.recommended_splits.len() >= 2,
"Expected at least 2 coherent clusters for god object, found {}",
analysis.recommended_splits.len()
);
}
}
/// Test that all clusters have acceptable quality metrics
/// Spec 262: Updated to use detector.rs after recommender.rs was removed.
#[test]
fn test_cluster_quality_metrics() {
let source_code = std::fs::read_to_string("src/organization/god_object/detector.rs")
.expect("Failed to read detector.rs");
let path = Path::new("src/organization/god_object/detector.rs");
let extracted = UnifiedFileExtractor::extract(path, &source_code)
.expect("Failed to extract recommender.rs");
let analyses = analyze_god_objects(path, &extracted);
if analyses.is_empty() {
println!("No god objects detected - skipping quality check");
return;
}
let analysis = &analyses[0];
if analysis.recommended_splits.is_empty() {
println!("No splits recommended - skipping quality check");
return;
}
// All clusters with quality metrics should have acceptable quality
let clusters_with_quality: Vec<_> = analysis
.recommended_splits
.iter()
.filter(|s| s.cluster_quality.is_some())
.collect();
if clusters_with_quality.is_empty() {
println!("No clusters with quality metrics - may be using legacy clustering");
return;
}
println!("\nCluster quality validation:");
for split in &clusters_with_quality {
if let Some(quality) = &split.cluster_quality {
println!(
" {} ({} methods): {}",
split.suggested_name,
split.method_count,
quality.quality_description()
);
// REQUIREMENT: Internal coherence > 0.5 (Spec 192)
assert!(
quality.internal_coherence > 0.5,
"Cluster '{}' has low internal coherence: {:.2} (threshold: 0.5)",
split.suggested_name,
quality.internal_coherence
);
// REQUIREMENT: Silhouette score > 0.4 for good clusters (Spec 192)
// Note: Some clusters may have 0.2-0.4 (fair) which is acceptable
if quality.silhouette_score < 0.2 {
panic!(
"Cluster '{}' has poor silhouette score: {:.2} (minimum: 0.2)",
split.suggested_name, quality.silhouette_score
);
}
}
}
println!(
"✓ All {} clusters meet quality thresholds",
clusters_with_quality.len()
);
}
/// Test that clustering is deterministic (same input → same output)
///
/// FIXME: This test is currently flaky (~50% failure rate) due to non-determinism
/// in the hierarchical clustering algorithm. Root causes:
///
/// 1. **Similarity matrix index invalidation**: The similarity matrix is built once
/// using initial cluster indices (0, 1, 2, 3...). As clusters merge and are removed
/// from the vector, indices shift, but the matrix still uses old indices. This causes
/// incorrect similarity lookups and non-deterministic merge decisions.
///
/// 2. **Floating-point tie-breaking**: When multiple cluster pairs have nearly identical
/// similarity scores (within epsilon), the merge order can vary due to rounding errors
/// in the similarity calculations, even with epsilon-based comparison.
///
/// Partial fixes applied:
/// - HashMap → BTreeMap conversions for deterministic iteration
/// - Epsilon-based floating-point comparison (ε = 1e-10)
/// - Deterministic tie-breaking using lexicographic index ordering
///
/// Required fixes:
/// - Rebuild similarity matrix after each merge (performance cost), OR
/// - Use stable cluster IDs instead of vector indices throughout the algorithm
///
/// Spec 262: Updated to use detector.rs after recommender.rs was removed.
#[test]
#[ignore = "Flaky test due to hierarchical clustering non-determinism - see FIXME comment"]
fn test_clustering_determinism() {
let source_code = std::fs::read_to_string("src/organization/god_object/detector.rs")
.expect("Failed to read detector.rs");
let path = Path::new("src/organization/god_object/detector.rs");
let extracted = UnifiedFileExtractor::extract(path, &source_code)
.expect("Failed to extract recommender.rs");
// Run clustering twice
let analyses1 = analyze_god_objects(path, &extracted);
let analyses2 = analyze_god_objects(path, &extracted);
if analyses1.is_empty() || analyses2.is_empty() {
println!("No god objects detected - test passes");
return;
}
let analysis1 = &analyses1[0];
let analysis2 = &analyses2[0];
// Should produce identical results
assert_eq!(
analysis1.recommended_splits.len(),
analysis2.recommended_splits.len(),
"Clustering should be deterministic"
);
// Check that split names and method counts match
for (split1, split2) in analysis1
.recommended_splits
.iter()
.zip(analysis2.recommended_splits.iter())
{
assert_eq!(
split1.suggested_name, split2.suggested_name,
"Split names should be identical across runs"
);
assert_eq!(
split1.method_count, split2.method_count,
"Method counts should be identical across runs"
);
}
println!("✓ Clustering is deterministic");
}