Просмотр исходного кода

feat(skills): Add go-ops, rust-ops, typescript-ops, docker-ops, ci-cd-ops, api-design-ops

Six new comprehensive skills (21,183 lines total):
- go-ops (4,730 lines): concurrency, error handling, testing, interfaces,
  generics, project structure, performance profiling
- rust-ops (5,256 lines): ownership, async/tokio, error handling, traits,
  serde, ecosystem (axum, sqlx, clap, tracing), testing
- typescript-ops (3,126 lines): type system, generics, utility types,
  mapped/conditional types, strict mode, Zod, ecosystem
- docker-ops (2,117 lines): Dockerfile best practices, multi-stage builds
  (Go/Rust/Node/Python), Compose patterns, optimization
- ci-cd-ops (2,745 lines): GitHub Actions, release automation
  (semantic-release, changesets, goreleaser), testing pipelines
- api-design-ops (3,209 lines): REST advanced, gRPC (protobuf, Go/Rust),
  GraphQL (schema, DataLoader, federation), API security

Also updates plugin.json to v1.8.0 with all 50 skills, fixes stale
-patterns references, and updates cross-references in tool-discovery,
README, AGENTS, and PLAN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0xDarkMatter 2 месяцев назад
Родитель
Сommit
baeda15ee9
51 измененных файлов с 21340 добавлено и 27 удалено
  1. 27 18
      .claude-plugin/plugin.json
  2. 1 1
      AGENTS.md
  3. 20 5
      README.md
  4. 1 1
      docs/PLAN.md
  5. 310 0
      skills/api-design-ops/SKILL.md
  6. 0 0
      skills/api-design-ops/assets/.gitkeep
  7. 640 0
      skills/api-design-ops/references/api-security.md
  8. 950 0
      skills/api-design-ops/references/graphql.md
  9. 736 0
      skills/api-design-ops/references/grpc.md
  10. 573 0
      skills/api-design-ops/references/rest-advanced.md
  11. 0 0
      skills/api-design-ops/scripts/.gitkeep
  12. 328 0
      skills/ci-cd-ops/SKILL.md
  13. 0 0
      skills/ci-cd-ops/assets/.gitkeep
  14. 783 0
      skills/ci-cd-ops/references/github-actions.md
  15. 647 0
      skills/ci-cd-ops/references/release-automation.md
  16. 987 0
      skills/ci-cd-ops/references/testing-pipelines.md
  17. 0 0
      skills/ci-cd-ops/scripts/.gitkeep
  18. 283 0
      skills/docker-ops/SKILL.md
  19. 0 0
      skills/docker-ops/assets/.gitkeep
  20. 743 0
      skills/docker-ops/references/compose-patterns.md
  21. 433 0
      skills/docker-ops/references/multi-stage-builds.md
  22. 658 0
      skills/docker-ops/references/optimization.md
  23. 0 0
      skills/docker-ops/scripts/.gitkeep
  24. 307 0
      skills/go-ops/SKILL.md
  25. 0 0
      skills/go-ops/assets/.gitkeep
  26. 976 0
      skills/go-ops/references/concurrency.md
  27. 704 0
      skills/go-ops/references/error-handling.md
  28. 721 0
      skills/go-ops/references/interfaces-generics.md
  29. 756 0
      skills/go-ops/references/performance.md
  30. 548 0
      skills/go-ops/references/project-structure.md
  31. 718 0
      skills/go-ops/references/testing.md
  32. 0 0
      skills/go-ops/scripts/.gitkeep
  33. 332 0
      skills/rust-ops/SKILL.md
  34. 0 0
      skills/rust-ops/assets/.gitkeep
  35. 1019 0
      skills/rust-ops/references/async-tokio.md
  36. 1005 0
      skills/rust-ops/references/ecosystem.md
  37. 655 0
      skills/rust-ops/references/error-handling.md
  38. 664 0
      skills/rust-ops/references/ownership-lifetimes.md
  39. 866 0
      skills/rust-ops/references/testing.md
  40. 715 0
      skills/rust-ops/references/traits-generics.md
  41. 0 0
      skills/rust-ops/scripts/.gitkeep
  42. 6 0
      skills/tool-discovery/SKILL.md
  43. 102 2
      skills/tool-discovery/references/skills-catalog.md
  44. 262 0
      skills/typescript-ops/SKILL.md
  45. 0 0
      skills/typescript-ops/assets/.gitkeep
  46. 563 0
      skills/typescript-ops/references/config-strict.md
  47. 621 0
      skills/typescript-ops/references/ecosystem.md
  48. 581 0
      skills/typescript-ops/references/generics-patterns.md
  49. 659 0
      skills/typescript-ops/references/type-system.md
  50. 440 0
      skills/typescript-ops/references/utility-types.md
  51. 0 0
      skills/typescript-ops/scripts/.gitkeep

+ 27 - 18
.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
   "name": "claude-mods",
-  "version": "1.6.0",
-  "description": "Custom commands, skills, and agents for Claude Code - session continuity, terminal canvas, 22 expert agents, 42 skills, 3 commands, 4 rules, modern CLI tools",
+  "version": "1.8.0",
+  "description": "Custom commands, skills, and agents for Claude Code - session continuity, 22 expert agents, 50 skills, 3 commands, 5 rules, modern CLI tools",
   "author": "0xDarkMatter",
   "repository": "https://github.com/0xDarkMatter/claude-mods",
   "license": "MIT",
@@ -47,54 +47,63 @@
       "agents/wrangler-expert.md"
     ],
     "skills": [
+      "skills/api-design-ops",
       "skills/atomise",
+      "skills/ci-cd-ops",
       "skills/claude-code-debug",
       "skills/claude-code-headless",
       "skills/claude-code-hooks",
       "skills/claude-code-templates",
-      "skills/cli-patterns",
+      "skills/cli-ops",
       "skills/code-stats",
       "skills/container-orchestration",
       "skills/data-processing",
       "skills/doc-scanner",
+      "skills/docker-ops",
       "skills/explain",
       "skills/file-search",
       "skills/find-replace",
       "skills/git-workflow",
+      "skills/go-ops",
       "skills/introspect",
       "skills/markitdown",
-      "skills/mcp-patterns",
+      "skills/mcp-ops",
+      "skills/postgres-ops",
       "skills/project-planner",
-      "skills/python-async-patterns",
-      "skills/python-cli-patterns",
-      "skills/python-database-patterns",
+      "skills/python-async-ops",
+      "skills/python-cli-ops",
+      "skills/python-database-ops",
       "skills/python-env",
-      "skills/python-fastapi-patterns",
-      "skills/python-observability-patterns",
-      "skills/python-pytest-patterns",
-      "skills/python-typing-patterns",
-      "skills/rest-patterns",
+      "skills/python-fastapi-ops",
+      "skills/python-observability-ops",
+      "skills/python-pytest-ops",
+      "skills/python-typing-ops",
+      "skills/rest-ops",
       "skills/review",
+      "skills/rust-ops",
       "skills/screenshot",
-      "skills/security-patterns",
+      "skills/security-ops",
       "skills/setperms",
       "skills/skill-creator",
       "skills/spawn",
-      "skills/sql-patterns",
+      "skills/sql-ops",
       "skills/sqlite-ops",
       "skills/structural-search",
-      "skills/tailwind-patterns",
+      "skills/tailwind-ops",
       "skills/task-runner",
       "skills/techdebt",
-      "skills/testing-patterns",
+      "skills/testing-ops",
       "skills/testgen",
-      "skills/tool-discovery"
+      "skills/tool-discovery",
+      "skills/typescript-ops",
+      "skills/unfold-admin"
     ],
     "rules": [
       "rules/cli-tools.md",
       "rules/commit-style.md",
       "rules/naming-conventions.md",
-      "rules/skill-agent-updates.md"
+      "rules/skill-agent-updates.md",
+      "rules/thinking.md"
     ],
     "output-styles": [
       "output-styles/vesper.md"

+ 1 - 1
AGENTS.md

@@ -5,7 +5,7 @@
 This is **claude-mods** - a collection of custom extensions for Claude Code:
 - **22 expert agents** for specialized domains (React, Python, Go, Rust, AWS, etc.)
 - **3 commands** for session management (/sync, /save) and experimental features (/canvas)
-- **44 skills** for CLI tools, patterns, workflows, and development tasks
+- **50 skills** for CLI tools, patterns, workflows, and development tasks
 - **Custom output styles** for response personality (e.g., Vesper)
 
 ## Installation

Разница между файлами не показана из-за своего большого размера
+ 20 - 5
README.md


+ 1 - 1
docs/PLAN.md

@@ -13,7 +13,7 @@
 | Component | Count | Notes |
 |-----------|-------|-------|
 | Agents | 22 | Domain experts (Python, Go, Rust, React, etc.) |
-| Skills | 44 | Operational skills, CLI tools, workflows, dev tasks |
+| Skills | 50 | Operational skills, CLI tools, workflows, dev tasks |
 | Commands | 3 | Session management (sync, save) + experimental (canvas) |
 | Rules | 5 | CLI tools, thinking, commit style, naming, skill-agent-updates |
 | Output Styles | 1 | Vesper personality |

+ 310 - 0
skills/api-design-ops/SKILL.md

@@ -0,0 +1,310 @@
+---
+name: api-design-ops
+description: "API design patterns for REST, gRPC, and GraphQL. Use for: api design, REST, gRPC, GraphQL, protobuf, schema design, api versioning, pagination, rate limiting, error format, OpenAPI, API authentication, JWT, OAuth2, API gateway, webhook, idempotency."
+allowed-tools: "Read Write Bash"
+related-skills: [rest-ops, security-ops, go-ops, rust-ops, typescript-ops]
+---
+
+# API Design Ops
+
+Comprehensive API design patterns covering REST (advanced), gRPC, and GraphQL. This skill provides decision frameworks, design patterns, and implementation guidance for building production APIs.
+
+## API Style Decision Tree
+
+```
+What kind of API do you need?
+|
++-- Internal microservice-to-microservice?
+|   +-- High throughput, low latency needed? --> gRPC
+|   +-- Streaming (real-time data, logs)? --> gRPC (bidirectional streaming)
+|   +-- Simple request/response, team comfort? --> REST
+|
++-- Public-facing API?
+|   +-- Third-party developers consuming it? --> REST (widest compatibility)
+|   +-- Mobile app with varied data needs? --> GraphQL
+|   +-- Browser-only, simple CRUD? --> REST
+|
++-- Frontend for your own app?
+|   +-- Multiple clients with different data shapes? --> GraphQL
+|   +-- Single client, straightforward data? --> REST
+|   +-- Real-time updates needed? --> GraphQL subscriptions or SSE
+|
++-- IoT / embedded / constrained devices?
+|   +-- Binary efficiency matters? --> gRPC
+|   +-- HTTP-only environments? --> REST
+```
+
+### Quick Comparison
+
+| Concern | REST | gRPC | GraphQL |
+|---------|------|------|---------|
+| Transport | HTTP/1.1+ | HTTP/2 | HTTP (any) |
+| Serialization | JSON (text) | Protobuf (binary) | JSON (text) |
+| Schema | OpenAPI (optional) | .proto (required) | SDL (required) |
+| Browser support | Native | Via gRPC-Web/Connect | Native |
+| Caching | HTTP caching built-in | Custom | Custom (normalized) |
+| Learning curve | Low | Medium | Medium-High |
+| Code generation | Optional | Required | Optional but recommended |
+| Streaming | SSE, WebSocket | Native (4 patterns) | Subscriptions |
+| Over-fetching | Common problem | No (typed) | Solved by design |
+| File uploads | Multipart native | Chunked streaming | Multipart spec (awkward) |
+
+## REST Resource Design Quick Reference
+
+### Resource Naming
+
+```
+GET    /users                  # Collection
+GET    /users/{id}             # Singleton
+GET    /users/{id}/orders      # Sub-collection
+POST   /users                  # Create
+PUT    /users/{id}             # Full replace
+PATCH  /users/{id}             # Partial update
+DELETE /users/{id}             # Remove
+
+# Naming rules:
+# - Plural nouns for collections: /users NOT /user
+# - Kebab-case for multi-word: /line-items NOT /lineItems
+# - No verbs in URLs: POST /orders NOT POST /create-order
+# - Max 3 levels deep: /users/{id}/orders (not /users/{id}/orders/{oid}/items/{iid}/details)
+```
+
+### HTTP Methods and Status Codes
+
+| Method | Success | Empty | Invalid | Not Found | Conflict |
+|--------|---------|-------|---------|-----------|----------|
+| GET | 200 | 200 (empty array) | 400 | 404 | - |
+| POST | 201 + Location | - | 400/422 | - | 409 |
+| PUT | 200 | - | 400/422 | 404 | 409 |
+| PATCH | 200 | - | 400/422 | 404 | 409 |
+| DELETE | 204 | 204 (already gone) | 400 | 404 | 409 |
+
+### HATEOAS (When Worth It)
+
+Use when: public APIs where discoverability matters, long-lived APIs, APIs that evolve frequently.
+Skip when: internal microservices, mobile backends, tight coupling is acceptable.
+
+```json
+{
+  "id": "order-123",
+  "status": "shipped",
+  "_links": {
+    "self": { "href": "/orders/order-123" },
+    "track": { "href": "/orders/order-123/tracking" },
+    "cancel": { "href": "/orders/order-123", "method": "DELETE" }
+  }
+}
+```
+
+## Pagination Decision Tree
+
+```
+What's your data like?
+|
++-- Stable data, UI needs "jump to page 5"?
+|   --> Offset pagination: ?page=5&per_page=20
+|   Tradeoff: Slow on large offsets (OFFSET 10000), inconsistent with inserts
+|
++-- Large dataset, forward-only traversal?
+|   --> Cursor pagination: ?after=eyJpZCI6MTIzfQ&limit=20
+|   Tradeoff: No random page access, but consistent and fast
+|
++-- Real-time feed, ordered by timestamp or ID?
+|   --> Keyset pagination: ?created_after=2024-01-01T00:00:00Z&limit=20
+|   Tradeoff: Requires a unique, sequential column; no page jumping
+```
+
+### Response Envelope
+
+```json
+{
+  "data": [...],
+  "pagination": {
+    "total": 1432,
+    "limit": 20,
+    "has_more": true,
+    "next_cursor": "eyJpZCI6MTQzMn0="
+  }
+}
+```
+
+## Error Response Format (RFC 7807)
+
+All APIs should use Problem Details (RFC 7807 / RFC 9457):
+
+```json
+{
+  "type": "https://api.example.com/errors/insufficient-funds",
+  "title": "Insufficient Funds",
+  "status": 422,
+  "detail": "Account xxxx-1234 has a balance of $10.00, but the transfer requires $25.00.",
+  "instance": "/transfers/txn-abc-123",
+  "balance": 1000,
+  "required": 2500
+}
+```
+
+### Field Reference
+
+| Field | Required | Description |
+|-------|----------|-------------|
+| `type` | Yes | URI identifying the error type (stable, documentable) |
+| `title` | Yes | Human-readable summary (same for all instances of this type) |
+| `status` | Yes | HTTP status code |
+| `detail` | Yes | Human-readable explanation specific to this occurrence |
+| `instance` | No | URI identifying the specific occurrence |
+| (extensions) | No | Additional machine-readable fields |
+
+### Validation Errors
+
+```json
+{
+  "type": "https://api.example.com/errors/validation",
+  "title": "Validation Failed",
+  "status": 422,
+  "detail": "The request body contains 2 validation errors.",
+  "errors": [
+    { "field": "email", "message": "Must be a valid email address", "code": "invalid_format" },
+    { "field": "age", "message": "Must be at least 18", "code": "out_of_range", "min": 18 }
+  ]
+}
+```
+
+## Versioning Strategies
+
+| Strategy | Example | Pros | Cons |
+|----------|---------|------|------|
+| URL path | `/v2/users` | Obvious, cacheable, easy routing | URL pollution, hard to sunset |
+| Accept header | `Accept: application/vnd.api.v2+json` | Clean URLs, content negotiation | Hidden, harder to test |
+| Query param | `/users?version=2` | Easy to add | Pollutes query string, caching issues |
+| Date-based | `API-Version: 2024-01-15` | Granular evolution (Stripe style) | Complex implementation |
+
+### Recommendation
+
+- **Public APIs**: URL path versioning (`/v1/`) - simplicity wins
+- **Internal APIs**: Header or no versioning (deploy in lockstep)
+- **Evolving APIs**: Date-based (Stripe model) if you have the engineering investment
+
+### Breaking Change Rules
+
+A breaking change is anything that can cause existing clients to fail:
+- Removing a field from a response
+- Renaming a field
+- Changing a field's type
+- Adding a required field to a request
+- Changing URL structure
+- Changing error formats
+- Removing an endpoint
+
+Non-breaking (safe):
+- Adding optional fields to requests
+- Adding fields to responses
+- Adding new endpoints
+- Adding new enum values (if client handles unknown values)
+
+## Rate Limiting Design
+
+### Algorithms
+
+| Algorithm | Behavior | Use When |
+|-----------|----------|----------|
+| Token bucket | Allows bursts, refills at steady rate | General API rate limiting |
+| Sliding window | Smooth distribution, no burst | Strict fairness needed |
+| Fixed window | Simple, potential burst at boundary | Low-stakes limiting |
+| Leaky bucket | Constant output rate | Queue processing |
+
+### Response Headers
+
+```
+X-RateLimit-Limit: 1000          # Max requests per window
+X-RateLimit-Remaining: 743       # Requests left in current window
+X-RateLimit-Reset: 1672531200    # Unix timestamp when window resets
+Retry-After: 30                  # Seconds to wait (on 429)
+```
+
+### 429 Response Body
+
+```json
+{
+  "type": "https://api.example.com/errors/rate-limit-exceeded",
+  "title": "Rate Limit Exceeded",
+  "status": 429,
+  "detail": "You have exceeded 1000 requests per hour. Try again in 30 seconds.",
+  "retry_after": 30
+}
+```
+
+## Idempotency
+
+### Which Methods Need Idempotency Keys?
+
+| Method | Idempotent by spec? | Needs key? |
+|--------|---------------------|------------|
+| GET | Yes | No |
+| PUT | Yes | No (full replacement is naturally idempotent) |
+| DELETE | Yes | No |
+| PATCH | No | Recommended for critical operations |
+| POST | No | **Yes** (always for payments, orders, transfers) |
+
+### Implementation
+
+```
+POST /payments
+Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
+Content-Type: application/json
+
+{ "amount": 2500, "currency": "usd", "customer": "cust_123" }
+```
+
+Server-side:
+1. Receive request with `Idempotency-Key` header
+2. Check if key exists in store (Redis, DB)
+3. If exists: return stored response (same status code + body)
+4. If not: process request, store response keyed by idempotency key
+5. Keys expire after 24-48 hours
+
+## Authentication Overview
+
+| Method | Use When | Security Level |
+|--------|----------|----------------|
+| API Key | Server-to-server, internal, simple | Low-Medium |
+| JWT (Bearer) | Stateless auth, microservices | Medium-High |
+| OAuth2 + PKCE | Third-party access, user delegation | High |
+| mTLS | Service mesh, zero-trust infra | Very High |
+
+### Decision Guide
+
+```
+Who is authenticating?
+|
++-- Your own frontend? --> JWT (short-lived access + refresh token)
++-- Third-party developer? --> OAuth2 (client credentials for server, PKCE for SPA)
++-- Another internal service? --> mTLS or JWT with service accounts
++-- Quick prototype? --> API key (but plan migration)
+```
+
+## Gotchas Table
+
+| Gotcha | Problem | Prevention |
+|--------|---------|------------|
+| Breaking changes in "non-breaking" release | Client crashes | Additive-only policy, contract tests |
+| N+1 in REST APIs | 100 users = 101 queries | Compound documents, `?include=`, or GraphQL |
+| Over-fetching | Mobile gets 50 fields, needs 3 | Sparse fieldsets `?fields=id,name` or GraphQL |
+| Under-fetching | 3 requests to build one view | Composite endpoints or BFF pattern |
+| CORS misconfiguration | Frontend can't reach API | Explicit allowed origins, never `*` with credentials |
+| Missing Content-Type | 415 or silent parsing failure | Validate Content-Type on every mutation endpoint |
+| Large payloads without pagination | OOM, timeouts | Always paginate collections, set max page size |
+| Inconsistent date formats | Parsing hell | ISO 8601 everywhere: `2024-01-15T10:30:00Z` |
+| No request IDs | Impossible to debug | Generate `X-Request-ID`, propagate through services |
+| Enum evolution | New value breaks old client | Document that enums may grow, clients must handle unknown |
+| Missing idempotency | Duplicate charges, orders | Idempotency keys on all POST endpoints with side effects |
+| Unbounded query complexity | GraphQL DoS | Depth limiting, cost analysis, persisted queries |
+
+## Reference Files
+
+| File | Contents |
+|------|----------|
+| `references/rest-advanced.md` | Resource modeling, PATCH strategies, caching, webhooks, bulk ops |
+| `references/grpc.md` | Protobuf, service definitions, Go/Rust, streaming, error handling |
+| `references/graphql.md` | Schema design, resolvers, DataLoader, federation, performance |
+| `references/api-security.md` | JWT, OAuth2, CORS, rate limiting, OWASP API Top 10 |

+ 0 - 0
skills/api-design-ops/assets/.gitkeep


+ 640 - 0
skills/api-design-ops/references/api-security.md

@@ -0,0 +1,640 @@
+# API Security Patterns
+
+## Table of Contents
+
+- [API Key Management](#api-key-management)
+- [JWT (JSON Web Tokens)](#jwt-json-web-tokens)
+- [OAuth2 Flows](#oauth2-flows)
+- [CORS](#cors)
+- [Rate Limiting Implementation](#rate-limiting-implementation)
+- [Input Validation](#input-validation)
+- [API Versioning and Deprecation](#api-versioning-and-deprecation)
+- [Transport Security](#transport-security)
+- [OWASP API Security Top 10](#owasp-api-security-top-10)
+
+---
+
+## API Key Management
+
+### Generation
+
+```go
+import "crypto/rand"
+
+func generateAPIKey() (string, error) {
+    // 32 bytes = 256 bits of entropy
+    b := make([]byte, 32)
+    if _, err := rand.Read(b); err != nil {
+        return "", err
+    }
+    // Prefix for easy identification and revocation
+    return "sk_live_" + base64.URLEncoding.EncodeToString(b), nil
+}
+```
+
+### Storage
+
+```
+NEVER store API keys in plaintext.
+
+Store: hash(api_key) in database
+Lookup: hash(incoming_key), compare to stored hashes
+Display: show only last 4 chars to user ("sk_live_...a1b2")
+```
+
+```go
+import "crypto/sha256"
+
+func hashAPIKey(key string) string {
+    h := sha256.Sum256([]byte(key))
+    return hex.EncodeToString(h[:])
+}
+```
+
+### Scoping
+
+```json
+{
+  "key_id": "key_abc123",
+  "name": "Production Read-Only",
+  "permissions": ["read:users", "read:orders"],
+  "rate_limit": 1000,
+  "allowed_ips": ["203.0.113.0/24"],
+  "expires_at": "2025-01-15T00:00:00Z",
+  "created_at": "2024-01-15T00:00:00Z"
+}
+```
+
+### Rotation Strategy
+
+1. Generate new key
+2. Both old and new keys work (grace period: 24-72 hours)
+3. Client updates to new key
+4. Old key is revoked
+5. Log all key usage for audit
+
+## JWT (JSON Web Tokens)
+
+### Structure
+
+```
+header.payload.signature
+
+# Header
+{
+  "alg": "RS256",          # Algorithm (RS256, ES256 - avoid HS256 for APIs)
+  "typ": "JWT",
+  "kid": "key-2024-01"    # Key ID for rotation
+}
+
+# Payload (Claims)
+{
+  "iss": "https://auth.example.com",     # Issuer
+  "sub": "user-123",                      # Subject (user ID)
+  "aud": "https://api.example.com",       # Audience
+  "exp": 1705312200,                      # Expires (15 min from now)
+  "iat": 1705311300,                      # Issued at
+  "jti": "unique-token-id",               # JWT ID (for revocation)
+  "scope": "read:users write:orders",     # Permissions
+  "org_id": "org-456"                     # Custom claim
+}
+```
+
+### Signing and Verification (Go)
+
+```go
+import "github.com/golang-jwt/jwt/v5"
+
+// Sign (auth service)
+func createAccessToken(userID string, scopes []string) (string, error) {
+    claims := jwt.MapClaims{
+        "sub":   userID,
+        "scope": strings.Join(scopes, " "),
+        "exp":   time.Now().Add(15 * time.Minute).Unix(),
+        "iat":   time.Now().Unix(),
+        "iss":   "https://auth.example.com",
+    }
+
+    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
+    token.Header["kid"] = currentKeyID
+
+    return token.SignedString(privateKey)
+}
+
+// Verify (API service)
+func verifyToken(tokenString string) (*jwt.Token, error) {
+    return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+        // Validate algorithm
+        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
+            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+        }
+
+        // Look up public key by kid
+        kid, _ := token.Header["kid"].(string)
+        pubKey, err := getPublicKey(kid)
+        if err != nil {
+            return nil, fmt.Errorf("unknown key ID: %s", kid)
+        }
+
+        return pubKey, nil
+    },
+        jwt.WithValidMethods([]string{"RS256"}),
+        jwt.WithIssuer("https://auth.example.com"),
+        jwt.WithAudience("https://api.example.com"),
+    )
+}
+```
+
+### Refresh Token Flow
+
+```
+1. Login: POST /auth/login
+   Response: { access_token (15 min), refresh_token (7 days) }
+
+2. API calls: Authorization: Bearer <access_token>
+
+3. Token expired (401): POST /auth/refresh
+   Body: { refresh_token }
+   Response: { access_token (new, 15 min), refresh_token (rotated) }
+
+4. Refresh token expired/revoked: redirect to login
+```
+
+### Token Storage
+
+| Environment | Access Token | Refresh Token |
+|-------------|-------------|---------------|
+| Browser SPA | Memory (JS variable) | HttpOnly Secure cookie |
+| Mobile app | Secure storage (Keychain/Keystore) | Secure storage |
+| Server-to-server | Environment variable | Environment variable |
+
+**Never store tokens in:**
+- localStorage (XSS vulnerable)
+- sessionStorage (XSS vulnerable)
+- Non-HttpOnly cookies (XSS vulnerable)
+- URL parameters (logged, cached, leaked via Referer)
+
+## OAuth2 Flows
+
+### Authorization Code + PKCE (SPAs, Mobile)
+
+```
+1. Client generates: code_verifier (random 43-128 chars)
+   code_challenge = BASE64URL(SHA256(code_verifier))
+
+2. Redirect to authorization server:
+   GET /authorize?
+     response_type=code&
+     client_id=app-123&
+     redirect_uri=https://app.example.com/callback&
+     scope=read:profile write:orders&
+     state=random-csrf-token&
+     code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
+     code_challenge_method=S256
+
+3. User authenticates, consents
+
+4. Redirect back with code:
+   GET /callback?code=auth-code-xyz&state=random-csrf-token
+
+5. Exchange code for tokens:
+   POST /token
+   {
+     "grant_type": "authorization_code",
+     "code": "auth-code-xyz",
+     "redirect_uri": "https://app.example.com/callback",
+     "client_id": "app-123",
+     "code_verifier": "the-original-random-string"
+   }
+
+6. Response:
+   {
+     "access_token": "eyJ...",
+     "token_type": "Bearer",
+     "expires_in": 900,
+     "refresh_token": "rt_...",
+     "scope": "read:profile write:orders"
+   }
+```
+
+### Client Credentials (Server-to-Server)
+
+```
+POST /token
+Content-Type: application/x-www-form-urlencoded
+
+grant_type=client_credentials&
+client_id=service-abc&
+client_secret=secret-xyz&
+scope=read:users
+
+Response:
+{
+  "access_token": "eyJ...",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+```
+
+### Device Flow (CLI Tools, Smart TVs)
+
+```
+1. Device requests code:
+   POST /device/code
+   { "client_id": "cli-app", "scope": "read:profile" }
+
+   Response:
+   {
+     "device_code": "device-code-abc",
+     "user_code": "ABCD-1234",
+     "verification_uri": "https://auth.example.com/device",
+     "expires_in": 600,
+     "interval": 5
+   }
+
+2. Display to user: "Go to https://auth.example.com/device and enter ABCD-1234"
+
+3. Device polls (every 5 seconds):
+   POST /token
+   { "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
+     "device_code": "device-code-abc", "client_id": "cli-app" }
+
+   While pending: { "error": "authorization_pending" }
+   When approved: { "access_token": "eyJ...", ... }
+```
+
+### Flow Selection Guide
+
+| Scenario | Flow |
+|----------|------|
+| SPA (browser) | Authorization Code + PKCE |
+| Mobile app | Authorization Code + PKCE |
+| Server-to-server | Client Credentials |
+| CLI tool | Device Flow |
+| Legacy (avoid) | Implicit (deprecated), ROPC (deprecated) |
+
+## CORS
+
+### Configuration
+
+```go
+func corsMiddleware(next http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        origin := r.Header.Get("Origin")
+
+        // Whitelist specific origins (NEVER use * with credentials)
+        allowedOrigins := map[string]bool{
+            "https://app.example.com":     true,
+            "https://staging.example.com": true,
+        }
+
+        if allowedOrigins[origin] {
+            w.Header().Set("Access-Control-Allow-Origin", origin)
+            w.Header().Set("Access-Control-Allow-Credentials", "true")
+            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
+            w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-ID, Idempotency-Key")
+            w.Header().Set("Access-Control-Expose-Headers", "X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset")
+            w.Header().Set("Access-Control-Max-Age", "86400")  // Cache preflight for 24h
+        }
+
+        // Handle preflight
+        if r.Method == "OPTIONS" {
+            w.WriteHeader(http.StatusNoContent)
+            return
+        }
+
+        next.ServeHTTP(w, r)
+    })
+}
+```
+
+### Common CORS Mistakes
+
+| Mistake | Risk | Fix |
+|---------|------|-----|
+| `Access-Control-Allow-Origin: *` with credentials | Credential theft | Whitelist specific origins |
+| Reflecting `Origin` header without validation | Any origin allowed | Check against whitelist |
+| Missing `Vary: Origin` | Cache poisoning | Add `Vary: Origin` header |
+| Not handling preflight (OPTIONS) | Mutations blocked | Return 204 for OPTIONS |
+| Allowing all headers | Header injection | Whitelist specific headers |
+
+## Rate Limiting Implementation
+
+### Token Bucket (Go + Redis)
+
+```go
+import "github.com/redis/go-redis/v9"
+
+type RateLimiter struct {
+    redis    *redis.Client
+    limit    int           // Max tokens
+    window   time.Duration // Refill window
+}
+
+func (rl *RateLimiter) Allow(ctx context.Context, key string) (bool, RateLimitInfo, error) {
+    now := time.Now().Unix()
+    windowKey := fmt.Sprintf("ratelimit:%s:%d", key, now/int64(rl.window.Seconds()))
+
+    pipe := rl.redis.Pipeline()
+    incr := pipe.Incr(ctx, windowKey)
+    pipe.Expire(ctx, windowKey, rl.window)
+    _, err := pipe.Exec(ctx)
+    if err != nil {
+        return false, RateLimitInfo{}, err
+    }
+
+    count := incr.Val()
+    remaining := rl.limit - int(count)
+    if remaining < 0 {
+        remaining = 0
+    }
+
+    info := RateLimitInfo{
+        Limit:     rl.limit,
+        Remaining: remaining,
+        Reset:     time.Unix(((now/int64(rl.window.Seconds()))+1)*int64(rl.window.Seconds()), 0),
+    }
+
+    return count <= int64(rl.limit), info, nil
+}
+
+type RateLimitInfo struct {
+    Limit     int
+    Remaining int
+    Reset     time.Time
+}
+```
+
+### Middleware
+
+```go
+func rateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
+    return func(next http.Handler) http.Handler {
+        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+            // Key by API key, user ID, or IP
+            key := extractRateLimitKey(r)
+
+            allowed, info, err := limiter.Allow(r.Context(), key)
+            if err != nil {
+                http.Error(w, "Internal Server Error", 500)
+                return
+            }
+
+            // Always set rate limit headers
+            w.Header().Set("X-RateLimit-Limit", strconv.Itoa(info.Limit))
+            w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(info.Remaining))
+            w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(info.Reset.Unix(), 10))
+
+            if !allowed {
+                retryAfter := int(time.Until(info.Reset).Seconds())
+                w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
+                w.WriteHeader(http.StatusTooManyRequests)
+                json.NewEncoder(w).Encode(map[string]interface{}{
+                    "type":        "https://api.example.com/errors/rate-limit",
+                    "title":       "Rate Limit Exceeded",
+                    "status":      429,
+                    "detail":      fmt.Sprintf("Rate limit of %d requests per hour exceeded", info.Limit),
+                    "retry_after": retryAfter,
+                })
+                return
+            }
+
+            next.ServeHTTP(w, r)
+        })
+    }
+}
+```
+
+### Tiered Rate Limits
+
+| Tier | Requests/Hour | Burst | Use Case |
+|------|---------------|-------|----------|
+| Free | 100 | 10/min | Trial users |
+| Basic | 1,000 | 100/min | Paid individuals |
+| Pro | 10,000 | 500/min | Teams |
+| Enterprise | 100,000 | 2,000/min | Custom SLA |
+
+## Input Validation
+
+### Validate at the Boundary
+
+```go
+// Use a validation library, not manual checks
+import "github.com/go-playground/validator/v10"
+
+type CreateUserRequest struct {
+    Name     string `json:"name" validate:"required,min=2,max=100"`
+    Email    string `json:"email" validate:"required,email"`
+    Age      int    `json:"age" validate:"omitempty,min=13,max=150"`
+    Website  string `json:"website" validate:"omitempty,url"`
+    Role     string `json:"role" validate:"required,oneof=admin member viewer"`
+    Password string `json:"password" validate:"required,min=8,max=128"`
+}
+
+var validate = validator.New()
+
+func handleCreateUser(w http.ResponseWriter, r *http.Request) {
+    var req CreateUserRequest
+    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+        respondError(w, 400, "Invalid JSON body")
+        return
+    }
+
+    if err := validate.Struct(req); err != nil {
+        validationErrors := err.(validator.ValidationErrors)
+        respondValidationErrors(w, validationErrors)
+        return
+    }
+
+    // Input is now validated - proceed
+}
+```
+
+### Validation Checklist
+
+| Check | Why |
+|-------|-----|
+| Max request body size | Prevent memory exhaustion |
+| String length limits | Prevent storage abuse |
+| Enum validation | Reject unknown values |
+| URL validation | Prevent SSRF (whitelist schemes) |
+| Email format | Reject obviously invalid |
+| Numeric bounds | Prevent overflow, nonsensical values |
+| Array max length | Prevent excessive processing |
+| Nested object depth | Prevent deep recursion |
+| Content-Type validation | Ensure expected format |
+| UTF-8 validation | Prevent encoding attacks |
+
+### Schema Validation (OpenAPI)
+
+```go
+import "github.com/getkin/kin-openapi/openapi3filter"
+
+// Validate requests against OpenAPI spec automatically
+router, _ := gorillamux.NewRouter(spec)
+
+func validationMiddleware(next http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        route, pathParams, _ := router.FindRoute(r)
+
+        input := &openapi3filter.RequestValidationInput{
+            Request:    r,
+            PathParams: pathParams,
+            Route:      route,
+        }
+
+        if err := openapi3filter.ValidateRequest(r.Context(), input); err != nil {
+            respondError(w, 400, err.Error())
+            return
+        }
+
+        next.ServeHTTP(w, r)
+    })
+}
+```
+
+## API Versioning and Deprecation
+
+### Deprecation Timeline
+
+```
+1. Announce deprecation (minimum 6 months before removal)
+   - Add Deprecation header to responses
+   - Update API documentation
+   - Email API key owners
+
+2. Warning period (3-6 months)
+   Deprecation: true
+   Sunset: Sat, 15 Jun 2025 00:00:00 GMT
+   Link: <https://docs.example.com/migration-guide>; rel="deprecation"
+
+3. Migration support
+   - Provide migration guide
+   - Offer parallel running of old and new versions
+   - Log deprecated endpoint usage for targeted outreach
+
+4. Removal
+   - Return 410 Gone with migration info
+   - Keep 410 response for 6+ months
+```
+
+### Sunset Header (RFC 8594)
+
+```
+HTTP/1.1 200 OK
+Sunset: Sat, 15 Jun 2025 00:00:00 GMT
+Deprecation: true
+Link: <https://api.example.com/v3/users>; rel="successor-version"
+```
+
+## Transport Security
+
+### TLS Configuration
+
+```go
+tlsConfig := &tls.Config{
+    MinVersion: tls.VersionTLS12,
+    CipherSuites: []uint16{
+        tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+        tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+        tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+        tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+    },
+    PreferServerCipherSuites: true,
+}
+
+server := &http.Server{
+    TLSConfig: tlsConfig,
+    // ...
+}
+```
+
+### Security Headers
+
+```go
+func securityHeaders(next http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+        w.Header().Set("X-Content-Type-Options", "nosniff")
+        w.Header().Set("X-Frame-Options", "DENY")
+        w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
+        w.Header().Set("Cache-Control", "no-store")   // For API responses with sensitive data
+        w.Header().Set("X-Request-ID", generateRequestID())
+        next.ServeHTTP(w, r)
+    })
+}
+```
+
+## OWASP API Security Top 10
+
+### 2023 Edition
+
+| # | Risk | Description | Prevention |
+|---|------|-------------|------------|
+| 1 | **Broken Object-Level Auth (BOLA)** | User accesses other users' objects via ID manipulation | Check ownership in every endpoint: `WHERE id = ? AND user_id = ?` |
+| 2 | **Broken Authentication** | Weak auth, credential stuffing, missing rate limits on login | Rate limit login, use strong password hashing (argon2id), MFA |
+| 3 | **Broken Object Property-Level Auth** | Mass assignment, excessive data exposure | Explicit allowlists for input fields, separate input/output DTOs |
+| 4 | **Unrestricted Resource Consumption** | No rate limits, unbounded queries, large payloads | Rate limiting, pagination limits, request size limits, timeouts |
+| 5 | **Broken Function-Level Auth** | Admin endpoints accessible to regular users | Role-based access control, deny by default, test auth on every endpoint |
+| 6 | **Unrestricted Access to Sensitive Business Flows** | Automated abuse (ticket scalping, spam) | Rate limiting, CAPTCHA, device fingerprinting, business logic limits |
+| 7 | **Server-Side Request Forgery (SSRF)** | API fetches attacker-controlled URLs | Validate/whitelist URLs, block internal networks, use allowlists |
+| 8 | **Security Misconfiguration** | Default configs, verbose errors, missing CORS | Harden defaults, strip stack traces in production, audit configs |
+| 9 | **Improper Inventory Management** | Shadow APIs, deprecated endpoints still active | API gateway, version inventory, automated discovery, sunset old versions |
+| 10 | **Unsafe Consumption of APIs** | Trusting third-party API responses without validation | Validate all external API responses, set timeouts, use TLS |
+
+### BOLA Prevention (Most Common API Vulnerability)
+
+```go
+// BAD: Only checks if resource exists
+func getOrder(w http.ResponseWriter, r *http.Request) {
+    orderID := chi.URLParam(r, "id")
+    order, _ := db.GetOrder(orderID)  // Anyone can access any order!
+    json.NewEncoder(w).Encode(order)
+}
+
+// GOOD: Checks ownership
+func getOrder(w http.ResponseWriter, r *http.Request) {
+    orderID := chi.URLParam(r, "id")
+    userID := r.Context().Value(userIDKey).(string)
+
+    order, err := db.GetOrderForUser(orderID, userID)
+    // SQL: SELECT * FROM orders WHERE id = $1 AND user_id = $2
+    if err != nil {
+        respondError(w, 404, "Order not found")  // 404, not 403 (don't leak existence)
+        return
+    }
+    json.NewEncoder(w).Encode(order)
+}
+```
+
+### Mass Assignment Prevention
+
+```go
+// BAD: Binding all fields from request
+func updateUser(w http.ResponseWriter, r *http.Request) {
+    var user User
+    json.NewDecoder(r.Body).Decode(&user)  // Attacker sets role=admin!
+    db.Save(&user)
+}
+
+// GOOD: Explicit allowlist of updatable fields
+type UpdateUserInput struct {
+    Name   *string `json:"name"`
+    Email  *string `json:"email"`
+    // role is NOT here - cannot be set via API
+}
+
+func updateUser(w http.ResponseWriter, r *http.Request) {
+    var input UpdateUserInput
+    json.NewDecoder(r.Body).Decode(&input)
+
+    user, _ := db.GetUser(userID)
+    if input.Name != nil {
+        user.Name = *input.Name
+    }
+    if input.Email != nil {
+        user.Email = *input.Email
+    }
+    db.Save(&user)
+}
+```

+ 950 - 0
skills/api-design-ops/references/graphql.md

@@ -0,0 +1,950 @@
+# GraphQL Patterns
+
+## Table of Contents
+
+- [Schema Design](#schema-design)
+- [Resolver Patterns](#resolver-patterns)
+- [Authentication and Authorization](#authentication-and-authorization)
+- [Error Handling](#error-handling)
+- [Pagination](#pagination)
+- [Fragments, Interfaces, Unions](#fragments-interfaces-unions)
+- [Schema Stitching and Federation](#schema-stitching-and-federation)
+- [Code-First vs Schema-First](#code-first-vs-schema-first)
+- [Performance](#performance)
+- [TypeScript and GraphQL](#typescript-and-graphql)
+- [When GraphQL Is Overkill](#when-graphql-is-overkill)
+
+---
+
+## Schema Design
+
+### Types, Queries, and Mutations
+
+```graphql
+# Scalar types: String, Int, Float, Boolean, ID
+# Custom scalars for domain types
+scalar DateTime
+scalar Email
+scalar URL
+
+type User {
+  id: ID!
+  name: String!
+  email: Email!
+  avatar: URL
+  role: UserRole!
+  posts(first: Int = 10, after: String): PostConnection!
+  createdAt: DateTime!
+}
+
+enum UserRole {
+  ADMIN
+  MEMBER
+  VIEWER
+}
+
+# Queries - read operations
+type Query {
+  user(id: ID!): User
+  users(
+    first: Int = 20
+    after: String
+    filter: UserFilter
+    orderBy: UserOrderBy = CREATED_AT_DESC
+  ): UserConnection!
+  me: User!              # Current authenticated user
+}
+
+# Mutations - write operations
+type Mutation {
+  createUser(input: CreateUserInput!): CreateUserPayload!
+  updateUser(input: UpdateUserInput!): UpdateUserPayload!
+  deleteUser(id: ID!): DeleteUserPayload!
+}
+
+# Input types (separate from output types)
+input CreateUserInput {
+  name: String!
+  email: Email!
+  role: UserRole = MEMBER
+}
+
+input UpdateUserInput {
+  id: ID!
+  name: String
+  email: Email
+  role: UserRole
+}
+
+input UserFilter {
+  role: UserRole
+  search: String
+  createdAfter: DateTime
+}
+
+enum UserOrderBy {
+  CREATED_AT_ASC
+  CREATED_AT_DESC
+  NAME_ASC
+  NAME_DESC
+}
+```
+
+### Mutation Payloads
+
+Always return a payload type (not the entity directly):
+
+```graphql
+type CreateUserPayload {
+  user: User!
+  clientMutationId: String   # Relay convention
+}
+
+type UpdateUserPayload {
+  user: User!
+}
+
+type DeleteUserPayload {
+  deletedId: ID!
+  success: Boolean!
+}
+
+# For operations that can partially fail
+type BulkDeleteUsersPayload {
+  deletedIds: [ID!]!
+  errors: [BulkError!]!
+}
+
+type BulkError {
+  id: ID!
+  message: String!
+  code: ErrorCode!
+}
+```
+
+### Subscriptions
+
+```graphql
+type Subscription {
+  # Simple subscription
+  orderStatusChanged(orderId: ID!): Order!
+
+  # Filtered subscription
+  newMessage(channelId: ID!): Message!
+
+  # With initial state
+  userPresence(teamId: ID!): PresenceEvent!
+}
+
+enum PresenceEventType {
+  ONLINE
+  OFFLINE
+  AWAY
+}
+
+type PresenceEvent {
+  user: User!
+  type: PresenceEventType!
+  timestamp: DateTime!
+}
+```
+
+## Resolver Patterns
+
+### Basic Resolver Structure (TypeScript)
+
+```typescript
+const resolvers: Resolvers = {
+  Query: {
+    user: async (_, { id }, context) => {
+      return context.dataSources.users.findById(id);
+    },
+    users: async (_, { first, after, filter }, context) => {
+      return context.dataSources.users.findMany({ first, after, filter });
+    },
+    me: async (_, __, context) => {
+      if (!context.currentUser) {
+        throw new AuthenticationError("Not authenticated");
+      }
+      return context.currentUser;
+    },
+  },
+
+  Mutation: {
+    createUser: async (_, { input }, context) => {
+      const user = await context.dataSources.users.create(input);
+      return { user };
+    },
+  },
+
+  // Field-level resolver (runs when field is requested)
+  User: {
+    posts: async (parent, { first, after }, context) => {
+      return context.dataSources.posts.findByUserId(parent.id, { first, after });
+    },
+    // Simple field mapping (usually not needed)
+    email: (parent) => parent.email,
+  },
+};
+```
+
+### The N+1 Problem and DataLoader
+
+Without DataLoader:
+```
+Query { users(first: 10) { posts { title } } }
+# 1 query for users + 10 queries for posts = 11 queries
+```
+
+With DataLoader:
+```typescript
+import DataLoader from "dataloader";
+
+// Create per-request DataLoader instances
+function createLoaders() {
+  return {
+    postsByUserId: new DataLoader<string, Post[]>(async (userIds) => {
+      // Single batched query: SELECT * FROM posts WHERE user_id IN (...)
+      const posts = await db.posts.findMany({
+        where: { userId: { in: [...userIds] } },
+      });
+
+      // Map results back to input order
+      const postsByUser = new Map<string, Post[]>();
+      for (const post of posts) {
+        const existing = postsByUser.get(post.userId) || [];
+        existing.push(post);
+        postsByUser.set(post.userId, existing);
+      }
+
+      return userIds.map((id) => postsByUser.get(id) || []);
+    }),
+
+    userById: new DataLoader<string, User | null>(async (ids) => {
+      const users = await db.users.findMany({
+        where: { id: { in: [...ids] } },
+      });
+      const userMap = new Map(users.map((u) => [u.id, u]));
+      return ids.map((id) => userMap.get(id) || null);
+    }),
+  };
+}
+
+// In resolver
+const resolvers = {
+  User: {
+    posts: (parent, args, context) => {
+      return context.loaders.postsByUserId.load(parent.id);
+    },
+  },
+  Post: {
+    author: (parent, args, context) => {
+      return context.loaders.userById.load(parent.authorId);
+    },
+  },
+};
+```
+
+## Authentication and Authorization
+
+### Context Setup
+
+```typescript
+// Server setup - extract user from token
+const server = new ApolloServer({
+  typeDefs,
+  resolvers,
+  context: async ({ req }) => {
+    const token = req.headers.authorization?.replace("Bearer ", "");
+    let currentUser = null;
+
+    if (token) {
+      try {
+        const decoded = await verifyJWT(token);
+        currentUser = await db.users.findById(decoded.sub);
+      } catch {
+        // Invalid token - currentUser remains null
+      }
+    }
+
+    return {
+      currentUser,
+      loaders: createLoaders(),
+      dataSources: createDataSources(),
+    };
+  },
+});
+```
+
+### Authorization Patterns
+
+**Directive-based (schema-level):**
+
+```graphql
+directive @auth(requires: UserRole = MEMBER) on FIELD_DEFINITION | OBJECT
+
+type Query {
+  users: [User!]! @auth(requires: ADMIN)
+  me: User! @auth
+}
+
+type User {
+  email: Email! @auth(requires: ADMIN)  # Only admins see emails
+  name: String!                          # Public field
+}
+```
+
+```typescript
+// Directive implementation
+class AuthDirective extends SchemaDirectiveVisitor {
+  visitFieldDefinition(field: GraphQLField<any, any>) {
+    const requiredRole = this.args.requires;
+    const originalResolve = field.resolve || defaultFieldResolver;
+
+    field.resolve = async (parent, args, context, info) => {
+      if (!context.currentUser) {
+        throw new AuthenticationError("Authentication required");
+      }
+      if (requiredRole && context.currentUser.role !== requiredRole) {
+        throw new ForbiddenError("Insufficient permissions");
+      }
+      return originalResolve(parent, args, context, info);
+    };
+  }
+}
+```
+
+**Resolver-level authorization:**
+
+```typescript
+const resolvers = {
+  Mutation: {
+    deleteUser: async (_, { id }, context) => {
+      // Only admins or the user themselves
+      if (context.currentUser.role !== "ADMIN" && context.currentUser.id !== id) {
+        throw new ForbiddenError("Cannot delete other users");
+      }
+      await context.dataSources.users.delete(id);
+      return { deletedId: id, success: true };
+    },
+  },
+};
+```
+
+## Error Handling
+
+### GraphQL Error Format
+
+```json
+{
+  "data": {
+    "createUser": null
+  },
+  "errors": [
+    {
+      "message": "Email already exists",
+      "locations": [{ "line": 2, "column": 3 }],
+      "path": ["createUser"],
+      "extensions": {
+        "code": "CONFLICT",
+        "field": "email",
+        "timestamp": "2024-01-15T10:30:00Z"
+      }
+    }
+  ]
+}
+```
+
+### Error Classification
+
+```typescript
+// Custom error classes
+class ValidationError extends GraphQLError {
+  constructor(message: string, field: string) {
+    super(message, {
+      extensions: {
+        code: "VALIDATION_ERROR",
+        field,
+      },
+    });
+  }
+}
+
+class BusinessRuleError extends GraphQLError {
+  constructor(message: string, rule: string) {
+    super(message, {
+      extensions: {
+        code: "BUSINESS_RULE_VIOLATION",
+        rule,
+      },
+    });
+  }
+}
+
+// Usage in resolvers
+const resolvers = {
+  Mutation: {
+    createUser: async (_, { input }, context) => {
+      if (!isValidEmail(input.email)) {
+        throw new ValidationError("Invalid email format", "email");
+      }
+
+      const existing = await context.dataSources.users.findByEmail(input.email);
+      if (existing) {
+        throw new BusinessRuleError("Email already registered", "unique_email");
+      }
+
+      const user = await context.dataSources.users.create(input);
+      return { user };
+    },
+  },
+};
+```
+
+### Partial Success Pattern
+
+```graphql
+type Mutation {
+  bulkCreateUsers(inputs: [CreateUserInput!]!): BulkCreateResult!
+}
+
+type BulkCreateResult {
+  users: [User!]!
+  errors: [CreateError!]!
+  totalRequested: Int!
+  totalCreated: Int!
+}
+
+type CreateError {
+  index: Int!        # Which input failed
+  message: String!
+  code: String!
+}
+```
+
+## Pagination
+
+### Relay Connection Spec
+
+```graphql
+type Query {
+  users(
+    first: Int       # Forward pagination
+    after: String    # Cursor
+    last: Int        # Backward pagination
+    before: String   # Cursor
+  ): UserConnection!
+}
+
+type UserConnection {
+  edges: [UserEdge!]!
+  pageInfo: PageInfo!
+  totalCount: Int      # Optional - expensive on large datasets
+}
+
+type UserEdge {
+  node: User!
+  cursor: String!      # Opaque cursor for this edge
+}
+
+type PageInfo {
+  hasNextPage: Boolean!
+  hasPreviousPage: Boolean!
+  startCursor: String
+  endCursor: String
+}
+```
+
+### Implementation
+
+```typescript
+async function connectionFromQuery<T>(
+  query: SelectQueryBuilder<T>,
+  args: { first?: number; after?: string; last?: number; before?: string }
+): Promise<Connection<T>> {
+  const limit = args.first || args.last || 20;
+  const maxLimit = 100;
+  const effectiveLimit = Math.min(limit, maxLimit);
+
+  let afterId: string | null = null;
+  if (args.after) {
+    afterId = Buffer.from(args.after, "base64").toString("utf8");
+  }
+
+  if (afterId) {
+    query = query.where("id > :afterId", { afterId });
+  }
+
+  // Fetch one extra to determine hasNextPage
+  const items = await query
+    .orderBy("id", "ASC")
+    .take(effectiveLimit + 1)
+    .getMany();
+
+  const hasNextPage = items.length > effectiveLimit;
+  const nodes = hasNextPage ? items.slice(0, effectiveLimit) : items;
+
+  const edges = nodes.map((node) => ({
+    node,
+    cursor: Buffer.from(node.id).toString("base64"),
+  }));
+
+  return {
+    edges,
+    pageInfo: {
+      hasNextPage,
+      hasPreviousPage: !!args.after,
+      startCursor: edges[0]?.cursor || null,
+      endCursor: edges[edges.length - 1]?.cursor || null,
+    },
+  };
+}
+```
+
+### Simple Pagination (Alternative)
+
+If Relay connections are overkill:
+
+```graphql
+type Query {
+  users(limit: Int = 20, offset: Int = 0): UserList!
+}
+
+type UserList {
+  items: [User!]!
+  total: Int!
+  hasMore: Boolean!
+}
+```
+
+## Fragments, Interfaces, Unions
+
+### Fragments (Client-Side Reuse)
+
+```graphql
+# Define reusable field sets
+fragment UserBasic on User {
+  id
+  name
+  avatar
+}
+
+fragment UserDetailed on User {
+  ...UserBasic
+  email
+  role
+  createdAt
+  posts(first: 5) {
+    edges {
+      node {
+        id
+        title
+      }
+    }
+  }
+}
+
+# Use in queries
+query {
+  me {
+    ...UserDetailed
+  }
+  users(first: 10) {
+    edges {
+      node {
+        ...UserBasic
+      }
+    }
+  }
+}
+```
+
+### Interfaces (Shared Fields)
+
+```graphql
+interface Node {
+  id: ID!
+}
+
+interface Timestamped {
+  createdAt: DateTime!
+  updatedAt: DateTime!
+}
+
+type User implements Node & Timestamped {
+  id: ID!
+  name: String!
+  createdAt: DateTime!
+  updatedAt: DateTime!
+}
+
+type Post implements Node & Timestamped {
+  id: ID!
+  title: String!
+  createdAt: DateTime!
+  updatedAt: DateTime!
+}
+
+# Query any Node by ID
+type Query {
+  node(id: ID!): Node
+}
+```
+
+### Unions (Polymorphic Results)
+
+```graphql
+union SearchResult = User | Post | Comment
+
+type Query {
+  search(query: String!): [SearchResult!]!
+}
+
+# Client query with type-specific fields
+query {
+  search(query: "graphql") {
+    ... on User {
+      id
+      name
+    }
+    ... on Post {
+      id
+      title
+      author { name }
+    }
+    ... on Comment {
+      id
+      body
+      post { title }
+    }
+  }
+}
+```
+
+```typescript
+// Resolver must include __typename
+const resolvers = {
+  SearchResult: {
+    __resolveType(obj: any) {
+      if (obj.email) return "User";
+      if (obj.title) return "Post";
+      if (obj.body) return "Comment";
+      return null;
+    },
+  },
+};
+```
+
+## Schema Stitching and Federation
+
+### Apollo Federation
+
+Split schema across microservices:
+
+```graphql
+# Users service
+type User @key(fields: "id") {
+  id: ID!
+  name: String!
+  email: String!
+}
+
+type Query {
+  user(id: ID!): User
+  me: User
+}
+```
+
+```graphql
+# Orders service - extends User from another service
+type User @key(fields: "id") {
+  id: ID!
+  orders: [Order!]!     # Added by this service
+}
+
+type Order @key(fields: "id") {
+  id: ID!
+  total: Int!
+  status: OrderStatus!
+  user: User!
+}
+
+type Query {
+  order(id: ID!): Order
+}
+```
+
+```typescript
+// Orders service resolver
+const resolvers = {
+  User: {
+    // Reference resolver - how to fetch User stub
+    __resolveReference(ref: { id: string }, context: Context) {
+      // Only need to resolve fields this service owns
+      return { id: ref.id };
+    },
+    orders(user: { id: string }, _, context: Context) {
+      return context.dataSources.orders.findByUserId(user.id);
+    },
+  },
+};
+```
+
+### When to Federate
+
+| Use Federation | Don't Federate |
+|----------------|----------------|
+| Multiple teams own different domains | Single team, single service |
+| Independent deployment needed | Monolith or simple microservices |
+| Schema > 500 types | Schema < 100 types |
+| Different scaling requirements | Uniform load |
+
+## Code-First vs Schema-First
+
+### Schema-First (SDL)
+
+Write `.graphql` files, generate types:
+
+```graphql
+# schema.graphql
+type Query {
+  user(id: ID!): User
+}
+```
+
+```typescript
+// Generated types (via graphql-codegen)
+export type QueryUserArgs = { id: string };
+export type QueryResolvers = {
+  user?: Resolver<Maybe<User>, {}, Context, QueryUserArgs>;
+};
+```
+
+**Pros**: Schema is the contract, readable, tooling-friendly
+**Cons**: Types and schema can drift, boilerplate
+
+### Code-First
+
+Write TypeScript/Go, generate schema:
+
+```typescript
+// Using Pothos (TypeScript)
+const builder = new SchemaBuilder<{
+  Context: Context;
+  Scalars: { DateTime: { Input: Date; Output: Date } };
+}>({});
+
+const UserType = builder.objectRef<User>("User").implement({
+  fields: (t) => ({
+    id: t.exposeID("id"),
+    name: t.exposeString("name"),
+    email: t.exposeString("email"),
+    posts: t.field({
+      type: [PostType],
+      resolve: (user, _, context) =>
+        context.loaders.postsByUserId.load(user.id),
+    }),
+  }),
+});
+
+builder.queryField("user", (t) =>
+  t.field({
+    type: UserType,
+    nullable: true,
+    args: { id: t.arg.id({ required: true }) },
+    resolve: (_, { id }, context) =>
+      context.dataSources.users.findById(id),
+  })
+);
+```
+
+**Pros**: Single source of truth, type-safe, refactor-friendly
+**Cons**: Schema less visible, framework lock-in
+
+### Recommendation
+
+- **Schema-first**: Public APIs, multi-language teams, API-design-driven
+- **Code-first**: TypeScript backends, rapid iteration, small teams
+
+## Performance
+
+### Query Complexity Analysis
+
+```typescript
+import { createComplexityLimitRule } from "graphql-validation-complexity";
+
+const server = new ApolloServer({
+  validationRules: [
+    createComplexityLimitRule(1000, {
+      scalarCost: 1,
+      objectCost: 2,
+      listFactor: 10,    // Multiplier for list fields
+      formatErrorMessage: (cost: number) =>
+        `Query too complex: cost ${cost} exceeds maximum 1000`,
+    }),
+  ],
+});
+```
+
+### Depth Limiting
+
+```typescript
+import depthLimit from "graphql-depth-limit";
+
+const server = new ApolloServer({
+  validationRules: [
+    depthLimit(7, { ignore: ["__schema"] }),  // Max 7 levels deep
+  ],
+});
+```
+
+### Persisted Queries
+
+Lock down which queries can execute (production hardening):
+
+```typescript
+// Build step: extract queries from client code
+// queries.json
+{
+  "abc123": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
+  "def456": "query ListUsers($first: Int) { users(first: $first) { edges { node { id name } } } }"
+}
+
+// Server: only allow registered queries
+const server = new ApolloServer({
+  persistedQueries: {
+    cache: new InMemoryLRUCache(),
+  },
+  // In production, reject non-persisted queries
+  allowBatchedHttpRequests: false,
+});
+```
+
+### Automatic Persisted Queries (APQ)
+
+```
+# Client sends hash first (saves bandwidth)
+POST /graphql
+{
+  "extensions": {
+    "persistedQuery": {
+      "version": 1,
+      "sha256Hash": "abc123hash..."
+    }
+  },
+  "variables": { "id": "user-123" }
+}
+
+# Server: "I don't have that hash"
+{ "errors": [{ "message": "PersistedQueryNotFound" }] }
+
+# Client retries with full query (cached for future)
+POST /graphql
+{
+  "query": "query GetUser($id: ID!) { ... }",
+  "extensions": {
+    "persistedQuery": {
+      "version": 1,
+      "sha256Hash": "abc123hash..."
+    }
+  }
+}
+```
+
+### Response Caching
+
+```typescript
+// Field-level cache hints
+const resolvers = {
+  Query: {
+    user: (_, { id }, __, info) => {
+      info.cacheControl.setCacheHint({ maxAge: 60, scope: "PRIVATE" });
+      return fetchUser(id);
+    },
+    products: (_, __, ___, info) => {
+      info.cacheControl.setCacheHint({ maxAge: 300, scope: "PUBLIC" });
+      return fetchProducts();
+    },
+  },
+};
+```
+
+## TypeScript and GraphQL
+
+### Code Generation (graphql-codegen)
+
+```yaml
+# codegen.yml
+schema: "./schema/**/*.graphql"
+documents: "./src/**/*.{ts,tsx}"
+generates:
+  ./src/generated/types.ts:
+    plugins:
+      - typescript
+      - typescript-resolvers
+    config:
+      contextType: "../context#Context"
+      mappers:
+        User: "../models#UserModel"
+
+  ./src/generated/operations.ts:
+    plugins:
+      - typescript
+      - typescript-operations
+      - typescript-react-apollo    # For React hooks
+```
+
+```bash
+npx graphql-codegen --watch
+```
+
+### Typed Client (urql / Apollo)
+
+```typescript
+// Auto-generated hook from codegen
+import { useGetUserQuery } from "./generated/operations";
+
+function UserProfile({ id }: { id: string }) {
+  const [{ data, fetching, error }] = useGetUserQuery({
+    variables: { id },
+  });
+
+  if (fetching) return <Loading />;
+  if (error) return <Error error={error} />;
+
+  // data.user is fully typed
+  return <h1>{data.user.name}</h1>;
+}
+```
+
+## When GraphQL Is Overkill
+
+### Skip GraphQL When
+
+- Simple CRUD with 1-2 clients (REST is simpler)
+- File upload heavy (REST multipart is native)
+- Real-time only (WebSocket/SSE is more direct)
+- Team has no GraphQL experience and timeline is tight
+- Caching is critical (HTTP caching with REST is free)
+- Public API for third-party devs (REST has wider tooling)
+
+### Use GraphQL When
+
+- Multiple clients need different data shapes (mobile, web, TV)
+- Deep, nested data with varied access patterns
+- Rapid frontend iteration (no backend changes for new views)
+- You have a federated microservice architecture
+- Over-fetching or under-fetching is a real measured problem
+- You can invest in proper tooling (codegen, DataLoader, complexity limits)
+
+### GraphQL Anti-Patterns
+
+| Anti-Pattern | Problem | Fix |
+|--------------|---------|-----|
+| No DataLoader | N+1 queries tank performance | Always batch with DataLoader |
+| No depth/complexity limits | DoS via nested queries | Set limits before production |
+| Huge input types | Mutations become dump trucks | Split into focused mutations |
+| Business logic in resolvers | Untestable, duplicated | Thin resolvers, service layer |
+| No error codes | Clients parse error strings | Use `extensions.code` |
+| Schema-per-team with no coordination | Inconsistent naming, types | Schema governance / federation |
+| Exposing DB schema as GraphQL schema | Coupling, security risk | Design for the client, not the DB |

+ 736 - 0
skills/api-design-ops/references/grpc.md

@@ -0,0 +1,736 @@
+# gRPC Patterns
+
+## Table of Contents
+
+- [Protocol Buffers (proto3)](#protocol-buffers-proto3)
+- [Service Definitions](#service-definitions)
+- [gRPC in Go](#grpc-in-go)
+- [gRPC in Rust](#grpc-in-rust)
+- [Interceptors and Middleware](#interceptors-and-middleware)
+- [Error Handling](#error-handling)
+- [Deadlines and Cancellation](#deadlines-and-cancellation)
+- [Health Checking](#health-checking)
+- [Reflection and CLI Tools](#reflection-and-cli-tools)
+- [gRPC-Web and Connect](#grpc-web-and-connect)
+- [When gRPC Beats REST](#when-grpc-beats-rest)
+
+---
+
+## Protocol Buffers (proto3)
+
+### Basic Syntax
+
+```protobuf
+syntax = "proto3";
+
+package myapi.v1;
+
+option go_package = "github.com/myorg/myapi/gen/go/myapi/v1";
+
+// Messages
+message User {
+  string id = 1;
+  string name = 2;
+  string email = 3;
+  UserRole role = 4;
+  google.protobuf.Timestamp created_at = 5;
+  optional string bio = 6;           // Explicit optional (presence tracking)
+  repeated string tags = 7;          // List
+  map<string, string> metadata = 8;  // Key-value map
+}
+
+// Enums (always start with 0 = UNSPECIFIED)
+enum UserRole {
+  USER_ROLE_UNSPECIFIED = 0;
+  USER_ROLE_ADMIN = 1;
+  USER_ROLE_MEMBER = 2;
+  USER_ROLE_VIEWER = 3;
+}
+
+// Oneof (mutually exclusive fields)
+message Notification {
+  string id = 1;
+  oneof channel {
+    EmailNotification email = 2;
+    SmsNotification sms = 3;
+    PushNotification push = 4;
+  }
+}
+
+message EmailNotification {
+  string subject = 1;
+  string body = 2;
+}
+message SmsNotification {
+  string phone = 1;
+  string text = 2;
+}
+message PushNotification {
+  string title = 1;
+  string body = 2;
+}
+```
+
+### Well-Known Types
+
+```protobuf
+import "google/protobuf/timestamp.proto";   // Timestamp
+import "google/protobuf/duration.proto";     // Duration
+import "google/protobuf/empty.proto";        // Empty (no fields)
+import "google/protobuf/wrappers.proto";     // Nullable primitives
+import "google/protobuf/struct.proto";       // Dynamic JSON-like
+import "google/protobuf/field_mask.proto";   // Partial updates
+import "google/protobuf/any.proto";          // Type-erased message
+
+message UpdateUserRequest {
+  string id = 1;
+  User user = 2;
+  google.protobuf.FieldMask update_mask = 3;  // Which fields to update
+}
+```
+
+### Proto Design Rules
+
+| Rule | Example |
+|------|---------|
+| Field numbers are forever | Never reuse a deleted field number |
+| Enums start at 0 = UNSPECIFIED | `USER_ROLE_UNSPECIFIED = 0` |
+| Use `optional` for presence | Distinguish "not set" from default value |
+| Prefix enum values with type name | `USER_ROLE_ADMIN` not `ADMIN` |
+| Package = `org.service.v1` | Enables API versioning |
+| Avoid `float`/`double` for money | Use `int64` cents or `string` |
+| Use FieldMask for partial updates | Explicit about which fields changed |
+| Reserved deleted fields | `reserved 5, 6; reserved "old_field";` |
+
+## Service Definitions
+
+### Four Communication Patterns
+
+```protobuf
+service UserService {
+  // Unary - simple request/response
+  rpc GetUser(GetUserRequest) returns (GetUserResponse);
+
+  // Server streaming - server sends multiple responses
+  rpc ListUsers(ListUsersRequest) returns (stream User);
+
+  // Client streaming - client sends multiple requests
+  rpc UploadUserPhotos(stream UploadPhotoRequest) returns (UploadSummary);
+
+  // Bidirectional streaming - both sides stream
+  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
+}
+
+message GetUserRequest {
+  string id = 1;
+}
+
+message GetUserResponse {
+  User user = 1;
+}
+
+message ListUsersRequest {
+  int32 page_size = 1;
+  string page_token = 2;
+  string filter = 3;
+}
+```
+
+### Request/Response Patterns
+
+```protobuf
+// Pagination (AIP-158 style)
+message ListUsersRequest {
+  int32 page_size = 1;            // Max items per page
+  string page_token = 2;          // Opaque token from previous response
+}
+
+message ListUsersResponse {
+  repeated User users = 1;
+  string next_page_token = 2;     // Empty = no more pages
+  int32 total_size = 3;           // Optional total count
+}
+
+// Batch operations
+message BatchGetUsersRequest {
+  repeated string ids = 1;        // Max 100
+}
+
+message BatchGetUsersResponse {
+  repeated User users = 1;
+}
+```
+
+## gRPC in Go
+
+### Server Implementation
+
+```go
+package main
+
+import (
+    "context"
+    "log"
+    "net"
+
+    "google.golang.org/grpc"
+    "google.golang.org/grpc/codes"
+    "google.golang.org/grpc/status"
+
+    pb "github.com/myorg/myapi/gen/go/myapi/v1"
+)
+
+type userServer struct {
+    pb.UnimplementedUserServiceServer  // Forward compatibility
+    store UserStore
+}
+
+func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
+    if req.GetId() == "" {
+        return nil, status.Error(codes.InvalidArgument, "id is required")
+    }
+
+    user, err := s.store.Get(ctx, req.GetId())
+    if err != nil {
+        if errors.Is(err, ErrNotFound) {
+            return nil, status.Errorf(codes.NotFound, "user %s not found", req.GetId())
+        }
+        return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
+    }
+
+    return &pb.GetUserResponse{User: user}, nil
+}
+
+// Server streaming
+func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
+    users, err := s.store.List(stream.Context(), req)
+    if err != nil {
+        return status.Errorf(codes.Internal, "failed to list users: %v", err)
+    }
+
+    for _, user := range users {
+        if err := stream.Send(user); err != nil {
+            return err
+        }
+    }
+    return nil
+}
+
+func main() {
+    lis, err := net.Listen("tcp", ":50051")
+    if err != nil {
+        log.Fatalf("failed to listen: %v", err)
+    }
+
+    server := grpc.NewServer(
+        grpc.UnaryInterceptor(loggingInterceptor),
+        grpc.ChainUnaryInterceptor(authInterceptor, loggingInterceptor),
+    )
+    pb.RegisterUserServiceServer(server, &userServer{store: NewUserStore()})
+
+    log.Println("gRPC server listening on :50051")
+    if err := server.Serve(lis); err != nil {
+        log.Fatalf("failed to serve: %v", err)
+    }
+}
+```
+
+### Client Usage (Go)
+
+```go
+func main() {
+    conn, err := grpc.Dial("localhost:50051",
+        grpc.WithTransportCredentials(insecure.NewCredentials()),
+        grpc.WithUnaryInterceptor(retryInterceptor),
+    )
+    if err != nil {
+        log.Fatalf("failed to connect: %v", err)
+    }
+    defer conn.Close()
+
+    client := pb.NewUserServiceClient(conn)
+
+    // Unary call with deadline
+    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+    defer cancel()
+
+    resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "user-123"})
+    if err != nil {
+        st, ok := status.FromError(err)
+        if ok {
+            log.Printf("gRPC error: code=%s, message=%s", st.Code(), st.Message())
+        }
+        return
+    }
+    log.Printf("User: %s", resp.GetUser().GetName())
+
+    // Server streaming
+    stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{PageSize: 100})
+    if err != nil {
+        log.Fatal(err)
+    }
+    for {
+        user, err := stream.Recv()
+        if err == io.EOF {
+            break
+        }
+        if err != nil {
+            log.Fatal(err)
+        }
+        log.Printf("User: %s", user.GetName())
+    }
+}
+```
+
+## gRPC in Rust
+
+### Server with Tonic
+
+```toml
+# Cargo.toml
+[dependencies]
+tonic = "0.12"
+prost = "0.13"
+tokio = { version = "1", features = ["full"] }
+
+[build-dependencies]
+tonic-build = "0.12"
+```
+
+```rust
+// build.rs
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    tonic_build::compile_protos("proto/myapi/v1/user.proto")?;
+    Ok(())
+}
+```
+
+```rust
+use tonic::{Request, Response, Status};
+
+pub mod myapi {
+    pub mod v1 {
+        tonic::include_proto!("myapi.v1");
+    }
+}
+use myapi::v1::user_service_server::{UserService, UserServiceServer};
+use myapi::v1::{GetUserRequest, GetUserResponse, User};
+
+#[derive(Default)]
+pub struct MyUserService;
+
+#[tonic::async_trait]
+impl UserService for MyUserService {
+    async fn get_user(
+        &self,
+        request: Request<GetUserRequest>,
+    ) -> Result<Response<GetUserResponse>, Status> {
+        let req = request.into_inner();
+
+        if req.id.is_empty() {
+            return Err(Status::invalid_argument("id is required"));
+        }
+
+        // Fetch user from store...
+        let user = User {
+            id: req.id,
+            name: "Alice".into(),
+            email: "alice@example.com".into(),
+            ..Default::default()
+        };
+
+        Ok(Response::new(GetUserResponse { user: Some(user) }))
+    }
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let addr = "[::1]:50051".parse()?;
+    let service = MyUserService::default();
+
+    tonic::transport::Server::builder()
+        .add_service(UserServiceServer::new(service))
+        .serve(addr)
+        .await?;
+
+    Ok(())
+}
+```
+
+### Client with Tonic
+
+```rust
+use myapi::v1::user_service_client::UserServiceClient;
+use myapi::v1::GetUserRequest;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let mut client = UserServiceClient::connect("http://[::1]:50051").await?;
+
+    let request = tonic::Request::new(GetUserRequest {
+        id: "user-123".into(),
+    });
+
+    let response = client.get_user(request).await?;
+    println!("User: {:?}", response.into_inner().user);
+
+    Ok(())
+}
+```
+
+## Interceptors and Middleware
+
+### Go Unary Interceptor
+
+```go
+func loggingInterceptor(
+    ctx context.Context,
+    req interface{},
+    info *grpc.UnaryServerInfo,
+    handler grpc.UnaryHandler,
+) (interface{}, error) {
+    start := time.Now()
+
+    // Extract metadata
+    md, _ := metadata.FromIncomingContext(ctx)
+    requestID := md.Get("x-request-id")
+
+    resp, err := handler(ctx, req)
+
+    st, _ := status.FromError(err)
+    log.Printf("method=%s duration=%s status=%s request_id=%v",
+        info.FullMethod, time.Since(start), st.Code(), requestID)
+
+    return resp, err
+}
+
+func authInterceptor(
+    ctx context.Context,
+    req interface{},
+    info *grpc.UnaryServerInfo,
+    handler grpc.UnaryHandler,
+) (interface{}, error) {
+    md, ok := metadata.FromIncomingContext(ctx)
+    if !ok {
+        return nil, status.Error(codes.Unauthenticated, "no metadata")
+    }
+
+    tokens := md.Get("authorization")
+    if len(tokens) == 0 {
+        return nil, status.Error(codes.Unauthenticated, "no token")
+    }
+
+    claims, err := validateToken(tokens[0])
+    if err != nil {
+        return nil, status.Error(codes.Unauthenticated, "invalid token")
+    }
+
+    // Add claims to context
+    ctx = context.WithValue(ctx, claimsKey, claims)
+    return handler(ctx, req)
+}
+```
+
+### Chaining Interceptors
+
+```go
+server := grpc.NewServer(
+    grpc.ChainUnaryInterceptor(
+        recoveryInterceptor,    // Panic recovery (outermost)
+        loggingInterceptor,     // Request logging
+        metricsInterceptor,     // Prometheus metrics
+        authInterceptor,        // Authentication
+        validationInterceptor,  // Request validation
+    ),
+    grpc.ChainStreamInterceptor(
+        streamLoggingInterceptor,
+        streamAuthInterceptor,
+    ),
+)
+```
+
+## Error Handling
+
+### gRPC Status Codes
+
+| Code | Name | Use When |
+|------|------|----------|
+| 0 | OK | Success |
+| 1 | CANCELLED | Client cancelled |
+| 2 | UNKNOWN | Unknown error (avoid - be specific) |
+| 3 | INVALID_ARGUMENT | Bad request (validation) |
+| 4 | DEADLINE_EXCEEDED | Timeout |
+| 5 | NOT_FOUND | Resource doesn't exist |
+| 6 | ALREADY_EXISTS | Conflict (duplicate) |
+| 7 | PERMISSION_DENIED | Authorized but not allowed |
+| 8 | RESOURCE_EXHAUSTED | Rate limit, quota |
+| 9 | FAILED_PRECONDITION | State not ready (e.g., non-empty directory) |
+| 10 | ABORTED | Concurrency conflict (retry) |
+| 11 | OUT_OF_RANGE | Seek past end |
+| 12 | UNIMPLEMENTED | Method not implemented |
+| 13 | INTERNAL | Internal server error |
+| 14 | UNAVAILABLE | Service down (retry with backoff) |
+| 16 | UNAUTHENTICATED | No valid credentials |
+
+### Rich Error Details (Go)
+
+```go
+import (
+    "google.golang.org/genproto/googleapis/rpc/errdetails"
+    "google.golang.org/grpc/status"
+)
+
+func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
+    // Validation with rich error details
+    var violations []*errdetails.BadRequest_FieldViolation
+
+    if req.GetEmail() == "" {
+        violations = append(violations, &errdetails.BadRequest_FieldViolation{
+            Field:       "email",
+            Description: "Email is required",
+        })
+    }
+    if len(req.GetName()) < 2 {
+        violations = append(violations, &errdetails.BadRequest_FieldViolation{
+            Field:       "name",
+            Description: "Name must be at least 2 characters",
+        })
+    }
+
+    if len(violations) > 0 {
+        st := status.New(codes.InvalidArgument, "validation failed")
+        br := &errdetails.BadRequest{FieldViolations: violations}
+        st, _ = st.WithDetails(br)
+        return nil, st.Err()
+    }
+
+    // ... proceed
+}
+```
+
+### Mapping gRPC to HTTP Status Codes
+
+| gRPC Code | HTTP Status |
+|-----------|-------------|
+| OK | 200 |
+| INVALID_ARGUMENT | 400 |
+| UNAUTHENTICATED | 401 |
+| PERMISSION_DENIED | 403 |
+| NOT_FOUND | 404 |
+| ALREADY_EXISTS | 409 |
+| RESOURCE_EXHAUSTED | 429 |
+| CANCELLED | 499 |
+| INTERNAL | 500 |
+| UNIMPLEMENTED | 501 |
+| UNAVAILABLE | 503 |
+| DEADLINE_EXCEEDED | 504 |
+
+## Deadlines and Cancellation
+
+### Setting Deadlines (Go Client)
+
+```go
+// Always set deadlines - never leave RPCs unbounded
+ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+defer cancel()
+
+resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "user-123"})
+if err != nil {
+    st, _ := status.FromError(err)
+    if st.Code() == codes.DeadlineExceeded {
+        // Handle timeout - maybe retry with longer deadline
+    }
+}
+```
+
+### Propagating Deadlines
+
+Deadlines automatically propagate through the call chain. If service A calls service B with a 5s deadline, and A takes 2s, B gets the remaining 3s.
+
+```go
+// Server-side: check remaining time
+deadline, ok := ctx.Deadline()
+if ok {
+    remaining := time.Until(deadline)
+    if remaining < 100*time.Millisecond {
+        return nil, status.Error(codes.DeadlineExceeded, "insufficient time remaining")
+    }
+}
+```
+
+## Health Checking
+
+### Standard Health Protocol
+
+```protobuf
+// Built-in: grpc.health.v1.Health
+service Health {
+  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
+  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
+}
+
+message HealthCheckRequest {
+  string service = 1;  // Empty = overall health
+}
+
+message HealthCheckResponse {
+  enum ServingStatus {
+    UNKNOWN = 0;
+    SERVING = 1;
+    NOT_SERVING = 2;
+    SERVICE_UNKNOWN = 3;
+  }
+  ServingStatus status = 1;
+}
+```
+
+### Go Implementation
+
+```go
+import "google.golang.org/grpc/health"
+import healthpb "google.golang.org/grpc/health/grpc_health_v1"
+
+server := grpc.NewServer()
+healthServer := health.NewServer()
+healthpb.RegisterHealthServer(server, healthServer)
+
+// Set status
+healthServer.SetServingStatus("myapi.v1.UserService", healthpb.HealthCheckResponse_SERVING)
+
+// Kubernetes uses grpc_health_probe
+// livenessProbe:
+//   exec:
+//     command: ["/bin/grpc_health_probe", "-addr=:50051"]
+```
+
+## Reflection and CLI Tools
+
+### Enable Reflection
+
+```go
+import "google.golang.org/grpc/reflection"
+
+server := grpc.NewServer()
+reflection.Register(server)  // Enable for dev/staging
+```
+
+### grpcurl (like curl for gRPC)
+
+```bash
+# List services
+grpcurl -plaintext localhost:50051 list
+
+# Describe a service
+grpcurl -plaintext localhost:50051 describe myapi.v1.UserService
+
+# Call a method
+grpcurl -plaintext -d '{"id": "user-123"}' \
+  localhost:50051 myapi.v1.UserService/GetUser
+
+# Server streaming
+grpcurl -plaintext -d '{"page_size": 10}' \
+  localhost:50051 myapi.v1.UserService/ListUsers
+
+# With metadata (headers)
+grpcurl -plaintext \
+  -H 'authorization: Bearer token123' \
+  -d '{"id": "user-123"}' \
+  localhost:50051 myapi.v1.UserService/GetUser
+```
+
+### buf (Modern Protobuf Tooling)
+
+```bash
+# Lint proto files
+buf lint
+
+# Detect breaking changes
+buf breaking --against '.git#branch=main'
+
+# Generate code
+buf generate
+
+# buf.yaml
+version: v2
+lint:
+  use:
+    - STANDARD
+breaking:
+  use:
+    - WIRE_JSON
+```
+
+## gRPC-Web and Connect
+
+### The Browser Problem
+
+Browsers cannot use gRPC natively (no HTTP/2 trailers, no bidirectional streaming). Solutions:
+
+| Solution | Approach | Streaming | Ecosystem |
+|----------|----------|-----------|-----------|
+| gRPC-Web | Proxy (Envoy) translates | Server-streaming only | Google official |
+| Connect | Native HTTP/1.1 + HTTP/2 | All patterns via HTTP/2 | Buf (connectrpc.com) |
+| gRPC-Gateway | Generate REST from proto | None (REST) | grpc-ecosystem |
+
+### Connect (Recommended for New Projects)
+
+```protobuf
+// Same .proto files - no changes needed
+service UserService {
+  rpc GetUser(GetUserRequest) returns (GetUserResponse);
+}
+```
+
+```typescript
+// TypeScript client (works in browser natively)
+import { createClient } from "@connectrpc/connect";
+import { createConnectTransport } from "@connectrpc/connect-web";
+import { UserService } from "./gen/myapi/v1/user_connect";
+
+const transport = createConnectTransport({
+  baseUrl: "https://api.example.com",
+});
+
+const client = createClient(UserService, transport);
+
+const response = await client.getUser({ id: "user-123" });
+console.log(response.user?.name);
+```
+
+Connect supports three protocols simultaneously:
+- **Connect protocol**: Simple HTTP POST with JSON or Protobuf
+- **gRPC protocol**: Standard gRPC (HTTP/2)
+- **gRPC-Web protocol**: Browser-compatible gRPC
+
+## When gRPC Beats REST
+
+### Use gRPC When
+
+- Internal service-to-service communication
+- Performance matters (10x smaller payloads, 7x faster serialization)
+- You need streaming (logs, real-time feeds, file uploads)
+- You want a strict contract between services
+- Polyglot environment (generate clients for any language)
+- Bidirectional communication
+
+### Use REST When
+
+- Public API consumed by third-party developers
+- Browser clients are primary (unless using Connect)
+- You need HTTP caching (CDN, browser cache)
+- Team is more familiar with REST
+- Simple CRUD with few relationships
+- Webhooks are a primary integration pattern
+
+### Hybrid Approach
+
+Many production systems use both:
+- gRPC for internal microservice communication
+- REST/GraphQL for external-facing APIs
+- gRPC-Gateway or Connect to expose gRPC services as REST
+
+```
+[Browser] --REST/GraphQL--> [API Gateway] --gRPC--> [User Service]
+                                          --gRPC--> [Order Service]
+                                          --gRPC--> [Payment Service]
+```

+ 573 - 0
skills/api-design-ops/references/rest-advanced.md

@@ -0,0 +1,573 @@
+# REST Advanced Patterns
+
+## Table of Contents
+
+- [Resource Modeling](#resource-modeling)
+- [HTTP Methods Beyond CRUD](#http-methods-beyond-crud)
+- [Content Negotiation](#content-negotiation)
+- [Pagination Implementations](#pagination-implementations)
+- [Filtering, Sorting, Field Selection](#filtering-sorting-field-selection)
+- [Bulk Operations](#bulk-operations)
+- [Long-Running Operations](#long-running-operations)
+- [HATEOAS and Hypermedia](#hateoas-and-hypermedia)
+- [API Documentation](#api-documentation)
+- [Webhook Design](#webhook-design)
+- [Caching](#caching)
+
+---
+
+## Resource Modeling
+
+### Collections vs Singletons
+
+```
+/users              # Collection - supports GET (list), POST (create)
+/users/{id}         # Singleton  - supports GET, PUT, PATCH, DELETE
+/users/{id}/profile # Singleton sub-resource (1:1 relationship)
+/users/{id}/orders  # Sub-collection (1:many relationship)
+```
+
+### Modeling Relationships
+
+**Approach 1: Sub-resources (strong ownership)**
+```
+GET /users/{id}/orders          # Orders belong to user
+POST /users/{id}/orders         # Create order for user
+```
+
+**Approach 2: Top-level with filters (independent entities)**
+```
+GET /orders?user_id={id}        # Orders exist independently
+GET /orders/{order_id}          # Direct access without user context
+```
+
+**Approach 3: Relationship endpoints (many-to-many)**
+```
+GET  /users/{id}/roles          # List user's roles
+PUT  /users/{id}/roles/{rid}    # Assign role (no body needed)
+DELETE /users/{id}/roles/{rid}  # Remove role
+```
+
+### When to Use Sub-Resources
+
+| Use sub-resource | Use top-level |
+|------------------|---------------|
+| Child can't exist without parent | Entity is independently meaningful |
+| Always accessed in parent context | Frequently queried across parents |
+| Moderate cardinality (< 1000) | High cardinality |
+| Lifecycle tied to parent | Independent lifecycle |
+
+### Resource Naming Patterns
+
+```
+# Actions that don't map to CRUD - use sub-resources
+POST /orders/{id}/cancel         # State transition
+POST /users/{id}/verify-email    # Trigger action
+POST /reports/{id}/export        # Async operation
+
+# Avoid: verbs as top-level resources
+POST /cancelOrder                # Bad
+POST /send-notification          # Bad
+
+# Search as a resource (when GET query string is too complex)
+POST /users/search
+{ "filters": { "age_range": [18, 30], "location": { "within": "10km", "of": [lat, lng] } } }
+```
+
+## HTTP Methods Beyond CRUD
+
+### PATCH Strategies
+
+**JSON Merge Patch (RFC 7396)** - Simple, intuitive:
+
+```
+PATCH /users/123
+Content-Type: application/merge-patch+json
+
+{ "name": "New Name", "address": null }
+```
+
+- Set `name` to "New Name"
+- Remove `address` (null = delete)
+- Leave all other fields unchanged
+- Limitation: cannot set a field TO null vs removing it
+
+**JSON Patch (RFC 6902)** - Precise operations:
+
+```
+PATCH /users/123
+Content-Type: application/json-patch+json
+
+[
+  { "op": "replace", "path": "/name", "value": "New Name" },
+  { "op": "remove", "path": "/address" },
+  { "op": "add", "path": "/tags/-", "value": "premium" },
+  { "op": "test", "path": "/version", "value": 5 }
+]
+```
+
+- Supports: add, remove, replace, move, copy, test
+- `test` enables optimistic concurrency (apply only if value matches)
+- More complex but unambiguous
+
+**Recommendation**: Use JSON Merge Patch for most APIs (simpler). Use JSON Patch when you need array manipulation or atomic test-and-set.
+
+### HEAD and OPTIONS
+
+```
+# HEAD - metadata without body (same headers as GET)
+HEAD /files/report.pdf
+# Returns: Content-Length, Content-Type, Last-Modified, ETag
+# Use: check existence, get size before download
+
+# OPTIONS - discover allowed methods (CORS preflight uses this)
+OPTIONS /users
+# Returns: Allow: GET, POST, HEAD, OPTIONS
+```
+
+## Content Negotiation
+
+### Accept Header
+
+```
+# Client requests specific format
+GET /users/123
+Accept: application/json              # JSON (default)
+Accept: application/xml               # XML
+Accept: text/csv                      # CSV export
+Accept: application/pdf               # PDF report
+
+# Versioning via media type
+Accept: application/vnd.myapi.v2+json  # Version in media type
+```
+
+### Content-Type on Requests
+
+```
+# Server must validate Content-Type on mutations
+POST /users
+Content-Type: application/json         # Standard
+Content-Type: multipart/form-data      # File uploads
+Content-Type: application/x-www-form-urlencoded  # Form data
+```
+
+### Implementation (Go)
+
+```go
+func handleGetUser(w http.ResponseWriter, r *http.Request) {
+    accept := r.Header.Get("Accept")
+    user := fetchUser(r)
+
+    switch {
+    case strings.Contains(accept, "application/xml"):
+        w.Header().Set("Content-Type", "application/xml")
+        xml.NewEncoder(w).Encode(user)
+    case strings.Contains(accept, "text/csv"):
+        w.Header().Set("Content-Type", "text/csv")
+        writeCSV(w, user)
+    default:
+        w.Header().Set("Content-Type", "application/json")
+        json.NewEncoder(w).Encode(user)
+    }
+}
+```
+
+## Pagination Implementations
+
+### Cursor-Based (Recommended for Most Cases)
+
+Encode the cursor as base64 for opacity:
+
+```go
+// Encode cursor
+type Cursor struct {
+    ID        int64     `json:"id"`
+    CreatedAt time.Time `json:"created_at"`
+}
+
+func encodeCursor(c Cursor) string {
+    b, _ := json.Marshal(c)
+    return base64.URLEncoding.EncodeToString(b)
+}
+
+func decodeCursor(s string) (Cursor, error) {
+    b, err := base64.URLEncoding.DecodeString(s)
+    if err != nil {
+        return Cursor{}, err
+    }
+    var c Cursor
+    return c, json.Unmarshal(b, &c)
+}
+```
+
+**Request/Response:**
+
+```
+GET /users?limit=20&after=eyJpZCI6MTIzLCJjcmVhdGVkX2F0IjoiMjAyNC0wMS0xNVQxMDozMDowMFoifQ==
+
+{
+  "data": [...],
+  "pagination": {
+    "has_more": true,
+    "next_cursor": "eyJpZCI6MTQzLCJjcmVhdGVkX2F0IjoiMjAyNC0wMS0xNlQwODoxNTowMFoifQ==",
+    "prev_cursor": "eyJpZCI6MTI0LCJjcmVhdGVkX2F0IjoiMjAyNC0wMS0xNVQxMTowMDowMFoifQ=="
+  }
+}
+```
+
+**SQL (keyset pagination under the hood):**
+
+```sql
+SELECT * FROM users
+WHERE (created_at, id) > ('2024-01-15T10:30:00Z', 123)
+ORDER BY created_at ASC, id ASC
+LIMIT 21;  -- fetch limit+1 to determine has_more
+```
+
+### Link Headers (RFC 8288)
+
+```
+Link: <https://api.example.com/users?after=abc123&limit=20>; rel="next",
+      <https://api.example.com/users?before=xyz789&limit=20>; rel="prev",
+      <https://api.example.com/users?limit=20>; rel="first"
+```
+
+### Total Count Considerations
+
+- `total` count requires a separate `COUNT(*)` query - expensive on large tables
+- Make it opt-in: `GET /users?limit=20&include_total=true`
+- Consider approximate counts: `SELECT reltuples FROM pg_class WHERE relname = 'users'`
+
+## Filtering, Sorting, Field Selection
+
+### Filtering
+
+```
+# Simple equality
+GET /users?status=active&role=admin
+
+# Operators (LHS brackets style - used by Stripe, Supabase)
+GET /users?created_at[gte]=2024-01-01&created_at[lt]=2024-02-01
+GET /products?price[lte]=100&category[in]=electronics,books
+
+# Operators (filter syntax)
+GET /users?filter=status eq "active" and age gt 18
+```
+
+### Sorting
+
+```
+# Simple (comma-separated, prefix - for descending)
+GET /users?sort=-created_at,name
+
+# Multiple fields
+GET /products?sort=category,-price    # category ASC, then price DESC
+```
+
+### Sparse Fieldsets
+
+```
+# Return only specific fields (reduces payload)
+GET /users?fields=id,name,email
+GET /users/123?fields=id,name,email,profile.avatar
+
+# Related resource fields
+GET /orders?fields=id,total&fields[customer]=id,name
+```
+
+## Bulk Operations
+
+### Batch Create
+
+```
+POST /users/batch
+Content-Type: application/json
+
+{
+  "items": [
+    { "name": "Alice", "email": "alice@example.com" },
+    { "name": "Bob", "email": "bob@example.com" }
+  ]
+}
+
+# Response: 207 Multi-Status
+{
+  "results": [
+    { "status": 201, "data": { "id": "u1", "name": "Alice" } },
+    { "status": 409, "error": { "type": "conflict", "detail": "Email already exists" } }
+  ],
+  "summary": { "succeeded": 1, "failed": 1 }
+}
+```
+
+### Batch Actions
+
+```
+POST /users/batch-action
+{
+  "action": "deactivate",
+  "ids": ["u1", "u2", "u3"],
+  "reason": "Account cleanup"
+}
+```
+
+### Guidelines
+
+- Set a maximum batch size (100-1000 items)
+- Return 207 Multi-Status for partial success
+- Include per-item status in response
+- Consider async processing for large batches (return 202 + job URL)
+
+## Long-Running Operations
+
+### Polling Pattern
+
+```
+# Start operation
+POST /reports/generate
+{ "type": "annual", "year": 2024 }
+
+# Response: 202 Accepted
+{
+  "operation_id": "op-abc-123",
+  "status": "pending",
+  "status_url": "/operations/op-abc-123",
+  "estimated_completion": "2024-01-15T10:35:00Z"
+}
+
+# Poll for status
+GET /operations/op-abc-123
+{
+  "operation_id": "op-abc-123",
+  "status": "completed",          # pending | running | completed | failed
+  "progress": 100,
+  "result_url": "/reports/rpt-xyz-789",
+  "completed_at": "2024-01-15T10:34:12Z"
+}
+```
+
+### Webhook Callback
+
+```
+POST /reports/generate
+{
+  "type": "annual",
+  "year": 2024,
+  "callback_url": "https://myapp.com/webhooks/report-ready"
+}
+
+# Server POSTs to callback_url when done:
+{
+  "event": "report.completed",
+  "operation_id": "op-abc-123",
+  "result_url": "/reports/rpt-xyz-789"
+}
+```
+
+### Server-Sent Events
+
+```
+GET /operations/op-abc-123/stream
+Accept: text/event-stream
+
+event: progress
+data: {"percent": 25, "stage": "fetching data"}
+
+event: progress
+data: {"percent": 75, "stage": "generating charts"}
+
+event: complete
+data: {"result_url": "/reports/rpt-xyz-789"}
+```
+
+## HATEOAS and Hypermedia
+
+### When It's Worth the Complexity
+
+| Worth it | Not worth it |
+|----------|--------------|
+| Public API with many consumers | Internal microservice |
+| API that evolves frequently | Stable, versioned API |
+| Workflow-driven (state machines) | Simple CRUD |
+| Discoverability is a feature | Clients are tightly coupled |
+
+### HAL (Hypertext Application Language)
+
+```json
+{
+  "id": "order-123",
+  "status": "pending_payment",
+  "total": 5999,
+  "_links": {
+    "self": { "href": "/orders/order-123" },
+    "pay": { "href": "/orders/order-123/pay", "method": "POST" },
+    "cancel": { "href": "/orders/order-123", "method": "DELETE" }
+  },
+  "_embedded": {
+    "items": [
+      {
+        "product_id": "prod-456",
+        "quantity": 2,
+        "_links": {
+          "product": { "href": "/products/prod-456" }
+        }
+      }
+    ]
+  }
+}
+```
+
+## API Documentation
+
+### OpenAPI 3.1 Structure
+
+```yaml
+openapi: 3.1.0
+info:
+  title: My API
+  version: 2.0.0
+  description: |
+    ## Authentication
+    All endpoints require Bearer token authentication.
+  contact:
+    email: api-support@example.com
+servers:
+  - url: https://api.example.com/v2
+    description: Production
+  - url: https://sandbox.example.com/v2
+    description: Sandbox
+
+paths:
+  /users:
+    get:
+      summary: List users
+      operationId: listUsers
+      tags: [Users]
+      parameters:
+        - name: limit
+          in: query
+          schema:
+            type: integer
+            default: 20
+            maximum: 100
+      responses:
+        '200':
+          description: Success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/UserList'
+```
+
+### Documentation Tools
+
+| Tool | Strength |
+|------|----------|
+| Redoc | Beautiful single-page docs from OpenAPI |
+| Swagger UI | Interactive "try it" playground |
+| Stoplight | Design-first with mock servers |
+| Mintlify | Modern docs with guides + API reference |
+
+## Webhook Design
+
+### Webhook Payload
+
+```json
+{
+  "id": "evt_abc123",
+  "type": "order.completed",
+  "created_at": "2024-01-15T10:30:00Z",
+  "api_version": "2024-01-15",
+  "data": {
+    "id": "order-456",
+    "status": "completed",
+    "total": 5999
+  }
+}
+```
+
+### Signature Verification
+
+```
+# Header
+X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
+
+# Compute: HMAC-SHA256(webhook_secret, raw_body)
+```
+
+```go
+func verifyWebhookSignature(secret, signature string, body []byte) bool {
+    mac := hmac.New(sha256.New, []byte(secret))
+    mac.Write(body)
+    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
+    return hmac.Equal([]byte(expected), []byte(signature))
+}
+```
+
+### Webhook Best Practices
+
+| Practice | Detail |
+|----------|--------|
+| Retry with backoff | 1s, 5s, 30s, 5m, 30m, 2h, 24h |
+| Idempotency | Include event ID, consumers must deduplicate |
+| Timeout | 30 second max wait for 2xx response |
+| Disable after failures | Disable after N consecutive failures, notify owner |
+| Event log | Provide UI/API to replay failed webhooks |
+| Thin payloads | Send IDs + event type, let consumer fetch full data |
+
+## Caching
+
+### ETag-Based (Strong Validation)
+
+```
+# First request
+GET /users/123
+ETag: "a1b2c3d4"
+
+# Subsequent request
+GET /users/123
+If-None-Match: "a1b2c3d4"
+
+# Response if unchanged: 304 Not Modified (no body)
+# Response if changed: 200 with new ETag
+```
+
+### Last-Modified (Weak Validation)
+
+```
+GET /users/123
+Last-Modified: Thu, 15 Jan 2024 10:30:00 GMT
+
+# Subsequent request
+GET /users/123
+If-Modified-Since: Thu, 15 Jan 2024 10:30:00 GMT
+```
+
+### Cache-Control Directives
+
+```
+# Public, cacheable for 1 hour
+Cache-Control: public, max-age=3600
+
+# Private (user-specific), cacheable for 5 minutes
+Cache-Control: private, max-age=300
+
+# No caching (real-time data)
+Cache-Control: no-store
+
+# Revalidate before using cache
+Cache-Control: no-cache
+
+# Stale-while-revalidate (serve stale, refresh in background)
+Cache-Control: public, max-age=60, stale-while-revalidate=300
+```
+
+### Caching Strategy by Resource Type
+
+| Resource Type | Strategy | Cache-Control |
+|---------------|----------|---------------|
+| Static assets | Immutable with hash | `public, max-age=31536000, immutable` |
+| User profile | Short-lived, private | `private, max-age=60` |
+| Product catalog | Medium, public | `public, max-age=300, stale-while-revalidate=600` |
+| Search results | No cache or very short | `no-store` or `max-age=10` |
+| Real-time data | No cache | `no-store` |

+ 0 - 0
skills/api-design-ops/scripts/.gitkeep


+ 328 - 0
skills/ci-cd-ops/SKILL.md

@@ -0,0 +1,328 @@
+---
+name: ci-cd-ops
+description: "CI/CD pipeline patterns with GitHub Actions, release automation, and testing strategies. Use for: github actions, workflow, CI, CD, pipeline, deploy, release, semantic release, changesets, goreleaser, matrix, cache, secrets, environment, artifact, reusable workflow, composite action."
+allowed-tools: "Read Write Bash"
+related-skills: [git-workflow, docker-ops, testing-ops]
+---
+
+# CI/CD Operations
+
+Comprehensive patterns for continuous integration, delivery, and deployment using GitHub Actions, release automation tools, and testing pipelines.
+
+## GitHub Actions Quick Reference
+
+### Workflow File Anatomy
+
+```yaml
+name: CI                          # Display name in Actions tab
+on:                               # Trigger events
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+permissions:                      # GITHUB_TOKEN scope (least privilege)
+  contents: read
+  pull-requests: write
+
+concurrency:                      # Prevent duplicate runs
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+env:                              # Workflow-level environment variables
+  NODE_VERSION: "20"
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+          cache: npm
+      - run: npm ci
+      - run: npm test
+```
+
+### Core Syntax Elements
+
+| Element | Purpose | Example |
+|---------|---------|---------|
+| `on` | Event triggers | `push`, `pull_request`, `schedule` |
+| `jobs.<id>.runs-on` | Runner selection | `ubuntu-latest`, `self-hosted` |
+| `jobs.<id>.needs` | Job dependencies | `needs: [build, lint]` |
+| `jobs.<id>.if` | Conditional execution | `if: github.event_name == 'push'` |
+| `jobs.<id>.strategy.matrix` | Parallel variants | `node-version: [18, 20, 22]` |
+| `jobs.<id>.environment` | Deployment target | `environment: production` |
+| `jobs.<id>.permissions` | Token scope | `contents: write` |
+| `steps[*].uses` | Use an action | `uses: actions/checkout@v4` |
+| `steps[*].run` | Run a command | `run: npm test` |
+| `steps[*].env` | Step environment | `env: { CI: true }` |
+
+## Trigger Decision Tree
+
+| Scenario | Trigger | Config |
+|----------|---------|--------|
+| Run tests on every PR | `pull_request` | `branches: [main]` |
+| Deploy on merge to main | `push` | `branches: [main]` |
+| Release on version tag | `push` | `tags: ['v*']` |
+| Nightly builds | `schedule` | `cron: '0 2 * * *'` |
+| Manual deployment | `workflow_dispatch` | `inputs: { environment: ... }` |
+| Called by another workflow | `workflow_call` | `inputs:`, `secrets:` |
+| On PR label change | `pull_request` | `types: [labeled]` |
+| On issue comment | `issue_comment` | `types: [created]` |
+| On release published | `release` | `types: [published]` |
+| On package push | `registry_package` | `types: [published]` |
+
+### Trigger Filter Patterns
+
+```yaml
+on:
+  push:
+    branches: [main, 'release/**']      # Branch patterns
+    paths: ['src/**', '!src/**/*.test.*'] # Path filters (ignore tests)
+    tags: ['v*']                          # Tag patterns
+  pull_request:
+    types: [opened, synchronize, reopened] # Default types
+    paths-ignore: ['docs/**', '*.md']     # Ignore docs-only changes
+```
+
+## Caching Strategies
+
+| Ecosystem | Action / Key | Path | Restore Key |
+|-----------|-------------|------|-------------|
+| Node (npm) | `actions/setup-node` with `cache: npm` | Auto | Auto |
+| Node (pnpm) | `actions/setup-node` with `cache: pnpm` | Auto | Auto |
+| Go modules | `actions/setup-go` with `cache: true` | Auto | Auto |
+| Cargo | `actions/cache@v4` | `~/.cargo/registry`, `target` | `cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}` |
+| pip / uv | `actions/setup-python` with `cache: pip` | Auto | Auto |
+| Docker layers | `docker/build-push-action` | Uses buildx cache | `type=gha` or `type=registry` |
+| Gradle | `actions/setup-java` with `cache: gradle` | Auto | Auto |
+| Composer | `actions/cache@v4` | `vendor` | `composer-${{ hashFiles('composer.lock') }}` |
+
+### Manual Cache Example
+
+```yaml
+- uses: actions/cache@v4
+  with:
+    path: |
+      ~/.cargo/bin
+      ~/.cargo/registry
+      ~/.cargo/git
+      target
+    key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
+    restore-keys: |
+      cargo-${{ runner.os }}-
+```
+
+## Matrix Strategy
+
+```yaml
+strategy:
+  fail-fast: false                    # Don't cancel siblings on failure
+  max-parallel: 4                     # Limit concurrent jobs
+  matrix:
+    os: [ubuntu-latest, windows-latest, macos-latest]
+    node-version: [18, 20, 22]
+    include:                          # Add specific combos
+      - os: ubuntu-latest
+        node-version: 22
+        coverage: true
+    exclude:                          # Remove specific combos
+      - os: windows-latest
+        node-version: 18
+```
+
+### Dynamic Matrix
+
+```yaml
+prepare:
+  runs-on: ubuntu-latest
+  outputs:
+    matrix: ${{ steps.set.outputs.matrix }}
+  steps:
+    - id: set
+      run: echo "matrix=$(jq -c . matrix.json)" >> "$GITHUB_OUTPUT"
+
+test:
+  needs: prepare
+  strategy:
+    matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
+```
+
+## Secrets Management
+
+| Scope | Access | Use Case |
+|-------|--------|----------|
+| Repository secrets | All workflows in repo | API keys, tokens |
+| Environment secrets | Jobs targeting that environment | Production credentials |
+| Organization secrets | Selected repos in org | Shared service accounts |
+| OIDC tokens | Federated identity | Cloud deployment (no stored secrets) |
+
+### Secrets Best Practices
+
+```yaml
+# Reference secrets - NEVER echo or log them
+- run: deploy --token ${{ secrets.DEPLOY_TOKEN }}
+
+# Mask custom values
+- run: echo "::add-mask::$CUSTOM_SECRET"
+
+# Use environments for deployment secrets
+jobs:
+  deploy:
+    environment: production           # Requires approval + has secrets
+    steps:
+      - run: deploy --key ${{ secrets.PROD_API_KEY }}
+```
+
+### OIDC for Cloud (No Stored Secrets)
+
+```yaml
+permissions:
+  id-token: write
+  contents: read
+
+steps:
+  - uses: aws-actions/configure-aws-credentials@v4
+    with:
+      role-to-assume: arn:aws:iam::123456789:role/github-actions
+      aws-region: us-east-1
+```
+
+## Common Workflow Patterns
+
+### Test on Pull Request
+
+```yaml
+name: Test
+on:
+  pull_request:
+    branches: [main]
+concurrency:
+  group: test-${{ github.head_ref }}
+  cancel-in-progress: true
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run lint
+      - run: npm test -- --coverage
+```
+
+### Deploy on Merge to Main
+
+```yaml
+name: Deploy
+on:
+  push:
+    branches: [main]
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    environment: production
+    steps:
+      - uses: actions/checkout@v4
+      - run: npm ci && npm run build
+      - run: npx wrangler deploy
+        env:
+          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
+```
+
+### Release on Tag
+
+```yaml
+name: Release
+on:
+  push:
+    tags: ['v*']
+permissions:
+  contents: write
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with: { fetch-depth: 0 }
+      - run: |
+          gh release create ${{ github.ref_name }} \
+            --generate-notes \
+            --title "${{ github.ref_name }}"
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+```
+
+## Gotchas Table
+
+| Gotcha | Problem | Fix |
+|--------|---------|-----|
+| Shallow clone | `git describe` fails, history missing | `actions/checkout@v4` with `fetch-depth: 0` |
+| Default permissions | `GITHUB_TOKEN` is read-only by default | Set `permissions:` explicitly |
+| Action pinning | `@main` can break without warning | Pin to SHA: `@abc123` or `@v4` |
+| Fork PR secrets | Secrets unavailable on fork PRs | Use `pull_request_target` carefully |
+| Concurrent deploys | Race condition on production | Use `concurrency:` groups |
+| Stale caches | Cache grows unbounded | Include lockfile hash in key |
+| Node.js version | `setup-node` defaults vary | Always specify `node-version` |
+| Docker layer cache | Rebuilds everything without cache | Use `cache-from: type=gha` |
+| Matrix + environment | Each matrix job needs approval | Use a single deploy job after matrix |
+| Path filters + required checks | Skipped jobs block merge | Use `paths-filter` action or make checks non-required |
+| `GITHUB_TOKEN` in PRs | Cannot trigger other workflows | Use a PAT or GitHub App token |
+| Windows line endings | Scripts fail with `\r\n` | Use `.gitattributes` or `core.autocrlf` |
+
+## Expression Syntax Quick Reference
+
+| Expression | Result |
+|------------|--------|
+| `${{ github.event_name }}` | `push`, `pull_request`, etc. |
+| `${{ github.ref_name }}` | Branch or tag name |
+| `${{ github.sha }}` | Full commit SHA |
+| `${{ github.actor }}` | User who triggered |
+| `${{ runner.os }}` | `Linux`, `Windows`, `macOS` |
+| `${{ contains(github.event.head_commit.message, '[skip ci]') }}` | Check commit message |
+| `${{ needs.build.outputs.version }}` | Output from prior job |
+| `${{ fromJson(steps.meta.outputs.json) }}` | Parse JSON output |
+| `${{ hashFiles('**/package-lock.json') }}` | Hash for cache keys |
+| `${{ format('refs/heads/{0}', matrix.branch) }}` | String formatting |
+| `${{ toJson(matrix) }}` | Debug: print matrix config |
+
+## Step Outputs
+
+```yaml
+steps:
+  - id: version
+    run: echo "value=$(cat VERSION)" >> "$GITHUB_OUTPUT"
+
+  - run: echo "Version is ${{ steps.version.outputs.value }}"
+```
+
+### Job Outputs (for Cross-Job Communication)
+
+```yaml
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    outputs:
+      artifact-id: ${{ steps.upload.outputs.artifact-id }}
+    steps:
+      - id: upload
+        run: echo "artifact-id=abc123" >> "$GITHUB_OUTPUT"
+
+  deploy:
+    needs: build
+    runs-on: ubuntu-latest
+    steps:
+      - run: echo "Deploying ${{ needs.build.outputs.artifact-id }}"
+```
+
+## Reference Files
+
+| File | Contents |
+|------|----------|
+| `references/github-actions.md` | Complete workflow syntax, reusable workflows, composite actions, OIDC, runners, debugging |
+| `references/release-automation.md` | Semantic versioning, semantic-release, changesets, goreleaser, changelog, publishing |
+| `references/testing-pipelines.md` | Test stages, parallelism, coverage, service containers, e2e in CI, deployment pipelines |

+ 0 - 0
skills/ci-cd-ops/assets/.gitkeep


+ 783 - 0
skills/ci-cd-ops/references/github-actions.md

@@ -0,0 +1,783 @@
+# GitHub Actions Reference
+
+## Table of Contents
+
+- [Workflow File Anatomy](#workflow-file-anatomy)
+- [Job Dependencies and Conditionals](#job-dependencies-and-conditionals)
+- [Reusable Workflows](#reusable-workflows)
+- [Composite Actions](#composite-actions)
+- [Matrix Strategy](#matrix-strategy)
+- [Artifacts](#artifacts)
+- [Environment Protection Rules](#environment-protection-rules)
+- [Concurrency Control](#concurrency-control)
+- [Self-Hosted Runners](#self-hosted-runners)
+- [OIDC for Cloud Deployment](#oidc-for-cloud-deployment)
+- [Common Action Recipes](#common-action-recipes)
+- [Debugging Workflows](#debugging-workflows)
+
+---
+
+## Workflow File Anatomy
+
+Every workflow lives in `.github/workflows/*.yml`. A complete annotated example:
+
+```yaml
+# .github/workflows/ci.yml
+name: CI Pipeline                     # Name shown in Actions tab
+
+# ── Triggers ──────────────────────────────────────────────
+on:
+  push:
+    branches: [main, 'release/**']
+    paths-ignore: ['docs/**', '*.md']
+  pull_request:
+    branches: [main]
+    types: [opened, synchronize, reopened]
+  schedule:
+    - cron: '0 6 * * 1'              # Weekly Monday 6am UTC
+  workflow_dispatch:                   # Manual trigger
+    inputs:
+      environment:
+        description: 'Deploy target'
+        required: true
+        default: 'staging'
+        type: choice
+        options: [staging, production]
+
+# ── Token Permissions (least privilege) ───────────────────
+permissions:
+  contents: read
+  pull-requests: write
+  checks: write
+
+# ── Concurrency ──────────────────────────────────────────
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+# ── Workflow Environment ─────────────────────────────────
+env:
+  CI: true
+  NODE_ENV: test
+
+# ── Jobs ─────────────────────────────────────────────────
+jobs:
+  lint:
+    name: Lint & Format
+    runs-on: ubuntu-latest
+    timeout-minutes: 10               # Prevent hung jobs
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version-file: '.nvmrc'
+          cache: npm
+      - run: npm ci
+      - run: npm run lint
+      - run: npm run format:check
+
+  test:
+    name: Test (${{ matrix.node-version }})
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    needs: lint                       # Run after lint passes
+    strategy:
+      fail-fast: false
+      matrix:
+        node-version: [18, 20, 22]
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: npm
+      - run: npm ci
+      - run: npm test -- --coverage
+      - uses: actions/upload-artifact@v4
+        if: always()                  # Upload even on failure
+        with:
+          name: coverage-${{ matrix.node-version }}
+          path: coverage/
+          retention-days: 7
+
+  deploy:
+    name: Deploy
+    runs-on: ubuntu-latest
+    needs: test
+    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+    environment: production           # Requires approval
+    steps:
+      - uses: actions/checkout@v4
+      - run: ./deploy.sh
+        env:
+          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
+```
+
+## Job Dependencies and Conditionals
+
+### Job Dependencies with `needs`
+
+```yaml
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps: [...]
+
+  test:
+    needs: build                      # Waits for build
+    runs-on: ubuntu-latest
+    steps: [...]
+
+  deploy:
+    needs: [build, test]              # Waits for both
+    runs-on: ubuntu-latest
+    steps: [...]
+```
+
+### Conditional Execution with `if`
+
+```yaml
+jobs:
+  deploy:
+    if: github.ref == 'refs/heads/main'
+    runs-on: ubuntu-latest
+
+  notify:
+    needs: deploy
+    if: always()                      # Run even if deploy fails
+    runs-on: ubuntu-latest
+
+  release:
+    if: startsWith(github.ref, 'refs/tags/v')
+    runs-on: ubuntu-latest
+
+steps:
+  - run: echo "Only on failure"
+    if: failure()
+
+  - run: echo "Only on success"
+    if: success()
+
+  - run: echo "Always run (cleanup)"
+    if: always()
+
+  - run: echo "Skip on forks"
+    if: github.repository == 'owner/repo'
+
+  - run: echo "Only for specific actor"
+    if: github.actor == 'dependabot[bot]'
+
+  - run: echo "Check PR label"
+    if: contains(github.event.pull_request.labels.*.name, 'deploy')
+```
+
+### Accessing Outputs from `needs`
+
+```yaml
+jobs:
+  check:
+    runs-on: ubuntu-latest
+    outputs:
+      should-deploy: ${{ steps.decision.outputs.deploy }}
+    steps:
+      - id: decision
+        run: |
+          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
+            echo "deploy=true" >> "$GITHUB_OUTPUT"
+          else
+            echo "deploy=false" >> "$GITHUB_OUTPUT"
+          fi
+
+  deploy:
+    needs: check
+    if: needs.check.outputs.should-deploy == 'true'
+    runs-on: ubuntu-latest
+    steps:
+      - run: echo "Deploying..."
+```
+
+## Reusable Workflows
+
+### Defining a Reusable Workflow
+
+```yaml
+# .github/workflows/reusable-test.yml
+name: Reusable Test Workflow
+on:
+  workflow_call:
+    inputs:
+      node-version:
+        description: 'Node.js version'
+        required: false
+        default: '20'
+        type: string
+      working-directory:
+        description: 'Directory to run tests in'
+        required: false
+        default: '.'
+        type: string
+    secrets:
+      NPM_TOKEN:
+        required: false
+        description: 'NPM auth token'
+    outputs:
+      coverage-percent:
+        description: 'Test coverage percentage'
+        value: ${{ jobs.test.outputs.coverage }}
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    outputs:
+      coverage: ${{ steps.cov.outputs.percent }}
+    defaults:
+      run:
+        working-directory: ${{ inputs.working-directory }}
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: ${{ inputs.node-version }}
+          cache: npm
+      - run: npm ci
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+      - run: npm test -- --coverage
+      - id: cov
+        run: |
+          PERCENT=$(jq '.total.lines.pct' coverage/coverage-summary.json)
+          echo "percent=$PERCENT" >> "$GITHUB_OUTPUT"
+```
+
+### Calling a Reusable Workflow
+
+```yaml
+# .github/workflows/ci.yml
+name: CI
+on: [push, pull_request]
+
+jobs:
+  test:
+    uses: ./.github/workflows/reusable-test.yml
+    with:
+      node-version: '20'
+    secrets:
+      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+  # Or inherit all secrets
+  test-inherit:
+    uses: ./.github/workflows/reusable-test.yml
+    secrets: inherit
+
+  # Call from another repo
+  test-external:
+    uses: org/shared-workflows/.github/workflows/test.yml@main
+    with:
+      node-version: '20'
+
+  report:
+    needs: test
+    runs-on: ubuntu-latest
+    steps:
+      - run: echo "Coverage was ${{ needs.test.outputs.coverage-percent }}%"
+```
+
+## Composite Actions
+
+### Creating a Composite Action
+
+```yaml
+# .github/actions/setup-project/action.yml
+name: 'Setup Project'
+description: 'Install dependencies and build'
+inputs:
+  node-version:
+    description: 'Node.js version'
+    required: false
+    default: '20'
+  install-command:
+    description: 'Install command'
+    required: false
+    default: 'npm ci'
+outputs:
+  cache-hit:
+    description: 'Whether cache was hit'
+    value: ${{ steps.cache.outputs.cache-hit }}
+
+runs:
+  using: composite
+  steps:
+    - uses: actions/setup-node@v4
+      with:
+        node-version: ${{ inputs.node-version }}
+
+    - id: cache
+      uses: actions/cache@v4
+      with:
+        path: node_modules
+        key: node-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
+
+    - if: steps.cache.outputs.cache-hit != 'true'
+      run: ${{ inputs.install-command }}
+      shell: bash
+
+    - run: npm run build
+      shell: bash                     # shell: is REQUIRED in composite
+```
+
+### Using a Composite Action
+
+```yaml
+steps:
+  - uses: actions/checkout@v4
+  - uses: ./.github/actions/setup-project
+    with:
+      node-version: '22'
+  - run: npm test
+```
+
+## Matrix Strategy
+
+### Basic Matrix
+
+```yaml
+strategy:
+  matrix:
+    os: [ubuntu-latest, windows-latest, macos-latest]
+    node: [18, 20, 22]
+    # Creates 3 x 3 = 9 jobs
+```
+
+### Include and Exclude
+
+```yaml
+strategy:
+  matrix:
+    os: [ubuntu-latest, windows-latest]
+    node: [18, 20]
+    include:
+      # Add a job with extra variables
+      - os: ubuntu-latest
+        node: 22
+        experimental: true
+      # Add variables to existing combo
+      - os: windows-latest
+        node: 20
+        npm-version: 10
+    exclude:
+      # Remove a specific combo
+      - os: windows-latest
+        node: 18
+```
+
+### Matrix with `continue-on-error`
+
+```yaml
+strategy:
+  fail-fast: false
+  matrix:
+    node: [18, 20, 22]
+    include:
+      - node: 22
+        experimental: true
+
+jobs:
+  test:
+    continue-on-error: ${{ matrix.experimental || false }}
+```
+
+### Single-Dimension Matrix (List of Configs)
+
+```yaml
+strategy:
+  matrix:
+    include:
+      - name: Unit Tests
+        command: npm run test:unit
+      - name: Integration Tests
+        command: npm run test:integration
+        timeout: 30
+      - name: E2E Tests
+        command: npm run test:e2e
+        timeout: 60
+```
+
+## Artifacts
+
+### Upload and Download Between Jobs
+
+```yaml
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - run: npm ci && npm run build
+
+      - uses: actions/upload-artifact@v4
+        with:
+          name: dist
+          path: dist/
+          retention-days: 1           # Short-lived build artifacts
+          if-no-files-found: error    # Fail if nothing to upload
+
+  deploy:
+    needs: build
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/download-artifact@v4
+        with:
+          name: dist
+          path: dist/
+
+      - run: ls -la dist/            # Verify download
+```
+
+### Multiple Artifact Upload (Matrix)
+
+```yaml
+# Upload with unique names per matrix
+- uses: actions/upload-artifact@v4
+  with:
+    name: results-${{ matrix.os }}-${{ matrix.node }}
+    path: test-results/
+
+# Download all in a later job
+- uses: actions/download-artifact@v4
+  with:
+    pattern: results-*
+    merge-multiple: true
+    path: all-results/
+```
+
+## Environment Protection Rules
+
+Environments provide deployment gates and scoped secrets.
+
+### Setting Up Environments
+
+Environments are configured in **Settings > Environments** on GitHub. Options:
+
+| Setting | Purpose |
+|---------|---------|
+| Required reviewers | Manual approval before deployment (up to 6 reviewers) |
+| Wait timer | Delay in minutes before deployment proceeds |
+| Deployment branches | Restrict which branches can deploy (e.g., only `main`) |
+| Environment secrets | Secrets scoped to this environment only |
+| Environment variables | Variables scoped to this environment |
+
+### Using Environments in Workflows
+
+```yaml
+jobs:
+  deploy-staging:
+    runs-on: ubuntu-latest
+    environment:
+      name: staging
+      url: https://staging.example.com   # Shown in deployment status
+    steps:
+      - run: deploy --env staging
+        env:
+          API_KEY: ${{ secrets.API_KEY }}  # Environment-scoped secret
+
+  deploy-production:
+    needs: deploy-staging
+    runs-on: ubuntu-latest
+    environment:
+      name: production
+      url: https://example.com
+    steps:
+      - run: deploy --env production
+```
+
+## Concurrency Control
+
+### Cancel Previous Runs on Same Branch
+
+```yaml
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+```
+
+### Deployment Queue (No Cancellation)
+
+```yaml
+concurrency:
+  group: deploy-production
+  cancel-in-progress: false           # Queue instead of cancel
+```
+
+### Per-PR Concurrency
+
+```yaml
+concurrency:
+  group: pr-${{ github.event.pull_request.number }}
+  cancel-in-progress: true
+```
+
+## Self-Hosted Runners
+
+### Runner Labels
+
+```yaml
+jobs:
+  build:
+    runs-on: [self-hosted, linux, x64, gpu]    # Match all labels
+```
+
+### Runner Groups (Enterprise/Org)
+
+```yaml
+jobs:
+  build:
+    runs-on:
+      group: production-runners
+      labels: [linux, x64]
+```
+
+### Hybrid Strategy
+
+```yaml
+strategy:
+  matrix:
+    runner: [ubuntu-latest, self-hosted]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.runner }}
+```
+
+## OIDC for Cloud Deployment
+
+OIDC eliminates stored cloud credentials. GitHub issues a short-lived JWT that your cloud provider trusts.
+
+### AWS
+
+```yaml
+permissions:
+  id-token: write
+  contents: read
+
+steps:
+  - uses: aws-actions/configure-aws-credentials@v4
+    with:
+      role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
+      aws-region: us-east-1
+      # No access keys needed
+
+  - run: aws s3 sync dist/ s3://my-bucket
+```
+
+### GCP
+
+```yaml
+permissions:
+  id-token: write
+  contents: read
+
+steps:
+  - uses: google-github-actions/auth@v2
+    with:
+      workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/github/providers/my-repo'
+      service_account: 'deploy@my-project.iam.gserviceaccount.com'
+
+  - uses: google-github-actions/setup-gcloud@v2
+
+  - run: gcloud run deploy my-service --image gcr.io/my-project/app
+```
+
+### Azure
+
+```yaml
+permissions:
+  id-token: write
+  contents: read
+
+steps:
+  - uses: azure/login@v2
+    with:
+      client-id: ${{ secrets.AZURE_CLIENT_ID }}
+      tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+
+  - run: az webapp deploy --name my-app --src-path dist/
+```
+
+## Common Action Recipes
+
+### Checkout
+
+```yaml
+# Standard checkout
+- uses: actions/checkout@v4
+
+# Full history (for changelogs, git describe)
+- uses: actions/checkout@v4
+  with:
+    fetch-depth: 0
+
+# Checkout PR head (for pull_request_target)
+- uses: actions/checkout@v4
+  with:
+    ref: ${{ github.event.pull_request.head.sha }}
+
+# Checkout with submodules
+- uses: actions/checkout@v4
+  with:
+    submodules: recursive
+    token: ${{ secrets.PAT }}         # For private submodules
+```
+
+### Setup Node.js
+
+```yaml
+- uses: actions/setup-node@v4
+  with:
+    node-version: 20
+    cache: npm                        # Or pnpm, yarn
+    registry-url: https://npm.pkg.github.com
+```
+
+### Setup Go
+
+```yaml
+- uses: actions/setup-go@v5
+  with:
+    go-version-file: go.mod           # Read from go.mod
+    cache: true                       # Cache go modules
+```
+
+### Setup Python
+
+```yaml
+- uses: actions/setup-python@v5
+  with:
+    python-version: '3.12'
+    cache: pip                        # Or pipenv, poetry
+```
+
+### Docker Build and Push
+
+```yaml
+- uses: docker/setup-buildx-action@v3
+
+- uses: docker/login-action@v3
+  with:
+    registry: ghcr.io
+    username: ${{ github.actor }}
+    password: ${{ secrets.GITHUB_TOKEN }}
+
+- uses: docker/metadata-action@v5
+  id: meta
+  with:
+    images: ghcr.io/${{ github.repository }}
+    tags: |
+      type=semver,pattern={{version}}
+      type=semver,pattern={{major}}.{{minor}}
+      type=sha,prefix=
+      type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
+
+- uses: docker/build-push-action@v6
+  with:
+    context: .
+    push: true
+    tags: ${{ steps.meta.outputs.tags }}
+    labels: ${{ steps.meta.outputs.labels }}
+    cache-from: type=gha
+    cache-to: type=gha,mode=max
+    platforms: linux/amd64,linux/arm64
+```
+
+## Debugging Workflows
+
+### Enable Debug Logging
+
+Set repository secret `ACTIONS_STEP_DEBUG` to `true` for verbose step output.
+
+Or re-run a failed job with "Enable debug logging" checkbox.
+
+### Debug Expressions
+
+```yaml
+- run: |
+    echo "Event: ${{ github.event_name }}"
+    echo "Ref: ${{ github.ref }}"
+    echo "SHA: ${{ github.sha }}"
+    echo "Actor: ${{ github.actor }}"
+    echo "Matrix: ${{ toJson(matrix) }}"
+    echo "Env: ${{ toJson(env) }}"
+
+# Dump full event payload
+- run: cat "$GITHUB_EVENT_PATH" | jq .
+```
+
+### Local Testing with `act`
+
+```bash
+# Install act (https://github.com/nektos/act)
+brew install act                      # macOS
+choco install act-cli                 # Windows
+
+# Run default event (push)
+act
+
+# Run specific workflow
+act -W .github/workflows/ci.yml
+
+# Run specific job
+act -j test
+
+# Run with specific event
+act pull_request
+
+# Pass secrets
+act -s GITHUB_TOKEN="$(gh auth token)"
+
+# Use specific runner image
+act -P ubuntu-latest=catthehacker/ubuntu:act-latest
+
+# Dry run (show what would run)
+act -n
+```
+
+### Common Debugging Patterns
+
+```yaml
+# Temporarily add to any step
+- run: |
+    echo "::group::Debug Info"
+    env | sort
+    echo "::endgroup::"
+
+# Check file existence
+- run: |
+    echo "::group::Workspace Contents"
+    find . -maxdepth 3 -type f | head -50
+    echo "::endgroup::"
+
+# Conditional debug step
+- if: runner.debug == '1'
+  run: |
+    echo "Debug mode enabled"
+    cat package.json | jq '.scripts'
+```
+
+### Workflow Run Annotations
+
+```yaml
+# Warning annotation
+- run: echo "::warning file=app.js,line=1::Missing error handling"
+
+# Error annotation
+- run: echo "::error file=app.js,line=10,col=5::Syntax error"
+
+# Notice annotation
+- run: echo "::notice::Deployment complete"
+
+# Group log lines
+- run: |
+    echo "::group::Install Dependencies"
+    npm ci
+    echo "::endgroup::"
+```

+ 647 - 0
skills/ci-cd-ops/references/release-automation.md

@@ -0,0 +1,647 @@
+# Release Automation Reference
+
+## Table of Contents
+
+- [Semantic Versioning](#semantic-versioning)
+- [Conventional Commits](#conventional-commits)
+- [Tool Comparison](#tool-comparison)
+- [semantic-release](#semantic-release)
+- [changesets](#changesets)
+- [release-please](#release-please)
+- [goreleaser](#goreleaser)
+- [Changelog Generation](#changelog-generation)
+- [GitHub Releases](#github-releases)
+- [NPM Publishing](#npm-publishing)
+- [Docker Image Tagging](#docker-image-tagging)
+- [Monorepo Release Strategies](#monorepo-release-strategies)
+
+---
+
+## Semantic Versioning
+
+Format: `MAJOR.MINOR.PATCH` (e.g., `2.4.1`)
+
+| Increment | When | Example |
+|-----------|------|---------|
+| MAJOR | Breaking API changes | `1.9.0` -> `2.0.0` |
+| MINOR | New features (backward compatible) | `2.0.0` -> `2.1.0` |
+| PATCH | Bug fixes (backward compatible) | `2.1.0` -> `2.1.1` |
+
+Pre-release versions: `2.0.0-alpha.1`, `2.0.0-beta.3`, `2.0.0-rc.1`
+
+Build metadata: `2.0.0+build.123` (ignored in version precedence)
+
+## Conventional Commits
+
+Format: `<type>(<scope>): <description>`
+
+| Type | Version Bump | Example |
+|------|-------------|---------|
+| `fix` | PATCH | `fix(auth): handle expired tokens` |
+| `feat` | MINOR | `feat(api): add user search endpoint` |
+| `feat` + `BREAKING CHANGE:` | MAJOR | `feat(api)!: change response format` |
+| `docs`, `chore`, `ci`, `style`, `refactor`, `test`, `perf` | None | `docs: update API reference` |
+
+Breaking changes can be indicated two ways:
+
+```
+feat(api)!: remove legacy endpoint
+
+BREAKING CHANGE: The /v1/users endpoint has been removed. Use /v2/users instead.
+```
+
+## Tool Comparison
+
+| Feature | semantic-release | changesets | release-please | goreleaser |
+|---------|-----------------|------------|----------------|------------|
+| Language | Any (Node-based) | Any (Node-based) | Any | Go projects |
+| Versioning | Automatic from commits | Manual (developer intent) | Automatic from commits | From git tags |
+| Changelog | Auto-generated | Manual + auto | Auto-generated | Auto-generated |
+| Monorepo | Via plugins | Native | Native | N/A |
+| CI integration | Deep | Moderate | GitHub-native | Deep |
+| NPM publish | Built-in | Built-in | Via workflow | N/A |
+| GitHub Release | Built-in | Via script | Built-in | Built-in |
+| Human review | No (fully auto) | Yes (PR-based) | Yes (PR-based) | No |
+| Best for | Full automation | Monorepos, team review | Google-style, simple setup | Go binaries |
+
+## semantic-release
+
+Fully automated versioning and publishing based on commit messages.
+
+### Configuration
+
+```json
+// .releaserc.json
+{
+  "branches": [
+    "main",
+    { "name": "next", "prerelease": true },
+    { "name": "beta", "prerelease": true }
+  ],
+  "plugins": [
+    "@semantic-release/commit-analyzer",
+    "@semantic-release/release-notes-generator",
+    "@semantic-release/changelog",
+    ["@semantic-release/npm", {
+      "npmPublish": true
+    }],
+    ["@semantic-release/github", {
+      "assets": ["dist/*.tar.gz"]
+    }],
+    ["@semantic-release/git", {
+      "assets": ["CHANGELOG.md", "package.json"],
+      "message": "chore(release): ${nextRelease.version} [skip ci]"
+    }]
+  ]
+}
+```
+
+### GitHub Actions Workflow
+
+```yaml
+# .github/workflows/release.yml
+name: Release
+on:
+  push:
+    branches: [main, next, beta]
+
+permissions:
+  contents: write
+  issues: write
+  pull-requests: write
+  packages: write
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          persist-credentials: false
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
+          cache: npm
+
+      - run: npm ci
+
+      - run: npx semantic-release
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+```
+
+### Custom Commit Analyzer Rules
+
+```json
+// .releaserc.json
+{
+  "plugins": [
+    ["@semantic-release/commit-analyzer", {
+      "preset": "conventionalcommits",
+      "releaseRules": [
+        { "type": "perf", "release": "patch" },
+        { "type": "refactor", "release": "patch" },
+        { "type": "docs", "scope": "api", "release": "patch" }
+      ]
+    }]
+  ]
+}
+```
+
+## changesets
+
+Developer-driven versioning with PR-based workflow. Ideal for monorepos.
+
+### Setup
+
+```bash
+npx @changesets/cli init
+# Creates .changeset/ directory with config.json
+```
+
+### Configuration
+
+```json
+// .changeset/config.json
+{
+  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
+  "changelog": "@changesets/cli/changelog",
+  "commit": false,
+  "fixed": [],
+  "linked": [["@myorg/core", "@myorg/utils"]],
+  "access": "public",
+  "baseBranch": "main",
+  "updateInternalDependencies": "patch",
+  "ignore": ["@myorg/docs", "@myorg/dev-tools"]
+}
+```
+
+### Developer Workflow
+
+```bash
+# 1. Create a changeset (interactive)
+npx changeset
+
+# 2. This creates a file like .changeset/brave-dogs-dance.md:
+# ---
+# "@myorg/core": minor
+# "@myorg/utils": patch
+# ---
+#
+# Add search functionality to core package
+
+# 3. Commit the changeset with your PR
+git add .changeset/brave-dogs-dance.md
+git commit -m "feat: add search functionality"
+```
+
+### GitHub Actions Workflow
+
+```yaml
+# .github/workflows/release.yml
+name: Release
+on:
+  push:
+    branches: [main]
+
+permissions:
+  contents: write
+  pull-requests: write
+  packages: write
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
+          cache: npm
+
+      - run: npm ci
+
+      - name: Create Release PR or Publish
+        uses: changesets/action@v1
+        with:
+          publish: npx changeset publish
+          version: npx changeset version
+          title: 'chore: version packages'
+          commit: 'chore: version packages'
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+```
+
+## release-please
+
+Google's release automation. Creates release PRs automatically from conventional commits.
+
+### GitHub Actions Workflow
+
+```yaml
+# .github/workflows/release.yml
+name: Release
+on:
+  push:
+    branches: [main]
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    outputs:
+      release_created: ${{ steps.release.outputs.release_created }}
+      tag_name: ${{ steps.release.outputs.tag_name }}
+    steps:
+      - uses: googleapis/release-please-action@v4
+        id: release
+        with:
+          release-type: node           # or python, go, simple, etc.
+
+      # Steps that only run on release
+      - uses: actions/checkout@v4
+        if: ${{ steps.release.outputs.release_created }}
+
+      - uses: actions/setup-node@v4
+        if: ${{ steps.release.outputs.release_created }}
+        with:
+          node-version: 20
+          registry-url: https://registry.npmjs.org
+
+      - run: npm ci && npm publish
+        if: ${{ steps.release.outputs.release_created }}
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+```
+
+### Configuration
+
+```json
+// release-please-config.json
+{
+  "packages": {
+    ".": {
+      "release-type": "node",
+      "changelog-path": "CHANGELOG.md",
+      "bump-minor-pre-major": true,
+      "bump-patch-for-minor-pre-major": true
+    }
+  }
+}
+```
+
+## goreleaser
+
+Release automation for Go projects: cross-compilation, archives, Docker images, and more.
+
+### Configuration
+
+```yaml
+# .goreleaser.yml
+version: 2
+
+before:
+  hooks:
+    - go mod tidy
+    - go generate ./...
+
+builds:
+  - id: myapp
+    main: ./cmd/myapp
+    binary: myapp
+    env:
+      - CGO_ENABLED=0
+    goos: [linux, darwin, windows]
+    goarch: [amd64, arm64]
+    ldflags:
+      - -s -w
+      - -X main.version={{.Version}}
+      - -X main.commit={{.Commit}}
+      - -X main.date={{.Date}}
+
+archives:
+  - id: default
+    format: tar.gz
+    format_overrides:
+      - goos: windows
+        format: zip
+    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
+
+dockers:
+  - image_templates:
+      - "ghcr.io/owner/myapp:{{ .Version }}"
+      - "ghcr.io/owner/myapp:latest"
+    dockerfile: Dockerfile
+    build_flag_templates:
+      - "--build-arg=VERSION={{.Version}}"
+
+checksum:
+  name_template: 'checksums.txt'
+
+changelog:
+  sort: asc
+  filters:
+    exclude:
+      - '^docs:'
+      - '^chore:'
+      - '^ci:'
+
+release:
+  github:
+    owner: myorg
+    name: myapp
+  draft: false
+  prerelease: auto
+```
+
+### GitHub Actions Workflow
+
+```yaml
+# .github/workflows/release.yml
+name: Release
+on:
+  push:
+    tags: ['v*']
+
+permissions:
+  contents: write
+  packages: write
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
+      - uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - uses: goreleaser/goreleaser-action@v6
+        with:
+          version: '~> v2'
+          args: release --clean
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+```
+
+### Local Testing
+
+```bash
+# Dry run (no publish)
+goreleaser release --snapshot --clean
+
+# Check config
+goreleaser check
+
+# Build only (no release)
+goreleaser build --snapshot --clean
+```
+
+## Changelog Generation
+
+### Standalone Changelog Tools
+
+```bash
+# conventional-changelog-cli
+npx conventional-changelog -p conventionalcommits -i CHANGELOG.md -s
+
+# git-cliff (Rust, fast)
+git cliff -o CHANGELOG.md
+git cliff --latest                    # Only latest release
+git cliff --unreleased                # Only unreleased changes
+```
+
+### git-cliff Configuration
+
+```toml
+# cliff.toml
+[changelog]
+header = "# Changelog\n\n"
+body = """
+{% for group, commits in commits | group_by(attribute="group") %}
+### {{ group | upper_first }}
+{% for commit in commits %}
+- {{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})\
+{% endfor %}
+{% endfor %}
+"""
+
+[git]
+conventional_commits = true
+filter_unconventional = true
+commit_parsers = [
+  { message = "^feat", group = "Features" },
+  { message = "^fix", group = "Bug Fixes" },
+  { message = "^perf", group = "Performance" },
+  { message = "^refactor", group = "Refactoring" },
+]
+```
+
+## GitHub Releases
+
+### Creating Releases with `gh`
+
+```bash
+# Auto-generate notes from commits
+gh release create v1.2.0 --generate-notes
+
+# With title and custom notes
+gh release create v1.2.0 \
+  --title "v1.2.0" \
+  --notes "## What's New
+- Feature A
+- Bug fix B"
+
+# Upload assets
+gh release create v1.2.0 dist/*.tar.gz checksums.txt
+
+# Create draft release
+gh release create v1.2.0 --draft
+
+# Create pre-release
+gh release create v2.0.0-beta.1 --prerelease
+
+# Edit existing release
+gh release edit v1.2.0 --draft=false
+```
+
+### GitHub Actions Release
+
+```yaml
+- run: |
+    gh release create "$TAG" \
+      --title "$TAG" \
+      --generate-notes \
+      dist/*
+  env:
+    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    TAG: ${{ github.ref_name }}
+```
+
+## NPM Publishing
+
+### Complete NPM Release Workflow
+
+```yaml
+name: Publish to NPM
+on:
+  push:
+    tags: ['v*']
+
+permissions:
+  contents: write
+  id-token: write                     # For npm provenance
+
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
+          registry-url: https://registry.npmjs.org
+
+      - run: npm ci
+      - run: npm test
+      - run: npm publish --provenance --access public
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+```
+
+### Publishing to GitHub Packages
+
+```yaml
+- uses: actions/setup-node@v4
+  with:
+    node-version: 20
+    registry-url: https://npm.pkg.github.com
+    scope: '@myorg'
+
+- run: npm publish
+  env:
+    NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+```
+
+## Docker Image Tagging
+
+### Tagging Strategy
+
+| Tag | Source | Example | Purpose |
+|-----|--------|---------|---------|
+| `latest` | Main branch | `myapp:latest` | Most recent stable |
+| `x.y.z` | Git tag | `myapp:1.2.3` | Immutable release |
+| `x.y` | Git tag | `myapp:1.2` | Latest patch |
+| `x` | Git tag | `myapp:1` | Latest minor |
+| `sha-abc1234` | Commit SHA | `myapp:sha-abc1234` | Exact build |
+| `pr-42` | PR number | `myapp:pr-42` | PR preview |
+| `edge` | Main branch | `myapp:edge` | Bleeding edge |
+
+### docker/metadata-action
+
+```yaml
+- uses: docker/metadata-action@v5
+  id: meta
+  with:
+    images: |
+      ghcr.io/${{ github.repository }}
+      docker.io/myorg/myapp
+    tags: |
+      type=semver,pattern={{version}}
+      type=semver,pattern={{major}}.{{minor}}
+      type=semver,pattern={{major}}
+      type=sha,prefix=
+      type=ref,event=branch
+      type=ref,event=pr
+      type=raw,value=latest,enable={{is_default_branch}}
+```
+
+## Monorepo Release Strategies
+
+### Independent Versioning (changesets)
+
+Each package has its own version. Best for library monorepos.
+
+```json
+// .changeset/config.json
+{
+  "fixed": [],
+  "linked": [["@myorg/client-*"]],   # These move together
+  "access": "public"
+}
+```
+
+### Fixed Versioning (release-please)
+
+All packages share one version. Best for application monorepos.
+
+```json
+// release-please-config.json
+{
+  "packages": {
+    "packages/core": { "release-type": "node" },
+    "packages/cli": { "release-type": "node" },
+    "packages/web": { "release-type": "node" }
+  },
+  "group-pull-requests-pattern": "chore: release main"
+}
+```
+
+### Path-Filtered Releases
+
+```yaml
+on:
+  push:
+    branches: [main]
+    paths:
+      - 'packages/api/**'
+
+jobs:
+  release-api:
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: packages/api
+    steps:
+      - uses: actions/checkout@v4
+      - run: npm ci
+      - run: npm publish
+```
+
+### Turborepo + changesets
+
+```yaml
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npx turbo run build --filter='...[origin/main]'
+      - uses: changesets/action@v1
+        with:
+          publish: npx changeset publish
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+```

+ 987 - 0
skills/ci-cd-ops/references/testing-pipelines.md

@@ -0,0 +1,987 @@
+# Testing Pipelines Reference
+
+## Table of Contents
+
+- [Test Stages](#test-stages)
+- [Parallel Test Execution](#parallel-test-execution)
+- [Test Splitting Strategies](#test-splitting-strategies)
+- [Code Coverage](#code-coverage)
+- [Database Testing in CI](#database-testing-in-ci)
+- [Docker in CI](#docker-in-ci)
+- [E2E Testing in CI](#e2e-testing-in-ci)
+- [Flaky Test Detection and Retry](#flaky-test-detection-and-retry)
+- [Performance Testing in CI](#performance-testing-in-ci)
+- [Status Checks and Branch Protection](#status-checks-and-branch-protection)
+- [Pull Request Checks Workflow](#pull-request-checks-workflow)
+- [Deployment Pipelines](#deployment-pipelines)
+
+---
+
+## Test Stages
+
+A typical CI pipeline progresses through these stages, failing fast on cheap checks:
+
+```
+┌─────────┐   ┌──────────┐   ┌─────────────┐   ┌──────────┐   ┌────────┐
+│  Lint    │──>│  Unit    │──>│ Integration │──>│  E2E     │──>│ Deploy │
+│  ~1 min  │   │  ~2 min  │   │  ~5 min     │   │  ~10 min │   │        │
+└─────────┘   └──────────┘   └─────────────┘   └──────────┘   └────────┘
+```
+
+### Stage Characteristics
+
+| Stage | Speed | Dependencies | Flakiness | What It Catches |
+|-------|-------|-------------|-----------|----------------|
+| Lint / Format | Fastest | None | None | Style, syntax, type errors |
+| Unit tests | Fast | None (mocked) | Low | Logic bugs, regressions |
+| Integration | Medium | Services (DB, cache) | Medium | API contracts, data flow |
+| E2E | Slow | Full environment | High | User-facing regressions |
+| Performance | Slow | Full environment | Medium | Performance regressions |
+
+### Staged Workflow
+
+```yaml
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run lint
+      - run: npm run typecheck
+
+  unit:
+    needs: lint
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run test:unit -- --coverage
+
+  integration:
+    needs: lint
+    runs-on: ubuntu-latest
+    services:
+      postgres:
+        image: postgres:16
+        env:
+          POSTGRES_DB: test
+          POSTGRES_USER: test
+          POSTGRES_PASSWORD: test
+        ports: ['5432:5432']
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run test:integration
+        env:
+          DATABASE_URL: postgresql://test:test@localhost:5432/test
+
+  e2e:
+    needs: [unit, integration]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npx playwright install --with-deps chromium
+      - run: npm run test:e2e
+      - uses: actions/upload-artifact@v4
+        if: failure()
+        with:
+          name: playwright-report
+          path: playwright-report/
+          retention-days: 7
+```
+
+## Parallel Test Execution
+
+### Matrix-Based Parallelism
+
+```yaml
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        shard: [1, 2, 3, 4]
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm test -- --shard=${{ matrix.shard }}/${{ strategy.job-total }}
+```
+
+### Playwright Sharding
+
+```yaml
+strategy:
+  matrix:
+    shard: [1/4, 2/4, 3/4, 4/4]
+steps:
+  - run: npx playwright test --shard=${{ matrix.shard }}
+
+  - uses: actions/upload-artifact@v4
+    if: always()
+    with:
+      name: blob-report-${{ strategy.job-index }}
+      path: blob-report/
+
+# Merge reports in a separate job
+merge-reports:
+  needs: test
+  runs-on: ubuntu-latest
+  steps:
+    - uses: actions/download-artifact@v4
+      with:
+        pattern: blob-report-*
+        merge-multiple: true
+        path: all-blob-reports
+
+    - run: npx playwright merge-reports --reporter html all-blob-reports
+```
+
+### Jest Parallelism
+
+```yaml
+# Jest auto-parallelizes across workers
+- run: npx jest --maxWorkers=50%      # Use half available CPUs
+- run: npx jest --maxWorkers=4        # Or specify exactly
+
+# With sharding (Jest 28+)
+- run: npx jest --shard=${{ matrix.shard }}/${{ strategy.job-total }}
+```
+
+## Test Splitting Strategies
+
+### By File Count (Simple)
+
+```bash
+# Split test files evenly across shards
+files=$(find src -name '*.test.ts' | sort)
+total=$(echo "$files" | wc -l)
+per_shard=$(( (total + SHARD_COUNT - 1) / SHARD_COUNT ))
+echo "$files" | sed -n "${start},${end}p"
+```
+
+### By Timing (Optimal)
+
+```yaml
+# Use test timing data from previous runs
+- uses: actions/cache@v4
+  with:
+    path: .test-timings
+    key: test-timings-${{ github.ref }}
+    restore-keys: test-timings-
+
+- run: |
+    npx jest --json --outputFile=results.json
+    # Store timing data for next run
+    jq '[.testResults[] | {file: .testFilePath, duration: .perfStats.runtime}]' \
+      results.json > .test-timings
+```
+
+### By Test Type
+
+```yaml
+strategy:
+  matrix:
+    include:
+      - name: unit
+        command: npm run test:unit
+        timeout: 10
+      - name: integration
+        command: npm run test:integration
+        timeout: 20
+      - name: e2e
+        command: npm run test:e2e
+        timeout: 30
+
+jobs:
+  test:
+    timeout-minutes: ${{ matrix.timeout }}
+    steps:
+      - run: ${{ matrix.command }}
+```
+
+## Code Coverage
+
+### Codecov
+
+```yaml
+- run: npm test -- --coverage
+
+- uses: codecov/codecov-action@v4
+  with:
+    token: ${{ secrets.CODECOV_TOKEN }}
+    files: coverage/lcov.info
+    flags: unittests
+    fail_ci_if_error: true
+```
+
+### Coveralls
+
+```yaml
+- run: npm test -- --coverage
+
+- uses: coverallsapp/github-action@v2
+  with:
+    github-token: ${{ secrets.GITHUB_TOKEN }}
+    path-to-lcov: coverage/lcov.info
+```
+
+### Coverage Gates
+
+```yaml
+# Fail if coverage drops
+- run: |
+    COVERAGE=$(jq '.total.lines.pct' coverage/coverage-summary.json)
+    echo "Coverage: ${COVERAGE}%"
+    if (( $(echo "$COVERAGE < 80" | bc -l) )); then
+      echo "::error::Coverage ${COVERAGE}% is below 80% threshold"
+      exit 1
+    fi
+```
+
+### Multi-Platform Coverage Merge
+
+```yaml
+# Upload per-shard coverage
+- uses: actions/upload-artifact@v4
+  with:
+    name: coverage-${{ matrix.shard }}
+    path: coverage/
+
+# Merge in separate job
+merge-coverage:
+  needs: test
+  runs-on: ubuntu-latest
+  steps:
+    - uses: actions/download-artifact@v4
+      with:
+        pattern: coverage-*
+        merge-multiple: true
+        path: all-coverage
+
+    - run: npx nyc merge all-coverage merged-coverage.json
+    - run: npx nyc report --reporter=lcov --temp-dir=.
+
+    - uses: codecov/codecov-action@v4
+      with:
+        token: ${{ secrets.CODECOV_TOKEN }}
+```
+
+## Database Testing in CI
+
+### Service Containers
+
+```yaml
+services:
+  postgres:
+    image: postgres:16-alpine
+    env:
+      POSTGRES_DB: test_db
+      POSTGRES_USER: test_user
+      POSTGRES_PASSWORD: test_pass
+    ports: ['5432:5432']
+    options: >-
+      --health-cmd pg_isready
+      --health-interval 10s
+      --health-timeout 5s
+      --health-retries 5
+
+  redis:
+    image: redis:7-alpine
+    ports: ['6379:6379']
+    options: >-
+      --health-cmd "redis-cli ping"
+      --health-interval 10s
+      --health-timeout 5s
+      --health-retries 5
+
+  mysql:
+    image: mysql:8
+    env:
+      MYSQL_ROOT_PASSWORD: root
+      MYSQL_DATABASE: test_db
+    ports: ['3306:3306']
+    options: >-
+      --health-cmd "mysqladmin ping -h localhost"
+      --health-interval 10s
+      --health-timeout 5s
+      --health-retries 5
+```
+
+### Testcontainers
+
+```yaml
+# Testcontainers manages its own containers - just needs Docker
+steps:
+  - uses: actions/checkout@v4
+  - uses: actions/setup-java@v4
+    with: { java-version: 21, distribution: temurin }
+
+  # Testcontainers needs Docker socket access (default on ubuntu-latest)
+  - run: ./gradlew test
+    env:
+      TESTCONTAINERS_RYUK_DISABLED: false
+```
+
+### Database Migrations in CI
+
+```yaml
+steps:
+  - run: npm run db:migrate
+    env:
+      DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
+
+  - run: npm run db:seed           # Optional test data
+
+  - run: npm run test:integration
+    env:
+      DATABASE_URL: postgresql://test_user:test_pass@localhost:5432/test_db
+```
+
+## Docker in CI
+
+### Docker-in-Docker (DinD)
+
+```yaml
+# Not recommended for GitHub Actions - use standard Docker
+# GitHub-hosted runners have Docker pre-installed
+
+steps:
+  - uses: actions/checkout@v4
+  - run: docker build -t myapp .
+  - run: docker run myapp npm test
+```
+
+### Docker-outside-of-Docker (DooD)
+
+```yaml
+# Mount the host Docker socket (for self-hosted runners)
+# GitHub-hosted runners use this by default
+steps:
+  - run: docker compose up -d
+  - run: docker compose run app npm test
+  - run: docker compose down
+```
+
+### Docker Compose in CI
+
+```yaml
+steps:
+  - uses: actions/checkout@v4
+
+  - run: docker compose -f docker-compose.test.yml up -d --wait
+  - run: docker compose -f docker-compose.test.yml run app npm test
+  - run: docker compose -f docker-compose.test.yml down -v
+
+  # Alternative: use --exit-code-from
+  - run: docker compose -f docker-compose.test.yml up --exit-code-from test
+```
+
+## E2E Testing in CI
+
+### Playwright
+
+```yaml
+steps:
+  - uses: actions/checkout@v4
+  - uses: actions/setup-node@v4
+    with: { node-version: 20, cache: npm }
+  - run: npm ci
+
+  # Install browsers (cache for speed)
+  - name: Cache Playwright browsers
+    uses: actions/cache@v4
+    id: playwright-cache
+    with:
+      path: ~/.cache/ms-playwright
+      key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
+
+  - if: steps.playwright-cache.outputs.cache-hit != 'true'
+    run: npx playwright install --with-deps chromium
+
+  - if: steps.playwright-cache.outputs.cache-hit == 'true'
+    run: npx playwright install-deps chromium
+
+  # Run tests
+  - run: npx playwright test
+    env:
+      CI: true
+
+  # Upload artifacts on failure
+  - uses: actions/upload-artifact@v4
+    if: failure()
+    with:
+      name: playwright-report
+      path: |
+        playwright-report/
+        test-results/
+      retention-days: 7
+```
+
+### Cypress
+
+```yaml
+steps:
+  - uses: actions/checkout@v4
+
+  - uses: cypress-io/github-action@v6
+    with:
+      build: npm run build
+      start: npm start
+      wait-on: 'http://localhost:3000'
+      wait-on-timeout: 120
+      browser: chrome
+      record: true                    # Cypress Cloud recording
+    env:
+      CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+
+  - uses: actions/upload-artifact@v4
+    if: failure()
+    with:
+      name: cypress-screenshots
+      path: cypress/screenshots/
+```
+
+### E2E with Containerized App
+
+```yaml
+steps:
+  - uses: actions/checkout@v4
+
+  # Start the app in Docker
+  - run: docker compose up -d --wait
+
+  # Run E2E tests against containerized app
+  - run: npm ci
+  - run: npx playwright test
+    env:
+      BASE_URL: http://localhost:3000
+
+  - run: docker compose down -v
+    if: always()
+```
+
+## Flaky Test Detection and Retry
+
+### GitHub Actions Retry
+
+```yaml
+# Retry the entire job
+- uses: nick-fields/retry@v3
+  with:
+    timeout_minutes: 10
+    max_attempts: 3
+    command: npm run test:e2e
+    retry_on: error
+```
+
+### Built-in Test Runner Retries
+
+```bash
+# Playwright
+npx playwright test --retries=2
+
+# Jest
+npx jest --bail --forceExit          # Fail fast, clean exit
+
+# Vitest
+npx vitest --retry=2
+
+# pytest
+pip install pytest-rerunfailures
+pytest --reruns 3 --reruns-delay 1
+```
+
+### Flaky Test Quarantine Pattern
+
+```yaml
+jobs:
+  stable-tests:
+    runs-on: ubuntu-latest
+    steps:
+      - run: npm test -- --testPathIgnorePatterns='flaky'
+
+  flaky-tests:
+    runs-on: ubuntu-latest
+    continue-on-error: true           # Don't block PR
+    steps:
+      - run: npm test -- --testPathPattern='flaky' --retries=3
+```
+
+### Detect New Flaky Tests
+
+```yaml
+# Run tests multiple times on PR to detect flakiness
+- run: |
+    for i in {1..5}; do
+      echo "Run $i of 5"
+      npm test -- --bail || exit 1
+    done
+```
+
+## Performance Testing in CI
+
+### Benchmark Comparison
+
+```yaml
+- uses: benchmark-action/github-action-benchmark@v1
+  with:
+    tool: 'customBiggerIsBetter'
+    output-file-path: benchmark-results.json
+    github-token: ${{ secrets.GITHUB_TOKEN }}
+    auto-push: true
+    alert-threshold: '150%'           # Alert if 50% slower
+    comment-on-alert: true
+    fail-on-alert: true
+```
+
+### Lighthouse CI
+
+```yaml
+steps:
+  - uses: actions/checkout@v4
+  - uses: actions/setup-node@v4
+    with: { node-version: 20, cache: npm }
+  - run: npm ci && npm run build
+
+  - name: Start server
+    run: npm start &
+    env: { PORT: 3000 }
+
+  - run: npx @lhci/cli autorun
+    env:
+      LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
+```
+
+### Lighthouse Configuration
+
+```json
+// lighthouserc.json
+{
+  "ci": {
+    "collect": {
+      "url": ["http://localhost:3000", "http://localhost:3000/about"],
+      "numberOfRuns": 3
+    },
+    "assert": {
+      "assertions": {
+        "categories:performance": ["error", { "minScore": 0.9 }],
+        "categories:accessibility": ["error", { "minScore": 0.95 }],
+        "categories:best-practices": ["error", { "minScore": 0.9 }],
+        "first-contentful-paint": ["warn", { "maxNumericValue": 2000 }]
+      }
+    },
+    "upload": {
+      "target": "temporary-public-storage"
+    }
+  }
+}
+```
+
+### Bundle Size Check
+
+```yaml
+- uses: andresz1/size-limit-action@v1
+  with:
+    github_token: ${{ secrets.GITHUB_TOKEN }}
+    # Reads config from .size-limit.json or package.json
+```
+
+## Status Checks and Branch Protection
+
+### Required Status Checks
+
+Configure in **Settings > Branches > Branch protection rules**:
+
+| Setting | Purpose |
+|---------|---------|
+| Require status checks to pass | Block merge until CI passes |
+| Require branches to be up to date | Ensure tests run against latest main |
+| Status checks to require | Select specific job names |
+
+### Handling Skipped Checks with Path Filters
+
+Problem: Path-filtered workflows skip jobs, blocking required checks.
+
+Solution 1: Paths-filter action with always-running workflow:
+
+```yaml
+name: CI
+on: [push, pull_request]
+
+jobs:
+  changes:
+    runs-on: ubuntu-latest
+    outputs:
+      src: ${{ steps.filter.outputs.src }}
+    steps:
+      - uses: dorny/paths-filter@v3
+        id: filter
+        with:
+          filters: |
+            src:
+              - 'src/**'
+              - 'package.json'
+
+  test:
+    needs: changes
+    if: needs.changes.outputs.src == 'true'
+    runs-on: ubuntu-latest
+    steps:
+      - run: npm test
+
+  # Always passes - use this as the required check
+  ci-success:
+    needs: [test]
+    if: always()
+    runs-on: ubuntu-latest
+    steps:
+      - run: |
+          if [[ "${{ needs.test.result }}" == "failure" ]]; then
+            exit 1
+          fi
+```
+
+Solution 2: Make the check non-required and use a merge queue.
+
+### Merge Queue
+
+```yaml
+on:
+  merge_group:                        # Triggered by merge queue
+    types: [checks_requested]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - run: npm ci && npm test
+```
+
+## Pull Request Checks Workflow
+
+Complete PR workflow with all common checks:
+
+```yaml
+name: PR Checks
+on:
+  pull_request:
+    branches: [main]
+
+concurrency:
+  group: pr-${{ github.event.pull_request.number }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+  pull-requests: write
+  checks: write
+
+jobs:
+  # ── Fast Checks ────────────────────────────────────────
+  lint:
+    name: Lint & Format
+    runs-on: ubuntu-latest
+    timeout-minutes: 5
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run lint
+      - run: npm run typecheck
+      - run: npm run format:check
+
+  # ── Unit Tests ─────────────────────────────────────────
+  unit:
+    name: Unit Tests
+    needs: lint
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run test:unit -- --coverage
+
+      - uses: codecov/codecov-action@v4
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          flags: unittests
+
+  # ── Integration Tests ─────────────────────────────────
+  integration:
+    name: Integration Tests
+    needs: lint
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    services:
+      postgres:
+        image: postgres:16-alpine
+        env:
+          POSTGRES_DB: test
+          POSTGRES_USER: test
+          POSTGRES_PASSWORD: test
+        ports: ['5432:5432']
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run db:migrate
+        env:
+          DATABASE_URL: postgresql://test:test@localhost:5432/test
+      - run: npm run test:integration
+        env:
+          DATABASE_URL: postgresql://test:test@localhost:5432/test
+
+  # ── E2E Tests ─────────────────────────────────────────
+  e2e:
+    name: E2E Tests (${{ matrix.shard }})
+    needs: [unit, integration]
+    runs-on: ubuntu-latest
+    timeout-minutes: 20
+    strategy:
+      fail-fast: false
+      matrix:
+        shard: [1/3, 2/3, 3/3]
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+
+      - name: Cache Playwright
+        uses: actions/cache@v4
+        with:
+          path: ~/.cache/ms-playwright
+          key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
+
+      - run: npx playwright install --with-deps chromium
+      - run: npx playwright test --shard=${{ matrix.shard }}
+
+      - uses: actions/upload-artifact@v4
+        if: failure()
+        with:
+          name: playwright-report-${{ strategy.job-index }}
+          path: playwright-report/
+          retention-days: 7
+
+  # ── Build Check ────────────────────────────────────────
+  build:
+    name: Build
+    needs: lint
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with: { node-version: 20, cache: npm }
+      - run: npm ci
+      - run: npm run build
+      - uses: actions/upload-artifact@v4
+        with:
+          name: build
+          path: dist/
+          retention-days: 1
+
+  # ── Gate Check (required status check) ─────────────────
+  ci-success:
+    name: CI Success
+    needs: [lint, unit, integration, e2e, build]
+    if: always()
+    runs-on: ubuntu-latest
+    steps:
+      - run: |
+          results=("${{ needs.lint.result }}" "${{ needs.unit.result }}" \
+                   "${{ needs.integration.result }}" "${{ needs.e2e.result }}" \
+                   "${{ needs.build.result }}")
+          for result in "${results[@]}"; do
+            if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
+              echo "::error::Job failed with result: $result"
+              exit 1
+            fi
+          done
+          echo "All checks passed"
+```
+
+## Deployment Pipelines
+
+### Staging to Production
+
+```yaml
+name: Deploy
+on:
+  push:
+    branches: [main]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - run: npm ci && npm run build
+      - uses: actions/upload-artifact@v4
+        with:
+          name: build
+          path: dist/
+
+  deploy-staging:
+    needs: build
+    runs-on: ubuntu-latest
+    environment:
+      name: staging
+      url: https://staging.example.com
+    steps:
+      - uses: actions/download-artifact@v4
+        with: { name: build, path: dist/ }
+      - run: ./deploy.sh staging
+        env:
+          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
+
+  smoke-test:
+    needs: deploy-staging
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - run: npm ci
+      - run: npx playwright test tests/smoke/
+        env:
+          BASE_URL: https://staging.example.com
+
+  deploy-production:
+    needs: smoke-test
+    runs-on: ubuntu-latest
+    environment:
+      name: production                # Manual approval required
+      url: https://example.com
+    steps:
+      - uses: actions/download-artifact@v4
+        with: { name: build, path: dist/ }
+      - run: ./deploy.sh production
+        env:
+          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
+```
+
+### Blue/Green Deployment
+
+```yaml
+deploy:
+  runs-on: ubuntu-latest
+  environment: production
+  steps:
+    - run: |
+        # Deploy to inactive slot
+        ACTIVE=$(curl -s https://example.com/slot)
+        INACTIVE=$([[ "$ACTIVE" == "blue" ]] && echo "green" || echo "blue")
+
+        # Deploy to inactive
+        deploy --slot "$INACTIVE"
+
+        # Health check on inactive
+        curl -sf "https://${INACTIVE}.example.com/health" || exit 1
+
+        # Swap traffic
+        swap-slots "$ACTIVE" "$INACTIVE"
+
+        echo "Swapped from $ACTIVE to $INACTIVE"
+```
+
+### Canary Deployment
+
+```yaml
+deploy:
+  runs-on: ubuntu-latest
+  environment: production
+  steps:
+    - name: Deploy canary (10%)
+      run: deploy --canary --weight=10
+
+    - name: Monitor canary (5 min)
+      run: |
+        for i in {1..5}; do
+          ERROR_RATE=$(curl -s https://metrics.example.com/error-rate)
+          if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
+            echo "::error::Error rate ${ERROR_RATE}% exceeds threshold"
+            deploy --rollback
+            exit 1
+          fi
+          sleep 60
+        done
+
+    - name: Promote canary (100%)
+      run: deploy --promote
+```
+
+### Rollback Pattern
+
+```yaml
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Version to rollback to'
+        required: true
+        type: string
+
+jobs:
+  rollback:
+    runs-on: ubuntu-latest
+    environment: production
+    steps:
+      - run: |
+          echo "Rolling back to ${{ inputs.version }}"
+          deploy --version "${{ inputs.version }}"
+
+      - run: |
+          curl -sf https://example.com/health || {
+            echo "::error::Rollback health check failed"
+            exit 1
+          }
+```
+
+### Multi-Region Deployment
+
+```yaml
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    environment: production
+    strategy:
+      max-parallel: 1                 # Deploy one region at a time
+      matrix:
+        region: [us-east-1, eu-west-1, ap-southeast-1]
+    steps:
+      - run: deploy --region ${{ matrix.region }}
+
+      - name: Region health check
+        run: |
+          curl -sf "https://${{ matrix.region }}.example.com/health" || {
+            echo "::error::Health check failed in ${{ matrix.region }}"
+            exit 1
+          }
+```

+ 0 - 0
skills/ci-cd-ops/scripts/.gitkeep


+ 283 - 0
skills/docker-ops/SKILL.md

@@ -0,0 +1,283 @@
+---
+name: docker-ops
+description: "Docker containerization patterns, Dockerfile best practices, multi-stage builds, and Docker Compose. Use for: docker, Dockerfile, docker-compose, container, image, multi-stage build, docker build, docker run, .dockerignore, health check, distroless, scratch image, BuildKit, layer caching, container security."
+allowed-tools: "Read Write Bash"
+related-skills: [container-orchestration, go-ops, rust-ops, ci-cd-ops]
+---
+
+# Docker Operations
+
+Comprehensive Docker patterns for building, running, and composing containerized applications.
+
+## Dockerfile Best Practices
+
+| Practice | Do | Don't |
+|----------|------|-------|
+| Base image | `FROM node:20-slim` | `FROM node:latest` |
+| Layer caching | Copy dependency files first, then source | `COPY . .` before `RUN install` |
+| Package install | `apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*` | Separate `RUN` for update and install |
+| User | `USER nonroot` (create if needed) | Run as root in production |
+| Multi-stage | Separate build and runtime stages | Ship compiler toolchains |
+| Secrets | `--mount=type=secret` (BuildKit) | `COPY .env .` or `ARG PASSWORD` |
+| ENTRYPOINT vs CMD | `ENTRYPOINT` for fixed binary, `CMD` for defaults | Relying on shell form for signal handling |
+| WORKDIR | `WORKDIR /app` | `RUN cd /app && ...` |
+| .dockerignore | Include `.git`, `node_modules`, `__pycache__` | No .dockerignore at all |
+| Labels | `LABEL org.opencontainers.image.*` | No metadata |
+
+## Multi-Stage Build Decision Tree
+
+Choose your runtime base image by language:
+
+```
+Go ──────────── CGO disabled? ──── Yes ──► scratch or distroless/static
+                                   No ───► distroless/base or alpine
+
+Rust ─────────── Static musl? ──── Yes ──► scratch or distroless/static
+                                   No ───► distroless/cc or debian-slim
+
+Node.js ──────── Need native? ──── Yes ──► node:20-slim
+                                   No ───► node:20-alpine (smaller)
+
+Python ────────── Need C libs? ─── Yes ──► python:3.12-slim
+                                   No ───► python:3.12-slim (still slim)
+
+Java ──────────── JRE only ──────────────► eclipse-temurin:21-jre-alpine
+```
+
+> **See:** `references/multi-stage-builds.md` for complete annotated examples per language.
+
+## Layer Caching Rules
+
+Docker caches each layer. A cache miss at layer N invalidates all subsequent layers.
+
+### What Invalidates Cache
+
+| Trigger | Effect |
+|---------|--------|
+| Changed file in `COPY`/`ADD` | Invalidates this layer + all below |
+| Changed `RUN` command text | Invalidates this layer + all below |
+| Changed `ARG` value | Invalidates from the `ARG` declaration down |
+| `--no-cache` flag | Invalidates everything |
+| Base image update | Invalidates everything |
+
+### Optimal Layer Order
+
+```dockerfile
+# 1. Base image (changes rarely)
+FROM python:3.12-slim
+
+# 2. System dependencies (changes rarely)
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    libpq-dev \
+    && rm -rf /var/lib/apt/lists/*
+
+# 3. Dependency files (changes occasionally)
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# 4. Application code (changes frequently)
+COPY src/ ./src/
+
+# 5. Runtime config (changes frequently)
+CMD ["python", "-m", "app"]
+```
+
+**Rule of thumb:** Order layers from least-frequently-changed to most-frequently-changed.
+
+## .dockerignore Essentials
+
+```dockerignore
+# Version control
+.git
+.gitignore
+
+# Dependencies (rebuilt in container)
+node_modules
+__pycache__
+*.pyc
+.venv
+vendor/
+
+# Build artifacts
+dist/
+build/
+target/
+*.egg-info
+
+# IDE and editor
+.vscode
+.idea
+*.swp
+*.swo
+
+# Docker files (prevent recursive context)
+Dockerfile*
+docker-compose*
+.dockerignore
+
+# Environment and secrets
+.env
+.env.*
+*.pem
+*.key
+
+# Documentation and tests (unless needed)
+docs/
+tests/
+*.md
+LICENSE
+```
+
+**Why it matters:** Without `.dockerignore`, `docker build` sends the entire context directory to the daemon. A `.git` folder alone can add hundreds of megabytes.
+
+## Docker Compose Quick Reference
+
+### Service Definition
+
+```yaml
+services:
+  web:
+    build:
+      context: .
+      dockerfile: Dockerfile
+      target: production        # Multi-stage target
+    image: myapp:latest
+    ports:
+      - "8080:8000"
+    environment:
+      DATABASE_URL: postgres://db:5432/app
+    env_file:
+      - .env
+    volumes:
+      - ./src:/app/src          # Bind mount (dev)
+      - app-data:/app/data      # Named volume (persistent)
+    depends_on:
+      db:
+        condition: service_healthy
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+      interval: 30s
+      timeout: 5s
+      retries: 3
+      start_period: 10s
+    restart: unless-stopped
+    networks:
+      - backend
+```
+
+### Volumes and Networks
+
+```yaml
+volumes:
+  app-data:           # Named volume (Docker-managed)
+  db-data:
+    driver: local
+
+networks:
+  backend:
+    driver: bridge
+  frontend:
+    driver: bridge
+```
+
+> **See:** `references/compose-patterns.md` for full patterns including profiles, watch mode, and override files.
+
+## Security Quick Reference
+
+| Area | Recommendation |
+|------|----------------|
+| User | Run as non-root: `RUN adduser -D appuser && USER appuser` |
+| Base image | Pin digest: `FROM python:3.12-slim@sha256:abc123...` |
+| Filesystem | Read-only root: `docker run --read-only --tmpfs /tmp` |
+| Capabilities | Drop all, add needed: `--cap-drop=ALL --cap-add=NET_BIND_SERVICE` |
+| Secrets | BuildKit secrets: `RUN --mount=type=secret,id=key cat /run/secrets/key` |
+| Scanning | Scan images: `trivy image myapp:latest` or `grype myapp:latest` |
+| No latest | Always use specific tags and pin versions |
+| Minimal image | Use distroless or scratch when possible |
+| No SUID | `RUN find / -perm /6000 -type f -exec chmod a-s {} +` |
+| Network | Use internal networks for backend services |
+
+### Non-Root User Pattern
+
+```dockerfile
+# Debian/Ubuntu-based
+RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
+COPY --chown=appuser:appuser . /app
+USER appuser
+
+# Alpine-based
+RUN addgroup -S appuser && adduser -S -G appuser appuser
+COPY --chown=appuser:appuser . /app
+USER appuser
+
+# Distroless (built-in nonroot user)
+FROM gcr.io/distroless/static:nonroot
+USER nonroot:nonroot
+```
+
+## Common Gotchas
+
+| Gotcha | Problem | Fix |
+|--------|---------|-----|
+| Large images | Shipping build tools, node_modules in final image | Multi-stage builds |
+| Cache busting | `COPY . .` before `RUN npm install` | Copy lockfile first, install, then copy source |
+| Secrets in layers | `COPY .env .` or `ARG SECRET=...` bakes secrets into image history | Use `--mount=type=secret` or runtime env vars |
+| PID 1 problem | App doesn't receive SIGTERM, zombie processes | Use `tini` as init or `exec` form for CMD |
+| Timezone | Container uses UTC | Set `TZ` env var or install `tzdata` |
+| DNS caching | Alpine musl DNS issues | Use `RUN apk add --no-cache libc6-compat` or switch to slim |
+| apt cache | `apt-get update` cached from old layer | Always combine `update && install` in one RUN |
+| Missing signals | Shell form (`CMD npm start`) wraps in `/bin/sh` | Exec form: `CMD ["node", "server.js"]` |
+| Build context size | Sending GB of data to daemon | Add `.dockerignore`, check with `docker build --progress=plain` |
+| Layer explosion | Each RUN creates a layer | Chain related commands with `&&` |
+
+### PID 1 / Signal Handling Fix
+
+```dockerfile
+# Option 1: Use tini as init process
+RUN apt-get update && apt-get install -y --no-install-recommends tini \
+    && rm -rf /var/lib/apt/lists/*
+ENTRYPOINT ["tini", "--"]
+CMD ["node", "server.js"]
+
+# Option 2: Docker init flag (Docker 23.0+)
+# docker run --init myapp
+
+# Option 3: Node.js - handle signals in code
+# process.on('SIGTERM', () => { server.close(); process.exit(0); });
+```
+
+## Essential Docker Commands
+
+```bash
+# Build
+docker build -t myapp:1.0 .
+docker build -t myapp:1.0 --target production .    # Multi-stage target
+docker build --no-cache -t myapp:1.0 .              # Force rebuild
+
+# Run
+docker run -d --name myapp -p 8080:8000 myapp:1.0
+docker run --rm -it myapp:1.0 /bin/sh               # Interactive debug
+docker run --read-only --tmpfs /tmp myapp:1.0        # Read-only root
+
+# Inspect
+docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
+docker history myapp:1.0                             # Layer breakdown
+docker inspect myapp:1.0 | jq '.[0].Config'         # Image config
+
+# Debug running container
+docker exec -it myapp /bin/sh
+docker logs -f myapp
+docker stats myapp
+
+# Cleanup
+docker system prune -a --volumes                     # Remove everything unused
+docker image prune -a                                # Remove unused images
+```
+
+## Reference Files
+
+| File | Contents |
+|------|----------|
+| `references/multi-stage-builds.md` | Per-language multi-stage patterns (Go, Rust, Node, Python) |
+| `references/compose-patterns.md` | Compose services, networking, profiles, watch, overrides |
+| `references/optimization.md` | Image size, BuildKit, security scanning, debugging |

+ 0 - 0
skills/docker-ops/assets/.gitkeep


+ 743 - 0
skills/docker-ops/references/compose-patterns.md

@@ -0,0 +1,743 @@
+# Docker Compose Patterns
+
+Production-ready Docker Compose patterns for multi-service applications.
+
+## Table of Contents
+
+- [Service Definitions](#service-definitions)
+- [Environment Variables](#environment-variables)
+- [Volume Patterns](#volume-patterns)
+- [Networking](#networking)
+- [Health Checks](#health-checks)
+- [Dependency Management](#dependency-management)
+- [Override Files](#override-files)
+- [Profiles for Optional Services](#profiles-for-optional-services)
+- [Docker Compose Watch](#docker-compose-watch)
+- [Development vs Production](#development-vs-production)
+- [Full Application Example](#full-application-example)
+
+---
+
+## Service Definitions
+
+### Build from Dockerfile
+
+```yaml
+services:
+  web:
+    build:
+      context: .
+      dockerfile: Dockerfile
+      target: production          # Multi-stage build target
+      args:
+        APP_VERSION: "1.2.3"      # Build-time args
+    image: myapp:latest           # Tag the built image
+    container_name: myapp-web     # Fixed container name
+    restart: unless-stopped
+```
+
+### Use Pre-Built Image
+
+```yaml
+services:
+  db:
+    image: postgres:16-alpine
+    restart: unless-stopped
+```
+
+### Resource Limits
+
+```yaml
+services:
+  worker:
+    image: myapp-worker:latest
+    deploy:
+      resources:
+        limits:
+          cpus: "2.0"
+          memory: 512M
+        reservations:
+          cpus: "0.5"
+          memory: 128M
+```
+
+---
+
+## Environment Variables
+
+### Inline
+
+```yaml
+services:
+  web:
+    environment:
+      NODE_ENV: production
+      DATABASE_URL: postgres://user:pass@db:5432/myapp
+      REDIS_URL: redis://cache:6379
+```
+
+### From File
+
+```yaml
+services:
+  web:
+    env_file:
+      - .env                     # Default variables
+      - .env.production          # Override for production
+```
+
+### Variable Interpolation
+
+```yaml
+# Uses host environment variables or .env file in project root
+services:
+  web:
+    image: myapp:${APP_VERSION:-latest}
+    environment:
+      DB_HOST: ${DB_HOST:?DB_HOST must be set}    # Fail if not set
+      LOG_LEVEL: ${LOG_LEVEL:-info}                # Default to "info"
+```
+
+### Precedence (highest to lowest)
+
+1. `docker compose run -e` or `docker compose exec -e`
+2. `environment:` in compose file
+3. `--env-file` CLI flag
+4. `env_file:` in compose file
+5. `.env` file in project directory
+6. Host environment variables
+
+---
+
+## Volume Patterns
+
+### Named Volumes (Persistent Data)
+
+```yaml
+services:
+  db:
+    image: postgres:16-alpine
+    volumes:
+      - db-data:/var/lib/postgresql/data    # Docker-managed volume
+
+volumes:
+  db-data:
+    driver: local
+```
+
+### Bind Mounts (Development)
+
+```yaml
+services:
+  web:
+    volumes:
+      - ./src:/app/src:cached          # Source code (cached for macOS perf)
+      - ./config:/app/config:ro        # Config files (read-only)
+```
+
+### tmpfs (In-Memory, Ephemeral)
+
+```yaml
+services:
+  web:
+    tmpfs:
+      - /tmp                            # Writable temp directory
+      - /app/cache:size=100M            # Capped in-memory cache
+    read_only: true                     # Read-only root filesystem
+```
+
+### Anonymous Volumes (Protect Container Paths)
+
+```yaml
+services:
+  web:
+    volumes:
+      - ./src:/app/src                  # Override source
+      - /app/node_modules              # But keep container's node_modules
+```
+
+---
+
+## Networking
+
+### Custom Networks
+
+```yaml
+services:
+  web:
+    networks:
+      - frontend
+      - backend
+
+  api:
+    networks:
+      - backend
+
+  db:
+    networks:
+      - backend                        # db is NOT accessible from frontend
+
+networks:
+  frontend:
+    driver: bridge
+  backend:
+    driver: bridge
+    internal: true                     # No external access
+```
+
+### Service Discovery
+
+Services on the same network can reach each other by service name:
+
+```yaml
+services:
+  web:
+    environment:
+      API_URL: http://api:3000         # "api" resolves to the api container
+      DB_HOST: db                      # "db" resolves to the db container
+
+  api:
+    # ...
+
+  db:
+    # ...
+```
+
+### Port Mapping
+
+```yaml
+services:
+  web:
+    ports:
+      - "8080:3000"                    # host:container
+      - "127.0.0.1:9090:9090"         # Bind to localhost only
+      - "3000"                         # Random host port -> container 3000
+```
+
+### Static IPs (When Needed)
+
+```yaml
+services:
+  dns:
+    networks:
+      backend:
+        ipv4_address: 172.20.0.10
+
+networks:
+  backend:
+    ipam:
+      config:
+        - subnet: 172.20.0.0/24
+```
+
+---
+
+## Health Checks
+
+### HTTP Health Check
+
+```yaml
+services:
+  web:
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
+      interval: 30s
+      timeout: 5s
+      retries: 3
+      start_period: 15s                # Grace period for startup
+```
+
+### TCP Health Check (No curl Available)
+
+```yaml
+services:
+  web:
+    healthcheck:
+      test: ["CMD-SHELL", "nc -z localhost 3000 || exit 1"]
+      interval: 15s
+      timeout: 3s
+      retries: 3
+```
+
+### Database Health Checks
+
+```yaml
+services:
+  postgres:
+    image: postgres:16-alpine
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U postgres"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+
+  mysql:
+    image: mysql:8
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+
+  redis:
+    image: redis:7-alpine
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 10s
+      timeout: 3s
+      retries: 5
+```
+
+---
+
+## Dependency Management
+
+### depends_on with Health Conditions
+
+```yaml
+services:
+  web:
+    depends_on:
+      db:
+        condition: service_healthy     # Wait for db to pass healthcheck
+      cache:
+        condition: service_healthy
+      migrations:
+        condition: service_completed_successfully  # Run-once service
+
+  migrations:
+    build: .
+    command: ["python", "manage.py", "migrate"]
+    depends_on:
+      db:
+        condition: service_healthy
+
+  db:
+    image: postgres:16-alpine
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U postgres"]
+      interval: 5s
+      timeout: 3s
+      retries: 10
+
+  cache:
+    image: redis:7-alpine
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 5s
+      timeout: 3s
+      retries: 5
+```
+
+**Important:** Without `condition: service_healthy`, `depends_on` only waits for the container to start, not for the service inside to be ready.
+
+---
+
+## Override Files
+
+Docker Compose automatically merges `docker-compose.yml` with `docker-compose.override.yml`.
+
+### Base: docker-compose.yml
+
+```yaml
+services:
+  web:
+    build: .
+    ports:
+      - "3000:3000"
+    environment:
+      NODE_ENV: production
+
+  db:
+    image: postgres:16-alpine
+    volumes:
+      - db-data:/var/lib/postgresql/data
+
+volumes:
+  db-data:
+```
+
+### Development Override: docker-compose.override.yml
+
+```yaml
+# Automatically merged with docker-compose.yml
+services:
+  web:
+    build:
+      target: development
+    volumes:
+      - ./src:/app/src                 # Live reload
+      - /app/node_modules
+    environment:
+      NODE_ENV: development
+      DEBUG: "true"
+    ports:
+      - "9229:9229"                    # Node debugger
+
+  db:
+    ports:
+      - "5432:5432"                    # Expose DB port for local tools
+    environment:
+      POSTGRES_PASSWORD: devpass
+```
+
+### Production Override: docker-compose.prod.yml
+
+```yaml
+services:
+  web:
+    build:
+      target: production
+    restart: always
+    deploy:
+      replicas: 2
+      resources:
+        limits:
+          memory: 512M
+
+  db:
+    restart: always
+    # No port exposure in production
+```
+
+### Using Override Files
+
+```bash
+# Development (auto-merges override.yml)
+docker compose up
+
+# Production (explicit file selection)
+docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
+
+# Testing
+docker compose -f docker-compose.yml -f docker-compose.test.yml run --rm test
+```
+
+---
+
+## Profiles for Optional Services
+
+Profiles let you define services that only start when explicitly requested.
+
+```yaml
+services:
+  web:
+    build: .
+    ports:
+      - "3000:3000"
+    # No profile = always starts
+
+  db:
+    image: postgres:16-alpine
+    # No profile = always starts
+
+  adminer:
+    image: adminer
+    ports:
+      - "8080:8080"
+    profiles:
+      - debug                         # Only starts with --profile debug
+
+  mailhog:
+    image: mailhog/mailhog
+    ports:
+      - "1025:1025"
+      - "8025:8025"
+    profiles:
+      - debug                         # Only starts with --profile debug
+
+  prometheus:
+    image: prom/prometheus
+    profiles:
+      - monitoring                     # Only starts with --profile monitoring
+
+  grafana:
+    image: grafana/grafana
+    profiles:
+      - monitoring
+```
+
+```bash
+# Start core services only
+docker compose up
+
+# Start with debug tools
+docker compose --profile debug up
+
+# Start with monitoring
+docker compose --profile monitoring up
+
+# Start with everything
+docker compose --profile debug --profile monitoring up
+```
+
+---
+
+## Docker Compose Watch
+
+Live reload for development without bind mounts (Compose 2.22+).
+
+```yaml
+services:
+  web:
+    build:
+      context: .
+      target: development
+    develop:
+      watch:
+        # Sync source files -> restart not needed (hot reload)
+        - action: sync
+          path: ./src
+          target: /app/src
+
+        # Rebuild when dependencies change
+        - action: rebuild
+          path: ./package.json
+
+        # Sync + restart when config changes
+        - action: sync+restart
+          path: ./config
+          target: /app/config
+
+        # Ignore patterns
+          ignore:
+            - "**/*.test.ts"
+            - "**/node_modules"
+```
+
+```bash
+# Start with file watching
+docker compose watch
+
+# Or alongside up
+docker compose up --watch
+```
+
+### Watch Actions
+
+| Action | Behavior | Use Case |
+|--------|----------|----------|
+| `sync` | Copy changed files into container | Source code with HMR/hot reload |
+| `rebuild` | Rebuild and recreate the container | Dependency changes (package.json) |
+| `sync+restart` | Copy files then restart container | Config files, non-HMR code |
+
+---
+
+## Development vs Production
+
+### Development Compose
+
+```yaml
+# docker-compose.yml (development-focused)
+services:
+  web:
+    build:
+      context: .
+      target: development
+    volumes:
+      - ./src:/app/src
+    ports:
+      - "3000:3000"
+      - "9229:9229"              # Debugger
+    environment:
+      NODE_ENV: development
+    command: ["npm", "run", "dev"]
+
+  db:
+    image: postgres:16-alpine
+    ports:
+      - "5432:5432"              # Exposed for local access
+    environment:
+      POSTGRES_PASSWORD: devpass
+      POSTGRES_DB: myapp_dev
+    volumes:
+      - db-data:/var/lib/postgresql/data
+      - ./db/seed.sql:/docker-entrypoint-initdb.d/seed.sql
+
+volumes:
+  db-data:
+```
+
+### Production Compose
+
+```yaml
+# docker-compose.prod.yml
+services:
+  web:
+    image: myregistry/myapp:${VERSION}    # Pre-built image
+    restart: always
+    read_only: true
+    tmpfs:
+      - /tmp
+    ports:
+      - "3000:3000"
+    environment:
+      NODE_ENV: production
+    deploy:
+      replicas: 2
+      resources:
+        limits:
+          memory: 512M
+          cpus: "1.0"
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
+      interval: 30s
+      retries: 3
+    logging:
+      driver: json-file
+      options:
+        max-size: "10m"
+        max-file: "3"
+
+  db:
+    image: postgres:16-alpine
+    restart: always
+    # NO port exposure
+    environment:
+      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
+    secrets:
+      - db_password
+    volumes:
+      - db-data:/var/lib/postgresql/data
+
+secrets:
+  db_password:
+    file: ./secrets/db_password.txt
+
+volumes:
+  db-data:
+```
+
+---
+
+## Full Application Example
+
+Web app + API + database + cache + worker + reverse proxy.
+
+```yaml
+services:
+  # ---- Reverse Proxy ----
+  nginx:
+    image: nginx:1.25-alpine
+    ports:
+      - "80:80"
+      - "443:443"
+    volumes:
+      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+      - ./nginx/certs:/etc/nginx/certs:ro
+    depends_on:
+      web:
+        condition: service_healthy
+    networks:
+      - frontend
+    restart: unless-stopped
+
+  # ---- Web Application ----
+  web:
+    build:
+      context: .
+      target: production
+    environment:
+      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/myapp
+      REDIS_URL: redis://cache:6379
+      API_URL: http://api:4000
+    depends_on:
+      db:
+        condition: service_healthy
+      cache:
+        condition: service_healthy
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
+      interval: 30s
+      timeout: 5s
+      retries: 3
+    networks:
+      - frontend
+      - backend
+    restart: unless-stopped
+
+  # ---- API Service ----
+  api:
+    build:
+      context: ./api
+      target: production
+    environment:
+      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/myapp
+      REDIS_URL: redis://cache:6379
+    depends_on:
+      db:
+        condition: service_healthy
+      cache:
+        condition: service_healthy
+    networks:
+      - backend
+    restart: unless-stopped
+
+  # ---- Background Worker ----
+  worker:
+    build:
+      context: .
+      target: production
+    command: ["node", "dist/worker.js"]
+    environment:
+      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/myapp
+      REDIS_URL: redis://cache:6379
+    depends_on:
+      db:
+        condition: service_healthy
+      cache:
+        condition: service_healthy
+    networks:
+      - backend
+    restart: unless-stopped
+    deploy:
+      replicas: 2
+      resources:
+        limits:
+          memory: 256M
+
+  # ---- Database ----
+  db:
+    image: postgres:16-alpine
+    environment:
+      POSTGRES_USER: app
+      POSTGRES_PASSWORD: ${DB_PASSWORD}
+      POSTGRES_DB: myapp
+    volumes:
+      - db-data:/var/lib/postgresql/data
+      - ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+    networks:
+      - backend
+    restart: unless-stopped
+
+  # ---- Cache ----
+  cache:
+    image: redis:7-alpine
+    command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
+    volumes:
+      - cache-data:/data
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 10s
+      timeout: 3s
+      retries: 5
+    networks:
+      - backend
+    restart: unless-stopped
+
+volumes:
+  db-data:
+  cache-data:
+
+networks:
+  frontend:
+    driver: bridge
+  backend:
+    driver: bridge
+    internal: true                     # Backend not accessible externally
+```

+ 433 - 0
skills/docker-ops/references/multi-stage-builds.md

@@ -0,0 +1,433 @@
+# Multi-Stage Build Patterns
+
+Language-specific multi-stage Dockerfile patterns for minimal, secure production images.
+
+## Table of Contents
+
+- [Go Multi-Stage Builds](#go-multi-stage-builds)
+- [Rust Multi-Stage Builds](#rust-multi-stage-builds)
+- [Node.js Multi-Stage Builds](#nodejs-multi-stage-builds)
+- [Python Multi-Stage Builds](#python-multi-stage-builds)
+- [Builder Pattern with Build Args](#builder-pattern-with-build-args)
+- [Cross-Compilation with Buildx](#cross-compilation-with-buildx)
+
+---
+
+## Go Multi-Stage Builds
+
+Go compiles to a static binary, making it ideal for scratch or distroless images.
+
+### Minimal Scratch Image (CGO Disabled)
+
+```dockerfile
+# ---- Build Stage ----
+FROM golang:1.22-alpine AS builder
+
+WORKDIR /build
+
+# Cache dependencies
+COPY go.mod go.sum ./
+RUN go mod download
+
+# Build static binary
+COPY . .
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
+    -ldflags="-s -w -X main.version=1.0.0" \
+    -o /app ./cmd/server
+
+# ---- Runtime Stage ----
+FROM scratch
+
+# Import CA certificates for HTTPS
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+
+# Import timezone data
+COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
+
+# Copy binary
+COPY --from=builder /app /app
+
+# Run as non-root (numeric UID since scratch has no /etc/passwd)
+USER 65534:65534
+
+EXPOSE 8080
+ENTRYPOINT ["/app"]
+```
+
+**Result:** ~10-15 MB image (vs ~800 MB with full golang image).
+
+### Distroless Alternative (With Debug Shell)
+
+```dockerfile
+FROM golang:1.22 AS builder
+WORKDIR /build
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server
+
+# distroless/static includes CA certs and tzdata
+FROM gcr.io/distroless/static:nonroot
+COPY --from=builder /app /app
+USER nonroot:nonroot
+EXPOSE 8080
+ENTRYPOINT ["/app"]
+```
+
+**When to use distroless over scratch:**
+- Need CA certificates without manually copying them
+- Want a non-root user without numeric UID hacks
+- Need debug variant (`gcr.io/distroless/static:debug`) for troubleshooting
+
+### Go with CGO Enabled
+
+```dockerfile
+FROM golang:1.22 AS builder
+WORKDIR /build
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+RUN go build -o /app ./cmd/server
+
+# Need glibc for CGO
+FROM gcr.io/distroless/base:nonroot
+COPY --from=builder /app /app
+USER nonroot:nonroot
+ENTRYPOINT ["/app"]
+```
+
+---
+
+## Rust Multi-Stage Builds
+
+### With cargo-chef (Optimized Layer Caching)
+
+cargo-chef separates dependency compilation from source compilation, enabling Docker layer caching for Rust dependencies.
+
+```dockerfile
+# ---- Chef Stage: Prepare dependency recipe ----
+FROM rust:1.77-slim AS chef
+RUN cargo install cargo-chef
+WORKDIR /build
+
+# ---- Planner Stage: Analyze dependencies ----
+FROM chef AS planner
+COPY . .
+RUN cargo chef prepare --recipe-path recipe.json
+
+# ---- Builder Stage: Build dependencies then source ----
+FROM chef AS builder
+
+# Build dependencies (cached unless Cargo.toml/Cargo.lock change)
+COPY --from=planner /build/recipe.json recipe.json
+RUN cargo chef cook --release --recipe-path recipe.json
+
+# Build application
+COPY . .
+RUN cargo build --release --bin server
+
+# ---- Runtime Stage ----
+FROM gcr.io/distroless/cc:nonroot
+
+COPY --from=builder /build/target/release/server /server
+
+USER nonroot:nonroot
+EXPOSE 8080
+ENTRYPOINT ["/server"]
+```
+
+**Why cargo-chef?** Without it, changing any source file recompiles all dependencies (~5-20 min). With cargo-chef, dependency compilation is cached unless `Cargo.toml` or `Cargo.lock` changes.
+
+### Static Musl Build (Scratch Target)
+
+```dockerfile
+FROM rust:1.77 AS builder
+
+# Add musl target for static linking
+RUN rustup target add x86_64-unknown-linux-musl
+RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+COPY Cargo.toml Cargo.lock ./
+COPY src/ ./src/
+
+RUN cargo build --release --target x86_64-unknown-linux-musl
+
+# Fully static binary -> scratch is fine
+FROM scratch
+COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/server /server
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+
+USER 65534:65534
+ENTRYPOINT ["/server"]
+```
+
+---
+
+## Node.js Multi-Stage Builds
+
+### Production Node.js App
+
+```dockerfile
+# ---- Build Stage ----
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+# Install dependencies (cached unless package files change)
+COPY package.json package-lock.json ./
+RUN npm ci
+
+# Build application (TypeScript, bundler, etc.)
+COPY tsconfig.json ./
+COPY src/ ./src/
+RUN npm run build
+
+# ---- Production Dependencies Stage ----
+FROM node:20-alpine AS deps
+
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm ci --omit=dev
+
+# ---- Runtime Stage ----
+FROM node:20-slim
+
+# Install tini for proper signal handling
+RUN apt-get update && apt-get install -y --no-install-recommends tini \
+    && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Create non-root user
+RUN groupadd -r appuser && useradd -r -g appuser appuser
+
+# Copy production dependencies and built code
+COPY --from=deps --chown=appuser:appuser /app/node_modules ./node_modules
+COPY --from=builder --chown=appuser:appuser /app/dist ./dist
+COPY --chown=appuser:appuser package.json ./
+
+USER appuser
+
+ENV NODE_ENV=production
+EXPOSE 3000
+
+ENTRYPOINT ["tini", "--"]
+CMD ["node", "dist/server.js"]
+```
+
+**Key decisions:**
+- `npm ci` over `npm install` for reproducible builds
+- `--omit=dev` to exclude devDependencies from runtime image
+- `tini` to handle PID 1 responsibilities (signal forwarding, zombie reaping)
+- `node:20-slim` over alpine to avoid musl compatibility issues with native modules
+
+### Next.js Standalone Build
+
+```dockerfile
+FROM node:20-alpine AS builder
+WORKDIR /app
+
+COPY package.json package-lock.json ./
+RUN npm ci
+
+COPY . .
+
+# next.config.js must have: output: 'standalone'
+RUN npm run build
+
+FROM node:20-alpine
+WORKDIR /app
+
+RUN addgroup -S appuser && adduser -S -G appuser appuser
+
+# Copy only the standalone output
+COPY --from=builder --chown=appuser:appuser /app/.next/standalone ./
+COPY --from=builder --chown=appuser:appuser /app/.next/static ./.next/static
+COPY --from=builder --chown=appuser:appuser /app/public ./public
+
+USER appuser
+
+ENV NODE_ENV=production
+ENV HOSTNAME=0.0.0.0
+EXPOSE 3000
+
+CMD ["node", "server.js"]
+```
+
+---
+
+## Python Multi-Stage Builds
+
+### With uv (Fast Dependency Management)
+
+```dockerfile
+# ---- Build Stage ----
+FROM python:3.12-slim AS builder
+
+# Install uv
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
+
+WORKDIR /app
+
+# Install dependencies into a virtual env (cached layer)
+COPY pyproject.toml uv.lock ./
+RUN uv sync --frozen --no-dev --no-install-project
+
+# Copy application source
+COPY src/ ./src/
+COPY pyproject.toml ./
+RUN uv sync --frozen --no-dev
+
+# ---- Runtime Stage ----
+FROM python:3.12-slim
+
+WORKDIR /app
+
+# Create non-root user
+RUN groupadd -r appuser && useradd -r -g appuser -d /app appuser
+
+# Copy virtual environment from builder
+COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
+COPY --from=builder --chown=appuser:appuser /app/src ./src
+
+# Ensure venv binaries are on PATH
+ENV PATH="/app/.venv/bin:$PATH"
+ENV PYTHONUNBUFFERED=1
+ENV PYTHONDONTWRITEBYTECODE=1
+
+USER appuser
+
+EXPOSE 8000
+CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
+```
+
+### With pip (Traditional)
+
+```dockerfile
+FROM python:3.12-slim AS builder
+
+WORKDIR /app
+
+# Create virtual environment
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+# Install dependencies
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+FROM python:3.12-slim
+
+WORKDIR /app
+
+RUN groupadd -r appuser && useradd -r -g appuser -d /app appuser
+
+# Copy only the virtual environment
+COPY --from=builder --chown=appuser:appuser /opt/venv /opt/venv
+COPY --chown=appuser:appuser src/ ./src/
+
+ENV PATH="/opt/venv/bin:$PATH"
+ENV PYTHONUNBUFFERED=1
+
+USER appuser
+
+EXPOSE 8000
+CMD ["gunicorn", "src.main:app", "-w", "4", "-b", "0.0.0.0:8000"]
+```
+
+---
+
+## Builder Pattern with Build Args
+
+Use `ARG` to parameterize builds without baking values into the final image.
+
+```dockerfile
+# Build-time arguments
+ARG GO_VERSION=1.22
+ARG APP_VERSION=dev
+
+FROM golang:${GO_VERSION}-alpine AS builder
+
+ARG APP_VERSION
+WORKDIR /build
+COPY . .
+RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${APP_VERSION}" \
+    -o /app ./cmd/server
+
+FROM scratch
+COPY --from=builder /app /app
+ENTRYPOINT ["/app"]
+```
+
+```bash
+# Pass args at build time
+docker build --build-arg APP_VERSION=1.2.3 --build-arg GO_VERSION=1.23 -t myapp .
+```
+
+**Important:** `ARG` values before `FROM` are only available in `FROM` lines. Redeclare `ARG` after `FROM` to use in `RUN` commands.
+
+---
+
+## Cross-Compilation with Buildx
+
+Build images for multiple platforms (amd64, arm64) from a single machine.
+
+### Setup
+
+```bash
+# Create a builder instance with multi-platform support
+docker buildx create --name multiarch --driver docker-container --use
+docker buildx inspect --bootstrap
+```
+
+### Cross-Platform Dockerfile
+
+```dockerfile
+# BUILDPLATFORM = host platform (where build runs)
+# TARGETPLATFORM = target platform (where image runs)
+FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
+
+ARG TARGETOS
+ARG TARGETARCH
+
+WORKDIR /build
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
+    go build -ldflags="-s -w" -o /app ./cmd/server
+
+FROM --platform=$TARGETPLATFORM gcr.io/distroless/static:nonroot
+COPY --from=builder /app /app
+USER nonroot:nonroot
+ENTRYPOINT ["/app"]
+```
+
+### Build and Push
+
+```bash
+# Build for multiple platforms and push to registry
+docker buildx build \
+    --platform linux/amd64,linux/arm64 \
+    -t myregistry/myapp:1.0 \
+    --push .
+
+# Build for local use (single platform)
+docker buildx build \
+    --platform linux/arm64 \
+    -t myapp:1.0 \
+    --load .
+```
+
+### Platform Variables Reference
+
+| Variable | Example Value | Available In |
+|----------|--------------|--------------|
+| `BUILDPLATFORM` | `linux/amd64` | `FROM --platform=` |
+| `TARGETPLATFORM` | `linux/arm64` | `FROM --platform=` |
+| `BUILDOS` | `linux` | `RUN` (after ARG) |
+| `BUILDARCH` | `amd64` | `RUN` (after ARG) |
+| `TARGETOS` | `linux` | `RUN` (after ARG) |
+| `TARGETARCH` | `arm64` | `RUN` (after ARG) |

+ 658 - 0
skills/docker-ops/references/optimization.md

@@ -0,0 +1,658 @@
+# Docker Optimization
+
+Image size reduction, BuildKit features, security scanning, and debugging techniques.
+
+## Table of Contents
+
+- [Base Image Selection](#base-image-selection)
+- [Layer Ordering Strategy](#layer-ordering-strategy)
+- [BuildKit Features](#buildkit-features)
+- [.dockerignore Patterns](#dockerignore-patterns)
+- [Multi-Platform Builds](#multi-platform-builds)
+- [Security Scanning](#security-scanning)
+- [Health Checks and Graceful Shutdown](#health-checks-and-graceful-shutdown)
+- [Container Resource Limits](#container-resource-limits)
+- [Logging Best Practices](#logging-best-practices)
+- [Debug Techniques](#debug-techniques)
+
+---
+
+## Base Image Selection
+
+| Image Type | Size | Use Case | Security |
+|------------|------|----------|----------|
+| `scratch` | 0 MB | Go/Rust static binaries | Minimal attack surface |
+| `distroless/static` | ~2 MB | Static binaries + CA certs | No shell, no package manager |
+| `distroless/base` | ~20 MB | Dynamic binaries (CGO) | No shell |
+| `distroless/cc` | ~25 MB | Rust/C++ with libgcc | No shell |
+| `alpine` | ~7 MB | Small general-purpose | Musl libc (compatibility issues) |
+| `*-slim` | ~80 MB | Python, Node, Java | Glibc, minimal packages |
+| `*:latest` | 200-900 MB | Development only | Full package set, large surface |
+
+### Decision Guide
+
+```
+Need shell for debugging?
+├── No  → distroless or scratch
+└── Yes → alpine or slim
+
+Using Go/Rust with static linking?
+├── Yes → scratch (smallest) or distroless/static (has CA certs)
+└── No  → distroless/base or slim
+
+Using Python/Node?
+├── Need native C extensions? → slim (glibc)
+└── Pure Python/JS?          → slim (still recommended) or alpine
+```
+
+### Size Comparison (Real-World Go App)
+
+```
+golang:1.22          ~820 MB
+golang:1.22-alpine   ~260 MB
+distroless/static     ~12 MB   (with app binary)
+scratch                ~8 MB   (with app binary)
+```
+
+---
+
+## Layer Ordering Strategy
+
+### Principle: Least-Changed First
+
+```dockerfile
+# Layer 1: Base (changes ~yearly)
+FROM python:3.12-slim
+
+# Layer 2: System packages (changes ~monthly)
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    libpq-dev curl \
+    && rm -rf /var/lib/apt/lists/*
+
+# Layer 3: Dependencies (changes ~weekly)
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Layer 4: Application code (changes ~daily)
+COPY src/ ./src/
+
+# Layer 5: Runtime config (changes ~daily)
+CMD ["python", "-m", "app"]
+```
+
+### Anti-Pattern: Cache-Busting Copy
+
+```dockerfile
+# BAD: Any source change invalidates npm install cache
+COPY . .
+RUN npm install
+RUN npm run build
+
+# GOOD: Install deps first, then copy source
+COPY package.json package-lock.json ./
+RUN npm ci
+COPY . .
+RUN npm run build
+```
+
+### Combining RUN Commands
+
+```dockerfile
+# BAD: 3 layers, apt cache persists in first layer
+RUN apt-get update
+RUN apt-get install -y curl
+RUN rm -rf /var/lib/apt/lists/*
+
+# GOOD: 1 layer, no residual cache
+RUN apt-get update \
+    && apt-get install -y --no-install-recommends curl \
+    && rm -rf /var/lib/apt/lists/*
+```
+
+---
+
+## BuildKit Features
+
+Enable BuildKit (default in Docker 23.0+):
+
+```bash
+export DOCKER_BUILDKIT=1
+# Or use docker buildx build
+```
+
+### Cache Mounts
+
+Persist package manager caches across builds without bloating the image.
+
+```dockerfile
+# apt cache mount
+RUN --mount=type=cache,target=/var/cache/apt \
+    --mount=type=cache,target=/var/lib/apt \
+    apt-get update && apt-get install -y libpq-dev
+
+# pip cache mount
+RUN --mount=type=cache,target=/root/.cache/pip \
+    pip install -r requirements.txt
+
+# npm cache mount
+RUN --mount=type=cache,target=/root/.npm \
+    npm ci
+
+# Go module cache mount
+RUN --mount=type=cache,target=/go/pkg/mod \
+    --mount=type=cache,target=/root/.cache/go-build \
+    go build -o /app .
+
+# Cargo cache mount (Rust)
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+    --mount=type=cache,target=/app/target \
+    cargo build --release
+```
+
+### Secret Mounts
+
+Pass secrets during build without persisting in image layers.
+
+```dockerfile
+# Dockerfile
+RUN --mount=type=secret,id=github_token \
+    GITHUB_TOKEN=$(cat /run/secrets/github_token) \
+    npm install --registry https://npm.pkg.github.com
+
+RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
+    pip install -r requirements.txt
+```
+
+```bash
+# Build command
+docker build --secret id=github_token,src=./github_token.txt -t myapp .
+docker build --secret id=pip_conf,src=./pip.conf -t myapp .
+```
+
+### SSH Mounts
+
+Forward SSH agent for private repository access during build.
+
+```dockerfile
+RUN --mount=type=ssh \
+    git clone git@github.com:private/repo.git /app/deps
+```
+
+```bash
+docker build --ssh default -t myapp .
+```
+
+### Heredoc Syntax (BuildKit)
+
+Multi-line scripts and inline files without escaping.
+
+```dockerfile
+# syntax=docker/dockerfile:1
+
+# Multi-line script
+RUN <<EOF
+apt-get update
+apt-get install -y curl jq
+rm -rf /var/lib/apt/lists/*
+EOF
+
+# Inline file creation
+COPY <<EOF /app/config.json
+{
+  "port": 3000,
+  "log_level": "info"
+}
+EOF
+
+# Inline script with different interpreter
+RUN <<'PYTHON'
+#!/usr/bin/env python3
+import json
+config = {"version": "1.0"}
+with open("/app/version.json", "w") as f:
+    json.dump(config, f)
+PYTHON
+```
+
+---
+
+## .dockerignore Patterns
+
+### Comprehensive .dockerignore
+
+```dockerignore
+# Version control
+.git
+.gitignore
+.gitmodules
+
+# CI/CD
+.github
+.gitlab-ci.yml
+.circleci
+Jenkinsfile
+
+# Docker (prevent recursive context)
+Dockerfile*
+docker-compose*
+.dockerignore
+
+# Dependencies (rebuilt in container)
+node_modules
+.venv
+__pycache__
+*.pyc
+vendor/
+target/
+
+# Build artifacts
+dist/
+build/
+*.egg-info
+*.whl
+
+# IDE
+.vscode
+.idea
+*.swp
+*.swo
+.DS_Store
+Thumbs.db
+
+# Environment and secrets
+.env
+.env.*
+*.pem
+*.key
+*.crt
+secrets/
+
+# Documentation (unless needed at runtime)
+docs/
+*.md
+LICENSE
+CHANGELOG
+
+# Tests (unless needed at runtime)
+tests/
+test/
+*_test.go
+*.test.js
+*.spec.ts
+coverage/
+.nyc_output
+```
+
+### Measuring Build Context
+
+```bash
+# See what Docker sends to the daemon
+docker build --progress=plain . 2>&1 | grep "transferring context"
+
+# List what would be sent (approximation)
+tar -czf - --exclude-from=.dockerignore . | wc -c
+```
+
+---
+
+## Multi-Platform Builds
+
+### Setup Buildx
+
+```bash
+# Create builder with multi-platform support
+docker buildx create --name multiarch --driver docker-container --use
+docker buildx inspect --bootstrap
+
+# List available platforms
+docker buildx ls
+```
+
+### Build for Multiple Platforms
+
+```bash
+# Build and push multi-arch manifest
+docker buildx build \
+    --platform linux/amd64,linux/arm64 \
+    -t myregistry/myapp:1.0 \
+    --push .
+
+# Build for local use (single platform only with --load)
+docker buildx build \
+    --platform linux/arm64 \
+    -t myapp:local \
+    --load .
+
+# Build and export to tarball
+docker buildx build \
+    --platform linux/amd64,linux/arm64 \
+    -t myapp:1.0 \
+    --output type=oci,dest=myapp.tar .
+```
+
+### QEMU for Cross-Architecture
+
+```bash
+# Install QEMU user-mode emulation
+docker run --privileged --rm tonistiigi/binfmt --install all
+
+# Verify
+docker buildx ls
+# Should show: linux/amd64, linux/arm64, linux/arm/v7, etc.
+```
+
+---
+
+## Security Scanning
+
+### Trivy (Recommended)
+
+```bash
+# Scan image for vulnerabilities
+trivy image myapp:latest
+
+# Scan with severity filter
+trivy image --severity HIGH,CRITICAL myapp:latest
+
+# Scan Dockerfile for misconfigurations
+trivy config Dockerfile
+
+# Scan and fail CI if critical vulns found
+trivy image --exit-code 1 --severity CRITICAL myapp:latest
+
+# JSON output for processing
+trivy image --format json --output results.json myapp:latest
+```
+
+### Grype
+
+```bash
+# Scan image
+grype myapp:latest
+
+# Only critical and high
+grype myapp:latest --only-fixed --fail-on high
+```
+
+### Docker Scout (Built-in)
+
+```bash
+# Quick vulnerability overview
+docker scout quickview myapp:latest
+
+# Detailed CVE list
+docker scout cves myapp:latest
+
+# Compare two images
+docker scout compare myapp:1.0 myapp:1.1
+
+# Recommendations
+docker scout recommendations myapp:latest
+```
+
+### CI Integration Example
+
+```yaml
+# GitHub Actions
+- name: Scan image
+  uses: aquasecurity/trivy-action@master
+  with:
+    image-ref: myapp:${{ github.sha }}
+    severity: HIGH,CRITICAL
+    exit-code: 1
+```
+
+---
+
+## Health Checks and Graceful Shutdown
+
+### Dockerfile HEALTHCHECK
+
+```dockerfile
+# HTTP check
+HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
+    CMD curl -f http://localhost:3000/health || exit 1
+
+# TCP check (no curl needed)
+HEALTHCHECK --interval=15s --timeout=3s --retries=3 \
+    CMD nc -z localhost 3000 || exit 1
+
+# Custom script
+COPY healthcheck.sh /usr/local/bin/
+HEALTHCHECK --interval=30s CMD ["healthcheck.sh"]
+```
+
+### Graceful Shutdown (SIGTERM Handling)
+
+```dockerfile
+# Set stop signal (default is SIGTERM)
+STOPSIGNAL SIGTERM
+
+# Use tini for proper signal forwarding
+RUN apt-get update && apt-get install -y --no-install-recommends tini \
+    && rm -rf /var/lib/apt/lists/*
+ENTRYPOINT ["tini", "--"]
+CMD ["node", "server.js"]
+```
+
+**Node.js signal handler:**
+
+```javascript
+process.on('SIGTERM', () => {
+  console.log('SIGTERM received, shutting down gracefully');
+  server.close(() => {
+    console.log('Server closed');
+    process.exit(0);
+  });
+  // Force exit after timeout
+  setTimeout(() => process.exit(1), 10000);
+});
+```
+
+**Python signal handler:**
+
+```python
+import signal, sys
+
+def shutdown(signum, frame):
+    print("Shutting down gracefully...")
+    # Close connections, flush buffers
+    sys.exit(0)
+
+signal.signal(signal.SIGTERM, shutdown)
+```
+
+### Stop Timeout
+
+```bash
+# Give container 30s to shut down before SIGKILL
+docker stop --time 30 myapp
+
+# In compose
+services:
+  web:
+    stop_grace_period: 30s
+```
+
+---
+
+## Container Resource Limits
+
+### Docker Run
+
+```bash
+# Memory limit (OOM-killed if exceeded)
+docker run --memory=512m --memory-swap=512m myapp
+
+# CPU limit
+docker run --cpus=1.5 myapp                    # 1.5 cores
+docker run --cpu-shares=512 myapp              # Relative weight
+
+# Combined
+docker run --memory=512m --cpus=2 --pids-limit=100 myapp
+```
+
+### Docker Compose
+
+```yaml
+services:
+  web:
+    deploy:
+      resources:
+        limits:
+          cpus: "2.0"
+          memory: 512M
+        reservations:
+          cpus: "0.5"
+          memory: 128M
+```
+
+### Filesystem Limits
+
+```bash
+# Read-only root filesystem
+docker run --read-only --tmpfs /tmp:size=100M myapp
+
+# Storage driver limit (overlay2)
+docker run --storage-opt size=10G myapp
+```
+
+---
+
+## Logging Best Practices
+
+### Application: Log to stdout/stderr
+
+```dockerfile
+# Redirect app logs to stdout (nginx example)
+RUN ln -sf /dev/stdout /var/log/nginx/access.log \
+    && ln -sf /dev/stderr /var/log/nginx/error.log
+```
+
+### Configure Log Driver
+
+```yaml
+# docker-compose.yml
+services:
+  web:
+    logging:
+      driver: json-file
+      options:
+        max-size: "10m"        # Rotate at 10 MB
+        max-file: "3"          # Keep 3 rotated files
+        compress: "true"
+```
+
+### Structured JSON Logging
+
+```bash
+# View JSON logs
+docker logs myapp --tail 50 | jq .
+
+# Filter by level
+docker logs myapp 2>&1 | jq 'select(.level == "error")'
+```
+
+---
+
+## Debug Techniques
+
+### Inspect Running Containers
+
+```bash
+# Shell into running container
+docker exec -it myapp /bin/sh
+docker exec -it myapp /bin/bash
+
+# Run command without shell
+docker exec myapp cat /app/config.json
+
+# View logs
+docker logs myapp                    # All logs
+docker logs -f myapp                 # Follow
+docker logs --since 5m myapp         # Last 5 minutes
+docker logs --tail 100 myapp         # Last 100 lines
+
+# Container stats
+docker stats myapp                   # Live CPU/mem/net/disk
+docker top myapp                     # Running processes
+
+# Full container details
+docker inspect myapp | jq '.[0].State'
+docker inspect myapp | jq '.[0].NetworkSettings.Networks'
+```
+
+### Debug a Failed Build
+
+```bash
+# Build with progress output
+docker build --progress=plain -t myapp .
+
+# Target a specific stage
+docker build --target builder -t myapp:debug .
+docker run -it myapp:debug /bin/sh
+
+# Use build output for debugging
+docker build --progress=plain . 2>&1 | tee build.log
+```
+
+### Ephemeral Debug Containers
+
+```bash
+# Attach a debug container to a running container's network
+docker run -it --rm \
+    --network container:myapp \
+    nicolaka/netshoot \
+    curl http://localhost:3000/health
+
+# Debug with full tools
+docker run -it --rm \
+    --pid container:myapp \
+    --network container:myapp \
+    nicolaka/netshoot \
+    bash
+```
+
+### Image Inspection
+
+```bash
+# Layer history and sizes
+docker history myapp:latest
+
+# Detailed layer analysis with dive
+dive myapp:latest
+
+# Export filesystem for inspection
+docker save myapp:latest | tar -xf - -C /tmp/image-layers
+
+# Check image config
+docker inspect myapp:latest | jq '.[0].Config'
+
+# Compare image sizes
+docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -h
+```
+
+### Compose Debugging
+
+```bash
+# View merged config
+docker compose config
+
+# View logs for all services
+docker compose logs -f
+
+# View logs for specific service
+docker compose logs -f web
+
+# Restart single service
+docker compose restart web
+
+# Rebuild and restart
+docker compose up -d --build web
+
+# Run one-off command
+docker compose run --rm web npm test
+
+# View service status
+docker compose ps
+```

+ 0 - 0
skills/docker-ops/scripts/.gitkeep


+ 307 - 0
skills/go-ops/SKILL.md

@@ -0,0 +1,307 @@
+---
+name: go-ops
+description: "Go development patterns, concurrency, error handling, testing, and project structure. Use for: golang, go, goroutine, channel, context, errgroup, go test, go mod, go build, interface, generics, table-driven tests, worker pool, sync.Mutex, sync.WaitGroup, pprof, go vet, golangci-lint, go workspace, functional options, middleware, http handler."
+allowed-tools: "Read Write Bash"
+related-skills: [docker-ops, ci-cd-ops, api-design-ops, testing-ops]
+---
+
+# Go Operations
+
+Comprehensive Go skill covering idiomatic patterns, concurrency, and production practices.
+
+## Module Quick Start
+
+```bash
+# New module
+go mod init github.com/user/project
+
+# Add dependency
+go get github.com/lib/pq@latest
+
+# Tidy (remove unused, add missing)
+go mod tidy
+
+# Vendor dependencies
+go mod vendor
+
+# Workspace (multi-module)
+go work init ./api ./shared
+go work use ./cli
+```
+
+## Error Handling Decision Tree
+
+```
+What kind of error?
+│
+├─ Known, expected condition (e.g. "not found")
+│  └─ Sentinel error: var ErrNotFound = errors.New("not found")
+│     └─ Caller checks: errors.Is(err, ErrNotFound)
+│
+├─ Need to carry structured data (status code, field name)
+│  └─ Custom error type: type ValidationError struct { Field, Message string }
+│     └─ Implement Error() string
+│     └─ Caller checks: errors.As(err, &validErr)
+│
+├─ Adding context to an existing error
+│  └─ Wrap: fmt.Errorf("load config: %w", err)
+│     └─ Preserves original for Is/As checks
+│
+├─ Truly unrecoverable (corrupted state, programmer bug)
+│  └─ panic("invariant violated: ...")
+│     └─ Almost never in library code
+│
+└─ Multiple errors from concurrent work
+   └─ errors.Join(err1, err2) or multierr package
+```
+
+### Error Wrapping Convention
+
+```go
+// Add context at each layer, don't repeat the function name
+func LoadUser(id int) (*User, error) {
+    row, err := db.Query("SELECT ...", id)
+    if err != nil {
+        return nil, fmt.Errorf("load user %d: %w", id, err)
+    }
+    // ...
+}
+```
+
+## Concurrency Decision Tree
+
+```
+What's the concurrency pattern?
+│
+├─ Run N independent tasks, collect results
+│  └─ errgroup.Group (cancels on first error)
+│
+├─ Fire-and-forget background work
+│  └─ go func() with context for cancellation
+│     └─ ALWAYS handle the error or log it
+│
+├─ Producer/consumer pipeline
+│  └─ Channels (buffered for throughput)
+│     └─ Close channel when producer is done
+│
+├─ Rate-limited concurrent work
+│  └─ Semaphore: make(chan struct{}, maxConcurrency)
+│
+├─ Shared mutable state
+│  └─ sync.Mutex or sync.RWMutex
+│     └─ Prefer channels if the state is simple
+│
+├─ One-time initialization
+│  └─ sync.Once
+│
+└─ Wait for N goroutines to finish (no error collection)
+   └─ sync.WaitGroup
+```
+
+### errgroup Quick Start
+
+```go
+import "golang.org/x/sync/errgroup"
+
+g, ctx := errgroup.WithContext(ctx)
+g.SetLimit(10) // max 10 concurrent goroutines
+
+for _, url := range urls {
+    g.Go(func() error {
+        return fetch(ctx, url)
+    })
+}
+
+if err := g.Wait(); err != nil {
+    return fmt.Errorf("fetch urls: %w", err)
+}
+```
+
+**Deep dive**: Load `./references/concurrency.md` for worker pools, fan-out/fan-in, pipeline patterns, context best practices.
+
+## Interface Design
+
+```
+Accept interfaces, return structs.
+```
+
+```go
+// Good: function accepts interface
+func Process(r io.Reader) error { ... }
+
+// Good: return concrete type
+func NewServer(cfg Config) *Server { ... }
+
+// Bad: returning interface (hides implementation, prevents extension)
+func NewServer(cfg Config) ServerInterface { ... }
+```
+
+### Common Stdlib Interfaces
+
+| Interface | Methods | Use For |
+|-----------|---------|---------|
+| `io.Reader` | `Read([]byte) (int, error)` | Any byte source |
+| `io.Writer` | `Write([]byte) (int, error)` | Any byte sink |
+| `io.Closer` | `Close() error` | Resource cleanup |
+| `fmt.Stringer` | `String() string` | String representation |
+| `error` | `Error() string` | Error values |
+| `sort.Interface` | `Len, Less, Swap` | Custom sorting |
+| `http.Handler` | `ServeHTTP(w, r)` | HTTP handlers |
+| `encoding.BinaryMarshaler` | `MarshalBinary() ([]byte, error)` | Binary encoding |
+
+### Functional Options Pattern
+
+```go
+type Option func(*Server)
+
+func WithPort(port int) Option {
+    return func(s *Server) { s.port = port }
+}
+
+func WithTimeout(d time.Duration) Option {
+    return func(s *Server) { s.timeout = d }
+}
+
+func NewServer(opts ...Option) *Server {
+    s := &Server{port: 8080, timeout: 30 * time.Second} // defaults
+    for _, opt := range opts {
+        opt(s)
+    }
+    return s
+}
+
+// Usage
+srv := NewServer(WithPort(9090), WithTimeout(5*time.Second))
+```
+
+**Deep dive**: Load `./references/interfaces-generics.md` for generics, type constraints, embedding, type assertions.
+
+## Testing Quick Reference
+
+```go
+// Table-driven test
+func TestAdd(t *testing.T) {
+    tests := []struct {
+        name     string
+        a, b     int
+        expected int
+    }{
+        {"positive", 1, 2, 3},
+        {"zero", 0, 0, 0},
+        {"negative", -1, -2, -3},
+    }
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            got := Add(tt.a, tt.b)
+            if got != tt.expected {
+                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
+            }
+        })
+    }
+}
+```
+
+```bash
+# Run tests
+go test ./...
+
+# With coverage
+go test -cover -coverprofile=coverage.out ./...
+go tool cover -html=coverage.out
+
+# Run specific test
+go test -run TestAdd ./pkg/math/
+
+# Benchmarks
+go test -bench=. -benchmem ./...
+
+# Race detector
+go test -race ./...
+
+# Fuzz testing
+go test -fuzz=FuzzParse ./...
+```
+
+**Deep dive**: Load `./references/testing.md` for mocking with interfaces, httptest, testcontainers, golden files.
+
+## Common Gotchas
+
+| Gotcha | Why | Fix |
+|--------|-----|-----|
+| Nil slice vs empty slice | `var s []int` is nil, `s := []int{}` is empty. `json.Marshal` gives `null` vs `[]` | Use `make([]int, 0)` or `[]int{}` if JSON matters |
+| Goroutine leak | Goroutine blocked on channel with no reader/writer | Use `context.WithCancel`, always provide exit path |
+| Defer in loop | Deferred calls don't run until function returns | Wrap loop body in a closure or use explicit cleanup |
+| Interface nil pitfall | `(*MyType)(nil)` assigned to `error` interface is not `== nil` | Return `nil` explicitly, not a nil typed pointer |
+| Range variable capture | Loop var reused (pre-Go 1.22) | Use `go func(v T) { ... }(v)` or upgrade to Go 1.22+ |
+| String concatenation in loop | O(n^2) allocation | Use `strings.Builder` |
+| `sync.WaitGroup` Add after Go | Race condition | Call `wg.Add(1)` before `go func()` |
+| Unbuffered channel deadlock | Send/receive must happen concurrently | Use buffered channel or separate goroutines |
+| `map` not safe for concurrent use | Race condition, may crash | Use `sync.Mutex` or `sync.Map` |
+
+## Project Structure
+
+```
+project/
+├── cmd/
+│   ├── api/main.go           # Entry points
+│   └── worker/main.go
+├── internal/                  # Private packages
+│   ├── handler/
+│   ├── service/
+│   └── repository/
+├── pkg/                       # Public packages (optional)
+├── go.mod
+├── go.sum
+├── Makefile                   # or justfile
+└── .golangci.yml
+```
+
+**Deep dive**: Load `./references/project-structure.md` for workspace mode, build tags, ldflags, linting config.
+
+## Performance Quick Reference
+
+```bash
+# CPU profile
+go test -cpuprofile=cpu.prof -bench=. ./...
+go tool pprof cpu.prof
+
+# Memory profile
+go test -memprofile=mem.prof -bench=. ./...
+go tool pprof -alloc_space mem.prof
+
+# Trace
+go test -trace=trace.out ./...
+go tool trace trace.out
+
+# Escape analysis
+go build -gcflags='-m' ./...
+```
+
+| Optimization | When | Pattern |
+|-------------|------|---------|
+| Pre-allocate slices | Known size | `make([]T, 0, n)` |
+| `strings.Builder` | String concatenation | `var b strings.Builder` |
+| `sync.Pool` | Frequent alloc/free of same type | `pool.Get()` / `pool.Put()` |
+| Struct field alignment | Memory-sensitive | Group fields by size (largest first) |
+| Buffer reuse | I/O-heavy | `bufio.NewReaderSize(r, 64*1024)` |
+
+**Deep dive**: Load `./references/performance.md` for pprof walkthrough, benchmarking patterns, escape analysis.
+
+## Reference Files
+
+Load these for deep-dive topics. Each is self-contained.
+
+| Reference | When to Load |
+|-----------|-------------|
+| `./references/concurrency.md` | Goroutines, channels, context, sync primitives, worker pools, pipelines |
+| `./references/error-handling.md` | Error wrapping, sentinel errors, custom types, multi-error, panic/recover |
+| `./references/testing.md` | Table tests, mocking, httptest, benchmarks, fuzz, testcontainers, golden files |
+| `./references/interfaces-generics.md` | Interface design, embedding, type assertions, generics, type constraints |
+| `./references/project-structure.md` | Standard layout, go.mod, workspaces, build tags, ldflags, golangci-lint |
+| `./references/performance.md` | pprof, trace, benchmarks, escape analysis, sync.Pool, struct alignment |
+
+## See Also
+
+- `docker-ops` - Multi-stage builds for Go binaries (scratch/distroless)
+- `ci-cd-ops` - Go CI pipelines, caching go modules, goreleaser
+- `testing-ops` - Cross-language testing strategies

+ 0 - 0
skills/go-ops/assets/.gitkeep


+ 976 - 0
skills/go-ops/references/concurrency.md

@@ -0,0 +1,976 @@
+# Go Concurrency Reference
+
+## Table of Contents
+
+1. [Goroutines](#goroutines)
+2. [Channels](#channels)
+3. [Select](#select)
+4. [Context](#context)
+5. [Sync Primitives](#sync-primitives)
+6. [errgroup](#errgroup)
+7. [Worker Pool Pattern](#worker-pool-pattern)
+8. [Fan-out / Fan-in](#fan-out--fan-in)
+9. [Pipeline Pattern](#pipeline-pattern)
+10. [Rate Limiting](#rate-limiting)
+11. [Common Mistakes](#common-mistakes)
+
+---
+
+## Goroutines
+
+### Launch Patterns
+
+```go
+// Anonymous function - capture variables carefully
+go func() {
+    doWork()
+}()
+
+// Named function
+go processItem(item)
+
+// Method
+go srv.handleConnection(conn)
+
+// Capture loop variable correctly (pre-Go 1.22)
+for _, item := range items {
+    item := item // shadow to capture
+    go func() {
+        process(item)
+    }()
+}
+
+// Go 1.22+: loop variable captured per iteration automatically
+for _, item := range items {
+    go func() {
+        process(item) // safe in Go 1.22+
+    }()
+}
+```
+
+### Goroutine Lifecycle
+
+Every goroutine needs an exit path. Establish ownership at creation time.
+
+```go
+func startWorker(ctx context.Context, jobs <-chan Job) {
+    go func() {
+        for {
+            select {
+            case <-ctx.Done():
+                return // clean exit on cancellation
+            case job, ok := <-jobs:
+                if !ok {
+                    return // channel closed
+                }
+                process(job)
+            }
+        }
+    }()
+}
+```
+
+### Avoid Goroutine Leaks
+
+A goroutine leaks when it blocks forever with no exit condition.
+
+```go
+// LEAK: goroutine blocks on send forever if nobody reads
+func bad() {
+    ch := make(chan int)
+    go func() {
+        ch <- compute() // blocks if caller exits
+    }()
+    // if caller returns without reading ch, goroutine leaks
+}
+
+// FIX: use buffered channel or context
+func good(ctx context.Context) {
+    ch := make(chan int, 1) // buffer absorbs the send
+    go func() {
+        select {
+        case ch <- compute():
+        case <-ctx.Done():
+        }
+    }()
+}
+```
+
+### Cost Model
+
+- Initial stack: ~2 KB (grows as needed, up to 1 GB by default)
+- Goroutines are multiplexed onto OS threads by the Go scheduler
+- Switching between goroutines is cheap (~100 ns) vs OS thread switch (~1 µs)
+- Practical limit: tens of thousands of goroutines; millions is unusual but possible
+- Use `runtime.NumGoroutine()` to inspect count; expose via `pprof` in production
+
+---
+
+## Channels
+
+### Buffered vs Unbuffered
+
+```go
+// Unbuffered: sender blocks until receiver is ready (synchronous handoff)
+ch := make(chan int)
+
+// Buffered: sender blocks only when buffer is full
+ch := make(chan int, 10)
+```
+
+Use unbuffered when you want a synchronization guarantee (the receiver got the value).
+Use buffered to decouple producer/consumer speeds or to avoid goroutine creation.
+
+### Directional Channels
+
+```go
+// Restrict channels at function boundaries for clarity and safety
+func produce(out chan<- int) { // send-only
+    out <- 42
+}
+
+func consume(in <-chan int) { // receive-only
+    v := <-in
+    fmt.Println(v)
+}
+
+func wire() {
+    ch := make(chan int, 1)
+    go produce(ch)
+    consume(ch)
+}
+```
+
+### Close Semantics
+
+```go
+// Only the sender should close
+close(ch)
+
+// Closed channel returns zero value immediately
+v, ok := <-ch
+// ok == false means channel is closed and drained
+
+// Panic conditions:
+// - closing a nil channel
+// - closing an already-closed channel
+// - sending on a closed channel
+```
+
+### Range over Channel
+
+```go
+// Range exits when channel is closed and drained
+for v := range ch {
+    process(v)
+}
+
+// Equivalent explicit loop
+for {
+    v, ok := <-ch
+    if !ok {
+        break
+    }
+    process(v)
+}
+```
+
+### Nil Channel Behavior
+
+```go
+var ch chan int // nil channel
+
+// Sending or receiving on nil blocks forever
+// <-ch   // blocks
+// ch <- 1 // blocks
+
+// Useful in select to disable a case dynamically
+func merge(a, b <-chan int) <-chan int {
+    out := make(chan int)
+    go func() {
+        defer close(out)
+        for a != nil || b != nil {
+            select {
+            case v, ok := <-a:
+                if !ok {
+                    a = nil // disable this case
+                    continue
+                }
+                out <- v
+            case v, ok := <-b:
+                if !ok {
+                    b = nil // disable this case
+                    continue
+                }
+                out <- v
+            }
+        }
+    }()
+    return out
+}
+```
+
+---
+
+## Select
+
+### Multi-channel Operations
+
+```go
+select {
+case msg := <-ch1:
+    handle(msg)
+case ch2 <- value:
+    // sent successfully
+case <-done:
+    return
+}
+```
+
+### Timeout Pattern
+
+```go
+func fetchWithTimeout(url string, timeout time.Duration) (*Response, error) {
+    result := make(chan *Response, 1)
+    go func() {
+        result <- fetch(url)
+    }()
+
+    select {
+    case resp := <-result:
+        return resp, nil
+    case <-time.After(timeout):
+        return nil, fmt.Errorf("fetch %s: timed out after %v", url, timeout)
+    }
+}
+```
+
+### Non-blocking with Default
+
+```go
+// Try to send/receive; skip if not ready
+select {
+case ch <- value:
+    // sent
+default:
+    // channel full or no receiver; drop or handle
+}
+
+// Non-blocking receive
+select {
+case v := <-ch:
+    use(v)
+default:
+    // nothing available right now
+}
+```
+
+### Priority Pattern
+
+Go's select is random when multiple cases are ready. Force priority explicitly.
+
+```go
+// Drain high-priority channel before processing low-priority
+func prioritySelect(hi, lo <-chan Job) {
+    for {
+        select {
+        case job := <-hi:
+            process(job)
+        default:
+            // hi empty; check both
+            select {
+            case job := <-hi:
+                process(job)
+            case job := <-lo:
+                process(job)
+            }
+        }
+    }
+}
+```
+
+---
+
+## Context
+
+### Create Root Contexts
+
+```go
+ctx := context.Background() // top-level; never cancelled
+ctx := context.TODO()       // placeholder; replace before shipping
+```
+
+### WithCancel
+
+```go
+ctx, cancel := context.WithCancel(parent)
+defer cancel() // always defer to free resources
+
+go func() {
+    <-ctx.Done()
+    fmt.Println("cancelled:", ctx.Err()) // context.Canceled
+}()
+
+cancel() // trigger cancellation
+```
+
+### WithTimeout and WithDeadline
+
+```go
+ctx, cancel := context.WithTimeout(parent, 5*time.Second)
+defer cancel()
+
+// WithDeadline takes an absolute time
+deadline := time.Now().Add(5 * time.Second)
+ctx, cancel = context.WithDeadline(parent, deadline)
+defer cancel()
+
+// Check remaining time
+if d, ok := ctx.Deadline(); ok {
+    remaining := time.Until(d)
+    fmt.Println("remaining:", remaining)
+}
+```
+
+### WithValue
+
+```go
+type contextKey string // unexported to avoid collisions
+
+const requestIDKey contextKey = "request-id"
+
+func withRequestID(ctx context.Context, id string) context.Context {
+    return context.WithValue(ctx, requestIDKey, id)
+}
+
+func requestIDFromContext(ctx context.Context) (string, bool) {
+    id, ok := ctx.Value(requestIDKey).(string)
+    return id, ok
+}
+```
+
+### Propagation Rules
+
+- Always pass `ctx` as the first argument to functions that do I/O
+- Never store context in a struct field (pass explicitly)
+- Derive child contexts; never modify the parent
+- Cancel is inherited: cancelling parent cancels all children
+
+### HTTP Middleware Pattern
+
+```go
+func requestIDMiddleware(next http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        id := r.Header.Get("X-Request-ID")
+        if id == "" {
+            id = generateID()
+        }
+        ctx := withRequestID(r.Context(), id)
+        next.ServeHTTP(w, r.WithContext(ctx))
+    })
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+    id, _ := requestIDFromContext(r.Context())
+    // id flows through without explicit passing
+}
+```
+
+---
+
+## Sync Primitives
+
+### Mutex
+
+```go
+type SafeMap struct {
+    mu sync.Mutex
+    m  map[string]int
+}
+
+func (s *SafeMap) Set(key string, val int) {
+    s.mu.Lock()
+    defer s.mu.Unlock()
+    s.m[key] = val
+}
+
+func (s *SafeMap) Get(key string) (int, bool) {
+    s.mu.Lock()
+    defer s.mu.Unlock()
+    v, ok := s.m[key]
+    return v, ok
+}
+```
+
+### RWMutex
+
+Use when reads vastly outnumber writes.
+
+```go
+type Cache struct {
+    mu   sync.RWMutex
+    data map[string]string
+}
+
+func (c *Cache) Get(key string) (string, bool) {
+    c.mu.RLock()         // multiple readers allowed
+    defer c.mu.RUnlock()
+    v, ok := c.data[key]
+    return v, ok
+}
+
+func (c *Cache) Set(key, val string) {
+    c.mu.Lock()          // exclusive write
+    defer c.mu.Unlock()
+    c.data[key] = val
+}
+```
+
+### Once
+
+```go
+var (
+    instance *DB
+    once     sync.Once
+)
+
+func GetDB() *DB {
+    once.Do(func() {
+        instance = connectDB()
+    })
+    return instance
+}
+```
+
+### WaitGroup
+
+```go
+var wg sync.WaitGroup
+
+for _, url := range urls {
+    wg.Add(1)
+    go func(u string) {
+        defer wg.Done()
+        fetch(u)
+    }(url)
+}
+
+wg.Wait() // block until all goroutines call Done
+```
+
+### Pool
+
+```go
+var bufPool = sync.Pool{
+    New: func() any {
+        return new(bytes.Buffer)
+    },
+}
+
+func processRequest(data []byte) []byte {
+    buf := bufPool.Get().(*bytes.Buffer)
+    defer func() {
+        buf.Reset()
+        bufPool.Put(buf)
+    }()
+
+    buf.Write(data)
+    // transform buf...
+    return buf.Bytes()
+}
+```
+
+### Atomic Operations
+
+```go
+import "sync/atomic"
+
+var counter atomic.Int64
+
+counter.Add(1)
+counter.Store(0)
+v := counter.Load()
+swapped := counter.CompareAndSwap(old, new)
+
+// Prefer atomic for simple counters; prefer Mutex for compound operations
+```
+
+### When to Use Each
+
+| Primitive | Use When |
+|-----------|----------|
+| `Mutex` | Protecting a struct with multiple fields |
+| `RWMutex` | Read-heavy access; reads >> writes |
+| `Once` | One-time initialization |
+| `WaitGroup` | Waiting for a collection of goroutines |
+| `Pool` | Reusing temporary objects to reduce GC pressure |
+| `atomic` | Single integer/pointer with no compound operations |
+| Channel | Transferring ownership of data between goroutines |
+
+---
+
+## errgroup
+
+### Basic Usage
+
+```go
+import "golang.org/x/sync/errgroup"
+
+func fetchAll(ctx context.Context, urls []string) ([][]byte, error) {
+    g, ctx := errgroup.WithContext(ctx)
+    results := make([][]byte, len(urls))
+
+    for i, url := range urls {
+        i, url := i, url
+        g.Go(func() error {
+            body, err := get(ctx, url)
+            if err != nil {
+                return fmt.Errorf("fetch %s: %w", url, err)
+            }
+            results[i] = body
+            return nil
+        })
+    }
+
+    if err := g.Wait(); err != nil {
+        return nil, err
+    }
+    return results, nil
+}
+```
+
+### Limit Concurrency with SetLimit
+
+```go
+g, ctx := errgroup.WithContext(ctx)
+g.SetLimit(10) // at most 10 goroutines at a time
+
+for _, url := range urls {
+    url := url
+    g.Go(func() error {
+        return process(ctx, url)
+    })
+}
+
+return g.Wait()
+```
+
+### Collect Results Safely
+
+Pre-allocate the result slice before launching goroutines. Each goroutine writes to its own index — no mutex needed because slice indices do not overlap.
+
+```go
+type Result struct {
+    URL  string
+    Data []byte
+}
+
+func gather(ctx context.Context, urls []string) ([]Result, error) {
+    g, ctx := errgroup.WithContext(ctx)
+    results := make([]Result, len(urls))
+
+    for i, url := range urls {
+        i, url := i, url
+        g.Go(func() error {
+            data, err := get(ctx, url)
+            if err != nil {
+                return err
+            }
+            results[i] = Result{URL: url, Data: data}
+            return nil
+        })
+    }
+
+    if err := g.Wait(); err != nil {
+        return nil, err
+    }
+    return results, nil
+}
+```
+
+---
+
+## Worker Pool Pattern
+
+```go
+type Job struct {
+    ID   int
+    Data []byte
+}
+
+type Result struct {
+    JobID  int
+    Output []byte
+    Err    error
+}
+
+func workerPool(
+    ctx context.Context,
+    jobs <-chan Job,
+    numWorkers int,
+) <-chan Result {
+    results := make(chan Result, numWorkers)
+
+    var wg sync.WaitGroup
+    for i := 0; i < numWorkers; i++ {
+        wg.Add(1)
+        go func() {
+            defer wg.Done()
+            for {
+                select {
+                case <-ctx.Done():
+                    return
+                case job, ok := <-jobs:
+                    if !ok {
+                        return
+                    }
+                    out, err := processJob(job)
+                    results <- Result{JobID: job.ID, Output: out, Err: err}
+                }
+            }
+        }()
+    }
+
+    // Close results when all workers finish
+    go func() {
+        wg.Wait()
+        close(results)
+    }()
+
+    return results
+}
+
+func run(ctx context.Context, allJobs []Job) error {
+    jobs := make(chan Job, len(allJobs))
+    for _, j := range allJobs {
+        jobs <- j
+    }
+    close(jobs)
+
+    results := workerPool(ctx, jobs, 5)
+
+    for r := range results {
+        if r.Err != nil {
+            return fmt.Errorf("job %d: %w", r.JobID, r.Err)
+        }
+        fmt.Printf("job %d done\n", r.JobID)
+    }
+    return nil
+}
+```
+
+---
+
+## Fan-out / Fan-in
+
+### Fan-out: Distribute One Channel to Many Workers
+
+```go
+func fanOut(in <-chan int, n int) []<-chan int {
+    outs := make([]<-chan int, n)
+    for i := 0; i < n; i++ {
+        ch := make(chan int)
+        outs[i] = ch
+        go func() {
+            defer close(ch)
+            for v := range in {
+                ch <- v
+            }
+        }()
+    }
+    return outs
+}
+```
+
+### Fan-in: Merge Multiple Channels into One
+
+```go
+func fanIn(ctx context.Context, ins ...<-chan int) <-chan int {
+    out := make(chan int)
+    var wg sync.WaitGroup
+
+    forward := func(ch <-chan int) {
+        defer wg.Done()
+        for {
+            select {
+            case v, ok := <-ch:
+                if !ok {
+                    return
+                }
+                select {
+                case out <- v:
+                case <-ctx.Done():
+                    return
+                }
+            case <-ctx.Done():
+                return
+            }
+        }
+    }
+
+    wg.Add(len(ins))
+    for _, ch := range ins {
+        go forward(ch)
+    }
+
+    go func() {
+        wg.Wait()
+        close(out)
+    }()
+
+    return out
+}
+```
+
+---
+
+## Pipeline Pattern
+
+Each stage reads from upstream and writes to downstream. Cancellation propagates via context.
+
+```go
+func generate(ctx context.Context, nums ...int) <-chan int {
+    out := make(chan int)
+    go func() {
+        defer close(out)
+        for _, n := range nums {
+            select {
+            case out <- n:
+            case <-ctx.Done():
+                return
+            }
+        }
+    }()
+    return out
+}
+
+func square(ctx context.Context, in <-chan int) <-chan int {
+    out := make(chan int)
+    go func() {
+        defer close(out)
+        for v := range in {
+            select {
+            case out <- v * v:
+            case <-ctx.Done():
+                return
+            }
+        }
+    }()
+    return out
+}
+
+func filter(ctx context.Context, in <-chan int, pred func(int) bool) <-chan int {
+    out := make(chan int)
+    go func() {
+        defer close(out)
+        for v := range in {
+            if pred(v) {
+                select {
+                case out <- v:
+                case <-ctx.Done():
+                    return
+                }
+            }
+        }
+    }()
+    return out
+}
+
+func runPipeline(ctx context.Context) {
+    nums := generate(ctx, 1, 2, 3, 4, 5)
+    squares := square(ctx, nums)
+    evens := filter(ctx, squares, func(n int) bool { return n%2 == 0 })
+
+    for v := range evens {
+        fmt.Println(v) // 4, 16
+    }
+}
+```
+
+---
+
+## Rate Limiting
+
+### Semaphore Pattern
+
+```go
+type Semaphore chan struct{}
+
+func NewSemaphore(n int) Semaphore {
+    return make(Semaphore, n)
+}
+
+func (s Semaphore) Acquire() { s <- struct{}{} }
+func (s Semaphore) Release() { <-s }
+
+func fetchConcurrently(ctx context.Context, urls []string, limit int) {
+    sem := NewSemaphore(limit)
+    var wg sync.WaitGroup
+
+    for _, url := range urls {
+        url := url
+        wg.Add(1)
+        go func() {
+            defer wg.Done()
+            sem.Acquire()
+            defer sem.Release()
+            fetch(ctx, url)
+        }()
+    }
+
+    wg.Wait()
+}
+```
+
+### time.Ticker Rate Limiter
+
+```go
+func rateLimitedFetch(ctx context.Context, urls []string, rps int) error {
+    ticker := time.NewTicker(time.Second / time.Duration(rps))
+    defer ticker.Stop()
+
+    for _, url := range urls {
+        select {
+        case <-ctx.Done():
+            return ctx.Err()
+        case <-ticker.C:
+            if err := fetch(ctx, url); err != nil {
+                return err
+            }
+        }
+    }
+    return nil
+}
+```
+
+### Token Bucket (using time/rate)
+
+```go
+import "golang.org/x/time/rate"
+
+limiter := rate.NewLimiter(rate.Limit(100), 10) // 100 req/s, burst 10
+
+func callAPI(ctx context.Context, req Request) error {
+    if err := limiter.Wait(ctx); err != nil {
+        return fmt.Errorf("rate limiter: %w", err)
+    }
+    return sendRequest(req)
+}
+```
+
+---
+
+## Common Mistakes
+
+### Goroutine Leak: Blocking Send with No Receiver
+
+```go
+// BAD
+func search(query string) <-chan Result {
+    ch := make(chan Result) // unbuffered
+    go func() {
+        ch <- doSearch(query) // blocks if caller gives up
+    }()
+    return ch
+}
+
+// GOOD: buffer of 1 so goroutine never blocks
+func search(ctx context.Context, query string) <-chan Result {
+    ch := make(chan Result, 1)
+    go func() {
+        select {
+        case ch <- doSearch(ctx, query):
+        case <-ctx.Done():
+        }
+    }()
+    return ch
+}
+```
+
+### Race Condition: Shared Variable without Protection
+
+```go
+// BAD: data race on count
+var count int
+var wg sync.WaitGroup
+for i := 0; i < 100; i++ {
+    wg.Add(1)
+    go func() {
+        defer wg.Done()
+        count++ // not safe
+    }()
+}
+
+// GOOD: use atomic or mutex
+var count atomic.Int64
+for i := 0; i < 100; i++ {
+    wg.Add(1)
+    go func() {
+        defer wg.Done()
+        count.Add(1)
+    }()
+}
+```
+
+### Deadlock: All Goroutines Waiting on Each Other
+
+```go
+// BAD: both goroutines block trying to send before anyone reads
+ch := make(chan int)
+ch <- 1 // blocks main goroutine
+go func() { ch <- 2 }() // never reached
+
+// GOOD: buffer or launch reader first
+ch := make(chan int, 2)
+ch <- 1
+ch <- 2
+```
+
+### Closing a Channel from the Wrong Side
+
+```go
+// BAD: receiver closes channel; sender may still write
+func consumer(ch chan int) {
+    close(ch) // panics if sender writes after this
+}
+
+// GOOD: only the producer closes
+func producer(ch chan<- int) {
+    defer close(ch)
+    for _, v := range data {
+        ch <- v
+    }
+}
+```
+
+### WaitGroup Counter Mismatch
+
+```go
+// BAD: Add inside goroutine; may call Wait before Add
+for _, item := range items {
+    go func(item Item) {
+        wg.Add(1) // too late
+        defer wg.Done()
+        process(item)
+    }(item)
+}
+wg.Wait()
+
+// GOOD: Add before launching goroutine
+for _, item := range items {
+    wg.Add(1)
+    go func(item Item) {
+        defer wg.Done()
+        process(item)
+    }(item)
+}
+wg.Wait()
+```
+
+### Detect Races at Test Time
+
+```go
+// Always run tests with the race detector
+// go test -race ./...
+// go build -race ./cmd/server
+```

+ 704 - 0
skills/go-ops/references/error-handling.md

@@ -0,0 +1,704 @@
+# Go Error Handling Reference
+
+## Table of Contents
+
+1. [Error Basics](#error-basics)
+2. [Wrap Errors with %w](#wrap-errors-with-w)
+3. [errors.Is and errors.As](#errorsis-and-errorsas)
+4. [Sentinel Errors](#sentinel-errors)
+5. [Custom Error Types](#custom-error-types)
+6. [Error Wrapping Strategy](#error-wrapping-strategy)
+7. [panic and recover](#panic-and-recover)
+8. [Errors in Goroutines](#errors-in-goroutines)
+9. [Multi-Error](#multi-error)
+10. [Test Errors](#test-errors)
+11. [Anti-Patterns](#anti-patterns)
+
+---
+
+## Error Basics
+
+### The error Interface
+
+```go
+type error interface {
+    Error() string
+}
+```
+
+Any type implementing `Error() string` satisfies the `error` interface.
+
+### Create Simple Errors
+
+```go
+import "errors"
+
+var err1 = errors.New("something went wrong")
+
+// fmt.Errorf for formatted messages (no wrapping)
+err2 := fmt.Errorf("user %d not found", id)
+
+// Return nil to signal success
+func divide(a, b float64) (float64, error) {
+    if b == 0 {
+        return 0, errors.New("division by zero")
+    }
+    return a / b, nil
+}
+
+// Check error
+result, err := divide(10, 0)
+if err != nil {
+    log.Fatal(err)
+}
+```
+
+---
+
+## Wrap Errors with %w
+
+The `%w` verb creates a wrapped error that preserves the original for inspection with `errors.Is` and `errors.As`.
+
+```go
+func getUser(id int64) (*User, error) {
+    row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
+    if err := row.Scan(&user); err != nil {
+        return nil, fmt.Errorf("get user %d: %w", id, err)
+    }
+    return &user, nil
+}
+
+func loadProfile(id int64) (*Profile, error) {
+    user, err := getUser(id)
+    if err != nil {
+        return nil, fmt.Errorf("load profile: %w", err)
+    }
+    // ...
+    return profile, nil
+}
+```
+
+The resulting error chain looks like:
+
+```
+load profile: get user 42: sql: no rows in result set
+```
+
+### Unwrap the Chain
+
+```go
+// errors.Unwrap returns the next error in the chain
+wrapped := fmt.Errorf("outer: %w", inner)
+inner == errors.Unwrap(wrapped) // true
+
+// Walk the full chain manually
+for err != nil {
+    fmt.Println(err)
+    err = errors.Unwrap(err)
+}
+```
+
+---
+
+## errors.Is and errors.As
+
+### errors.Is — Identity Check
+
+Use `errors.Is` to check whether a specific sentinel error appears anywhere in the chain.
+
+```go
+var ErrNotFound = errors.New("not found")
+
+err := fmt.Errorf("query: %w", ErrNotFound)
+
+errors.Is(err, ErrNotFound) // true — searches the whole chain
+err == ErrNotFound          // false — direct comparison misses the wrapping
+```
+
+### errors.As — Type Check
+
+Use `errors.As` to extract a typed error from anywhere in the chain.
+
+```go
+type ValidationError struct {
+    Field   string
+    Message string
+}
+
+func (e *ValidationError) Error() string {
+    return fmt.Sprintf("%s: %s", e.Field, e.Message)
+}
+
+err := fmt.Errorf("create user: %w", &ValidationError{Field: "email", Message: "invalid"})
+
+var valErr *ValidationError
+if errors.As(err, &valErr) {
+    fmt.Println(valErr.Field)   // "email"
+    fmt.Println(valErr.Message) // "invalid"
+}
+```
+
+### Custom Is Method
+
+Implement `Is` when equality should be value-based rather than pointer-based.
+
+```go
+type StatusError struct {
+    Code int
+}
+
+func (e *StatusError) Error() string {
+    return fmt.Sprintf("status %d", e.Code)
+}
+
+func (e *StatusError) Is(target error) bool {
+    t, ok := target.(*StatusError)
+    if !ok {
+        return false
+    }
+    return e.Code == t.Code
+}
+
+ErrNotFound := &StatusError{Code: 404}
+
+err := fmt.Errorf("request: %w", &StatusError{Code: 404})
+errors.Is(err, ErrNotFound) // true — matched by value
+```
+
+---
+
+## Sentinel Errors
+
+Sentinel errors are package-level variables used as well-known error values.
+
+```go
+var (
+    ErrNotFound     = errors.New("not found")
+    ErrUnauthorized = errors.New("unauthorized")
+    ErrConflict     = errors.New("conflict")
+)
+
+func FindUser(id int64) (*User, error) {
+    if id == 0 {
+        return nil, ErrNotFound
+    }
+    // ...
+}
+
+// Caller checks identity
+user, err := FindUser(id)
+if errors.Is(err, ErrNotFound) {
+    http.Error(w, "Not Found", http.StatusNotFound)
+    return
+}
+```
+
+### Stdlib Sentinel Examples
+
+```go
+io.EOF              // end of stream; not an error condition
+io.ErrUnexpectedEOF // stream ended mid-record; is an error
+sql.ErrNoRows       // query returned zero rows
+os.ErrNotExist      // file does not exist (use errors.Is, not ==)
+context.Canceled    // context was cancelled
+context.DeadlineExceeded // context deadline passed
+```
+
+Note: `os.ErrNotExist` wraps multiple underlying errors (`syscall.ENOENT`, etc.). Always use `errors.Is(err, os.ErrNotExist)` rather than direct comparison.
+
+---
+
+## Custom Error Types
+
+### Struct Error with Extra Fields
+
+```go
+type NotFoundError struct {
+    Resource string
+    ID       int64
+}
+
+func (e *NotFoundError) Error() string {
+    return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
+}
+
+func GetOrder(id int64) (*Order, error) {
+    order := findOrder(id)
+    if order == nil {
+        return nil, &NotFoundError{Resource: "order", ID: id}
+    }
+    return order, nil
+}
+
+// Extract and use the extra fields
+var notFound *NotFoundError
+if errors.As(err, &notFound) {
+    log.Printf("missing resource: %s %d", notFound.Resource, notFound.ID)
+}
+```
+
+### HTTPError with Status Code
+
+```go
+type HTTPError struct {
+    Code    int
+    Message string
+    Cause   error
+}
+
+func (e *HTTPError) Error() string {
+    if e.Cause != nil {
+        return fmt.Sprintf("HTTP %d: %s: %v", e.Code, e.Message, e.Cause)
+    }
+    return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
+}
+
+func (e *HTTPError) Unwrap() error {
+    return e.Cause
+}
+
+// Implement Unwrap to keep the chain intact
+```
+
+---
+
+## Error Wrapping Strategy
+
+### Add Context at Each Layer
+
+```go
+// Repository layer: wrap with operation context
+func (r *UserRepo) Find(id int64) (*User, error) {
+    var u User
+    err := r.db.Get(&u, "SELECT * FROM users WHERE id = $1", id)
+    if err != nil {
+        return nil, fmt.Errorf("find user %d: %w", id, err)
+    }
+    return &u, nil
+}
+
+// Service layer: wrap with business operation context
+func (s *UserService) GetProfile(id int64) (*Profile, error) {
+    user, err := s.repo.Find(id)
+    if err != nil {
+        return nil, fmt.Errorf("get profile: %w", err)
+    }
+    // ...
+}
+
+// Handler layer: inspect and translate for the caller
+func (h *Handler) handleGetProfile(w http.ResponseWriter, r *http.Request) {
+    id := parseID(r)
+    profile, err := h.svc.GetProfile(id)
+    if err != nil {
+        if errors.Is(err, sql.ErrNoRows) {
+            http.Error(w, "not found", http.StatusNotFound)
+            return
+        }
+        http.Error(w, "internal error", http.StatusInternalServerError)
+        log.Printf("get profile: %v", err) // log full chain here
+        return
+    }
+    writeJSON(w, profile)
+}
+```
+
+### Message Conventions
+
+- Use lowercase for error strings (Go convention)
+- Use `: ` to separate context from cause
+- Do not end with punctuation
+- Do not duplicate information already in the wrapped error
+
+```go
+// GOOD
+fmt.Errorf("parse config: %w", err)
+fmt.Errorf("connect to database %s: %w", dsn, err)
+
+// BAD: redundant — the wrapped error already says "failed"
+fmt.Errorf("failed to connect: %w", err)
+
+// BAD: capitalized
+fmt.Errorf("Parse config: %w", err)
+```
+
+### Log Once, at the Top
+
+```go
+// BAD: logged at every layer, duplicates output
+func (r *Repo) Find(id int64) (*User, error) {
+    err := query()
+    if err != nil {
+        log.Printf("repo error: %v", err)  // logged here
+        return nil, fmt.Errorf("find: %w", err)
+    }
+}
+
+func (s *Svc) Get(id int64) (*User, error) {
+    u, err := r.Find(id)
+    if err != nil {
+        log.Printf("svc error: %v", err)   // logged again
+        return nil, fmt.Errorf("get: %w", err)
+    }
+}
+
+// GOOD: wrap through; log once at the edge (handler/main)
+```
+
+---
+
+## panic and recover
+
+### When panic Is Legitimate
+
+- Programmer errors that cannot be corrected at runtime (nil dereference, index out of range)
+- Impossible conditions in initialization (`init` or package `var` blocks)
+- Internal consistency violations inside a package (never cross package boundaries)
+
+```go
+func mustParseURL(raw string) *url.URL {
+    u, err := url.Parse(raw)
+    if err != nil {
+        panic(fmt.Sprintf("invalid hardcoded URL %q: %v", raw, err))
+    }
+    return u
+}
+
+// Use Must* pattern for hardcoded values only; never for user input
+var baseURL = mustParseURL("https://api.example.com")
+```
+
+### recover in HTTP Middleware
+
+```go
+func recoveryMiddleware(next http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        defer func() {
+            if rec := recover(); rec != nil {
+                // Log with stack trace
+                buf := make([]byte, 4096)
+                n := runtime.Stack(buf, false)
+                log.Printf("panic recovered: %v\n%s", rec, buf[:n])
+
+                http.Error(w, "internal server error", http.StatusInternalServerError)
+            }
+        }()
+        next.ServeHTTP(w, r)
+    })
+}
+```
+
+### Do Not recover Across Package Boundaries
+
+A library must never let a panic escape to the caller. Recover internally and return an error.
+
+```go
+func (p *Parser) Parse(input []byte) (result Result, err error) {
+    defer func() {
+        if rec := recover(); rec != nil {
+            err = fmt.Errorf("parse panicked: %v", rec)
+        }
+    }()
+    result = p.doParse(input)
+    return
+}
+```
+
+---
+
+## Errors in Goroutines
+
+### Channel-Based
+
+```go
+func runAsync(ctx context.Context, fn func() error) <-chan error {
+    errCh := make(chan error, 1) // buffer of 1 prevents leak
+    go func() {
+        errCh <- fn()
+    }()
+    return errCh
+}
+
+errCh := runAsync(ctx, doWork)
+
+select {
+case err := <-errCh:
+    if err != nil {
+        return fmt.Errorf("async work: %w", err)
+    }
+case <-ctx.Done():
+    return ctx.Err()
+}
+```
+
+### errgroup (Preferred)
+
+```go
+g, ctx := errgroup.WithContext(ctx)
+
+g.Go(func() error {
+    return stepA(ctx)
+})
+g.Go(func() error {
+    return stepB(ctx)
+})
+
+// Wait returns the first non-nil error; other goroutines see ctx cancelled
+if err := g.Wait(); err != nil {
+    return fmt.Errorf("pipeline: %w", err)
+}
+```
+
+### Error Aggregation
+
+When all errors matter (not just the first):
+
+```go
+type MultiError struct {
+    Errors []error
+}
+
+func (m *MultiError) Error() string {
+    msgs := make([]string, len(m.Errors))
+    for i, e := range m.Errors {
+        msgs[i] = e.Error()
+    }
+    return strings.Join(msgs, "; ")
+}
+
+func runAll(fns []func() error) error {
+    var mu sync.Mutex
+    var errs []error
+    var wg sync.WaitGroup
+
+    for _, fn := range fns {
+        fn := fn
+        wg.Add(1)
+        go func() {
+            defer wg.Done()
+            if err := fn(); err != nil {
+                mu.Lock()
+                errs = append(errs, err)
+                mu.Unlock()
+            }
+        }()
+    }
+
+    wg.Wait()
+    if len(errs) > 0 {
+        return &MultiError{Errors: errs}
+    }
+    return nil
+}
+```
+
+---
+
+## Multi-Error
+
+### errors.Join (Go 1.20+)
+
+```go
+err1 := errors.New("validation failed on field email")
+err2 := errors.New("validation failed on field phone")
+
+combined := errors.Join(err1, err2)
+fmt.Println(combined)
+// validation failed on field email
+// validation failed on field phone
+
+errors.Is(combined, err1) // true
+errors.Is(combined, err2) // true
+```
+
+### Collect Validation Errors
+
+```go
+func validateUser(u User) error {
+    var errs []error
+
+    if u.Name == "" {
+        errs = append(errs, errors.New("name is required"))
+    }
+    if !isValidEmail(u.Email) {
+        errs = append(errs, fmt.Errorf("email %q is invalid", u.Email))
+    }
+    if u.Age < 0 {
+        errs = append(errs, errors.New("age must not be negative"))
+    }
+
+    return errors.Join(errs...) // nil if errs is empty
+}
+```
+
+---
+
+## Test Errors
+
+### Check Sentinel Errors with errors.Is
+
+```go
+func TestFindUser_NotFound(t *testing.T) {
+    repo := NewRepo(testDB)
+
+    _, err := repo.Find(999)
+
+    if !errors.Is(err, ErrNotFound) {
+        t.Errorf("expected ErrNotFound, got %v", err)
+    }
+}
+```
+
+### Extract Typed Errors with errors.As
+
+```go
+func TestValidate_InvalidEmail(t *testing.T) {
+    err := validateUser(User{Name: "Alice", Email: "bad"})
+
+    var valErr *ValidationError
+    if !errors.As(err, &valErr) {
+        t.Fatalf("expected *ValidationError, got %T: %v", err, err)
+    }
+    if valErr.Field != "email" {
+        t.Errorf("expected field 'email', got %q", valErr.Field)
+    }
+}
+```
+
+### Table-Driven Error Tests
+
+```go
+func TestDivide(t *testing.T) {
+    tests := []struct {
+        name    string
+        a, b    float64
+        wantErr bool
+        errIs   error
+    }{
+        {"normal", 10, 2, false, nil},
+        {"divide by zero", 10, 0, true, ErrDivisionByZero},
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            _, err := Divide(tt.a, tt.b)
+            if (err != nil) != tt.wantErr {
+                t.Fatalf("wantErr=%v, got err=%v", tt.wantErr, err)
+            }
+            if tt.errIs != nil && !errors.Is(err, tt.errIs) {
+                t.Errorf("errors.Is(%v, %v) = false", err, tt.errIs)
+            }
+        })
+    }
+}
+```
+
+---
+
+## Anti-Patterns
+
+### Stringly-Typed Error Checks
+
+```go
+// BAD: fragile; breaks on message change
+if err.Error() == "not found" { ... }
+if strings.Contains(err.Error(), "timeout") { ... }
+
+// GOOD: use sentinel or typed errors
+if errors.Is(err, ErrNotFound) { ... }
+
+var netErr *net.OpError
+if errors.As(err, &netErr) && netErr.Timeout() { ... }
+```
+
+### Swallowing Errors
+
+```go
+// BAD: silent discard
+result, _ := doSomething()
+json.Unmarshal(data, &v) // ignoring error
+
+// GOOD: handle or at minimum log
+result, err := doSomething()
+if err != nil {
+    return fmt.Errorf("doSomething: %w", err)
+}
+
+if err := json.Unmarshal(data, &v); err != nil {
+    return fmt.Errorf("unmarshal response: %w", err)
+}
+```
+
+### Log and Return (Double Logging)
+
+```go
+// BAD: causes duplicate log lines
+func getUser(id int64) (*User, error) {
+    user, err := db.Find(id)
+    if err != nil {
+        log.Printf("db.Find error: %v", err) // logged here
+        return nil, fmt.Errorf("find user: %w", err)
+    }
+    return user, nil
+}
+// caller also logs → same error appears twice
+
+// GOOD: wrap and propagate; log once at the boundary
+func getUser(id int64) (*User, error) {
+    user, err := db.Find(id)
+    if err != nil {
+        return nil, fmt.Errorf("find user %d: %w", id, err)
+    }
+    return user, nil
+}
+```
+
+### Over-Wrapping with Redundant Context
+
+```go
+// BAD: "failed to" is noise; wrapped error already explains what happened
+return fmt.Errorf("failed to get user: failed to query database: %w", err)
+
+// GOOD: each layer adds one meaningful label
+return fmt.Errorf("get user %d: %w", id, err)
+```
+
+### Panic for Expected Errors
+
+```go
+// BAD: panicking on user-controlled input
+func ParseAge(s string) int {
+    n, err := strconv.Atoi(s)
+    if err != nil {
+        panic("invalid age") // crashes the program
+    }
+    return n
+}
+
+// GOOD: return the error
+func ParseAge(s string) (int, error) {
+    n, err := strconv.Atoi(s)
+    if err != nil {
+        return 0, fmt.Errorf("parse age %q: %w", s, err)
+    }
+    return n, nil
+}
+```
+
+### Returning Non-nil Error with Non-zero Value
+
+```go
+// BAD: caller may use the value even when err != nil
+func compute() (Result, error) {
+    if bad {
+        return Result{partial: true}, errors.New("incomplete")
+    }
+}
+
+// GOOD: return zero value on error so callers don't accidentally use it
+func compute() (Result, error) {
+    if bad {
+        return Result{}, errors.New("incomplete")
+    }
+}
+```

+ 721 - 0
skills/go-ops/references/interfaces-generics.md

@@ -0,0 +1,721 @@
+# Go Interfaces and Generics Reference
+
+## Table of Contents
+
+1. [Interface Design Principles](#1-interface-design-principles)
+2. [Interface Composition](#2-interface-composition)
+3. [Type Assertions](#3-type-assertions)
+4. [Empty Interface and any](#4-empty-interface-and-any)
+5. [Generics Basics](#5-generics-basics)
+6. [Generic Functions](#6-generic-functions)
+7. [Generic Types](#7-generic-types)
+8. [Constraints](#8-constraints)
+9. [When NOT to Use Generics](#9-when-not-to-use-generics)
+10. [Functional Options](#10-functional-options)
+11. [Builder Pattern](#11-builder-pattern)
+12. [Strategy via Interfaces](#12-strategy-via-interfaces)
+
+---
+
+## 1. Interface Design Principles
+
+**Accept interfaces, return concrete types.** Callers decide what abstraction they need; implementations should not hide their type behind an interface at the return site.
+
+```go
+// BAD: returns interface, hides the concrete type unnecessarily
+func NewStore() Store {
+    return &postgresStore{}
+}
+
+// GOOD: returns concrete pointer; callers that need the interface accept it
+func NewStore() *PostgresStore {
+    return &postgresStore{}
+}
+```
+
+**Keep interfaces small.** One or two methods is the ideal. Large interfaces are hard to mock and hard to satisfy.
+
+```go
+// BAD: one interface does too much
+type UserService interface {
+    GetUser(id int64) (*User, error)
+    CreateUser(u *User) error
+    DeleteUser(id int64) error
+    SendWelcomeEmail(u *User) error
+    AuditLog(action string) error
+}
+
+// GOOD: split by role
+type UserReader interface {
+    GetUser(id int64) (*User, error)
+}
+
+type UserWriter interface {
+    CreateUser(u *User) error
+    DeleteUser(id int64) error
+}
+
+type Notifier interface {
+    SendWelcomeEmail(u *User) error
+}
+```
+
+**Define interfaces at the point of use (consumer), not the provider.** This avoids import cycles and keeps packages decoupled.
+
+Standard library examples of well-sized interfaces:
+
+```go
+// io package: one method each
+type Reader interface { Read(p []byte) (n int, err error) }
+type Writer interface { Write(p []byte) (n int, err error) }
+type Closer interface { Close() error }
+
+// fmt package: one method
+type Stringer interface { String() string }
+
+// sort package: three methods (minimum needed for the algorithm)
+type Interface interface {
+    Len() int
+    Less(i, j int) bool
+    Swap(i, j int)
+}
+```
+
+---
+
+## 2. Interface Composition
+
+Embed smaller interfaces to build larger ones. Only embed what callers genuinely need together.
+
+```go
+// Compose from stdlib primitives
+type ReadWriter interface {
+    io.Reader
+    io.Writer
+}
+
+type ReadWriteCloser interface {
+    io.Reader
+    io.Writer
+    io.Closer
+}
+
+// Compose from your own interfaces
+type Repository interface {
+    UserReader
+    UserWriter
+}
+
+// Satisfy a composed interface with one struct
+type postgresStore struct{ db *sql.DB }
+
+func (s *postgresStore) GetUser(id int64) (*User, error)    { /* ... */ }
+func (s *postgresStore) CreateUser(u *User) error           { /* ... */ }
+func (s *postgresStore) DeleteUser(id int64) error          { /* ... */ }
+
+var _ Repository = (*postgresStore)(nil) // compile-time check
+```
+
+The blank identifier assignment `var _ Repository = (*postgresStore)(nil)` is a zero-cost compile-time assertion that `*postgresStore` satisfies `Repository`.
+
+---
+
+## 3. Type Assertions
+
+### Single-Value Form (Panics on Failure)
+
+Use only when you are certain of the type, such as immediately after a type switch.
+
+```go
+var v interface{} = "hello"
+s := v.(string) // panics if v is not a string
+```
+
+### Comma-OK Form (Safe)
+
+```go
+var v interface{} = "hello"
+
+s, ok := v.(string)
+if !ok {
+    // handle wrong type
+}
+```
+
+### Type Switch
+
+The idiomatic way to branch on dynamic type. The variable `x` is narrowed to the concrete type in each case.
+
+```go
+func describe(v interface{}) string {
+    switch x := v.(type) {
+    case string:
+        return fmt.Sprintf("string of length %d", len(x))
+    case int:
+        return fmt.Sprintf("int: %d", x)
+    case []byte:
+        return fmt.Sprintf("bytes: %x", x)
+    case fmt.Stringer:
+        return fmt.Sprintf("stringer: %s", x.String())
+    case nil:
+        return "nil"
+    default:
+        return fmt.Sprintf("unknown type: %T", x)
+    }
+}
+```
+
+---
+
+## 4. Empty Interface and any
+
+`any` is an alias for `interface{}` introduced in Go 1.18. Prefer `any` in new code.
+
+Use `any` only when the type is genuinely unknown at compile time: codec targets, generic containers before generics were available, or variadic logging arguments.
+
+```go
+// Legitimate: JSON decode target unknown at call site
+func Decode(r io.Reader, dst any) error {
+    return json.NewDecoder(r).Decode(dst)
+}
+
+// Legitimate: structured logging with arbitrary fields
+func Info(msg string, fields ...any) { /* ... */ }
+
+// Avoid: using any when a concrete type or interface would work
+func Process(v any) {         // BAD if callers always pass *User
+    u := v.(*User)            // forced assertion everywhere
+}
+
+func Process(u *User) { /* ... */ } // GOOD
+```
+
+Do not use `any` as a way to avoid thinking about types. Every `any` is a deferred type error waiting for runtime.
+
+---
+
+## 5. Generics Basics
+
+Go generics use type parameters in square brackets. Introduced in Go 1.18.
+
+```go
+// Type parameter T with constraint comparable
+func Contains[T comparable](slice []T, item T) bool {
+    for _, v := range slice {
+        if v == item {
+            return true
+        }
+    }
+    return false
+}
+
+// Usage - type inferred from arguments
+found := Contains([]string{"a", "b", "c"}, "b") // true
+found = Contains([]int{1, 2, 3}, 4)              // false
+
+// Multiple type parameters
+func Map[K comparable, V any](m map[K]V, f func(V) V) map[K]V {
+    out := make(map[K]V, len(m))
+    for k, v := range m {
+        out[k] = f(v)
+    }
+    return out
+}
+```
+
+Type inference works in most cases. Provide explicit type arguments only when the compiler cannot infer them.
+
+```go
+// Explicit type argument needed when return type differs from arguments
+func Zero[T any]() T {
+    var zero T
+    return zero
+}
+
+z := Zero[int]()    // must be explicit: no argument to infer from
+```
+
+---
+
+## 6. Generic Functions
+
+### Filter, Map, Reduce
+
+```go
+func Filter[T any](slice []T, predicate func(T) bool) []T {
+    var result []T
+    for _, v := range slice {
+        if predicate(v) {
+            result = append(result, v)
+        }
+    }
+    return result
+}
+
+func Map[T, U any](slice []T, f func(T) U) []U {
+    result := make([]U, len(slice))
+    for i, v := range slice {
+        result[i] = f(v)
+    }
+    return result
+}
+
+func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U {
+    acc := initial
+    for _, v := range slice {
+        acc = f(acc, v)
+    }
+    return acc
+}
+
+// Keys returns the keys of a map in unspecified order
+func Keys[K comparable, V any](m map[K]V) []K {
+    keys := make([]K, 0, len(m))
+    for k := range m {
+        keys = append(keys, k)
+    }
+    return keys
+}
+
+// Values returns the values of a map in unspecified order
+func Values[K comparable, V any](m map[K]V) []V {
+    values := make([]V, 0, len(m))
+    for _, v := range m {
+        values = append(values, v)
+    }
+    return values
+}
+```
+
+---
+
+## 7. Generic Types
+
+### Stack
+
+```go
+type Stack[T any] struct {
+    items []T
+}
+
+func (s *Stack[T]) Push(item T) {
+    s.items = append(s.items, item)
+}
+
+func (s *Stack[T]) Pop() (T, bool) {
+    if len(s.items) == 0 {
+        var zero T
+        return zero, false
+    }
+    n := len(s.items) - 1
+    item := s.items[n]
+    s.items = s.items[:n]
+    return item, true
+}
+
+func (s *Stack[T]) Len() int { return len(s.items) }
+```
+
+### Result Type
+
+Encode success or failure without error returns scattered through call sites.
+
+```go
+type Result[T any] struct {
+    value T
+    err   error
+}
+
+func Ok[T any](value T) Result[T]    { return Result[T]{value: value} }
+func Err[T any](err error) Result[T] { return Result[T]{err: err} }
+
+func (r Result[T]) Unwrap() (T, error) { return r.value, r.err }
+
+func (r Result[T]) Must() T {
+    if r.err != nil {
+        panic(r.err)
+    }
+    return r.value
+}
+```
+
+### Generic Cache with TTL
+
+```go
+type entry[V any] struct {
+    value     V
+    expiresAt time.Time
+}
+
+type Cache[K comparable, V any] struct {
+    mu   sync.RWMutex
+    data map[K]entry[V]
+    ttl  time.Duration
+}
+
+func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {
+    return &Cache[K, V]{data: make(map[K]entry[V]), ttl: ttl}
+}
+
+func (c *Cache[K, V]) Set(key K, value V) {
+    c.mu.Lock()
+    defer c.mu.Unlock()
+    c.data[key] = entry[V]{value: value, expiresAt: time.Now().Add(c.ttl)}
+}
+
+func (c *Cache[K, V]) Get(key K) (V, bool) {
+    c.mu.RLock()
+    defer c.mu.RUnlock()
+    e, ok := c.data[key]
+    if !ok || time.Now().After(e.expiresAt) {
+        var zero V
+        return zero, false
+    }
+    return e.value, true
+}
+```
+
+---
+
+## 8. Constraints
+
+### Built-In Constraints
+
+```go
+// comparable: supports == and != (maps, channels, basic types, structs of comparable fields)
+func Index[T comparable](slice []T, item T) int {
+    for i, v := range slice {
+        if v == item {
+            return i
+        }
+    }
+    return -1
+}
+
+// any: no constraint, widest possible
+func Ptr[T any](v T) *T { return &v }
+```
+
+### golang.org/x/exp/constraints
+
+```go
+import "golang.org/x/exp/constraints"
+
+// Ordered: all types that support <, <=, >, >=
+func Min[T constraints.Ordered](a, b T) T {
+    if a < b {
+        return a
+    }
+    return b
+}
+
+func Max[T constraints.Ordered](a, b T) T {
+    if a > b {
+        return a
+    }
+    return b
+}
+
+func Clamp[T constraints.Ordered](v, lo, hi T) T {
+    return Min(Max(v, lo), hi)
+}
+```
+
+### Custom Constraints
+
+```go
+// Union of specific types
+type Integer interface {
+    ~int | ~int8 | ~int16 | ~int32 | ~int64
+}
+
+type Float interface {
+    ~float32 | ~float64
+}
+
+type Number interface {
+    Integer | Float
+}
+
+func Sum[T Number](nums []T) T {
+    var total T
+    for _, n := range nums {
+        total += n
+    }
+    return total
+}
+```
+
+### Tilde (~) for Underlying Types
+
+`~T` includes all types whose underlying type is `T`. Without `~`, named types are excluded.
+
+```go
+type Celsius float64
+type Fahrenheit float64
+
+// Without ~: Celsius and Fahrenheit do not satisfy Float
+type Float interface { float32 | float64 }
+
+// With ~: Celsius and Fahrenheit satisfy ~float64
+type Float interface { ~float32 | ~float64 }
+
+func Convert[T ~float64](v T) T { return v * 9 / 5 + 32 }
+
+c := Celsius(100)
+f := Convert(c) // works because ~float64 includes Celsius
+```
+
+---
+
+## 9. When NOT to Use Generics
+
+**Use an interface when behavior varies by type.** Generics parametrize over structure, not behavior. If the algorithm calls different methods depending on the type, use an interface.
+
+```go
+// BAD: generics cannot help here - behavior is type-specific
+func Process[T any](v T) {
+    // Cannot call v.Serialize() without a constraint defining it
+}
+
+// GOOD: interface captures the varying behavior
+type Processor interface {
+    Process() error
+}
+func Run(p Processor) error { return p.Process() }
+```
+
+**Use a concrete type when you only have one type.** Adding a type parameter for a function that only ever handles `string` or `int` adds noise with no benefit.
+
+```go
+// Unnecessary generics
+func ParseInt[T ~string](s T) (int64, error) {
+    return strconv.ParseInt(string(s), 10, 64)
+}
+
+// Simpler and clearer
+func ParseInt(s string) (int64, error) {
+    return strconv.ParseInt(s, 10, 64)
+}
+```
+
+**Prefer `any` + type switch for heterogeneous collections** where types are enumerable and fixed. Generics do not simplify this case.
+
+---
+
+## 10. Functional Options
+
+The functional options pattern gives constructors optional, named parameters with default values and forward compatibility.
+
+```go
+type Server struct {
+    host    string
+    port    int
+    timeout time.Duration
+    maxConn int
+    logger  *slog.Logger
+}
+
+type Option func(*Server) error
+
+func WithHost(host string) Option {
+    return func(s *Server) error {
+        if host == "" {
+            return errors.New("host cannot be empty")
+        }
+        s.host = host
+        return nil
+    }
+}
+
+func WithPort(port int) Option {
+    return func(s *Server) error {
+        if port < 1 || port > 65535 {
+            return fmt.Errorf("invalid port: %d", port)
+        }
+        s.port = port
+        return nil
+    }
+}
+
+func WithTimeout(d time.Duration) Option {
+    return func(s *Server) error {
+        if d <= 0 {
+            return errors.New("timeout must be positive")
+        }
+        s.timeout = d
+        return nil
+    }
+}
+
+func WithLogger(l *slog.Logger) Option {
+    return func(s *Server) error {
+        s.logger = l
+        return nil
+    }
+}
+
+func NewServer(opts ...Option) (*Server, error) {
+    s := &Server{ // defaults
+        host:    "localhost",
+        port:    8080,
+        timeout: 30 * time.Second,
+        maxConn: 100,
+        logger:  slog.Default(),
+    }
+    for _, opt := range opts {
+        if err := opt(s); err != nil {
+            return nil, fmt.Errorf("applying option: %w", err)
+        }
+    }
+    return s, nil
+}
+
+// Usage
+srv, err := NewServer(
+    WithHost("0.0.0.0"),
+    WithPort(9090),
+    WithTimeout(time.Minute),
+)
+```
+
+---
+
+## 11. Builder Pattern
+
+Use when construction requires many steps and partial construction is meaningful.
+
+```go
+type QueryBuilder struct {
+    table   string
+    columns []string
+    where   []string
+    orderBy string
+    limit   int
+    args    []any
+    err     error // carry errors through the chain
+}
+
+func NewQuery(table string) *QueryBuilder {
+    if table == "" {
+        return &QueryBuilder{err: errors.New("table name required")}
+    }
+    return &QueryBuilder{table: table, columns: []string{"*"}}
+}
+
+func (q *QueryBuilder) Select(cols ...string) *QueryBuilder {
+    if q.err != nil {
+        return q
+    }
+    q.columns = cols
+    return q
+}
+
+func (q *QueryBuilder) Where(condition string, args ...any) *QueryBuilder {
+    if q.err != nil {
+        return q
+    }
+    q.where = append(q.where, condition)
+    q.args = append(q.args, args...)
+    return q
+}
+
+func (q *QueryBuilder) OrderBy(col string) *QueryBuilder {
+    if q.err != nil {
+        return q
+    }
+    q.orderBy = col
+    return q
+}
+
+func (q *QueryBuilder) Limit(n int) *QueryBuilder {
+    if q.err != nil {
+        return q
+    }
+    if n < 0 {
+        q.err = fmt.Errorf("limit must be non-negative, got %d", n)
+        return q
+    }
+    q.limit = n
+    return q
+}
+
+func (q *QueryBuilder) Build() (string, []any, error) {
+    if q.err != nil {
+        return "", nil, q.err
+    }
+    // assemble SQL from q.table, q.columns, q.where, q.orderBy, q.limit
+    sql := fmt.Sprintf("SELECT %s FROM %s", strings.Join(q.columns, ", "), q.table)
+    if len(q.where) > 0 {
+        sql += " WHERE " + strings.Join(q.where, " AND ")
+    }
+    if q.orderBy != "" {
+        sql += " ORDER BY " + q.orderBy
+    }
+    if q.limit > 0 {
+        sql += fmt.Sprintf(" LIMIT %d", q.limit)
+    }
+    return sql, q.args, nil
+}
+
+// Usage
+sql, args, err := NewQuery("users").
+    Select("id", "name", "email").
+    Where("active = $1", true).
+    Where("role = $2", "admin").
+    OrderBy("name").
+    Limit(25).
+    Build()
+```
+
+---
+
+## 12. Strategy via Interfaces
+
+Swap algorithms at runtime by accepting an interface. The caller chooses the strategy; the function does not need to know the implementation.
+
+```go
+// Define the strategy interface
+type Hasher interface {
+    Hash(data []byte) []byte
+    Name() string
+}
+
+// Multiple implementations
+type SHA256Hasher struct{}
+
+func (SHA256Hasher) Hash(data []byte) []byte {
+    h := sha256.Sum256(data)
+    return h[:]
+}
+func (SHA256Hasher) Name() string { return "sha256" }
+
+type Blake2Hasher struct{}
+
+func (Blake2Hasher) Hash(data []byte) []byte {
+    h := blake2b.Sum256(data)
+    return h[:]
+}
+func (Blake2Hasher) Name() string { return "blake2b" }
+
+// Consumer accepts the interface - does not care about the algorithm
+type FileStore struct {
+    hasher Hasher
+}
+
+func NewFileStore(h Hasher) *FileStore {
+    return &FileStore{hasher: h}
+}
+
+func (fs *FileStore) Store(path string, data []byte) error {
+    checksum := fs.hasher.Hash(data)
+    // write data and checksum to path
+    return writeWithChecksum(path, data, checksum, fs.hasher.Name())
+}
+
+// Swap strategies at call site
+fastStore := NewFileStore(Blake2Hasher{})
+secureStore := NewFileStore(SHA256Hasher{})
+```
+
+This pattern is the Go equivalent of the Gang of Four Strategy pattern. It composes without inheritance and is trivially testable: inject a `mockHasher` that returns fixed bytes.

+ 756 - 0
skills/go-ops/references/performance.md

@@ -0,0 +1,756 @@
+# Go Performance Reference
+
+## Table of Contents
+
+1. [pprof](#pprof)
+2. [go tool trace](#go-tool-trace)
+3. [Benchmarks](#benchmarks)
+4. [Escape Analysis](#escape-analysis)
+5. [Memory Optimization](#memory-optimization)
+6. [String Performance](#string-performance)
+7. [Struct Alignment](#struct-alignment)
+8. [Map Performance](#map-performance)
+9. [I/O Performance](#io-performance)
+10. [Inlining](#inlining)
+11. [Common Performance Anti-Patterns](#common-performance-anti-patterns)
+
+---
+
+## pprof
+
+### Enable pprof in a Production Server
+
+```go
+import (
+    "net/http"
+    _ "net/http/pprof"  // Side-effect import registers handlers on DefaultServeMux
+)
+
+func main() {
+    // Serve pprof on a separate port — never expose this publicly
+    go func() {
+        log.Println(http.ListenAndServe("localhost:6060", nil))
+    }()
+
+    // ... start your actual server
+}
+```
+
+### Collect and Analyze CPU Profiles
+
+```bash
+# 30-second CPU profile from a running server
+go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
+
+# Inside pprof interactive shell
+(pprof) top10          # Top 10 functions by CPU
+(pprof) list myFunc    # Annotated source for a function
+(pprof) web            # Open flame graph in browser (requires graphviz)
+(pprof) png > cpu.png  # Export to image
+```
+
+### Collect and Analyze Memory Profiles
+
+```bash
+# Heap profile (in-use allocations)
+go tool pprof http://localhost:6060/debug/pprof/heap
+
+# Allocation profile (all allocations since start)
+go tool pprof http://localhost:6060/debug/pprof/allocs
+
+# Inside pprof
+(pprof) top            # Top allocators
+(pprof) inuse_space    # Sort by in-use bytes
+(pprof) alloc_objects  # Sort by allocation count
+```
+
+### Profile Goroutines
+
+```bash
+# Goroutine profile — shows all running goroutines with stack traces
+go tool pprof http://localhost:6060/debug/pprof/goroutine
+
+# Or view in browser for a quick human-readable dump
+curl http://localhost:6060/debug/pprof/goroutine?debug=2
+```
+
+### Write Profiles Programmatically
+
+```go
+import "runtime/pprof"
+
+// CPU profile
+f, _ := os.Create("cpu.prof")
+pprof.StartCPUProfile(f)
+defer pprof.StopCPUProfile()
+
+// Memory profile (write at end of program or specific checkpoint)
+f, _ := os.Create("mem.prof")
+runtime.GC()  // Force GC for accurate snapshot
+pprof.WriteHeapProfile(f)
+f.Close()
+```
+
+### Compare Two Profiles
+
+```bash
+# Capture baseline and after a change, then diff them
+go tool pprof -base baseline.prof current.prof
+```
+
+---
+
+## go tool trace
+
+The tracer records goroutine scheduling, GC pauses, and syscalls at microsecond resolution.
+
+### Record a Trace
+
+```bash
+# From a live server
+curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out
+
+# Or programmatically
+import "runtime/trace"
+
+f, _ := os.Create("trace.out")
+trace.Start(f)
+defer trace.Stop()
+```
+
+### Analyze a Trace
+
+```bash
+go tool trace trace.out   # Opens browser-based UI
+```
+
+Key views in the UI:
+
+- **Goroutine analysis**: Which goroutines ran, for how long, what blocked them
+- **View trace**: Timeline of all goroutines across P (processor) threads
+- **Minimum mutator utilization (MMU)**: Percentage of time your code ran vs GC
+
+### Identify Scheduling Latency
+
+Look for goroutines spending time in "Runnable" state — this means they are ready to run but waiting for a P. Signs of over-subscription: too many goroutines competing for `GOMAXPROCS` slots.
+
+```go
+// Instrument specific regions in the trace
+import "runtime/trace"
+
+ctx, task := trace.NewTask(ctx, "processOrder")
+defer task.End()
+
+trace.WithRegion(ctx, "validateInput", func() {
+    validate(input)
+})
+```
+
+---
+
+## Benchmarks
+
+### Write Effective Benchmarks
+
+```go
+func BenchmarkProcess(b *testing.B) {
+    data := generateLargeInput()  // Setup before timer
+
+    b.ResetTimer()  // Exclude setup from measurement
+    for i := 0; i < b.N; i++ {
+        Process(data)
+    }
+}
+```
+
+### Use b.StopTimer / b.StartTimer for Per-Iteration Setup
+
+```go
+func BenchmarkSort(b *testing.B) {
+    for i := 0; i < b.N; i++ {
+        b.StopTimer()
+        data := generateUnsortedSlice(1000)  // Re-create per iteration
+        b.StartTimer()
+
+        sort.Ints(data)
+    }
+}
+```
+
+### Allocations Matter — Report Them
+
+```go
+func BenchmarkParse(b *testing.B) {
+    b.ReportAllocs()  // Show allocs/op and B/op in output
+    for i := 0; i < b.N; i++ {
+        Parse(input)
+    }
+}
+```
+
+### Run Benchmarks
+
+```bash
+go test -bench=. -benchmem -count=5 ./...
+
+# Run only matching benchmarks
+go test -bench=BenchmarkProcess -benchmem -run=^$ ./pkg/processor
+
+# -run=^$ suppresses tests, runs only benchmarks
+```
+
+### Compare Results with benchstat
+
+```bash
+go install golang.org/x/perf/cmd/benchstat@latest
+
+# Capture two runs
+go test -bench=. -count=10 ./... > before.txt
+# Make your change
+go test -bench=. -count=10 ./... > after.txt
+
+benchstat before.txt after.txt
+```
+
+Output shows statistical significance: `p < 0.05` means the difference is likely real, not noise. Use `-count=10` or more for reliable statistics.
+
+---
+
+## Escape Analysis
+
+### Inspect Escape Decisions
+
+```bash
+go build -gcflags='-m' ./...         # Basic escape analysis
+go build -gcflags='-m=2' ./...       # Verbose (shows escape reason)
+go test -gcflags='-m' ./...          # On test files
+```
+
+### Understand Heap vs Stack
+
+Values escape to the heap when:
+
+- Their address is returned or stored in a longer-lived structure
+- They are assigned to an interface
+- They are too large for the stack (default stack starts at 8KB, goroutines grow as needed but large locals still escape)
+- The compiler cannot prove the lifetime is bounded
+
+```go
+// Stack allocated — does NOT escape
+func sumSquares(nums []int) int {
+    total := 0  // total stays on stack
+    for _, n := range nums {
+        total += n * n
+    }
+    return total
+}
+
+// Heap allocated — escapes because address is returned
+func newCounter() *int {
+    n := 0
+    return &n  // n escapes: "moved to heap: n"
+}
+
+// Interface assignment causes escape
+func logValue(v interface{}) {  // Passing int here allocates on heap
+    fmt.Println(v)
+}
+```
+
+### Reduce Allocations with Value Receivers
+
+```go
+// BAD: Pointer causes allocation when assigned to interface
+type Point struct{ X, Y float64 }
+func (p *Point) String() string { return fmt.Sprintf("(%f, %f)", p.X, p.Y) }
+
+// GOOD: Value receiver, may stay on stack
+func (p Point) String() string { return fmt.Sprintf("(%f, %f)", p.X, p.Y) }
+```
+
+---
+
+## Memory Optimization
+
+### Use sync.Pool for Frequently Allocated Short-Lived Objects
+
+```go
+var bufPool = sync.Pool{
+    New: func() interface{} {
+        return new(bytes.Buffer)
+    },
+}
+
+func formatMessage(data []byte) string {
+    buf := bufPool.Get().(*bytes.Buffer)
+    defer func() {
+        buf.Reset()
+        bufPool.Put(buf)
+    }()
+
+    buf.Write(data)
+    // ... format into buf
+    return buf.String()
+}
+```
+
+Pool objects may be collected by GC at any time. Never store state that must survive across GC cycles in a pool.
+
+### Pre-Allocate Slices
+
+```go
+// BAD: O(n) reallocations as slice grows
+var results []Result
+for _, item := range items {
+    results = append(results, process(item))
+}
+
+// GOOD: Single allocation
+results := make([]Result, 0, len(items))
+for _, item := range items {
+    results = append(results, process(item))
+}
+```
+
+### Reuse Slices Across Calls
+
+```go
+type Processor struct {
+    buf []byte  // Reused across calls
+}
+
+func (p *Processor) Process(input []byte) []byte {
+    p.buf = p.buf[:0]           // Reset length, keep capacity
+    p.buf = append(p.buf, input...)
+    // ... transform p.buf
+    return p.buf
+}
+```
+
+### Avoid Large Value Copies
+
+```go
+type LargeStruct struct {
+    Data [4096]byte
+    // ...
+}
+
+// BAD: Copies 4KB on every call
+func processLarge(s LargeStruct) { ... }
+
+// GOOD: Pass pointer
+func processLarge(s *LargeStruct) { ... }
+```
+
+---
+
+## String Performance
+
+### Use strings.Builder for Concatenation
+
+```go
+// BAD: Creates a new string on every iteration
+var result string
+for _, s := range parts {
+    result += s + ", "
+}
+
+// GOOD: Single allocation
+var sb strings.Builder
+sb.Grow(estimatedSize)  // Pre-grow if you know the size
+for _, s := range parts {
+    sb.WriteString(s)
+    sb.WriteString(", ")
+}
+result := sb.String()
+```
+
+### Convert Between []byte and string Without Allocation
+
+The standard `string(b)` and `[]byte(s)` conversions always allocate. For read-only access within a single goroutine, use `unsafe`:
+
+```go
+import "unsafe"
+
+// []byte to string — zero copy, safe only if you don't modify b afterward
+func bytesToString(b []byte) string {
+    return unsafe.String(unsafe.SliceData(b), len(b))
+}
+
+// string to []byte — zero copy, safe only for reads
+func stringToBytes(s string) []byte {
+    return unsafe.Slice(unsafe.StringData(s), len(s))
+}
+```
+
+These are valid as of Go 1.20. Do not use the older `*(*string)(unsafe.Pointer(&b))` pattern.
+
+### Avoid fmt.Sprintf for Simple Concatenation
+
+```go
+// BAD: Heap allocation, format parsing overhead
+key := fmt.Sprintf("%s:%d", prefix, id)
+
+// GOOD: strconv is faster for basic conversions
+key := prefix + ":" + strconv.Itoa(id)
+
+// GOOD: For multiple parts, strings.Join or Builder
+key := strings.Join([]string{prefix, strconv.Itoa(id)}, ":")
+```
+
+---
+
+## Struct Alignment
+
+The CPU reads memory in aligned chunks. Padding bytes are inserted to satisfy alignment requirements. Reordering fields from largest to smallest eliminates wasted bytes.
+
+```go
+// BAD: 24 bytes due to padding
+type BadLayout struct {
+    Active  bool    // 1 byte + 7 bytes padding
+    Count   int64   // 8 bytes
+    Flag    bool    // 1 byte + 7 bytes padding
+}
+
+// GOOD: 16 bytes, no padding
+type GoodLayout struct {
+    Count   int64   // 8 bytes
+    Active  bool    // 1 byte
+    Flag    bool    // 1 byte + 6 bytes padding (to align to 8)
+}
+```
+
+### Check Sizes and Padding
+
+```go
+import "unsafe"
+
+fmt.Println(unsafe.Sizeof(BadLayout{}))   // 24
+fmt.Println(unsafe.Sizeof(GoodLayout{}))  // 16
+```
+
+### Use fieldalignment to Find Problems Automatically
+
+```bash
+go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
+
+fieldalignment ./...         # Report structs with inefficient layout
+fieldalignment -fix ./...    # Rewrite fields automatically
+```
+
+### Cache Line Considerations for Concurrent Structs
+
+Fields accessed by different goroutines should be on separate cache lines (64 bytes) to prevent false sharing:
+
+```go
+type Counters struct {
+    reads  int64
+    _      [56]byte  // Pad to fill cache line
+    writes int64
+}
+```
+
+---
+
+## Map Performance
+
+### Pre-Size Maps
+
+```go
+// BAD: Map grows incrementally, triggering multiple rehashes
+m := make(map[string]int)
+for _, item := range items {
+    m[item.Key] = item.Value
+}
+
+// GOOD: Single allocation
+m := make(map[string]int, len(items))
+for _, item := range items {
+    m[item.Key] = item.Value
+}
+```
+
+### Use Switch for Small Key Sets
+
+For fewer than ~8 fixed string keys, a switch statement is faster than a map due to branch prediction and no hashing overhead:
+
+```go
+// Faster for small, known sets
+func httpMethodCode(method string) int {
+    switch method {
+    case "GET":    return 0
+    case "POST":   return 1
+    case "PUT":    return 2
+    case "DELETE": return 3
+    default:       return -1
+    }
+}
+```
+
+### Choose sync.Map vs Sharded Map
+
+`sync.Map` is optimized for two specific cases:
+1. Write-once, read-many (mostly reads after initial population)
+2. Many goroutines reading/writing disjoint keys
+
+For general concurrent access with frequent writes, a sharded map with per-shard mutexes outperforms `sync.Map`:
+
+```go
+const numShards = 256
+
+type ShardedMap struct {
+    shards [numShards]struct {
+        sync.RWMutex
+        m map[string]interface{}
+    }
+}
+
+func (sm *ShardedMap) shard(key string) int {
+    h := fnv.New32()
+    h.Write([]byte(key))
+    return int(h.Sum32()) % numShards
+}
+
+func (sm *ShardedMap) Get(key string) (interface{}, bool) {
+    s := &sm.shards[sm.shard(key)]
+    s.RLock()
+    v, ok := s.m[key]
+    s.RUnlock()
+    return v, ok
+}
+```
+
+---
+
+## I/O Performance
+
+### Always Wrap with bufio
+
+Unbuffered reads and writes issue a syscall for every call. Buffering batches them:
+
+```go
+// BAD: Syscall per line
+f, _ := os.Open("data.txt")
+scanner := bufio.NewScanner(f)  // This is already buffered — correct
+
+// BAD: Syscall per Write call
+f, _ := os.Create("out.txt")
+fmt.Fprintln(f, line)  // Goes through direct write
+
+// GOOD: Buffered writes
+f, _ := os.Create("out.txt")
+bw := bufio.NewWriterSize(f, 64*1024)  // 64KB buffer
+defer bw.Flush()
+fmt.Fprintln(bw, line)
+```
+
+### Use io.Copy for Efficient Transfers
+
+`io.Copy` uses a 32KB internal buffer and delegates to `sendfile(2)` or `splice(2)` when both sides support it (e.g., `*os.File` to `*net.TCPConn`):
+
+```go
+// Efficient file download with no intermediate allocation
+func serveFile(w http.ResponseWriter, path string) error {
+    f, err := os.Open(path)
+    if err != nil {
+        return err
+    }
+    defer f.Close()
+
+    _, err = io.Copy(w, f)
+    return err
+}
+```
+
+### Use io.Pipe for Producer-Consumer Pipelines
+
+```go
+pr, pw := io.Pipe()
+
+go func() {
+    defer pw.Close()
+    json.NewEncoder(pw).Encode(largeStruct)  // Streams without buffering whole JSON
+}()
+
+http.Post(url, "application/json", pr)
+```
+
+### Limit Reads to Avoid Memory Exhaustion
+
+```go
+const maxBodySize = 1 << 20  // 1MB
+
+r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
+if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+    http.Error(w, "request too large or invalid", http.StatusBadRequest)
+    return
+}
+```
+
+---
+
+## Inlining
+
+The compiler inlines small functions to eliminate call overhead. A function is inlined when its "cost" (an internal AST node count) stays below a threshold (~80 nodes).
+
+### Check What Gets Inlined
+
+```bash
+go build -gcflags='-m' ./...
+
+# Output includes:
+# ./pkg/math.go:12:6: can inline Add
+# ./pkg/handler.go:45:12: inlining call to Add
+# ./pkg/handler.go:60:5: cannot inline processLarge: function too complex
+```
+
+### Write Inlineable Functions
+
+```go
+// Inlineable: small, no closures, no defer
+func clamp(v, min, max int) int {
+    if v < min { return min }
+    if v > max { return max }
+    return v
+}
+
+// NOT inlineable: contains a closure
+func makeAdder(n int) func(int) int {
+    return func(x int) int { return x + n }
+}
+```
+
+### Prevent Inlining
+
+```go
+//go:noinline  // Force a function to never be inlined (useful for benchmarking)
+func expensiveOperation(data []byte) Result {
+    // ...
+}
+```
+
+Use `//go:noinline` in benchmarks when you want to measure the cost of a function call itself, or to prevent the compiler from optimizing away a call you want to measure.
+
+---
+
+## Common Performance Anti-Patterns
+
+### Reflection in Hot Paths
+
+Reflection bypasses type-system optimizations, performs map lookups, and allocates. Avoid in code called frequently:
+
+```go
+// BAD: reflect.ValueOf allocates, method lookup is slow
+func setField(obj interface{}, name string, value interface{}) {
+    v := reflect.ValueOf(obj).Elem()
+    v.FieldByName(name).Set(reflect.ValueOf(value))
+}
+
+// GOOD: Generated code or type switch
+func applyUpdate(u *User, field string, value interface{}) {
+    switch field {
+    case "Name":  u.Name = value.(string)
+    case "Email": u.Email = value.(string)
+    }
+}
+```
+
+### fmt.Sprintf in Hot Paths
+
+`fmt.Sprintf` parses a format string, uses reflection, and typically allocates:
+
+```go
+// BAD in hot path
+key := fmt.Sprintf("user:%d:session:%s", userID, sessionID)
+
+// GOOD: strconv + concatenation
+key := "user:" + strconv.FormatInt(userID, 10) + ":session:" + sessionID
+
+// GOOD for complex formatting: pre-build a template or use strings.Builder
+```
+
+### Unnecessary Allocations in Loops
+
+```go
+// BAD: Allocates a new map every iteration
+for _, item := range items {
+    m := map[string]int{"count": item.Count}
+    process(m)
+}
+
+// GOOD: Allocate once, reuse
+m := make(map[string]int, 1)
+for _, item := range items {
+    m["count"] = item.Count
+    process(m)
+    // Clear before next iteration if needed
+    for k := range m { delete(m, k) }
+}
+```
+
+### Goroutine Leak from Unclosed Channels
+
+```go
+// BAD: Goroutine blocked forever if consumer exits early
+func generate(nums ...int) <-chan int {
+    out := make(chan int)
+    go func() {
+        for _, n := range nums {
+            out <- n  // Blocks forever if nobody reads
+        }
+        close(out)
+    }()
+    return out
+}
+
+// GOOD: Use context for cancellation
+func generate(ctx context.Context, nums ...int) <-chan int {
+    out := make(chan int, len(nums))
+    go func() {
+        defer close(out)
+        for _, n := range nums {
+            select {
+            case out <- n:
+            case <-ctx.Done():
+                return
+            }
+        }
+    }()
+    return out
+}
+```
+
+### Copying a Mutex
+
+Mutexes must not be copied after first use. Copying a locked mutex will deadlock; copying an unlocked mutex silently creates a new, independent lock:
+
+```go
+// BAD: Copies the mutex
+type Cache struct{ mu sync.Mutex; data map[string]int }
+func copyCache(c Cache) Cache { return c }  // Copies mu — wrong
+
+// GOOD: Always pass and return pointers for types containing mutexes
+func processCache(c *Cache) { ... }
+```
+
+### Defer in a Tight Loop
+
+Defers execute at function return, not loop iteration. Inside a loop, defers pile up and all run together at the end:
+
+```go
+// BAD: All files stay open until the function returns
+for _, path := range paths {
+    f, _ := os.Open(path)
+    defer f.Close()  // Runs at function exit, not loop end
+    process(f)
+}
+
+// GOOD: Wrap in a closure or extract to a helper
+for _, path := range paths {
+    func() {
+        f, _ := os.Open(path)
+        defer f.Close()  // Now runs at end of this closure
+        process(f)
+    }()
+}
+```

+ 548 - 0
skills/go-ops/references/project-structure.md

@@ -0,0 +1,548 @@
+# Go Project Structure Reference
+
+## Table of Contents
+
+1. [Standard Project Layout](#standard-project-layout)
+2. [Module Management](#module-management)
+3. [Workspace Mode](#workspace-mode)
+4. [Build Tags](#build-tags)
+5. [Build Configuration](#build-configuration)
+6. [Makefile and Justfile Patterns](#makefile-and-justfile-patterns)
+7. [Linting](#linting)
+8. [Code Generation](#code-generation)
+9. [Release](#release)
+
+---
+
+## Standard Project Layout
+
+The Go community has converged on a layout that separates public, private, and executable code clearly.
+
+```
+myapp/
+├── cmd/                    # Executable entry points (one dir per binary)
+│   ├── server/
+│   │   └── main.go
+│   └── worker/
+│       └── main.go
+├── internal/               # Private packages (import-restricted by go toolchain)
+│   ├── config/
+│   │   └── config.go
+│   ├── handler/
+│   │   └── user.go
+│   ├── service/
+│   │   └── user.go
+│   └── repository/
+│       └── user.go
+├── pkg/                    # Public packages (importable by external projects)
+│   └── validator/
+│       └── validator.go
+├── api/                    # API definitions (OpenAPI, protobuf, gRPC)
+│   └── openapi.yaml
+├── web/                    # Web assets, templates
+├── scripts/                # Build, install, CI scripts
+├── configs/                # Config file templates
+├── testdata/               # Test fixtures (go tools ignore dirs starting with "testdata")
+├── go.mod
+├── go.sum
+├── Makefile (or justfile)
+└── README.md
+```
+
+### When to Use Each Directory
+
+**cmd/**: Place `main.go` files here. Each subdirectory is a separate binary. Keep main.go thin — parse flags, load config, wire dependencies, then call into `internal/`.
+
+**internal/**: Use for everything application-specific. The Go toolchain enforces that packages under `internal/` can only be imported by code in the parent directory tree. Use this for business logic, handlers, database access.
+
+**pkg/**: Only create this if you genuinely want external projects to import your code. Most applications do not need `pkg/` at all. Avoid the anti-pattern of putting everything in `pkg/` just to follow the template.
+
+```go
+// cmd/server/main.go — wire dependencies here, logic lives in internal/
+func main() {
+    cfg, err := config.Load()
+    if err != nil {
+        log.Fatalf("loading config: %v", err)
+    }
+
+    db, err := database.Connect(cfg.DatabaseURL)
+    if err != nil {
+        log.Fatalf("connecting to database: %v", err)
+    }
+
+    repo := repository.NewUser(db)
+    svc := service.NewUser(repo)
+    h := handler.NewUser(svc)
+
+    srv := &http.Server{Addr: cfg.Addr, Handler: h.Routes()}
+    log.Fatal(srv.ListenAndServe())
+}
+```
+
+---
+
+## Module Management
+
+### go.mod Directives
+
+```
+module github.com/myorg/myapp
+
+go 1.22
+
+require (
+    github.com/lib/pq v1.10.9
+    golang.org/x/sync v0.6.0
+)
+
+// Replace a dependency with a local version during development
+replace github.com/myorg/shared => ../shared
+
+// Exclude a specific broken version
+exclude github.com/bad/module v1.2.3
+```
+
+**require**: Direct and indirect dependencies. The `// indirect` comment marks transitive dependencies that aren't directly imported by your code but are required by your dependencies.
+
+**replace**: Use for local development of shared modules, or to patch a dependency without forking. Remove before merging to main — `replace` directives break downstream consumers.
+
+**exclude**: Prevents a specific version from being selected by MVS. Useful when a version has a known bug and you want to force a later version.
+
+### Manage go.sum
+
+`go.sum` contains the expected cryptographic checksums of module content. Commit it to version control. Never edit it manually. Regenerate with:
+
+```bash
+go mod tidy        # Add missing, remove unused dependencies
+go mod verify      # Verify checksums against go.sum
+go mod download    # Pre-download modules (useful in Docker layers)
+```
+
+### Private Modules
+
+Configure the Go toolchain to skip the public checksum database and proxy for private code:
+
+```bash
+# Tell go to bypass proxy and sumdb for private modules
+export GOPRIVATE=github.com/myorg/*
+
+# Separate controls for proxy and sumdb
+export GONOSUMCHECK=github.com/myorg/*
+export GONOPROXY=github.com/myorg/*
+
+# Use a corporate proxy for public modules
+export GOPROXY=https://proxy.company.com,direct
+```
+
+In CI, set these as environment variables. For `.netrc`-based auth with private GitHub:
+
+```
+machine github.com login git password <personal-access-token>
+```
+
+---
+
+## Workspace Mode
+
+Workspaces allow multiple modules to be developed together without `replace` directives.
+
+```bash
+go work init ./app ./shared ./tools   # Creates go.work
+go work use ./new-module              # Add another module
+go work sync                          # Sync dependencies
+```
+
+**go.work file:**
+
+```
+go 1.22
+
+use (
+    ./app
+    ./shared
+    ./tools
+)
+```
+
+### When Workspaces Help
+
+- Developing two modules simultaneously (e.g., a library and a consuming app)
+- Monorepo with multiple Go modules
+- Testing unreleased changes to a shared package before publishing
+
+### When Workspaces Do Not Help
+
+- Single-module repos (no benefit)
+- Production builds — exclude `go.work` from Docker contexts with `.dockerignore`
+
+```
+# .dockerignore
+go.work
+go.work.sum
+```
+
+---
+
+## Build Tags
+
+Build tags control which files are included in a build. The modern syntax uses `//go:build`.
+
+```go
+//go:build integration
+
+package mypackage
+```
+
+```go
+//go:build linux && amd64
+
+package mypackage
+```
+
+```go
+//go:build !windows
+
+package mypackage
+```
+
+### Common Tag Patterns
+
+```go
+//go:build ignore          // Exclude from normal builds (e.g., generation scripts)
+
+//go:build integration     // Integration tests requiring real external services
+
+//go:build e2e             // End-to-end tests
+
+//go:build cgo             // Only build when CGO is enabled
+```
+
+### Run Builds with Tags
+
+```bash
+go test -tags integration ./...
+go build -tags production ./cmd/server
+go vet -tags integration ./...
+```
+
+### Separate Integration Tests
+
+```go
+//go:build integration
+
+package repository_test
+
+import (
+    "testing"
+    "os"
+)
+
+func TestUserRepository_Integration(t *testing.T) {
+    dsn := os.Getenv("TEST_DATABASE_URL")
+    if dsn == "" {
+        t.Skip("TEST_DATABASE_URL not set")
+    }
+    // ... test against real database
+}
+```
+
+---
+
+## Build Configuration
+
+### Inject Version Information at Build Time
+
+```go
+// internal/version/version.go
+package version
+
+var (
+    Version   = "dev"
+    GitCommit = "unknown"
+    BuildDate = "unknown"
+)
+```
+
+```bash
+go build \
+  -ldflags="-X github.com/myorg/myapp/internal/version.Version=1.2.3 \
+             -X github.com/myorg/myapp/internal/version.GitCommit=$(git rev-parse --short HEAD) \
+             -X github.com/myorg/myapp/internal/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+  ./cmd/server
+```
+
+### Build Static Binaries
+
+```bash
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
+  -ldflags="-s -w" \
+  -trimpath \
+  -o bin/server \
+  ./cmd/server
+```
+
+- `CGO_ENABLED=0`: Disable cgo, produce a statically linked binary
+- `-s -w`: Strip debug info and DWARF symbols (reduces binary size ~30%)
+- `-trimpath`: Remove local file paths from the binary (reproducible builds, avoids leaking local paths)
+
+### Cross-Compile
+
+```bash
+GOOS=windows GOARCH=amd64 go build ./cmd/server
+GOOS=darwin  GOARCH=arm64 go build ./cmd/server
+GOOS=linux   GOARCH=arm64 go build ./cmd/server
+```
+
+---
+
+## Makefile and Justfile Patterns
+
+### Makefile
+
+```makefile
+BINARY     := bin/server
+VERSION    := $(shell git describe --tags --always --dirty)
+COMMIT     := $(shell git rev-parse --short HEAD)
+BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
+LDFLAGS    := -X main.version=$(VERSION) -X main.commit=$(COMMIT)
+
+.PHONY: build test lint generate clean docker
+
+build:
+	CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -trimpath -o $(BINARY) ./cmd/server
+
+test:
+	go test -race -coverprofile=coverage.out ./...
+	go tool cover -html=coverage.out -o coverage.html
+
+test-integration:
+	go test -race -tags integration ./...
+
+lint:
+	golangci-lint run ./...
+
+generate:
+	go generate ./...
+
+clean:
+	rm -rf bin/ coverage.out coverage.html
+
+docker:
+	docker build --build-arg VERSION=$(VERSION) -t myapp:$(VERSION) .
+
+tidy:
+	go mod tidy
+	go mod verify
+```
+
+### Justfile
+
+```just
+version := `git describe --tags --always --dirty`
+commit  := `git rev-parse --short HEAD`
+
+build:
+    CGO_ENABLED=0 go build \
+        -ldflags="-X main.version={{version}} -X main.commit={{commit}}" \
+        -trimpath -o bin/server ./cmd/server
+
+test:
+    go test -race -coverprofile=coverage.out ./...
+
+test-integration:
+    go test -race -tags integration ./...
+
+lint:
+    golangci-lint run ./...
+
+generate:
+    go generate ./...
+
+tidy:
+    go mod tidy && go mod verify
+```
+
+---
+
+## Linting
+
+### Install and Run golangci-lint
+
+```bash
+# Install (do not use go install — use the official installer)
+curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \
+  | sh -s -- -b $(go env GOPATH)/bin v1.57.2
+
+golangci-lint run ./...
+golangci-lint run --fix ./...    # Auto-fix where possible
+```
+
+### Recommended .golangci.yml
+
+```yaml
+linters:
+  enable:
+    - errcheck        # Check all error returns are handled
+    - gosimple        # Simplification suggestions
+    - govet           # go vet checks
+    - ineffassign     # Detect unused variable assignments
+    - staticcheck     # Comprehensive static analysis
+    - unused          # Detect unused code
+    - gofmt           # Enforce gofmt formatting
+    - goimports       # Enforce import grouping
+    - gocritic        # Opinionated style checks
+    - misspell        # Catch common misspellings
+    - prealloc        # Suggest slice pre-allocation
+    - exhaustive      # Enforce exhaustive enum switches
+    - noctx           # Detect HTTP requests without context
+
+linters-settings:
+  errcheck:
+    check-blank: true
+  govet:
+    enable-all: true
+  gocritic:
+    enabled-tags: [diagnostic, style, performance]
+
+issues:
+  exclude-rules:
+    - path: _test\.go
+      linters: [errcheck]   # Relax error checking in tests
+```
+
+### Suppress Specific Warnings
+
+```go
+//nolint:errcheck  // Intentionally ignoring close error on best-effort cleanup
+defer f.Close()
+
+//nolint:exhaustive  // Default case handles unrecognized values
+switch status {
+case Active:
+    return "active"
+default:
+    return "unknown"
+}
+```
+
+---
+
+## Code Generation
+
+### go generate
+
+Place `//go:generate` directives in the file where the generated output belongs conceptually.
+
+```go
+// internal/domain/status.go
+//go:generate stringer -type=Status
+
+type Status int
+
+const (
+    Active Status = iota
+    Inactive
+    Pending
+)
+```
+
+```go
+// internal/repository/mock_store.go (or a dedicated mocks/ dir)
+//go:generate mockgen -source=store.go -destination=mock_store.go -package=repository
+```
+
+Run all generators:
+
+```bash
+go generate ./...
+```
+
+### Embed Static Files
+
+```go
+import "embed"
+
+//go:embed templates/*.html
+var templateFS embed.FS
+
+//go:embed migrations
+var migrationsFS embed.FS
+
+//go:embed static/app.js static/app.css
+var staticFiles embed.FS
+```
+
+- Paths are relative to the file containing the directive
+- Supports glob patterns and directories
+- Embedded files are read-only at runtime
+
+---
+
+## Release
+
+### goreleaser
+
+```yaml
+# .goreleaser.yml
+project_name: myapp
+
+builds:
+  - id: server
+    main: ./cmd/server
+    binary: server
+    env:
+      - CGO_ENABLED=0
+    goos: [linux, darwin, windows]
+    goarch: [amd64, arm64]
+    ldflags:
+      - -s -w
+      - -X main.version={{.Version}}
+      - -X main.commit={{.Commit}}
+      - -X main.date={{.Date}}
+    flags:
+      - -trimpath
+
+archives:
+  - format: tar.gz
+    format_overrides:
+      - goos: windows
+        format: zip
+
+checksum:
+  name_template: "checksums.txt"
+
+changelog:
+  sort: asc
+  filters:
+    exclude: ['^docs:', '^test:', Merge pull request]
+```
+
+```bash
+# Dry run to verify configuration
+goreleaser release --snapshot --clean
+
+# Publish a real release (requires GITHUB_TOKEN)
+goreleaser release --clean
+```
+
+### Manual Cross-Compile Script
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+
+VERSION=$(git describe --tags --always)
+PLATFORMS=("linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64" "windows/amd64")
+
+for platform in "${PLATFORMS[@]}"; do
+    GOOS="${platform%/*}"
+    GOARCH="${platform#*/}"
+    output="dist/server_${GOOS}_${GOARCH}"
+    [[ "$GOOS" == "windows" ]] && output="${output}.exe"
+
+    echo "Building $output"
+    CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build \
+        -ldflags="-s -w -X main.version=${VERSION}" \
+        -trimpath -o "$output" ./cmd/server
+done
+```

+ 718 - 0
skills/go-ops/references/testing.md

@@ -0,0 +1,718 @@
+# Go Testing Reference
+
+## Table of Contents
+
+1. [Table-Driven Tests](#1-table-driven-tests)
+2. [Test Helpers](#2-test-helpers)
+3. [Mocking with Interfaces](#3-mocking-with-interfaces)
+4. [testify](#4-testify)
+5. [httptest](#5-httptest)
+6. [Benchmarks](#6-benchmarks)
+7. [Fuzz Testing](#7-fuzz-testing)
+8. [Integration Tests](#8-integration-tests)
+9. [Golden Files](#9-golden-files)
+10. [Test Fixtures and TestMain](#10-test-fixtures-and-testmain)
+11. [Race Detection](#11-race-detection)
+12. [Coverage](#12-coverage)
+
+---
+
+## 1. Table-Driven Tests
+
+Write tests as a slice of structs. Name the slice `tests` and each element `tt`. Run each with `t.Run`.
+
+```go
+func TestDivide(t *testing.T) {
+    t.Parallel()
+
+    tests := []struct {
+        name      string
+        dividend  float64
+        divisor   float64
+        want      float64
+        wantErr   bool
+    }{
+        {name: "positive", dividend: 10, divisor: 2, want: 5},
+        {name: "negative divisor", dividend: 10, divisor: -2, want: -5},
+        {name: "fractional result", dividend: 7, divisor: 2, want: 3.5},
+        {name: "zero divisor", dividend: 10, divisor: 0, wantErr: true},
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            t.Parallel() // run subtests in parallel when safe
+
+            got, err := Divide(tt.dividend, tt.divisor)
+
+            if tt.wantErr {
+                if err == nil {
+                    t.Fatal("expected error, got nil")
+                }
+                return
+            }
+
+            if err != nil {
+                t.Fatalf("unexpected error: %v", err)
+            }
+            if got != tt.want {
+                t.Errorf("Divide(%v, %v) = %v, want %v",
+                    tt.dividend, tt.divisor, got, tt.want)
+            }
+        })
+    }
+}
+```
+
+Use `t.Fatal` when further execution is meaningless. Use `t.Error` to accumulate multiple failures. Capture loop variables before `t.Parallel()` in Go versions before 1.22 (Go 1.22+ fixes loop variable capture automatically).
+
+---
+
+## 2. Test Helpers
+
+### t.Helper
+
+Mark helper functions with `t.Helper()` so failures report the caller's line, not the helper's.
+
+```go
+func assertNoError(t *testing.T, err error) {
+    t.Helper()
+    if err != nil {
+        t.Fatalf("unexpected error: %v", err)
+    }
+}
+
+func assertEqual[T comparable](t *testing.T, got, want T) {
+    t.Helper()
+    if got != want {
+        t.Errorf("got %v, want %v", got, want)
+    }
+}
+```
+
+### t.Cleanup
+
+Register cleanup functions that run even if the test panics or calls `t.Fatal`.
+
+```go
+func newTestDB(t *testing.T) *sql.DB {
+    t.Helper()
+
+    db, err := sql.Open("sqlite3", ":memory:")
+    if err != nil {
+        t.Fatalf("opening db: %v", err)
+    }
+
+    t.Cleanup(func() {
+        if err := db.Close(); err != nil {
+            t.Errorf("closing db: %v", err)
+        }
+    })
+
+    return db
+}
+```
+
+### t.TempDir
+
+Use `t.TempDir()` instead of `os.MkdirTemp`. It is automatically removed after the test.
+
+```go
+func TestWriteFile(t *testing.T) {
+    dir := t.TempDir() // cleaned up automatically
+
+    path := filepath.Join(dir, "output.txt")
+    err := WriteFile(path, []byte("hello"))
+    if err != nil {
+        t.Fatal(err)
+    }
+
+    got, err := os.ReadFile(path)
+    if err != nil {
+        t.Fatal(err)
+    }
+    if string(got) != "hello" {
+        t.Errorf("got %q, want %q", got, "hello")
+    }
+}
+```
+
+### testdata Directory
+
+Place static input files in `testdata/`. The Go tool ignores this directory for builds. Reference files relative to the package root using `filepath.Join("testdata", "input.json")`.
+
+```go
+func TestParseConfig(t *testing.T) {
+    data, err := os.ReadFile(filepath.Join("testdata", "config.json"))
+    if err != nil {
+        t.Fatal(err)
+    }
+
+    cfg, err := ParseConfig(data)
+    if err != nil {
+        t.Fatalf("ParseConfig: %v", err)
+    }
+    if cfg.Port != 8080 {
+        t.Errorf("got port %d, want 8080", cfg.Port)
+    }
+}
+```
+
+---
+
+## 3. Mocking with Interfaces
+
+Define narrow interfaces at the point of use, not in the package that implements them.
+
+```go
+// Define the interface (in the consumer's package)
+type UserStore interface {
+    GetUser(ctx context.Context, id int64) (*User, error)
+    SaveUser(ctx context.Context, u *User) error
+}
+
+// Hand-rolled mock (no external dependencies)
+type mockUserStore struct {
+    getUser  func(ctx context.Context, id int64) (*User, error)
+    saveUser func(ctx context.Context, u *User) error
+    calls    []string
+}
+
+func (m *mockUserStore) GetUser(ctx context.Context, id int64) (*User, error) {
+    m.calls = append(m.calls, "GetUser")
+    if m.getUser != nil {
+        return m.getUser(ctx, id)
+    }
+    return nil, nil
+}
+
+func (m *mockUserStore) SaveUser(ctx context.Context, u *User) error {
+    m.calls = append(m.calls, "SaveUser")
+    if m.saveUser != nil {
+        return m.saveUser(ctx, u)
+    }
+    return nil
+}
+
+// Test using the mock
+func TestUserService_Promote(t *testing.T) {
+    store := &mockUserStore{
+        getUser: func(_ context.Context, id int64) (*User, error) {
+            return &User{ID: id, Role: "member"}, nil
+        },
+        saveUser: func(_ context.Context, u *User) error {
+            if u.Role != "admin" {
+                return fmt.Errorf("expected role admin, got %s", u.Role)
+            }
+            return nil
+        },
+    }
+
+    svc := NewUserService(store)
+    err := svc.Promote(context.Background(), 42)
+    if err != nil {
+        t.Fatalf("Promote: %v", err)
+    }
+    if len(store.calls) != 2 {
+        t.Errorf("expected 2 calls, got %d: %v", len(store.calls), store.calls)
+    }
+}
+```
+
+---
+
+## 4. testify
+
+Install: `go get github.com/stretchr/testify`.
+
+### assert vs require
+
+`assert` logs failure and continues. `require` stops the test immediately (calls `t.FailNow`).
+
+```go
+import (
+    "testing"
+    "github.com/stretchr/testify/assert"
+    "github.com/stretchr/testify/require"
+)
+
+func TestUserCreation(t *testing.T) {
+    user, err := NewUser("alice@example.com")
+
+    require.NoError(t, err)            // stop if error
+    require.NotNil(t, user)            // stop if nil
+
+    assert.Equal(t, "alice@example.com", user.Email)
+    assert.Empty(t, user.PasswordHash) // multiple checks continue on failure
+    assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second)
+}
+```
+
+### testify/suite
+
+Group related tests with shared setup/teardown.
+
+```go
+import "github.com/stretchr/testify/suite"
+
+type UserSuite struct {
+    suite.Suite
+    db  *sql.DB
+    svc *UserService
+}
+
+func (s *UserSuite) SetupSuite() {
+    db, err := sql.Open("sqlite3", ":memory:")
+    s.Require().NoError(err)
+    s.db = db
+    s.svc = NewUserService(db)
+}
+
+func (s *UserSuite) TearDownSuite() {
+    s.db.Close()
+}
+
+func (s *UserSuite) SetupTest() {
+    _, err := s.db.Exec("DELETE FROM users")
+    s.Require().NoError(err)
+}
+
+func (s *UserSuite) TestCreate() {
+    u, err := s.svc.Create(context.Background(), "bob@example.com")
+    s.Require().NoError(err)
+    s.Equal("bob@example.com", u.Email)
+}
+
+func TestUserSuite(t *testing.T) {
+    suite.Run(t, new(UserSuite))
+}
+```
+
+### testify/mock
+
+Use `mock.Mock` for dynamic expectations with call counting.
+
+```go
+import "github.com/stretchr/testify/mock"
+
+type MockStore struct {
+    mock.Mock
+}
+
+func (m *MockStore) GetUser(ctx context.Context, id int64) (*User, error) {
+    args := m.Called(ctx, id)
+    return args.Get(0).(*User), args.Error(1)
+}
+
+func TestWithMock(t *testing.T) {
+    store := new(MockStore)
+    store.On("GetUser", mock.Anything, int64(1)).
+        Return(&User{ID: 1, Name: "Alice"}, nil)
+
+    svc := NewUserService(store)
+    user, err := svc.GetUser(context.Background(), 1)
+
+    require.NoError(t, err)
+    assert.Equal(t, "Alice", user.Name)
+    store.AssertExpectations(t)
+}
+```
+
+---
+
+## 5. httptest
+
+### Test HTTP Handlers Directly
+
+```go
+import "net/http/httptest"
+
+func TestGetUserHandler(t *testing.T) {
+    store := &mockUserStore{
+        getUser: func(_ context.Context, id int64) (*User, error) {
+            return &User{ID: id, Name: "Alice"}, nil
+        },
+    }
+    h := NewHandler(store)
+
+    req := httptest.NewRequest(http.MethodGet, "/users/1", nil)
+    w := httptest.NewRecorder()
+
+    h.ServeHTTP(w, req)
+
+    resp := w.Result()
+    defer resp.Body.Close()
+
+    if resp.StatusCode != http.StatusOK {
+        t.Fatalf("status %d, want 200", resp.StatusCode)
+    }
+
+    var got User
+    if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
+        t.Fatalf("decoding response: %v", err)
+    }
+    if got.Name != "Alice" {
+        t.Errorf("name %q, want Alice", got.Name)
+    }
+}
+```
+
+### Test HTTP Clients Against a Real Server
+
+```go
+func TestAPIClient(t *testing.T) {
+    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        if r.URL.Path != "/v1/users/42" {
+            t.Errorf("unexpected path: %s", r.URL.Path)
+        }
+        w.Header().Set("Content-Type", "application/json")
+        fmt.Fprintln(w, `{"id":42,"name":"Bob"}`)
+    }))
+    defer srv.Close()
+
+    client := NewAPIClient(srv.URL)
+    user, err := client.GetUser(context.Background(), 42)
+
+    if err != nil {
+        t.Fatalf("GetUser: %v", err)
+    }
+    if user.Name != "Bob" {
+        t.Errorf("name %q, want Bob", user.Name)
+    }
+}
+
+// TLS variant
+func TestAPIClientTLS(t *testing.T) {
+    srv := httptest.NewTLSServer(myHandler)
+    defer srv.Close()
+
+    client := srv.Client() // pre-configured to trust the test certificate
+    resp, err := client.Get(srv.URL + "/health")
+    // ...
+}
+```
+
+---
+
+## 6. Benchmarks
+
+Functions named `BenchmarkXxx` receive `*testing.B`. Run with `go test -bench=. -benchmem`.
+
+```go
+func BenchmarkEncode(b *testing.B) {
+    user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}
+
+    b.ReportAllocs()  // show allocations per op
+    b.ResetTimer()    // exclude setup time
+
+    for b.Loop() {    // Go 1.24+; use i := 0; i < b.N; i++ for older versions
+        if _, err := json.Marshal(user); err != nil {
+            b.Fatal(err)
+        }
+    }
+}
+
+// Sub-benchmarks compare implementations
+func BenchmarkEncoding(b *testing.B) {
+    user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}
+
+    b.Run("json/stdlib", func(b *testing.B) {
+        for b.Loop() {
+            json.Marshal(user)
+        }
+    })
+
+    b.Run("json/sonic", func(b *testing.B) {
+        for b.Loop() {
+            sonic.Marshal(user)
+        }
+    })
+}
+
+// Parallel benchmark
+func BenchmarkEncodeParallel(b *testing.B) {
+    user := &User{ID: 1, Name: "Alice"}
+
+    b.RunParallel(func(pb *testing.PB) {
+        for pb.Next() {
+            json.Marshal(user)
+        }
+    })
+}
+```
+
+Run and compare: `go test -bench=BenchmarkEncoding -benchmem -count=5 | tee new.txt && benchstat old.txt new.txt`.
+
+---
+
+## 7. Fuzz Testing
+
+Fuzz tests find inputs that crash your code. Run normally as unit tests; enable fuzzing with `-fuzz`.
+
+```go
+func FuzzParseURL(f *testing.F) {
+    // Seed the corpus with known-good inputs
+    f.Add("https://example.com/path?q=1")
+    f.Add("http://localhost:8080")
+    f.Add("")
+    f.Add("not-a-url")
+
+    f.Fuzz(func(t *testing.T, raw string) {
+        // Must not panic
+        u, err := ParseURL(raw)
+        if err != nil {
+            return // errors are acceptable
+        }
+
+        // Round-trip property: re-parsing the output must succeed
+        reparsed, err := ParseURL(u.String())
+        if err != nil {
+            t.Errorf("round-trip failed for %q: %v", u.String(), err)
+        }
+        if reparsed.String() != u.String() {
+            t.Errorf("round-trip mismatch: %q != %q", reparsed.String(), u.String())
+        }
+    })
+}
+```
+
+Run fuzzing: `go test -fuzz=FuzzParseURL -fuzztime=30s`. Failing inputs are saved to `testdata/fuzz/FuzzParseURL/`. Reproduce: `go test -run=FuzzParseURL/testdata/fuzz/FuzzParseURL/<id>`.
+
+---
+
+## 8. Integration Tests
+
+### Build Tags
+
+Guard integration tests with a build tag so `go test ./...` skips them by default.
+
+```go
+//go:build integration
+
+package store_test
+
+import (
+    "testing"
+    // ...
+)
+
+func TestPostgresUserStore(t *testing.T) {
+    dsn := os.Getenv("TEST_DSN")
+    if dsn == "" {
+        t.Skip("TEST_DSN not set")
+    }
+    // ...
+}
+```
+
+Run: `go test -tags integration ./...`
+
+### testcontainers-go
+
+Spin up real databases in Docker for integration tests.
+
+```go
+//go:build integration
+
+func TestWithPostgres(t *testing.T) {
+    ctx := context.Background()
+
+    container, err := postgres.RunContainer(ctx,
+        testcontainers.WithImage("postgres:16"),
+        postgres.WithDatabase("testdb"),
+        postgres.WithUsername("test"),
+        postgres.WithPassword("test"),
+        testcontainers.WithWaitStrategy(
+            wait.ForLog("database system is ready to accept connections").
+                WithOccurrence(2)),
+    )
+    if err != nil {
+        t.Fatalf("starting postgres: %v", err)
+    }
+    t.Cleanup(func() { container.Terminate(ctx) })
+
+    dsn, err := container.ConnectionString(ctx, "sslmode=disable")
+    if err != nil {
+        t.Fatal(err)
+    }
+
+    db, err := sql.Open("postgres", dsn)
+    if err != nil {
+        t.Fatal(err)
+    }
+    t.Cleanup(func() { db.Close() })
+
+    // Run migrations, then test
+    runMigrations(t, db)
+    store := NewPostgresStore(db)
+    // ... test store methods
+}
+```
+
+---
+
+## 9. Golden Files
+
+Golden files store expected output. Re-generate them with `-update`.
+
+```go
+var update = flag.Bool("update", false, "update golden files")
+
+func TestRenderMarkdown(t *testing.T) {
+    input, err := os.ReadFile(filepath.Join("testdata", "input.md"))
+    if err != nil {
+        t.Fatal(err)
+    }
+
+    got := RenderMarkdown(input)
+
+    golden := filepath.Join("testdata", "golden", "output.html")
+
+    if *update {
+        err := os.WriteFile(golden, got, 0644)
+        if err != nil {
+            t.Fatal(err)
+        }
+        return
+    }
+
+    want, err := os.ReadFile(golden)
+    if err != nil {
+        t.Fatal(err)
+    }
+
+    if !bytes.Equal(got, want) {
+        t.Errorf("output mismatch (-want +got):\n%s",
+            cmp.Diff(string(want), string(got)))
+    }
+}
+```
+
+Run `go test -run=TestRenderMarkdown -update` to regenerate, then commit the golden files.
+
+---
+
+## 10. Test Fixtures and TestMain
+
+### TestMain for Global Setup
+
+```go
+func TestMain(m *testing.M) {
+    // Setup: runs once before any test
+    db, err := setupTestDatabase()
+    if err != nil {
+        fmt.Fprintf(os.Stderr, "setup: %v\n", err)
+        os.Exit(1)
+    }
+    globalDB = db
+
+    // Run tests
+    code := m.Run()
+
+    // Teardown: runs once after all tests
+    db.Close()
+    os.Exit(code)
+}
+```
+
+### Per-Test Setup with t.Cleanup
+
+Prefer `t.Cleanup` over `defer` in test helpers; it composes across multiple helpers cleanly.
+
+```go
+func prepareUser(t *testing.T, db *sql.DB, email string) *User {
+    t.Helper()
+
+    u, err := db.CreateUser(context.Background(), email)
+    if err != nil {
+        t.Fatalf("creating user: %v", err)
+    }
+
+    t.Cleanup(func() {
+        if err := db.DeleteUser(context.Background(), u.ID); err != nil {
+            t.Logf("cleanup: deleting user %d: %v", u.ID, err)
+        }
+    })
+
+    return u
+}
+```
+
+---
+
+## 11. Race Detection
+
+Enable with `go test -race ./...`. The race detector adds ~5-10x overhead; use it in CI.
+
+### Common Race: Shared State in Goroutines
+
+```go
+// RACE: multiple goroutines write to results without synchronization
+func badCollect(items []Item) []Result {
+    results := make([]Result, 0, len(items))
+    var wg sync.WaitGroup
+    for _, item := range items {
+        wg.Add(1)
+        go func(it Item) {
+            defer wg.Done()
+            results = append(results, process(it)) // DATA RACE
+        }(item)
+    }
+    wg.Wait()
+    return results
+}
+
+// FIXED: preallocate by index
+func goodCollect(items []Item) []Result {
+    results := make([]Result, len(items))
+    var wg sync.WaitGroup
+    for i, item := range items {
+        wg.Add(1)
+        go func(i int, it Item) {
+            defer wg.Done()
+            results[i] = process(it) // safe: each goroutine owns its index
+        }(i, item)
+    }
+    wg.Wait()
+    return results
+}
+```
+
+### Common Race: Closing Over Loop Variables (pre-Go 1.22)
+
+```go
+// RACE in Go < 1.22
+for _, url := range urls {
+    go func() {
+        fetch(url) // captures loop variable by reference
+    }()
+}
+
+// FIXED
+for _, url := range urls {
+    url := url // shadow with local copy
+    go func() {
+        fetch(url)
+    }()
+}
+```
+
+---
+
+## 12. Coverage
+
+```bash
+# Generate coverage profile
+go test -coverprofile=coverage.out ./...
+
+# View summary by package
+go tool cover -func=coverage.out
+
+# Open interactive HTML report
+go tool cover -html=coverage.out
+
+# Enforce a minimum threshold in CI
+go test -coverprofile=coverage.out ./...
+go tool cover -func=coverage.out | awk '/^total:/ {pct=$3+0; if (pct < 80) {print "coverage "$pct"% below 80%"; exit 1}}'
+```
+
+Target 80% coverage for business logic. Avoid chasing 100%: generated code, main functions, and deliberate error paths that only trigger under hardware failure are not worth testing directly.

+ 0 - 0
skills/go-ops/scripts/.gitkeep


+ 332 - 0
skills/rust-ops/SKILL.md

@@ -0,0 +1,332 @@
+---
+name: rust-ops
+description: "Rust development patterns, ownership, async, error handling, and ecosystem. Use for: rust, cargo, ownership, borrow checker, lifetime, tokio, serde, trait, Result, Option, async rust, crate, derive, impl, enum, pattern matching, Arc, Mutex, Send, Sync, thiserror, anyhow, clap, axum, sqlx, reqwest, rayon, tracing."
+allowed-tools: "Read Write Bash"
+related-skills: [docker-ops, ci-cd-ops, testing-ops]
+---
+
+# Rust Operations
+
+Comprehensive Rust skill covering ownership, async, error handling, and the production ecosystem.
+
+## Ownership Quick Reference
+
+```
+Who owns the value?
+│
+├─ Need to transfer ownership
+│  └─ Move: let s2 = s1;  (s1 is invalid after this)
+│
+├─ Need to read without owning
+│  └─ Shared borrow: &T (multiple allowed, no mutation)
+│
+├─ Need to mutate without owning
+│  └─ Exclusive borrow: &mut T (only one, no other borrows)
+│
+├─ Need to share ownership across threads
+│  └─ Arc<T> (atomic reference counting)
+│     └─ Need mutation too? Arc<Mutex<T>>
+│
+├─ Need to share ownership single-threaded
+│  └─ Rc<T> (reference counting, not Send)
+│     └─ Need mutation too? Rc<RefCell<T>>
+│
+└─ Need to avoid cloning large data
+   └─ Cow<'a, T> (clone-on-write, borrows when possible)
+```
+
+### The Borrow Rules
+
+1. At any time, you can have **either** one `&mut T` **or** any number of `&T`
+2. References must always be valid (no dangling)
+3. These rules are enforced at compile time (zero runtime cost)
+
+## Error Handling Decision Tree
+
+```
+What kind of error?
+│
+├─ Operation might not have a value (no error info needed)
+│  └─ Option<T>: Some(value) or None
+│
+├─ Library code (callers need to match on error variants)
+│  └─ thiserror: #[derive(Error)] enum with variants
+│     └─ Each variant can wrap source errors with #[from]
+│
+├─ Application code (just need context, not matching)
+│  └─ anyhow: anyhow::Result<T>, .context("msg")
+│
+├─ Converting between error types
+│  └─ impl From<SourceError> for MyError
+│     └─ Or use #[from] with thiserror
+│
+└─ Truly unrecoverable (violating invariants)
+   └─ panic!() or unwrap() - avoid in library code
+```
+
+### thiserror (Library Errors)
+
+```rust
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum AppError {
+    #[error("database error: {0}")]
+    Database(#[from] sqlx::Error),
+
+    #[error("not found: {entity} with id {id}")]
+    NotFound { entity: &'static str, id: i64 },
+
+    #[error("validation failed: {0}")]
+    Validation(String),
+}
+```
+
+### anyhow (Application Errors)
+
+```rust
+use anyhow::{Context, Result};
+
+fn load_config(path: &str) -> Result<Config> {
+    let content = std::fs::read_to_string(path)
+        .context("failed to read config file")?;
+    let config: Config = toml::from_str(&content)
+        .context("failed to parse config")?;
+    Ok(config)
+}
+```
+
+### The ? Operator
+
+```rust
+// ? on Result: returns Err early, unwraps Ok
+let file = File::open(path)?;
+
+// ? on Option: returns None early, unwraps Some
+let first = items.first()?;
+
+// Chain with map_err for context
+let port: u16 = env::var("PORT")
+    .map_err(|_| AppError::Config("PORT not set"))?
+    .parse()
+    .map_err(|_| AppError::Config("PORT not a number"))?;
+```
+
+**Deep dive**: Load `./references/error-handling.md` for Result/Option combinators, error conversion patterns, panic/recover.
+
+## Trait Design Quick Reference
+
+### Common Derives
+
+```rust
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]  // Value types
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]  // API types
+#[derive(Debug, thiserror::Error)]  // Error types
+```
+
+### Trait Objects vs Generics
+
+| | Trait Objects (`dyn Trait`) | Generics (`T: Trait`) |
+|---|---|---|
+| Dispatch | Dynamic (vtable) | Static (monomorphized) |
+| Binary size | Smaller | Larger (per-type copies) |
+| Performance | Slight overhead | Zero-cost |
+| Heterogeneous collections | Yes | No |
+| Use when | Runtime polymorphism, plugin systems | Performance-critical, known types |
+
+```rust
+// Generics (preferred when types known at compile time)
+fn process<T: Display>(item: T) { println!("{item}"); }
+
+// Trait objects (when you need heterogeneous collections)
+fn process_all(items: &[Box<dyn Display>]) {
+    for item in items { println!("{item}"); }
+}
+```
+
+### Key Traits to Know
+
+| Trait | Purpose | Auto-derive? |
+|-------|---------|-------------|
+| `Debug` | Debug formatting | Yes |
+| `Clone` | Explicit copy | Yes |
+| `Copy` | Implicit copy (small, stack-only) | Yes |
+| `Display` | User-facing formatting | No (impl manually) |
+| `From`/`Into` | Type conversion | No (impl `From`, get `Into` free) |
+| `Send` | Safe to send between threads | Auto |
+| `Sync` | Safe to share references between threads | Auto |
+| `Deref` | Smart pointer dereference | No |
+| `Iterator` | Iteration protocol | No |
+| `Default` | Default value | Yes |
+
+**Deep dive**: Load `./references/traits-generics.md` for associated types, supertraits, sealed traits, extension traits.
+
+## Async Decision Tree
+
+```
+Do you need async?
+│
+├─ I/O-heavy (network, files, databases)
+│  └─ Yes. Use tokio.
+│
+├─ CPU-heavy computation
+│  └─ No. Use rayon for data parallelism.
+│     └─ Or tokio::task::spawn_blocking for mixing with async
+│
+├─ Simple scripts or CLI tools
+│  └─ Probably not. Blocking I/O is fine.
+│
+└─ Yes, I need async:
+   │
+   ├─ Runtime: tokio (dominant), or async-std
+   ├─ HTTP client: reqwest
+   ├─ HTTP server: axum (tower-based) or actix-web
+   ├─ Database: sqlx (compile-time checked)
+   └─ Structured logging: tracing
+```
+
+### tokio Quick Start
+
+```rust
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    // Spawn concurrent tasks
+    let (a, b) = tokio::join!(
+        fetch_users(),
+        fetch_orders(),
+    );
+
+    // Select first to complete
+    tokio::select! {
+        result = long_operation() => handle(result),
+        _ = tokio::time::sleep(Duration::from_secs(5)) => {
+            eprintln!("timeout");
+        }
+    }
+
+    Ok(())
+}
+```
+
+### Channel Types
+
+| Channel | Use Case | Import |
+|---------|----------|--------|
+| `mpsc` | Multiple producers, single consumer | `tokio::sync::mpsc` |
+| `oneshot` | Single value, single use | `tokio::sync::oneshot` |
+| `broadcast` | Multiple consumers, all get every message | `tokio::sync::broadcast` |
+| `watch` | Single value, latest-only (config reload) | `tokio::sync::watch` |
+
+**Deep dive**: Load `./references/async-tokio.md` for spawn patterns, graceful shutdown, Mutex choice, async traits, streams.
+
+## Cargo Quick Reference
+
+```bash
+# Create project
+cargo new my-project        # binary
+cargo new my-lib --lib      # library
+
+# Build and run
+cargo build                 # debug
+cargo build --release       # optimized
+cargo run -- args           # build + run
+cargo run --example name    # run example
+
+# Test
+cargo test                  # all tests
+cargo test test_name        # specific test
+cargo test -- --nocapture   # show println output
+
+# Dependencies
+cargo add serde --features derive    # add dep
+cargo add tokio -F full              # shorthand
+cargo update                         # update lock file
+
+# Check without building
+cargo check                 # fast type checking
+cargo clippy                # lints
+cargo fmt                   # format
+
+# Workspace
+cargo test --workspace      # test all crates
+cargo build -p my-crate     # build specific crate
+```
+
+### Feature Flags
+
+```toml
+[features]
+default = ["json"]
+json = ["dep:serde_json"]
+full = ["json", "yaml", "toml"]
+
+[dependencies]
+serde_json = { version = "1", optional = true }
+```
+
+## Common Gotchas
+
+| Gotcha | Why | Fix |
+|--------|-----|-----|
+| `String` vs `&str` | Owned vs borrowed, function signatures | Accept `&str` in params, return `String` |
+| Borrow checker fight | Borrowing self while mutating | Split struct, use indices, clone (if cheap) |
+| Lifetime elision confusion | Hidden lifetimes in function signatures | Write them out explicitly to understand, then elide |
+| `impl Trait` in return | Different branches must return same type | Use `Box<dyn Trait>` for heterogeneous returns |
+| `tokio::Mutex` vs `std::Mutex` | `std::Mutex` can't be held across `.await` | Use `tokio::Mutex` across await points |
+| Orphan rule | Can't impl foreign trait for foreign type | Newtype pattern: `struct Wrapper(ForeignType)` |
+| `Pin` confusion | Required for self-referential async futures | Use `Box::pin()`, don't fight it |
+| `Send` bounds on async | Spawned futures must be `Send` | Avoid `Rc`, `RefCell` in async; use `Arc`, `Mutex` |
+| `.unwrap()` in production | Panics on None/Err | Use `?`, `.unwrap_or()`, `.expect("reason")` |
+
+## serde Quick Reference
+
+```rust
+use serde::{Serialize, Deserialize};
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct User {
+    user_id: i64,
+    display_name: String,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    email: Option<String>,
+
+    #[serde(default)]
+    is_active: bool,
+
+    #[serde(rename = "type")]
+    user_type: UserType,
+
+    #[serde(with = "chrono::serde::ts_seconds")]
+    created_at: DateTime<Utc>,
+}
+
+// Serialize
+let json = serde_json::to_string(&user)?;
+let yaml = serde_yaml::to_string(&user)?;
+
+// Deserialize
+let user: User = serde_json::from_str(&json)?;
+```
+
+**Deep dive**: Load `./references/ecosystem.md` for serde advanced usage, clap, reqwest, sqlx, axum, tracing, rayon.
+
+## Reference Files
+
+Load these for deep-dive topics. Each is self-contained.
+
+| Reference | When to Load |
+|-----------|-------------|
+| `./references/ownership-lifetimes.md` | Borrowing rules, lifetime annotations, elision, interior mutability, common borrow checker patterns |
+| `./references/traits-generics.md` | Trait design, associated types, supertraits, generics, constraints, sealed/extension traits |
+| `./references/error-handling.md` | Result/Option combinators, thiserror/anyhow deep dive, error conversion, panic/recover |
+| `./references/async-tokio.md` | tokio runtime, spawn, channels, select, streams, graceful shutdown, async traits, Mutex choice |
+| `./references/ecosystem.md` | serde advanced, clap, reqwest, sqlx, axum, tracing, rayon, itertools, Cow |
+| `./references/testing.md` | Unit/integration/doc tests, async tests, mockall, proptest, criterion benchmarks |
+
+## See Also
+
+- `docker-ops` - Multi-stage builds for Rust (scratch/distroless, cargo-chef for layer caching)
+- `ci-cd-ops` - Rust CI pipelines, cargo caching, cross-compilation
+- `testing-ops` - Cross-language testing strategies

+ 0 - 0
skills/rust-ops/assets/.gitkeep


Разница между файлами не показана из-за своего большого размера
+ 1019 - 0
skills/rust-ops/references/async-tokio.md


Разница между файлами не показана из-за своего большого размера
+ 1005 - 0
skills/rust-ops/references/ecosystem.md


+ 655 - 0
skills/rust-ops/references/error-handling.md

@@ -0,0 +1,655 @@
+# Rust Error Handling Reference
+
+## Table of Contents
+
+1. [Result and Option](#1-result-and-option)
+2. [The ? Operator](#2-the--operator)
+3. [thiserror](#3-thiserror)
+4. [anyhow](#4-anyhow)
+5. [Custom Error Enums](#5-custom-error-enums)
+6. [Error Conversion](#6-error-conversion)
+7. [Error Context](#7-error-context)
+8. [panic vs Result](#8-panic-vs-result)
+9. [Result in main](#9-result-in-main)
+10. [Anti-Patterns](#10-anti-patterns)
+
+---
+
+## 1. Result and Option
+
+### Basics
+
+```rust
+// Result<T, E>: Ok(T) on success, Err(E) on failure
+fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
+    s.parse::<u16>()
+}
+
+// Option<T>: Some(T) or None
+fn find_user(id: u64) -> Option<User> {
+    users.get(&id).cloned()
+}
+```
+
+### map and and_then
+
+```rust
+// map: transform Ok/Some without touching Err/None
+let doubled: Option<i32> = Some(5).map(|x| x * 2);          // Some(10)
+let upper: Result<String, _> = Ok("hi").map(|s: &str| s.to_uppercase());
+
+// and_then: chain fallible operations (flatMap)
+fn load_config(path: &str) -> Result<Config, Error> {
+    read_file(path)
+        .and_then(|contents| parse_toml(&contents))
+        .and_then(|raw| validate_config(raw))
+}
+
+// Option::and_then for chaining lookups
+let city = get_user(id)
+    .and_then(|user| get_address(user.address_id))
+    .and_then(|addr| addr.city);
+```
+
+### unwrap_or and unwrap_or_else
+
+```rust
+// unwrap_or: provide a fallback value (evaluated eagerly)
+let port: u16 = parse_port(s).unwrap_or(8080);
+let name: String = maybe_name.unwrap_or_else(String::new);
+
+// unwrap_or_else: provide a closure (evaluated lazily — prefer for expensive defaults)
+let config = load_config("app.toml")
+    .unwrap_or_else(|_| Config::default());
+
+// unwrap_or_default: use the Default impl
+let value: Vec<u8> = maybe_bytes.unwrap_or_default();
+```
+
+### ok_or and transpose
+
+```rust
+// ok_or: convert Option into Result
+let user = find_user(id).ok_or(Error::UserNotFound(id))?;
+
+// ok_or_else: lazy version
+let user = find_user(id)
+    .ok_or_else(|| Error::UserNotFound(id))?;
+
+// transpose: flip Option<Result<T, E>> <-> Result<Option<T>, E>
+let maybe_result: Option<Result<u32, _>> = Some("42".parse());
+let result_maybe: Result<Option<u32>, _> = maybe_result.transpose();  // Ok(Some(42))
+
+// Useful in iterators when you want the first error or all-None
+let parsed: Result<Vec<u32>, _> = strings
+    .iter()
+    .map(|s| s.parse::<u32>())
+    .collect();  // fails on first parse error
+```
+
+### Other Useful Methods
+
+```rust
+// is_ok, is_err, is_some, is_none
+if result.is_err() { log_failure(); }
+
+// map_err: transform only the error type
+let result = op().map_err(|e| format!("Operation failed: {e}"));
+
+// or / or_else: provide alternative on failure
+let result = primary().or_else(|_| fallback());
+
+// inspect / inspect_err: side effects without consuming
+let result = load().inspect(|v| tracing::debug!(?v, "loaded"))
+                   .inspect_err(|e| tracing::warn!(?e, "load failed"));
+
+// flatten: Option<Option<T>> -> Option<T>, Result<Result<T,E>,E> -> Result<T,E>
+let flat: Option<i32> = Some(Some(5)).flatten();  // Some(5)
+```
+
+---
+
+## 2. The ? Operator
+
+### Result Propagation
+
+```rust
+// ? desugars to: match on Err, call From::from on the error, return early
+fn read_config(path: &str) -> Result<Config, AppError> {
+    let text = std::fs::read_to_string(path)?;  // io::Error -> AppError via From
+    let config: Config = toml::from_str(&text)?; // toml::Error -> AppError via From
+    Ok(config)
+}
+```
+
+### Option Propagation
+
+```rust
+// ? on Option returns None immediately (requires the function to return Option)
+fn first_line_word(text: &str) -> Option<&str> {
+    text.lines().next()?.split_whitespace().next()
+}
+
+// Cannot mix Option? and Result? in the same function without conversion
+// Use .ok_or() or .ok_or_else() to convert Option -> Result
+fn find_section(text: &str) -> Result<&str, AppError> {
+    text.lines()
+        .find(|l| l.starts_with('['))
+        .ok_or(AppError::NoSection)?
+        .trim()
+        .into()
+}
+```
+
+### From Conversion
+
+```rust
+// ? calls From::from automatically. Define From impls to unlock ?
+impl From<std::io::Error> for AppError {
+    fn from(e: std::io::Error) -> Self {
+        AppError::Io(e)
+    }
+}
+
+// Now io::Error can be converted with ?
+fn write_output(data: &[u8]) -> Result<(), AppError> {
+    std::fs::write("out.bin", data)?;  // io::Error converted automatically
+    Ok(())
+}
+```
+
+### Early Return Pattern
+
+```rust
+// ? enables clean early-return without match chains
+fn process(input: &str) -> Result<Output, AppError> {
+    let parsed = parse(input)?;
+    let validated = validate(parsed)?;
+    let enriched = enrich(validated)?;
+    Ok(transform(enriched))
+}
+```
+
+---
+
+## 3. thiserror
+
+thiserror generates `std::error::Error` impls via derive macros. Use it in **libraries**.
+
+### Derive Error and Format Messages
+
+```rust
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum AppError {
+    #[error("user {id} not found")]
+    UserNotFound { id: u64 },
+
+    #[error("invalid email address: {0}")]
+    InvalidEmail(String),
+
+    #[error("timeout after {0:?}")]
+    Timeout(std::time::Duration),
+
+    #[error("internal error")]
+    Internal,
+}
+```
+
+### #[from] for Automatic Conversion
+
+```rust
+#[derive(Debug, Error)]
+pub enum AppError {
+    // #[from] generates From<io::Error> for AppError
+    #[error("IO error: {0}")]
+    Io(#[from] std::io::Error),
+
+    // Enables ? on sqlx operations automatically
+    #[error("database error: {0}")]
+    Database(#[from] sqlx::Error),
+
+    #[error("serialization error: {0}")]
+    Json(#[from] serde_json::Error),
+}
+```
+
+### #[source] for Error Chains
+
+```rust
+#[derive(Debug, Error)]
+pub enum AppError {
+    // #[source] exposes inner error via Error::source()
+    // #[from] implies #[source] automatically
+    #[error("config load failed")]
+    Config {
+        #[source]
+        cause: std::io::Error,
+    },
+
+    // transparent: delegate Display and source to inner error
+    #[error(transparent)]
+    Other(#[from] anyhow::Error),
+}
+```
+
+### Struct Errors with thiserror
+
+```rust
+#[derive(Debug, Error)]
+#[error("parse failed at line {line}: {message}")]
+pub struct ParseError {
+    pub line: usize,
+    pub message: String,
+    #[source]
+    pub cause: Option<std::num::ParseIntError>,
+}
+```
+
+---
+
+## 4. anyhow
+
+anyhow provides a single opaque error type for **application** (binary) code.
+
+### anyhow::Result and anyhow!()
+
+```rust
+use anyhow::{anyhow, bail, ensure, Context, Result};
+
+fn load(path: &str) -> Result<Config> {
+    let text = std::fs::read_to_string(path)?;  // any error works with ?
+    let config = serde_json::from_str(&text)?;
+    Ok(config)
+}
+
+// anyhow!() creates an ad-hoc error
+fn validate(n: i32) -> Result<i32> {
+    if n < 0 {
+        return Err(anyhow!("expected non-negative, got {n}"));
+    }
+    Ok(n)
+}
+```
+
+### bail! and ensure!
+
+```rust
+fn process(value: i32) -> Result<()> {
+    // bail!() is return Err(anyhow!(...))
+    if value > 1000 {
+        bail!("value {value} exceeds maximum of 1000");
+    }
+
+    // ensure!() is if !condition { bail!(...) }
+    ensure!(value >= 0, "value must be non-negative, got {value}");
+
+    Ok(())
+}
+```
+
+### .context() and .with_context()
+
+```rust
+fn init() -> Result<()> {
+    let config = std::fs::read_to_string("config.toml")
+        .context("failed to read config.toml")?;
+
+    // with_context: lazy, use when message is expensive to build
+    let parsed: Config = toml::from_str(&config)
+        .with_context(|| format!("failed to parse config (len={})", config.len()))?;
+
+    Ok(())
+}
+```
+
+### Downcasting
+
+```rust
+fn handle(err: anyhow::Error) {
+    // Check if the underlying error is a specific type
+    if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
+        eprintln!("IO error: {io_err}");
+    } else {
+        eprintln!("Unknown error: {err:#}");
+    }
+}
+
+// {:#} prints the full error chain
+// {:?} prints the debug representation including backtrace
+```
+
+---
+
+## 5. Custom Error Enums
+
+### Design Error Hierarchies
+
+```rust
+// Top-level public error: coarse-grained, stable API surface
+#[derive(Debug, Error)]
+pub enum ServiceError {
+    #[error("authentication failed")]
+    Auth(#[from] AuthError),
+
+    #[error("database unavailable")]
+    Database(#[from] DbError),
+
+    #[error("request invalid: {0}")]
+    Validation(String),
+}
+
+// Sub-module error: fine-grained, internal
+#[derive(Debug, Error)]
+pub enum AuthError {
+    #[error("token expired")]
+    TokenExpired,
+
+    #[error("invalid signature")]
+    BadSignature,
+
+    #[error("user {0} locked")]
+    AccountLocked(u64),
+}
+```
+
+### When to Split vs Combine
+
+```rust
+// SPLIT when:
+// - Callers need to pattern-match specific variants
+// - Different modules own different error domains
+// - You want stable public API with internal flexibility
+
+// COMBINE (single enum) when:
+// - Small codebase with few error kinds
+// - Errors don't need distinct handling by callers
+// - Internal-only code
+
+// Guideline: one error enum per public API boundary (crate, module, trait)
+```
+
+---
+
+## 6. Error Conversion
+
+### impl From
+
+```rust
+impl From<std::io::Error> for AppError {
+    fn from(e: std::io::Error) -> Self {
+        match e.kind() {
+            std::io::ErrorKind::NotFound => AppError::NotFound,
+            std::io::ErrorKind::PermissionDenied => AppError::Forbidden,
+            _ => AppError::Io(e),
+        }
+    }
+}
+```
+
+### Manual Conversion
+
+```rust
+// When From is too broad, convert explicitly with map_err
+fn read_key(path: &str) -> Result<Vec<u8>, AppError> {
+    std::fs::read(path).map_err(|e| {
+        if e.kind() == std::io::ErrorKind::NotFound {
+            AppError::KeyMissing(path.to_string())
+        } else {
+            AppError::Io(e)
+        }
+    })
+}
+```
+
+### Converting Between Error Crates
+
+```rust
+// thiserror library error -> anyhow application error: just use ?
+// anyhow error -> thiserror: use #[error(transparent)] or explicit wrapping
+
+#[derive(Debug, Error)]
+pub enum AppError {
+    #[error(transparent)]
+    Internal(#[from] anyhow::Error),
+}
+
+// Or convert with a helper
+fn wrap(e: anyhow::Error) -> AppError {
+    AppError::Internal(e)
+}
+```
+
+---
+
+## 7. Error Context
+
+### Add Context Without Losing Source
+
+```rust
+// anyhow .context() preserves the original error as source
+let data = fetch(url).context("failed to fetch user data")?;
+
+// thiserror: wrap in a variant with #[source]
+#[derive(Debug, Error)]
+pub enum LoadError {
+    #[error("failed to read {path}")]
+    Read {
+        path: String,
+        #[source]
+        cause: std::io::Error,
+    },
+}
+
+fn load(path: &str) -> Result<Vec<u8>, LoadError> {
+    std::fs::read(path).map_err(|cause| LoadError::Read {
+        path: path.to_string(),
+        cause,
+    })
+}
+```
+
+### Wrapping Strategy
+
+```rust
+// Layer context at each boundary crossing
+// 1. Low-level: return raw errors with thiserror
+// 2. Service layer: add domain context with .context()
+// 3. Handler/main: print full chain with {:#}
+
+fn read_user(id: u64) -> Result<User> {
+    let row = db.query_one(id)
+        .with_context(|| format!("db lookup failed for user {id}"))?;
+    parse_user(row)
+        .with_context(|| format!("failed to parse user {id} from db row"))
+}
+```
+
+---
+
+## 8. panic vs Result
+
+### When panic Is Legitimate
+
+```rust
+// 1. Tests: use assert!, assert_eq!, unwrap() freely
+#[test]
+fn test_parse() {
+    assert_eq!(parse("42").unwrap(), 42);
+}
+
+// 2. Initialization that cannot recover
+fn main() {
+    let config = load_config().expect("failed to load required config");
+}
+
+// 3. Invariant violations that indicate a programmer bug
+fn get_first(v: &[i32]) -> i32 {
+    // Caller contract: v must not be empty
+    v[0]  // panics on empty — that is correct behaviour
+}
+
+// 4. Prototype / throwaway code (use todo!, unimplemented!)
+fn not_implemented_yet() -> String {
+    todo!("implement serialization")
+}
+```
+
+### catch_unwind for Panic Isolation
+
+```rust
+use std::panic;
+
+// Catch panics from untrusted code (plugin, FFI boundary)
+let result = panic::catch_unwind(|| {
+    potentially_panicking_code()
+});
+
+match result {
+    Ok(value) => println!("success: {value:?}"),
+    Err(_) => eprintln!("caught a panic"),
+}
+
+// Note: catch_unwind does NOT catch abort-mode panics or stack overflows
+```
+
+---
+
+## 9. Result in main
+
+### Return Result from main
+
+```rust
+// main can return Result<(), E> where E: Debug
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let config = load_config()?;
+    run(config)?;
+    Ok(())
+}
+
+// With anyhow for full error chains
+fn main() -> anyhow::Result<()> {
+    let config = load_config().context("startup failed")?;
+    run(config)?;
+    Ok(())
+}
+```
+
+### ExitCode and process::exit
+
+```rust
+use std::process::ExitCode;
+
+fn main() -> ExitCode {
+    match run() {
+        Ok(()) => ExitCode::SUCCESS,
+        Err(e) => {
+            eprintln!("error: {e:#}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+// process::exit for immediate termination (skips destructors)
+fn must_succeed() {
+    if let Err(e) = critical_setup() {
+        eprintln!("fatal: {e}");
+        std::process::exit(1);
+    }
+}
+```
+
+### Termination Trait
+
+```rust
+// For custom exit codes beyond 0/1
+use std::process::{ExitCode, Termination};
+
+struct AppExit(u8);
+
+impl Termination for AppExit {
+    fn report(self) -> ExitCode {
+        ExitCode::from(self.0)
+    }
+}
+
+fn main() -> AppExit {
+    match run() {
+        Ok(()) => AppExit(0),
+        Err(AppError::ConfigMissing) => AppExit(2),
+        Err(_) => AppExit(1),
+    }
+}
+```
+
+---
+
+## 10. Anti-Patterns
+
+### .unwrap() Everywhere
+
+```rust
+// BAD: panics on any error in production
+let text = std::fs::read_to_string("config.toml").unwrap();
+let user = find_user(id).unwrap();
+
+// GOOD: propagate with ?, provide defaults, or handle explicitly
+let text = std::fs::read_to_string("config.toml")
+    .context("config.toml is required")?;
+let user = find_user(id).ok_or(AppError::UserNotFound(id))?;
+```
+
+### Stringly Typed Errors
+
+```rust
+// BAD: callers cannot inspect or match on error kind
+fn load(path: &str) -> Result<Data, String> {
+    std::fs::read_to_string(path).map_err(|e| e.to_string())
+}
+
+// GOOD: typed errors callers can handle
+fn load(path: &str) -> Result<Data, AppError> {
+    let text = std::fs::read_to_string(path)?;
+    Ok(parse(&text)?)
+}
+```
+
+### Excessive Error Types
+
+```rust
+// BAD: one error type per function — impossible to use
+fn read_name() -> Result<String, ReadNameError> { ... }
+fn parse_age() -> Result<u32, ParseAgeError> { ... }
+fn validate() -> Result<(), ValidateError> { ... }
+
+// GOOD: one error type per domain boundary
+fn load_user(id: u64) -> Result<User, UserError> { ... }
+```
+
+### Ignoring Errors with let _ =
+
+```rust
+// BAD: silently discards errors — hides bugs
+let _ = send_notification(user);
+let _ = std::fs::remove_file(tmp);
+
+// GOOD: log or explicitly decide to ignore
+if let Err(e) = send_notification(user) {
+    tracing::warn!(?e, "notification failed, continuing");
+}
+
+// If truly safe to ignore, be explicit about why
+std::fs::remove_file(tmp).ok();  // .ok() signals intentional ignore
+```
+
+### Boxing Without Cause
+
+```rust
+// BAD: loses type information, callers can't downcast easily
+fn run() -> Result<(), Box<dyn std::error::Error>> { ... }
+
+// GOOD in main / test harnesses, BAD in library APIs
+// For libraries, use typed errors via thiserror
+// For applications, use anyhow::Result
+```

+ 664 - 0
skills/rust-ops/references/ownership-lifetimes.md

@@ -0,0 +1,664 @@
+# Ownership and Lifetimes Reference
+
+## Table of Contents
+
+1. [Move Semantics](#1-move-semantics)
+2. [Borrowing Rules](#2-borrowing-rules)
+3. [Lifetime Annotations](#3-lifetime-annotations)
+4. [Lifetime Elision Rules](#4-lifetime-elision-rules)
+5. [static Lifetime](#5-static-lifetime)
+6. [Interior Mutability](#6-interior-mutability)
+7. [Common Borrow Checker Patterns](#7-common-borrow-checker-patterns)
+8. [NLL (Non-Lexical Lifetimes)](#8-nll-non-lexical-lifetimes)
+9. [Self-Referential Structs](#9-self-referential-structs)
+
+---
+
+## 1. Move Semantics
+
+### Understand What Moves vs What Copies
+
+Types that implement `Copy` are implicitly duplicated on assignment. All others are moved.
+
+**Copy types:** All integer primitives, `f32`/`f64`, `bool`, `char`, raw pointers, references (`&T`), arrays of Copy types, tuples of Copy types.
+
+**Move types:** `String`, `Vec<T>`, `Box<T>`, `HashMap`, any struct containing a move type.
+
+```rust
+// Copy - both variables remain valid
+let x: i32 = 5;
+let y = x;
+println!("{} {}", x, y); // OK
+
+// Move - s1 is no longer valid after assignment
+let s1 = String::from("hello");
+let s2 = s1;
+// println!("{}", s1); // ERROR: value moved
+
+// Clone to keep both
+let s3 = String::from("hello");
+let s4 = s3.clone();
+println!("{} {}", s3, s4); // OK
+```
+
+### Recognize Moves in Function Calls
+
+Passing a move type to a function moves ownership into that function. The caller loses access.
+
+```rust
+fn consume(s: String) {
+    println!("{}", s);
+} // s is dropped here
+
+fn borrow(s: &String) {
+    println!("{}", s);
+} // s is NOT dropped; caller retains ownership
+
+fn main() {
+    let s = String::from("hello");
+    borrow(&s);   // s still valid
+    consume(s);   // s moved into consume
+    // consume(s);  // ERROR: s already moved
+}
+```
+
+### Handle Moves in Closures
+
+Closures capture variables by the minimum required (reference, mutable reference, or move). Use `move` to force ownership transfer.
+
+```rust
+let s = String::from("hello");
+
+// Closure borrows s by reference (default when possible)
+let print = || println!("{}", s);
+print();
+println!("{}", s); // s still valid
+
+// Force move into closure (required for threads)
+let s2 = String::from("world");
+let owned = move || println!("{}", s2);
+// println!("{}", s2); // ERROR: s2 moved into closure
+owned();
+```
+
+Closures sent to threads must own their data because the thread may outlive the caller's stack:
+
+```rust
+let data = vec![1, 2, 3];
+std::thread::spawn(move || {
+    println!("{:?}", data); // data moved into thread
+});
+```
+
+### Avoid Moves in Loops
+
+Moving a value inside a loop consumes it on the first iteration. Use references or `clone` strategically.
+
+```rust
+let items = vec![String::from("a"), String::from("b")];
+
+// BAD: moves items on first iteration if iterating by value
+// for item in items { ... } // items consumed after loop
+
+// GOOD: iterate by reference
+for item in &items {
+    println!("{}", item);
+}
+println!("{:?}", items); // items still valid
+
+// GOOD: when you need ownership, iterate by value and handle each
+for item in items {
+    process(item); // each item moved individually, that's fine
+}
+```
+
+---
+
+## 2. Borrowing Rules
+
+### Apply the Core Rules
+
+1. At any point, you may have either one `&mut T` or any number of `&T` references — never both simultaneously.
+2. References must always point to valid data (no dangling references).
+
+```rust
+let mut s = String::from("hello");
+
+let r1 = &s;
+let r2 = &s;
+// let r3 = &mut s; // ERROR: cannot borrow as mutable while borrowed as immutable
+println!("{} {}", r1, r2);
+// r1 and r2 go out of scope here (NLL)
+
+let r3 = &mut s; // OK now
+r3.push_str("!");
+```
+
+### Understand Reborrowing
+
+A `&mut T` can be "reborrowed" as `&T` or a shorter-lived `&mut T`. The compiler inserts reborrows automatically in most cases.
+
+```rust
+fn modify(s: &mut String) {
+    // Reborrow: passing &mut *s creates a new &mut with shorter lifetime
+    takes_str(&*s);    // reborrow as &str
+    s.push_str("!"); // original mutable ref still usable after reborrow ends
+}
+
+fn takes_str(s: &str) {
+    println!("{}", s);
+}
+```
+
+### Recognize Temporary Borrows
+
+Method calls that return references extend the borrow of `self` for the duration the reference is held.
+
+```rust
+let mut map: HashMap<&str, Vec<i32>> = HashMap::new();
+map.insert("key", vec![1, 2, 3]);
+
+// This holds an immutable borrow of map via get()
+let val = map.get("key").unwrap();
+println!("{:?}", val);
+// val borrow ends here
+
+map.insert("other", vec![4]); // OK: no active borrows
+```
+
+---
+
+## 3. Lifetime Annotations
+
+### Read Lifetime Syntax
+
+Lifetime parameters start with `'` and appear in angle brackets. They describe relationships, not durations.
+
+```rust
+// 'a is a generic lifetime parameter
+fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
+    if x.len() > y.len() { x } else { y }
+}
+```
+
+The annotation says: "the returned reference lives at least as long as the shorter of x and y."
+
+### Annotate Function Lifetimes
+
+Only annotate when the compiler cannot infer the relationship (multiple input references, output borrows from one of them).
+
+```rust
+// Input and output tied to first argument only
+fn first_word<'a>(s: &'a str) -> &'a str {
+    s.split_whitespace().next().unwrap_or("")
+}
+
+// Two unrelated input lifetimes
+fn split_at<'a, 'b>(s: &'a str, _sep: &'b str) -> (&'a str, &'a str) {
+    let mid = s.len() / 2;
+    (&s[..mid], &s[mid..])
+}
+
+// Output may come from either input - must unify lifetimes
+fn pick<'a>(a: &'a str, b: &'a str, use_a: bool) -> &'a str {
+    if use_a { a } else { b }
+}
+```
+
+### Annotate Struct Lifetimes
+
+Structs holding references must declare the lifetime of those references.
+
+```rust
+struct Excerpt<'a> {
+    text: &'a str,
+}
+
+impl<'a> Excerpt<'a> {
+    // &self lifetime elided (elision rule 3)
+    fn content(&self) -> &str {
+        self.text
+    }
+
+    // Must annotate: output could be self.text or announcement
+    fn announce<'b>(&'a self, announcement: &'b str) -> &'a str {
+        println!("{}", announcement);
+        self.text
+    }
+}
+```
+
+### Use Multiple Lifetime Parameters
+
+Use multiple parameters when outputs have different source lifetimes.
+
+```rust
+struct Cache<'data, 'key> {
+    data: &'data [u8],
+    key: &'key str,
+}
+
+// 'long outlives 'short: items from 'long can be stored where 'short is needed
+fn merge<'long: 'short, 'short>(
+    primary: &'long str,
+    fallback: &'short str,
+    use_primary: bool,
+) -> &'short str {
+    if use_primary { primary } else { fallback }
+}
+```
+
+---
+
+## 4. Lifetime Elision Rules
+
+### Apply the Three Elision Rules
+
+The compiler applies these rules in order before requiring annotations:
+
+**Rule 1:** Each reference parameter gets its own distinct lifetime.
+```rust
+fn foo(x: &str, y: &str) -> &str
+// becomes:
+fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &??? str
+// output lifetime unknown - annotation required
+```
+
+**Rule 2:** If there is exactly one input lifetime, it applies to all outputs.
+```rust
+fn first_word(s: &str) -> &str
+// becomes:
+fn first_word<'a>(s: &'a str) -> &'a str // inferred
+```
+
+**Rule 3:** If one of the inputs is `&self` or `&mut self`, the output gets self's lifetime.
+```rust
+impl Foo {
+    fn bar(&self, x: &str) -> &str
+    // becomes:
+    fn bar<'a, 'b>(&'a self, x: &'b str) -> &'a str // inferred
+}
+```
+
+### Know When to Annotate
+
+Annotate when:
+- Multiple input references and the output could come from more than one of them
+- A struct holds a reference
+- You need to express a lifetime bound (`T: 'a`)
+
+Omit when:
+- Single input reference (rule 2 applies)
+- Method returning reference derived from `&self` (rule 3 applies)
+- Output is an owned type (no lifetime needed)
+
+---
+
+## 5. 'static Lifetime
+
+### Understand String Literals vs Owned Data
+
+`&'static str` means the reference points to data embedded in the binary — always valid.
+
+```rust
+let s: &'static str = "I am in the binary"; // string literal
+
+// Owned String is NOT 'static, but can produce &str with any lifetime
+let owned = String::from("dynamic");
+let borrowed: &str = &owned; // lifetime tied to owned, not 'static
+```
+
+### Correct the T: 'static Misconception
+
+`T: 'static` does NOT mean T lives forever. It means T contains no non-static references — T may be dropped at any time.
+
+```rust
+// T: 'static = T owns all its data (no borrowed references inside)
+fn store<T: 'static>(val: T) {
+    std::thread::spawn(move || drop(val)); // safe: T outlives any borrow
+}
+
+store(String::from("owned")); // OK: String owns its data
+store(42i32);                 // OK: Copy type, no references
+
+let s = String::from("temp");
+// store(&s); // ERROR: &s has lifetime tied to s, not 'static
+```
+
+### Use 'static in Error and Trait Objects
+
+Error types commonly require `'static` so they can be sent across threads or stored.
+
+```rust
+fn might_fail() -> Result<(), Box<dyn std::error::Error + 'static>> {
+    std::fs::read_to_string("missing.txt")?;
+    Ok(())
+}
+
+// Thread-sendable trait object
+fn run_task(task: Box<dyn Fn() + Send + 'static>) {
+    std::thread::spawn(task);
+}
+```
+
+---
+
+## 6. Interior Mutability
+
+### Use Cell<T> for Copy Types
+
+`Cell<T>` allows mutation through a shared reference. It is `!Sync` (single-threaded only) and works only for `Copy` types.
+
+```rust
+use std::cell::Cell;
+
+struct Counter {
+    count: Cell<u32>,
+}
+
+impl Counter {
+    fn increment(&self) { // &self, not &mut self
+        self.count.set(self.count.get() + 1);
+    }
+    fn value(&self) -> u32 {
+        self.count.get()
+    }
+}
+
+let c = Counter { count: Cell::new(0) };
+c.increment();
+c.increment();
+println!("{}", c.value()); // 2
+```
+
+### Use RefCell<T> for Non-Copy Types
+
+`RefCell<T>` enforces borrow rules at runtime. Panics if rules are violated. Also `!Sync`.
+
+```rust
+use std::cell::RefCell;
+
+let data = RefCell::new(vec![1, 2, 3]);
+
+// Immutable borrow
+let r = data.borrow();
+println!("{:?}", *r);
+drop(r); // release before mutable borrow
+
+// Mutable borrow
+data.borrow_mut().push(4);
+
+// try_borrow / try_borrow_mut avoid panics
+match data.try_borrow_mut() {
+    Ok(mut v) => v.push(5),
+    Err(_) => eprintln!("already borrowed"),
+}
+```
+
+Common pattern: `Rc<RefCell<T>>` for shared, mutable ownership in single-threaded code.
+
+```rust
+use std::rc::Rc;
+use std::cell::RefCell;
+
+let shared = Rc::new(RefCell::new(vec![]));
+let clone = Rc::clone(&shared);
+
+shared.borrow_mut().push(1);
+clone.borrow_mut().push(2);
+println!("{:?}", shared.borrow()); // [1, 2]
+```
+
+### Use OnceCell and OnceLock for Lazy Initialization
+
+`OnceCell<T>` initializes a value at most once. `OnceLock<T>` is the thread-safe version.
+
+```rust
+use std::cell::OnceCell;
+
+struct Config {
+    expensive: OnceCell<Vec<u8>>,
+}
+
+impl Config {
+    fn data(&self) -> &Vec<u8> {
+        self.expensive.get_or_init(|| {
+            expensive_computation()
+        })
+    }
+}
+
+// OnceLock for global statics (thread-safe)
+use std::sync::OnceLock;
+
+static INSTANCE: OnceLock<String> = OnceLock::new();
+
+fn get_instance() -> &'static String {
+    INSTANCE.get_or_init(|| String::from("initialized once"))
+}
+```
+
+### Choose the Right Type
+
+| Type | Thread-safe | Works with | Runtime check |
+|------|-------------|------------|---------------|
+| `Cell<T>` | No | `Copy` types | No (get/set) |
+| `RefCell<T>` | No | Any `T` | Yes (panics) |
+| `OnceCell<T>` | No | Any `T` | No (init once) |
+| `OnceLock<T>` | Yes | Any `T: Send + Sync` | No (init once) |
+| `Mutex<T>` | Yes | Any `T: Send` | Blocks |
+| `RwLock<T>` | Yes | Any `T: Send + Sync` | Blocks |
+
+---
+
+## 7. Common Borrow Checker Patterns
+
+### Split Borrows to Borrow Multiple Fields
+
+The borrow checker tracks fields independently. Access them through separate references.
+
+```rust
+struct Point { x: f64, y: f64 }
+
+let mut p = Point { x: 1.0, y: 2.0 };
+
+// ERROR: cannot borrow p.x as mutable because p is also borrowed
+// let rx = &mut p.x;
+// let ry = &mut p.y;
+
+// OK: split into two mutable references to distinct fields
+let rx = &mut p.x;
+let ry = &mut p.y;
+*rx += 1.0;
+*ry += 1.0;
+```
+
+For slices, use `split_at_mut`:
+
+```rust
+let mut data = vec![1, 2, 3, 4, 5];
+let (left, right) = data.split_at_mut(2);
+left[0] = 10;
+right[0] = 30;
+```
+
+### Use Indices Instead of References
+
+When a data structure is being modified, holding an index avoids borrow conflicts.
+
+```rust
+// BAD: first_ref holds a borrow while we try to modify vec
+// let first_ref = &vec[0];
+// vec.push(99); // ERROR
+
+// GOOD: store index, re-access after modification
+let first_idx = 0;
+vec.push(99);
+println!("{}", vec[first_idx]); // re-borrow, no conflict
+```
+
+### Use Temporary Variables to Shorten Borrow Scope
+
+Extracting a value before using it can satisfy the borrow checker.
+
+```rust
+fn process(map: &mut HashMap<String, Vec<i32>>, key: &str) {
+    // This fails: cannot borrow map as mutable while key is borrowed from it
+    // if map.contains_key(key) {
+    //     map.get_mut(key).unwrap().push(1);
+    // }
+
+    // Clone the key to avoid holding a reference into map
+    let key = key.to_string();
+    map.entry(key).or_default().push(1);
+}
+```
+
+### Use the Entry API for Maps
+
+`entry` combines lookup and insert in one operation, avoiding double borrows.
+
+```rust
+use std::collections::HashMap;
+
+let mut scores: HashMap<String, Vec<i32>> = HashMap::new();
+
+// BAD: two separate borrows
+// if !scores.contains_key("Alice") {
+//     scores.insert("Alice".to_string(), vec![]);
+// }
+// scores.get_mut("Alice").unwrap().push(10);
+
+// GOOD: entry API
+scores.entry("Alice".to_string()).or_default().push(10);
+scores.entry("Bob".to_string()).or_insert_with(Vec::new).push(5);
+
+// Modify existing or insert computed value
+scores.entry("Carol".to_string())
+    .and_modify(|v| v.push(99))
+    .or_insert_with(|| vec![0]);
+```
+
+### Restructure Loops That Hold Borrows
+
+Collecting indices or keys before iterating avoids holding a reference during modification.
+
+```rust
+let mut map: HashMap<i32, i32> = HashMap::new();
+map.insert(1, 10);
+map.insert(2, 20);
+
+// Collect keys first, then iterate
+let keys: Vec<i32> = map.keys().cloned().collect();
+for key in keys {
+    if key % 2 == 0 {
+        map.remove(&key); // OK: no active borrow from .keys()
+    }
+}
+```
+
+---
+
+## 8. NLL (Non-Lexical Lifetimes)
+
+### Understand What NLL Provides
+
+Before NLL (pre-2018 edition), borrows lasted until the end of the lexical block. NLL ends borrows at the last point of use.
+
+```rust
+let mut s = String::from("hello");
+
+let r = &s;
+println!("{}", r); // last use of r
+
+// Pre-NLL: ERROR here because r's scope extended to end of block
+// NLL: OK because r is no longer used after the println
+s.push_str(" world");
+println!("{}", s);
+```
+
+### Know NLL's Limits
+
+NLL does not help when a borrow is inside a loop or the returned reference ties back to self.
+
+```rust
+// This still fails even with NLL - the borrow from get() ties to map
+fn first_or_insert(map: &mut HashMap<i32, i32>, key: i32) -> &i32 {
+    if let Some(val) = map.get(&key) {
+        return val; // borrows map
+    }
+    map.insert(key, 0); // ERROR: map already borrowed by return path
+    map.get(&key).unwrap()
+}
+
+// Fix: use entry API
+fn first_or_insert_fixed(map: &mut HashMap<i32, i32>, key: i32) -> &i32 {
+    map.entry(key).or_insert(0)
+}
+```
+
+---
+
+## 9. Self-Referential Structs
+
+### Understand Why Self-Referential Structs Fail
+
+A struct cannot hold a reference to one of its own fields because moving the struct would invalidate the reference.
+
+```rust
+// This does NOT compile
+struct SelfRef {
+    data: String,
+    ptr: &str, // would need lifetime tied to self.data — impossible
+}
+```
+
+### Use the ouroboros Crate
+
+`ouroboros` generates safe self-referential structs via macro.
+
+```rust
+// Cargo.toml: ouroboros = "0.18"
+use ouroboros::self_referencing;
+
+#[self_referencing]
+struct ParsedDocument {
+    raw: String,
+    #[borrows(raw)]
+    #[covariant]
+    parsed: Vec<&'this str>,
+}
+
+let doc = ParsedDocumentBuilder {
+    raw: String::from("hello world foo"),
+    parsed_builder: |raw: &str| raw.split_whitespace().collect(),
+}.build();
+
+doc.with_parsed(|words| println!("{:?}", words));
+```
+
+### Use Pin for Futures and Async
+
+`Pin<P>` prevents moving the pinned value. The async runtime uses it to allow self-referential futures.
+
+```rust
+use std::pin::Pin;
+use std::marker::PhantomPinned;
+
+struct Unmovable {
+    data: String,
+    // self_ref would point into data
+    _pin: PhantomPinned,
+}
+
+// Create pinned on heap
+let pinned = Box::pin(Unmovable {
+    data: String::from("hello"),
+    _pin: PhantomPinned,
+});
+
+// Can call methods through Pin
+// Cannot move out of Pin<Box<T>> if T: !Unpin
+```
+
+In practice, `Pin` appears most often in custom `Future` implementations and when building async combinators. Prefer `async fn` and existing executor abstractions over manual `Pin` management.

+ 866 - 0
skills/rust-ops/references/testing.md

@@ -0,0 +1,866 @@
+# Rust Testing Reference
+
+## Table of Contents
+
+1. [Unit Tests](#1-unit-tests)
+2. [Integration Tests](#2-integration-tests)
+3. [Doc Tests](#3-doc-tests)
+4. [Async Tests](#4-async-tests)
+5. [mockall](#5-mockall)
+6. [Test Fixtures](#6-test-fixtures)
+7. [Property-Based Testing](#7-property-based-testing)
+8. [Benchmarks](#8-benchmarks)
+9. [Snapshot Testing](#9-snapshot-testing)
+10. [Test Organization](#10-test-organization)
+11. [CI Patterns](#11-ci-patterns)
+
+---
+
+## 1. Unit Tests
+
+### Write Tests in `#[cfg(test)]` Modules
+
+```rust
+pub fn divide(a: f64, b: f64) -> Result<f64, String> {
+    if b == 0.0 {
+        Err("division by zero".to_string())
+    } else {
+        Ok(a / b)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;  // bring parent module into scope
+
+    #[test]
+    fn divide_positive_numbers() {
+        assert_eq!(divide(10.0, 2.0), Ok(5.0));
+    }
+
+    #[test]
+    fn divide_returns_error_on_zero() {
+        assert!(divide(1.0, 0.0).is_err());
+    }
+
+    #[test]
+    #[should_panic(expected = "index out of bounds")]
+    fn panics_on_bad_index() {
+        let v: Vec<i32> = vec![];
+        let _ = v[0];
+    }
+
+    // Return Result from a test - failure message from the Err variant
+    #[test]
+    fn parse_valid_input() -> Result<(), String> {
+        let n: i32 = "42".parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
+        assert_eq!(n, 42);
+        Ok(())
+    }
+}
+```
+
+### Use Assert Macros Effectively
+
+```rust
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn assert_variants() {
+        let x = 5;
+
+        assert!(x > 0);                          // boolean
+        assert_eq!(x, 5);                        // equality (implements PartialEq + Debug)
+        assert_ne!(x, 99);                       // inequality
+        assert_eq!(x, 5, "Expected 5, got {}", x);  // with message
+
+        // Floating point - check within epsilon
+        let f = 0.1 + 0.2;
+        assert!((f - 0.3).abs() < 1e-10, "float comparison failed: {}", f);
+    }
+}
+```
+
+---
+
+## 2. Integration Tests
+
+### Organize Tests in the `tests/` Directory
+
+```
+my_crate/
+├── src/
+│   └── lib.rs
+└── tests/
+    ├── common/
+    │   └── mod.rs        # shared helpers (not a test file)
+    ├── api_test.rs
+    └── db_test.rs
+```
+
+```rust
+// tests/common/mod.rs - shared setup, not discovered as a test binary
+pub fn setup_logging() {
+    let _ = tracing_subscriber::fmt::try_init();
+}
+
+pub fn load_fixture(name: &str) -> serde_json::Value {
+    let path = std::path::Path::new("tests/fixtures").join(name);
+    let bytes = std::fs::read(path).expect("fixture not found");
+    serde_json::from_slice(&bytes).expect("invalid fixture JSON")
+}
+```
+
+```rust
+// tests/api_test.rs - each file becomes a separate test binary
+mod common;
+
+use my_crate::ApiClient;
+
+#[test]
+fn client_builds_with_defaults() {
+    common::setup_logging();
+    let client = ApiClient::new("http://localhost");
+    assert_eq!(client.base_url(), "http://localhost");
+}
+```
+
+### Share State Between Integration Test Files
+
+```rust
+// tests/common/mod.rs
+use std::sync::OnceLock;
+
+static SERVER: OnceLock<TestServer> = OnceLock::new();
+
+pub fn get_server() -> &'static TestServer {
+    SERVER.get_or_init(|| TestServer::start())
+}
+```
+
+---
+
+## 3. Doc Tests
+
+### Write Testable Examples in Documentation
+
+```rust
+/// Parses a version string into major, minor, patch components.
+///
+/// # Examples
+///
+/// ```
+/// use my_crate::parse_version;
+///
+/// let (major, minor, patch) = parse_version("1.2.3").unwrap();
+/// assert_eq!((major, minor, patch), (1, 2, 3));
+/// ```
+///
+/// Returns `None` for invalid input:
+///
+/// ```
+/// use my_crate::parse_version;
+/// assert!(parse_version("not_a_version").is_none());
+/// ```
+pub fn parse_version(s: &str) -> Option<(u32, u32, u32)> {
+    // ...
+}
+```
+
+### Use Hidden Setup Lines
+
+```rust
+/// Demonstrates the cache in action.
+///
+/// ```
+/// # use my_crate::Cache;
+/// # let mut cache = Cache::new(100);  // hidden: sets up state
+/// cache.insert("key", "value");
+/// assert_eq!(cache.get("key"), Some("value"));
+/// ```
+```
+
+### Mark Non-Runnable Examples
+
+```rust
+/// Connect to the database.
+///
+/// ```no_run
+/// # use my_crate::connect;
+/// // This compiles but does not run (needs a real database)
+/// let pool = connect("postgres://localhost/mydb").unwrap();
+/// ```
+///
+/// This example is only shown, not compiled:
+///
+/// ```ignore
+/// // Complex setup omitted
+/// some_impossible_setup();
+/// ```
+///
+/// This example should fail to compile:
+///
+/// ```compile_fail
+/// let x: u32 = "not a number";  // type error
+/// ```
+```
+
+---
+
+## 4. Async Tests
+
+### Test with `#[tokio::test]`
+
+```rust
+#[tokio::test]
+async fn fetch_returns_data() {
+    let client = build_client();
+    let result = client.fetch("https://example.com").await;
+    assert!(result.is_ok());
+}
+
+// Multi-thread runtime (matches production tokio::main)
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn concurrent_requests() {
+    let (r1, r2) = tokio::join!(
+        do_request("a"),
+        do_request("b"),
+    );
+    assert!(r1.is_ok());
+    assert!(r2.is_ok());
+}
+
+// Current-thread runtime (deterministic, good for unit tests)
+#[tokio::test(flavor = "current_thread")]
+async fn sequential_processing() {
+    let result = process_sequentially(vec![1, 2, 3]).await;
+    assert_eq!(result, vec![2, 4, 6]);
+}
+```
+
+### Mock Time with `tokio::time::pause`
+
+```rust
+use tokio::time::{self, Duration, Instant};
+
+#[tokio::test]
+async fn cache_expires_after_ttl() {
+    time::pause();  // freeze the clock
+
+    let cache = Cache::with_ttl(Duration::from_secs(60));
+    cache.insert("key", "value");
+
+    assert_eq!(cache.get("key"), Some("value"));
+
+    time::advance(Duration::from_secs(61)).await;  // advance clock
+
+    assert_eq!(cache.get("key"), None);  // now expired
+}
+```
+
+---
+
+## 5. mockall
+
+```toml
+mockall = "0.12"
+```
+
+### Automock a Trait
+
+```rust
+use mockall::automock;
+
+#[automock]
+pub trait UserRepository: Send + Sync {
+    async fn find_by_id(&self, id: u64) -> Option<User>;
+    async fn save(&self, user: &User) -> Result<(), DbError>;
+    fn count(&self) -> usize;
+}
+```
+
+### Configure Expectations in Tests
+
+```rust
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use mockall::predicate::*;
+
+    #[tokio::test]
+    async fn get_user_returns_user_when_found() {
+        let mut mock = MockUserRepository::new();
+
+        mock.expect_find_by_id()
+            .with(eq(42u64))                    // match specific argument
+            .times(1)                           // must be called exactly once
+            .returning(|_| Some(User { id: 42, name: "Alice".to_string() }));
+
+        let service = UserService::new(mock);
+        let user = service.get_user(42).await.unwrap();
+        assert_eq!(user.name, "Alice");
+    }
+
+    #[tokio::test]
+    async fn get_user_returns_error_when_not_found() {
+        let mut mock = MockUserRepository::new();
+
+        mock.expect_find_by_id()
+            .returning(|_| None);  // any argument, always None
+
+        let service = UserService::new(mock);
+        let result = service.get_user(99).await;
+        assert!(matches!(result, Err(ServiceError::NotFound)));
+    }
+
+    #[test]
+    fn saves_only_valid_users() {
+        let mut mock = MockUserRepository::new();
+
+        mock.expect_save()
+            .withf(|user| !user.name.is_empty())  // custom predicate
+            .times(1)
+            .returning(|_| Ok(()));
+
+        // mock verifies expectations on drop
+    }
+}
+```
+
+### Chain Sequences of Calls
+
+```rust
+use mockall::Sequence;
+
+#[test]
+fn retries_on_first_failure() {
+    let mut mock = MockUserRepository::new();
+    let mut seq = Sequence::new();
+
+    mock.expect_count()
+        .times(1)
+        .in_sequence(&mut seq)
+        .returning(|| 0);
+
+    mock.expect_count()
+        .times(1)
+        .in_sequence(&mut seq)
+        .returning(|| 5);
+
+    assert_eq!(mock.count(), 0);
+    assert_eq!(mock.count(), 5);
+}
+```
+
+### Mock Structs (not just traits)
+
+```rust
+use mockall::mock;
+
+mock! {
+    pub HttpClient {
+        pub fn get(&self, url: &str) -> Result<String, reqwest::Error>;
+        pub fn post(&self, url: &str, body: &str) -> Result<String, reqwest::Error>;
+    }
+}
+```
+
+---
+
+## 6. Test Fixtures
+
+### Set Up and Tear Down with Drop
+
+```rust
+pub struct TestDb {
+    pub pool: sqlx::PgPool,
+    pub db_name: String,
+}
+
+impl TestDb {
+    pub async fn new() -> Self {
+        let db_name = format!("test_{}", uuid::Uuid::new_v4().simple());
+        let admin_pool = sqlx::PgPool::connect("postgres://localhost/postgres").await.unwrap();
+
+        sqlx::query(&format!("CREATE DATABASE {}", db_name))
+            .execute(&admin_pool)
+            .await
+            .unwrap();
+
+        let pool = sqlx::PgPool::connect(&format!("postgres://localhost/{}", db_name))
+            .await
+            .unwrap();
+
+        sqlx::migrate!("./migrations").run(&pool).await.unwrap();
+
+        TestDb { pool, db_name }
+    }
+}
+
+impl Drop for TestDb {
+    fn drop(&mut self) {
+        // Schedule async cleanup - use a blocking approach here
+        let db_name = self.db_name.clone();
+        std::thread::spawn(move || {
+            let rt = tokio::runtime::Runtime::new().unwrap();
+            rt.block_on(async {
+                let pool = sqlx::PgPool::connect("postgres://localhost/postgres").await.unwrap();
+                sqlx::query(&format!("DROP DATABASE IF EXISTS {}", db_name))
+                    .execute(&pool)
+                    .await
+                    .ok();
+            });
+        });
+    }
+}
+```
+
+### Share Expensive Setup with `OnceLock`
+
+```rust
+use std::sync::OnceLock;
+
+static CONFIG: OnceLock<TestConfig> = OnceLock::new();
+
+fn test_config() -> &'static TestConfig {
+    CONFIG.get_or_init(|| TestConfig::load_from_env())
+}
+
+#[test]
+fn uses_shared_config() {
+    let config = test_config();
+    assert!(!config.api_key.is_empty());
+}
+```
+
+### Use Temporary Directories
+
+```rust
+use tempfile::TempDir;
+
+#[test]
+fn writes_output_file() {
+    let dir = TempDir::new().unwrap();  // deleted on drop
+    let file_path = dir.path().join("output.txt");
+
+    write_results(&file_path, &[1, 2, 3]).unwrap();
+
+    let contents = std::fs::read_to_string(&file_path).unwrap();
+    assert!(contents.contains("1"));
+}
+
+// Keep dir alive for the test scope
+#[test]
+fn reads_fixture_from_temp() {
+    let dir = TempDir::new().unwrap();
+    std::fs::write(dir.path().join("input.json"), br#"{"key":"value"}"#).unwrap();
+
+    let result = process_file(dir.path().join("input.json")).unwrap();
+    assert_eq!(result.get("key").unwrap(), "value");
+    // dir dropped here, cleanup happens
+}
+```
+
+---
+
+## 7. Property-Based Testing
+
+```toml
+proptest = "1"
+```
+
+### Write Property Tests
+
+```rust
+use proptest::prelude::*;
+
+proptest! {
+    #[test]
+    fn parse_then_serialize_roundtrips(s in "[a-zA-Z0-9]{1,20}") {
+        let parsed = parse_identifier(&s).unwrap();
+        let serialized = serialize_identifier(&parsed);
+        prop_assert_eq!(s, serialized);
+    }
+
+    #[test]
+    fn sort_is_idempotent(mut v in prop::collection::vec(any::<i32>(), 0..100)) {
+        v.sort();
+        let sorted_once = v.clone();
+        v.sort();
+        prop_assert_eq!(sorted_once, v);
+    }
+
+    #[test]
+    fn addition_commutes(a in 0i32..1000, b in 0i32..1000) {
+        prop_assert_eq!(a + b, b + a);
+    }
+}
+```
+
+### Derive `Arbitrary` for Custom Types
+
+```rust
+use proptest_derive::Arbitrary;
+
+#[derive(Debug, Clone, Arbitrary)]
+pub struct User {
+    #[proptest(regex = "[a-z]{3,20}")]
+    pub username: String,
+    pub age: u8,
+    pub active: bool,
+}
+
+proptest! {
+    #[test]
+    fn user_validation_never_panics(user in any::<User>()) {
+        // Should return Ok or Err, never panic
+        let _ = validate_user(&user);
+    }
+}
+```
+
+### Handle Shrinking and Regression Files
+
+Proptest automatically saves failing inputs to `proptest-regressions/` and replays them on subsequent runs. Commit these files to catch regressions. Suppress with `#[proptest(skip_shrink)]` for expensive types.
+
+---
+
+## 8. Benchmarks
+
+```toml
+[dev-dependencies]
+criterion = { version = "0.5", features = ["html_reports"] }
+
+[[bench]]
+name = "my_bench"
+harness = false
+```
+
+### Write Criterion Benchmarks
+
+```rust
+// benches/my_bench.rs
+use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
+use my_crate::{parse, process};
+
+fn bench_parse(c: &mut Criterion) {
+    let input = "example input string";
+
+    c.bench_function("parse_simple", |b| {
+        b.iter(|| parse(criterion::black_box(input)))
+    });
+}
+
+fn bench_process_sizes(c: &mut Criterion) {
+    let mut group = c.benchmark_group("process");
+
+    for size in [100usize, 1_000, 10_000] {
+        let data: Vec<u8> = (0..size).map(|i| i as u8).collect();
+
+        group.throughput(Throughput::Bytes(size as u64));
+        group.bench_with_input(BenchmarkId::from_parameter(size), &data, |b, data| {
+            b.iter(|| process(criterion::black_box(data)))
+        });
+    }
+
+    group.finish();
+}
+
+fn bench_comparison(c: &mut Criterion) {
+    let mut group = c.benchmark_group("sort_comparison");
+    let data: Vec<i32> = (0..1000).rev().collect();
+
+    group.bench_function("std_sort", |b| {
+        b.iter(|| {
+            let mut v = data.clone();
+            v.sort();
+            v
+        })
+    });
+
+    group.bench_function("unstable_sort", |b| {
+        b.iter(|| {
+            let mut v = data.clone();
+            v.sort_unstable();
+            v
+        })
+    });
+
+    group.finish();
+}
+
+criterion_group!(benches, bench_parse, bench_process_sizes, bench_comparison);
+criterion_main!(benches);
+```
+
+### Run Benchmarks and Generate Flamegraphs
+
+```bash
+# Run all benchmarks
+cargo bench
+
+# Run specific benchmark
+cargo bench --bench my_bench parse
+
+# Save baseline for comparison
+cargo bench -- --save-baseline before
+# ... make changes ...
+cargo bench -- --baseline before
+
+# Generate flamegraph (requires cargo-flamegraph and perf/dtrace)
+cargo flamegraph --bench my_bench -- --bench bench_parse
+```
+
+---
+
+## 9. Snapshot Testing
+
+```toml
+insta = { version = "1", features = ["json", "yaml", "redactions"] }
+```
+
+### Assert with Snapshots
+
+```rust
+use insta::assert_snapshot;
+
+#[test]
+fn renders_report() {
+    let report = generate_report(&sample_data());
+    assert_snapshot!(report);
+    // First run: creates snapshot file in snapshots/ directory
+    // Subsequent runs: compares against saved snapshot
+}
+
+// JSON snapshots (pretty-printed, sorted keys)
+use insta::assert_json_snapshot;
+
+#[test]
+fn serializes_user() {
+    let user = User { id: 1, name: "Alice".into(), active: true };
+    assert_json_snapshot!(user);
+}
+```
+
+### Use Redactions for Dynamic Values
+
+```rust
+use insta::assert_json_snapshot;
+
+#[test]
+fn snapshot_with_dynamic_id() {
+    let response = create_item("test");
+    assert_json_snapshot!(response, {
+        ".id" => "[id]",                // replace dynamic id
+        ".created_at" => "[timestamp]", // replace timestamp
+    });
+}
+```
+
+### Review and Accept Snapshots
+
+```bash
+# Install the review tool
+cargo install cargo-insta
+
+# Run tests (failures create .snap.new files)
+cargo test
+
+# Review all pending snapshots interactively
+cargo insta review
+
+# Accept all pending snapshots at once
+cargo insta accept
+```
+
+Commit `.snap` files alongside code. They are the expected output and act as documentation.
+
+---
+
+## 10. Test Organization
+
+### Build a Common Test Utilities Module
+
+```
+tests/
+├── common/
+│   ├── mod.rs          # re-exports all helpers
+│   ├── fixtures.rs     # load JSON/TOML test data
+│   ├── builders.rs     # test builder patterns for structs
+│   └── assertions.rs   # custom assert helpers
+```
+
+```rust
+// tests/common/builders.rs
+pub struct UserBuilder {
+    id: u64,
+    name: String,
+    email: String,
+}
+
+impl UserBuilder {
+    pub fn new() -> Self {
+        UserBuilder { id: 1, name: "Test User".into(), email: "test@example.com".into() }
+    }
+    pub fn id(mut self, id: u64) -> Self { self.id = id; self }
+    pub fn name(mut self, name: impl Into<String>) -> Self { self.name = name.into(); self }
+    pub fn build(self) -> User {
+        User { id: self.id, name: self.name, email: self.email }
+    }
+}
+```
+
+### Extract a `test-utils` Workspace Crate
+
+For large workspaces, extract test utilities into a dedicated crate:
+
+```toml
+# Cargo.toml (workspace root)
+[workspace]
+members = ["my-app", "my-lib", "test-utils"]
+
+# my-lib/Cargo.toml
+[dev-dependencies]
+test-utils = { path = "../test-utils" }
+```
+
+This avoids duplicating helpers across crates and allows `#[cfg(test)]`-gated re-exports.
+
+### Write Custom Assertion Helpers
+
+```rust
+// tests/common/assertions.rs
+pub fn assert_sorted<T: Ord + std::fmt::Debug>(items: &[T]) {
+    for window in items.windows(2) {
+        assert!(
+            window[0] <= window[1],
+            "Expected sorted slice, found {:?} before {:?}",
+            window[0], window[1]
+        );
+    }
+}
+
+pub fn assert_error_contains(result: &anyhow::Result<()>, expected: &str) {
+    match result {
+        Err(e) => assert!(
+            e.to_string().contains(expected),
+            "Expected error to contain '{}', got: {}",
+            expected, e
+        ),
+        Ok(_) => panic!("Expected error containing '{}', got Ok", expected),
+    }
+}
+```
+
+---
+
+## 11. CI Patterns
+
+### Run the Full Test Suite
+
+```yaml
+# .github/workflows/ci.yml
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          components: clippy, rustfmt
+      - uses: Swatinem/rust-cache@v2
+
+      - name: Format check
+        run: cargo fmt --all -- --check
+
+      - name: Lint
+        run: cargo clippy --all-targets --all-features -- -D warnings
+
+      - name: Test
+        run: cargo test --workspace --all-features
+        env:
+          DATABASE_URL: postgres://postgres:postgres@localhost/test
+
+      - name: Doc test
+        run: cargo test --doc --workspace
+```
+
+### Test a Feature Matrix
+
+```yaml
+strategy:
+  matrix:
+    features: ["", "feature-a", "feature-b", "full"]
+steps:
+  - name: Test feature set
+    run: cargo test --no-default-features --features "${{ matrix.features }}"
+```
+
+### Measure Coverage with `cargo-llvm-cov`
+
+```bash
+# Install
+cargo install cargo-llvm-cov
+
+# Generate coverage report
+cargo llvm-cov --workspace --all-features --lcov --output-path lcov.info
+
+# HTML report locally
+cargo llvm-cov --workspace --html
+open target/llvm-cov/html/index.html
+```
+
+```yaml
+# In CI
+- name: Coverage
+  run: cargo llvm-cov --workspace --all-features --lcov --output-path lcov.info
+- uses: codecov/codecov-action@v4
+  with:
+    files: lcov.info
+```
+
+### Run Tests Against a Live Database in CI
+
+```yaml
+services:
+  postgres:
+    image: postgres:16
+    env:
+      POSTGRES_PASSWORD: postgres
+      POSTGRES_DB: test
+    ports:
+      - 5432:5432
+    options: >-
+      --health-cmd pg_isready
+      --health-interval 10s
+      --health-timeout 5s
+      --health-retries 5
+```
+
+### Check for Unused Dependencies
+
+```bash
+cargo install cargo-machete
+cargo machete
+
+# Or for dependency audit
+cargo install cargo-audit
+cargo audit
+```
+
+### Enforce MSRV (Minimum Supported Rust Version)
+
+```toml
+# Cargo.toml
+[package]
+rust-version = "1.75"
+```
+
+```yaml
+- uses: dtolnay/rust-toolchain@1.75
+- run: cargo test --workspace
+```

+ 715 - 0
skills/rust-ops/references/traits-generics.md

@@ -0,0 +1,715 @@
+# Traits and Generics Reference
+
+## Table of Contents
+
+1. [Trait Definition](#1-trait-definition)
+2. [Trait Bounds](#2-trait-bounds)
+3. [Associated Types vs Generic Parameters](#3-associated-types-vs-generic-parameters)
+4. [Supertraits](#4-supertraits)
+5. [Trait Objects](#5-trait-objects)
+6. [Derive Macros](#6-derive-macros)
+7. [Common Trait Implementations](#7-common-trait-implementations)
+8. [Sealed Traits](#8-sealed-traits)
+9. [Extension Traits](#9-extension-traits)
+10. [Generics](#10-generics)
+11. [Blanket Implementations](#11-blanket-implementations)
+
+---
+
+## 1. Trait Definition
+
+### Define Methods, Default Implementations, and Associated Functions
+
+```rust
+pub trait Greet {
+    // Required method — implementors must provide this
+    fn name(&self) -> &str;
+
+    // Default method — implementors may override
+    fn greeting(&self) -> String {
+        format!("Hello, {}!", self.name())
+    }
+
+    // Associated function (no self) — often used as constructors
+    fn kind() -> &'static str {
+        "greeter"
+    }
+}
+
+struct Person {
+    name: String,
+}
+
+impl Greet for Person {
+    fn name(&self) -> &str {
+        &self.name
+    }
+    // greeting() uses the default implementation
+}
+
+let p = Person { name: "Alice".into() };
+println!("{}", p.greeting()); // "Hello, Alice!"
+println!("{}", Person::kind()); // "greeter"
+```
+
+### Define Traits with Associated Types and Constants
+
+```rust
+pub trait Encode {
+    type Output;
+    const VERSION: u8 = 1;
+
+    fn encode(&self) -> Self::Output;
+}
+
+struct Json(String);
+
+impl Encode for Json {
+    type Output = Vec<u8>;
+    const VERSION: u8 = 2; // override default
+
+    fn encode(&self) -> Vec<u8> {
+        self.0.as_bytes().to_vec()
+    }
+}
+```
+
+---
+
+## 2. Trait Bounds
+
+### Apply Bounds to Functions
+
+```rust
+// Inline bound
+fn print_item<T: std::fmt::Display>(item: T) {
+    println!("{}", item);
+}
+
+// Where clause — cleaner for multiple or complex bounds
+fn log<T, E>(result: Result<T, E>)
+where
+    T: std::fmt::Debug,
+    E: std::fmt::Display,
+{
+    match result {
+        Ok(v) => println!("OK: {:?}", v),
+        Err(e) => eprintln!("ERR: {}", e),
+    }
+}
+
+// Multiple bounds with +
+fn serialize_and_print<T: serde::Serialize + std::fmt::Debug>(val: &T) {
+    println!("{:?}", val);
+    let json = serde_json::to_string(val).unwrap();
+    println!("{}", json);
+}
+```
+
+### Use impl Trait in Argument Position
+
+`impl Trait` in argument position is syntactic sugar for a generic parameter with that bound. Each call site can use a different concrete type.
+
+```rust
+// These are equivalent
+fn process(item: impl std::fmt::Display) { println!("{}", item); }
+fn process<T: std::fmt::Display>(item: T) { println!("{}", item); }
+
+// impl Trait in return position — hides the concrete type
+fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
+    move |y| x + y
+}
+```
+
+Note: `impl Trait` in return position always returns the same concrete type — it is not a trait object. Use `Box<dyn Trait>` when you need to return different types.
+
+### Apply Bounds to Structs and Impls
+
+```rust
+struct Wrapper<T: Clone> {
+    val: T,
+}
+
+// Bound on impl — methods only available when T: Clone + std::fmt::Debug
+impl<T: Clone + std::fmt::Debug> Wrapper<T> {
+    fn inspect(&self) -> T {
+        println!("{:?}", self.val);
+        self.val.clone()
+    }
+}
+```
+
+---
+
+## 3. Associated Types vs Generic Parameters
+
+### Choose Associated Types for One-to-One Relationships
+
+Use associated types when there is only one sensible implementation per type. `Iterator` is the canonical example — a type can only produce one kind of item.
+
+```rust
+// Associated type: Vec<i32> implements Iterator<Item = &i32>
+// There is exactly one Item type per implementor
+trait Iterator {
+    type Item;
+    fn next(&mut self) -> Option<Self::Item>;
+}
+
+// Caller syntax is clean
+fn sum_iter<I: Iterator<Item = i32>>(mut it: I) -> i32 {
+    let mut total = 0;
+    while let Some(n) = it.next() { total += n; }
+    total
+}
+```
+
+### Choose Generic Parameters for Multiple Implementations
+
+Use generic parameters when a type may implement the trait for many different type arguments. `From<T>` is the canonical example — `String` implements `From<&str>`, `From<char>`, etc.
+
+```rust
+// Generic parameter: String can implement Converter for many T
+trait Converter<T> {
+    fn convert(&self) -> T;
+}
+
+struct Celsius(f64);
+
+impl Converter<f64> for Celsius {
+    fn convert(&self) -> f64 { self.0 }
+}
+
+impl Converter<String> for Celsius {
+    fn convert(&self) -> String { format!("{}°C", self.0) }
+}
+
+let c = Celsius(100.0);
+let f: f64 = c.convert();
+let s: String = c.convert();
+```
+
+---
+
+## 4. Supertraits
+
+### Require Other Traits
+
+A supertrait is a trait that must be implemented before another trait can be implemented. Declare it with `:` after the trait name.
+
+```rust
+use std::fmt;
+
+// Animal requires Display and Debug
+trait Animal: fmt::Display + fmt::Debug {
+    fn sound(&self) -> &str;
+}
+
+#[derive(Debug)]
+struct Dog;
+
+impl fmt::Display for Dog {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "Dog")
+    }
+}
+
+impl Animal for Dog {
+    fn sound(&self) -> &str { "woof" }
+}
+```
+
+### Coerce to a Supertrait Object
+
+You can use a trait object of the supertrait when you only need shared functionality.
+
+```rust
+fn describe(animal: &dyn fmt::Display) {
+    println!("{}", animal);
+}
+
+let dog = Dog;
+describe(&dog as &dyn fmt::Display);
+```
+
+---
+
+## 5. Trait Objects
+
+### Create and Use dyn Trait
+
+A trait object (`dyn Trait`) is a fat pointer: a data pointer plus a vtable pointer. They enable dynamic dispatch.
+
+```rust
+trait Shape {
+    fn area(&self) -> f64;
+}
+
+struct Circle { radius: f64 }
+struct Square { side: f64 }
+
+impl Shape for Circle {
+    fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
+}
+
+impl Shape for Square {
+    fn area(&self) -> f64 { self.side * self.side }
+}
+
+// Heterogeneous collection via Box<dyn Trait>
+let shapes: Vec<Box<dyn Shape>> = vec![
+    Box::new(Circle { radius: 1.0 }),
+    Box::new(Square { side: 2.0 }),
+];
+
+for shape in &shapes {
+    println!("area = {:.2}", shape.area());
+}
+```
+
+### Satisfy Object Safety Rules
+
+A trait is object-safe (usable as `dyn Trait`) if:
+- It has no methods that return `Self`
+- It has no generic methods
+- All methods are dispatchable (take `&self`, `&mut self`, or `Box<Self>`)
+
+```rust
+// NOT object-safe: clone() returns Self
+// trait Cloneable: Clone {} // cannot be dyn
+
+// Object-safe version: return Box<dyn Trait>
+trait DynClone {
+    fn clone_box(&self) -> Box<dyn DynClone>;
+}
+
+// NOT object-safe: generic method
+trait Bad {
+    fn convert<T>(&self) -> T; // generic method — not dispatchable
+}
+
+// OK: use associated type instead
+trait Good {
+    type Output;
+    fn convert(&self) -> Self::Output;
+}
+```
+
+### Add Send + Sync to Trait Objects for Threads
+
+```rust
+// Sendable trait object
+fn spawn_worker(task: Box<dyn Fn() + Send + 'static>) {
+    std::thread::spawn(task);
+}
+
+// Arc<dyn Trait + Send + Sync> for shared access across threads
+use std::sync::Arc;
+let shared: Arc<dyn Shape + Send + Sync> = Arc::new(Circle { radius: 1.0 });
+```
+
+---
+
+## 6. Derive Macros
+
+### Use Common Derives
+
+```rust
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
+struct Config {
+    name: String,
+    value: u32,
+}
+
+// Debug: {:?} and {:#?} formatting
+// Clone: .clone() method
+// PartialEq/Eq: == and != operators
+// Hash: usable as HashMap/HashSet key (requires PartialEq + Eq)
+// Default: Config::default() returns Config { name: "", value: 0 }
+
+#[derive(PartialOrd, Ord, PartialEq, Eq)]
+struct Version(u32, u32, u32);
+
+// PartialOrd/Ord: <, >, <=, >= operators; enables .sort() on Vec<Version>
+// Ord requires PartialOrd; PartialOrd requires PartialEq
+```
+
+### Extend with the derive_more Crate
+
+`derive_more` provides derives for common trait impls that the standard library does not include.
+
+```rust
+// Cargo.toml: derive_more = { version = "1", features = ["display", "from", "into"] }
+use derive_more::{Display, From, Into};
+
+#[derive(Display, From, Into)]
+#[display("User({name}, {id})")]
+struct UserId {
+    name: String,
+    id: u64,
+}
+
+let id = UserId::from(("Alice".to_string(), 42u64));
+let (name, num): (String, u64) = id.into();
+```
+
+---
+
+## 7. Common Trait Implementations
+
+### Implement Display
+
+```rust
+use std::fmt;
+
+struct Point { x: f64, y: f64 }
+
+impl fmt::Display for Point {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "({:.2}, {:.2})", self.x, self.y)
+    }
+}
+
+// Implementing Display gives .to_string() for free via blanket impl
+let p = Point { x: 1.0, y: 2.0 };
+println!("{}", p);
+let s: String = p.to_string();
+```
+
+### Implement From and Into
+
+Implement `From`; `Into` is derived automatically via a blanket impl.
+
+```rust
+struct Meters(f64);
+struct Feet(f64);
+
+impl From<Meters> for Feet {
+    fn from(m: Meters) -> Self {
+        Feet(m.0 * 3.28084)
+    }
+}
+
+let m = Meters(1.0);
+let f: Feet = m.into();      // Into<Feet> for Meters — derived from From
+let f2 = Feet::from(Meters(2.0)); // From<Meters> for Feet
+```
+
+### Implement FromStr
+
+```rust
+use std::str::FromStr;
+
+#[derive(Debug)]
+struct Color { r: u8, g: u8, b: u8 }
+
+#[derive(Debug)]
+struct ParseColorError(String);
+
+impl std::fmt::Display for ParseColorError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "parse color error: {}", self.0)
+    }
+}
+
+impl FromStr for Color {
+    type Err = ParseColorError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let parts: Vec<&str> = s.split(',').collect();
+        if parts.len() != 3 {
+            return Err(ParseColorError("expected R,G,B".into()));
+        }
+        let parse = |p: &str| p.trim().parse::<u8>()
+            .map_err(|e| ParseColorError(e.to_string()));
+        Ok(Color { r: parse(parts[0])?, g: parse(parts[1])?, b: parse(parts[2])? })
+    }
+}
+
+let c: Color = "255, 128, 0".parse().unwrap();
+```
+
+### Implement Deref and DerefMut
+
+`Deref` enables the `*` operator and auto-deref coercions. Use it for smart-pointer-like types, not general type conversions.
+
+```rust
+use std::ops::{Deref, DerefMut};
+
+struct Wrapper<T>(Vec<T>);
+
+impl<T> Deref for Wrapper<T> {
+    type Target = Vec<T>;
+    fn deref(&self) -> &Vec<T> { &self.0 }
+}
+
+impl<T> DerefMut for Wrapper<T> {
+    fn deref_mut(&mut self) -> &mut Vec<T> { &mut self.0 }
+}
+
+let mut w = Wrapper(vec![1, 2, 3]);
+w.push(4);         // DerefMut: Vec::push via auto-deref
+println!("{}", w.len()); // Deref: Vec::len via auto-deref
+```
+
+### Implement AsRef and AsMut
+
+`AsRef<T>` is for cheap reference conversions. Prefer it over `Deref` in function parameters.
+
+```rust
+// Accept String, &str, PathBuf, &Path, etc. — anything that is AsRef<str>
+fn print_upper(s: impl AsRef<str>) {
+    println!("{}", s.as_ref().to_uppercase());
+}
+
+print_upper("hello");
+print_upper(String::from("world"));
+
+// AsRef<Path> for filesystem functions
+fn read_config(path: impl AsRef<std::path::Path>) -> std::io::Result<String> {
+    std::fs::read_to_string(path)
+}
+```
+
+---
+
+## 8. Sealed Traits
+
+### Prevent External Implementations
+
+The sealed trait pattern restricts who can implement a trait — useful for stable API surfaces in libraries.
+
+```rust
+// In your library crate
+mod private {
+    pub trait Sealed {}
+}
+
+pub trait MyTrait: private::Sealed {
+    fn do_thing(&self);
+}
+
+// Implement Sealed only for types you control
+pub struct TypeA;
+pub struct TypeB;
+
+impl private::Sealed for TypeA {}
+impl private::Sealed for TypeB {}
+
+impl MyTrait for TypeA {
+    fn do_thing(&self) { println!("A"); }
+}
+
+impl MyTrait for TypeB {
+    fn do_thing(&self) { println!("B"); }
+}
+
+// External users CANNOT implement MyTrait because they cannot implement
+// private::Sealed (it's not publicly accessible)
+```
+
+---
+
+## 9. Extension Traits
+
+### Add Methods to Foreign Types
+
+Extension traits let you add methods to types you do not own, including primitives and standard library types.
+
+```rust
+pub trait StrExt {
+    fn word_count(&self) -> usize;
+    fn capitalize(&self) -> String;
+}
+
+impl StrExt for str {
+    fn word_count(&self) -> usize {
+        self.split_whitespace().count()
+    }
+
+    fn capitalize(&self) -> String {
+        let mut chars = self.chars();
+        match chars.next() {
+            None => String::new(),
+            Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
+        }
+    }
+}
+
+// Bring the trait into scope to use the methods
+use crate::StrExt;
+println!("{}", "hello world".word_count()); // 2
+println!("{}", "hello".capitalize());       // "Hello"
+```
+
+Extension traits must be in scope (imported) to use their methods. This is why `use std::io::Write` and similar imports are necessary.
+
+---
+
+## 10. Generics
+
+### Use Type Parameters
+
+```rust
+// Generic struct
+struct Stack<T> {
+    items: Vec<T>,
+}
+
+impl<T> Stack<T> {
+    fn push(&mut self, item: T) { self.items.push(item); }
+    fn pop(&mut self) -> Option<T> { self.items.pop() }
+    fn is_empty(&self) -> bool { self.items.is_empty() }
+}
+
+// Generic enum
+enum Either<L, R> {
+    Left(L),
+    Right(R),
+}
+```
+
+### Use Const Generics
+
+Const generics allow types to be parameterized by constant values (integers, booleans, chars).
+
+```rust
+// Array type parameterized by size — zero-cost abstraction
+struct Matrix<T, const ROWS: usize, const COLS: usize> {
+    data: [[T; COLS]; ROWS],
+}
+
+impl<T: Default + Copy, const R: usize, const C: usize> Matrix<T, R, C> {
+    fn new() -> Self {
+        Matrix { data: [[T::default(); C]; R] }
+    }
+
+    fn rows(&self) -> usize { R }
+    fn cols(&self) -> usize { C }
+}
+
+let m: Matrix<f64, 3, 4> = Matrix::new();
+assert_eq!(m.rows(), 3);
+```
+
+### Use PhantomData for Marker Types
+
+`PhantomData<T>` tells the compiler that a type logically contains `T` without storing it, affecting variance and drop checking.
+
+```rust
+use std::marker::PhantomData;
+
+// A typed ID that cannot be mixed between entity types
+struct Id<T> {
+    value: u64,
+    _phantom: PhantomData<T>,
+}
+
+impl<T> Id<T> {
+    fn new(value: u64) -> Self {
+        Id { value, _phantom: PhantomData }
+    }
+}
+
+struct User;
+struct Order;
+
+let user_id: Id<User> = Id::new(1);
+let order_id: Id<Order> = Id::new(1);
+// Cannot mix: user_id and order_id are different types even though value is same
+```
+
+### Use the Turbofish Syntax
+
+When the compiler cannot infer a generic type argument, use `::<>` (turbofish) to supply it explicitly.
+
+```rust
+// Collect requires knowing what to collect into
+let nums: Vec<i32> = "1 2 3".split(' ')
+    .map(|s| s.parse::<i32>().unwrap()) // turbofish on parse
+    .collect::<Vec<_>>();               // turbofish on collect (alternative to type annotation)
+
+// Any generic function may need turbofish
+fn identity<T>(val: T) -> T { val }
+let x = identity::<String>(String::from("hello"));
+```
+
+### Express Lifetime Constraints on Generic Types
+
+`T: 'a` means "all references inside T live at least as long as 'a". This is required when storing generic types behind references.
+
+```rust
+struct Holder<'a, T: 'a> {
+    reference: &'a T,
+}
+
+// 'static bound: T contains no non-static references
+// (common for thread-spawning and stored callbacks)
+fn store_callback<F: Fn() + Send + 'static>(f: F) {
+    std::thread::spawn(f);
+}
+```
+
+---
+
+## 11. Blanket Implementations
+
+### Understand Blanket Impls
+
+A blanket implementation applies a trait to any type that satisfies certain bounds, rather than to a specific named type.
+
+```rust
+// From the standard library — any T that implements Display also gets ToString
+impl<T: std::fmt::Display> ToString for T {
+    fn to_string(&self) -> String {
+        format!("{}", self)
+    }
+}
+
+// This is why any Display type has .to_string() for free
+42i32.to_string();
+3.14f64.to_string();
+```
+
+### Write Blanket Impls for Your Own Traits
+
+```rust
+trait Summary {
+    fn summarize(&self) -> String;
+}
+
+// Any type that implements Display also gets a free Summary impl
+impl<T: std::fmt::Display> Summary for T {
+    fn summarize(&self) -> String {
+        format!("Summary: {}", self)
+    }
+}
+```
+
+Be careful: blanket impls can create conflicts. Two blanket impls that could overlap will fail to compile (the orphan rule plus coherence checking prevents ambiguity).
+
+### Apply the Orphan Rule
+
+You can implement a trait for a type only if either the trait or the type is defined in your crate. Both cannot be foreign.
+
+```rust
+// OK: MyTrait (yours) for String (foreign)
+impl MyTrait for String { ... }
+
+// OK: Display (foreign) for MyType (yours)
+impl std::fmt::Display for MyType { ... }
+
+// ERROR: Display (foreign) for Vec<T> (foreign) — orphan rule violation
+// impl std::fmt::Display for Vec<i32> { ... }
+```
+
+Work around this using the newtype pattern:
+
+```rust
+struct MyVec(Vec<i32>);
+
+impl std::fmt::Display for MyVec {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:?}", self.0)
+    }
+}
+```

+ 0 - 0
skills/rust-ops/scripts/.gitkeep


+ 6 - 0
skills/tool-discovery/SKILL.md

@@ -33,6 +33,12 @@ Is this a reference/lookup task?
 | **structural-search** | ast-grep, sg, ast pattern |
 | **git-workflow** | lazygit, gh, delta, rebase |
 | **python-env** | uv, venv, pyproject |
+| **go-ops** | golang, go, goroutine, channel, context, errgroup, go test |
+| **rust-ops** | rust, cargo, ownership, tokio, serde, trait, Result, Option |
+| **typescript-ops** | typescript, type system, generics, utility types, Zod |
+| **docker-ops** | docker, Dockerfile, docker-compose, multi-stage build |
+| **ci-cd-ops** | github actions, CI, CD, pipeline, release, workflow |
+| **api-design-ops** | api design, gRPC, GraphQL, REST advanced, protobuf |
 | **rest-ops** | http methods, status codes |
 | **sql-ops** | cte, window functions |
 | **postgres-ops** | postgresql, postgres, EXPLAIN ANALYZE, vacuum, pgbouncer, JSONB, RLS, replication |

+ 102 - 2
skills/tool-discovery/references/skills-catalog.md

@@ -2,6 +2,102 @@
 
 Complete reference for all available skills.
 
+## Language & Framework Skills
+
+Comprehensive operational expertise for specific languages and frameworks.
+
+### go-ops
+
+**Triggers:** golang, go, goroutine, channel, context, errgroup, go test, go mod, interface, generics, go build, worker pool
+
+**Use For:**
+- Concurrency patterns (goroutines, channels, errgroup, worker pools)
+- Error handling (sentinel errors, custom types, wrapping, errors.Is/As)
+- Testing (table-driven, httptest, benchmarks, fuzz, mocking with interfaces)
+- Interface design, generics, functional options
+- Project structure, module management, workspaces
+- Performance profiling (pprof, trace, escape analysis)
+
+**References:** concurrency.md, error-handling.md, testing.md, interfaces-generics.md, project-structure.md, performance.md
+
+---
+
+### rust-ops
+
+**Triggers:** rust, cargo, ownership, borrow checker, lifetime, tokio, serde, trait, Result, Option, async rust, crate
+
+**Use For:**
+- Ownership, borrowing, lifetimes, interior mutability
+- Traits, generics, associated types, derive macros
+- Error handling (thiserror, anyhow, Result/Option combinators)
+- Async with tokio (spawn, channels, select, graceful shutdown)
+- Ecosystem (serde, clap, reqwest, sqlx, axum, tracing, rayon)
+- Testing (mockall, proptest, criterion, insta)
+
+**References:** ownership-lifetimes.md, traits-generics.md, error-handling.md, async-tokio.md, ecosystem.md, testing.md
+
+---
+
+### typescript-ops
+
+**Triggers:** typescript, type system, generics, utility types, Zod, mapped types, conditional types, tsconfig, strict mode
+
+**Use For:**
+- Type narrowing, type guards, discriminated unions
+- Generics, conditional types, mapped types, template literal types
+- Utility types (Partial, Pick, Omit, Record, ReturnType, etc.)
+- tsconfig configuration, strict mode migration
+- Runtime validation (Zod, Valibot), type-safe APIs (tRPC)
+
+**References:** type-system.md, generics-patterns.md, utility-types.md, config-strict.md, ecosystem.md
+
+---
+
+## Infrastructure Skills
+
+### docker-ops
+
+**Triggers:** docker, Dockerfile, docker-compose, container, image, multi-stage build, distroless, BuildKit
+
+**Use For:**
+- Dockerfile best practices, multi-stage builds (Go, Rust, Node, Python)
+- Docker Compose patterns (services, volumes, networking, health checks)
+- Image optimization, layer caching, security scanning
+- BuildKit features, cross-platform builds
+
+**References:** multi-stage-builds.md, compose-patterns.md, optimization.md
+
+---
+
+### ci-cd-ops
+
+**Triggers:** github actions, CI, CD, pipeline, workflow, release, semantic release, changesets, goreleaser
+
+**Use For:**
+- GitHub Actions workflow syntax, triggers, matrix strategy
+- Caching strategies (node_modules, go modules, cargo, pip)
+- Release automation (semantic-release, changesets, goreleaser)
+- Testing pipelines, code coverage, deployment gates
+
+**References:** github-actions.md, release-automation.md, testing-pipelines.md
+
+---
+
+### api-design-ops
+
+**Triggers:** api design, gRPC, GraphQL, protobuf, api versioning, pagination, rate limiting, webhook, idempotency
+
+**Use For:**
+- API style selection (REST vs gRPC vs GraphQL)
+- REST advanced patterns (pagination, PATCH, bulk ops, webhooks)
+- gRPC (protobuf, streaming, Go/Rust implementations)
+- GraphQL (schema design, DataLoader, federation)
+- API security (JWT, OAuth2, rate limiting, OWASP API Top 10)
+
+**References:** rest-advanced.md, grpc.md, graphql.md, api-security.md
+
+---
+
 ## Pattern Skills
 
 Quick reference for common patterns and syntax.
@@ -252,9 +348,13 @@ Project and development workflow automation.
 | JSON files | data-processing |
 | YAML/TOML | data-processing |
 | SQL databases | sql-ops, postgres-ops, sqlite-ops |
-| TypeScript/JS | file-search, structural-search |
+| Go | go-ops |
+| Rust | rust-ops |
+| TypeScript/JS | typescript-ops, file-search, structural-search |
 | Python | python-env, structural-search |
-| API endpoints | rest-ops |
+| API design | api-design-ops, rest-ops |
+| Docker/containers | docker-ops, container-orchestration |
+| CI/CD | ci-cd-ops, git-workflow |
 | CSS/Tailwind | tailwind-ops |
 
 ### By Task

+ 262 - 0
skills/typescript-ops/SKILL.md

@@ -0,0 +1,262 @@
+---
+name: typescript-ops
+description: "TypeScript type system, generics, utility types, strict mode, and ecosystem patterns. Use for: typescript, ts, type, generic, utility type, Partial, Pick, Omit, Record, Exclude, Extract, ReturnType, Parameters, keyof, typeof, infer, mapped type, conditional type, template literal type, discriminated union, type guard, type assertion, type narrowing, tsconfig, strict mode, declaration file, zod, valibot."
+allowed-tools: "Read Write Bash"
+related-skills: [react-ops, testing-ops]
+---
+
+# TypeScript Operations
+
+Comprehensive TypeScript skill covering the type system, generics, and production patterns.
+
+## Type Narrowing Decision Tree
+
+```
+How to narrow a type?
+│
+├─ Primitive type check
+│  └─ typeof: typeof x === "string"
+│
+├─ Instance check
+│  └─ instanceof: x instanceof Date
+│
+├─ Property existence
+│  └─ in: "email" in user
+│
+├─ Discriminated union
+│  └─ switch on literal field: switch (event.type)
+│
+├─ Null/undefined check
+│  └─ Truthiness: if (x) or if (x != null)
+│
+├─ Custom logic
+│  └─ Type predicate: function isUser(x: unknown): x is User
+│
+└─ Assertion (you know better than TS)
+   └─ as: value as string (escape hatch, avoid when possible)
+```
+
+### Type Guard Example
+
+```typescript
+interface Dog { bark(): void; breed: string }
+interface Cat { meow(): void; color: string }
+
+function isDog(pet: Dog | Cat): pet is Dog {
+    return "bark" in pet;
+}
+
+function handlePet(pet: Dog | Cat) {
+    if (isDog(pet)) {
+        pet.bark(); // TS knows it's Dog here
+    } else {
+        pet.meow(); // TS knows it's Cat here
+    }
+}
+```
+
+### Discriminated Unions
+
+```typescript
+type Result<T> =
+    | { status: "success"; data: T }
+    | { status: "error"; error: string }
+    | { status: "loading" };
+
+function handle<T>(result: Result<T>) {
+    switch (result.status) {
+        case "success": return result.data;     // data is available
+        case "error":   throw new Error(result.error); // error is available
+        case "loading": return null;
+    }
+    // Exhaustiveness check: result is `never` here
+    const _exhaustive: never = result;
+}
+```
+
+## Utility Types Cheat Sheet
+
+| Utility | What It Does | Example |
+|---------|-------------|---------|
+| `Partial<T>` | All props optional | `Partial<User>` for update payloads |
+| `Required<T>` | All props required | `Required<Config>` for validated config |
+| `Readonly<T>` | All props readonly | `Readonly<State>` for immutable state |
+| `Pick<T, K>` | Select specific props | `Pick<User, "id" \| "name">` |
+| `Omit<T, K>` | Remove specific props | `Omit<User, "password">` |
+| `Record<K, V>` | Object with typed keys/values | `Record<string, number>` |
+| `Exclude<U, E>` | Remove types from union | `Exclude<Status, "deleted">` |
+| `Extract<U, E>` | Keep types from union | `Extract<Event, { type: "click" }>` |
+| `NonNullable<T>` | Remove null/undefined | `NonNullable<string \| null>` |
+| `ReturnType<F>` | Function return type | `ReturnType<typeof fetchUser>` |
+| `Parameters<F>` | Function params as tuple | `Parameters<typeof createUser>` |
+| `Awaited<T>` | Unwrap Promise type | `Awaited<Promise<User>>` = `User` |
+
+## Generic Patterns
+
+### Constrained Generics
+
+```typescript
+// Basic constraint
+function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
+    return obj[key];
+}
+
+// Multiple constraints
+function merge<T extends object, U extends object>(a: T, b: U): T & U {
+    return { ...a, ...b };
+}
+
+// Default generic type
+type ApiResponse<T = unknown> = {
+    data: T;
+    status: number;
+};
+```
+
+### Conditional Types
+
+```typescript
+// Basic conditional
+type IsString<T> = T extends string ? true : false;
+
+// infer keyword - extract inner type
+type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
+type UnwrapArray<T> = T extends (infer U)[] ? U : T;
+
+// Distributive conditional (distributes over union)
+type ToArray<T> = T extends any ? T[] : never;
+// ToArray<string | number> = string[] | number[]
+
+// Prevent distribution with wrapping
+type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
+// ToArrayNonDist<string | number> = (string | number)[]
+```
+
+### Mapped Types
+
+```typescript
+// Make all properties optional and nullable
+type Nullable<T> = { [K in keyof T]: T[K] | null };
+
+// Add prefix to keys
+type Prefixed<T, P extends string> = {
+    [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
+};
+// Prefixed<{ name: string }, "get"> = { getName: string }
+
+// Filter keys by value type
+type StringKeys<T> = {
+    [K in keyof T as T[K] extends string ? K : never]: T[K];
+};
+```
+
+**Deep dive**: Load `./references/generics-patterns.md` for advanced type-level programming, recursive types, template literal types.
+
+## tsconfig Quick Reference
+
+```jsonc
+{
+    "compilerOptions": {
+        // Strict mode (always enable)
+        "strict": true,               // Enables all strict checks
+        "noUncheckedIndexedAccess": true,  // arr[0] is T | undefined
+
+        // Module system
+        "module": "esnext",           // or "nodenext" for Node
+        "moduleResolution": "bundler", // or "nodenext"
+        "esModuleInterop": true,
+
+        // Output
+        "target": "es2022",
+        "outDir": "dist",
+        "declaration": true,          // Generate .d.ts
+        "sourceMap": true,
+
+        // Paths
+        "baseUrl": ".",
+        "paths": { "@/*": ["src/*"] },
+
+        // Strictness extras
+        "noUnusedLocals": true,
+        "noUnusedParameters": true,
+        "noFallthroughCasesInSwitch": true,
+        "forceConsistentCasingInFileNames": true
+    },
+    "include": ["src"],
+    "exclude": ["node_modules", "dist"]
+}
+```
+
+**Deep dive**: Load `./references/config-strict.md` for strict mode migration, monorepo config, project references.
+
+## Common Gotchas
+
+| Gotcha | Why | Fix |
+|--------|-----|-----|
+| `any` leaks | `any` disables type checking for everything it touches | Use `unknown` + narrowing instead |
+| `as` assertions hide bugs | Assertion doesn't check at runtime | Use type guards or validation (Zod) |
+| `enum` quirks | Numeric enums are not type-safe, reverse mappings confuse | Use `as const` objects or string literal unions |
+| `object` vs `Record` vs `{}` | `{}` matches any non-null value, `object` is non-primitive | Use `Record<string, unknown>` for "any object" |
+| Array index access | `arr[999]` returns `T` not `T \| undefined` by default | Enable `noUncheckedIndexedAccess` |
+| Optional vs undefined | `{ x?: string }` allows missing key, `{ x: string \| undefined }` requires key | Be explicit about which you mean |
+| `!` non-null assertion | Silences null checks, no runtime effect | Use `?? defaultValue` or proper null check |
+| Structural typing surprise | `{ a: 1, b: 2 }` assignable to `{ a: number }` | Use branded types for nominal typing |
+
+## Branded / Nominal Types
+
+```typescript
+// Prevent accidentally mixing types that are structurally identical
+type UserId = string & { readonly __brand: "UserId" };
+type OrderId = string & { readonly __brand: "OrderId" };
+
+function createUserId(id: string): UserId { return id as UserId; }
+
+function getUser(id: UserId) { /* ... */ }
+
+const userId = createUserId("u-123");
+const orderId = "o-456" as OrderId;
+
+getUser(userId);   // OK
+getUser(orderId);  // Error: OrderId not assignable to UserId
+```
+
+## Runtime Validation (Zod)
+
+```typescript
+import { z } from "zod";
+
+// Define schema
+const UserSchema = z.object({
+    id: z.number(),
+    name: z.string().min(1),
+    email: z.string().email(),
+    role: z.enum(["admin", "user"]),
+    settings: z.object({
+        theme: z.enum(["light", "dark"]).default("light"),
+    }).optional(),
+});
+
+// Infer type from schema
+type User = z.infer<typeof UserSchema>;
+
+// Validate
+const user = UserSchema.parse(untrustedData);       // throws on invalid
+const result = UserSchema.safeParse(untrustedData);  // returns { success, data/error }
+```
+
+## Reference Files
+
+Load these for deep-dive topics. Each is self-contained.
+
+| Reference | When to Load |
+|-----------|-------------|
+| `./references/type-system.md` | Advanced types, branded types, type-level programming, satisfies operator |
+| `./references/generics-patterns.md` | Generic constraints, conditional types, mapped types, template literals, recursive types |
+| `./references/utility-types.md` | All built-in utility types with examples, custom utility types |
+| `./references/config-strict.md` | tsconfig deep dive, strict mode migration, project references, monorepo setup |
+| `./references/ecosystem.md` | Zod/Valibot, type-safe API clients, ORM types, testing with Vitest |
+
+## See Also
+
+- `testing-ops` - Cross-language testing strategies
+- `ci-cd-ops` - TypeScript CI pipelines, type checking in CI

+ 0 - 0
skills/typescript-ops/assets/.gitkeep


+ 563 - 0
skills/typescript-ops/references/config-strict.md

@@ -0,0 +1,563 @@
+# TypeScript Configuration and Strict Mode Reference
+
+## Table of Contents
+
+1. [Strict Mode Flags](#strict-mode-flags)
+2. [Migration Strategy](#migration-strategy)
+3. [Module Configuration](#module-configuration)
+4. [Path Aliases](#path-aliases)
+5. [Project References](#project-references)
+6. [Monorepo Setup](#monorepo-setup)
+7. [Declaration Files](#declaration-files)
+
+---
+
+## Strict Mode Flags
+
+### Enable the Full Strict Suite
+
+`"strict": true` is shorthand for enabling all individual strict flags at once. Always enable it.
+
+```json
+{
+  "compilerOptions": {
+    "strict": true,
+
+    // Additional strictness beyond "strict": true
+    "noUncheckedIndexedAccess": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "exactOptionalPropertyTypes": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitOverride": true
+  }
+}
+```
+
+### Understand Each Flag
+
+**strictNullChecks** - `null` and `undefined` are not assignable to other types. The most impactful flag.
+
+```typescript
+// Without strictNullChecks: null assignable to anything
+// With strictNullChecks:
+function getLength(s: string): number {
+  return s.length; // OK
+}
+getLength(null); // Error: Argument of type 'null' is not assignable to parameter of type 'string'
+
+// Forces explicit null handling:
+function getName(user: { name: string } | null): string {
+  return user?.name ?? 'Anonymous';
+}
+```
+
+**strictFunctionTypes** - Function parameters are checked contravariantly, not bivariantly.
+
+```typescript
+type Animal = { name: string };
+type Dog = Animal & { breed: string };
+
+type AnimalCallback = (a: Animal) => void;
+type DogCallback    = (d: Dog) => void;
+
+let animalCb: AnimalCallback = (a) => console.log(a.name);
+let dogCb: DogCallback = (d) => console.log(d.breed);
+
+// With strictFunctionTypes, this is an error (unsafe in callback position):
+// dogCb = animalCb; // DogCallback expects d.breed but AnimalCallback only provides a.name
+```
+
+**strictBindCallApply** - `.bind()`, `.call()`, `.apply()` are type-checked.
+
+```typescript
+function add(a: number, b: number): number { return a + b; }
+
+add.call(null, 1, 2);   // OK
+add.call(null, '1', 2); // Error: Argument of type 'string' not assignable to 'number'
+add.bind(null, 1)(2);   // OK, typed as () => number after bind
+```
+
+**strictPropertyInitialization** - Class properties must be assigned in the constructor.
+
+```typescript
+class Service {
+  name: string;       // Error: not definitely assigned
+  id: string;         // Error: not definitely assigned
+
+  // Fix options:
+  optA: string = '';                              // default value
+  optB!: string;                                  // definite assignment assertion (use sparingly)
+  optC: string | undefined;                       // allow undefined
+  constructor() { this.optA = this.optA; }       // assign in constructor
+}
+```
+
+**noImplicitAny** - Variables whose type cannot be inferred default to `any` - this flag makes that an error.
+
+```typescript
+function process(data) { // Error: 'data' implicitly has an 'any' type
+  return data.value;
+}
+
+function process(data: { value: string }): string { // OK
+  return data.value;
+}
+```
+
+**noImplicitThis** - `this` usage without explicit annotation is an error.
+
+```typescript
+function greet() {
+  return this.name; // Error: 'this' implicitly has type 'any'
+}
+
+function greet(this: { name: string }): string {
+  return this.name; // OK - this is typed
+}
+```
+
+**useUnknownInCatchVariables** (part of strict in TS 4.4+) - Catch clause variables are `unknown`, not `any`.
+
+```typescript
+try {
+  riskyOperation();
+} catch (err) {
+  // err is 'unknown' - must narrow before use
+  if (err instanceof Error) {
+    console.error(err.message); // OK
+  } else {
+    console.error(String(err)); // handle non-Error throws
+  }
+}
+```
+
+**noUncheckedIndexedAccess** - Index signatures include `undefined` in return type.
+
+```typescript
+const map: Record<string, string> = {};
+const value = map['key']; // string | undefined (not just string)
+
+// Forces null checking:
+if (value !== undefined) {
+  console.log(value.toUpperCase()); // OK
+}
+```
+
+**exactOptionalPropertyTypes** - Distinguishes between `prop?: T` (absent or T) and `prop: T | undefined`.
+
+```typescript
+interface A { name?: string; }
+
+// With exactOptionalPropertyTypes:
+const a: A = { name: undefined }; // Error: undefined is not the same as absent
+const b: A = {};                   // OK - name is absent
+const c: A = { name: 'Alice' };    // OK
+```
+
+---
+
+## Migration Strategy
+
+### Adopt Strict Mode Incrementally
+
+```json
+// Phase 1: Start here - catches the worst issues
+{
+  "compilerOptions": {
+    "noImplicitAny": true,
+    "strictNullChecks": true
+  }
+}
+
+// Phase 2: Add remaining strict flags
+{
+  "compilerOptions": {
+    "strict": true
+  }
+}
+
+// Phase 3: Tighten further
+{
+  "compilerOptions": {
+    "strict": true,
+    "noUncheckedIndexedAccess": true,
+    "exactOptionalPropertyTypes": true,
+    "noImplicitReturns": true
+  }
+}
+```
+
+### Use @ts-expect-error for Tracked Suppressions
+
+Prefer `@ts-expect-error` over `@ts-ignore`. The former causes a type error if the suppressed line no longer has an error, making it self-cleaning.
+
+```typescript
+// @ts-ignore - silently does nothing if the error is later fixed (dead suppression)
+const x: string = 42;
+
+// @ts-expect-error - causes a type error when the suppressed error is fixed
+// @ts-expect-error: temporary until API is updated
+const y: string = legacyApi.getValue();
+```
+
+### Migration Checklist
+
+```
+[ ] Enable noImplicitAny first - forces all untyped code to be explicit
+[ ] Add @ts-expect-error to suppress errors in files not yet migrated
+[ ] Enable strictNullChecks - fix null/undefined handling
+[ ] Enable strict: true - address remaining flags
+[ ] Track suppressions with: grep -r "@ts-expect-error" . --include="*.ts"
+[ ] Eliminate suppressions file by file
+[ ] Enable noUncheckedIndexedAccess as final step (highest refactor cost)
+```
+
+---
+
+## Module Configuration
+
+### Choose the Right module and moduleResolution
+
+```json
+// For Node.js with CommonJS
+{
+  "compilerOptions": {
+    "module": "CommonJS",
+    "moduleResolution": "Node"
+  }
+}
+
+// For Node.js with ESM (Node 18+) - recommended for new Node projects
+{
+  "compilerOptions": {
+    "module": "Node16",       // or "NodeNext"
+    "moduleResolution": "Node16"
+  }
+}
+
+// For bundlers (Vite, webpack, esbuild, Rollup)
+{
+  "compilerOptions": {
+    "module": "ESNext",
+    "moduleResolution": "Bundler"
+  }
+}
+
+// For browser projects with no bundler (rare)
+{
+  "compilerOptions": {
+    "module": "ESNext",
+    "moduleResolution": "Classic"  // avoid - use Bundler or Node16
+  }
+}
+```
+
+### Understand ESM vs CJS Interop Issues
+
+With `Node16`/`NodeNext`, you must use explicit `.js` extensions in relative imports (even for `.ts` files).
+
+```typescript
+// tsconfig.json: "module": "Node16"
+
+// WRONG - no extension
+import { helper } from './helper';
+
+// CORRECT - use .js extension (TypeScript resolves it to .ts)
+import { helper } from './helper.js';
+```
+
+Set `"type": "module"` in `package.json` to use ESM, or use `.mts`/`.cts` file extensions to override per-file.
+
+```json
+// package.json
+{
+  "type": "module"
+}
+```
+
+---
+
+## Path Aliases
+
+### Configure Paths in tsconfig.json
+
+```json
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*":         ["./src/*"],
+      "@components/*": ["./src/components/*"],
+      "@utils/*":    ["./src/utils/*"],
+      "@types/*":    ["./src/types/*"]
+    }
+  }
+}
+```
+
+### Use Paths with Vite
+
+```typescript
+// vite.config.ts
+import { defineConfig } from 'vite';
+import { resolve } from 'path';
+
+export default defineConfig({
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, './src'),
+      '@components': resolve(__dirname, './src/components'),
+      '@utils': resolve(__dirname, './src/utils'),
+    },
+  },
+});
+```
+
+### Use Paths with Node (tsx / tsconfig-paths)
+
+```bash
+# Option 1: tsx (recommended for scripts/CLIs)
+npx tsx --tsconfig tsconfig.json src/index.ts
+
+# Option 2: tsconfig-paths with ts-node
+npx ts-node -r tsconfig-paths/register src/index.ts
+
+# Option 3: tsconfig-paths at runtime (after compilation)
+node -r tsconfig-paths/register dist/index.js
+```
+
+```typescript
+// tsconfig-paths at runtime setup
+// bootstrap.js
+const { register } = require('tsconfig-paths');
+const tsConfig = require('./tsconfig.json');
+register({
+  baseUrl: tsConfig.compilerOptions.baseUrl,
+  paths: tsConfig.compilerOptions.paths,
+});
+require('./dist/index.js');
+```
+
+---
+
+## Project References
+
+### Set Up Composite Projects
+
+Project references allow incremental builds and better IDE performance in large repos.
+
+```json
+// packages/shared/tsconfig.json
+{
+  "compilerOptions": {
+    "composite": true,    // required for project references
+    "declaration": true,  // required for project references
+    "declarationMap": true,
+    "outDir": "./dist",
+    "rootDir": "./src"
+  }
+}
+
+// packages/app/tsconfig.json
+{
+  "compilerOptions": {
+    "outDir": "./dist",
+    "rootDir": "./src"
+  },
+  "references": [
+    { "path": "../shared" }
+  ]
+}
+```
+
+### Build with --build Mode
+
+```bash
+# Build all referenced projects in dependency order
+tsc --build
+
+# Build and watch
+tsc --build --watch
+
+# Clean built outputs
+tsc --build --clean
+
+# Force rebuild
+tsc --build --force
+```
+
+---
+
+## Monorepo Setup
+
+### Define a Root tsconfig for Shared Settings
+
+```json
+// tsconfig.base.json (root)
+{
+  "compilerOptions": {
+    "strict": true,
+    "noUncheckedIndexedAccess": true,
+    "noImplicitReturns": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true
+  }
+}
+
+// packages/server/tsconfig.json
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "module": "CommonJS",
+    "moduleResolution": "Node",
+    "target": "ES2022",
+    "outDir": "./dist",
+    "rootDir": "./src"
+  },
+  "include": ["src/**/*"],
+  "references": [{ "path": "../shared" }]
+}
+
+// packages/web/tsconfig.json
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "target": "ES2022",
+    "jsx": "react-jsx",
+    "outDir": "./dist",
+    "rootDir": "./src"
+  },
+  "include": ["src/**/*"],
+  "references": [{ "path": "../shared" }]
+}
+```
+
+### Use a Root tsconfig for IDE Support
+
+```json
+// tsconfig.json (root - IDE only, not for building)
+{
+  "files": [],
+  "references": [
+    { "path": "./packages/shared" },
+    { "path": "./packages/server" },
+    { "path": "./packages/web" }
+  ]
+}
+```
+
+---
+
+## Declaration Files
+
+### Write Ambient Declarations for Untyped Modules
+
+```typescript
+// types/untyped-module.d.ts
+declare module 'some-legacy-package' {
+  export interface Options {
+    timeout?: number;
+    retries?: number;
+  }
+
+  export function connect(url: string, options?: Options): Promise<void>;
+  export function disconnect(): void;
+
+  export default {
+    connect,
+    disconnect,
+  };
+}
+
+// Wildcard module for assets (e.g., CSS, SVG)
+declare module '*.svg' {
+  const content: string;
+  export default content;
+}
+
+declare module '*.png' {
+  const content: string;
+  export default content;
+}
+
+declare module '*.css' {
+  const styles: Record<string, string>;
+  export default styles;
+}
+```
+
+### Augment Global Scope
+
+```typescript
+// global.d.ts
+declare global {
+  // Extend the Window interface
+  interface Window {
+    __APP_VERSION__: string;
+    analytics: {
+      track(event: string, props?: Record<string, unknown>): void;
+    };
+  }
+
+  // Extend ProcessEnv for typed environment variables
+  namespace NodeJS {
+    interface ProcessEnv {
+      NODE_ENV: 'development' | 'production' | 'test';
+      DATABASE_URL: string;
+      API_KEY: string;
+      PORT?: string;
+    }
+  }
+}
+
+export {}; // This export makes the file a module, enabling declare global
+```
+
+### Use Triple-Slash Directives
+
+```typescript
+// Reference a type definition file
+/// <reference types="node" />
+/// <reference types="jest" />
+
+// Reference a specific .d.ts file
+/// <reference path="../types/custom.d.ts" />
+
+// Reference a lib
+/// <reference lib="dom" />
+/// <reference lib="es2022" />
+```
+
+### Write a .d.ts for a Hand-Authored JavaScript Library
+
+```typescript
+// src/math-helpers.js (source)
+function add(a, b) { return a + b; }
+function multiply(a, b) { return a * b; }
+module.exports = { add, multiply };
+
+// src/math-helpers.d.ts (declaration)
+export declare function add(a: number, b: number): number;
+export declare function multiply(a: number, b: number): number;
+```
+
+### Configure Declaration Output
+
+```json
+{
+  "compilerOptions": {
+    "declaration": true,         // emit .d.ts files
+    "declarationDir": "./types", // output directory for .d.ts (optional)
+    "declarationMap": true,      // emit .d.ts.map for source navigation
+    "emitDeclarationOnly": true  // only emit .d.ts, no JS (when bundler handles JS)
+  }
+}
+```

+ 621 - 0
skills/typescript-ops/references/ecosystem.md

@@ -0,0 +1,621 @@
+# TypeScript Ecosystem Reference
+
+## Table of Contents
+
+1. [Runtime Validation](#runtime-validation)
+2. [Type-Safe API Clients](#type-safe-api-clients)
+3. [ORM Types](#orm-types)
+4. [Testing with Types](#testing-with-types)
+5. [Type-Safe Routing](#type-safe-routing)
+6. [Effect](#effect)
+7. [ts-pattern](#ts-pattern)
+8. [Type Challenges](#type-challenges)
+
+---
+
+## Runtime Validation
+
+### Use Zod for Schema Validation with Type Inference
+
+Zod is the most widely adopted runtime validation library. Define a schema once; infer the TypeScript type from it.
+
+```typescript
+import { z } from 'zod';
+
+// Define schema
+const UserSchema = z.object({
+  id: z.string().uuid(),
+  name: z.string().min(1).max(100),
+  email: z.string().email(),
+  age: z.number().int().min(0).max(150).optional(),
+  role: z.enum(['admin', 'user', 'moderator']),
+  createdAt: z.coerce.date(),
+});
+
+// Infer TypeScript type from schema - single source of truth
+type User = z.infer<typeof UserSchema>;
+// { id: string; name: string; email: string; age?: number; role: 'admin' | 'user' | 'moderator'; createdAt: Date }
+
+// Parse and validate (throws ZodError on failure)
+const user = UserSchema.parse(rawData);
+
+// Safe parse (returns success/failure object, never throws)
+const result = UserSchema.safeParse(rawData);
+if (result.success) {
+  console.log(result.data); // typed as User
+} else {
+  console.error(result.error.flatten()); // ZodError with friendly message structure
+}
+```
+
+### Apply Zod Transforms and Refinements
+
+```typescript
+const PasswordSchema = z
+  .string()
+  .min(8, 'Password must be at least 8 characters')
+  .regex(/[A-Z]/, 'Password must contain an uppercase letter')
+  .regex(/[0-9]/, 'Password must contain a number');
+
+// Transform: parse then convert
+const DateStringSchema = z.string().transform((s) => new Date(s));
+type DateValue = z.infer<typeof DateStringSchema>; // Date (output type after transform)
+// Input type is string; output type is Date
+
+// Refine: validate with custom logic
+const EvenNumberSchema = z.number().refine(
+  (n) => n % 2 === 0,
+  { message: 'Number must be even' }
+);
+
+// Discriminated union (Zod version)
+const ApiResponseSchema = z.discriminatedUnion('status', [
+  z.object({ status: z.literal('success'), data: z.unknown() }),
+  z.object({ status: z.literal('error'), message: z.string() }),
+]);
+type ApiResponse = z.infer<typeof ApiResponseSchema>;
+```
+
+### Use Valibot as a Tree-Shakeable Alternative
+
+Valibot has an almost identical API to Zod but is tree-shakeable by design, resulting in much smaller bundles for edge/browser deployments.
+
+```typescript
+import * as v from 'valibot';
+
+const UserSchema = v.object({
+  id: v.pipe(v.string(), v.uuid()),
+  name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
+  email: v.pipe(v.string(), v.email()),
+  role: v.picklist(['admin', 'user', 'moderator']),
+});
+
+type User = v.InferOutput<typeof UserSchema>;
+
+const result = v.safeParse(UserSchema, rawData);
+if (result.success) {
+  console.log(result.output); // typed as User
+}
+```
+
+### Compare Zod vs Valibot
+
+| Concern | Zod | Valibot |
+|---------|-----|---------|
+| Bundle size | ~13 kB min+gz | ~0.5-2 kB (tree-shaken) |
+| API style | Method chaining | Pipe/function composition |
+| Ecosystem | Larger (more integrations) | Smaller but growing |
+| Best for | Node.js / full-stack | Edge / browser |
+| Async validation | `z.refine(async ...)` | `v.pipeAsync(...)` |
+
+---
+
+## Type-Safe API Clients
+
+### Build a Type-Safe Fetch Wrapper
+
+```typescript
+type ApiResponse<T> =
+  | { ok: true; data: T; status: number }
+  | { ok: false; error: string; status: number };
+
+async function apiFetch<T>(
+  url: string,
+  schema: { parse: (data: unknown) => T },
+  init?: RequestInit
+): Promise<ApiResponse<T>> {
+  try {
+    const response = await fetch(url, init);
+    const json: unknown = await response.json();
+
+    if (!response.ok) {
+      return { ok: false, error: String(json), status: response.status };
+    }
+
+    const data = schema.parse(json);
+    return { ok: true, data, status: response.status };
+  } catch (err) {
+    return { ok: false, error: err instanceof Error ? err.message : 'Unknown error', status: 0 };
+  }
+}
+
+// Usage with Zod schema
+const UsersSchema = z.array(UserSchema);
+const result = await apiFetch('/api/users', UsersSchema);
+if (result.ok) {
+  result.data; // User[]
+}
+```
+
+### Use openapi-typescript for Contract-First APIs
+
+```bash
+# Generate TypeScript types from an OpenAPI spec
+npx openapi-typescript ./openapi.yaml -o ./src/types/api.d.ts
+# or from a URL
+npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.d.ts
+```
+
+```typescript
+import type { paths, components } from './types/api.d.ts';
+
+// Use generated types in a typed client
+type GetUserParams = paths['/users/{id}']['get']['parameters'];
+type GetUserResponse = paths['/users/{id}']['get']['responses']['200']['content']['application/json'];
+type User = components['schemas']['User'];
+```
+
+### Add tRPC for End-to-End Type Safety
+
+```typescript
+// server/router.ts
+import { initTRPC } from '@trpc/server';
+import { z } from 'zod';
+
+const t = initTRPC.create();
+
+export const appRouter = t.router({
+  user: t.router({
+    getById: t.procedure
+      .input(z.object({ id: z.string() }))
+      .query(async ({ input }) => {
+        return await db.user.findUnique({ where: { id: input.id } });
+      }),
+
+    create: t.procedure
+      .input(z.object({ name: z.string(), email: z.string().email() }))
+      .mutation(async ({ input }) => {
+        return await db.user.create({ data: input });
+      }),
+  }),
+});
+
+export type AppRouter = typeof appRouter;
+
+// client/trpc.ts
+import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
+import type { AppRouter } from '../server/router';
+
+const trpc = createTRPCProxyClient<AppRouter>({
+  links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
+});
+
+// Fully type-safe - input and output types inferred from router
+const user = await trpc.user.getById.query({ id: '123' });
+// user is typed as the return type of the resolver - no manual typing needed
+```
+
+---
+
+## ORM Types
+
+### Generate Types with Prisma
+
+Prisma generates complete TypeScript types from the schema file.
+
+```prisma
+// prisma/schema.prisma
+model User {
+  id        String   @id @default(cuid())
+  email     String   @unique
+  name      String?
+  posts     Post[]
+  createdAt DateTime @default(now())
+}
+```
+
+```typescript
+import { PrismaClient } from '@prisma/client';
+// Prisma generates:
+// - PrismaClient with typed query methods
+// - User, Post, etc. model types
+// - UserCreateInput, UserUpdateInput, UserWhereInput, etc.
+
+const db = new PrismaClient();
+
+// Fully typed queries
+const user = await db.user.findUniqueOrThrow({
+  where: { email: 'alice@example.com' },
+  include: { posts: true },
+});
+// user: User & { posts: Post[] }
+
+// Use generated input types
+import type { Prisma } from '@prisma/client';
+
+async function createUser(data: Prisma.UserCreateInput) {
+  return db.user.create({ data });
+}
+
+// Select subsets for performance
+type UserPreview = Prisma.UserGetPayload<{
+  select: { id: true; name: true; email: true };
+}>;
+```
+
+### Write Type-Safe Queries with Drizzle ORM
+
+Drizzle is a SQL-first ORM where types flow from the schema definition.
+
+```typescript
+import { pgTable, text, integer, timestamp } from 'drizzle-orm/pg-core';
+import { drizzle } from 'drizzle-orm/node-postgres';
+import { eq } from 'drizzle-orm';
+
+const users = pgTable('users', {
+  id: text('id').primaryKey(),
+  name: text('name').notNull(),
+  email: text('email').notNull().unique(),
+  age: integer('age'),
+  createdAt: timestamp('created_at').defaultNow(),
+});
+
+// Infer types directly from table definition
+type User = typeof users.$inferSelect;    // for SELECT results
+type NewUser = typeof users.$inferInsert; // for INSERT data
+
+const db = drizzle(pool);
+
+// Type-safe queries - IDE autocomplete on column names
+const allUsers = await db.select().from(users);
+// allUsers: User[]
+
+const alice = await db.select().from(users).where(eq(users.email, 'alice@example.com'));
+// alice: User[]
+
+await db.insert(users).values({ id: '1', name: 'Alice', email: 'alice@example.com' });
+// Type error if required fields are missing
+```
+
+### Query with Kysely for SQL-First Type Safety
+
+Kysely provides type-safe query building without code generation.
+
+```typescript
+import { Kysely, PostgresDialect } from 'kysely';
+
+interface Database {
+  users: { id: string; name: string; email: string; age: number | null };
+  posts: { id: string; userId: string; title: string; content: string };
+}
+
+const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });
+
+const users = await db
+  .selectFrom('users')
+  .select(['id', 'name', 'email'])
+  .where('age', '>', 18)
+  .execute();
+// users: Array<{ id: string; name: string; email: string }>
+```
+
+---
+
+## Testing with Types
+
+### Use expectTypeOf in Vitest
+
+```typescript
+import { expectTypeOf, test } from 'vitest';
+
+test('identity function preserves type', () => {
+  function identity<T>(value: T): T { return value; }
+
+  expectTypeOf(identity('hello')).toEqualTypeOf<string>();
+  expectTypeOf(identity(42)).toEqualTypeOf<number>();
+  expectTypeOf(identity).toBeFunction();
+  expectTypeOf(identity).parameter(0).toBeString();
+});
+
+test('Result type narrows correctly', () => {
+  type Result<T> = { ok: true; value: T } | { ok: false; error: string };
+
+  function ok<T>(value: T): Result<T> { return { ok: true, value }; }
+  function err<T>(error: string): Result<T> { return { ok: false, error }; }
+
+  expectTypeOf(ok('data')).toEqualTypeOf<Result<string>>();
+  expectTypeOf(err<number>('oops')).toEqualTypeOf<Result<number>>();
+});
+```
+
+### Use assertType for Compile-Time Checks
+
+```typescript
+import { assertType, test } from 'vitest';
+
+test('types are correct', () => {
+  // assertType<T>(value) asserts value matches type T at compile time
+  // (no runtime effect - type-only check)
+  assertType<string>('hello');
+  assertType<number>(42);
+
+  // @ts-expect-error assertions that should fail
+  // @ts-expect-error
+  assertType<string>(42); // fails: 42 is not string
+});
+```
+
+### Use tsd for Testing Declaration Files
+
+`tsd` is dedicated to testing `.d.ts` files. It checks that type definitions behave correctly.
+
+```typescript
+// index.test-d.ts
+import { expectType, expectError, expectAssignable } from 'tsd';
+import { getUser, createUser } from './index.js';
+
+// Check return types
+expectType<Promise<User>>(getUser('123'));
+
+// Check that invalid calls produce errors
+expectError(getUser(123)); // Error: number not assignable to string
+
+// Check assignability (less strict than equality)
+expectAssignable<{ id: string }>(await getUser('1'));
+```
+
+```json
+// package.json
+{
+  "scripts": {
+    "test:types": "tsd"
+  },
+  "tsd": {
+    "directory": "test"
+  }
+}
+```
+
+---
+
+## Type-Safe Routing
+
+### Use Next.js Typed Routes
+
+Next.js 13+ supports experimental typed routes that validate `href` values.
+
+```json
+// next.config.js
+{
+  "experimental": {
+    "typedRoutes": true
+  }
+}
+```
+
+```typescript
+import Link from 'next/link';
+
+// TypeScript validates the href against your actual routes
+<Link href="/about">About</Link>            // OK if /about exists
+<Link href="/users/[id]">User</Link>        // Error: must pass actual id
+<Link href={{ pathname: '/users/[id]', params: { id: '1' } }}>User</Link> // OK
+```
+
+### Build Type-Safe Path Parameters
+
+```typescript
+// Generic route parameter extractor
+type ExtractParams<T extends string> =
+  T extends `${string}:${infer Param}/${infer Rest}`
+    ? { [K in Param]: string } & ExtractParams<Rest>
+    : T extends `${string}:${infer Param}`
+    ? { [K in Param]: string }
+    : Record<string, never>;
+
+type Prettify<T> = { [K in keyof T]: T[K] } & {};
+
+function createRoute<T extends string>(
+  template: T
+): { path: T; build(params: Prettify<ExtractParams<T>>): string } {
+  return {
+    path: template,
+    build(params) {
+      return Object.entries(params).reduce(
+        (path, [key, value]) => path.replace(`:${key}`, value as string),
+        template
+      );
+    },
+  };
+}
+
+const userRoute = createRoute('/users/:userId/posts/:postId');
+const url = userRoute.build({ userId: '1', postId: '42' }); // '/users/1/posts/42'
+// TypeScript error if userId or postId is missing
+```
+
+---
+
+## Effect
+
+### Use Effect-TS for Typed Functional Error Handling
+
+Effect models computations as `Effect<Value, Error, Requirements>`. Errors are part of the type, not thrown.
+
+```typescript
+import { Effect, pipe } from 'effect';
+
+// Define typed errors
+class UserNotFoundError {
+  readonly _tag = 'UserNotFoundError';
+  constructor(readonly id: string) {}
+}
+
+class DatabaseError {
+  readonly _tag = 'DatabaseError';
+  constructor(readonly message: string) {}
+}
+
+// Effect<User, UserNotFoundError | DatabaseError, never>
+// Value: User, Error: UserNotFoundError | DatabaseError, Requirements: none
+const getUser = (id: string): Effect.Effect<User, UserNotFoundError | DatabaseError> =>
+  Effect.tryPromise({
+    try: () => db.user.findUniqueOrThrow({ where: { id } }),
+    catch: (e) =>
+      e instanceof Error && e.message.includes('No User found')
+        ? new UserNotFoundError(id)
+        : new DatabaseError(String(e)),
+  });
+
+// Compose effects with pipe
+const program = pipe(
+  getUser('123'),
+  Effect.map((user) => user.name),
+  Effect.catchTag('UserNotFoundError', (e) =>
+    Effect.succeed(`User ${e.id} not found`)
+  ),
+  // DatabaseError is still in the error channel - must be handled or propagated
+);
+
+// Run the effect
+const result = await Effect.runPromise(program);
+```
+
+---
+
+## ts-pattern
+
+### Match Exhaustively with ts-pattern
+
+`ts-pattern` provides pattern matching with full TypeScript type narrowing.
+
+```typescript
+import { match, P } from 'ts-pattern';
+
+type ApiState =
+  | { status: 'idle' }
+  | { status: 'loading' }
+  | { status: 'success'; data: User[] }
+  | { status: 'error'; error: Error };
+
+function render(state: ApiState): string {
+  return match(state)
+    .with({ status: 'idle' },    () => 'Ready')
+    .with({ status: 'loading' }, () => 'Loading...')
+    .with({ status: 'success', data: P.select() }, (data) =>
+      `Loaded ${data.length} users`
+    )
+    .with({ status: 'error', error: P.select() }, (error) =>
+      `Error: ${error.message}`
+    )
+    .exhaustive(); // Compile error if a variant is unhandled
+}
+
+// Pattern guards
+const result = match(value)
+  .with(P.number.gt(100), (n) => `Big: ${n}`)
+  .with(P.number.lt(0),   (n) => `Negative: ${n}`)
+  .with(P.number,          (n) => `Normal: ${n}`)
+  .with(P.string,          (s) => `String: ${s}`)
+  .otherwise(() => 'Unknown');
+
+// Nested matching
+const message = match(response)
+  .with({ type: 'error', code: P.union(401, 403) }, () => 'Unauthorized')
+  .with({ type: 'error', code: 404 },               () => 'Not Found')
+  .with({ type: 'error' },                           () => 'Server Error')
+  .with({ type: 'success' },                         () => 'OK')
+  .exhaustive();
+```
+
+---
+
+## Type Challenges
+
+### Practice Advanced Types Effectively
+
+The `type-challenges` repository (github.com/type-challenges/type-challenges) provides 200+ graded exercises.
+
+```typescript
+// Example: Implement Readonly<T> from scratch
+type MyReadonly<T> = {
+  readonly [K in keyof T]: T[K];
+};
+
+// Example: Implement Pick<T, K>
+type MyPick<T, K extends keyof T> = {
+  [P in K]: T[P];
+};
+
+// Example: Implement Exclude<T, U>
+type MyExclude<T, U> = T extends U ? never : T;
+
+// Example: Implement ReturnType<T>
+type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
+
+// Example: Deep Readonly
+type DeepReadonly<T> = keyof T extends never
+  ? T
+  : { readonly [K in keyof T]: DeepReadonly<T[K]> };
+```
+
+### Use the TypeScript Playground
+
+The TypeScript Playground (typescriptlang.org/play) supports:
+- Sharing type puzzles via URL
+- Viewing emitted JavaScript
+- Checking against multiple TS versions
+- Running code in browser
+
+### Recommended Learning Resources
+
+| Resource | Focus |
+|----------|-------|
+| `type-challenges` on GitHub | Exercises from easy to extreme |
+| Matt Pocock's Total TypeScript | Tutorials and workshops |
+| TypeScript Deep Dive (basarat) | Comprehensive free book |
+| Official TS Handbook | Language reference |
+| tsdocs.dev | Browse type definitions for any npm package |
+| typescript-eslint.io | Type-aware lint rules |
+
+### Set Up a Type Testing Playground Locally
+
+```bash
+mkdir ts-playground && cd ts-playground
+npm init -y
+npm install -D typescript tsx @types/node
+
+cat > tsconfig.json << 'EOF'
+{
+  "compilerOptions": {
+    "strict": true,
+    "noUncheckedIndexedAccess": true,
+    "exactOptionalPropertyTypes": true,
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "Bundler"
+  }
+}
+EOF
+
+# Write type experiments
+cat > playground.ts << 'EOF'
+type Test = /* your type here */;
+type Expect<T extends true> = T;
+type Equal<A, B> = A extends B ? B extends A ? true : false : false;
+
+type Case1 = Expect<Equal<Test, ExpectedType>>;
+EOF
+
+npx tsx playground.ts
+```

+ 581 - 0
skills/typescript-ops/references/generics-patterns.md

@@ -0,0 +1,581 @@
+# TypeScript Generics Patterns Reference
+
+## Table of Contents
+
+1. [Generic Functions](#generic-functions)
+2. [Generic Classes](#generic-classes)
+3. [Generic Interfaces](#generic-interfaces)
+4. [Conditional Types](#conditional-types)
+5. [Mapped Types](#mapped-types)
+6. [Template Literal Types in Generics](#template-literal-types-in-generics)
+7. [Variadic Tuple Types](#variadic-tuple-types)
+8. [Higher-Kinded Types](#higher-kinded-types)
+9. [Builder Pattern](#builder-pattern)
+10. [Common Generic Patterns](#common-generic-patterns)
+
+---
+
+## Generic Functions
+
+### Infer Type Parameters From Arguments
+
+TypeScript infers type parameters from call-site arguments. Prefer inference over explicit type args.
+
+```typescript
+// Inferred: T = string from the argument
+function identity<T>(value: T): T {
+  return value;
+}
+const s = identity('hello'); // T inferred as string
+
+// Constraint: T must have a length property
+function longest<T extends { length: number }>(a: T, b: T): T {
+  return a.length >= b.length ? a : b;
+}
+longest('alice', 'bob');         // OK - strings have length
+longest([1, 2, 3], [1, 2]);     // OK - arrays have length
+longest({ length: 5 }, { length: 3 }); // OK
+
+// Multiple type parameters with relationship constraint
+function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
+  return obj[key];
+}
+
+const user = { id: 1, name: 'Alice', active: true };
+getProperty(user, 'name');   // string
+getProperty(user, 'active'); // boolean
+getProperty(user, 'foo');    // Error: 'foo' is not a key of typeof user
+```
+
+### Use Default Type Parameters
+
+```typescript
+// Default type parameter when not specified
+function createArray<T = string>(length: number, fill: T): T[] {
+  return Array(length).fill(fill);
+}
+
+const strings = createArray(3, 'x');   // string[] - T inferred
+const numbers = createArray(3, 0);     // number[] - T inferred
+const explicit = createArray<boolean>(3, true); // boolean[]
+
+// Useful in generic components/hooks
+interface PaginatedResponse<T = unknown> {
+  data: T[];
+  total: number;
+  page: number;
+}
+```
+
+---
+
+## Generic Classes
+
+### Parameterize Class Behavior
+
+```typescript
+class Stack<T> {
+  private items: T[] = [];
+
+  push(item: T): void {
+    this.items.push(item);
+  }
+
+  pop(): T | undefined {
+    return this.items.pop();
+  }
+
+  peek(): T | undefined {
+    return this.items[this.items.length - 1];
+  }
+
+  get size(): number {
+    return this.items.length;
+  }
+}
+
+const numStack = new Stack<number>();
+numStack.push(1);
+numStack.push(2);
+const top = numStack.pop(); // number | undefined
+```
+
+### Recognize the Static Members Limitation
+
+Static members cannot reference a class's type parameters. The type parameter belongs to an instance.
+
+```typescript
+class Container<T> {
+  value: T;  // OK - instance member
+
+  constructor(value: T) {
+    this.value = value;
+  }
+
+  // static defaultValue: T; // Error: static members can't reference type parameters
+
+  // Workaround: use a separate factory type or factory method
+  static create<U>(value: U): Container<U> {
+    return new Container(value);
+  }
+}
+```
+
+---
+
+## Generic Interfaces
+
+### Implement the Repository Pattern
+
+```typescript
+interface Repository<T, ID = string> {
+  findById(id: ID): Promise<T | null>;
+  findAll(filter?: Partial<T>): Promise<T[]>;
+  save(entity: T): Promise<T>;
+  delete(id: ID): Promise<void>;
+}
+
+interface User {
+  id: string;
+  name: string;
+  email: string;
+}
+
+class UserRepository implements Repository<User, string> {
+  async findById(id: string): Promise<User | null> { /* ... */ return null; }
+  async findAll(filter?: Partial<User>): Promise<User[]> { /* ... */ return []; }
+  async save(entity: User): Promise<User> { /* ... */ return entity; }
+  async delete(id: string): Promise<void> { /* ... */ }
+}
+```
+
+### Implement the Factory Pattern
+
+```typescript
+interface Factory<T, TArgs extends unknown[] = []> {
+  create(...args: TArgs): T;
+}
+
+class ConnectionFactory implements Factory<Connection, [string, number]> {
+  create(host: string, port: number): Connection {
+    return new Connection(host, port);
+  }
+}
+```
+
+---
+
+## Conditional Types
+
+### Distribute Over Union Types
+
+Conditional types distribute over naked type parameters in unions.
+
+```typescript
+// Distributes: IsString<string | number> = IsString<string> | IsString<number>
+type IsString<T> = T extends string ? true : false;
+type Test = IsString<string | number>; // true | false = boolean
+
+// To prevent distribution, wrap in a tuple
+type IsStringExact<T> = [T] extends [string] ? true : false;
+type Test2 = IsStringExact<string | number>; // false
+```
+
+### Use infer to Extract Types
+
+```typescript
+// Extract the element type from an array
+type UnpackArray<T> = T extends (infer U)[] ? U : T;
+type Item = UnpackArray<string[]>; // string
+type Same = UnpackArray<number>;   // number
+
+// Extract return type (equivalent to built-in ReturnType)
+type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
+
+// Extract the resolved value of a Promise
+type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
+type Value = Awaited<Promise<Promise<string>>>; // string
+
+// Extract first parameter type
+type FirstParam<T> = T extends (first: infer F, ...rest: unknown[]) => unknown ? F : never;
+type F = FirstParam<(a: string, b: number) => void>; // string
+
+// Extract constructor instance type
+type InstanceOf<T> = T extends new (...args: unknown[]) => infer I ? I : never;
+```
+
+### Nest Conditional Types for Complex Logic
+
+```typescript
+type TypeName<T> =
+  T extends string  ? 'string'  :
+  T extends number  ? 'number'  :
+  T extends boolean ? 'boolean' :
+  T extends null    ? 'null'    :
+  T extends undefined ? 'undefined' :
+  T extends Function ? 'function' :
+  'object';
+
+type A = TypeName<string>;   // 'string'
+type B = TypeName<() => void>; // 'function'
+type C = TypeName<{ a: 1 }>;  // 'object'
+
+// Filter a union: keep only string keys from a type
+type StringKeys<T> = {
+  [K in keyof T]: T[K] extends string ? K : never;
+}[keyof T];
+
+interface Mixed { id: string; count: number; name: string; active: boolean; }
+type OnlyStringFields = StringKeys<Mixed>; // 'id' | 'name'
+```
+
+---
+
+## Mapped Types
+
+### Remap Keys with the as Clause
+
+```typescript
+// Prefix all keys
+type Prefixed<T, P extends string> = {
+  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
+};
+
+interface User { id: string; name: string; }
+type PrefixedUser = Prefixed<User, 'user'>; // { userId: string; userName: string }
+
+// Filter keys by value type
+type PickByValue<T, V> = {
+  [K in keyof T as T[K] extends V ? K : never]: T[K];
+};
+
+interface Config { debug: boolean; port: number; host: string; verbose: boolean; }
+type BooleanConfig = PickByValue<Config, boolean>; // { debug: boolean; verbose: boolean }
+```
+
+### Apply Modifiers with + and -
+
+```typescript
+// Add readonly and optional
+type Immutable<T> = {
+  +readonly [K in keyof T]+?: T[K];
+};
+
+// Remove readonly and optional (make mutable and required)
+type Mutable<T> = {
+  -readonly [K in keyof T]-?: T[K];
+};
+
+interface Optional {
+  readonly id?: string;
+  readonly name?: string;
+}
+
+type Concrete = Mutable<Optional>; // { id: string; name: string }
+```
+
+### Combine Mapped and Conditional Types
+
+```typescript
+// Make only specific keys optional
+type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
+
+interface Post {
+  id: string;
+  title: string;
+  content: string;
+  publishedAt: Date;
+}
+
+type DraftPost = MakeOptional<Post, 'id' | 'publishedAt'>;
+// { title: string; content: string; id?: string; publishedAt?: Date }
+```
+
+---
+
+## Template Literal Types in Generics
+
+### Build Type-Safe Event Emitters
+
+```typescript
+type EventMap = {
+  userCreated: { userId: string };
+  orderPlaced: { orderId: string; total: number };
+  sessionExpired: { sessionId: string };
+};
+
+type EventListener<TMap, TEvent extends keyof TMap> =
+  (event: TMap[TEvent]) => void;
+
+type Emitter<TMap> = {
+  on<TEvent extends keyof TMap>(
+    event: TEvent,
+    listener: EventListener<TMap, TEvent>
+  ): void;
+  emit<TEvent extends keyof TMap>(event: TEvent, data: TMap[TEvent]): void;
+};
+
+declare const emitter: Emitter<EventMap>;
+
+emitter.on('userCreated', (e) => console.log(e.userId));   // OK
+emitter.on('orderPlaced', (e) => console.log(e.total));    // OK
+emitter.emit('userCreated', { userId: '123' });             // OK
+emitter.emit('userCreated', { orderId: '123' });            // Error: wrong shape
+```
+
+### Extract Route Parameters
+
+```typescript
+type RouteParams<T extends string> =
+  T extends `${string}:${infer Param}/${infer Rest}`
+    ? { [K in Param | keyof RouteParams<Rest>]: string }
+    : T extends `${string}:${infer Param}`
+    ? { [K in Param]: string }
+    : Record<string, never>;
+
+function buildRoute<T extends string>(
+  template: T,
+  params: RouteParams<T>
+): string {
+  return Object.entries(params).reduce(
+    (path, [key, value]) => path.replace(`:${key}`, value as string),
+    template
+  );
+}
+
+const url = buildRoute('/users/:userId/posts/:postId', {
+  userId: '1',
+  postId: '42',
+}); // '/users/1/posts/42'
+```
+
+---
+
+## Variadic Tuple Types
+
+### Spread Tuples for Function Composition
+
+```typescript
+// Concatenate two tuple types
+type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
+type T1 = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
+
+// Strongly typed pipe/compose
+type Pipe<T extends ((...args: unknown[]) => unknown)[]> =
+  T extends [infer First, ...infer Rest]
+    ? First extends (...args: infer A) => infer R
+      ? Rest extends []
+        ? (...args: A) => R
+        : Pipe<[(...args: A) => R, ...Extract<Rest, ((...args: unknown[]) => unknown)[]>]>
+      : never
+    : never;
+
+// Prepend and append to tuples
+type Prepend<T, Tuple extends unknown[]> = [T, ...Tuple];
+type Append<Tuple extends unknown[], T>  = [...Tuple, T];
+
+type WithFirst = Prepend<string, [number, boolean]>; // [string, number, boolean]
+type WithLast  = Append<[string, number], boolean>;   // [string, number, boolean]
+```
+
+### Build Type-Safe curry
+
+```typescript
+type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
+type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never;
+
+type Curry<TArgs extends unknown[], TReturn> =
+  TArgs extends []
+    ? TReturn
+    : (arg: Head<TArgs>) => Curry<Tail<TArgs>, TReturn>;
+
+declare function curry<TArgs extends unknown[], TReturn>(
+  fn: (...args: TArgs) => TReturn
+): Curry<TArgs, TReturn>;
+
+const add = curry((a: number, b: number, c: number) => a + b + c);
+const add5 = add(5);          // Curry<[number, number], number>
+const add5and3 = add5(3);     // Curry<[number], number>
+const result = add5and3(2);   // number = 10
+```
+
+---
+
+## Higher-Kinded Types
+
+### Emulate HKT with Interface Lookup
+
+TypeScript doesn't natively support higher-kinded types, but they can be emulated with a registry pattern.
+
+```typescript
+// Define a type-level registry for type constructors
+interface HKTRegistry {
+  // Registered types go here via module augmentation
+}
+
+type HKT = keyof HKTRegistry;
+type Apply<F extends HKT, A> = HKTRegistry[F] extends { type: unknown }
+  ? (HKTRegistry[F] & { arg: A })['type']
+  : never;
+
+// Register Array as a type constructor
+declare module './hkt' {
+  interface HKTRegistry {
+    Array: { type: Array<this['arg']> };
+  }
+}
+
+// Functor interface using HKT
+interface Functor<F extends HKT> {
+  map<A, B>(fa: Apply<F, A>, f: (a: A) => B): Apply<F, B>;
+}
+```
+
+---
+
+## Builder Pattern
+
+### Track Builder State in the Type System
+
+```typescript
+type BuilderState = {
+  hasName: boolean;
+  hasAge: boolean;
+};
+
+type Builder<State extends BuilderState, T = {}> = {
+  setName(name: string): Builder<State & { hasName: true }, T & { name: string }>;
+  setAge(age: number): Builder<State & { hasAge: true }, T & { age: number }>;
+} & (State['hasName'] extends true
+  ? State['hasAge'] extends true
+    ? { build(): T }
+    : {}
+  : {});
+
+declare function createBuilder(): Builder<{ hasName: false; hasAge: false }>;
+
+const builder = createBuilder();
+const user = builder.setName('Alice').setAge(30).build();
+// user: { name: string } & { age: number }
+
+// Compile errors:
+// builder.build() - Error: build() not available until required fields set
+// builder.setName('Alice').build() - Error: age not set
+```
+
+### Use Fluent Interface with Immutable Type Accumulation
+
+```typescript
+class QueryBuilder<T extends Record<string, unknown> = Record<string, never>> {
+  private conditions: string[] = [];
+  private selectedFields: string[] = [];
+
+  select<K extends string>(field: K): QueryBuilder<T & Record<K, unknown>> {
+    this.selectedFields.push(field);
+    return this as unknown as QueryBuilder<T & Record<K, unknown>>;
+  }
+
+  where(condition: string): this {
+    this.conditions.push(condition);
+    return this;
+  }
+
+  build(): { fields: string[]; conditions: string[] } {
+    return { fields: this.selectedFields, conditions: this.conditions };
+  }
+}
+
+const query = new QueryBuilder()
+  .select('id')
+  .select('name')
+  .where('active = true')
+  .build();
+```
+
+---
+
+## Common Generic Patterns
+
+### MaybePromise
+
+```typescript
+type MaybePromise<T> = T | Promise<T>;
+
+async function normalize<T>(value: MaybePromise<T>): Promise<T> {
+  return await value;
+}
+```
+
+### DeepPartial
+
+```typescript
+type DeepPartial<T> = T extends (infer U)[]
+  ? DeepPartial<U>[]
+  : T extends object
+  ? { [K in keyof T]?: DeepPartial<T[K]> }
+  : T;
+```
+
+### PathOf and Get
+
+```typescript
+// PathOf: all dot-notation paths into an object
+type PathOf<T> = T extends object
+  ? { [K in keyof T]: K extends string
+      ? T[K] extends object
+        ? K | `${K}.${PathOf<T[K]>}`
+        : K
+      : never
+    }[keyof T]
+  : never;
+
+// Get: value at a path
+type Get<T, P extends string> =
+  P extends `${infer K}.${infer Rest}`
+    ? K extends keyof T ? Get<T[K], Rest> : never
+    : P extends keyof T ? T[P] : never;
+```
+
+### Prettify (Flatten Intersection Types for Readability)
+
+```typescript
+type Prettify<T> = { [K in keyof T]: T[K] } & {};
+
+type A = { id: string } & { name: string } & { age: number };
+type B = Prettify<A>; // { id: string; name: string; age: number }
+// B displays as a single object in IDE hover, much more readable
+```
+
+### RequireAtLeastOne
+
+```typescript
+type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
+  Omit<T, Keys> &
+  { [K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>> }[Keys];
+
+interface ContactOptions {
+  email?: string;
+  phone?: string;
+  address?: string;
+}
+
+type Contact = RequireAtLeastOne<ContactOptions>;
+// Must provide at least one of email, phone, or address
+```
+
+### RequireExactlyOne
+
+```typescript
+type RequireExactlyOne<T, Keys extends keyof T = keyof T> =
+  Omit<T, Keys> &
+  { [K in Keys]: Required<Pick<T, K>> & { [O in Exclude<Keys, K>]?: never } }[Keys];
+
+interface PaymentMethod {
+  creditCard?: { number: string };
+  bankTransfer?: { account: string };
+  paypal?: { email: string };
+}
+
+type Payment = RequireExactlyOne<PaymentMethod>;
+// Must provide exactly one payment method
+```

+ 659 - 0
skills/typescript-ops/references/type-system.md

@@ -0,0 +1,659 @@
+# TypeScript Type System Reference
+
+## Table of Contents
+
+1. [Literal Types](#literal-types)
+2. [Discriminated Unions](#discriminated-unions)
+3. [Branded/Nominal Types](#brandednominal-types)
+4. [Template Literal Types](#template-literal-types)
+5. [Recursive Types](#recursive-types)
+6. [satisfies Operator](#satisfies-operator)
+7. [Type Assertions](#type-assertions)
+8. [Declaration Merging](#declaration-merging)
+9. [Type-Level Arithmetic](#type-level-arithmetic)
+10. [Variance](#variance)
+
+---
+
+## Literal Types
+
+### Understand String, Number, and Boolean Literals
+
+Literal types restrict a value to one specific value rather than the broader primitive type.
+
+```typescript
+// String literal
+type Direction = 'north' | 'south' | 'east' | 'west';
+type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
+
+// Number literal
+type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
+type HttpSuccess = 200 | 201 | 204;
+
+// Boolean literal
+type Truthy = true;
+type Falsy = false;
+
+// Mixed literal union
+type Status = 'pending' | 'fulfilled' | 'rejected';
+type Result = 0 | 1 | -1;
+
+function move(direction: Direction): void {
+  console.log(`Moving ${direction}`);
+}
+
+move('north');  // OK
+move('up');     // Error: Argument of type '"up"' is not assignable to parameter of type 'Direction'
+```
+
+### Use const Assertions to Preserve Literal Types
+
+Without `as const`, TypeScript widens literals to their primitive types. With it, literals are preserved.
+
+```typescript
+// Without as const - types are widened
+const config = {
+  endpoint: '/api',   // string
+  retries: 3,         // number
+  methods: ['GET', 'POST'], // string[]
+};
+
+// With as const - all literals preserved
+const CONFIG = {
+  endpoint: '/api',   // '/api'
+  retries: 3,         // 3
+  methods: ['GET', 'POST'], // readonly ['GET', 'POST']
+} as const;
+
+type Endpoint = typeof CONFIG.endpoint;  // '/api'
+type Retry   = typeof CONFIG.retries;    // 3
+type Methods = typeof CONFIG.methods[number]; // 'GET' | 'POST'
+
+// as const on arrays
+const ROLES = ['admin', 'user', 'moderator'] as const;
+type Role = typeof ROLES[number]; // 'admin' | 'user' | 'moderator'
+
+// as const on function arguments
+function configure<T extends object>(opts: T): Readonly<T> {
+  return Object.freeze(opts);
+}
+
+const opts = configure({ debug: true, port: 3000 } as const);
+// opts.debug is true (not boolean), opts.port is 3000 (not number)
+```
+
+### Derive Union Types from const Arrays
+
+```typescript
+const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const;
+type HttpMethod = typeof HTTP_METHODS[number];
+
+// Enum alternative using as const object
+const Color = {
+  Red: 'red',
+  Green: 'green',
+  Blue: 'blue',
+} as const;
+
+type Color = typeof Color[keyof typeof Color]; // 'red' | 'green' | 'blue'
+```
+
+---
+
+## Discriminated Unions
+
+### Build Discriminated Unions with a Shared Literal Property
+
+Every variant shares a common property (the discriminant) with a unique literal type.
+
+```typescript
+type Shape =
+  | { kind: 'circle'; radius: number }
+  | { kind: 'rectangle'; width: number; height: number }
+  | { kind: 'triangle'; base: number; height: number };
+
+function area(shape: Shape): number {
+  switch (shape.kind) {
+    case 'circle':
+      return Math.PI * shape.radius ** 2;
+    case 'rectangle':
+      return shape.width * shape.height;
+    case 'triangle':
+      return 0.5 * shape.base * shape.height;
+  }
+}
+```
+
+### Implement Exhaustiveness Checking with never
+
+When all union variants are handled, the remaining type is `never`. Passing `never` to a function that expects `never` causes a type error when a new variant is added.
+
+```typescript
+function assertNever(value: never, message?: string): never {
+  throw new Error(message ?? `Unhandled discriminated union member: ${JSON.stringify(value)}`);
+}
+
+type NetworkState =
+  | { status: 'idle' }
+  | { status: 'loading' }
+  | { status: 'success'; data: string }
+  | { status: 'error'; error: Error };
+
+function handleState(state: NetworkState): string {
+  switch (state.status) {
+    case 'idle':    return 'Waiting...';
+    case 'loading': return 'Loading...';
+    case 'success': return state.data;
+    case 'error':   return state.error.message;
+    default:        return assertNever(state); // Compile error if case is missing
+  }
+}
+```
+
+### Model Result Types as Discriminated Unions
+
+```typescript
+type Ok<T>  = { ok: true;  value: T };
+type Err<E> = { ok: false; error: E };
+type Result<T, E = Error> = Ok<T> | Err<E>;
+
+function divide(a: number, b: number): Result<number, string> {
+  if (b === 0) return { ok: false, error: 'Division by zero' };
+  return { ok: true, value: a / b };
+}
+
+const result = divide(10, 2);
+if (result.ok) {
+  console.log(result.value); // number
+} else {
+  console.error(result.error); // string
+}
+```
+
+---
+
+## Branded/Nominal Types
+
+### Create Branded Types to Prevent Type Confusion
+
+TypeScript uses structural typing: two types with the same shape are interchangeable. Branding adds a phantom property to make them nominally distinct.
+
+```typescript
+type Brand<T, B extends string> = T & { readonly __brand: B };
+
+type UserId   = Brand<string, 'UserId'>;
+type OrderId  = Brand<string, 'OrderId'>;
+type Email    = Brand<string, 'Email'>;
+type Dollars  = Brand<number, 'Dollars'>;
+type Cents    = Brand<number, 'Cents'>;
+
+// Without branding these are all just 'string' - interchangeable and unsafe.
+// With branding they are distinct.
+function getUser(id: UserId): void { /* ... */ }
+function getOrder(id: OrderId): void { /* ... */ }
+
+declare const userId: UserId;
+declare const orderId: OrderId;
+
+getUser(userId);   // OK
+getUser(orderId);  // Error: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'
+```
+
+### Write Validation Functions That Return Branded Types
+
+```typescript
+function brandUserId(raw: string): UserId {
+  return raw as UserId;
+}
+
+function parseEmail(raw: string): Email {
+  if (!/^[^@]+@[^@]+\.[^@]+$/.test(raw)) {
+    throw new Error(`Invalid email: ${raw}`);
+  }
+  return raw as Email;
+}
+
+function parseDollars(amount: number): Dollars {
+  if (amount < 0) throw new Error('Dollars cannot be negative');
+  return amount as Dollars;
+}
+
+// Use in domain logic - type system enforces correct usage
+function sendInvoice(to: Email, amount: Dollars): void { /* ... */ }
+```
+
+### Use Opaque Types via Unique Symbol (Advanced)
+
+For stricter encapsulation, use unique symbols as the brand key.
+
+```typescript
+declare const _brand: unique symbol;
+
+type Opaque<T, Tag> = T & { readonly [_brand]: Tag };
+
+type PositiveInt = Opaque<number, 'PositiveInt'>;
+
+function toPositiveInt(n: number): PositiveInt {
+  if (!Number.isInteger(n) || n <= 0) {
+    throw new Error(`Expected positive integer, got ${n}`);
+  }
+  return n as PositiveInt;
+}
+```
+
+---
+
+## Template Literal Types
+
+### Build Type-Safe String Patterns
+
+Template literal types compose string literals at the type level.
+
+```typescript
+type EventName<T extends string> = `on${Capitalize<T>}`;
+type ClickEvent = EventName<'click'>;   // 'onClick'
+type ChangeEvent = EventName<'change'>; // 'onChange'
+
+type CSSProperty = 'margin' | 'padding';
+type CSSUnit = 'px' | 'em' | 'rem' | '%';
+type CSSValue = `${number}${CSSUnit}`; // '10px', '1.5em', etc.
+
+// Route parameter extraction
+type RouteParam<T extends string> =
+  T extends `${string}:${infer Param}/${infer Rest}`
+    ? Param | RouteParam<Rest>
+    : T extends `${string}:${infer Param}`
+    ? Param
+    : never;
+
+type Params = RouteParam<'/users/:userId/posts/:postId'>;
+// 'userId' | 'postId'
+```
+
+### Use String Manipulation Types
+
+```typescript
+type Uppercased = Uppercase<'hello world'>;   // 'HELLO WORLD'
+type Lowercased = Lowercase<'HELLO WORLD'>;   // 'hello world'
+type Capitalized = Capitalize<'hello'>;       // 'Hello'
+type Uncapitalized = Uncapitalize<'Hello'>;   // 'hello'
+
+// Build getter/setter types
+type Getter<T extends string> = `get${Capitalize<T>}`;
+type Setter<T extends string> = `set${Capitalize<T>}`;
+
+type FieldName = 'name' | 'age' | 'email';
+type Getters = { [K in FieldName as Getter<K>]: string };
+// { getName: string; getAge: string; getEmail: string }
+
+// Build event handler types from object keys
+type EventHandlers<T> = {
+  [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
+};
+
+interface FormFields { name: string; age: number; }
+type FormHandlers = EventHandlers<FormFields>;
+// { onNameChange: (value: string) => void; onAgeChange: (value: number) => void }
+```
+
+---
+
+## Recursive Types
+
+### Define the JSON Type
+
+```typescript
+type JsonPrimitive = string | number | boolean | null;
+type JsonArray    = JsonValue[];
+type JsonObject   = { [key: string]: JsonValue };
+type JsonValue    = JsonPrimitive | JsonArray | JsonObject;
+
+// Usage
+const data: JsonValue = {
+  name: 'Alice',
+  scores: [1, 2, 3],
+  address: { city: 'NYC', zip: null },
+};
+```
+
+### Implement Deep Readonly and Deep Partial
+
+```typescript
+type DeepReadonly<T> = T extends (infer U)[]
+  ? ReadonlyArray<DeepReadonly<U>>
+  : T extends object
+  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
+  : T;
+
+type DeepPartial<T> = T extends (infer U)[]
+  ? DeepPartial<U>[]
+  : T extends object
+  ? { [K in keyof T]?: DeepPartial<T[K]> }
+  : T;
+
+interface Config {
+  server: { host: string; port: number };
+  database: { url: string; poolSize: number };
+}
+
+type ReadonlyConfig = DeepReadonly<Config>;
+// server.host and all nested props are readonly
+
+type PartialConfig = DeepPartial<Config>;
+// All nested props optional
+```
+
+### Build Path Types for Safe Object Access
+
+```typescript
+type PathOf<T, Sep extends string = '.'> =
+  T extends object
+    ? {
+        [K in keyof T]: K extends string
+          ? T[K] extends object
+            ? K | `${K}${Sep}${PathOf<T[K], Sep>}`
+            : K
+          : never;
+      }[keyof T]
+    : never;
+
+type ValueAt<T, P extends string> =
+  P extends `${infer K}.${infer Rest}`
+    ? K extends keyof T
+      ? ValueAt<T[K], Rest>
+      : never
+    : P extends keyof T
+    ? T[P]
+    : never;
+
+interface User {
+  id: string;
+  profile: { name: string; address: { city: string } };
+}
+
+type UserPath = PathOf<User>;
+// 'id' | 'profile' | 'profile.name' | 'profile.address' | 'profile.address.city'
+
+type CityType = ValueAt<User, 'profile.address.city'>; // string
+```
+
+---
+
+## satisfies Operator
+
+### Validate Type Without Widening
+
+The `satisfies` operator checks that a value matches a type while preserving the most specific type.
+
+```typescript
+// Problem without satisfies:
+type Palette = Record<string, [number, number, number] | string>;
+
+const palette1: Palette = {
+  red: [255, 0, 0],
+  green: '#00ff00',
+};
+// palette1.red is [number, number, number] | string - information lost
+
+// With satisfies:
+const palette2 = {
+  red: [255, 0, 0],
+  green: '#00ff00',
+} satisfies Palette;
+// palette2.red is [number, number, number] - specific type preserved
+// palette2.green is string - specific type preserved
+
+palette2.red.map(v => v * 2); // OK - TypeScript knows it's an array
+palette2.green.toUpperCase(); // OK - TypeScript knows it's a string
+```
+
+### Combine satisfies with as const
+
+```typescript
+const routes = {
+  home: '/',
+  about: '/about',
+  user: '/users/:id',
+} as const satisfies Record<string, `/${string}`>;
+
+// Routes values are literal types, not string
+type HomeRoute = typeof routes.home; // '/'
+```
+
+### Use satisfies for Configuration Objects
+
+```typescript
+interface PluginConfig {
+  name: string;
+  version: string;
+  hooks?: {
+    beforeBuild?: () => void;
+    afterBuild?: () => void;
+  };
+}
+
+const myPlugin = {
+  name: 'my-plugin',
+  version: '1.0.0',
+  hooks: {
+    beforeBuild: () => console.log('building...'),
+  },
+} satisfies PluginConfig;
+
+// myPlugin.name is 'my-plugin' not string
+// TypeScript checks shape against PluginConfig at definition site
+```
+
+---
+
+## Type Assertions
+
+### Understand When Assertions Are Safe
+
+Type assertions (`as T`) override TypeScript's type inference. They are safe only when you have external information the compiler cannot verify.
+
+```typescript
+// SAFE: narrowing after a runtime check
+function processInput(input: unknown): string {
+  if (typeof input === 'string') {
+    return input; // narrowed, no assertion needed
+  }
+  // We know from domain logic this is always serializable
+  return String(input);
+}
+
+// SAFE: DOM API returns Element | null, but we know the element exists
+const canvas = document.getElementById('canvas') as HTMLCanvasElement;
+
+// UNSAFE: asserting unrelated types
+const num = 42 as unknown as string; // compiles, crashes at runtime
+```
+
+### Use Double Assertion as Escape Hatch
+
+When TypeScript refuses an assertion because types don't overlap, cast through `unknown`.
+
+```typescript
+// Only do this when you have proof the cast is correct
+function forceType<T>(value: unknown): T {
+  return value as T;
+}
+
+// Explicit escape: cast through unknown
+const risky = someValue as unknown as TargetType;
+```
+
+### Prefer Type Guards Over Assertions
+
+```typescript
+// BAD: assertion with no runtime check
+function getUser(data: unknown): User {
+  return data as User; // unsafe, no verification
+}
+
+// GOOD: type guard with runtime verification
+function isUser(data: unknown): data is User {
+  return (
+    typeof data === 'object' &&
+    data !== null &&
+    'id' in data &&
+    typeof (data as Record<string, unknown>).id === 'string' &&
+    'name' in data &&
+    typeof (data as Record<string, unknown>).name === 'string'
+  );
+}
+
+function getUser(data: unknown): User {
+  if (!isUser(data)) throw new Error('Invalid user data');
+  return data; // safe, narrowed by type guard
+}
+```
+
+---
+
+## Declaration Merging
+
+### Merge Interfaces to Extend Third-Party Types
+
+```typescript
+// Original interface from a library
+interface Request {
+  method: string;
+  url: string;
+}
+
+// Your augmentation - merges with above
+interface Request {
+  user?: { id: string; role: string };
+  requestId: string;
+}
+
+// Result: Request has method, url, user, requestId
+```
+
+### Augment Modules to Add Types to External Packages
+
+```typescript
+// express-augment.d.ts
+import 'express';
+
+declare module 'express' {
+  interface Request {
+    user?: { id: string; role: 'admin' | 'user' };
+    sessionId: string;
+  }
+}
+```
+
+### Augment Global Scope
+
+```typescript
+// global.d.ts
+declare global {
+  interface Window {
+    analytics: {
+      track(event: string, properties?: Record<string, unknown>): void;
+    };
+  }
+
+  interface Array<T> {
+    // Add a custom method to all arrays
+    groupBy<K extends string>(keyFn: (item: T) => K): Record<K, T[]>;
+  }
+}
+
+export {}; // Required to make this a module (not a script)
+```
+
+---
+
+## Type-Level Arithmetic
+
+### Measure Tuple Lengths
+
+```typescript
+type Length<T extends readonly unknown[]> = T['length'];
+
+type Three = Length<[1, 2, 3]>; // 3
+type Zero  = Length<[]>;         // 0
+```
+
+### Build a Recursive Counter
+
+```typescript
+// Build a tuple of length N, then read its length
+type BuildTuple<N extends number, T extends unknown[] = []> =
+  T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>;
+
+type Add<A extends number, B extends number> =
+  Length<[...BuildTuple<A>, ...BuildTuple<B>]>;
+
+type Sum = Add<3, 4>; // 7
+
+type Subtract<A extends number, B extends number> =
+  BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
+    ? Length<Rest>
+    : never;
+
+type Diff = Subtract<7, 3>; // 4
+```
+
+---
+
+## Variance
+
+### Understand Covariance and Contravariance
+
+- **Covariant**: A `Producer<Dog>` is assignable to `Producer<Animal>` (output position)
+- **Contravariant**: A `Consumer<Animal>` is assignable to `Consumer<Dog>` (input position)
+- **Invariant**: Neither assignment is safe
+
+```typescript
+// Covariant: return type position
+type Producer<out T> = () => T;
+
+declare let animalProducer: Producer<Animal>;
+declare let dogProducer: Producer<Dog>;
+
+animalProducer = dogProducer; // OK - Dog is a subtype of Animal
+
+// Contravariant: parameter type position
+type Consumer<in T> = (value: T) => void;
+
+declare let animalConsumer: Consumer<Animal>;
+declare let dogConsumer: Consumer<Dog>;
+
+dogConsumer = animalConsumer; // OK - Consumer<Animal> handles any Animal including Dog
+animalConsumer = dogConsumer; // Error - Consumer<Dog> can't handle all Animals
+```
+
+### Apply in/out Variance Annotations (TypeScript 4.7+)
+
+```typescript
+interface Animal { name: string; }
+interface Dog extends Animal { breed: string; }
+
+// Explicitly mark variance for clarity and performance
+interface ReadableStream<out T> {   // covariant - only produces T
+  read(): T;
+}
+
+interface WritableStream<in T> {    // contravariant - only consumes T
+  write(value: T): void;
+}
+
+interface Transform<in TInput, out TOutput> { // bivariant
+  transform(input: TInput): TOutput;
+}
+```
+
+### Recognize Function Parameter Bivariance Trap
+
+```typescript
+// strictFunctionTypes catches this
+type Callback = (event: MouseEvent) => void;
+type Handler  = (event: Event) => void;
+
+// With strictFunctionTypes: NOT assignable (correct)
+// Without strictFunctionTypes: assignable (unsafe)
+```

+ 440 - 0
skills/typescript-ops/references/utility-types.md

@@ -0,0 +1,440 @@
+# TypeScript Utility Types Reference
+
+## Table of Contents
+
+1. [Built-in Utility Types](#built-in-utility-types)
+2. [Custom Utility Types](#custom-utility-types)
+3. [Type-Safe Object Operations](#type-safe-object-operations)
+4. [Array/Tuple Utilities](#arraytuple-utilities)
+5. [Function Utilities](#function-utilities)
+
+---
+
+## Built-in Utility Types
+
+### Object Shape Utilities
+
+```typescript
+// Partial<T> - Make all properties optional
+interface User { id: string; name: string; email: string; }
+type UpdateUser = Partial<User>; // { id?: string; name?: string; email?: string }
+
+function updateUser(id: string, patch: Partial<User>): User { /* ... */ }
+
+// Required<T> - Make all properties required
+interface Config { host?: string; port?: number; debug?: boolean; }
+type StrictConfig = Required<Config>; // { host: string; port: number; debug: boolean }
+
+// Readonly<T> - Make all properties readonly
+type ImmutableUser = Readonly<User>; // { readonly id: string; readonly name: string; ... }
+const frozen: ImmutableUser = { id: '1', name: 'Alice', email: 'a@b.com' };
+// frozen.name = 'Bob'; // Error: cannot assign to 'name' because it is a read-only property
+
+// Record<K, T> - Create an object type with keys K and values T
+type UserMap    = Record<string, User>;
+type StatusMap  = Record<'active' | 'inactive' | 'banned', number>;
+type HttpStatus = Record<200 | 404 | 500, string>;
+
+// Pick<T, K> - Select a subset of properties
+type UserPreview = Pick<User, 'id' | 'name'>; // { id: string; name: string }
+type Credentials = Pick<User, 'email'>;         // { email: string }
+
+// Omit<T, K> - Exclude specific properties
+type PublicUser = Omit<User, 'email'>;   // { id: string; name: string }
+type NewUser    = Omit<User, 'id'>;      // { name: string; email: string }
+```
+
+### Union Manipulation Utilities
+
+```typescript
+// Exclude<T, U> - Remove U from union T
+type NonBoolean  = Exclude<string | number | boolean, boolean>; // string | number
+type NonNullish  = Exclude<string | null | undefined, null | undefined>; // string
+type NonString   = Exclude<string | number | boolean, string>; // number | boolean
+
+// Extract<T, U> - Keep only members of T that are assignable to U
+type OnlyStrings = Extract<string | number | boolean, string>; // string
+type Primitives  = Extract<string | number | { id: string }, string | number>; // string | number
+
+// NonNullable<T> - Remove null and undefined from T
+type SafeString = NonNullable<string | null | undefined>; // string
+type SafeUser   = NonNullable<User | null | undefined>;   // User
+```
+
+### Function Utilities
+
+```typescript
+function fetchData(url: string, timeout: number, headers: Record<string, string>): Promise<unknown> {
+  return fetch(url);
+}
+
+// ReturnType<T> - Get the return type of a function type
+type FetchResult = ReturnType<typeof fetchData>;     // Promise<unknown>
+type StringLength = ReturnType<typeof String.prototype.indexOf>; // number
+
+// Parameters<T> - Get parameters as a tuple type
+type FetchParams = Parameters<typeof fetchData>;
+// [url: string, timeout: number, headers: Record<string, string>]
+
+// Call a function with stored parameters
+function withDefaults<T extends (...args: unknown[]) => unknown>(
+  fn: T,
+  defaults: Partial<Parameters<T>>
+) { /* ... */ }
+
+// ConstructorParameters<T> - Get constructor parameter types
+class HttpClient {
+  constructor(baseUrl: string, timeout: number) {}
+}
+type HttpArgs = ConstructorParameters<typeof HttpClient>; // [string, number]
+
+// InstanceType<T> - Get the type of a class instance
+type ClientInstance = InstanceType<typeof HttpClient>; // HttpClient
+
+// ThisParameterType<T> - Extract the type of 'this'
+function greet(this: { name: string }, greeting: string): string {
+  return `${greeting}, ${this.name}`;
+}
+type GreetThis = ThisParameterType<typeof greet>; // { name: string }
+
+// OmitThisParameter<T> - Remove this parameter from function type
+type GreetFn = OmitThisParameter<typeof greet>; // (greeting: string) => string
+```
+
+### Awaited and String Utilities
+
+```typescript
+// Awaited<T> - Recursively unwrap Promise
+type A = Awaited<Promise<string>>;          // string
+type B = Awaited<Promise<Promise<number>>>; // number
+type C = Awaited<string | Promise<number>>; // string | number
+
+async function loadData(): Promise<User[]> { return []; }
+type LoadResult = Awaited<ReturnType<typeof loadData>>; // User[]
+
+// String manipulation (compile-time only, no runtime effect)
+type UpperName = Uppercase<'hello'>;      // 'HELLO'
+type LowerName = Lowercase<'WORLD'>;      // 'world'
+type CapName   = Capitalize<'alice'>;     // 'Alice'
+type UnCapName = Uncapitalize<'Hello'>;   // 'hello'
+
+// Useful for generating method names
+type Methods<T extends string> = `get${Capitalize<T>}` | `set${Capitalize<T>}`;
+type NameMethods = Methods<'name' | 'age'>; // 'getName' | 'getAge' | 'setName' | 'setAge'
+```
+
+---
+
+## Custom Utility Types
+
+### DeepReadonly
+
+```typescript
+type DeepReadonly<T> =
+  T extends (infer U)[]
+    ? ReadonlyArray<DeepReadonly<U>>
+    : T extends object
+    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
+    : T;
+
+interface AppState {
+  user: { id: string; profile: { name: string; bio: string } };
+  settings: { theme: string; notifications: boolean[] };
+}
+
+type FrozenState = DeepReadonly<AppState>;
+declare const state: FrozenState;
+// state.user.profile.name = 'x'; // Error - deeply readonly
+```
+
+### DeepPartial
+
+```typescript
+type DeepPartial<T> =
+  T extends (infer U)[]
+    ? DeepPartial<U>[]
+    : T extends object
+    ? { [K in keyof T]?: DeepPartial<T[K]> }
+    : T;
+
+// Useful for deep merge / patch operations
+function deepMerge<T>(target: T, patch: DeepPartial<T>): T {
+  if (typeof patch !== 'object' || patch === null) return patch as T;
+  const result = { ...target };
+  for (const key of Object.keys(patch) as (keyof T)[]) {
+    const val = patch[key as keyof typeof patch];
+    if (val !== undefined) {
+      (result[key] as unknown) = typeof val === 'object' && val !== null
+        ? deepMerge(result[key] as object, val as DeepPartial<object>)
+        : val;
+    }
+  }
+  return result;
+}
+```
+
+### Nullable and Optional
+
+```typescript
+type Nullable<T> = T | null;
+type Optional<T> = T | undefined;
+type NullableOptional<T> = T | null | undefined;
+
+// Require at least one of specified keys
+type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
+  Omit<T, Keys> &
+  { [K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>> }[Keys];
+
+// Require exactly one of specified keys
+type RequireExactlyOne<T, Keys extends keyof T = keyof T> =
+  Omit<T, Keys> &
+  { [K in Keys]: Required<Pick<T, K>> & { [O in Exclude<Keys, K>]?: never } }[Keys];
+```
+
+### Merge and UnionToIntersection
+
+```typescript
+// Merge two types, second overrides first
+type Merge<T, U> = Omit<T, keyof U> & U;
+
+type A = { id: string; name: string; active: boolean };
+type B = { name: number; extra: string }; // name changes type
+type C = Merge<A, B>; // { id: string; active: boolean; name: number; extra: string }
+
+// Convert a union to an intersection
+type UnionToIntersection<U> =
+  (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void
+    ? I
+    : never;
+
+type IntersectedABC = UnionToIntersection<{ a: string } | { b: number } | { c: boolean }>;
+// { a: string } & { b: number } & { c: boolean }
+
+// Prettify - flatten intersection types for readable IDE output
+type Prettify<T> = { [K in keyof T]: T[K] } & {};
+```
+
+### Exact and StrictOmit
+
+```typescript
+// Ensure no extra properties (useful in function params)
+type Exact<T, Shape> = T extends Shape
+  ? Exclude<keyof T, keyof Shape> extends never
+    ? T
+    : never
+  : never;
+
+// StrictOmit: errors if K is not in T (unlike Omit which silently ignores)
+type StrictOmit<T, K extends keyof T> = Omit<T, K>;
+```
+
+---
+
+## Type-Safe Object Operations
+
+### Type-Safe pick
+
+```typescript
+function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
+  return keys.reduce((acc, key) => {
+    acc[key] = obj[key];
+    return acc;
+  }, {} as Pick<T, K>);
+}
+
+const user: User = { id: '1', name: 'Alice', email: 'a@b.com' };
+const preview = pick(user, ['id', 'name']); // { id: string; name: string }
+// TypeScript knows preview has only 'id' and 'name'
+```
+
+### Type-Safe omit
+
+```typescript
+function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
+  const result = { ...obj };
+  keys.forEach((key) => delete result[key]);
+  return result as Omit<T, K>;
+}
+
+const publicUser = omit(user, ['email']); // { id: string; name: string }
+```
+
+### Type-Safe merge
+
+```typescript
+function merge<T extends object, U extends object>(base: T, override: U): Merge<T, U> {
+  return { ...base, ...override } as Merge<T, U>;
+}
+
+type Merge<T, U> = Omit<T, keyof U> & U;
+```
+
+### Type-Safe diff (keys present in T but not U)
+
+```typescript
+type Diff<T, U> = Pick<T, Exclude<keyof T, keyof U>>;
+
+type OnlyInA = Diff<{ a: string; b: number; c: boolean }, { b: number; d: string }>;
+// { a: string; c: boolean }
+```
+
+---
+
+## Array/Tuple Utilities
+
+### Head, Tail, Last, Reverse
+
+```typescript
+// First element of a tuple
+type Head<T extends unknown[]> =
+  T extends [infer H, ...unknown[]] ? H : never;
+
+// All but first element
+type Tail<T extends unknown[]> =
+  T extends [unknown, ...infer R] ? R : never;
+
+// Last element of a tuple
+type Last<T extends unknown[]> =
+  T extends [...unknown[], infer L] ? L : never;
+
+// Reverse a tuple
+type Reverse<T extends unknown[], Acc extends unknown[] = []> =
+  T extends [infer Head, ...infer Rest]
+    ? Reverse<Rest, [Head, ...Acc]>
+    : Acc;
+
+type H = Head<[string, number, boolean]>; // string
+type T = Tail<[string, number, boolean]>; // [number, boolean]
+type L = Last<[string, number, boolean]>; // boolean
+type R = Reverse<[1, 2, 3]>;             // [3, 2, 1]
+```
+
+### Flatten Types
+
+```typescript
+// Flatten one level
+type Flatten<T extends unknown[]> =
+  T extends (infer U)[] ? U : T;
+
+type F = Flatten<string[][]>; // string[]
+
+// Flatten nested arrays recursively
+type DeepFlatten<T> =
+  T extends (infer U)[]
+    ? U extends unknown[]
+      ? DeepFlatten<U>
+      : U
+    : T;
+
+type Deep = DeepFlatten<string[][][]>; // string
+```
+
+### Zip Two Tuples
+
+```typescript
+type Zip<T extends unknown[], U extends unknown[]> =
+  T extends [infer TH, ...infer TR]
+    ? U extends [infer UH, ...infer UR]
+      ? [[TH, UH], ...Zip<TR, UR>]
+      : []
+    : [];
+
+type Zipped = Zip<[1, 2, 3], ['a', 'b', 'c']>;
+// [[1, 'a'], [2, 'b'], [3, 'c']]
+```
+
+### Length and Indices
+
+```typescript
+type Length<T extends readonly unknown[]> = T['length'];
+
+// Generate numeric union of indices
+type Indices<T extends readonly unknown[]> =
+  Exclude<keyof T, keyof []>;
+
+type Len = Length<[1, 2, 3]>; // 3
+type Idx = Indices<['a', 'b', 'c']>; // '0' | '1' | '2'
+```
+
+---
+
+## Function Utilities
+
+### Promisify Type
+
+```typescript
+// Convert a callback-style function type to one returning a Promise
+type Promisify<T extends (...args: unknown[]) => unknown> =
+  T extends (...args: infer A) => infer R
+    ? R extends Promise<unknown>
+      ? T
+      : (...args: A) => Promise<Awaited<R>>
+    : never;
+
+type SyncFn = (x: number) => string;
+type AsyncFn = Promisify<SyncFn>; // (x: number) => Promise<string>
+```
+
+### Curry Type
+
+```typescript
+type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
+type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never;
+
+type Curried<TArgs extends unknown[], TReturn> =
+  TArgs extends []
+    ? TReturn
+    : (arg: Head<TArgs>) => Curried<Tail<TArgs>, TReturn>;
+
+declare function curry<TArgs extends unknown[], TReturn>(
+  fn: (...args: TArgs) => TReturn
+): Curried<TArgs, TReturn>;
+
+const add = curry((a: number, b: number) => a + b);
+const inc = add(1);     // Curried<[number], number> = (b: number) => number
+const two = inc(1);     // number
+```
+
+### Overload Helper
+
+```typescript
+// Extract all overload signatures as a union
+type Overloads<T extends (...args: unknown[]) => unknown> =
+  T extends {
+    (...args: infer A1): infer R1;
+    (...args: infer A2): infer R2;
+    (...args: infer A3): infer R3;
+    (...args: infer A4): infer R4;
+  }
+    ? ((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3) | ((...args: A4) => R4)
+    : T extends {
+        (...args: infer A1): infer R1;
+        (...args: infer A2): infer R2;
+        (...args: infer A3): infer R3;
+      }
+    ? ((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3)
+    : T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2 }
+    ? ((...args: A1) => R1) | ((...args: A2) => R2)
+    : T;
+```
+
+### Memoize with Type Safety
+
+```typescript
+type AnyFn = (...args: unknown[]) => unknown;
+
+function memoize<T extends AnyFn>(fn: T): T {
+  const cache = new Map<string, ReturnType<T>>();
+  return ((...args: Parameters<T>): ReturnType<T> => {
+    const key = JSON.stringify(args);
+    if (cache.has(key)) return cache.get(key) as ReturnType<T>;
+    const result = fn(...args) as ReturnType<T>;
+    cache.set(key, result);
+    return result;
+  }) as T;
+}
+
+const expensiveCalc = memoize((a: number, b: number): number => a * b);
+expensiveCalc(2, 3); // 6 - computed
+expensiveCalc(2, 3); // 6 - cached
+```

+ 0 - 0
skills/typescript-ops/scripts/.gitkeep