Skip to main content

The Setup: A “Simple” Macro Enhancement

After successfully building 5 derive macros that eliminated 4,700 lines of boilerplate, I wanted to add one more feature: auto-generated CRUD methods. The change seemed straightforward:
// BEFORE: Macro generated struct, but methods were manual
#[derive(DynamoDbRepository)]
#[aggregate = "Account"]
pub struct DynamoDbAccountRepository;

impl DynamoDbAccountRepository {
    // Manual implementation of save, get, query, delete
    pub async fn save(&self, account: Account) -> Result<()> { /* ... */ }
    pub async fn get(&self, id: AccountId) -> Result<Option<Account>> { /* ... */ }
}

// AFTER: Macro generates methods too
#[derive(DynamoDbRepository)]
#[aggregate = "Account"]
pub struct DynamoDbAccountRepository;

// Macro now generates db_save, db_get, db_query, db_delete automatically
Expected impact: Save 200 lines per entity. Actual impact: 30 commits of cascading fixes across the crm crate.
The Lesson Learned Early: Macro changes are not isolated changes. They’re multiplied changes across every usage site.

The Cascade: 30 Commits in 24 Hours

Commit 508f43e:
feat(dynamodb-derive): add CRUD method generation to DynamoDbEntity macro

Breaking Change:
Crates using DynamoDbEntity must now add dependencies:
- aws-runtime
- aws-sdk-dynamodb
- serde_dynamo
I thought: “I’ll update the 4 affected crates manually. Should take 2 hours.” What actually happened:

Hour 1: The First Errors

Commit cd7c3e1:
fix(crm): remove duplicate DynamoDbLegalEntityRepository export
Simple enough. The new macro generated code that conflicted with existing exports. Commit 1ef9150:
fix(crm): fix syntax errors and add missing imports
Also straightforward. New generated methods needed additional imports. Status after 1 hour: 5 commits, still 214 compilation errors.

Hour 2-3: The Pattern Emerges

Commit 23d0baf:
fix(crm): partial fix for organization_dynamodb.rs factory pattern
Wait. “Partial fix”? This should be a complete fix. What I discovered: The macro’s generated methods used a different factory pattern than the manual methods.
// Manual code expected:
let client = self.client.clone();  // self.client is a field

// Macro generated:
let client = self.client();  // self.client() is a method
The cascade begins. Every repository in crm had the same issue. Next 8 commits:
  • 9e0815f: replace self.client with client
  • 11fe195: add client creation to save_calendar method
  • b1a1ff0: batch fix organization_dynamodb.rs methods
  • eb5e215: fix pipeline_dynamodb client calls
  • a7fbb9b: fix remaining client() method calls
Status after 3 hours: 13 commits, 147 errors remaining.

Hour 4-6: Type System Whack-a-Mole

This is where it got interesting. Each fix revealed new errors. Commit 8d6c763:
fix(crm): update pk_for_id calls to new single-parameter signature
The macro changed the primary key method signature:
  • Old: pk_for_id(tenant_id, capsule_id, id)
  • New: pk_for_id(id) (macro infers tenant/capsule from entity)
Impact: 47 call sites across 12 files needed updates. AI’s approach: Fix them one at a time, commit after each file. Why this was wrong: Each commit introduced new errors because files depend on each other. The commits:
  • 9249626: fix pk_for_id and gsi method signatures
  • 7503f9f: add GSI methods and fix syntax errors
  • 47fd4ae: fix CrmError variants, field access errors
  • 3afc5a0: move contact methods to ContactRepository impl block
  • 3862c3e: fix E0425 errors (missing values) and self.client() calls
Status after 6 hours: 24 commits, 68 errors. The pain: I was no longer making progress. Each fix created 2-3 new errors.

The Breaking Point: When I Stopped Trusting AI

Hour 7 (next morning): I found the AI session had made 6 more commits overnight (I left it running). Latest commit (f1ed1e7):
fix(crm): fix CrmError::Repository tuple variant usage
I checked the code:
// AI's fix:
return Err(CrmError::Repository("save failed".to_string()));

// But CrmError::Repository is not a tuple variant!
// It's a struct variant:
CrmError::Repository {
    source: RepositoryError,
    entity: String,
}
AI had hallucinated the error type structure. At this point: I realized the fundamental problem.
The Core Issue: AI was fixing errors in isolation, not understanding the system holistically.Each commit fixed the immediate compilation error without considering:
  • Why did this error appear?
  • What upstream change caused it?
  • Are there related errors with the same root cause?

The Fix: Human Intervention

What I did:
  1. Stopped the AI session
  2. Read the original macro change (commit 508f43e)
  3. Made a list of ALL changes the macro introduced:
    • New method names (save → db_save)
    • New factory pattern (client field → client() method)
    • New error types (EventStoreError instead of RepositoryError)
    • New dependency requirements (serde_dynamo)
  4. Fixed them systematically in 3 commits instead of 30:
Commit 1: Update all method names
# Used sed to update all save() → db_save() calls
find crm -name "*.rs" -exec sed -i '' 's/\.save(/\.db_save(/g' {} +
Commit 2: Update factory pattern
// Updated trait definition once, all implementations inherited it
trait Repository {
    fn client(&self) -> &CapsuleClient;  // Method, not field
}
Commit 3: Update error handling
// Map old error type to new error type
impl From<RepositoryError> for CrmError {
    fn from(e: RepositoryError) -> Self {
        CrmError::Repository {
            source: e,
            entity: "unknown".to_string(),
        }
    }
}
Total time for human fix: 45 minutes AI’s time: 24 hours (and still had errors)

What Went Wrong: AI’s Fix-in-Session Anti-Pattern

The Problem

When AI encounters a compilation error, it follows this pattern:
  1. Read the error message
  2. Understand the immediate cause
  3. Fix that specific error
  4. Compile
  5. If more errors, repeat from step 1
Why this fails for cascading errors:
  • Each fix addresses symptoms, not root cause
  • No holistic understanding of “what changed upstream”
  • No batching of related fixes
  • Creates more errors by partial fixes

The Example

Error cascade from macro change:
error[E0599]: no method named `save` found for type `DynamoDbAccountRepository`
  --> crm/src/services/account_service.rs:45:28
   |
45 |         self.repo.save(account).await?;
   |                   ^^^^ method not found

AI Fix: Rename save → db_save in account_service.rs

New Error:
error[E0599]: no method named `save` found for type `DynamoDbContactRepository`
  --> crm/src/services/contact_service.rs:67:28
   |
67 |         self.repo.save(contact).await?;
   |                   ^^^^ method not found

AI Fix: Rename save → db_save in contact_service.rs

New Error:
error[E0308]: mismatched types - expected `CrmError`, found `EventStoreError`
  --> crm/src/services/account_service.rs:45:9
   |
45 |         self.repo.db_save(account).await?;
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

AI Fix: Add error conversion in account_service.rs

New Error: ...
The pattern: AI fixed 1 error → triggered 2 new errors → repeat. What AI should have done:
  1. Recognize pattern: “All save() calls need to be db_save()”
  2. Fix ALL save() calls in one commit
  3. Recognize pattern: “All error types changed”
  4. Add unified error conversion
  5. Compile once → done
Why AI didn’t do this: AI optimizes for “fix the current error” not “understand the change pattern.”

What I Learned: When to Stop Using AI

Red Flags That AI Is Stuck

Red Flag #1: Diminishing Returns Track errors fixed per commit:
  • Commits 1-5: Average 12 errors fixed each
  • Commits 6-15: Average 4 errors fixed each
  • Commits 16-30: Average 1 error fixed each (sometimes net increase!)
If errors per commit drops below 3: Stop AI, intervene manually.
Red Flag #2: Repeated Fix Patterns If you see the same type of fix across multiple commits:
  • “fix client() calls” (7 commits)
  • “fix pk_for_id signature” (5 commits)
  • “fix error variants” (6 commits)
This means: AI found a pattern but only fixed one instance at a time. Solution: Batch fix all instances manually.
Red Flag #3: Partial Fixes in Commit Messages Commit messages with “partial fix” or “fix remaining” indicate AI doesn’t have a complete solution. Examples from the cascade:
  • “partial fix for organization_dynamodb.rs factory pattern”
  • “fix remaining client() method calls”
  • “batch fix organization_dynamodb.rs methods”
When you see these: AI is guessing, not solving.

When to Use AI vs. Manual Intervention

Isolated errors:
  • Single file affected
  • Error is self-contained
  • Fix doesn’t affect other files
Example:
error: unused import `EventStoreError`
  --> crm/src/lib.rs:12:5
AI can fix: Remove the unused import. Done.
Systematic but understood errors:
  • Pattern is clear
  • AI successfully batches fixes
  • Each fix reduces total error count
Example:
12 errors: all missing #[derive(Clone)] on event types
AI can fix: Add Clone derive to all 12 event types in one commit.

What We Learned: The Fix-in-Session Anti-Pattern

The Anti-Pattern

Fix-in-Session workflow:
  1. Make change (e.g., update macro)
  2. Compilation errors appear (30+ errors)
  3. Ask AI: “Fix all compilation errors”
  4. AI fixes one error at a time
  5. Each fix potentially creates new errors
  6. Repeat until done (or stuck)
Why this fails:
  • AI lacks global view of the change
  • Optimizes for immediate error, not root cause
  • Creates intermediate broken states
  • Risk of hallucinating fixes when stuck

The Better Approach: Understand-Then-Fix

Step 1: Understand the change
  • What did the macro modification actually change?
  • What are ALL the breaking changes?
  • Which files will be affected?
Step 2: Categorize errors
  • Group errors by type (method name changes, type mismatches, missing imports)
  • Identify pattern in each category
Step 3: Fix by category
  • Batch fix all method name changes in one commit
  • Batch fix all type mismatches in another commit
  • etc.
Step 4: Verify
  • Compile
  • Run tests
  • Verify no regressions

Real Example: How I Should Have Done It

The right approach for the CRUD macro change: Commit 1: Update macro (breaking change)
git commit -m "feat: add CRUD method generation (BREAKING)"
Commit 2: Update all downstream crates (atomic fix)
# Before committing, fix all breaking changes:
1. Update method calls (save  db_save) across all files
2. Add required dependencies to Cargo.toml files
3. Update error handling for new error types
4. Verify compilation
5. Run tests

git commit -m "refactor: adapt to CRUD macro breaking changes"
Total commits: 2 Total time: 2 hours Errors introduced: 0 (tested before committing) Contrast with what happened:
  • 31 commits (1 feature + 30 fixes)
  • 24 hours
  • Multiple intermediate broken states

The Meta-Learning: AI Isn’t Good at Debugging Its Own Changes

Why AI Struggles with Cascading Errors

Problem 1: No Mental Model of the System Human developer:
  • Knows that changing a macro affects all users of that macro
  • Can predict which files will break
  • Can grep for all usage sites before making the change
AI developer:
  • Doesn’t build system-wide mental model
  • Only sees errors after they appear
  • Fixes reactively, not proactively

Problem 2: Optimizes for Local, Not Global Each error fix is locally optimal (fixes that specific error) but globally suboptimal (creates more errors elsewhere). Example:
// Error: Method `client` not found
// AI's fix: Add client() method to this struct

// But the REAL fix: Change self.client to self.client() in all call sites
AI added 7 new client() methods instead of updating 7 call sites.
Problem 3: No “Step Back” Capability After 15 commits with diminishing returns, AI doesn’t think:
  • “This approach isn’t working”
  • “Maybe I should try a different strategy”
  • “Let me understand the root cause first”
AI just keeps applying the same fix pattern, hoping it will eventually work. Human advantage: Can recognize “I’m stuck” and change approach.

Principles Established

What we learned: Macro changes are breaking changes that affect multiple crates simultaneously.New rule: Before making breaking macro changes:
  1. List all affected crates
  2. Identify all breaking changes (method renames, type changes, etc.)
  3. Create migration checklist
  4. Fix all affected crates in a single atomic commit
  5. Never commit broken intermediate states
Practice: Use workspace-wide compilation to verify all crates before committing.
What we learned: If error count isn’t decreasing steadily, AI is stuck.Tracking metric: Errors fixed per commit
  • Healthy: 5-10 errors fixed per commit
  • Warning: 2-4 errors fixed per commit
  • Critical: Less than 2 errors per commit
Action: If metric hits “warning” level for 3 consecutive commits, stop AI and intervene manually.
What we learned: 30 small commits are worse than 1 comprehensive commit for systematic changes.When to batch:
  • Method rename across many files
  • Type signature changes
  • Dependency updates
  • Error handling pattern changes
Tools:
# Find all usage sites
rg "\.save\(" --type rust

# Batch rename
sd '\.save\(' '.db_save(' crm/**/*.rs

# Verify
cargo check
AI limitation: AI doesn’t use these batch tools effectively. Prefers file-by-file fixes.
What we learned: Unconstrained “fix all errors” leads to hallucination and inefficiency.Better prompt:
Fix compilation errors in crm crate.

Constraints:
- Maximum 5 commits
- Group related fixes in single commit
- Do NOT add new methods/types
- Only update call sites to match new signatures
- If stuck after 3 commits, report and stop
This forces AI to: Think strategically about batching fixes instead of fixing one at a time.
What we learned: The cascade started because I didn’t verify the macro’s generated code before migrating all entities.New practice:
  1. Make macro change
  2. Use cargo expand on ONE entity
  3. Review generated code
  4. Test with that entity
  5. Only then migrate other entities
Time cost: 30 minutes of upfront verificationTime saved: 24 hours of error fixing

The Recovery: How I Fixed It

After recognizing AI was stuck, I took over: Step 1: Understand the root cause (30 minutes) Read the macro change commit carefully. Listed all breaking changes:
  • save()db_save()
  • get()db_get()
  • client field → client() method
  • RepositoryErrorEventStoreError
  • New dependencies required
Step 2: Batch fix each breaking change (3 commits, 45 minutes)
rg "\.save\(" -t rust | cut -d: -f1 | sort -u | xargs sd '\.save\(' '.db_save('
rg "\.get\(" -t rust | cut -d: -f1 | sort -u | xargs sd '\.get\(' '.db_get('
git commit -m "refactor: update to new CRUD method names"

git commit -m "refactor: update to client() method pattern"

git commit -m "fix: add error type conversions for macro changes"
Step 3: Verify (15 minutes)
cargo check --workspace  # All clean
cargo test --workspace   # 142 tests passing
Total recovery time: 90 minutes Lesson: For systematic refactoring, human batch fixes beat AI incremental fixes by 16x.

Metrics: The Cost of the Cascade

Commits: 31 (1 feature + 30 fixes)Time: 24 hoursFinal state: Still had 14 errorsErrors introduced: ~40 new errors during fixing processToken usage: 850k tokens ($13)Developer frustration: High

Code Example: The Hallucinated Fix

Here’s the most egregious example of AI hallucination during the cascade: The error:
error[E0308]: mismatched types
  --> crm/src/repositories/organization_dynamodb.rs:123:9
   |
   | expected enum `CrmError`
   | found enum `EventStoreError`
AI’s fix (commit c62ed84):
// AI added a conversion that doesn't make sense:
impl From<EventStoreError> for CrmError {
    fn from(e: EventStoreError) -> Self {
        CrmError::Repository("Event store error".to_string())  // ❌ WRONG
    }
}
Two problems:
  1. CrmError::Repository is NOT a tuple variant with a String
  2. Lost the actual error details from EventStoreError
The correct fix:
impl From<EventStoreError> for CrmError {
    fn from(e: EventStoreError) -> Self {
        CrmError::Repository {
            source: RepositoryError::EventStore(e),
            entity: "unknown".to_string(),
        }
    }
}
Why AI hallucinated: By commit 24, AI’s context was full of partial fixes and error messages. It started guessing based on patterns it saw, not actual code structure. Lesson: AI quality degrades in long error-fixing sessions. Fresh session or human intervention needed.

The Surprising Discovery: AI Can Prevent This

After recovering from the cascade, I asked a fresh Evaluator session:
Review the macro change that caused 30 fix commits.

Could this have been prevented?
What checks should run before making breaking macro changes?
Evaluator’s response (impressive): Pre-Change Checklist for Macro Modifications:
  1. Impact Analysis:
    • Run cargo tree to find all crates using the macro
    • Grep for all usage sites across workspace
    • Estimate affected lines of code
  2. Breaking Change Detection:
    • Compare old vs new generated code (cargo expand)
    • List method signature changes
    • List type changes
    • List new dependencies
  3. Migration Plan:
    • Create checklist of required updates per crate
    • Batch fixes by category (method renames, type updates, etc.)
    • Prepare workspace-wide test command
  4. Atomic Commit Strategy:
    • Update macro
    • Update ALL downstream crates
    • Verify workspace compilation
    • Commit atomically
This checklist is now in my CLAUDE.md. The irony: AI can prevent cascading errors (via planning) but can’t fix them efficiently (via reactive debugging).

Takeaways

For breaking changes:
  1. Always create migration plan first
  2. Fix all affected sites atomically
  3. Never commit intermediate broken states
  4. Use batch tools (sed, sd, rg) for systematic renames
For AI collaboration:
  1. Monitor error-fixing progress (errors fixed per commit)
  2. Intervene when progress stalls (< 3 errors per commit)
  3. Use AI for planning prevention, human for reactive fixes
  4. Fresh AI sessions for post-mortem analysis
The meta-lesson: AI is excellent at preventing problems (via thorough planning) but poor at fixing complex cascading problems (reactive debugging). Use AI proactively, not reactively.