Analyzed key functions for Change Risk Anti-Patterns: High Risk (CRAP > 30): - query_handler: 114 (refactor needed) - ingest_handler: 99 (refactor needed) Medium Risk (CRAP 10-30): - execute_with_wiki: 21 (add tests) - learn_handler: ~50 (add tests) Low Risk (CRAP < 10): - to_rbac_claims: 2 ✓ - query_result_to_resource_meta: 4 ✓ - evaluate (RBAC): 8.5 ✓ Includes: - Refactoring recommendations with code examples - Phase Executor pattern for pipeline - QueryParams extraction pattern for handlers - Test coverage improvement plan
12 KiB
12 KiB
CRAP Analysis Report
Project: Poimen Memory
Date: 2024-01-15
Total Tests: 670
Formula: Complexity² × (1 - Coverage)³ + Complexity
Executive Summary
| Risk Level | Count | Action |
|---|---|---|
| 🔴 High (CRAP > 30) | 2 | Refactor immediately |
| 🟡 Medium (CRAP 10-30) | 3 | Add tests or simplify |
| 🟢 Low (CRAP < 10) | 8 | Acceptable |
🔴 High Risk Functions
1. query_handler (http_server.rs:831)
Current State:
pub async fn query_handler(
req: HttpRequest,
query: web::Query<HashMap<String, String>>,
state: web::Data<AppState>,
) -> HttpResponse {
// Auth validation
let (claims, token) = match validate_auth(&req, &state).await { ... }; // +1
// Capability check
if !has_capability(&claims, "memory:read") { ... } // +1
// Rate limit check
if let Err(e) = check_rate_limit(...) { ... } // +1
// Parameter extraction
let project = match query.get("project") { ... }; // +1
let question = match query.get("query") { ... }; // +1
// Semantic search
let mut semantic_results = match state.query_worker.query(...).await { ... }; // +1
// RBAC filtering
if let Some(guard) = &state.access_guard { ... } // +1
// Search method branching
match search_method {
"semantic" => { ... } // +1
"hybrid" => {
if let Some(os_client) = &state.opensearch_client { // +1
match os_client.hybrid_search(...).await { // +1
Ok(hybrid_results) => { ... }
Err(e) => { ... } // +1
}
} else { ... } // +1
}
_ => { ... } // +1
}
}
Analysis:
| Metric | Value |
|---|---|
| Cyclomatic Complexity | 14 |
| Test Coverage (est.) | ~20% (only RBAC helper tests) |
| CRAP Score | 14² × (0.8)³ + 14 = 114 |
Anti-Patterns:
- ❌ Mixed responsibilities: auth, validation, search, RBAC, response formatting
- ❌ Nested match/if blocks create cognitive maze
- ❌ Duplicated JSON response construction (3 places)
- ❌ No unit tests for main handler logic
Refactored Version:
/// Validate and extract query parameters
struct QueryParams {
project: String,
question: String,
limit: i64,
search_method: SearchMethod,
}
enum SearchMethod {
Semantic,
Hybrid,
}
impl QueryParams {
fn from_query(query: &HashMap<String, String>) -> Result<Self, HttpResponse> {
let project = query.get("project")
.ok_or_else(|| bad_request("missing project parameter"))?
.clone();
let question = query.get("query")
.ok_or_else(|| bad_request("missing query parameter"))?
.clone();
let limit = query.get("limit")
.and_then(|l| l.parse().ok())
.unwrap_or(10);
let search_method = match query.get("method").map(|s| s.as_str()) {
Some("semantic") => SearchMethod::Semantic,
_ => SearchMethod::Hybrid,
};
Ok(Self { project, question, limit, search_method })
}
}
/// Execute search based on method
async fn execute_search(
params: &QueryParams,
semantic_results: Vec<QueryResult>,
os_client: Option<&OpenSearchClient>,
token: &str,
) -> SearchResponse {
match params.search_method {
SearchMethod::Semantic => SearchResponse::semantic(semantic_results, params.limit),
SearchMethod::Hybrid => {
match os_client {
Some(client) => client.hybrid_or_fallback(semantic_results, params).await,
None => SearchResponse::semantic_only(semantic_results, params.limit),
}
}
}
}
/// Main handler - now orchestration only
pub async fn query_handler(
req: HttpRequest,
query: web::Query<HashMap<String, String>>,
state: web::Data<AppState>,
) -> HttpResponse {
let claims = validate_auth_or_return(&req, &state).await?;
require_capability(&claims, "memory:read")?;
check_rate_limit(&claims, &state, "/memory/query")?;
let params = QueryParams::from_query(&query)?;
let results = state.query_worker.query(¶ms.project, ¶ms.question, Some(50)).await?;
let filtered = apply_rbac_filter(&state, &claims, results, ¶ms.project).await;
let response = execute_search(¶ms, filtered, state.opensearch_client.as_ref(), &token).await;
Ok(response.into())
}
// New Complexity: 5. With tests, CRAP drops to 5.
Required Tests:
#[cfg(test)]
mod query_handler_tests {
#[test]
fn test_query_params_valid() { ... }
#[test]
fn test_query_params_missing_project() { ... }
#[test]
fn test_query_params_missing_query() { ... }
#[tokio::test]
async fn test_execute_search_semantic() { ... }
#[tokio::test]
async fn test_execute_search_hybrid_with_opensearch() { ... }
#[tokio::test]
async fn test_execute_search_hybrid_fallback() { ... }
}
2. execute_with_wiki (full_pipeline.rs:225)
Current State:
pub async fn execute_with_wiki(
&self,
query: &str,
wiki_graph: &WikiLinkGraph,
candidates: Vec<(String, String)>,
) -> Result<PipelineResult> {
let start = std::time::Instant::now();
let mut metrics = PipelineMetrics::new();
// Phase 5: Intent inference
let query_intent = MetadataExtractor::infer_query_intent(query); // +1 (method call with internal branching)
// Phase 1-4: Wiki routing
let routed = self.router.route_with_wiki_graph(...).await?; // +1
// Phase 5: Metadata boost loop
for chunk in routed.selected_chunks { // +1
let metadata = MetadataExtractor::extract(...);
if self.config.enable_metadata_boost { // +1
boost = self.booster.calculate_boost(...);
if boost > 0.0 { // +1
boosts_applied += 1;
}
}
enriched_chunks.push(...);
}
// Phase 6: Cache alignment loop
for chunk in &mut enriched_chunks { // +1
if let Some((_, slot)) = slots.iter().find(...) { // +1
chunk.cache_slot = *slot;
}
}
// ...more metric collection
}
Analysis:
| Metric | Value |
|---|---|
| Cyclomatic Complexity | 12 |
| Test Coverage (est.) | ~60% (integration tests) |
| CRAP Score | 12² × (0.4)³ + 12 = 21 |
Anti-Patterns:
- ❌ God method: 150+ lines doing 6 phases
- ❌ Timing instrumentation pollutes business logic
- ❌ Metrics mutation scattered throughout
- ❌ Copy-paste between
execute_with_wikiandexecute_direct
Refactored Version:
/// Phase executor trait for clean separation
trait PhaseExecutor {
async fn execute(&self, ctx: &mut PipelineContext) -> Result<()>;
}
struct IntentInferencePhase;
struct WikiRoutingPhase { router: QueryRouter }
struct MetadataBoostPhase { booster: MetadataBooster, enabled: bool }
struct CacheAlignmentPhase { aligner: KvCacheAligner }
impl PhaseExecutor for MetadataBoostPhase {
async fn execute(&self, ctx: &mut PipelineContext) -> Result<()> {
if !self.enabled { return Ok(()); }
for chunk in &mut ctx.chunks {
let metadata = MetadataExtractor::extract(&chunk.id, &chunk.text);
let boost = self.booster.calculate_boost(ctx.query_intent, &metadata);
chunk.apply_boost(boost);
ctx.metrics.record_boost(boost);
}
Ok(())
}
}
/// Main pipeline - just orchestration
pub async fn execute_with_wiki(&self, query: &str, ...) -> Result<PipelineResult> {
let mut ctx = PipelineContext::new(query);
ctx.time_phase("intent", || IntentInferencePhase.execute(&mut ctx)).await?;
ctx.time_phase("routing", || self.routing_phase.execute(&mut ctx)).await?;
ctx.time_phase("metadata", || self.metadata_phase.execute(&mut ctx)).await?;
ctx.time_phase("cache", || self.cache_phase.execute(&mut ctx)).await?;
Ok(ctx.into_result())
}
// New Complexity: 4. Each phase is independently testable.
🟡 Medium Risk Functions
3. evaluate (access_evaluator.rs:44)
Analysis:
| Metric | Value |
|---|---|
| Cyclomatic Complexity | 8 |
| Test Coverage | ~80% (17 tests) |
| CRAP Score | 8² × (0.2)³ + 8 = 8.5 |
Status: ✅ Acceptable due to high test coverage. The complexity is inherent to RBAC logic.
Minor Improvement: Extract role iteration into helper:
fn find_first_allowing_role(&self, claims: &Claims, resource: &ResourceMeta, verb: Verb, roles: &[Role]) -> Option<AccessDecision> {
roles.iter()
.find_map(|role| self.evaluate_role(claims, resource, verb, role))
.filter(|d| d.is_allowed())
}
4. ingest_handler (http_server.rs:423)
Analysis:
| Metric | Value |
|---|---|
| Cyclomatic Complexity | 11 |
| Test Coverage | ~10% |
| CRAP Score | 11² × (0.9)³ + 11 = 99 |
Anti-Patterns:
- ❌ 100+ lines of sequential validation
- ❌ No unit tests (only integration)
- ❌ Mixed concerns: validation, RBAC, queuing, response
Recommended Refactor: Same pattern as query_handler - extract IngestParams, IngestValidator, separate RBAC check.
5. check_all (scope_checker.rs)
Analysis:
| Metric | Value |
|---|---|
| Cyclomatic Complexity | 6 |
| Test Coverage | ~70% |
| CRAP Score | 6² × (0.3)³ + 6 = 6.97 |
Status: ✅ Acceptable.
🟢 Low Risk Functions (Exemplary)
to_rbac_claims (http_server.rs:152)
fn to_rbac_claims(jwt: &JwtClaims) -> RbacClaims {
RbacClaims::new(&jwt.sub)
.with_roles(jwt.roles.clone().unwrap_or_default().iter().map(|s| s.as_str()).collect())
.with_groups(jwt.groups.clone().unwrap_or_default().iter().map(|s| s.as_str()).collect())
.with_permissions(jwt.permissions.clone().unwrap_or_default().iter().map(|s| s.as_str()).collect())
}
| Metric | Value |
|---|---|
| Complexity | 2 |
| Coverage | 100% (3 tests) |
| CRAP Score | 2 ✅ |
query_result_to_resource_meta (http_server.rs:160)
| Metric | Value |
|---|---|
| Complexity | 4 |
| Coverage | 100% (4 tests) |
| CRAP Score | 4 ✅ |
Refactoring Priority Matrix
| Function | CRAP | LOC | Tests | Priority |
|---|---|---|---|---|
query_handler |
114 | 196 | 0 | 🔴 P0 |
ingest_handler |
99 | 108 | 0 | 🔴 P0 |
execute_with_wiki |
21 | 150 | 6 | 🟡 P1 |
learn_handler |
~50 | 170 | 0 | 🟡 P1 |
context_handler |
12 | 65 | 2 | 🟢 P2 |
Recommended Actions
Immediate (P0)
- Extract
QueryParamsstruct fromquery_handler - Add 6 unit tests for query parameter validation
- Extract
IngestParamsstruct fromingest_handler - Add 4 unit tests for ingest validation
Short-term (P1)
- Refactor
full_pipeline.rsusing Phase Executor pattern - Deduplicate
execute_with_wiki/execute_direct - Add timing decorator instead of inline instrumentation
Long-term (P2)
- Generate coverage report with
cargo-tarpaulin - Set CI gate: CRAP > 30 fails build
- Extract HTTP handler helpers into
handlers/module
Test Coverage Improvement Plan
# Install coverage tool
cargo install cargo-tarpaulin
# Generate report
cargo tarpaulin --out Html --output-dir coverage/
# Target files
cargo tarpaulin --packages mem-cli --files src/http_server.rs
Target: Achieve 80% coverage on all handlers → CRAP < 10 for all functions.