Files
skills/cognitive-load/SKILL.md
Mathias d6a71e370e
Some checks failed
release / tag (push) Has been cancelled
chore: bootstrap skills library — 19 skills + installer + CI auto-tag
Phase 1 of mathias/skills extraction (infra#62 Track D — homelab
next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill
dirs + SKILLS_INDEX.md) and adds the installation surface:

- Taskfile.yml — install / update / list / release / check targets
- install.sh — bootstrap installer for hosts without Task. Idempotent
  symlink wirer; default checkout at ~/.local/share/skills/ on every
  host; SKILLS_REF env var pins a tag (default: main).
- .gitea/workflows/release.yml — auto-tag every push to main by
  Bump-Type footer (major/minor/patch, default patch). Skipped when
  commit contains [skip-release].
- README — usage, versioning, contribution flow, secret-hygiene rule.

Phase 1 wires Claude Code only (~/.claude/skills/<name> global +
<repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode,
antigravity, and gitea-resident agents (cobalt-dingo, agentsquad)
once their skill conventions are researched.

Public repo, markdown-only — no secrets, no client names. Verified
via pre-push grep before initial push.

[skip-release]
2026-05-24 14:59:54 +02:00

10 KiB
Raw Blame History

name, description
name description
cognitive-load 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 01000.

CLI Score Rating Scale

CLI Score Rating Action
0100 Excellent Model codebase
101250 Good Minor improvements
251400 Moderate Address hotspots
401600 Concerning Plan refactoring
601800 Poor Significant refactoring needed
801999 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: 510 (watch closely)
  • Complex: 1015 (consider refactoring)
  • Critical: > 15 (must refactor)

Detection:

# 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: 12 levels
  • Acceptable: 3 levels
  • Warning: 4 levels
  • Critical: 5+ levels

Fix: Early returns and guard clauses eliminate deep nesting:

// 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:

// 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:

# 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:

# 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:

// 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:

// 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:

// 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:

// 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:

// 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

    find . -name "*.go" ! -name "*_test.go" | xargs wc -l | sort -rn | head -20
    
  2. Complexity sweep: Find high cyclomatic complexity

    gocyclo -over 10 ./...
    
  3. Naming sweep: Find packages with vague names

    ls internal/ # Are there `util`, `common`, `helpers`?
    
  4. Coupling sweep: Find files with many imports

    grep -l "^import" **/*.go | xargs grep -c '"' | sort -t: -k2 -rn | head -10
    
  5. Duplication sweep:

    # 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