debtmap 0.16.4

Code complexity and technical debt analyzer
Documentation
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# Lean debtmap workflow - fix top 3 tech debt items following Stillwater philosophy

# Phase 1: Generate coverage data
- shell: "just coverage-lcov"

# Phase 2: Analyze tech debt (markdown format, top 3)
- shell: "debtmap analyze . --lcov target/coverage/lcov.info --top 3 --format markdown"

# Phase 3: Fix the identified issues following Stillwater philosophy
- claude: |
    Fix the following technical debt issues:

    ${shell.output}

    # Stillwater Philosophy

    ## The Name

    **Stillwater** is more than a name - it's a mental model:

    ```
           Still Waters
          ╱            ╲
     Pure Logic      Effects
         ↓              ↓
      Unchanging     Flowing
     Predictable    Performing I/O
      Testable      At boundaries
    ```

    Like a still pond with water flowing through it, your application should have:
    - Pure business logic that doesn't change
    - Effects that move data in and out

    ## Core Beliefs

    ### 1. Pure Core, Imperative Shell

    **The Problem:**
    Most code mixes business logic with I/O, making it hard to:
    - Test (need to mock databases, APIs, filesystems)
    - Reason about (what does this function *actually* do?)
    - Reuse (tightly coupled to infrastructure)

    **The Stillwater Way:**
    ```rust
    // ❌ Typical mixed code
    fn process_user(id: UserId, db: &Database) -> Result<User, Error> {
        let user = db.fetch_user(id)?;  // I/O
        if user.age < 18 {              // Logic
            return Err(Error::TooYoung);
        }
        let discount = if user.premium { // Logic
            0.15
        } else {
            0.05
        };
        user.discount = discount;        // Logic
        db.save_user(&user)?;           // I/O
        Ok(user)
    }

    // ✓ Stillwater separated
    // Pure logic (the "still" core)
    fn calculate_discount(user: &User) -> f64 {
        if user.premium { 0.15 } else { 0.05 }
    }

    fn validate_age(age: u8) -> Result<(), Error> {
        if age >= 18 { Ok(()) } else { Err(Error::TooYoung) }
    }

    fn apply_discount(user: User, discount: f64) -> User {
        User { discount, ..user }
    }

    // Effects (the "water" shell)
    fn process_user_effect(id: UserId) -> Effect<User, Error, AppEnv> {
        IO::query(|db| db.fetch_user(id))           // I/O
            .and_then(|user| {
                validate_age(user.age)?;            // Pure!
                let discount = calculate_discount(&user); // Pure!
                let updated = apply_discount(user, discount); // Pure!
                Effect::pure(updated)
            })
            .and_then(|user| {
                IO::execute(|db| db.save_user(&user)) // I/O
                    .map(|_| user)
            })
    }
    ```

    **Benefits:**
    - Pure functions: 100% testable, no mocks
    - Clear data flow: see exactly what transforms what
    - Reusable logic: discount calculation works anywhere
    - Easy to reason about: no hidden side effects

    ### 2. Fail Fast vs Fail Completely

    **The Problem:**
    Validation usually stops at the first error. User submits a form with 5 fields, gets "email invalid" error, fixes it, submits again, gets "password too weak", etc. Frustrating!

    **The Stillwater Way:**
    ```rust
    // ❌ Standard Result: stops at first error
    fn validate_user(input: UserInput) -> Result<User, Error> {
        let email = validate_email(&input.email)?;     // Stops here if invalid
        let password = validate_password(&input.pwd)?; // Never reached
        let age = validate_age(input.age)?;           // Never reached
        Ok(User { email, password, age })
    }

    // ✓ Stillwater: accumulates ALL errors
    fn validate_user(input: UserInput) -> Validation<User, Vec<Error>> {
        Validation::all((
            validate_email(&input.email),
            validate_password(&input.pwd),
            validate_age(input.age),
        ))
        .map(|(email, password, age)| User { email, password, age })
    }
    // Returns: Err(vec![EmailError, PasswordError, AgeError])
    ```

    **When to use which:**
    - **Result (fail fast)**: Sequential operations where later steps depend on earlier
      - Example: Fetch user, then fetch their orders
    - **Validation (fail completely)**: Independent validations that should all be checked
      - Example: Form validation, config validation

    ### 3. Errors Should Tell Stories

    **The Problem:**
    Deep call stacks lose context:
    ```
    Error: No such file or directory
    ```
    Which file? What were we trying to do? Why?

    **The Stillwater Way:**
    ```rust
    fetch_config()
        .context("Loading application configuration")
        .and_then(|cfg| parse_config(cfg))
        .context("Parsing YAML configuration")
        .and_then(|cfg| validate_config(cfg))
        .context("Validating configuration values")
    ```

    Error output:
    ```
    Error: No such file or directory
      -> Loading application configuration
      -> Parsing YAML configuration
      -> Validating configuration values
    ```

    Now you know exactly what failed and why.

    ### 4. Composition Over Complexity

    **The Problem:**
    Large functions that do everything are hard to test and understand.

    **The Stillwater Way:**
    Build complex behavior from simple, composable pieces:

    ```rust
    // Small, focused, pure functions
    fn parse_line(line: &str) -> Result<Record, ParseError>;
    fn validate_record(rec: Record) -> Validation<ValidRecord, Vec<Error>>;
    fn enrich_record(rec: ValidRecord, ref_data: &RefData) -> EnrichedRecord;
    fn aggregate(records: Vec<EnrichedRecord>) -> Report;

    // Compose them
    fn pipeline(input: String, ref_data: RefData) -> Effect<Report, Error, Env> {
        input.lines()
            .map(parse_line)
            .collect::<Result<Vec<_>, _>>()?
            .into_iter()
            .map(validate_record)
            .collect::<Validation<Vec<_>, _>>()?
            .into_iter()
            .map(|r| enrich_record(r, &ref_data))
            .collect()
            |> aggregate
            |> Effect::pure
    }
    ```

    Each piece:
    - Does one thing
    - Is easily testable
    - Is reusable
    - Has clear types

    ### 5. Types Guide, Don't Restrict

    **The Problem:**
    Heavy type machinery (HKTs, complex traits) makes code hard to understand and compile errors cryptic.

    **The Stillwater Way:**
    Use types to make wrong code hard to write, but keep them simple:

    ```rust
    // Effect<T, E, Env> tells you:
    // - T: what it produces
    // - E: how it can fail
    // - Env: what it needs to run

    // You can't:
    // - Run an effect without environment (compiler error)
    // - Mix effects with different environments (type mismatch)
    // - Forget to handle errors (must call .run())

    // But you can:
    // - Understand what's happening (no magic)
    // - Get clear error messages (simple types)
    // - Refactor safely (types guide you)
    ```

    ### 6. Pragmatism Over Purity

    **The Stillwater Way:**
    We're not trying to be Haskell. We're trying to be **better Rust**.

    **What we DON'T do:**
    - ❌ Force monad abstraction (Rust doesn't have HKTs)
    - ❌ Fight the borrow checker (work with ownership)
    - ❌ Replace standard library (integrate with Result/Option)
    - ❌ Macro-heavy DSLs (prefer clear code)

    **What we DO:**
    - ✓ Provide concrete, useful types
    - ✓ Work with `?` operator
    - ✓ Zero-cost via generics
    - ✓ Integrate with async/await
    - ✓ Help you write better Rust

    ### 7. Parse, Don't Validate

    **The Problem:**
    Validation at runtime means you keep re-checking the same invariants:

    ```rust
    // ❌ Runtime validation - checked everywhere, forgotten somewhere
    fn process_user(age: i32) -> Result<(), Error> {
        if age < 0 { return Err(Error::InvalidAge); }  // Check here...
        // ...later in another function
        if age < 0 { return Err(Error::InvalidAge); }  // ...and again here
    }
    ```

    **The Stillwater Way:**
    Use refined types to encode invariants at the type level:

    ```rust
    use stillwater::refined::{Refined, Positive};

    // ✓ Compile-time guarantee - impossible to have invalid data
    type Age = Refined<i32, Positive>;

    fn process_user(age: Age) -> Result<(), Error> {
        // No validation needed - Age is ALWAYS positive by construction
        let years = age.value();  // Safe access
    }

    // Validation happens once at the boundary
    let age: Age = Refined::new(25)?;  // Fails if not positive
    ```

    **Benefits:**
    - Invariants encoded in the type system
    - Illegal states become unrepresentable
    - No defensive checks scattered throughout code
    - Self-documenting APIs

    ### 8. Type-Level Resource Safety

    **The Problem:**
    Resource leaks are subtle and hard to catch:

    ```rust
    // ❌ Easy to forget cleanup
    let file = open_file(path)?;
    do_work(&file)?;  // If this fails, file leaks
    file.close()?;
    ```

    **The Stillwater Way:**
    Track resource acquisition and release at the type level:

    ```rust
    use stillwater::effect::resource::*;

    // ✓ Compiler ensures resources are released
    fn safe_file_op(path: &str) -> impl ResourceEffect<Acquires = Empty, Releases = Empty> {
        bracket::<FileRes>()
            .acquire(open_file(path))
            .release(|f| async move { f.close().await })
            .use_fn(|f| do_work(f))
    }
    // Type signature PROVES no resource leaks - guaranteed cleanup even on error
    ```

    **Benefits:**
    - Compiler catches resource leaks
    - Zero runtime overhead (compile-time only)
    - LIFO cleanup ordering for multiple resources
    - Guaranteed cleanup even when errors occur

    ### 9. Resilience as Data

    **The Problem:**
    Retry logic is usually tangled with business code:

    ```rust
    // ❌ Retry logic scattered and untestable
    async fn fetch_data() -> Result<Data, Error> {
        let mut attempts = 0;
        loop {
            match api_call().await {
                Ok(data) => return Ok(data),
                Err(e) if attempts < 3 => {
                    attempts += 1;
                    sleep(Duration::from_secs(1 << attempts)).await;
                }
                Err(e) => return Err(e),
            }
        }
    }
    ```

    **The Stillwater Way:**
    Define retry policies as pure, testable data:

    ```rust
    use stillwater::RetryPolicy;

    // ✓ Policy as data - testable without I/O
    let policy = RetryPolicy::exponential(Duration::from_millis(100))
        .with_max_retries(5)
        .with_jitter(0.25);

    // Test the policy in isolation
    assert_eq!(policy.delay_for_attempt(0), Some(Duration::from_millis(100)));
    assert_eq!(policy.delay_for_attempt(1), Some(Duration::from_millis(200)));

    // Apply to any effect
    Effect::retry(|| fetch_data(), policy);
    ```

    **Benefits:**
    - Retry policies are testable pure values
    - Reusable across different operations
    - Clear separation from business logic
    - Conditional retry, hooks, and timeouts

    ### 10. Accumulation Without Threading

    **The Problem:**
    Logging and metrics require threading state through your code:

    ```rust
    // ❌ Manual state threading
    fn process(x: i32, logs: &mut Vec<String>) -> Result<i32, Error> {
        logs.push("Starting");
        let y = step1(x, logs)?;
        logs.push(format!("After step 1: {}", y));
        step2(y, logs)
    }
    ```

    **The Stillwater Way:**
    Use Writer effect to automatically accumulate logs alongside computation:

    ```rust
    use stillwater::effect::writer::prelude::*;

    // ✓ Automatic accumulation - no threading
    fn process(x: i32) -> impl WriterEffect<Output = i32, Writes = Vec<String>> {
        tell_one("Starting".to_string())
            .and_then(move |_| pure(x * 2))
            .tap_tell(|y| vec![format!("After step 1: {}", y)])
    }

    // Get both result and accumulated logs
    let (result, logs) = process(21).run_writer(&()).await;
    ```

    **Benefits:**
    - Logs, metrics, audit trails accumulate automatically
    - Works with any Monoid (Vec, Sum, custom types)
    - No state threading cluttering your code
    - Pure business logic remains pure

    ## Quick Refactoring Decision Tree

    For functions with complexity > 10:

    ```
    Is it a visitor pattern or large switch/match?
    ├─ YES → Don't refactor, add tests if needed
    └─ NO → Continue
       │
       Does it classify/categorize inputs?
       ├─ YES → Extract as pure static function
       └─ NO → Continue
          │
          Does it have repeated similar conditions?
          ├─ YES → Consolidate with pattern matching
          └─ NO → Continue
             │
             Does it have nested loops?
             ├─ YES → Convert to iterator chains
             └─ NO → Consider if refactoring is needed
    ```

    ## Common Pitfalls to Avoid

    ### When Refactoring for Complexity:

    ❌ **DON'T:**
    - Extract helper methods that are only called once
    - Create test-only helper functions (helpers not used in production code)
    - Break apart a clear match/switch into multiple functions
    - Add abstraction layers for simple logic
    - Refactor visitor pattern implementations (they're meant to have many branches)
    - Create 5+ helper methods for a 15-line function

    ✅ **DO:**
    - Extract reusable classification/decision logic as static pure functions
    - Use functional patterns (map, filter, fold) where appropriate
    - Consolidate similar patterns into single functions
    - Keep related logic together
    - Accept that some functions legitimately have high complexity
    - Test the extracted pure functions thoroughly
    - Use macros to eliminate repetitive patterns (like repeated field merging)

    ## Functional Programming Preferences

    **Prefer these patterns:**
    - Pure functions over stateful methods
    - Static methods for classification/utility functions
    - Match expressions with guards over if-else chains
    - Iterator chains over imperative loops
    - Function composition over deep nesting
    - Immutability by default

    **Example of good refactoring:**
    ```rust
    // Extract classification logic as pure static function
    impl MyStruct {
        // This can be tested in isolation and reused
        fn classify_item(name: &str) -> ItemType {
            match () {
                _ if name.starts_with("test_") => ItemType::Test,
                _ if name.contains("_impl") => ItemType::Implementation,
                _ => ItemType::Regular,
            }
        }

        // Main function uses the pure classifier
        fn process(&mut self, name: &str) {
            let item_type = Self::classify_item(name);
            // ... rest of logic
        }
    }
    ```

    ## Orchestration and I/O Function Guidelines

    When debtmap flags orchestration or I/O functions as untested:

    1. **Recognize the pattern**: Functions with cyclomatic complexity = 1 that coordinate modules or perform I/O are not the real debt
    2. **Extract testable logic**: Instead of testing I/O directly, extract pure functions that can be unit tested
    3. **Follow functional programming principles**:
       - Pure core: Business logic in pure functions
       - Imperative shell: Thin orchestration/I/O wrappers that don't need testing
    4. **Common patterns to extract**:
       - Formatting functions: Extract logic that builds strings from data
       - Parsing functions: Move to dedicated parser modules
       - Decision functions: Extract "should we do X" logic from "do X" execution
    5. **Don't force unit tests on**:
       - Functions that just print to stdout
       - Simple delegation to other modules
       - Module orchestration that just sequences calls
       - File I/O wrappers

    ## Quality Gates

    - All tests must pass
    - No clippy warnings
    - Formatted with cargo fmt
    - Commit with clear message explaining what was fixed and why

    **IMPORTANT**: When making commits, do NOT include attribution text like "🤖 Generated with Claude Code" or "Co-Authored-By: Claude" in commit messages.

  commit_required: true

# Phase 4: Run tests
- shell: "just test"
  on_failure:
    claude: "/prodigy-debug-test-failure --output ${shell.output}"
    max_attempts: 5
    fail_workflow: true

# Phase 5: Lint and format
- shell: "just fmt && just lint"
  on_failure:
    claude: "/prodigy-lint ${shell.output}"
    max_attempts: 5
    fail_workflow: true