Files
gh-buzzdan-ai-coding-rules-…/skills/pre-commit-review/examples.md
2025-11-29 18:02:42 +08:00

22 KiB

Pre-Commit Review Examples

Real-world examples of design review findings with before/after comparisons.

Example 0: Detection Workflow

Shows how the pre-commit-review skill analyzes code step-by-step using LLM reasoning.

Input: user/service.go

package user

type UserService struct {
    Repo Repository
}

func (s *UserService) CreateUser(id string, email string) error {
    // Validate ID
    if id == "" {
        return errors.New("empty id")
    }

    // Validate email
    if !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }

    // Open DB
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return err
    }
    defer db.Close()

    // Build query
    query := "INSERT INTO users (id, email) VALUES (?, ?)"
    _, err = db.Exec(query, id, email)
    if err != nil {
        return err
    }

    // Send email
    _ = sendEmail(email, "Welcome!")

    return nil
}

LLM Analysis Step-by-Step

Step 1: Apply Primitive Obsession Check (reference.md #1)

Questions LLM asks:

  • Q: Are primitives being validated?

  • A: Yes - 'id' string validated at line 9, 'email' string validated at line 14

  • Q: Is same validation repeated elsewhere?

  • A: [Uses Grep tool to search codebase]

    • Found in handler.go:23 (same id validation)
    • Found in repository.go:45 (same email validation)
  • Q: Would types add value beyond validation?

  • A: Yes

    • Email: Complex validation pattern, domain concept
    • UserID: Validation scattered, type safety benefit
  • Q: Calculate juiciness score (see reference.md):

    UserID:
    - Validation in 3 places (+1)
    - Simple empty check (+0)
    - Used in 8 locations (+2)
    Score: 3/10 → MEDIUM priority
    
    Email:
    - Validation in 3 places (+1)
    - Regex validation possible (+3)
    - Domain/LocalPart methods (+2)
    - Used in 8 locations (+2)
    Score: 8/10 → HIGH priority
    

Finding identified:

🔴 Design Debt: Primitive obsession
- string id → UserID type (MEDIUM juiciness)
- string email → Email type (HIGH juiciness)

Step 2: Apply Storifying Check (reference.md #2)

Questions LLM asks:

  • Q: Does function read like a story?

  • A: No - mixes abstraction levels:

    • Lines 7-16: High-level validation
    • Lines 18-23: Low-level DB connection setup
    • Lines 25-31: SQL implementation details
    • Lines 33-34: High-level notification
  • Q: Can I explain flow in 3-5 clear steps?

  • A: Should be: validateUser() → saveUser() → notifyUser() But currently mixes infrastructure and business logic

Finding identified:

🟡 Readability Debt: Function not storified
Mixed abstraction levels in CreateUser
Should extract: validateUser(), saveUser(), notifyUser()

Step 3: Apply Self-Validating Types Check (reference.md #3)

Questions LLM asks:

  • Q: Do methods check if struct fields are nil/invalid?

  • A: No checks in this method, but...

  • Q: Could UserService.Repo be nil?

  • A: Yes - struct can be created without validation

    svc := &UserService{} // Repo is nil!
    svc.CreateUser(...) // Will panic if we use Repo
    
  • Q: Is there a constructor with validation?

  • A: [Searches codebase] No NewUserService constructor found

Finding identified:

🔴 Design Debt: Non-self-validating type
UserService can be created with nil Repo
Need NewUserService constructor

Step 4: Apply Design Bugs Check (reference.md #8)

Questions LLM asks:

  • Q: Potential nil dereferences?

  • A: Not obvious in this function (but found UserService.Repo issue above)

  • Q: Are errors ignored?

  • A: YES! Line 34: _ = sendEmail(email, "Welcome!") Silent failure, no visibility into delivery failures

  • Q: Resources cleaned up?

  • A: Yes, defer db.Close() at line 23 ✓

  • Q: Context propagated?

  • A: No ctx parameter, but no downstream calls needing it

Finding identified:

🐛 Bug: Ignored error
Line 34: _ = sendEmail() silently ignores failure
Should at least log the error

Step 5: Apply Other Checks

  • Abstraction Levels (#4): Already covered in storifying
  • Architecture (#5): Single file, can't assess pattern
  • Naming (#6): Names are clear and idiomatic ✓
  • Testing (#7): No test file in scope
  • Comment Quality: Missing (no package docs, no function docs)
  • File Structure: Single type, reasonable size ✓

Analysis Complete - Categorize & Prioritize

Bugs (Priority 1 - Fix immediately):

  1. Ignored error at line 34

Design Debt (Priority 2 - Fix before commit): 2. Primitive obsession: string email (HIGH juiciness: 8/10) 3. Primitive obsession: string id (MEDIUM juiciness: 3/10) 4. Non-self-validating: UserService

Readability Debt (Priority 3 - Improves maintainability): 5. Not storified: CreateUser function


Output Report

📊 CODE REVIEW REPORT
Generated: 2025-11-09 15:45:00
Scope: user/service.go (1 file, 40 lines)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SUMMARY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Total findings: 5
🐛 Bugs: 1 (fix immediately)
🔴 Design Debt: 3 (fix before commit recommended)
🟡 Readability Debt: 1 (improves maintainability)
🟢 Polish: 0

Estimated fix effort: 50 minutes total
  - Critical (bugs + high juiciness design): 25 min
  - Recommended (medium design + readability): 25 min

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🐛 BUGS (1) - FIX IMMEDIATELY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. Ignored error
   Location: user/service.go:34
   Code: _ = sendEmail(email, "Welcome!")

   Issue: Email sending failure silently ignored
   Impact: No visibility into delivery failures, hard to debug

   Fix: Log error at minimum:
     if err := sendEmail(email, "Welcome!"); err != nil {
       log.Printf("failed to send welcome email: %v", err)
     }

   Better: Return error if critical:
     if err := sendEmail(email, "Welcome!"); err != nil {
       return fmt.Errorf("send welcome email: %w", err)
     }

   Effort: 2 min

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 DESIGN DEBT (3) - FIX BEFORE COMMIT RECOMMENDED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. Primitive obsession: string email (HIGH juiciness: 8/10)
   Locations: Line 6, 14, 28, 34
   Also found in: handler.go:23, repository.go:45

   Juiciness Score: 8/10
   - Validation in 3 places (+1)
   - Complex regex validation possible (+3)
   - Methods: Domain(), LocalPart() (+2)
   - Used in 8 locations (+2)

   Current:
     func CreateUser(id string, email string) error {
       if !strings.Contains(email, "@") {
         return errors.New("invalid email")
       }
       // ...
     }

   Better:
     type Email string

     func ParseEmail(s string) (Email, error) {
       if !emailRegex.MatchString(s) {
         return "", fmt.Errorf("invalid email: %s", s)
       }
       return Email(s), nil
     }

     func (e Email) Domain() string { /* ... */ }
     func (e Email) LocalPart() string { /* ... */ }
     func (e Email) String() string { return string(e) }

     func CreateUser(id string, email Email) error {
       // No validation needed, guaranteed valid
     }

   Why: Type safety, centralized validation, prevents invalid emails
   Fix: Use @code-designing skill → Create Email type
   Effort: 20 min

2. Primitive obsession: string id (MEDIUM juiciness: 3/10)
   Locations: Line 6, 9, 28
   Also found in: handler.go:23, repository.go:45

   Juiciness Score: 3/10
   - Validation in 3 places (+1)
   - Simple empty check (+0)
   - Used in 8 locations (+2)

   Note: Borderline case. Judgment call on whether to create type.

   Better:
     type UserID string

     func ParseUserID(s string) (UserID, error) {
       if s == "" {
         return "", errors.New("empty user id")
       }
       return UserID(s), nil
     }

   Why: Centralizes validation, type safety
   Fix: Use @code-designing skill → Create UserID type
   Effort: 10 min

3. Non-self-validating type: UserService
   Location: user/service.go:4

   Issue: UserService.Repo is public, can be nil
   No constructor to validate dependencies

   Current:
     type UserService struct {
       Repo Repository  // Can be nil!
     }

     svc := &UserService{}  // Invalid state allowed

   Better:
     type UserService struct {
       repo Repository  // Private
     }

     func NewUserService(repo Repository) (*UserService, error) {
       if repo == nil {
         return nil, errors.New("repo required")
       }
       return &UserService{repo: repo}, nil
     }

     func (s *UserService) CreateUser(id string, email Email) error {
       // No nil checks needed - constructor guarantees validity
       return s.repo.Save(...)
     }

   Why: Impossible to create invalid service, eliminates defensive checks
   Fix: Use @code-designing skill → Add constructor
   Effort: 10 min

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟡 READABILITY DEBT (1) - IMPROVES MAINTAINABILITY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. Function not storified: CreateUser
   Location: user/service.go:6-36

   Issue: Mixes 3 abstraction levels:
   - Lines 7-16: High-level validation
   - Lines 18-31: Low-level DB connection/SQL
   - Lines 33-34: High-level notification

   Flow not clear at a glance, hard to test pieces independently.

   Better:
     func CreateUser(id string, email Email) error {
       if err := validateUser(id, email); err != nil {
         return err
       }

       if err := saveUser(id, email); err != nil {
         return err
       }

       if err := notifyUser(email); err != nil {
         return err
       }

       return nil
     }

     func validateUser(id string, email Email) error { /* ... */ }
     func saveUser(id string, email Email) error { /* ... */ }
     func notifyUser(email Email) error { /* ... */ }

   Why: Reads like a story, testable pieces, clear intent
   Fix: Use @refactoring skill → Storifying pattern
   Effort: 15 min

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
RECOMMENDATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Priority 1: Fix immediately (2 min)
  ☐ Fix ignored error (log or return)

Priority 2: Fix before commit (40 min)
  ☐ Create Email type (HIGH juiciness) @code-designing
  ☐ Create UserID type (MEDIUM juiciness) @code-designing
  ☐ Add NewUserService constructor @code-designing

Priority 3: Improve maintainability (15 min)
  ☐ Storify CreateUser function @refactoring

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SKILLS TO USE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

@code-designing: For creating Email, UserID types and NewUserService
@refactoring: For storifying CreateUser function
Manual: For fixing ignored error (simple change)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
END OF REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This example demonstrates how the reviewer skill applies the complete checklist from reference.md systematically, using LLM reasoning to detect issues that linters cannot catch.


Example 1: Primitive Obsession + Self-Validating Types

Before (Design Debt 🔴)

package user

type UserService struct {
    DB *sql.DB  // Might be nil
}

func (s *UserService) CreateUser(id string, email string) error {
    // Defensive check
    if s.DB == nil {
        return errors.New("db is nil")
    }

    // Primitive validation
    if id == "" {
        return errors.New("id required")
    }
    if !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }

    // Business logic
    _, err := s.DB.Exec("INSERT INTO users (id, email) VALUES ($1, $2)", id, email)
    return err
}

Review Findings:

  • 🔴 Design Debt: Primitive obsession on id and email
  • 🔴 Design Debt: Non-self-validating type (UserService.DB might be nil)

After (No Debt)

package user

type UserID string
type Email string

func NewUserID(s string) (UserID, error) {
    if s == "" {
        return "", errors.New("id required")
    }
    return UserID(s), nil
}

func NewEmail(s string) (Email, error) {
    if !strings.Contains(s, "@") {
        return "", errors.New("invalid email")
    }
    return Email(s), nil
}

type UserService struct {
    db *sql.DB
}

func NewUserService(db *sql.DB) (*UserService, error) {
    if db == nil {
        return nil, errors.New("db is required")
    }
    return &UserService{db: db}, nil
}

func (s *UserService) CreateUser(id UserID, email Email) error {
    // No validation needed - types guarantee validity
    _, err := s.db.Exec("INSERT INTO users (id, email) VALUES ($1, $2)", id, email)
    return err
}

Example 2: Mixed Abstraction Levels + Storifying

Before (Readability Debt 🟡)

func ProcessPayment(orderID string, amount float64) error {
    // High-level: validation
    if orderID == "" {
        return errors.New("invalid order id")
    }
    if amount <= 0 {
        return errors.New("invalid amount")
    }

    // Low-level: HTTP client setup
    client := &http.Client{Timeout: 10 * time.Second}
    req, err := http.NewRequest("POST", "https://api.payment.com/charge", nil)
    if err != nil {
        return err
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    // Low-level: JSON marshaling
    body := map[string]interface{}{
        "order_id": orderID,
        "amount":   amount,
    }
    jsonBody, err := json.Marshal(body)
    if err != nil {
        return err
    }
    req.Body = io.NopCloser(bytes.NewReader(jsonBody))

    // Low-level: HTTP call
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // High-level: logging
    log.Printf("Payment processed for order %s", orderID)
    return nil
}

Review Findings:

  • 🟡 Readability Debt: Mixed abstraction levels (business + HTTP details)
  • 🟡 Readability Debt: Function not storified (hard to see flow)

After (No Debt)

func ProcessPayment(orderID OrderID, amount Amount) error {
    if err := validatePayment(orderID, amount); err != nil {
        return err
    }

    if err := chargePaymentGateway(orderID, amount); err != nil {
        return err
    }

    logPaymentSuccess(orderID)
    return nil
}

func validatePayment(orderID OrderID, amount Amount) error {
    // Validation logic only (already validated by types, but could have business rules)
    return nil
}

func chargePaymentGateway(orderID OrderID, amount Amount) error {
    // HTTP client logic encapsulated
    client := newPaymentClient()
    return client.Charge(orderID, amount)
}

func logPaymentSuccess(orderID OrderID) {
    log.Printf("Payment processed for order %s", orderID)
}

Example 3: Horizontal Layers → Vertical Slices

Before (Design Debt 🔴)

project/
├── domain/
│   └── user.go
├── service/
│   └── user_service.go
├── repository/
│   └── user_repository.go
└── handler/
    └── user_handler.go

Review Finding:

  • 🔴 Design Debt: Horizontal layering instead of vertical slices
  • Impact: User feature changes require touching 4 different directories

After (No Debt)

project/
└── user/
    ├── user.go           # Domain type
    ├── service.go        # Business logic
    ├── repository.go     # Persistence
    ├── handler.go        # HTTP
    └── user_test.go

Benefits:

  • All user-related code in one place
  • Easy to understand complete feature
  • Independent testing/deployment

Example 4: Generic Naming

Before (Readability Debt 🟡)

package common

type DataManager struct {
    store Storage
}

func (m *DataManager) ProcessData(data interface{}) (interface{}, error) {
    // ...
}

func HandleRequest(ctx context.Context, data map[string]interface{}) error {
    // ...
}

Review Findings:

  • 🟡 Readability Debt: Generic package name (common)
  • 🟡 Readability Debt: Vague type name (DataManager)
  • 🟡 Readability Debt: Meaningless function names (ProcessData, HandleRequest)

After (No Debt)

package user

type Service struct {
    repo Repository
}

func (s *Service) Create(ctx context.Context, u User) error {
    // ...
}

func (s *Service) Authenticate(ctx context.Context, credentials Credentials) (Token, error) {
    // ...
}

Example 5: Testing Anti-Patterns

Before (Design Debt 🔴)

package user  // Same package

// Testing private function
func TestValidateEmailInternal(t *testing.T) {
    assert.True(t, validateEmailInternal("test@example.com"))
}

// Heavy mocking
func TestCreateUser(t *testing.T) {
    mockRepo := &MockRepository{}
    mockEmailer := &MockEmailer{}

    mockRepo.On("Save", mock.Anything).Return(nil)
    mockEmailer.On("Send", mock.Anything).Return(nil)

    svc := &UserService{
        Repo:    mockRepo,
        Emailer: mockEmailer,
    }

    err := svc.CreateUser("123", "test@example.com")
    assert.NoError(t, err)

    mockRepo.AssertExpectations(t)
}

// Flaky with time.Sleep
func TestAsyncOperation(t *testing.T) {
    go doAsyncWork()
    time.Sleep(100 * time.Millisecond)  // ❌ Flaky
    assert.True(t, workCompleted)
}

Review Findings:

  • 🔴 Design Debt: Testing private methods
  • 🔴 Design Debt: Using mocks instead of real implementations
  • 🔴 Design Debt: Flaky test with time.Sleep

After (No Debt)

package user_test  // External package

// Test public API only
func TestService_CreateUser(t *testing.T) {
    // Use real implementations
    repo := user.NewInMemoryRepository()
    emailer := user.NewTestEmailer()

    svc, err := user.NewUserService(repo, emailer)
    require.NoError(t, err)

    id, _ := user.NewUserID("123")
    email, _ := user.NewEmail("test@example.com")

    u := user.User{ID: id, Email: email}
    err = svc.CreateUser(context.Background(), u)

    assert.NoError(t, err)

    // Verify via public API
    retrieved, err := svc.GetUser(context.Background(), id)
    assert.NoError(t, err)
    assert.Equal(t, email, retrieved.Email)
}

// No flakiness with channels
func TestAsyncOperation(t *testing.T) {
    done := make(chan struct{})

    go func() {
        doAsyncWork()
        close(done)
    }()

    select {
    case <-done:
        // Success
    case <-time.After(1 * time.Second):
        t.Fatal("timeout waiting for async work")
    }
}

Example 6: Complete Commit Review Output

Scenario

Developer adds user authentication feature with some design issues.

Review Output

📋 COMMIT READINESS SUMMARY

✅ Linter: Passed (0 issues)
✅ Tests: 87% coverage (5 new types, 23 test cases)
⚠️  Design Review: 5 findings (see below)

🎯 COMMIT SCOPE
Modified:
- user/service.go (+120, -30 lines)
- user/auth.go (new file, +85 lines)

Added:
- user/user_id.go (new type: UserID)
- user/password.go (new type: Password)

Tests:
- user/service_test.go (+95 lines)
- user/auth_test.go (new, +140 lines)

⚠️  DESIGN REVIEW FINDINGS

🔴 DESIGN DEBT (Recommended to fix):

1. user/service.go:67 - Primitive obsession on session token
   Current: func CreateSession(userID UserID) (string, error)
   Better:  func CreateSession(userID UserID) (SessionToken, error)
   Why: Session tokens should be validated types to prevent empty/invalid tokens
   Fix: Use @code-designing to create SessionToken type with validation

2. user/auth.go:34 - Non-self-validating type
   Current:
     type Authenticator struct {
         HashCost int  // Could be invalid
     }
   Better:
     func NewAuthenticator(hashCost int) (*Authenticator, error) {
         if hashCost < 4 || hashCost > 31 {
             return nil, errors.New("invalid hash cost")
         }
         // ...
     }
   Why: Constructor should validate, methods shouldn't need defensive checks
   Fix: Use @code-designing to add validating constructor

🟡 READABILITY DEBT (Consider fixing):

3. user/auth.go:89 - Mixed abstraction levels in Authenticate()
   Function mixes high-level auth flow with low-level bcrypt details
   Why: Harder to understand auth logic at a glance
   Fix: Use @refactoring to extract password comparison to separate function

4. user/service.go:45 - Function could be storified better
   Current: validateAndCreateUser() does validation + creation in one function
   Better: Split into validateUser() and createUser() for clarity
   Why: Single responsibility, easier to test each part
   Fix: Use @refactoring to split responsibilities

🟢 POLISH OPPORTUNITIES:

5. user/auth.go:12 - Less idiomatic naming
   Current: ComparePasswordWithHash
   Better:  PasswordMatches
   Why: More concise, Go-style naming

📝 BROADER CONTEXT:
While reviewing user/service.go, noticed email is still stored as string type
(line 23). Consider refactoring to use Email type consistently across the file
for better type safety (similar to UserID change in this commit).

💡 SUGGESTED COMMIT MESSAGE
Add user authentication with self-validating types

- Introduce UserID and Password self-validating types
- Implement Authenticator with bcrypt password hashing
- Add CreateSession and Authenticate methods
- Achieve 87% test coverage with real bcrypt testing

Follows primitive obsession principles with type-safe IDs and passwords.

────────────────────────────────────────

Would you like to:
1. Commit as-is (5 design findings remain)
2. Fix design debt only (🔴 items 1-2), then commit
3. Fix design + readability debt (🔴🟡 items 1-4), then commit
4. Fix all findings including polish (🔴🟡🟢 all items), then commit
5. Expand scope to refactor email type throughout file, then commit