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

347 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 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:**
```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: 12 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