--- name: cognitive-load description: Measure how much mental effort a codebase demands from developers. Use when identifying hotspots, before architectural decisions, or when code feels hard to understand. --- # Cognitive Load Analysis ## Overview Cognitive load is the mental effort required to understand, modify, and reason about code. High cognitive load leads to bugs, slow development, and burnout. This skill provides a framework for measuring and reducing cognitive load using the **Cognitive Load Index (CLI)**, scored 0–1000. ## CLI Score Rating Scale | CLI Score | Rating | Action | |-----------|--------|--------| | 0–100 | Excellent | Model codebase | | 101–250 | Good | Minor improvements | | 251–400 | Moderate | Address hotspots | | 401–600 | Concerning | Plan refactoring | | 601–800 | Poor | Significant refactoring needed | | 801–999 | Severe | Consider rewrite of affected areas | ## The 8 Dimensions ### D1: Structural Complexity Cyclomatic complexity — the number of independent paths through code. Each `if`, `for`, `switch`, `case`, `&&`, `||` adds one. **Thresholds for Go:** - Simple function: < 5 (no issue) - Moderate: 5–10 (watch closely) - Complex: 10–15 (consider refactoring) - Critical: > 15 (must refactor) **Detection:** ```bash # Install: go install github.com/fzipp/gocyclo/cmd/gocyclo@latest gocyclo -over 10 ./... ``` **Go hotspots:** Request handlers that mix auth, validation, business logic, and response formatting in one function. ### D2: Nesting Depth Maximum nesting depth — how many levels deep a reader must track. **Thresholds for Go:** - Ideal: 1–2 levels - Acceptable: 3 levels - Warning: 4 levels - Critical: 5+ levels **Fix:** Early returns and guard clauses eliminate deep nesting: ```go // Bad: 4 levels deep func process(r *http.Request) error { if r != nil { if r.Body != nil { data, err := io.ReadAll(r.Body) if err == nil { // actual work at level 4 } } } return nil } // Good: guard clauses, 1 level func process(r *http.Request) error { if r == nil { return errors.New("nil request") } if r.Body == nil { return errors.New("no request body") } data, err := io.ReadAll(r.Body) if err != nil { return fmt.Errorf("read body: %w", err) } // actual work at level 1 return nil } ``` ### D3: Volume/Size Lines of code per file and function. Size alone isn't the issue — hidden responsibilities are. **Thresholds for Go:** - Function: > 50 lines is a signal (Go can go longer, but be honest about why) - File: > 400 lines often means too many responsibilities - Package: > 2000 lines total may need splitting **Go note:** Table-driven tests inflate LOC legitimately. Distinguish test LOC from production LOC when assessing. ### D4: Naming Quality Identifiers that don't reveal intent force readers to build a mental model from context. **Poor naming signals:** - Single-letter variables outside loop indices: `x`, `d`, `tmp` - Generic suffixes: `Manager`, `Handler`, `Processor`, `Data`, `Info`, `Util` - Abbreviations: `usrLst`, `custRepo`, `cfg2` - Inconsistency: `getUserById`, `FetchCustomerByID`, `retrieveClientById` for the same concept **Good naming in Go:** ```go // Bad func (m *Manager) proc(d interface{}) error { ... } // Good func (s *OrderService) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) { ... } ``` ### D5: Coupling How many packages/types a given unit depends on. High coupling means a change in one place ripples widely. **Detection:** ```bash # Count imports per file grep -c "^import" *.go # or examine import blocks manually ``` **Go-specific coupling smells:** - A domain type that imports from `database/sql`, `net/http`, and `smtp` simultaneously - A package that imports more than 10 other internal packages - Circular imports (Go compiler rejects these, but near-circular is still a smell) ### D6: Cohesion Whether things that belong together are together. Low cohesion means related code is scattered. **Signs of low cohesion:** - A function that calls many unrelated packages - A package where functions don't share data or purpose - Utility packages that grow without bound (`util`, `common`, `helpers`) ### D7: Duplication Repeated logic that must be updated in multiple places when requirements change. **Detection:** ```bash # Simple: find identical error messages that suggest copy-paste grep -r "error:" --include="*.go" | sort | uniq -d ``` **Go note:** Apply the Rule of Three — don't extract the first or second duplication. The third is the signal. ### D8: Navigability How easily can a reader find relevant code and understand entry points? **Signs of poor navigability:** - No clear entry point (no `main.go`, no `New*` constructors) - Package names that don't reflect content - Files that mix many unrelated types - Deep directory nesting with unclear boundaries ## Go-Specific Cognitive Load Hotspots ### Deeply Nested Error Handling Go's explicit error handling is a feature, but it can create visual noise: ```go // High cognitive load: multiple nested error paths func (s *Service) Process(ctx context.Context, id string) error { u, err := s.store.GetUser(ctx, id) if err != nil { if errors.Is(err, ErrNotFound) { o, err := s.store.GetOrgByUserID(ctx, id) if err != nil { if errors.Is(err, ErrNotFound) { return ErrEntityNotFound } return fmt.Errorf("get org: %w", err) } // ... more nesting } return fmt.Errorf("get user: %w", err) } // ... happy path } // Lower cognitive load: extract to focused functions func (s *Service) Process(ctx context.Context, id string) error { entity, err := s.resolveEntity(ctx, id) if err != nil { return fmt.Errorf("resolve entity: %w", err) } return s.processEntity(ctx, entity) } ``` ### Goroutine Soup Goroutines without clear lifecycle management are high cognitive load: ```go // High load: who owns these goroutines? When do they stop? func startWorkers() { go processQueue() go cleanupExpired() go sendNotifications() } // Lower load: explicit lifecycle with context and WaitGroup func startWorkers(ctx context.Context) *WorkerPool { pool := &WorkerPool{} pool.wg.Add(3) go pool.processQueue(ctx) go pool.cleanupExpired(ctx) go pool.sendNotifications(ctx) return pool } func (p *WorkerPool) Shutdown() { p.cancel() p.wg.Wait() } ``` ### Over-Generalization with Generics Generics add expressive power but also cognitive load. Use them sparingly: ```go // High load: generic just because you can func Transform[T any, U any](items []T, fn func(T) U) []U { ... } // Lower load: concrete types when you have only one use case func transformOrders(orders []Order) []OrderSummary { ... } ``` Extract generics only when you genuinely have multiple type applications. ### Init Functions `init()` functions run invisibly and create hidden state: ```go // High load: what order do these run? What state do they set? func init() { db = mustConnectDB() cache = newCache() } // Lower load: explicit initialization in main or constructor func main() { db := mustConnectDB(cfg.DatabaseURL) cache := newCache(cfg.CacheSize) svc := NewService(db, cache) // ... } ``` ### Long Interfaces An interface with 10 methods forces implementors to understand all 10 methods before reasoning about any: ```go // High cognitive load: 10 methods before you can implement type UserRepository interface { Create(ctx context.Context, u User) error GetByID(ctx context.Context, id UserID) (User, error) GetByEmail(ctx context.Context, email string) (User, error) Update(ctx context.Context, u User) error Delete(ctx context.Context, id UserID) error List(ctx context.Context, filter Filter) ([]User, error) Count(ctx context.Context, filter Filter) (int64, error) Exists(ctx context.Context, id UserID) (bool, error) Lock(ctx context.Context, id UserID) error Unlock(ctx context.Context, id UserID) error } // Lower load: separate by use case type UserReader interface { GetByID(ctx context.Context, id UserID) (User, error) GetByEmail(ctx context.Context, email string) (User, error) } type UserWriter interface { Create(ctx context.Context, u User) error Update(ctx context.Context, u User) error Delete(ctx context.Context, id UserID) error } ``` ## When to Measure - Before starting refactoring work: establish a baseline - When a package/file is frequently the source of bugs - When team members consistently report confusion about a subsystem - Before a major feature addition that will touch complex code ## Hotspot Identification Process 1. **Size sweep:** Find files > 400 LOC and functions > 50 LOC ```bash find . -name "*.go" ! -name "*_test.go" | xargs wc -l | sort -rn | head -20 ``` 2. **Complexity sweep:** Find high cyclomatic complexity ```bash gocyclo -over 10 ./... ``` 3. **Naming sweep:** Find packages with vague names ```bash ls internal/ # Are there `util`, `common`, `helpers`? ``` 4. **Coupling sweep:** Find files with many imports ```bash grep -l "^import" **/*.go | xargs grep -c '"' | sort -t: -k2 -rn | head -10 ``` 5. **Duplication sweep:** ```bash # Look for copied error strings, identical function signatures grep -rn "TODO\|FIXME\|HACK" --include="*.go" # Technical debt markers ``` ## Reduction Strategies | High CLI Dimension | Primary Fix | |-------------------|-------------| | D1: Structural Complexity | Extract function, replace conditional with polymorphism | | D2: Nesting Depth | Guard clauses, early returns | | D3: Volume/Size | Extract type, split package | | D4: Naming | Rename (most impactful, cheapest refactoring) | | D5: Coupling | Introduce interface, apply DIP | | D6: Cohesion | Extract type, reorganize package | | D7: Duplication | Extract shared function (after Rule of Three) | | D8: Navigability | Reorganize package structure, add package docs | ## Cross-References - Load `refactoring` skill for systematic techniques to reduce identified hotspots - Load `solid` skill for structural fixes (coupling, cohesion) - Load `clean-code` skill for naming improvements