[autoresearch] baseline: system prompt quality tests (5/5 passing)
This commit is contained in:
1 parent
05cc036f6d
commit
60b0101717
142 files changed
+18589
-9947
No files matched your search
@@ -0,0 +1,62 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When building UI components, implementing user-facing interfaces, or working with frontend accessibility
|
||||
---
|
||||
|
||||
## Accessibility Principles
|
||||
|
||||
### Core Standard
|
||||
Follow **WCAG 2.1 Level AA** as the baseline for all user-facing interfaces.
|
||||
|
||||
### Semantic HTML
|
||||
- Use semantic elements (`<nav>`, `<main>`, `<article>`, `<section>`, `<button>`, `<form>`) over generic `<div>` and `<span>`
|
||||
- Each page has exactly one `<h1>`, with proper heading hierarchy (h1 → h2 → h3, no skipping)
|
||||
- Use `<label>` elements associated with form inputs (via `for`/`id` or wrapping)
|
||||
- Use `<table>` for tabular data, never for layout
|
||||
|
||||
### Keyboard Navigation
|
||||
- All interactive elements must be reachable via Tab key
|
||||
- Focus order follows logical reading order
|
||||
- Focus indicators must be visible (never `outline: none` without replacement)
|
||||
- Custom components implement proper keyboard patterns (Arrow keys for menus, Escape to close modals)
|
||||
- No keyboard traps — user can always Tab away from any component
|
||||
|
||||
### ARIA Attributes
|
||||
- **First rule of ARIA:** Don't use ARIA if a native HTML element can do the job
|
||||
- Use `aria-label` or `aria-labelledby` for elements without visible text labels
|
||||
- Use `aria-live` regions for dynamic content updates (toasts, notifications)
|
||||
- Use `role` attributes only for custom widgets that lack native semantics
|
||||
- Use `aria-expanded`, `aria-controls` for disclosure patterns (dropdowns, accordions)
|
||||
|
||||
### Color and Contrast
|
||||
- Text contrast ratio: minimum **4.5:1** against background (AA normal text)
|
||||
- Large text contrast ratio: minimum **3:1** (AA large text)
|
||||
- **Never use color alone** to convey information (add icons, text, or patterns)
|
||||
- Support both light and dark themes with proper contrast in each
|
||||
|
||||
### Images and Media
|
||||
- All `<img>` elements have descriptive `alt` text (or `alt=""` for decorative images)
|
||||
- Videos have captions or transcripts
|
||||
- Audio controls are accessible and labeled
|
||||
|
||||
### Forms
|
||||
- Error messages are programmatically associated with inputs (`aria-describedby`)
|
||||
- Required fields are indicated both visually and programmatically (`required` attribute)
|
||||
- Form validation errors are announced to screen readers
|
||||
|
||||
### Accessibility Checklist
|
||||
|
||||
- [ ] All interactive elements reachable via keyboard?
|
||||
- [ ] Focus indicators visible on all interactive elements?
|
||||
- [ ] Semantic HTML elements used (no `<div>` buttons)?
|
||||
- [ ] Heading hierarchy is correct (h1 → h2 → h3)?
|
||||
- [ ] Color contrast meets AA standards (4.5:1)?
|
||||
- [ ] Color is not the sole indicator of meaning?
|
||||
- [ ] Images have appropriate alt text?
|
||||
- [ ] Form inputs have associated labels?
|
||||
- [ ] ARIA used correctly (native first)?
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Security Principles @security-principles.md (XSS prevention in user content)
|
||||
- Testing Strategy @testing-strategy.md (accessibility testing)
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing REST/HTTP APIs (endpoints, handlers, middleware, or response formatting)
|
||||
---
|
||||
|
||||
## API Design Principles
|
||||
|
||||
### RESTful API Standards
|
||||
|
||||
**Resource-Based URLs:**
|
||||
|
||||
- Use plural nouns for resources: `/api/{version}/users`, `/api/{version}/orders`
|
||||
- Hierarchical relationships: `/api/{version}/users/:userId/orders`
|
||||
- Avoid verbs in URLs: `/api/{version}/getUser` ❌ → `/api/{version}/users/:id` ✅
|
||||
|
||||
**HTTP Methods:**
|
||||
|
||||
- GET: Read/retrieve resource (safe, idempotent, cacheable)
|
||||
- POST: Create new resource (not idempotent)
|
||||
- PUT: Replace entire resource (idempotent)
|
||||
- PATCH: Partial update (idempotent)
|
||||
- DELETE: Remove resource (idempotent)
|
||||
|
||||
**Versioning:**
|
||||
|
||||
- URL path versioning: `/api/{version}/users` e.g.`/api/v1/users` (explicit, clear)
|
||||
|
||||
**Pagination:**
|
||||
|
||||
- Limit results per page (default 20, max 100)
|
||||
- Cursor-based: `?cursor=abc123` (better for real-time data)
|
||||
- Offset-based: `?page=2&limit=20` (simpler, less accurate for changing data)
|
||||
|
||||
**Filtering and Sorting:**
|
||||
|
||||
- Filtering: `?status=active&role=admin`
|
||||
- Sorting: `?sort=created_at:desc,name:asc`
|
||||
- Searching: `?q=search+term`
|
||||
|
||||
### HTTP Status Codes and Error Categories
|
||||
|
||||
**Success Codes:**
|
||||
- 200 OK: Success (GET, PUT, PATCH)
|
||||
- 201 Created: Resource created successfully (POST)
|
||||
- 204 No Content: Success with no response body (DELETE)
|
||||
|
||||
**Client Error Codes (4xx) - User Can Fix:**
|
||||
|
||||
**Validation Errors (400 Bad Request):**
|
||||
- User input doesn't meet requirements
|
||||
- Examples: Invalid email, password too short, required field missing
|
||||
- Response: Detailed field-level errors
|
||||
- User action: Correct input and retry
|
||||
|
||||
**Authentication Errors (401 Unauthorized):**
|
||||
- Identity verification failed
|
||||
- Examples: Invalid credentials, expired token, missing token
|
||||
- User action: Provide valid credentials
|
||||
|
||||
**Authorization Errors (403 Forbidden):**
|
||||
- Permission denied (user identified but lacks permission)
|
||||
- User action: Contact admin for permission
|
||||
|
||||
**Not Found Errors (404 Not Found):**
|
||||
- Resource doesn't exist or user lacks permission to know it exists
|
||||
- User action: None (doesn't exist)
|
||||
|
||||
**Business Rule Violations (409 Conflict / 422 Unprocessable Entity):**
|
||||
- Domain rule violations
|
||||
- Examples: Insufficient balance, duplicate email, order already shipped
|
||||
- Response: Business rule explanation
|
||||
- User action: Depends on business context
|
||||
|
||||
**Rate Limiting (429 Too Many Requests):**
|
||||
- Too many requests in time window
|
||||
- User action: Wait and retry
|
||||
|
||||
**Server Error Codes (5xx) - System Issue:**
|
||||
|
||||
**Infrastructure Errors (500/502/503):**
|
||||
- Database down, network timeout, external service failure
|
||||
- Response: Generic message with correlation ID
|
||||
- User action: None (system issue, retry later)
|
||||
|
||||
### API Success Response Format
|
||||
```
|
||||
{
|
||||
"data": { /* resource or array of resources */ },
|
||||
"meta": {
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"perPage": 20
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/v1/users?page=1",
|
||||
"next": "/api/v1/users?page=2",
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Error Response Format
|
||||
|
||||
All API errors must follow a consistent envelope structure, matching the success format where possible or using a standard error envelope.
|
||||
```
|
||||
{
|
||||
"status": "error", // Transport: Always "error" or "fail"
|
||||
"code": 400, // Transport: Redundant HTTP Status
|
||||
"error": { // Domain: The actual business problem
|
||||
"code": "VALIDATION_ERROR", // Machine-readable business code (UPPER_SNAKE)
|
||||
"message": "Invalid email format", // Human-readable message
|
||||
"details": { // Optional: Structured context
|
||||
"field": "email",
|
||||
"reason": "Must be a valid address"
|
||||
},
|
||||
"correlationId": "req-1234567890", // Ops: Traceability
|
||||
"doc_url": "https://..." // Optional: Help link
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Data Serialization and Interchange Principles @data-serialization-and-interchange-principles.md
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Architectural Patterns — Testability-First Design
|
||||
|
||||
### Core Principle
|
||||
All code must be independently testable without running the full application or external infrastructure.
|
||||
|
||||
### Universal Architecture Rules
|
||||
|
||||
#### Rule 1: I/O Isolation
|
||||
**Problem:** Tightly coupled I/O makes tests slow, flaky, and environment-dependent.
|
||||
|
||||
**Solution:** Abstract all I/O behind interfaces/contracts:
|
||||
- Database queries
|
||||
- HTTP calls (to external APIs)
|
||||
- File system operations
|
||||
- Time/randomness (for determinism)
|
||||
- Message queues
|
||||
|
||||
**Implementation Discovery:**
|
||||
1. Search the codebase for existing abstraction patterns by looking for symbols named: `Interface`, `Mock`, `Repository`, `Store`, `Adapter` (use your available codebase search tool — Pathfinder, grep, IDE symbol search, etc.)
|
||||
2. Match the style (interface in Go, Protocol in Python, interface in TypeScript)
|
||||
3. Implement production adapter AND test adapter
|
||||
|
||||
**Example (Go):**
|
||||
|
||||
```Go
|
||||
|
||||
// Contract
|
||||
type UserStore interface {
|
||||
Create(ctx context.Context, user User) error
|
||||
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
}
|
||||
|
||||
// Production adapter
|
||||
type PostgresUserStore struct { /* ... */ }
|
||||
|
||||
// Test adapter
|
||||
type MockUserStore struct { /* ... */ }
|
||||
```
|
||||
|
||||
**Example (TypeScript/Vue):**
|
||||
```typescript
|
||||
|
||||
// Contract (service layer)
|
||||
export interface TaskAPI {
|
||||
createTask(title: string): Promise<Task>;
|
||||
getTasks(): Promise<Task[]>;
|
||||
}
|
||||
|
||||
// Production adapter
|
||||
export class BackendTaskAPI implements TaskAPI { /* ... */ }
|
||||
|
||||
// Test adapter (vi.mock or manual)
|
||||
export class MockTaskAPI implements TaskAPI { /* ... */ }
|
||||
|
||||
```
|
||||
|
||||
#### Rule 2: Pure Business Logic
|
||||
**Problem:** Business rules mixed with I/O are impossible to test without infrastructure.
|
||||
|
||||
**Solution:** Extract calculations, validations, transformations into pure functions:
|
||||
- Input → Output, no side effects
|
||||
- Deterministic: same input = same output
|
||||
- No I/O inside business rules
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
|
||||
// ✅ Pure function - easy to test
|
||||
func calculateDiscount(items []Item, coupon Coupon) (float64, error) {
|
||||
// Pure calculation, returns value
|
||||
}
|
||||
|
||||
// ❌ Impure - database call inside
|
||||
func calculateDiscount(ctx context.Context, items []Item, coupon Coupon) (float64, error) {
|
||||
validCoupon, err := db.GetCoupon(ctx, coupon.ID) // NO!
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Correct approach:**
|
||||
```
|
||||
|
||||
// 1. Fetch dependencies first (in handler/service)
|
||||
validCoupon, err := store.GetCoupon(ctx, coupon.ID)
|
||||
|
||||
// 2. Pass to pure logic
|
||||
discount, err := calculateDiscount(items, validCoupon)
|
||||
|
||||
// 3. Persist result
|
||||
err = store.SaveOrder(ctx, order)
|
||||
|
||||
```
|
||||
|
||||
#### Rule 3: Dependency Direction
|
||||
**Principle:** Dependencies point inward toward business logic.
|
||||
|
||||
```
|
||||
|
||||
┌──────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ (DB, HTTP, Files, External APIs) │
|
||||
│ │
|
||||
│ Depends on ↓ │
|
||||
└──────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────┐
|
||||
│ Contracts/Interfaces Layer │
|
||||
│ (Abstract ports - no implementation)│
|
||||
│ │
|
||||
│ Depends on ↓ │
|
||||
└──────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────┐
|
||||
│ Business Logic Layer │
|
||||
│ (Pure functions, domain rules) │
|
||||
│ NO dependencies on infrastructure │
|
||||
└──────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
**Never:**
|
||||
- Business logic imports database driver
|
||||
- Domain entities import HTTP framework
|
||||
- Core calculations import config files
|
||||
|
||||
**Always:**
|
||||
- Infrastructure implements interfaces defined by business layer
|
||||
- Business logic receives dependencies via injection
|
||||
|
||||
### Pattern Discovery Protocol
|
||||
|
||||
**Before implementing ANY feature:**
|
||||
|
||||
1. **Search existing patterns** (MANDATORY):
|
||||
Search the codebase for symbols named: `Interface`, `Repository`, `Service`, `Store`, `Mock`
|
||||
(use your available codebase search tool — Pathfinder, grep, IDE symbol search, etc.)
|
||||
|
||||
2. **Examine 3 existing modules** for consistency:
|
||||
- How do they handle database access?
|
||||
- Where are pure functions vs I/O operations?
|
||||
- What testing patterns exist?
|
||||
|
||||
3. **Document pattern** (>80% consistency required):
|
||||
- "Following pattern from [task, user, auth] modules"
|
||||
- "X/Y modules use interface-based stores"
|
||||
- "All tests use [MockStore, vi.mock, TestingPinia] pattern"
|
||||
|
||||
4. **If consistency <80%**: STOP and report fragmentation to human.
|
||||
|
||||
### Testability Compliance
|
||||
|
||||
These are **architectural** requirements that the code structure must satisfy — not testing mechanics. A design that cannot meet them is not compliant with this architecture.
|
||||
|
||||
**Unit testability (non-negotiable):**
|
||||
- Unit tests MUST run without starting any database, external service, or network call
|
||||
- All I/O dependencies MUST be abstractable to the point where a mock can replace them
|
||||
- Business logic MUST be exercisable in isolation from infrastructure (enforced by Rules 1 & 2)
|
||||
|
||||
**Integration testability:**
|
||||
- Every I/O adapter MUST be independently testable against real infrastructure
|
||||
- Adapters must be replaceable — the application must not hard-wire a specific implementation
|
||||
|
||||
**Test co-location (structural rule):**
|
||||
- Default: Unit and integration tests co-locate with the implementation they test (test lives next to the code)
|
||||
- E2E tests: isolated in a dedicated directory — `e2e/` at the project root for single-app projects, or `apps/e2e/` for monorepos. They cross feature boundaries and belong to none. See Testing Strategy @testing-strategy.md for the complete E2E directory layout.
|
||||
- **Language overrides:** Some ecosystems have different conventions (e.g., Flutter uses a mirrored `test/` directory, Rust uses inline `#[cfg(test)]` blocks). When a language-specific project structure file exists, its test location rules take precedence over this default. See the relevant `project-structure-*` file for authoritative guidance.
|
||||
|
||||
> For test pyramid proportions, naming conventions, tool choices, and language-specific tooling, see Testing Strategy @testing-strategy.md.
|
||||
|
||||
### Language-Specific Idioms
|
||||
|
||||
For language-specific abstraction patterns and test tooling, see the dedicated idiom files listed in `code-idioms-and-conventions.md`. Those files contain the authoritative guidance per ecosystem and are independently maintainable.
|
||||
|
||||
**Universal rule:** In every language, the testability pattern is the same — I/O is behind an interface, business logic is pure, and tests inject a mock/fake implementation. The idiom files describe *how* to express this pattern in each ecosystem's syntax.
|
||||
|
||||
### Enforcement Checklist
|
||||
|
||||
Before marking code complete, verify:
|
||||
- [ ] Can I run unit tests without starting database/external services?
|
||||
- [ ] Are all I/O operations behind an abstraction?
|
||||
- [ ] Is business logic pure (no side effects)?
|
||||
- [ ] Do integration tests exist for all adapters?
|
||||
- [ ] Does pattern match existing codebase (>80% consistency)?
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
- Project Structure @project-structure.md
|
||||
- Database Design Principles @database-design-principles.md
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: >
|
||||
When configuring Kubernetes deployments, GitOps infrastructure, or deployment
|
||||
strategies (blue-green, canary, rolling). Load this file in addition to
|
||||
ci-cd-principles.md when the tech stack uses Kubernetes or a managed container
|
||||
platform with orchestration (GKE, EKS, AKS).
|
||||
---
|
||||
|
||||
## CI/CD — GitOps and Kubernetes Deployment
|
||||
|
||||
> **Agent scope:** This is a supplement to `ci-cd-principles.md` (Level 2).
|
||||
> Apply only when the project targets Kubernetes or a Kubernetes-based managed platform.
|
||||
> Do not apply rolling/canary/blue-green patterns to Docker Compose or serverless deployments.
|
||||
|
||||
---
|
||||
|
||||
### Choosing a Deployment Strategy
|
||||
|
||||
| Strategy | When to use | Trade-off |
|
||||
|----------|-------------|-----------|
|
||||
| **Rolling** | Default for most services; SLO requirements | Simple, but mixes versions briefly |
|
||||
| **Blue-Green** | Zero-downtime with instant rollback | Doubles infrastructure cost during switch |
|
||||
| **Canary** | Risk-reducing incremental rollout; A/B variants | Requires traffic splitting capability |
|
||||
|
||||
---
|
||||
|
||||
### Rolling Deployment (Default)
|
||||
|
||||
Kubernetes native. Suitable for most stateless services.
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25% # Max pods above desired count during update
|
||||
maxUnavailable: 0% # Zero-downtime: don't remove pods before new ones ready
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Set `maxUnavailable: 0` for production services with SLO requirements
|
||||
- Set `minReadySeconds` to let pods stabilize before proceeding
|
||||
- Configure `terminationGracePeriodSeconds` to finish in-flight requests before shutdown
|
||||
|
||||
---
|
||||
|
||||
### Blue-Green Deployment
|
||||
|
||||
**When:** Zero-downtime deployments where rollback must be instant and clean.
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Blue (v1) │ │ Green (v2) │
|
||||
│ [LIVE 100%] │────→│ [STANDBY] │
|
||||
└──────────────┘ └──────────────┘
|
||||
↕ Switch load balancer
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Blue (v1) │ │ Green (v2) │
|
||||
│ [STANDBY] │ │ [LIVE 100%] │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Both environments must be identical in infrastructure
|
||||
- Run smoke tests against green before switching traffic
|
||||
- Keep blue alive for at least 30 minutes post-switch (fast rollback window)
|
||||
- Database migrations must be backward-compatible (blue still runs against the same DB)
|
||||
|
||||
---
|
||||
|
||||
### Canary Deployment
|
||||
|
||||
**When:** Risk-reducing incremental rollout; A/B testing deployment variants.
|
||||
|
||||
```
|
||||
Traffic split during rollout:
|
||||
5% → canary (v2)
|
||||
95% → stable (v1)
|
||||
↓ metrics look good
|
||||
25% → canary
|
||||
75% → stable
|
||||
↓ bake time passes
|
||||
100% → canary (now stable)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Define success metrics before starting rollout (error rate, latency SLO)
|
||||
- Set automatic rollback threshold: if canary error rate > 2× baseline → auto-rollback
|
||||
- Minimum bake time per traffic increment: 15–30 minutes
|
||||
- Feature flags complement canary routing for functional (not just traffic) testing
|
||||
|
||||
---
|
||||
|
||||
### GitOps Pattern
|
||||
|
||||
For Kubernetes environments, use **declarative GitOps** instead of imperative `kubectl apply`.
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```
|
||||
Application Repo (code) → CI builds image → updates tag in Config Repo
|
||||
Config Repo (K8s manifests) → ArgoCD/Flux syncs to cluster automatically
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Git is the **single source of truth** for cluster state
|
||||
- All changes to production go through PRs on the config repo — no direct `kubectl` in prod
|
||||
- ArgoCD/Flux continuously reconciles — any manual drift is auto-corrected
|
||||
- Secrets reference external secret stores — **never plaintext in git**
|
||||
|
||||
#### Example ArgoCD Application
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: myapp-production
|
||||
spec:
|
||||
source:
|
||||
repoURL: https://github.com/org/config-repo
|
||||
path: environments/production/myapp
|
||||
targetRevision: HEAD
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: production
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true # Remove resources deleted from git
|
||||
selfHeal: true # Reconcile manual drift
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Secrets Management in Kubernetes
|
||||
|
||||
Never store plaintext secrets in git — even in private repositories.
|
||||
|
||||
| Tool | Pattern |
|
||||
|------|---------|
|
||||
| **External Secrets Operator** | Syncs secrets from AWS Secrets Manager, GCP Secret Manager, Vault into K8s Secrets |
|
||||
| **Sealed Secrets** | Encrypts secrets with a cluster-held key; safe to commit encrypted form |
|
||||
| **Vault Agent Injector** | Sidecars inject secrets directly into pods at runtime |
|
||||
|
||||
Choose based on your cloud provider or existing tooling. Document the choice in your
|
||||
technical architecture document.
|
||||
|
||||
---
|
||||
|
||||
### Kubernetes CI/CD Checklist
|
||||
|
||||
- [ ] Deployment strategy defined and documented (rolling / blue-green / canary)?
|
||||
- [ ] `maxUnavailable: 0` set for production services with SLO?
|
||||
- [ ] `terminationGracePeriodSeconds` configured for graceful shutdown?
|
||||
- [ ] GitOps in place — no direct `kubectl apply` commands in production CI pipeline?
|
||||
- [ ] Config repo exists, separate from application repo?
|
||||
- [ ] All PRs to config repo require review before ArgoCD/Flux sync?
|
||||
- [ ] Secrets stored in external secret store, not plaintext in git?
|
||||
- [ ] Rollback procedure tested and documented?
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
|
||||
- CI/CD Principles @ci-cd-principles.md (core pipeline — read first)
|
||||
- Security Principles @security-principles.md (secrets management)
|
||||
- Monitoring and Alerting Principles @monitoring-and-alerting-principles.md (canary success metrics)
|
||||
- Feature Flags @feature-flags-principles.md (canary + feature flag interaction)
|
||||
@@ -0,0 +1,348 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When configuring CI/CD pipelines, deployment processes, release strategies, or build configurations
|
||||
---
|
||||
|
||||
## CI/CD Principles
|
||||
|
||||
> **Agent scope:** This rule applies when writing CI/CD manifests
|
||||
> (Dockerfile, docker-compose, GitHub Actions, GitLab CI, etc.).
|
||||
> It is layered by deployment complexity — apply only the levels relevant to the project.
|
||||
|
||||
---
|
||||
|
||||
### Deployment Complexity Levels
|
||||
|
||||
| Level | Applies When | Key Additions |
|
||||
|-------|-------------|---------------|
|
||||
| **0 — All projects** | Always | Lint, test, security scan, secrets management |
|
||||
| **1 — Containerized** | Docker image is the artifact | Multi-stage build, image scan, SBOM attestation |
|
||||
| **2 — Orchestrated** | Kubernetes or managed container platform | Deployment strategies, GitOps |
|
||||
|
||||
Load supplementary rules when reaching Level 2:
|
||||
- Deployment strategies + GitOps → **@ci-cd-gitops-kubernetes.md**
|
||||
|
||||
---
|
||||
|
||||
### Level 0 — Universal Pipeline Design
|
||||
|
||||
**Pipeline Stages (in order):**
|
||||
|
||||
1. **Lint** — static analysis, formatting checks
|
||||
2. **Build** — compile, bundle, generate artifacts
|
||||
3. **Unit Test** — fast tests with mocked dependencies
|
||||
4. **Integration Test** — tests against real dependencies (Testcontainers)
|
||||
5. **Security Scan** — dependency audit, SAST, secrets detection
|
||||
6. **Deploy** — push to target environment
|
||||
|
||||
**Rules:**
|
||||
|
||||
- **Fail fast** — run cheapest checks first (lint before build, build before test)
|
||||
- **Pipeline must be deterministic** — same input = same output, every time
|
||||
- **Keep pipelines under 15 minutes** — optimize slow stages
|
||||
- **Never skip failing steps** — fix the pipeline, don't bypass it
|
||||
- **Build once, deploy many** — same artifact promotes through all environments
|
||||
|
||||
---
|
||||
|
||||
### Level 0 — Deploy Target Examples
|
||||
|
||||
The deploy stage varies by target. The pipeline stages before it are identical.
|
||||
|
||||
**Docker Compose (local / staging):**
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
**Cloud Run:**
|
||||
```bash
|
||||
gcloud run deploy myapp \
|
||||
--image gcr.io/project/myapp:$GIT_SHA \
|
||||
--region us-central1
|
||||
```
|
||||
|
||||
**Vercel (frontend SPA):**
|
||||
```bash
|
||||
vercel deploy --prod
|
||||
```
|
||||
|
||||
**Kubernetes:**
|
||||
Use GitOps — see **@ci-cd-gitops-kubernetes.md**.
|
||||
|
||||
---
|
||||
|
||||
### Level 0 — Manifest Patterns
|
||||
|
||||
#### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod # Pin via go.mod
|
||||
cache: true # Cache dependencies
|
||||
- run: gofumpt -l -e -d .
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
|
||||
test:
|
||||
needs: lint # Fail fast: lint before test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- run: go test -race -cover ./...
|
||||
|
||||
security:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Scan for secrets
|
||||
uses: trufflesecurity/trufflehog@v3 # Pin to release tag
|
||||
- name: Audit dependencies
|
||||
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Pin action versions (`@v4`, not `@latest` or `@main`)
|
||||
- Use `needs:` to enforce stage ordering
|
||||
- Cache dependencies (`cache: true` in setup actions)
|
||||
- Use `go-version-file` / `node-version-file` instead of hardcoding versions
|
||||
- Never put secrets in workflow files — use `${{ secrets.NAME }}`
|
||||
|
||||
---
|
||||
|
||||
### Level 1 — Containerized Projects
|
||||
|
||||
#### Dockerfile (Multi-Stage Build)
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Build
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download # Cache dependencies
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /bin/api ./cmd/api
|
||||
|
||||
# Stage 2: Runtime (minimal image)
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
COPY --from=builder /bin/api /bin/api
|
||||
EXPOSE 8080
|
||||
CMD ["/bin/api"]
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Always use multi-stage builds (build → runtime)
|
||||
- Pin base image versions (never use `:latest`)
|
||||
- Copy dependency files first, then source (layer caching)
|
||||
- Use minimal runtime images (distroless, alpine, scratch)
|
||||
- Never copy `.env`, secrets, or `.git` into images
|
||||
|
||||
#### Docker Compose (Local Development)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./apps/backend # Path per project-structure.md
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env_file: .env # Environment config
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine # Pin versions
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Always define health checks for dependencies
|
||||
- Use `depends_on` with `condition: service_healthy`
|
||||
- Pin all image versions
|
||||
- Use volumes for persistent data
|
||||
- Never hardcode credentials — use env_file or environment variables
|
||||
|
||||
#### Image Scan + SBOM Attestation
|
||||
|
||||
After building and pushing a container image, scan it and attach a signed SBOM attestation.
|
||||
|
||||
**Preferred approach: Cosign keyless signing (no key management required)**
|
||||
|
||||
Cosign integrates with your CI provider's OIDC token (GitHub Actions, GitLab CI) to sign
|
||||
images and attestations without storing or rotating cryptographic keys. The signature is
|
||||
anchored to a transparency log (Rekor), making it auditable and policy-enforceable.
|
||||
|
||||
```yaml
|
||||
build:
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # Required for Cosign keyless signing
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
- name: Build and push image
|
||||
id: build
|
||||
run: |
|
||||
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
|
||||
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
|
||||
- name: Scan container image
|
||||
run: |
|
||||
trivy image \
|
||||
--severity HIGH,CRITICAL \
|
||||
--exit-code 1 \
|
||||
ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
syft ghcr.io/${{ github.repository }}:${{ github.sha }} \
|
||||
-o cyclonedx-json > sbom.json
|
||||
|
||||
- name: Attest SBOM to image (keyless, OIDC-backed)
|
||||
run: |
|
||||
cosign attest \
|
||||
--predicate sbom.json \
|
||||
--type cyclonedx \
|
||||
ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
# Cosign uses the GitHub Actions OIDC token automatically.
|
||||
# No COSIGN_PASSWORD or secret keys required.
|
||||
```
|
||||
|
||||
**How the SBOM travels with the image:**
|
||||
|
||||
The SBOM attestation is stored as an OCI reference in the same container registry alongside
|
||||
the image digest. It requires no additional infrastructure — any registry that supports OCI
|
||||
artifacts (ghcr.io, Google Artifact Registry, AWS ECR, Docker Hub) works out of the box.
|
||||
|
||||
To verify the attestation at any time:
|
||||
```bash
|
||||
cosign verify-attestation \
|
||||
--type cyclonedx \
|
||||
ghcr.io/org/app@sha256:<digest>
|
||||
```
|
||||
|
||||
**Use ORAS instead of Cosign when:**
|
||||
- You need to attach arbitrary supply chain artifacts (scan reports, provenance JSON, build logs)
|
||||
that go beyond what Cosign's attestation model covers.
|
||||
- `oras attach ghcr.io/org/app@sha256:<digest> scan-report.json`
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Prefer Cosign keyless signing — eliminates secret key management overhead
|
||||
- SBOM is attached to the image in the OCI registry — not stored as a CI artifact
|
||||
- Scan BEFORE attesting — the SBOM reflects the scanned image
|
||||
- For non-containerized apps (Vercel, Netlify frontend), use `npm audit`/`yarn audit` instead;
|
||||
no SBOM attachment applies
|
||||
|
||||
---
|
||||
|
||||
### Deployment vs Release (Feature Flags)
|
||||
|
||||
Code deployment and feature release are separate concerns. When the PRD or technical
|
||||
architecture explicitly requires gradual rollout, A/B testing, or kill switches, feature
|
||||
flags can decouple them.
|
||||
|
||||
> **Agent rule:** Do NOT implement feature flags unless explicitly required by the
|
||||
> PRD or technical architecture document. See **@feature-flags-principles.md** for
|
||||
> implementation guidance when they are required.
|
||||
|
||||
---
|
||||
|
||||
### Environment Promotion
|
||||
|
||||
```
|
||||
dev → staging → production
|
||||
```
|
||||
|
||||
- **Dev:** Deployed on every push to feature branch
|
||||
- **Staging:** Deployed on merge to main/develop
|
||||
- **Production:** Deployed via manual approval or automated release
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Same artifacts promote through environments (build once, deploy many)
|
||||
- Environment-specific config via environment variables, not build flags
|
||||
- Never deploy directly to production without staging validation
|
||||
|
||||
---
|
||||
|
||||
### CI/CD Checklist
|
||||
|
||||
**Always (all projects):**
|
||||
- [ ] Pipeline stages run in correct order (lint → build → test → security → deploy)?
|
||||
- [ ] All versions pinned (base images, CI actions, tool versions)?
|
||||
- [ ] Dependency caching enabled?
|
||||
- [ ] No secrets in config files (use env vars or secrets manager)?
|
||||
- [ ] Secret scanning in CI?
|
||||
- [ ] Health checks defined for all service dependencies?
|
||||
- [ ] Pipeline completes in under 15 minutes?
|
||||
|
||||
**If building container images (Level 1):**
|
||||
- [ ] Multi-stage Docker builds used?
|
||||
- [ ] Container image scanned for HIGH/CRITICAL CVEs?
|
||||
- [ ] SBOM generated and attested to image via Cosign (keyless)?
|
||||
|
||||
**If deploying to Kubernetes (Level 2):**
|
||||
- [ ] Deployment strategy defined (blue-green, canary, or rolling)?
|
||||
- [ ] GitOps in place — no direct `kubectl apply` in production?
|
||||
- [ ] Secrets reference external store, not plaintext in git?
|
||||
- See **@ci-cd-gitops-kubernetes.md** for full checklist
|
||||
|
||||
**If feature flags are required by PRD/architecture:**
|
||||
- [ ] Flag infrastructure specified in tech architecture document?
|
||||
- [ ] Every flag has an owner and expiry date?
|
||||
- See **@feature-flags-principles.md** for full checklist
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
|
||||
- Code Completion Mandate @code-completion-mandate.md (validation before ship)
|
||||
- Security Mandate @security-mandate.md (secrets management)
|
||||
- Security Principles @security-principles.md (image scanning, SBOM)
|
||||
- Git Workflow Principles @git-workflow-principles.md (branch strategy)
|
||||
- Project Structure @project-structure.md (service paths)
|
||||
- Testing Strategy @testing-strategy.md (unit and integration test stages)
|
||||
- GitOps + Kubernetes Deployment @ci-cd-gitops-kubernetes.md
|
||||
- Feature Flags @feature-flags-principles.md
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Code Completion Mandate
|
||||
|
||||
### Universal Requirement
|
||||
|
||||
**Before marking any code task as complete, you MUST run automated quality checks and remediate all issues.**
|
||||
|
||||
This is NOT OPTIONAL. Delivering code without validation violates the Rugged Software Constitution @rugged-software-constitution.md.
|
||||
|
||||
### The Completion Checklist
|
||||
|
||||
Every code generation task follows this workflow:
|
||||
|
||||
1. **Generate** - Write the code based on requirements
|
||||
2. **Validate** - Run language-appropriate quality checks (see below)
|
||||
3. **Remediate** - Fix all detected issues
|
||||
4. **Verify** - Re-run checks to confirm fixes
|
||||
5. **Deliver** - Mark task complete only after all checks pass
|
||||
|
||||
**Never skip validation "to save time." Validation IS the work.**
|
||||
|
||||
### Language-Specific Quality Commands
|
||||
|
||||
The authoritative commands for each language live in the corresponding idiom file — this keeps the work of maintaining them close to the language expertise. Load the relevant file to get the exact commands to run:
|
||||
|
||||
| Language | Idiom File | Commands Section |
|
||||
| -------------------- | ---------------------------------- | -------------------------------- |
|
||||
| **Go** | @go-idioms-and-patterns.md | § Formatting and Static Analysis |
|
||||
| **TypeScript / Vue** | @typescript-idioms-and-patterns.md | § Formatting and Static Analysis |
|
||||
| **Vue 3** | @vue-idioms-and-patterns.md | § Linting and Type Checking |
|
||||
| **Flutter / Dart** | @flutter-idioms-and-patterns.md | § Linting and Formatting |
|
||||
| **Rust** | @rust-idioms-and-patterns.md | § Clippy and Formatting |
|
||||
|
||||
### Failure Protocol
|
||||
|
||||
**If any quality check fails:**
|
||||
|
||||
1. Read the error output completely
|
||||
2. Fix the identified issues in the code
|
||||
3. Re-run the failing command
|
||||
4. Do not proceed until all checks pass
|
||||
|
||||
> Never disable a lint rule or suppress a warning to make checks pass. Fix the root cause.
|
||||
|
||||
### Related Principles
|
||||
- Rugged Software Constitution @rugged-software-constitution.md
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Code Idioms and Conventions
|
||||
|
||||
### Universal Principle
|
||||
|
||||
**Write idiomatic code for the target language:**
|
||||
|
||||
- Code should look natural to developers familiar with that language
|
||||
- Follow established community conventions, not personal preferences
|
||||
- Use language built-ins and standard library effectively
|
||||
- Apply language-appropriate patterns (don't force patterns from other languages)
|
||||
|
||||
### Idiomatic Code Characteristics
|
||||
|
||||
- Leverages language features (don't avoid features unnecessarily)
|
||||
- Follows language naming conventions
|
||||
- Uses appropriate error handling for language (exceptions vs Result types)
|
||||
- Applies established community patterns
|
||||
|
||||
### Avoid Cross-Language Anti-Patterns
|
||||
|
||||
- ❌ Don't write "Java in Python" or "C in Go"
|
||||
- ❌ Don't force OOP patterns in functional languages
|
||||
- ❌ Don't avoid language features because they're "unfamiliar"
|
||||
- ✅ Learn and apply language-specific idioms
|
||||
|
||||
### Language-Specific Idioms
|
||||
|
||||
This file defines the universal principle. Each language has a dedicated file with concrete patterns, tooling choices, and idiom-specific rules. **Load the relevant file when working in that language** — it is the authoritative source for that ecosystem.
|
||||
|
||||
| Language / Framework | Idiom File | When to Load |
|
||||
| -------------------- | ---------------------------------- | ----------------------------------------------- |
|
||||
| **Go** | @go-idioms-and-patterns.md | Go services, APIs, CLI tools |
|
||||
| **TypeScript** | @typescript-idioms-and-patterns.md | Any TypeScript project (backend, frontend, CLI) |
|
||||
| **Vue 3** | @vue-idioms-and-patterns.md | Vue components, Pinia stores, composables |
|
||||
| **Flutter / Dart** | @flutter-idioms-and-patterns.md | Mobile apps, Flutter widgets, Riverpod |
|
||||
| **Rust** | @rust-idioms-and-patterns.md | Rust binaries, libraries, workspaces |
|
||||
|
||||
> Each language-specific file is independently changeable. It governs only its own ecosystem's coding idioms and defers to the relevant cross-cutting principle files (error handling, testing, logging, etc.) for universal guidance.
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Code Completion Mandate @code-completion-mandate.md
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Code Organization Principles
|
||||
|
||||
- Generate small, focused functions with clear single purposes (typically 10-50 lines)
|
||||
- Keep cognitive complexity low (cyclomatic complexity < 10 for most functions)
|
||||
- Maintain clear boundaries between different layers (presentation, business logic, data access)
|
||||
- Design for testability from the start, avoiding tight coupling that prevents testing
|
||||
- Apply consistent naming conventions that reveal intent without requiring comments
|
||||
|
||||
### Module Boundaries
|
||||
**Problem:** Cross-module coupling makes changes ripple across codebase.
|
||||
|
||||
**Solution:** Feature-based organization with clear public interfaces:
|
||||
- One feature = one directory
|
||||
- Each module exposes a public API (exported functions/classes)
|
||||
- Internal implementation details are private
|
||||
- Cross-module calls only through public API
|
||||
|
||||
**Directory Structure (Language-Agnostic):**
|
||||
|
||||
> Paths below are illustrative examples following `project-structure.md` — the single source of truth for project layout.
|
||||
> **Note:** The generic filenames below are illustrative. Always use language-specific naming conventions from the relevant `project-structure-*` file (e.g., `storage.go` in Go, `task.api.ts` in TypeScript, `task_repository.dart` in Dart).
|
||||
|
||||
```
|
||||
/task
|
||||
|
||||
- public_api.{ext} # Exported interface
|
||||
- business.{ext} # Pure logic
|
||||
- store.{ext} # I/O abstraction (interface)
|
||||
- postgres.{ext} # I/O implementation
|
||||
- mock.{ext} # Test implementation
|
||||
- test.{ext} # Unit tests (mocked I/O)
|
||||
- integration.test.{ext} # Integration tests (real I/O)
|
||||
```
|
||||
|
||||
**Go Example:**
|
||||
```
|
||||
apps/backend/internal/features/task/
|
||||
|
||||
- service.go # Public API of this feature (Service struct)
|
||||
- handler.go # HTTP handlers
|
||||
- logic.go # Pure domain logic
|
||||
- storage.go # interface Storage
|
||||
- storage_pg.go # implements Storage (PostgreSQL)
|
||||
- storage_mock.go # Mock implementation for testing
|
||||
- logic_test.go # Unit tests with MockStorage
|
||||
- postgres_integration_test.go # Integration test with Testcontainers
|
||||
```
|
||||
|
||||
**Vue Example:**
|
||||
```
|
||||
/apps/frontend/src/features/task
|
||||
|
||||
- index.ts # Public exports
|
||||
- task.service.ts # Business logic
|
||||
- task.api.ts # interface TaskAPI
|
||||
- task.api.backend.ts # implements TaskAPI
|
||||
- task.store.ts # Pinia store (uses TaskAPI)
|
||||
- task.service.spec.ts # Unit tests (mock API)
|
||||
```
|
||||
|
||||
### Feature Interaction Patterns
|
||||
|
||||
**Direct Import**
|
||||
|
||||
When a feature needs another feature's capabilities, import its Service directly:
|
||||
|
||||
```go
|
||||
// In features/order/logic.go
|
||||
import "yourapp/internal/features/task"
|
||||
|
||||
type Logic struct {
|
||||
taskService *task.Service // Direct dependency injection
|
||||
}
|
||||
|
||||
func (l *Logic) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
|
||||
// Use task service directly
|
||||
task, err := l.taskService.GetTask(ctx, req.TaskID)
|
||||
// ... rest of logic
|
||||
}
|
||||
```
|
||||
|
||||
**Rules**
|
||||
|
||||
- Only import Service (public API), never internal files like logic.go or storage.go
|
||||
- Declare dependency in the dependent feature's Service constructor
|
||||
- Wire dependencies in cmd/api/main.go
|
||||
|
||||
**Wiring Example**
|
||||
```go
|
||||
// cmd/api/main.go
|
||||
taskService := task.NewService(taskStorage)
|
||||
orderService := order.NewService(orderStorage, taskService) // Pass task service
|
||||
```
|
||||
|
||||
### Avoid Circular Dependencies
|
||||
|
||||
**Problem:** Module A imports B, B imports A
|
||||
|
||||
- Causes build failures, initialization issues
|
||||
- Indicates poor module boundaries
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Extract shared code to third module
|
||||
- Restructure dependencies (A→C, B→C)
|
||||
- Use dependency injection
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Project Structure @project-structure.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When executing external commands, shell scripts, or system processes
|
||||
---
|
||||
|
||||
## Command Execution Principles
|
||||
|
||||
### Security
|
||||
|
||||
**Never execute user input directly:**
|
||||
|
||||
- ❌ `exec(userInput)`
|
||||
- ❌ `shell("rm " + userFile)`
|
||||
- ✅ Use argument lists, not shell string concatenation
|
||||
- ✅ Validate and sanitize all arguments
|
||||
|
||||
**Run with minimum permissions:**
|
||||
|
||||
- Never run commands as root/admin without explicit human approval. If elevated permissions are absolutely required, STOP and request authorization.
|
||||
- Use least-privilege service accounts
|
||||
|
||||
### Portability
|
||||
|
||||
**Use language standard library:**
|
||||
|
||||
- Avoid shell commands when standard library provides functionality
|
||||
- Example: Use file I/O APIs instead of `cat`, `cp`, `mv`
|
||||
|
||||
**Test on all target OS:**
|
||||
|
||||
- Windows, Linux, macOS have different commands and behaviors
|
||||
- Use path joining functions (don't concatenate with /)
|
||||
|
||||
### Error Handling
|
||||
|
||||
**Check exit codes:**
|
||||
|
||||
- Non-zero exit code = failure
|
||||
- Capture and log stderr
|
||||
- Set timeouts for long-running commands
|
||||
- Handle "command not found" gracefully
|
||||
|
||||
### Command Execution Checklist
|
||||
|
||||
- [ ] Is user input sanitized/validated before use in commands?
|
||||
- [ ] Are arguments passed as lists (not shell string concatenation)?
|
||||
- [ ] Are commands running with minimum necessary permissions?
|
||||
- [ ] Are exit codes checked and errors handled?
|
||||
- [ ] Are timeouts set for long-running commands?
|
||||
- [ ] Is stderr captured and logged?
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Concurrency and Threading Mandate
|
||||
|
||||
### When to Use Concurrency
|
||||
|
||||
**I/O-Bound Operations:** Use asynchronous I/O, event-driven concurrency, or coroutines when waiting on network requests, file I/O, or database queries.
|
||||
|
||||
**CPU-Bound Operations:** Use OS threads or thread pools for heavy computation, data processing, or encoding work where CPU cycles dominate.
|
||||
|
||||
**Don't Over-Use Concurrency:** Concurrency adds significant complexity (race conditions, deadlocks, debugging difficulty). Profile first — only add concurrency when there is a measurable performance benefit.
|
||||
|
||||
For implementation details (race condition prevention, deadlock avoidance, message passing patterns), see Concurrency and Threading Principles @concurrency-and-threading-principles.md.
|
||||
|
||||
### When NOT to Use Concurrency
|
||||
- Simple synchronous operations
|
||||
- No measurable performance benefit
|
||||
- Avoid premature optimization
|
||||
|
||||
### Related Principles
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Performance Optimization Principles @performance-optimization-principles.md
|
||||
- Rust Idioms and Patterns @rust-idioms-and-patterns.md
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing concurrent, parallel, or multi-threaded code (async/await, threads, goroutines, actors)
|
||||
---
|
||||
|
||||
## Concurrency and Threading Principles
|
||||
|
||||
**1. Avoid Race Conditions**
|
||||
|
||||
**What is a race condition:**
|
||||
|
||||
- Multiple threads access shared data concurrently
|
||||
- At least one thread writes/modifies the data
|
||||
- No synchronization mechanism in place
|
||||
- Result depends on unpredictable thread execution timing
|
||||
|
||||
**Prevention strategies:**
|
||||
|
||||
- Synchronization: Locks, mutexes, semaphores
|
||||
- Immutability: Immutable data is thread-safe by default
|
||||
- Message passing: Send data between threads instead of sharing
|
||||
- Thread-local storage: Each thread has its own copy
|
||||
|
||||
**Detection:**
|
||||
|
||||
- Go: Run with `-race` flag (race detector)
|
||||
- Rust: Miri tool for undefined behavior detection
|
||||
- C/C++: ThreadSanitizer (TSan)
|
||||
- Java: JCStress, FindBugs
|
||||
|
||||
**2. Prevent Deadlocks**
|
||||
|
||||
**What is a deadlock:**
|
||||
|
||||
- Two or more threads waiting for each other indefinitely
|
||||
- Example: Thread A holds Lock 1, waits for Lock 2; Thread B holds Lock 2, waits for Lock 1
|
||||
|
||||
**Four conditions (ALL must be true for deadlock):**
|
||||
|
||||
1. Mutual exclusion: Resources held exclusively (locks)
|
||||
2. Hold and wait: Holding one resource while waiting for another
|
||||
3. No preemption: Can't force unlock
|
||||
4. Circular wait: A waits for B, B waits for A
|
||||
|
||||
**Prevention (break any one condition):**
|
||||
|
||||
- Lock ordering: Always acquire locks in same order
|
||||
- Timeout: Use try_lock with timeout, back off and retry
|
||||
- Avoid nested locks: Don't hold multiple locks simultaneously
|
||||
- Use lock-free data structures when possible
|
||||
|
||||
**3. Prefer Immutability**
|
||||
|
||||
- Immutable data = thread-safe by default (no synchronization needed)
|
||||
- Share immutable data freely between threads
|
||||
- Use immutable data structures where possible (Rust default, functional languages)
|
||||
- If data must change, use message passing instead of shared mutable state
|
||||
|
||||
**4. Message Passing Over Shared Memory**
|
||||
|
||||
- "Don't communicate by sharing memory; share memory by communicating" (Go proverb)
|
||||
- Send data through channels/queues instead of accessing shared memory
|
||||
- Reduces need for locks and synchronization
|
||||
- Easier to reason about and test
|
||||
|
||||
**5. Graceful Degradation**
|
||||
|
||||
- Handle concurrency errors gracefully (timeouts, retries, circuit breakers)
|
||||
- Don't crash entire application on one thread failure
|
||||
- Use supervisors/monitors for fault tolerance (Erlang/Elixir actor model)
|
||||
- Implement backpressure for producer-consumer scenarios
|
||||
|
||||
### Concurrency Models by Use Case
|
||||
|
||||
- **I/O-bound:** async/await, event loops, coroutines, green threads
|
||||
- **CPU-bound:** OS threads, thread pools, parallel processing
|
||||
- **Actor model:** Erlang/Elixir actors, Akka (message passing, isolated state)
|
||||
- **CSP (Communicating Sequential Processes):** Go channels, Rust channels
|
||||
|
||||
### Testing Concurrent Code
|
||||
|
||||
- Write unit tests with controlled concurrency (deterministic execution)
|
||||
- Test timeout scenarios and resource exhaustion
|
||||
- Test thread pool full, queue full scenarios
|
||||
|
||||
### Related Principles
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working with application configuration, environment variables, settings files, or secrets management
|
||||
---
|
||||
|
||||
## Configuration Management Principles
|
||||
|
||||
### Separation of Configuration and Code
|
||||
|
||||
**Configuration:**
|
||||
|
||||
- Environment-specific values (URLs, credentials, timeouts)
|
||||
- Changes between dev/staging/prod
|
||||
- Can change without code deployment
|
||||
|
||||
**Code:**
|
||||
|
||||
- Business logic and application behavior
|
||||
- Same across all environments
|
||||
- Requires deployment to change
|
||||
|
||||
**Never hardcode configuration in code:**
|
||||
|
||||
- ❌ `const DB_URL = "postgresql://prod-db:5432/myapp"`
|
||||
- ✅ `const DB_URL = process.env.DATABASE_URL`
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
**Validate at startup:**
|
||||
|
||||
- Check all required configuration is present
|
||||
- Fail fast if required config is missing or invalid
|
||||
- Provide clear error messages for misconfiguration
|
||||
- Example: "DATABASE_URL environment variable is required"
|
||||
|
||||
**Validation checks:**
|
||||
|
||||
- Type (string, number, boolean, enum)
|
||||
- Format (URL, email, file path)
|
||||
- Range (port numbers 1-65535)
|
||||
- Dependencies (if feature X enabled, config Y required)
|
||||
|
||||
### Configuration Hierarchy
|
||||
|
||||
**Precedence (highest to lowest):**
|
||||
|
||||
1. **Command-line arguments:** Override everything (for testing, debugging)
|
||||
2. **Environment variables:** Override config files
|
||||
3. **Config files:** Environment-specific (config.prod.yaml, config.dev.yaml)
|
||||
4. **Defaults:** Reasonable defaults in code (fallback)
|
||||
|
||||
**Example:**
|
||||
|
||||
Database port resolution:
|
||||
|
||||
1. Check CLI arg: --db-port=5433
|
||||
|
||||
2. Check env var: DB_PORT=5432
|
||||
|
||||
3. Check config file: database.port=5432
|
||||
|
||||
4. Use default: 5432
|
||||
|
||||
### Configuration Organization
|
||||
|
||||
Hybrid Approach (config files + .env files): define the structure of configuration in config files (e.g. config/database.yaml) and use .env files to inject the secret values.
|
||||
|
||||
**.env files:** Description: A file dedicated to a specific environment (development) for production these values comes from secrets/environment platfrom or manager not a physical `.env` file on disk. When to Use: Use this only for secrets (API keys, passwords) and a few environment-specific values (like a server IP). These files except `.env.template` should never be committed to version control (git).
|
||||
|
||||
- `.env.template` - Consist of credentials and secrets with blank value (SHOULD commit to git)
|
||||
- `.env.development` - Local development credentials and secrets (SHOULD NOT commit to git)
|
||||
|
||||
**Example `.env.development`:**
|
||||
```
|
||||
DEV_DB_HOST=123.45.67.89
|
||||
DEV_DB_USERNAME=prod_user
|
||||
DEV_DB_PASSWORD=a_very_secure_production_password
|
||||
```
|
||||
|
||||
**Feature files:** Description: Settings are grouped into files based on what they do (database, auth, etc.). This keeps your configuration organized. When to Use: Use this as your primary method for organizing non-secret settings. It’s the best way to keep your configuration clean and scalable as your application grows.
|
||||
|
||||
- `config/database.yaml` - Database settings
|
||||
- `config/redis.yaml` - Cache settings
|
||||
- `config/auth.yaml` - Authentication settings
|
||||
|
||||
**Example `config/database.yaml`:**
|
||||
```
|
||||
default: &default
|
||||
adapter: postgresql
|
||||
pool: 5
|
||||
development:
|
||||
<<: *default
|
||||
host: localhost
|
||||
database: myapp_dev
|
||||
username: <%= ENV['DEV_DB_USERNAME'] %> # Placeholder for a secret
|
||||
password: <%= ENV['DEV_DB_PASSWORD'] %>
|
||||
production:
|
||||
<<: *default
|
||||
host: <%= ENV['PROD_DB_HOST'] %>
|
||||
database: myapp_prod
|
||||
username: <%= ENV['PROD_DB_USERNAME'] %>
|
||||
password: <%= ENV['PROD_DB_PASSWORD'] %>
|
||||
```
|
||||
|
||||
> **Note:** Feature flag configuration is a distinct, PRD-gated concern — not routine application configuration. See Feature Flags Principles @feature-flags-principles.md for when and how feature flags are used.
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Feature Flags Principles @feature-flags-principles.md
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Core Design Principles
|
||||
|
||||
### SOLID Principles
|
||||
|
||||
**Single Responsibility Principle (SRP):**
|
||||
|
||||
- Each class, module, or function should have ONE and ONLY ONE reason to change
|
||||
- Generate focused, cohesive units of functionality
|
||||
- If explaining what something does requires "and", it likely violates SRP
|
||||
|
||||
**Open/Closed Principle (OCP):**
|
||||
|
||||
- Software entities should be open for extension but closed for modification
|
||||
- Design abstractions (interfaces, ports) that allow behavior changes without modifying existing code
|
||||
- Use composition and dependency injection to enable extensibility
|
||||
|
||||
**Liskov Substitution Principle (LSP):**
|
||||
|
||||
- Subtypes must be substitutable for their base types without altering program correctness
|
||||
- Inheritance hierarchies must maintain behavioral consistency
|
||||
- If substituting a subclass breaks functionality, LSP is violated
|
||||
|
||||
**Interface Segregation Principle (ISP):**
|
||||
|
||||
- Clients should not be forced to depend on interfaces they don't use
|
||||
- Create focused, role-specific interfaces rather than monolithic ones
|
||||
- Many small, cohesive interfaces > one large, general-purpose interface
|
||||
|
||||
**Dependency Inversion Principle (DIP):**
|
||||
|
||||
- Depend on abstractions (interfaces/ports), not concretions (implementations/adapters)
|
||||
- High-level modules should not depend on low-level modules; both should depend on abstractions
|
||||
- Core principle enabling Testability-First architecture
|
||||
|
||||
### Essential Design Practices
|
||||
|
||||
**DRY (Don't Repeat Yourself):**
|
||||
|
||||
- Eliminate code duplication through proper abstraction, shared utilities, composable functions
|
||||
- Each piece of knowledge should have single, authoritative representation
|
||||
- Don't duplicate logic, algorithms, or business rules
|
||||
|
||||
**YAGNI (You Aren't Gonna Need It):**
|
||||
|
||||
**CRITICAL:** Code maintainability always prevail
|
||||
|
||||
- Avoid implementing functionality before it's actually required
|
||||
- Don't add features based on speculation about future needs
|
||||
- Build for today's requirements, refactor when needs change
|
||||
|
||||
**KISS (Keep It Simple, Stupid):**
|
||||
|
||||
**CRITICAL:** Code maintainability always prevail
|
||||
|
||||
- Prefer simple(simple to maintain), straightforward solutions over complex, clever ones
|
||||
- Complexity should be justified by actual requirements, not theoretical flexibility
|
||||
- Simple code is easier to test, maintain, and debug
|
||||
|
||||
**Separation of Concerns:**
|
||||
|
||||
- Divide program functionality into distinct sections with minimal overlap
|
||||
- Each concern should be isolated in its own module or layer
|
||||
|
||||
**Composition Over Inheritance:**
|
||||
|
||||
- Favor object composition and delegation over class inheritance for code reuse
|
||||
- Composition is more flexible and easier to test
|
||||
- Use interfaces/traits for polymorphism instead of deep inheritance hierarchies
|
||||
|
||||
**Principle of Least Astonishment:**
|
||||
|
||||
- Code should behave in ways that users and maintainers naturally expect
|
||||
- Avoid surprising or counterintuitive behavior
|
||||
- Follow established conventions and patterns
|
||||
|
||||
**User Experience vs Maintainability:**
|
||||
|
||||
- Both user experience AND code maintainability matter
|
||||
- When they conflict, **prefer maintainable code** that can evolve
|
||||
- Poor UX from clean code can be fixed; poor code from UX pressure becomes tech debt
|
||||
- Maintainability enables future UX improvements
|
||||
- Never sacrifice code quality for short-term UX gains
|
||||
|
||||
### Related Principles
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
- Documentation Principles @documentation-principles.md
|
||||
- Accessibility Principles @accessibility-principles.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When serializing, deserializing, or validating data formats (JSON, XML, YAML, Protocol Buffers, MessagePack, etc.)
|
||||
---
|
||||
|
||||
## Data Serialization and Interchange Principles
|
||||
|
||||
### Validate at System Boundaries
|
||||
|
||||
**All data entering system must be validated:**
|
||||
|
||||
- API requests, file uploads, message queue messages
|
||||
- Validate type, format, range, required fields
|
||||
- Fail fast on invalid data (don't process partially valid data)
|
||||
- Return clear validation errors to client
|
||||
|
||||
### Handle Encoding Explicitly
|
||||
|
||||
**Default to UTF-8:**
|
||||
|
||||
- UTF-8 for all text data (API responses, file contents, database strings)
|
||||
- Specify encoding explicitly when reading/writing files
|
||||
- Handle encoding errors gracefully (replacement characters or error)
|
||||
|
||||
**Encoding errors:**
|
||||
|
||||
- Invalid UTF-8 sequences (malformed bytes)
|
||||
- Mixing encodings (reading UTF-8 as ISO-8859-1)
|
||||
- Always validate and normalize encoding
|
||||
|
||||
### Serialization Format Selection
|
||||
|
||||
**JSON:**
|
||||
|
||||
- Human-readable, widely supported, language-agnostic
|
||||
- Good for: APIs, configuration files, web applications
|
||||
- Limitations: No binary support, larger size than binary formats
|
||||
|
||||
**Protocol Buffers:**
|
||||
|
||||
- Compact binary format, fast serialization/deserialization
|
||||
- Schema evolution (backward/forward compatibility)
|
||||
- Good for: Internal microservices, high-throughput systems
|
||||
- Limitations: Not human-readable, requires schema definition
|
||||
|
||||
**MessagePack:**
|
||||
|
||||
- Binary JSON-like format, faster and more compact than JSON
|
||||
- Good for: Internal APIs, when JSON too slow but readability still desired
|
||||
- Limitations: Less widely supported than JSON
|
||||
|
||||
**XML:**
|
||||
|
||||
- Verbose, legacy systems, document-oriented
|
||||
- Good for: Enterprise systems, SOAP APIs, RSS/Atom feeds
|
||||
- Limitations: Verbosity, complexity, security issues (XXE attacks)
|
||||
|
||||
**YAML:**
|
||||
|
||||
- Human-friendly, good for configuration files
|
||||
- Good for: Config files, Infrastructure as Code (Kubernetes, CI/CD)
|
||||
- Limitations: Complex parsing, performance, security issues (arbitrary code execution)
|
||||
|
||||
### Security Considerations
|
||||
|
||||
**Validate before deserialization:**
|
||||
|
||||
- Prevent deserialization attacks (arbitrary code execution)
|
||||
- Set size limits on payloads (prevent memory exhaustion)
|
||||
- Whitelist allowed types/classes for deserialization
|
||||
|
||||
**Disable dangerous features:**
|
||||
|
||||
- XML: Disable external entity processing (XXE prevention)
|
||||
- YAML: Disable unsafe constructors
|
||||
- Python pickle: Never deserialize untrusted data
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When designing database schemas, writing migrations, creating queries, or working with transaction boundaries
|
||||
---
|
||||
|
||||
## Database Design Principles
|
||||
|
||||
### Schema Design
|
||||
|
||||
**Normalization:**
|
||||
- Start with 3NF (Third Normal Form) and denormalize only when measured performance requires it
|
||||
- Each table should represent a single entity
|
||||
- Avoid storing derived/calculated data unless caching is explicitly needed
|
||||
|
||||
**Naming Conventions:**
|
||||
- Tables: plural, snake_case (e.g., `users`, `task_assignments`)
|
||||
- Columns: singular, snake_case (e.g., `created_at`, `user_id`)
|
||||
- Foreign keys: `{referenced_table_singular}_id` (e.g., `user_id`, `task_id`)
|
||||
- Indexes: `idx_{table}_{columns}` (e.g., `idx_users_email`)
|
||||
- Constraints: `{type}_{table}_{columns}` (e.g., `uq_users_email`, `fk_tasks_user_id`)
|
||||
|
||||
**Required columns for all tables:**
|
||||
- `id` — primary key (UUID preferred, auto-increment acceptable)
|
||||
- `created_at` — timestamp, set on insert, never updated
|
||||
- `updated_at` — timestamp, updated on every modification
|
||||
|
||||
### Migrations
|
||||
|
||||
**Safety Rules:**
|
||||
- **Never drop columns in production** without a deprecation period
|
||||
- **Never rename columns directly** — add new, migrate data, drop old
|
||||
- **Always make migrations reversible** — include both up and down
|
||||
- **Test migrations on a copy of production data** before applying
|
||||
|
||||
**Migration Strategy:**
|
||||
1. Additive changes first (add column, add table)
|
||||
2. Backfill data
|
||||
3. Update application code
|
||||
4. Remove old columns/tables in a future migration
|
||||
|
||||
### Queries
|
||||
|
||||
**Performance:**
|
||||
- **Always use parameterized queries** (never string concatenation)
|
||||
- **Index columns** used in WHERE, JOIN, and ORDER BY clauses
|
||||
- **Avoid SELECT \*** — specify only needed columns
|
||||
- **Watch for N+1 queries** — use JOINs or batch loading
|
||||
- **Set query timeouts** to prevent long-running queries from blocking
|
||||
|
||||
**Transactions:**
|
||||
- Use transactions for operations that modify multiple rows/tables
|
||||
- Keep transactions as short as possible
|
||||
- Handle deadlocks with retry logic
|
||||
- Never hold transactions open during user interaction or external API calls
|
||||
|
||||
### Database Design Checklist
|
||||
|
||||
- [ ] Schema follows naming conventions?
|
||||
- [ ] All tables have `id`, `created_at`, `updated_at`?
|
||||
- [ ] Foreign keys have proper constraints and indexes?
|
||||
- [ ] Queries are parameterized (no SQL injection)?
|
||||
- [ ] Indexes exist for frequent query patterns?
|
||||
- [ ] Migrations are reversible?
|
||||
- [ ] Transactions are short and focused?
|
||||
- [ ] N+1 queries avoided?
|
||||
|
||||
### Related Principles
|
||||
- Security Principles @security-principles.md (SQL injection prevention)
|
||||
- Performance Optimization Principles @performance-optimization-principles.md (query performance)
|
||||
- Error Handling Principles @error-handling-principles.md (transaction error handling)
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When managing project dependencies, configuring package files, or organizing module imports
|
||||
---
|
||||
|
||||
## Dependency Management Principles
|
||||
|
||||
### Version Pinning
|
||||
|
||||
**Production:** Pin exact versions (1.2.3, not ^1.2.0)
|
||||
|
||||
- Prevents supply chain attacks
|
||||
- Prevents unexpected breakage from patch updates
|
||||
- Ensures reproducible builds
|
||||
|
||||
**Use lock files:**
|
||||
|
||||
- `package-lock.json` (Node.js / npm)
|
||||
- `pnpm-lock.yaml` (Node.js / pnpm)
|
||||
- `yarn.lock` (Node.js / yarn)
|
||||
- `Cargo.lock` (Rust)
|
||||
- `go.sum` (Go)
|
||||
- `pubspec.lock` (Flutter / Dart)
|
||||
- `requirements.txt` / `poetry.lock` (Python)
|
||||
|
||||
### Minimize Dependencies
|
||||
|
||||
**Every dependency is a liability:**
|
||||
|
||||
- Potential security vulnerability
|
||||
- Increased build time and artifact size
|
||||
- Maintenance burden (updates, compatibility)
|
||||
|
||||
**Ask before adding dependency:**
|
||||
|
||||
- "Can I implement this in 50 lines?"
|
||||
- "Is this functionality critical?"
|
||||
- "Is this dependency actively maintained?"
|
||||
- "Is this the latest stable version?"
|
||||
|
||||
### Organize Imports
|
||||
|
||||
**Grouping:**
|
||||
|
||||
1. Standard library
|
||||
2. External dependencies
|
||||
3. Internal modules
|
||||
|
||||
**Sorting:** Alphabetical within groups
|
||||
|
||||
**Cleanup:** Remove unused imports (use linter/formatter)
|
||||
|
||||
### Dependency Management Checklist
|
||||
|
||||
- [ ] Are production dependencies pinned to exact versions?
|
||||
- [ ] Is the lock file committed to version control?
|
||||
- [ ] Can each dependency be justified (not implementable in <50 lines)?
|
||||
- [ ] Are all dependencies actively maintained and on latest stable versions?
|
||||
- [ ] Are imports organized by group (stdlib → external → internal)?
|
||||
- [ ] Are unused imports removed?
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Documentation Principles
|
||||
|
||||
### Self-Documenting Code
|
||||
|
||||
**Clear naming reduces need for comments:**
|
||||
|
||||
- Code shows WHAT is happening
|
||||
- Comments explain WHY it's done this way
|
||||
|
||||
**When to comment:**
|
||||
|
||||
- Complex business logic deserves explanation
|
||||
- Non-obvious algorithms (explain approach)
|
||||
- Workarounds for bugs (link to issue tracker)
|
||||
- Performance optimizations (explain trade-offs)
|
||||
|
||||
### Documentation Levels
|
||||
|
||||
1. **Inline comments:** Explain WHY for complex code
|
||||
2. **Function/method docs:** API contract (parameters, returns, errors)
|
||||
3. **Module/package docs:** High-level purpose and usage
|
||||
4. **README:** Setup, usage, examples
|
||||
5. **Architecture docs:** System design, component interactions, use valid mermaid diagram
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing error handling, working with error types, or designing error recovery strategies (validation errors, business rule violations, infrastructure failures, resource cleanup)
|
||||
---
|
||||
|
||||
## Error Handling Principles
|
||||
|
||||
**1. Never Fail Silently:**
|
||||
|
||||
- All errors must be handled explicitly (no empty catch blocks)
|
||||
- If you catch an error, do something with it (log, return, transform, retry)
|
||||
|
||||
**2. Fail Fast:**
|
||||
|
||||
- Detect and report errors as early as possible
|
||||
- Validate at system boundaries before processing
|
||||
- Don't process invalid data hoping it'll work out
|
||||
|
||||
**3. Provide Context:**
|
||||
|
||||
- Include error codes, correlation IDs, actionable messages
|
||||
- Enough information for debugging without exposing sensitive details
|
||||
- Example: "Database query failed (correlation-id: abc-123)" not "SELECT * FROM users WHERE..."
|
||||
|
||||
**4. Separate Concerns:**
|
||||
|
||||
- Different handlers for different error types
|
||||
- Business errors ≠ technical errors ≠ security errors
|
||||
|
||||
**5. Resource Cleanup:**
|
||||
|
||||
- Always clean up in error scenarios (close files, release connections, unlock resources)
|
||||
- Use language-appropriate patterns (defer, finally, RAII, context managers)
|
||||
|
||||
**6. No Information Leakage:**
|
||||
|
||||
- Sanitize error messages for external consumption
|
||||
- Don't expose stack traces, SQL queries, file paths, internal structure to users
|
||||
- Log full details internally, show generic message externally
|
||||
|
||||
### Application Error Object (Internal/Log Format)
|
||||
```
|
||||
{
|
||||
"status": "error",
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "User-friendly error message",
|
||||
"correlationId": "uuid-for-tracking",
|
||||
"details": {
|
||||
"field": "email",
|
||||
"issue": "Invalid email format",
|
||||
"provided": "invalid-email",
|
||||
"expected": "valid email format ([email protected])"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling Checklist
|
||||
|
||||
- [ ] Are all error paths explicitly handled (no empty catch blocks)?
|
||||
- [ ] Do errors include correlation IDs for debugging?
|
||||
- [ ] Are sensitive details sanitized before returning to client?
|
||||
- [ ] Are resources cleaned up in all error scenarios?
|
||||
- [ ] Are errors logged at appropriate levels (warn for 4xx, error for 5xx)?
|
||||
- [ ] Are error tests written (negative test cases)?
|
||||
- [ ] Is error handling consistent across application?
|
||||
|
||||
### Related Principles
|
||||
- API Design Principles @api-design-principles.md - API Error Response Format section
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: >
|
||||
ONLY when feature flags are explicitly required by the PRD, technical
|
||||
architecture document, or tech stack specification. Do NOT load this file
|
||||
or introduce feature flags unless the requirement document explicitly calls
|
||||
for gradual rollout, A/B testing, kill switches, or feature entitlement gating.
|
||||
---
|
||||
|
||||
## Feature Flags Principles
|
||||
|
||||
> [!CAUTION]
|
||||
> **Do NOT implement feature flags unless explicitly required.**
|
||||
>
|
||||
> Feature flags add real operational complexity: flag evaluation infrastructure,
|
||||
> lifecycle management, and a multiplicative increase in test permutations.
|
||||
> The cost is non-trivial for solo developers or simple projects.
|
||||
>
|
||||
> Only introduce feature flags when the **PRD** or **technical architecture document**
|
||||
> explicitly specifies one of:
|
||||
> - Gradual / percentage rollout of a risky feature
|
||||
> - A/B testing or experimentation
|
||||
> - Emergency kill switches for production code paths
|
||||
> - Feature entitlement gating by user tier or permission
|
||||
>
|
||||
> If none of these are specified, deploy code directly. Do not introduce flags speculatively.
|
||||
|
||||
---
|
||||
|
||||
### When to Use Feature Flags
|
||||
|
||||
Feature flags decouple **deployment** from **release**. Code ships to production but stays
|
||||
dormant until the flag enables it. This is valuable for:
|
||||
|
||||
| Use case | Flag type | Example |
|
||||
|----------|-----------|---------|
|
||||
| Gradual rollout | Release flag | `new-checkout-flow: 0→5→25→100%` |
|
||||
| Emergency kill switch | Ops flag | `use-legacy-payment-provider` |
|
||||
| A/B test / experiment | Experiment flag | `button-color-blue-vs-green` |
|
||||
| Tier / permission gating | Permission flag | `pro-tier-analytics` |
|
||||
|
||||
---
|
||||
|
||||
### Infrastructure Requirements
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The flag evaluation backend **must be specified in the technical architecture document**
|
||||
> before implementation begins. Do NOT choose a provider independently — ask the user
|
||||
> which infrastructure to use.
|
||||
|
||||
| Approach | When to use | Notes |
|
||||
|----------|-------------|-------|
|
||||
| **Managed SaaS** | Teams > 5, multi-environment, complex targeting | LaunchDarkly, Flagsmith, Unleash Cloud |
|
||||
| **Self-hosted** | Full control, no SaaS dependency | Unleash (OSS), Flagsmith (OSS) |
|
||||
| **Firebase Remote Config** | Mobile apps in the Firebase ecosystem | Firebase SDK required |
|
||||
| **Static config (YAML / env)** | Solo dev, simple on/off, single environment | Loaded at startup; no runtime targeting |
|
||||
|
||||
Flag configuration is infrastructure — it must be provisioned before code references it.
|
||||
|
||||
---
|
||||
|
||||
### Flag Evaluation Rules
|
||||
|
||||
- **Evaluate server-side** for security-sensitive features (never trust the client)
|
||||
- **Evaluate at request boundary** — never inside pure business logic functions
|
||||
- **Default to disabled** — if the flag service is unreachable, the flag is off (fail closed)
|
||||
- **Wrap flag checks once** — in a service method or middleware, not scattered across business logic
|
||||
|
||||
**Pattern:**
|
||||
```
|
||||
// ✅ Flag evaluation at the boundary (handler or service entry point)
|
||||
func (s *Service) CreateOrder(ctx context.Context, req Request) (Response, error) {
|
||||
useNewPricing := s.flags.IsEnabled(ctx, "new-pricing-engine", req.UserID)
|
||||
if useNewPricing {
|
||||
return s.handleWithNewPricing(ctx, req)
|
||||
}
|
||||
return s.handleWithLegacyPricing(ctx, req)
|
||||
}
|
||||
|
||||
// ❌ Flag evaluation buried inside business logic
|
||||
func calculateDiscount(items []Item) float64 {
|
||||
if flags.IsEnabled("new-discount-rules") { ... } // NO
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Flag Lifecycle Rules
|
||||
|
||||
Flags are temporary by design. Flag debt accumulates fast and becomes a maintenance burden.
|
||||
|
||||
- Every flag **must have an owner** (the team or developer responsible for its lifecycle)
|
||||
- Every **release flag** has a **maximum lifespan of 90 days**
|
||||
- When a release flag reaches 100% rollout, **create a ticket immediately** to remove it
|
||||
- **Ops (kill switch) flags** can be permanent but must be documented as such
|
||||
- **Experiment flags** must be removed when the experiment concludes
|
||||
- Review all flags in sprint planning — any flag past its expiry date is tech debt
|
||||
|
||||
---
|
||||
|
||||
### Testing with Feature Flags
|
||||
|
||||
Feature flags multiply test permutations. Keep tests tractable:
|
||||
|
||||
- Unit tests test **each branch independently** (flag on + flag off)
|
||||
- Integration tests default the flag to its **production-expected state** (usually fully on or off)
|
||||
- Do not write tests that enumerate every possible flag combination
|
||||
- Use a test-only flag override mechanism (e.g., `flags.Override(ctx, "flag", true)`) — never
|
||||
branch test logic on environment variables
|
||||
|
||||
---
|
||||
|
||||
### CI/CD Checklist (Feature Flags)
|
||||
|
||||
- [ ] Flag infrastructure specified in technical architecture document?
|
||||
- [ ] Flag backend provisioned and accessible by the application?
|
||||
- [ ] Every flag has an owner and expiry date set?
|
||||
- [ ] Flag evaluation is server-side for security-sensitive paths?
|
||||
- [ ] Default state is `disabled` (fail closed)?
|
||||
- [ ] Unit tests cover both flag-on and flag-off code paths?
|
||||
- [ ] Removal ticket created once flag reaches 100% rollout?
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
|
||||
- CI/CD Principles @ci-cd-principles.md (deployment vs release concept)
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Core Design Principles @core-design-principles.md (YAGNI)
|
||||
- Security Mandate @security-mandate.md (server-side evaluation)
|
||||
@@ -0,0 +1,871 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Flutter or Dart code, working on mobile UI, using Riverpod for state management, or building Flutter widgets
|
||||
---
|
||||
|
||||
## Flutter Idioms and Patterns (Riverpod 3)
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Flutter is a UI toolkit first — performance is a first-class concern. `const` widgets and immutable data keep the render tree efficient. **Riverpod 3** is the canonical state management solution: compile-safe, testable without `BuildContext`, no implicit global state, and with automatic retry and pause/resume built in.
|
||||
|
||||
**Code generation is mandatory.** All providers must use `@riverpod` / `@Riverpod(keepAlive: true)` annotations with `riverpod_generator` and `build_runner`. This catches type errors, missing overrides, and broken provider graphs at compile time — bugs are caught before they reach users.
|
||||
|
||||
**Required dependencies:**
|
||||
|
||||
```yaml
|
||||
# pubspec.yaml
|
||||
dependencies:
|
||||
flutter_riverpod: 3.2.1
|
||||
riverpod_annotation: 4.0.2
|
||||
|
||||
dev_dependencies:
|
||||
riverpod_generator: 4.0.3
|
||||
build_runner: # latest
|
||||
riverpod_lint: # latest
|
||||
```
|
||||
|
||||
> **Scope:** This file covers Flutter/Dart *coding idioms*. For file and folder layout, see `project-structure-flutter-mobile.md`. For test naming, see `testing-strategy.md`. For general error handling principles, see `error-handling-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### `const` Constructors — Everywhere
|
||||
|
||||
Make every widget `const` when possible. `const` widgets are created once and never rebuilt unless their inputs change — this is Flutter's most impactful performance optimization.
|
||||
|
||||
```dart
|
||||
// ✅ const constructor — widget is rebuild-safe
|
||||
class TaskCard extends StatelessWidget {
|
||||
const TaskCard({super.key, required this.task});
|
||||
final Task task;
|
||||
// ...
|
||||
}
|
||||
|
||||
// Usage — compile-time constant
|
||||
const TaskCard(task: myTask)
|
||||
|
||||
// ❌ Missing const — rebuilt on every parent rebuild
|
||||
TaskCard(task: myTask)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every `StatelessWidget` that has no mutable state must have a `const` constructor
|
||||
- Pass `const` keyword at the call site, not just the definition
|
||||
- Lint rule `prefer_const_constructors` must be enabled in `analysis_options.yaml`
|
||||
|
||||
---
|
||||
|
||||
### Widget Decomposition
|
||||
|
||||
Large `build` methods are the primary source of performance problems and unmaintainable UI code.
|
||||
|
||||
1. **Extract a new widget when a subtree has distinct responsibilities**
|
||||
```dart
|
||||
// ❌ Everything in one build method
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
// 30 lines of header...
|
||||
// 50 lines of list...
|
||||
// 20 lines of footer...
|
||||
]);
|
||||
}
|
||||
|
||||
// ✅ Each subtree is a named widget with a const constructor
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
const TaskHeader(),
|
||||
const TaskList(),
|
||||
const TaskFooter(),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Never use builder methods (`_buildHeader()`) as a substitute for extracting widgets**
|
||||
- Builder methods do not benefit from `const` and always rerun on parent rebuild
|
||||
- Extract a proper `StatelessWidget` or `ConsumerWidget` instead
|
||||
|
||||
3. **Keep `build` methods under ~30 lines** — if longer, decompose
|
||||
|
||||
---
|
||||
|
||||
### Immutable Data with `freezed`
|
||||
|
||||
All domain models must be immutable. Use the `freezed` package for:
|
||||
- Immutable value objects with `copyWith`
|
||||
- Union/sealed types (loading, success, error states)
|
||||
- Generated `==`, `hashCode`, and `toString`
|
||||
|
||||
```dart
|
||||
// task/models/task.dart
|
||||
@freezed
|
||||
class Task with _$Task {
|
||||
const factory Task({
|
||||
required String id,
|
||||
required String title,
|
||||
@Default(TaskStatus.pending) TaskStatus status,
|
||||
DateTime? dueDate,
|
||||
}) = _Task;
|
||||
|
||||
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
|
||||
}
|
||||
|
||||
// Usage — immutable update via copyWith
|
||||
final updated = task.copyWith(status: TaskStatus.done);
|
||||
|
||||
// ❌ Never mutate a model directly
|
||||
task.status = TaskStatus.done; // compile error — field is final
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- All domain models use `@freezed`
|
||||
- Never expose mutable fields on domain models
|
||||
- Run `dart run build_runner build` after changing freezed models
|
||||
|
||||
---
|
||||
|
||||
### Provider Decision Tree
|
||||
|
||||
Use this tree to pick the correct provider pattern. **Follow it top-to-bottom — the first match wins.**
|
||||
|
||||
```
|
||||
Does the provider have side-effects (create/update/delete)? ──> YES ──┐
|
||||
│ │
|
||||
NO │
|
||||
│ Is it async?
|
||||
│ │ │
|
||||
Is it async? YES NO
|
||||
│ │ │ │
|
||||
YES NO ▼ ▼
|
||||
│ │ @riverpod @riverpod
|
||||
▼ ▼ class class
|
||||
@riverpod @riverpod (AsyncNotifier) (Notifier)
|
||||
function function
|
||||
(FutureProvider) (Provider)
|
||||
```
|
||||
|
||||
**Decision summary:**
|
||||
|
||||
| Has side-effects? | Async? | Pattern | Provider type generated |
|
||||
|---|---|---|---|
|
||||
| No | No | `@riverpod` function | `Provider` |
|
||||
| No | Yes | `@riverpod` async function | `FutureProvider` |
|
||||
| No | Stream | `@riverpod` `Stream` function | `StreamProvider` |
|
||||
| Yes | No | `@riverpod` class | `NotifierProvider` |
|
||||
| Yes | Yes | `@riverpod` class with `Future` build | `AsyncNotifierProvider` |
|
||||
| Yes | Stream | `@riverpod` class with `Stream` build | `StreamNotifierProvider` |
|
||||
|
||||
---
|
||||
|
||||
### Riverpod — State Management
|
||||
|
||||
**Riverpod 3 is the only state management solution** used in this project. Do not introduce BLoC, Cubit, Provider (package:provider), or GetX.
|
||||
|
||||
> For file layout of state/ directories, see `project-structure-flutter-mobile.md`.
|
||||
|
||||
#### App Entry Point — `ProviderScope`
|
||||
|
||||
Every Flutter app using Riverpod must wrap the root widget in `ProviderScope`. This creates the `ProviderContainer` that powers all providers in the widget tree.
|
||||
|
||||
```dart
|
||||
// main.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Exactly one `ProviderScope` at the app root — never nest `ProviderScope` widgets
|
||||
- Use `overrides:` parameter only in tests (see Testing section)
|
||||
- For retry configuration, pass `retry:` to the root `ProviderScope` (see Runtime Behaviors)
|
||||
|
||||
#### Provider Definition — Class-Based (Side-Effects)
|
||||
|
||||
Use class-based providers when the provider needs methods to modify state.
|
||||
|
||||
```dart
|
||||
// Synchronous notifier — e.g., a filter or toggle
|
||||
@riverpod
|
||||
class TaskFilter extends _$TaskFilter {
|
||||
@override
|
||||
TaskFilterState build() {
|
||||
return const TaskFilterState();
|
||||
}
|
||||
|
||||
void setStatus(TaskStatus? status) {
|
||||
state = state.copyWith(status: status);
|
||||
}
|
||||
|
||||
void toggleShowCompleted() {
|
||||
state = state.copyWith(showCompleted: !state.showCompleted);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// Async notifier — e.g., CRUD operations
|
||||
@riverpod
|
||||
class TaskList extends _$TaskList {
|
||||
@override
|
||||
Future<List<Task>> build() async {
|
||||
return ref.watch(taskRepositoryProvider).getTasks();
|
||||
}
|
||||
|
||||
Future<void> addTask(CreateTaskRequest request) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.createTask(request);
|
||||
// REQUIRED: check ref.mounted after every await
|
||||
if (!ref.mounted) return state.requireValue;
|
||||
return repo.getTasks();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteTask(String id) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.deleteTask(id);
|
||||
if (!ref.mounted) return state.requireValue;
|
||||
return repo.getTasks();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Provider Definition — Functional (Read-Only / Computed)
|
||||
|
||||
Use functional providers for derived values with no side-effects.
|
||||
|
||||
```dart
|
||||
// Computed value — filtered task list
|
||||
@riverpod
|
||||
List<Task> filteredTasks(Ref ref) {
|
||||
final tasks = ref.watch(taskListProvider).valueOrNull ?? [];
|
||||
final filter = ref.watch(taskFilterProvider);
|
||||
return tasks.where((t) => filter.matches(t)).toList();
|
||||
}
|
||||
|
||||
// Async one-shot read — e.g., fetch a single task
|
||||
@riverpod
|
||||
Future<Task> taskDetail(Ref ref, String id) async {
|
||||
return ref.watch(taskRepositoryProvider).getById(id);
|
||||
}
|
||||
|
||||
// Stream — real-time data
|
||||
@riverpod
|
||||
Stream<List<Task>> taskStream(Ref ref) {
|
||||
return ref.watch(taskRepositoryProvider).watchAll();
|
||||
}
|
||||
```
|
||||
|
||||
**Family providers (parameterized):** When a provider takes extra arguments beyond `Ref`, Riverpod creates a *family* — each unique argument combination gets its own independent provider instance with its own cache and disposal. With code generation (which we mandate), **any number of parameters** are supported, including named, optional, and default values. Constraints:
|
||||
- All parameters must implement `==` and `hashCode` (primitives, `freezed` models, Dart records all work)
|
||||
- Each family member is disposed independently when no listener remains
|
||||
|
||||
```dart
|
||||
// ✅ Family with a single key
|
||||
@riverpod
|
||||
Future<Task> taskDetail(Ref ref, String id) async {
|
||||
return ref.watch(taskRepositoryProvider).getById(id);
|
||||
}
|
||||
|
||||
// ✅ Multiple parameters — fully supported with code generation
|
||||
@riverpod
|
||||
Future<List<Task>> projectTasks(Ref ref, String projectId, {TaskStatus? status}) async {
|
||||
return ref.watch(taskRepositoryProvider).getByProject(projectId, status: status);
|
||||
}
|
||||
// Usage: ref.watch(projectTasksProvider('proj-1', status: TaskStatus.done))
|
||||
```
|
||||
|
||||
#### `ref.watch` vs `ref.read`
|
||||
|
||||
```dart
|
||||
// ✅ ref.watch — subscribes to changes, use inside build() or widget build
|
||||
final tasks = ref.watch(taskListProvider);
|
||||
|
||||
// ✅ ref.read — one-time read, use inside event handlers / notifier actions
|
||||
Future<void> onSubmit() async {
|
||||
await ref.read(taskListProvider.notifier).addTask(request);
|
||||
}
|
||||
|
||||
// ❌ Never use ref.watch inside async functions or event handlers
|
||||
Future<void> onSubmit() async {
|
||||
final tasks = ref.watch(taskListProvider); // WRONG — causes errors
|
||||
}
|
||||
```
|
||||
|
||||
#### `Ref.mounted` — Mandatory After Awaits
|
||||
|
||||
Riverpod 3 throws if you interact with a disposed `Ref` or `Notifier`. **Always check `ref.mounted` after any `await` in a notifier.**
|
||||
|
||||
```dart
|
||||
Future<void> updateTask(Task task) async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.update(task);
|
||||
|
||||
// REQUIRED: provider may have been disposed during await
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = await AsyncValue.guard(() => repo.getTasks());
|
||||
}
|
||||
```
|
||||
|
||||
#### Auto-Dispose and `keepAlive`
|
||||
|
||||
```dart
|
||||
// ✅ autoDispose is the DEFAULT with code generation (@riverpod)
|
||||
// Provider is disposed when no consumers are listening
|
||||
@riverpod
|
||||
Future<Task> taskDetail(Ref ref, String id) async {
|
||||
return ref.watch(taskRepositoryProvider).getById(id);
|
||||
}
|
||||
|
||||
// ✅ Opt into keepAlive explicitly for app-wide, long-lived state
|
||||
@Riverpod(keepAlive: true)
|
||||
class AuthState extends _$AuthState {
|
||||
@override
|
||||
Future<User?> build() async {
|
||||
return ref.watch(authRepositoryProvider).getCurrentUser();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Repositories should also be keepAlive — they hold connection state and
|
||||
// should not be re-initialized every time a screen rebuilds
|
||||
@Riverpod(keepAlive: true)
|
||||
TaskRepository taskRepository(Ref ref) {
|
||||
return TaskRepositoryImpl(apiClient: ref.watch(apiClientProvider));
|
||||
}
|
||||
|
||||
// ❌ Do not set keepAlive: false — that is the default
|
||||
// ❌ Do not set keepAlive: true for screen-scoped state
|
||||
```
|
||||
|
||||
#### Repository Interface Pattern
|
||||
|
||||
All data access goes through an abstract repository interface. This is the Flutter expression of the Testability-First architecture (see `architectural-pattern.md`).
|
||||
|
||||
```dart
|
||||
// repository/task_repository.dart — Abstract interface (contract)
|
||||
abstract class TaskRepository {
|
||||
Future<List<Task>> getTasks();
|
||||
Future<Task> getById(String id);
|
||||
Future<void> createTask(CreateTaskRequest request);
|
||||
Future<void> deleteTask(String id);
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// repository/task_repository_impl.dart — Production adapter
|
||||
class TaskRepositoryImpl implements TaskRepository {
|
||||
const TaskRepositoryImpl({required this.apiClient});
|
||||
final ApiClient apiClient;
|
||||
|
||||
@override
|
||||
Future<List<Task>> getTasks() async {
|
||||
final response = await apiClient.get('/tasks');
|
||||
return (response.data as List)
|
||||
.map((e) => Task.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Task> getById(String id) async {
|
||||
final response = await apiClient.get('/tasks/$id');
|
||||
return Task.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createTask(CreateTaskRequest request) async {
|
||||
await apiClient.post('/tasks', data: request.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTask(String id) async {
|
||||
await apiClient.delete('/tasks/$id');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// repository/task_repository_mock.dart — Test adapter
|
||||
class MockTaskRepository implements TaskRepository {
|
||||
final List<Task> _tasks = [];
|
||||
|
||||
@override
|
||||
Future<List<Task>> getTasks() async => List.unmodifiable(_tasks);
|
||||
|
||||
@override
|
||||
Future<Task> getById(String id) async =>
|
||||
_tasks.firstWhere((t) => t.id == id);
|
||||
|
||||
@override
|
||||
Future<void> createTask(CreateTaskRequest request) async {
|
||||
_tasks.add(Task(id: 'mock-id', title: request.title));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTask(String id) async {
|
||||
_tasks.removeWhere((t) => t.id == id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// Wiring — provider that the rest of the app depends on
|
||||
@Riverpod(keepAlive: true)
|
||||
TaskRepository taskRepository(Ref ref) {
|
||||
return TaskRepositoryImpl(apiClient: ref.watch(apiClientProvider));
|
||||
}
|
||||
|
||||
// In tests, override with mock:
|
||||
// taskRepositoryProvider.overrideWith((_) => MockTaskRepository())
|
||||
```
|
||||
|
||||
#### ConsumerWidget vs ConsumerStatefulWidget
|
||||
|
||||
```dart
|
||||
// ✅ Prefer ConsumerWidget — stateless, simpler
|
||||
class TaskListView extends ConsumerWidget {
|
||||
const TaskListView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asyncTasks = ref.watch(taskListProvider);
|
||||
return asyncTasks.when(
|
||||
data: (tasks) => TaskListBody(tasks: tasks),
|
||||
loading: () => const LoadingIndicator(),
|
||||
error: (e, _) => ErrorView(error: e),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Use ConsumerStatefulWidget only when local widget state + riverpod is needed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Riverpod 3 — Runtime Behaviors
|
||||
|
||||
#### Automatic Retry
|
||||
|
||||
Riverpod 3 automatically retries providers that throw, using exponential backoff. This improves resilience against transient network failures.
|
||||
|
||||
```dart
|
||||
// ✅ Default behavior — providers auto-retry on failure
|
||||
// No action needed for standard use
|
||||
|
||||
// ✅ Disable retry for a specific provider when failure is non-transient
|
||||
@Riverpod(retry: null)
|
||||
Future<Config> appConfig(Ref ref) async {
|
||||
return ref.watch(configRepositoryProvider).load();
|
||||
}
|
||||
|
||||
// ✅ Disable globally via ProviderScope
|
||||
ProviderScope(
|
||||
retry: (_, __) => null, // disable for all providers
|
||||
child: const MyApp(),
|
||||
)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Leave auto-retry enabled for network/IO operations (transient failures)
|
||||
- Disable for operations where retry is unsafe (non-idempotent writes) or pointless (validation errors)
|
||||
|
||||
#### Pause / Resume (Out-of-View Providers)
|
||||
|
||||
Riverpod 3 automatically **pauses** provider listeners when the consuming widget is no longer visible. When the widget becomes visible again, listeners resume.
|
||||
|
||||
This is automatic — no action needed. Be aware of it when debugging.
|
||||
|
||||
#### `ProviderException` Wrapping
|
||||
|
||||
When a provider fails, reading it in Riverpod 3 throws a `ProviderException` wrapping the original error, not the raw exception.
|
||||
|
||||
```dart
|
||||
// ✅ Catch ProviderException when reading providers that may fail
|
||||
try {
|
||||
final value = container.read(myProvider);
|
||||
} on ProviderException catch (e) {
|
||||
// e.exception contains the original error
|
||||
// e.provider contains which provider failed
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// ✅ Assert on provider failure in tests
|
||||
expect(
|
||||
() => container.read(myProvider),
|
||||
throwsA(isA<ProviderException>()),
|
||||
);
|
||||
```
|
||||
|
||||
#### State Change Detection (`==`)
|
||||
|
||||
Riverpod 3 uses the `==` operator (not `identical`) to determine if state changed and rebuilds are needed. This means:
|
||||
|
||||
- `freezed` models work correctly out of the box (generated `==`)
|
||||
- Custom models **must** implement `==` / `hashCode` or use `freezed`
|
||||
- Override `updateShouldNotify` on a `Notifier` for custom comparison logic
|
||||
|
||||
---
|
||||
|
||||
### Async Patterns
|
||||
|
||||
1. **Always handle all three `AsyncValue` states: data, loading, error**
|
||||
```dart
|
||||
// ✅ Exhaustive
|
||||
asyncValue.when(
|
||||
data: (data) => DataWidget(data: data),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, stack) => ErrorText(err.toString()),
|
||||
);
|
||||
```
|
||||
|
||||
2. **Surfacing notifier errors to UI** — use `when(error:...)` for exhaustive handling; use `hasError` only for conditional checks alongside a separate data display:
|
||||
```dart
|
||||
// ✅ Exhaustive — covers all states, preferred for full-screen states
|
||||
asyncTasks.when(
|
||||
data: (tasks) => TaskListBody(tasks: tasks),
|
||||
loading: () => const LoadingIndicator(),
|
||||
error: (e, _) => ErrorView(error: e),
|
||||
);
|
||||
|
||||
// ✅ Conditional — show inline error banner while keeping stale data visible
|
||||
if (asyncTasks.hasError) {
|
||||
// show snackbar or inline error
|
||||
}
|
||||
final tasks = asyncTasks.valueOrNull ?? const [];
|
||||
```
|
||||
|
||||
3. **Use safe `AsyncValue` accessors to prevent runtime crashes**
|
||||
```dart
|
||||
// ✅ Safe — returns null if state is loading or error
|
||||
final tasks = ref.watch(taskListProvider).valueOrNull;
|
||||
|
||||
// ⚠️ Unsafe — throws StateError if state is not AsyncData
|
||||
// Only use when you have already confirmed the state is loaded
|
||||
final tasks = ref.watch(taskListProvider).requireValue;
|
||||
```
|
||||
|
||||
4. **Use `AsyncValue.guard` inside notifier actions to wrap async calls**
|
||||
- It catches exceptions and wraps them in `AsyncError` automatically
|
||||
|
||||
5. **Use `StreamProvider` for real-time data** — never poll manually with `Timer`
|
||||
|
||||
6. **Always check `ref.mounted` after `await`** — see `Ref.mounted` section above
|
||||
|
||||
7. **Force a provider to re-fetch with `ref.invalidate`**
|
||||
```dart
|
||||
// ✅ Invalidate from outside a notifier (e.g., after a form submit in a widget)
|
||||
ref.invalidate(taskListProvider);
|
||||
// The next watch/read will trigger a fresh build()
|
||||
|
||||
// ✅ Invalidate from inside a notifier after a mutation
|
||||
Future<void> addTask(CreateTaskRequest request) async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.createTask(request);
|
||||
if (!ref.mounted) return;
|
||||
ref.invalidateSelf(); // triggers build() to re-run and return fresh list
|
||||
}
|
||||
```
|
||||
|
||||
**Decision: `invalidateSelf()` vs manual state assignment**
|
||||
```
|
||||
After a mutation:
|
||||
├── Need optimistic UI (instant visual update before server confirms)?
|
||||
│ └── YES → Set state manually via AsyncValue.guard
|
||||
│ (e.g., remove item from list immediately, then call API)
|
||||
└── NO → ref.invalidateSelf() (simpler, always correct — re-runs build())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Handling in Flutter/Riverpod
|
||||
|
||||
Define a typed exception hierarchy using sealed classes (Dart 3+). Map infrastructure exceptions to domain exceptions inside notifier actions.
|
||||
|
||||
```dart
|
||||
// core/errors/app_exception.dart — Typed exception hierarchy
|
||||
sealed class AppException implements Exception {
|
||||
const AppException(this.message);
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class NetworkException extends AppException {
|
||||
const NetworkException(super.message, {this.statusCode});
|
||||
final int? statusCode;
|
||||
}
|
||||
|
||||
class ValidationException extends AppException {
|
||||
const ValidationException(super.message, {required this.field});
|
||||
final String field;
|
||||
}
|
||||
|
||||
class NotFoundException extends AppException {
|
||||
const NotFoundException(super.message);
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// ✅ Map infrastructure exceptions to domain exceptions inside notifier actions
|
||||
Future<void> addTask(CreateTaskRequest request) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
try {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.createTask(request);
|
||||
if (!ref.mounted) return state.requireValue;
|
||||
return repo.getTasks();
|
||||
} on DioException catch (e) {
|
||||
// Transform infrastructure error → domain error
|
||||
throw NetworkException(
|
||||
'Failed to create task: ${e.message}',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// ✅ Handle typed errors in UI
|
||||
asyncTasks.when(
|
||||
data: (tasks) => TaskListBody(tasks: tasks),
|
||||
loading: () => const LoadingIndicator(),
|
||||
error: (error, _) => switch (error) {
|
||||
NetworkException() => ErrorView(message: 'Network error: ${error.message}'),
|
||||
NotFoundException() => const ErrorView(message: 'Not found'),
|
||||
_ => ErrorView(message: 'Unexpected error: $error'),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- All custom exceptions extend `AppException` (sealed class)
|
||||
- Infrastructure exceptions (`DioException`, `SocketException`) are caught and re-thrown as domain exceptions
|
||||
- Never expose infrastructure types (Dio, HTTP status codes) to the UI layer
|
||||
- Use exhaustive `switch` on the sealed class in error display
|
||||
- See `error-handling-principles.md` for general error handling guidance
|
||||
|
||||
---
|
||||
|
||||
### Navigation with `go_router`
|
||||
|
||||
**`go_router` is the canonical navigation library.**
|
||||
|
||||
```dart
|
||||
// core/router/app_router.dart
|
||||
@riverpod
|
||||
GoRouter appRouter(Ref ref) {
|
||||
return GoRouter(
|
||||
initialLocation: '/tasks',
|
||||
routes: [
|
||||
GoRoute(path: '/tasks', builder: (_, __) => const TaskListView()),
|
||||
GoRoute(
|
||||
path: '/tasks/:id',
|
||||
builder: (_, state) => TaskDetailView(
|
||||
// state.pathParameters['id'] is guaranteed non-null by the :id
|
||||
// route pattern — acceptable use of ! in route infrastructure code
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate — always by path, never by widget reference
|
||||
context.go('/tasks/$taskId');
|
||||
context.push('/tasks/new'); // push adds to the back stack
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Dart Language Idioms
|
||||
|
||||
1. **Null safety — use `?.`, `??`, and `??=` idiomatically**
|
||||
```dart
|
||||
final city = user?.address?.city ?? 'Unknown';
|
||||
cache ??= await compute(); // assign only if null
|
||||
```
|
||||
|
||||
2. **Use `late` only for fields initialized before first use that cannot be `final`**
|
||||
- Prefer `final` fields initialized in the constructor
|
||||
- `late` without initialization is an unsafe nullable escape hatch
|
||||
|
||||
3. **Extension methods for adding behaviour to types you don't own**
|
||||
```dart
|
||||
extension TaskStatusLabel on TaskStatus {
|
||||
String get label => switch (this) {
|
||||
TaskStatus.pending => 'Pending',
|
||||
TaskStatus.done => 'Done',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
4. **Use `switch` expressions (Dart 3+) for exhaustive pattern matching**
|
||||
```dart
|
||||
final label = status switch {
|
||||
TaskStatus.pending => 'Pending',
|
||||
TaskStatus.done => 'Done',
|
||||
// Compiler error if a case is missing
|
||||
};
|
||||
```
|
||||
|
||||
5. **Avoid `dynamic` — it is the Dart equivalent of TypeScript's `any`**
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Flutter/Riverpod 3 test patterns.
|
||||
|
||||
#### Unit Test Providers with `ProviderContainer.test`
|
||||
|
||||
`ProviderContainer.test` creates an isolated container that **auto-disposes after the test** — no manual `addTearDown` needed.
|
||||
|
||||
```dart
|
||||
test('addTask updates state', () async {
|
||||
final container = ProviderContainer.test(overrides: [
|
||||
taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
|
||||
]);
|
||||
|
||||
await container.read(taskListProvider.notifier).addTask(request);
|
||||
expect(container.read(taskListProvider).value, hasLength(1));
|
||||
});
|
||||
```
|
||||
|
||||
#### Override Only the `build` Method with `overrideWithBuild`
|
||||
|
||||
When you need to control initial state but keep the notifier's methods intact:
|
||||
|
||||
```dart
|
||||
test('deleteTask removes item from pre-seeded list', () async {
|
||||
final container = ProviderContainer.test(overrides: [
|
||||
taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
|
||||
taskListProvider.overrideWithBuild((ref, notifier) {
|
||||
// Seed initial state — methods (addTask, deleteTask) still work
|
||||
return Future.value([mockTask1, mockTask2]);
|
||||
}),
|
||||
]);
|
||||
|
||||
await container.read(taskListProvider.notifier).deleteTask(mockTask1.id);
|
||||
expect(container.read(taskListProvider).value, hasLength(1));
|
||||
});
|
||||
```
|
||||
|
||||
#### Widget Tests with `ProviderScope`
|
||||
|
||||
```dart
|
||||
testWidgets('shows task list', (tester) async {
|
||||
await tester.pumpWidget(ProviderScope(
|
||||
overrides: [
|
||||
taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
|
||||
],
|
||||
child: const MaterialApp(home: TaskListView()),
|
||||
));
|
||||
expect(find.byType(TaskCard), findsWidgets);
|
||||
});
|
||||
```
|
||||
|
||||
#### Use `mockito` with `@GenerateNiceMocks` for Interface Mocks
|
||||
|
||||
Place the annotation on the library (top of the test file or on `main`) and add a `part` directive for the generated file:
|
||||
|
||||
```dart
|
||||
// task_notifier_test.dart
|
||||
@GenerateNiceMocks([MockSpec<TaskRepository>()])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'task_notifier_test.mocks.dart'; // generated by build_runner
|
||||
|
||||
void main() {
|
||||
// tests use MockTaskRepository()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Anti-Patterns — NEVER DO THIS
|
||||
|
||||
These are common mistakes agents reproduce from Riverpod 2 training data. **Every item below is a hard rule violation.**
|
||||
|
||||
| ❌ Anti-Pattern | ✅ Correct Pattern |
|
||||
|---|---|
|
||||
| `StateProvider` | `@riverpod` class with `Notifier` |
|
||||
| `StateNotifierProvider` | `@riverpod` class with `Notifier` |
|
||||
| `ChangeNotifierProvider` | `@riverpod` class with `Notifier` |
|
||||
| `import 'package:riverpod/legacy.dart'` | Never import legacy APIs |
|
||||
| Typed ref subclass (`TaskDetailRef ref`) | `Ref ref` — Riverpod 3 uses a single `Ref` type; typed subclasses were a Riverpod 2 codegen artifact that no longer exists |
|
||||
| `ref.watch` inside async/event handler | `ref.read` for one-shot reads in handlers |
|
||||
| Accessing `state`/`ref` after `await` without `ref.mounted` check | Always check `ref.mounted` after `await` |
|
||||
| `ProviderContainer()` + `addTearDown(container.dispose)` | `ProviderContainer.test(overrides: [...])` |
|
||||
| `Timer.periodic` for polling data | `StreamProvider` or `Stream` return |
|
||||
| `keepAlive: false` in annotation | Omit — false is the default |
|
||||
| Manual providers without `@riverpod` annotation | Always use code generation |
|
||||
| Catching raw exceptions from provider reads | Catch `ProviderException` |
|
||||
| `overrideWith((_) => MockNotifier())` when only initial state needs seeding | `overrideWithBuild(...)` — keeps notifier methods intact, only seeds initial state |
|
||||
|
||||
---
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
| Tool | Purpose | Config File |
|
||||
| --- | --- | --- |
|
||||
| `dart format` | Canonical formatting | — (built-in) |
|
||||
| `flutter analyze` | Static analysis + lint | `analysis_options.yaml` |
|
||||
| `riverpod_lint` | Riverpod-specific lint rules | `dev_dependencies` |
|
||||
| `dart pub deps` | Dependency audit | — |
|
||||
| `dart run build_runner build` | Generate provider code | — |
|
||||
|
||||
**Mandatory `analysis_options.yaml` settings (Dart 3+):**
|
||||
```yaml
|
||||
analyzer:
|
||||
language:
|
||||
strict-casts: true
|
||||
strict-raw-types: true
|
||||
errors:
|
||||
invalid_assignment: error
|
||||
plugins:
|
||||
- riverpod_lint
|
||||
linter:
|
||||
rules:
|
||||
- prefer_const_constructors
|
||||
- prefer_const_declarations
|
||||
- avoid_dynamic_calls
|
||||
- avoid_print
|
||||
- use_super_parameters
|
||||
```
|
||||
|
||||
**After any code change involving providers, run:**
|
||||
```bash
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
flutter analyze
|
||||
dart format .
|
||||
```
|
||||
|
||||
**During active development, use `watch` mode to avoid re-running manually:**
|
||||
```bash
|
||||
dart run build_runner watch --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Project Structure — Flutter Mobile @project-structure-flutter-mobile.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When creating branches, committing code, managing PRs, or working with version control
|
||||
---
|
||||
|
||||
## Git Workflow Principles
|
||||
|
||||
### Commit Messages — Conventional Commits
|
||||
|
||||
**Format:**
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
| Type | Purpose |
|
||||
| ---------- | -------------------------------- |
|
||||
| `feat` | New feature |
|
||||
| `fix` | Bug fix |
|
||||
| `docs` | Documentation only |
|
||||
| `style` | Formatting, semicolons, etc. |
|
||||
| `refactor` | Code change (no new feature/fix) |
|
||||
| `test` | Adding or updating tests |
|
||||
| `chore` | Maintenance, dependencies |
|
||||
| `perf` | Performance improvement |
|
||||
| `ci` | CI/CD configuration changes |
|
||||
|
||||
**Rules:**
|
||||
- Description is imperative mood ("add" not "added", "fix" not "fixes")
|
||||
- Scope matches the feature area (e.g., `task`, `auth`, `ui`)
|
||||
- Description is concise (<72 characters)
|
||||
- Body explains **why**, not what (the diff shows what)
|
||||
|
||||
### Branch Naming
|
||||
|
||||
**Format:** `<type>/<ticket-or-short-description>`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
feat/task-crud-api
|
||||
fix/auth-token-expiry
|
||||
refactor/storage-layer
|
||||
chore/update-deps
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Use lowercase with hyphens (kebab-case)
|
||||
- Prefix matches commit type
|
||||
- Keep branch names short but descriptive
|
||||
|
||||
### Commit Hygiene
|
||||
|
||||
- **One logical change per commit** — don't mix unrelated changes
|
||||
- **Never commit broken tests** — all tests must pass before committing
|
||||
- **Don't commit debug code** — remove console.log, print statements, TODO hacks
|
||||
- **Don't commit secrets** — use `.gitignore` and environment variables
|
||||
|
||||
### PR Size Guidelines
|
||||
|
||||
- **Ideal:** <400 lines changed
|
||||
- **Acceptable:** 400-800 lines
|
||||
- **Too large:** >800 lines — split into smaller PRs
|
||||
|
||||
**Why:** Large PRs get rubber-stamped. Small PRs get thoughtful reviews.
|
||||
|
||||
### Merge Strategy
|
||||
|
||||
- **Feature branches → main:** Squash merge (clean history)
|
||||
- **Release branches:** Merge commit (preserve history)
|
||||
- **Hotfixes:** Cherry-pick to affected branches
|
||||
|
||||
### Git Workflow Checklist
|
||||
|
||||
- [ ] Branch named with correct type prefix?
|
||||
- [ ] All commits follow conventional format?
|
||||
- [ ] No debug code or secrets committed?
|
||||
- [ ] All tests pass before committing?
|
||||
- [ ] PR is <400 lines (or justified if larger)?
|
||||
- [ ] Commit messages explain why, not just what?
|
||||
|
||||
### Related Principles
|
||||
- Code Completion Mandate @code-completion-mandate.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Security Mandate @security-mandate.md
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Go code, reviewing Go idioms, or working on a Go backend project
|
||||
---
|
||||
|
||||
## Go Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Go favors simplicity, explicitness, and readability. The language is intentionally small — resist the urge to import patterns from other languages. If it looks boring and obvious, it's probably idiomatic Go.
|
||||
|
||||
> **Scope:** This file covers Go-specific *coding idioms*. For file layout, see `project-structure-go-backend.md`. For test naming conventions, see `testing-strategy.md`. For logging library choice, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Always return errors — never panic in library or business code**
|
||||
- `panic` is reserved for truly unrecoverable states (programmer errors, nil dereference)
|
||||
- Use `recover` only at top-level goroutine boundaries (middleware, server startup)
|
||||
|
||||
2. **Wrap errors with context using `%w`**
|
||||
```go
|
||||
// ✅ Preserves the error chain for errors.Is / errors.As
|
||||
return fmt.Errorf("creating task for user %s: %w", userID, err)
|
||||
|
||||
// ❌ Loses the error chain
|
||||
return fmt.Errorf("creating task: %v", err)
|
||||
```
|
||||
|
||||
3. **Use sentinel errors for expected branch conditions**
|
||||
```go
|
||||
// Define in errors.go
|
||||
var ErrNotFound = errors.New("not found")
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
|
||||
// Caller checks with errors.Is
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
// handle
|
||||
}
|
||||
```
|
||||
|
||||
4. **Use typed errors for rich domain errors**
|
||||
```go
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// Caller unwraps with errors.As
|
||||
var ve *ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
// access ve.Field, ve.Message
|
||||
}
|
||||
```
|
||||
|
||||
5. **Handle errors at the right level** — propagate upward until you have enough context to act on them; don't swallow or re-wrap the same error twice.
|
||||
|
||||
---
|
||||
|
||||
### Interfaces
|
||||
|
||||
1. **Keep interfaces small — one or two methods is ideal**
|
||||
```go
|
||||
// ✅ Focused, composable
|
||||
type Reader interface { Read(p []byte) (n int, err error) }
|
||||
type Writer interface { Write(p []byte) (n int, err error) }
|
||||
|
||||
// ❌ Monolithic
|
||||
type FileManager interface {
|
||||
Read(...); Write(...); Delete(...); List(...); Stat(...)
|
||||
}
|
||||
```
|
||||
|
||||
2. **"Accept interfaces, return structs"**
|
||||
- Function parameters: accept interfaces for flexibility and testability
|
||||
- Return values: return concrete structs so callers can access all methods
|
||||
|
||||
3. **Define interfaces where they are *used*, not where they are *implemented***
|
||||
```go
|
||||
// ✅ Defined in the consumer package (task feature)
|
||||
// task/storage.go
|
||||
type Storage interface {
|
||||
GetByID(ctx context.Context, id string) (*Task, error)
|
||||
}
|
||||
|
||||
// postgres.go implements Storage — it does NOT define it
|
||||
```
|
||||
|
||||
4. **Implicit satisfaction is a feature — don't use embedding to "implement" interfaces**
|
||||
- Any type with the right method set satisfies an interface automatically
|
||||
- No `implements` keyword needed or wanted
|
||||
|
||||
---
|
||||
|
||||
### Goroutines and Channels
|
||||
|
||||
> For general concurrency principles (race conditions, deadlocks, message passing), see `concurrency-and-threading-principles.md`. This section covers Go-specific mechanics.
|
||||
|
||||
1. **Always pass `context.Context` as the first parameter**
|
||||
```go
|
||||
// ✅
|
||||
func (s *Service) GetTask(ctx context.Context, id string) (*Task, error)
|
||||
|
||||
// ❌ — no way to cancel or propagate deadlines
|
||||
func (s *Service) GetTask(id string) (*Task, error)
|
||||
```
|
||||
|
||||
2. **Never start a goroutine without knowing how it will stop**
|
||||
```go
|
||||
// ✅ Goroutine is bounded by context cancellation
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case item := <-ch:
|
||||
process(item)
|
||||
}
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
3. **Use `errgroup` for concurrent fan-out with error collection**
|
||||
```go
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return fetchUsers(ctx) })
|
||||
g.Go(func() error { return fetchOrders(ctx) })
|
||||
if err := g.Wait(); err != nil { ... }
|
||||
```
|
||||
|
||||
4. **Prefer channels for ownership transfer; mutexes for shared state**
|
||||
- Channel: "I'm handing this data to you"
|
||||
- Mutex: "We're both reading/writing this shared thing"
|
||||
|
||||
5. **Close channels from the sender, never the receiver**
|
||||
|
||||
---
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
1. **Receiver names: short, consistent, and the first letter of the type**
|
||||
```go
|
||||
func (s *Service) Create(...) {} // ✅
|
||||
func (svc *Service) Create(...) {} // ❌ — too verbose
|
||||
func (self *Service) Create(...) {} // ❌ — not Go
|
||||
```
|
||||
|
||||
2. **Package names: short, lowercase, no underscores, no plurals**
|
||||
```go
|
||||
package task // ✅
|
||||
package tasks // ❌ plural
|
||||
package task_service // ❌ underscore
|
||||
```
|
||||
|
||||
3. **Acronyms follow Go conventions (all caps or all lowercase)**
|
||||
```go
|
||||
userID // ✅
|
||||
userId // ❌
|
||||
HTTPClient // ✅
|
||||
HttpClient // ❌
|
||||
```
|
||||
|
||||
4. **Unexported identifiers omit the type name** — if it's private, keep it terse
|
||||
|
||||
5. **Don't stutter** — `task.Task` is fine; `task.TaskService` is not
|
||||
|
||||
---
|
||||
|
||||
### Idiomatic Patterns
|
||||
|
||||
1. **Functional options for optional configuration**
|
||||
```go
|
||||
type Option func(*Service)
|
||||
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(s *Service) { s.timeout = d }
|
||||
}
|
||||
|
||||
func NewService(store Storage, opts ...Option) *Service {
|
||||
s := &Service{store: store, timeout: 30 * time.Second}
|
||||
for _, o := range opts { o(s) }
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
2. **`defer` for cleanup — always use error-checked closures**
|
||||
|
||||
Every deferred cleanup call that returns an error MUST check and log the error.
|
||||
Never use bare `defer X.Close()` — the discarded error hides resource leak failures.
|
||||
|
||||
```go
|
||||
// ❌ NEVER: Error silently discarded
|
||||
defer rows.Close()
|
||||
|
||||
// ✅ ALWAYS: Error-checked closure with structured logging
|
||||
rows, err := db.QueryContext(ctx, query)
|
||||
if err != nil { return fmt.Errorf("querying tasks: %w", err) }
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
slog.Warn("failed to close rows", "error", err, "operation", "ListTasks")
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**Transaction rollback:**
|
||||
```go
|
||||
// ❌ NEVER
|
||||
defer tx.Rollback()
|
||||
|
||||
// ✅ ALWAYS: Guard against sql.ErrTxDone (already committed)
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil && !errors.Is(err, sql.ErrTxDone) {
|
||||
slog.Error("failed to rollback transaction", "error", err, "operation", "CreateOrder")
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**HTTP response body:**
|
||||
```go
|
||||
// ❌ NEVER
|
||||
defer resp.Body.Close()
|
||||
|
||||
// ✅ ALWAYS: Drain then close (prevents connection reuse issues)
|
||||
defer func() {
|
||||
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
|
||||
slog.Warn("failed to drain response body", "error", err)
|
||||
}
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
slog.Warn("failed to close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
3. **Avoid `init()` functions** — they run implicitly and make testing harder; prefer explicit initialization in `main` or constructors
|
||||
|
||||
4. **Prefer `struct` embedding over inheritance for code reuse**, but only when the embedded type truly represents an "is-a" relationship
|
||||
|
||||
5. **Use named return values only for documentation or `defer`-based cleanup** — never rely on naked returns in non-trivial functions
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test file naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Go-specific tooling only.
|
||||
|
||||
1. **Table-driven tests are the default pattern**
|
||||
```go
|
||||
func TestCalculateDiscount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input float64
|
||||
expected float64
|
||||
wantErr bool
|
||||
}{
|
||||
{"zero items", 0, 0, false},
|
||||
{"negative input", -1, 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := calculateDiscount(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use `testify` for assertions** (`require` for fatal assertions, `assert` for non-fatal)
|
||||
|
||||
3. **Run with the race detector in CI** — `go test -race ./...`
|
||||
|
||||
4. **Use `httptest.NewRecorder()` for HTTP handler tests** — no live server needed
|
||||
|
||||
5. **Test behaviour, not implementation** — assert on outputs and side effects, not internal field values
|
||||
|
||||
---
|
||||
|
||||
### Formatting and Static Analysis
|
||||
|
||||
All of the following **must pass with zero warnings/errors** before any commit. See `code-completion-mandate.md` for the full checklist.
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
| ----------------------- | ------------------------ | -------------------- |
|
||||
| `gofumpt` / `goimports` | Canonical formatting | `gofumpt -l -w .` |
|
||||
| `go vet` | Correctness checks | `go vet ./...` |
|
||||
| `staticcheck` | Advanced static analysis | `staticcheck ./...` |
|
||||
| `gosec` | Security scanning | `gosec -quiet ./...` |
|
||||
| `golangci-lint` | Aggregated linter (CI) | `golangci-lint run` |
|
||||
| `govulncheck` | Dependency CVE scanning | `govulncheck ./...` |
|
||||
|
||||
- Never disable a linter without a comment explaining why
|
||||
- **`//nolint:errcheck` is NEVER acceptable.** If a function returns an error, handle it — even in `defer`. Use an error-checked closure (see § Idiomatic Patterns above). This is the #1 source of audit findings.
|
||||
- Other `//nolint:` directives require a `// NOLINT:` rationale comment AND must be approved during code review
|
||||
- Fast iteration during development: `go vet ./...` type-checks and catches correctness issues without producing binaries (analogous to `cargo check`) — reserve `golangci-lint` for pre-commit
|
||||
|
||||
> **Logging:** Never use `fmt.Println` or `log.Printf` in production service code — these produce unstructured output. Use `log/slog` (stdlib, Go 1.21+) or the project's chosen adapter. See `logging-and-observability-principles.md` for the required library and patterns.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Project Structure — Go Backend @project-structure-go-backend.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Logging and Observability Mandate
|
||||
|
||||
### Observability Overview
|
||||
|
||||
Observability is built on **three pillars** — this mandate governs Logs. For Metrics and Health Checks, see Monitoring and Alerting Principles @monitoring-and-alerting-principles.md.
|
||||
|
||||
1. **Logs** — *What happened* (events, errors, state changes) — governed by this file
|
||||
2. **Metrics** — *How much / how many* (quantitative measurements, RED/USE) — see `monitoring-and-alerting-principles.md`
|
||||
3. **Traces** — *How did it happen* (request flow through system) — platform-level concern (OpenTelemetry, Jaeger)
|
||||
|
||||
### Universal Requirement: All Operations Must Be Logged
|
||||
|
||||
**Every operation entry point MUST include logging. No exceptions.**
|
||||
|
||||
**What constitutes an "operation" (mandatory logging):**
|
||||
- API endpoints and request handlers
|
||||
- Background jobs and queue workers
|
||||
- Event handlers and message consumers
|
||||
- Scheduled tasks and cron jobs
|
||||
- CLI commands
|
||||
- External service calls (to third-party APIs)
|
||||
- Database transactions
|
||||
|
||||
**What is NOT an operation (no direct logging):**
|
||||
- Pure business logic functions (called within operations)
|
||||
- Utility and helper functions
|
||||
- Data transformations and validators
|
||||
|
||||
**Minimum logging requirement (3 points):**
|
||||
1. **Operation start:** Log at entry with context (correlationId, userId, operation name)
|
||||
2. **Operation success:** Log completion with duration and result identifiers
|
||||
3. **Operation failure:** Log error with full context (correlationId, error details, stack trace)
|
||||
|
||||
**Mandatory context in all logs:**
|
||||
- `correlationId`: UUID for tracing across services
|
||||
- `operation`: Clear operation name (e.g., "create_order", "process_payment")
|
||||
- `duration`: Execution time in milliseconds
|
||||
- `userId`: Actor who triggered the operation (when applicable)
|
||||
- `error`: Full error context on failures
|
||||
|
||||
**Enforcement strategy:**
|
||||
Prefer middleware, decorators, or framework interceptors for automatic operation logging rather than manual logging in every handler. This ensures coverage without code duplication.
|
||||
|
||||
**When implementing any operation entry point, you MUST add logging before proceeding with implementation.**
|
||||
|
||||
### Implementation Guide
|
||||
|
||||
For log levels, structured logging patterns, language-specific implementations, security considerations, and code examples, see Logging and Observability Principles @logging-and-observability-principles.md.
|
||||
|
||||
### Related Principles
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Monitoring and Alerting Principles @monitoring-and-alerting-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
@@ -0,0 +1,361 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing logging, working with loggers, or setting up observability for operations (API handlers, database queries, background jobs, external API calls)
|
||||
---
|
||||
|
||||
## Logging and Observability Principles
|
||||
|
||||
> **⚠️ Prerequisite:** All operations MUST be logged per Logging and Observability Mandate @logging-and-observability-mandate.md. This guide provides implementation patterns only.
|
||||
|
||||
|
||||
### Logging Standards
|
||||
|
||||
#### Log Levels (Standard Priority)
|
||||
|
||||
Use consistent log levels across all services:
|
||||
|
||||
| Level | When to Use | Examples |
|
||||
| --------- | --------------------------------------- | -------------------------------------------------------- |
|
||||
| **TRACE** | Extremely detailed diagnostic info | Function entry/exit, variable states (dev only) |
|
||||
| **DEBUG** | Detailed flow for debugging | Query execution, cache hits/misses, state transitions |
|
||||
| **INFO** | General informational messages | Request started, task created, user logged in |
|
||||
| **WARN** | Potentially harmful situations | Deprecated API usage, fallback triggered, retry attempt |
|
||||
| **ERROR** | Error events that allow app to continue | Request failed, external API timeout, validation failure |
|
||||
| **FATAL** | Severe errors causing shutdown | Database unreachable, critical config missing |
|
||||
|
||||
#### Logging Rules
|
||||
|
||||
**1. Every request/operation must log:**
|
||||
```
|
||||
|
||||
// Start of operation
|
||||
log.Info("creating task",
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
"title", task.Title,
|
||||
)
|
||||
|
||||
// Success
|
||||
log.Info("task created successfully",
|
||||
"correlationId", correlationID,
|
||||
"taskId", task.ID,
|
||||
"duration", time.Since(start),
|
||||
)
|
||||
|
||||
// Error
|
||||
log.Error("failed to create task",
|
||||
"correlationId", correlationID,
|
||||
"error", err,
|
||||
"userId", userID,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
**2. Always include context:**
|
||||
- `correlationId`: Trace requests across services (UUID)
|
||||
- `userId`: Who triggered the action
|
||||
- `duration`: Operation timing (milliseconds)
|
||||
- `error`: Error details (if failed)
|
||||
|
||||
|
||||
**3. Structured logging only** (no string formatting):
|
||||
```
|
||||
|
||||
// ✅ Structured
|
||||
log.Info("user login", "userId", userID, "ip", clientIP)
|
||||
|
||||
// ❌ String formatting
|
||||
log.Info(fmt.Sprintf("User %s logged in from %s", userID, clientIP))
|
||||
|
||||
```
|
||||
|
||||
**4. Security - Never log:**
|
||||
- Passwords or password hashes
|
||||
- API keys or tokens
|
||||
- Credit card numbers
|
||||
- PII in production logs (email/phone only if necessary and sanitized)
|
||||
- Full request/response bodies (unless DEBUG level in non-prod)
|
||||
|
||||
**5. Performance - Never log in hot paths:**
|
||||
- Inside tight loops
|
||||
- Per-item processing in batch operations (use summary instead)
|
||||
- Synchronous logging in latency-critical paths
|
||||
|
||||
**Best Practice:** "Use logger middleware redaction (e.g., pino-redact, zap masking) rather than manual string manipulation."
|
||||
|
||||
#### Language-Specific Implementations
|
||||
|
||||
##### Go (using slog standard library)
|
||||
```
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// Configure logger
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo, // Production default
|
||||
}))
|
||||
|
||||
// Usage
|
||||
logger.Info("operation started",
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
)
|
||||
|
||||
logger.Error("operation failed",
|
||||
"correlationId", correlationID,
|
||||
"error", err,
|
||||
"retryCount", retries,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### TypeScript/Node.js (using pino)
|
||||
```
|
||||
|
||||
import pino from 'pino';
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
});
|
||||
|
||||
logger.info({
|
||||
correlationId,
|
||||
userId,
|
||||
duration: Date.now() - startTime,
|
||||
}, 'task created successfully');
|
||||
|
||||
logger.error({
|
||||
correlationId,
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
}, 'failed to create task');
|
||||
|
||||
```
|
||||
|
||||
##### Python (using structlog)
|
||||
```python
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
logger.info("task_created",
|
||||
correlation_id=correlation_id,
|
||||
user_id=user_id,
|
||||
task_id=task.id,
|
||||
)
|
||||
|
||||
logger.error("task_creation_failed",
|
||||
correlation_id=correlation_id,
|
||||
error=str(err),
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Log Patterns by Operation Type
|
||||
|
||||
##### API Request/Response
|
||||
```
|
||||
|
||||
// Request received
|
||||
log.Info("request received",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
)
|
||||
|
||||
// Request completed
|
||||
log.Info("request completed",
|
||||
"correlationId", correlationID,
|
||||
"status", statusCode,
|
||||
"duration", duration,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Database Operations
|
||||
```
|
||||
|
||||
// Query start (DEBUG level)
|
||||
log.Debug("executing query",
|
||||
"correlationId", correlationID,
|
||||
"query", "SELECT * FROM tasks WHERE user_id = $1",
|
||||
)
|
||||
|
||||
// Query success (DEBUG level)
|
||||
log.Debug("query completed",
|
||||
"correlationId", correlationID,
|
||||
"rowsReturned", len(results),
|
||||
"duration", duration,
|
||||
)
|
||||
|
||||
// Query error (ERROR level)
|
||||
log.Error("query failed",
|
||||
"correlationId", correlationID,
|
||||
"error", err,
|
||||
"query", "SELECT * FROM tasks WHERE user_id = $1",
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### External API Calls
|
||||
```
|
||||
|
||||
// Call start
|
||||
log.Info("calling external API",
|
||||
"correlationId", correlationID,
|
||||
"service", "email-provider",
|
||||
"endpoint", "/send",
|
||||
)
|
||||
|
||||
// Retry (WARN level)
|
||||
log.Warn("retrying external API call",
|
||||
"correlationId", correlationID,
|
||||
"service", "email-provider",
|
||||
"attempt", retryCount,
|
||||
"error", err,
|
||||
)
|
||||
|
||||
// Circuit breaker open (WARN level)
|
||||
log.Warn("circuit breaker opened",
|
||||
"correlationId", correlationID,
|
||||
"service", "email-provider",
|
||||
"failureCount", failures,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Background Jobs
|
||||
```
|
||||
|
||||
// Job start
|
||||
log.Info("job started",
|
||||
"jobId", jobID,
|
||||
"jobType", "email-digest",
|
||||
)
|
||||
|
||||
// Progress (INFO level - periodic, not per-item)
|
||||
log.Info("job progress",
|
||||
"jobId", jobID,
|
||||
"processed", 1000,
|
||||
"total", 5000,
|
||||
"percentComplete", 20,
|
||||
)
|
||||
|
||||
// Job complete
|
||||
log.Info("job completed",
|
||||
"jobId", jobID,
|
||||
"duration", duration,
|
||||
"itemsProcessed", count,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Error Scenarios
|
||||
```
|
||||
|
||||
// Recoverable error (ERROR level)
|
||||
log.Error("validation failed",
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
"error", "invalid email format",
|
||||
"input", sanitizedInput, // Sanitized!
|
||||
)
|
||||
|
||||
// Fatal error (FATAL level)
|
||||
log.Fatal("critical dependency unavailable",
|
||||
"error", err,
|
||||
"dependency", "database",
|
||||
"action", "shutting down",
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
#### Environment-Specific Configuration
|
||||
|
||||
| Environment | Level | Format | Destination |
|
||||
| --------------- | ----- | ---------------- | ----------------------- |
|
||||
| **Development** | DEBUG | Pretty (colored) | Console |
|
||||
| **Staging** | INFO | JSON | Stdout → CloudWatch/GCP |
|
||||
| **Production** | INFO | JSON | Stdout → CloudWatch/GCP |
|
||||
|
||||
**Configuration (Go example):**
|
||||
```
|
||||
|
||||
func configureLogger() *slog.Logger {
|
||||
var handler slog.Handler
|
||||
|
||||
level := slog.LevelInfo
|
||||
if os.Getenv("ENV") == "development" {
|
||||
level = slog.LevelDebug
|
||||
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
} else {
|
||||
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
|
||||
return slog.New(handler)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Testing Logs
|
||||
|
||||
**Unit tests:** Capture and assert on log output
|
||||
```
|
||||
|
||||
// Go example
|
||||
func TestUserLogin(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||
|
||||
// Test operation
|
||||
service := NewUserService(logger, mockStore)
|
||||
err := service.Login(ctx, email, password)
|
||||
|
||||
// Assert logs
|
||||
require.NoError(t, err)
|
||||
logs := buf.String()
|
||||
assert.Contains(t, logs, "user login successful")
|
||||
assert.Contains(t, logs, email)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Monitoring Integration
|
||||
|
||||
**Correlation IDs:**
|
||||
- Generate at ingress (API gateway, first handler)
|
||||
- Propagate through all services
|
||||
- Include in all logs, errors, and traces
|
||||
- Format: UUID v4
|
||||
|
||||
**Log aggregation:**
|
||||
- Ship to centralized system (CloudWatch, GCP Logs, Datadog)
|
||||
- Index by: correlationId, userId, level, timestamp
|
||||
- Alert on ERROR/FATAL patterns
|
||||
- Dashboard: request rates, error rates, latency
|
||||
|
||||
#### Checklist for Every Feature
|
||||
|
||||
- [ ] All public operations log INFO on start
|
||||
- [ ] All operations log INFO/ERROR on complete/failure
|
||||
- [ ] All logs include correlationId
|
||||
- [ ] No sensitive data in logs
|
||||
- [ ] Structured logging (key-value pairs)
|
||||
- [ ] Appropriate log level used
|
||||
- [ ] Error logs include error details
|
||||
- [ ] Performance-critical paths use DEBUG level
|
||||
|
||||
### Related Principles
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Monitoring and Alerting Principles @monitoring-and-alerting-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing health checks, metrics instrumentation, error tracking integration, or production observability code
|
||||
---
|
||||
|
||||
## Monitoring and Alerting Principles
|
||||
|
||||
> **Scope:** This rule covers what the agent **implements in code**. Organizational
|
||||
> concerns (SLO/SLI definitions, on-call rotation, alert escalation) are out of scope —
|
||||
> those are team/org decisions, not code generation concerns.
|
||||
|
||||
### Health Checks
|
||||
|
||||
**Every service must expose health check endpoints:**
|
||||
|
||||
- **`/health` (Liveness)** — "Is the process alive?"
|
||||
- Returns 200 if the process is running
|
||||
- No dependency checks (database, cache, etc.)
|
||||
- Used by orchestrators to decide whether to restart
|
||||
|
||||
- **`/ready` (Readiness)** — "Can the service accept traffic?"
|
||||
- Checks all critical dependencies (database, cache, message queue)
|
||||
- Returns 503 if any dependency is unavailable
|
||||
- Used by load balancers to route traffic
|
||||
|
||||
**Rules:**
|
||||
- Health checks must be fast (< 1 second)
|
||||
- Health checks must not have side effects
|
||||
- Separate liveness from readiness — they serve different purposes
|
||||
|
||||
### Metrics Instrumentation
|
||||
|
||||
**Instrument code using the RED method for services:**
|
||||
- **Rate** — requests per second
|
||||
- **Errors** — error count/rate
|
||||
- **Duration** — request latency (use histograms, not averages)
|
||||
|
||||
**Instrument code using the USE method for resources:**
|
||||
- **Utilization** — how much of the resource is used
|
||||
- **Saturation** — how much work is queued
|
||||
- **Errors** — error count for the resource
|
||||
|
||||
**Rules:**
|
||||
- Use counters for things that only go up (requests, errors)
|
||||
- Use gauges for things that go up and down (connections, queue depth)
|
||||
- Use histograms for distributions (latency, response size)
|
||||
- Label metrics consistently (service, method, status_code)
|
||||
- Don't create high-cardinality labels (no user IDs as labels)
|
||||
|
||||
### Error Tracking Integration
|
||||
|
||||
- **Capture unhandled exceptions** with full stack traces
|
||||
- **Include context** — user ID, request ID, correlation ID
|
||||
- **Group errors** by root cause, not by instance
|
||||
- **Set severity levels** based on user impact
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
- **Circuit breakers** for external dependencies — stop calling failing services
|
||||
- **Fallbacks** for non-critical features — serve cached data, show reduced UI
|
||||
- **Timeouts** on all external calls — never wait indefinitely
|
||||
- **Retry with backoff** for transient failures — exponential backoff with jitter
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
This rule is tool-agnostic. Whether the project uses Datadog, LGTM stack, Sentry,
|
||||
New Relic, or CloudWatch — the code patterns (health checks, metrics, error tracking)
|
||||
are the same. The specific client library belongs in project-level configuration.
|
||||
|
||||
### Monitoring Checklist
|
||||
|
||||
- [ ] Health check endpoints implemented (/health and /ready)?
|
||||
- [ ] Liveness probe has no dependency checks?
|
||||
- [ ] Readiness probe checks all critical dependencies?
|
||||
- [ ] Key operations instrumented with RED/USE metrics?
|
||||
- [ ] No high-cardinality metric labels?
|
||||
- [ ] Unhandled exceptions captured with context?
|
||||
- [ ] Circuit breakers on external dependencies?
|
||||
- [ ] Timeouts on all external calls?
|
||||
|
||||
### Related Principles
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on performance optimization, profiling, benchmarking, or performance-critical code paths
|
||||
---
|
||||
|
||||
## Performance Optimization Principles
|
||||
|
||||
### Measure Before Optimizing
|
||||
|
||||
**"Premature optimization is the root of all evil" - Donald Knuth**
|
||||
|
||||
**Process:**
|
||||
|
||||
1. **Measure:** Profile to find actual bottlenecks (don't guess)
|
||||
2. **Identify:** Find the 20% of code consuming 80% of resources
|
||||
3. **Optimize:** Improve that specific bottleneck
|
||||
4. **Measure again:** Verify improvement with benchmarks
|
||||
5. **Repeat:** Only if still not meeting performance goals
|
||||
|
||||
**Don't optimize:**
|
||||
|
||||
- Code that's "fast enough" for requirements
|
||||
- Code that's rarely executed
|
||||
- Without measurable performance problem
|
||||
|
||||
### Choose Appropriate Data Structures
|
||||
|
||||
**Selection matters:**
|
||||
|
||||
- Hash map: O(1) lookup, unordered
|
||||
- Array/list: O(1) index access, O(n) search, ordered
|
||||
- Binary tree: O(log n) operations, sorted order
|
||||
- Set: O(1) membership testing, unique elements
|
||||
|
||||
**Wrong choice causes performance degradation:**
|
||||
|
||||
- Using array for lookups: O(n) when O(1) possible with hash map
|
||||
- Using list for sorted data: O(n log n) sort vs O(log n) tree operations
|
||||
|
||||
### Avoid Premature Abstraction
|
||||
|
||||
**Abstraction has costs:**
|
||||
|
||||
- Runtime overhead (indirection, virtual dispatch, dynamic resolution)
|
||||
- Cognitive overhead (understanding layers of abstraction)
|
||||
- Maintenance overhead (changes ripple through abstractions)
|
||||
|
||||
**Start concrete, abstract when pattern emerges:**
|
||||
|
||||
- Write straightforward code first
|
||||
- Identify duplication and common patterns
|
||||
- Abstract only when there's clear benefit
|
||||
- Don't add "for future flexibility" without evidence
|
||||
|
||||
### Optimization Techniques
|
||||
|
||||
**Caching:**
|
||||
|
||||
- Store expensive computation results
|
||||
- Cache database queries, API responses, rendered templates
|
||||
- Use appropriate cache invalidation strategy
|
||||
- Set TTL (time-to-live) for cache entries
|
||||
|
||||
**Lazy Loading:**
|
||||
|
||||
- Compute only when needed
|
||||
- Load data on-demand, not upfront
|
||||
- Defer expensive operations until required
|
||||
|
||||
**Batching:**
|
||||
|
||||
- Process multiple items together
|
||||
- Batch database queries (N queries → 1 query)
|
||||
- Batch API requests where possible
|
||||
|
||||
**Async I/O:**
|
||||
|
||||
- Don't block on I/O operations
|
||||
- Use async/await for concurrent I/O
|
||||
- Process multiple I/O operations in parallel
|
||||
|
||||
**Connection Pooling:**
|
||||
|
||||
- Reuse expensive resources (database connections, HTTP connections)
|
||||
- See Resource and Memory Management Principles @resources-and-memory-management-principles.md for pool sizing and configuration
|
||||
|
||||
### Performance Optimization Checklist
|
||||
|
||||
- [ ] Is there a measured performance problem (not a guess)?
|
||||
- [ ] Have you profiled to find the actual bottleneck?
|
||||
- [ ] Are appropriate data structures chosen for the access pattern?
|
||||
- [ ] Are expensive operations cached with proper invalidation?
|
||||
- [ ] Are batch operations used instead of N+1 queries?
|
||||
- [ ] Are I/O operations non-blocking where appropriate?
|
||||
- [ ] Have you measured improvement after optimization?
|
||||
|
||||
### Related Principles
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Flutter or React Native mobile application, setting up mobile project structure
|
||||
---
|
||||
|
||||
## Flutter/Mobile Layout
|
||||
|
||||
Use this structure for Flutter or React Native mobile applications. The vertical slice principle applies — features are self-contained modules.
|
||||
|
||||
```
|
||||
apps/
|
||||
mobile/ # Mobile application source code (Flutter)
|
||||
lib/
|
||||
core/ # Foundational concerns (the "Framework")
|
||||
network/ # HTTP client setup, interceptors
|
||||
api_client.dart # Dio/http client with base config
|
||||
api_interceptor.dart # Auth token, logging interceptors
|
||||
storage/ # Local storage abstractions
|
||||
local_storage.dart # SharedPreferences / Hive wrapper
|
||||
theme/ # App theming
|
||||
app_theme.dart # ThemeData, color schemes
|
||||
app_typography.dart # TextStyles, font families
|
||||
router/ # Navigation / routing
|
||||
app_router.dart # GoRouter config (@riverpod function)
|
||||
app_router.g.dart # Generated provider code
|
||||
constants/ # App-wide constants
|
||||
|
||||
features/ # Business Features (Vertical Slices)
|
||||
task/ # Task management feature
|
||||
# --- Presentation ---
|
||||
screens/
|
||||
task_list_screen.dart # Full screen (route target, ConsumerWidget)
|
||||
task_detail_screen.dart
|
||||
widgets/ # Feature-specific widgets
|
||||
task_card.dart
|
||||
task_form.dart
|
||||
|
||||
# --- State Management (Riverpod 3) ---
|
||||
state/
|
||||
task_notifier.dart # @riverpod class (Notifier/AsyncNotifier)
|
||||
task_notifier.g.dart # Generated provider code (build_runner)
|
||||
task_state.dart # State classes (generated via freezed)
|
||||
|
||||
# --- Domain (Business Logic) ---
|
||||
models/
|
||||
task.dart # Domain model (freezed)
|
||||
task.g.dart # Generated code (json_serializable)
|
||||
task.freezed.dart # Generated code (freezed)
|
||||
logic/
|
||||
task_logic.dart # Pure business rules
|
||||
|
||||
# --- Data (I/O Abstraction) ---
|
||||
repository/
|
||||
task_repository.dart # Abstract repository interface
|
||||
task_repository_impl.dart # Implementation (API + cache)
|
||||
task_repository_mock.dart # Mock for testing
|
||||
api/
|
||||
task_api.dart # REST API calls (Dio)
|
||||
auth/ # Authentication feature
|
||||
...
|
||||
settings/ # Settings feature
|
||||
...
|
||||
|
||||
shared/ # Shared across features
|
||||
widgets/ # Reusable UI components (NO domain logic)
|
||||
app_button.dart
|
||||
app_text_field.dart
|
||||
loading_indicator.dart
|
||||
utils/ # Pure utility functions
|
||||
date_formatter.dart
|
||||
validators.dart
|
||||
models/ # Shared data models
|
||||
api_response.dart
|
||||
pagination.dart
|
||||
|
||||
test/ # Test directory (mirrors lib/ layout)
|
||||
features/
|
||||
task/
|
||||
state/
|
||||
task_notifier_test.dart # Unit tests for state logic
|
||||
logic/
|
||||
task_logic_test.dart # Unit tests for pure business rules
|
||||
widgets/
|
||||
task_card_test.dart # Widget tests
|
||||
api/
|
||||
task_api_test.dart # API integration tests
|
||||
integration_test/ # Integration / E2E tests
|
||||
task_flow_test.dart
|
||||
|
||||
pubspec.yaml # Dependencies
|
||||
analysis_options.yaml # Lint rules (includes riverpod_lint)
|
||||
```
|
||||
|
||||
**Key differences from web frontend:**
|
||||
- `screens/` replaces `views/` — mobile uses screen-based navigation
|
||||
- `widgets/` replaces `components/` — Flutter's terminology
|
||||
- `state/` replaces `store/` — Riverpod 3 `@riverpod` class (Notifier/AsyncNotifier) instead of Pinia/Redux
|
||||
- `*.g.dart` files in `state/` — generated provider code from `riverpod_generator` + `build_runner`
|
||||
- `repository/` replaces `api/` — mobile often caches data locally
|
||||
- No separate `di/` directory — Riverpod 3 uses `ProviderScope` at the app root for dependency injection; no manual service locator needed
|
||||
|
||||
**Codegen artifacts (gitignore-able):**
|
||||
- `*.g.dart` — generated by `riverpod_generator` and `json_serializable`
|
||||
- `*.freezed.dart` — generated by `freezed`
|
||||
- Run `dart run build_runner build --delete-conflicting-outputs` after changes
|
||||
|
||||
**Test location:**
|
||||
|
||||
Tests live in the `test/` directory, mirroring the `lib/` layout. This is Flutter's default convention — `flutter test` discovers tests here automatically with no extra configuration.
|
||||
|
||||
- **Naming:** `*_test.dart` (unit), `*_integration_test.dart` (integration)
|
||||
- **Discovery:** `flutter test` (runs all tests in `test/`)
|
||||
- **Structure:** Mirror the feature directory structure from `lib/`
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Flutter Idioms and Patterns @flutter-idioms-and-patterns.md (Riverpod 3 patterns, widget idioms)
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Go backend project, setting up Go project structure, or organizing Go services and APIs
|
||||
---
|
||||
|
||||
## Go Backend Layout
|
||||
|
||||
Use this structure for Go backend applications. The vertical slice principle applies — features are packages, not layers.
|
||||
|
||||
```
|
||||
apps/
|
||||
backend/ # Backend application source code
|
||||
cmd/
|
||||
api/
|
||||
main.go # Entry point: Wires dependencies, router, starts server
|
||||
internal/ # Private application code
|
||||
platform/ # Foundational technical concerns (The "Framework")
|
||||
database/ # DB connection logic
|
||||
server/ # HTTP server setup (Router, Middleware)
|
||||
logger/ # Structured logging setup
|
||||
features/ # Business Features (Vertical Slices)
|
||||
task/ # Task management
|
||||
# --- Interface Definition ---
|
||||
service.go # The public API of this feature (Service struct)
|
||||
|
||||
# --- Delivery (HTTP) ---
|
||||
handler.go # HTTP Handlers
|
||||
handler_test.go # Component tests (httptest + mock service)
|
||||
|
||||
# --- Domain (Business Logic) ---
|
||||
logic.go # Core business logic methods
|
||||
logic_test.go # Unit tests (Pure functions + mock storage)
|
||||
models.go # Domain structs (Task, NewTaskRequest)
|
||||
errors.go # Feature-specific errors
|
||||
|
||||
# --- Storage (Data Access) ---
|
||||
storage.go # Storage Interface definition
|
||||
storage_pg.go # Postgres implementation
|
||||
postgres_integration_test.go # Integration tests (Real DB/Testcontainers)
|
||||
storage_mock.go # Mock implementation
|
||||
...
|
||||
order/ # Order management
|
||||
handler.go
|
||||
logic.go
|
||||
storage.go
|
||||
...
|
||||
```
|
||||
|
||||
**Key Go conventions:**
|
||||
- `cmd/` for entry points — each subdirectory is a separate binary
|
||||
- `internal/` for private packages — enforced by Go compiler (cannot be imported externally)
|
||||
- `platform/` for foundational concerns (database, server, logger)
|
||||
- `features/` for vertical business slices — each feature is self-contained
|
||||
- Tests live alongside the code they test (`_test.go` suffix)
|
||||
- `go.mod` at the root — single module for the entire application
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Go Idioms and Patterns @go-idioms-and-patterns.md (coding idioms, error handling, naming)
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Python backend project, setting up Python project structure, or organizing Python services and APIs
|
||||
---
|
||||
|
||||
## Python Backend Layout
|
||||
|
||||
Use this structure for Python backend applications. The vertical slice principle applies — features are packages, not technical layers.
|
||||
|
||||
```
|
||||
apps/
|
||||
backend/ # Backend application source code
|
||||
pyproject.toml # Project metadata, dependencies, tool configs
|
||||
src/
|
||||
yourapp/ # Importable package (src-layout, preferred)
|
||||
main.py # Entry point: creates app, wires dependencies, starts server
|
||||
platform/ # Foundational technical concerns (the "Framework")
|
||||
database.py # DB engine and session factory
|
||||
server.py # ASGI app setup (FastAPI/Starlette router, middleware)
|
||||
logger.py # structlog / logging configuration
|
||||
config.py # Settings (pydantic-settings BaseSettings)
|
||||
features/ # Business Features (Vertical Slices)
|
||||
task/ # Task management
|
||||
__init__.py
|
||||
|
||||
# --- Public API ---
|
||||
service.py # TaskService class (public API of this feature)
|
||||
|
||||
# --- Delivery (HTTP) ---
|
||||
router.py # FastAPI APIRouter with HTTP endpoints
|
||||
schemas.py # Pydantic request/response models (API boundary only)
|
||||
test_router.py # Component tests (TestClient + mock service)
|
||||
|
||||
# --- Domain (Business Logic) ---
|
||||
logic.py # Pure domain functions (no I/O)
|
||||
models.py # Domain dataclasses (Task, Priority, etc.)
|
||||
errors.py # Feature-specific exceptions
|
||||
test_logic.py # Unit tests (pure functions — no mocks needed)
|
||||
|
||||
# --- Storage (Data Access) ---
|
||||
storage.py # TaskStorage Protocol (interface)
|
||||
storage_pg.py # PostgreSQL implementation (asyncpg / SQLAlchemy)
|
||||
storage_mock.py # InMemoryTaskStorage (test implementation)
|
||||
test_storage_pg.py # Integration tests (real DB via testcontainers)
|
||||
|
||||
order/ # Order management
|
||||
service.py
|
||||
router.py
|
||||
schemas.py
|
||||
logic.py
|
||||
models.py
|
||||
storage.py
|
||||
storage_pg.py
|
||||
storage_mock.py
|
||||
...
|
||||
tests/ # Optional: top-level E2E tests (cross-feature boundaries)
|
||||
e2e/
|
||||
test_create_task_api.e2e.py
|
||||
```
|
||||
|
||||
**Key Python conventions:**
|
||||
|
||||
- **`src/` layout** is strongly preferred — prevents accidental imports of the development tree and matches packaging best practices (PEP 517 / `pypa/build`)
|
||||
- **`pyproject.toml`** is the single configuration file for `ruff`, `mypy`, `pytest`, `bandit`, and packaging metadata — do not create `setup.cfg`, `.flake8`, or `mypy.ini` files
|
||||
- **Feature packages** use `__init__.py` — keep it empty or use it solely to re-export the feature's public API
|
||||
- **`platform/`** holds technical infrastructure that features depend on (database sessions, configuration, logging); features never import each other's `platform/` code directly
|
||||
- **Tests co-locate** with the code they test (`test_*.py` in the same directory) except for E2E tests which go in `tests/e2e/`
|
||||
- **`storage_mock.py`** is a production-quality in-memory implementation, not a `unittest.Mock` — it is the recommended test double for unit and integration tests
|
||||
|
||||
### Dependency Wiring (main.py)
|
||||
|
||||
```python
|
||||
# src/yourapp/main.py
|
||||
from yourapp.platform.database import create_engine
|
||||
from yourapp.platform.server import create_app
|
||||
from yourapp.features.task.storage_pg import PostgresTaskStorage
|
||||
from yourapp.features.task.service import TaskService
|
||||
from yourapp.features.task.router import build_router as build_task_router
|
||||
|
||||
def create_application() -> FastAPI:
|
||||
engine = create_engine()
|
||||
task_storage = PostgresTaskStorage(engine=engine)
|
||||
task_service = TaskService(storage=task_storage)
|
||||
|
||||
app = create_app()
|
||||
app.include_router(build_task_router(service=task_service))
|
||||
return app
|
||||
|
||||
app = create_application()
|
||||
```
|
||||
|
||||
### Configuration (platform/config.py)
|
||||
|
||||
```python
|
||||
# src/yourapp/platform/config.py
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class Settings(BaseSettings):
|
||||
database_url: str
|
||||
secret_key: str
|
||||
debug: bool = False
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
```
|
||||
|
||||
### pyproject.toml Baseline
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "yourapp"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/yourapp"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "UP", "S", "B", "ANN"]
|
||||
|
||||
[tool.mypy]
|
||||
strict = true
|
||||
python_version = "3.11"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["src"]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.bandit]
|
||||
skips = ["B101"] # assert statements allowed in test files
|
||||
```
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Python Idioms and Patterns @python-idioms-and-patterns.md (coding idioms, error handling, naming)
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Rust or Cargo project, setting up Rust project structure, or organizing Rust crates and workspaces
|
||||
---
|
||||
|
||||
## Rust/Cargo Layout
|
||||
|
||||
Use these structures for Rust projects. The vertical slice principle applies — features are modules, not layers.
|
||||
|
||||
### Single Binary Crate
|
||||
|
||||
For CLI tools, standalone servers, and single-purpose applications.
|
||||
|
||||
```
|
||||
project-root/
|
||||
Cargo.toml # Package manifest + dependencies
|
||||
build.rs # Build script (optional — for FFI, codegen)
|
||||
src/
|
||||
main.rs # Entry point: CLI parsing, server startup, wires dependencies
|
||||
lib.rs # Library root — re-exports public modules (enables integration testing)
|
||||
|
||||
# --- Foundational Concerns (The "Platform") ---
|
||||
config.rs # Configuration loading (env vars, config files)
|
||||
error.rs # Application-wide error types (thiserror enum)
|
||||
telemetry.rs # Tracing/logging setup (tracing, tracing-subscriber)
|
||||
|
||||
# --- Business Features (Vertical Slices) ---
|
||||
features/
|
||||
mod.rs # Re-exports feature modules
|
||||
task/
|
||||
mod.rs # Public API of this feature — re-exports types + service
|
||||
service.rs # Feature service (business logic orchestration)
|
||||
logic.rs # Pure business rules (no I/O, easily testable)
|
||||
models.rs # Domain structs (Task, NewTaskRequest)
|
||||
error.rs # Feature-specific error types
|
||||
repository.rs # Storage trait definition
|
||||
postgres.rs # Postgres implementation of repository trait
|
||||
mock.rs # Mock implementation for testing
|
||||
auth/
|
||||
mod.rs
|
||||
service.rs
|
||||
logic.rs
|
||||
models.rs
|
||||
...
|
||||
|
||||
# --- Delivery Layer (HTTP/gRPC) ---
|
||||
handlers/
|
||||
mod.rs
|
||||
task_handler.rs # HTTP handlers for task feature (axum/actix extractors)
|
||||
auth_handler.rs
|
||||
router.rs # Route definitions, middleware stack
|
||||
|
||||
tests/ # Integration tests (compiled as separate crate, access only pub API)
|
||||
common/
|
||||
mod.rs # Shared test fixtures, helpers, test database setup
|
||||
task_api_test.rs # Full API integration tests
|
||||
auth_flow_test.rs
|
||||
|
||||
benches/ # Benchmarks (criterion)
|
||||
task_benchmark.rs
|
||||
```
|
||||
|
||||
**Key Rust conventions:**
|
||||
- `src/lib.rs` + `src/main.rs` — enables integration testing against `lib.rs` public API
|
||||
- `mod.rs` re-exports — each feature's `mod.rs` is its public boundary
|
||||
- `error.rs` per feature — typed errors with `thiserror`, composed at app level with `#[from]`
|
||||
- `tests/` directory — integration tests see only the public API, enforcing encapsulation
|
||||
- No `controllers/` or `services/` at the top level — features are the top-level organization
|
||||
|
||||
---
|
||||
|
||||
### Library Crate
|
||||
|
||||
For publishable libraries, SDKs, and reusable components.
|
||||
|
||||
```
|
||||
project-root/
|
||||
Cargo.toml # Package manifest (lib target)
|
||||
src/
|
||||
lib.rs # Crate root — public API surface, module re-exports
|
||||
parser.rs # Public module
|
||||
ast.rs # Public module
|
||||
error.rs # Public error types (thiserror)
|
||||
internal/ # Private implementation details
|
||||
mod.rs
|
||||
optimizer.rs
|
||||
cache.rs
|
||||
|
||||
examples/ # Runnable examples (cargo run --example)
|
||||
basic_usage.rs
|
||||
advanced_config.rs
|
||||
|
||||
tests/ # Integration tests
|
||||
parsing_test.rs
|
||||
|
||||
benches/ # Benchmarks
|
||||
parser_benchmark.rs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cargo Workspace (Multi-Crate)
|
||||
|
||||
For complex projects with multiple internal crates. This is the recommended structure for projects where distinct subsystems benefit from separate compilation units and explicit dependency boundaries.
|
||||
|
||||
```
|
||||
project-root/
|
||||
Cargo.toml # [workspace] manifest — lists all members
|
||||
Cargo.lock # Locked dependency versions (committed for binaries)
|
||||
|
||||
crates/
|
||||
myapp/ # Main binary crate (entry point, wiring)
|
||||
Cargo.toml # Depends on other workspace crates
|
||||
src/
|
||||
main.rs # CLI parsing, server setup
|
||||
lib.rs
|
||||
config.rs
|
||||
error.rs
|
||||
api/ # API / protocol handling
|
||||
mod.rs
|
||||
routes.rs # Route definitions
|
||||
handlers.rs # Request handlers
|
||||
|
||||
myapp-parser/ # Domain-specific engine crate
|
||||
Cargo.toml
|
||||
build.rs # Build script (optional — for FFI, codegen)
|
||||
src/
|
||||
lib.rs
|
||||
parser.rs # Core parsing logic
|
||||
transform.rs # Data transformations
|
||||
cache.rs # Caching layer
|
||||
tests/ # Per-crate integration tests (public API only)
|
||||
parser_integration.rs
|
||||
|
||||
myapp-client/ # External service client crate
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs
|
||||
client.rs # Client implementation
|
||||
lifecycle.rs # Connection management
|
||||
retry.rs # Retry and resilience logic
|
||||
|
||||
myapp-search/ # Search / query engine crate
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs
|
||||
search.rs # Search implementation
|
||||
filter.rs # Result filtering
|
||||
tests/ # Per-crate integration tests
|
||||
search_integration.rs
|
||||
|
||||
myapp-common/ # Shared types and utilities
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs
|
||||
types.rs # Shared domain types
|
||||
error.rs # Common error types
|
||||
tests/ # Per-crate integration tests
|
||||
common_integration.rs
|
||||
|
||||
tests/ # Workspace-level integration tests
|
||||
integration/
|
||||
full_pipeline_test.rs # End-to-end tests across crates
|
||||
|
||||
config/ # Default configuration
|
||||
myapp.config.default.json
|
||||
```
|
||||
|
||||
**Key differences from Go/Node layouts:**
|
||||
- `crates/` replaces `apps/` — Cargo workspace with explicit inter-crate dependencies in `Cargo.toml`
|
||||
- Each crate has its own `Cargo.toml` — dependencies are scoped, compile separately, enforces boundaries
|
||||
- `build.rs` — Rust-specific build script for FFI compilation (C grammars for tree-sitter)
|
||||
- `Cargo.lock` committed — standard practice for binary projects (not for library crates)
|
||||
- No `node_modules/`, `vendor/` — dependencies are managed by Cargo globally in `~/.cargo/`
|
||||
- Feature flags in `Cargo.toml` — use `[features]` for optional functionality instead of build-time env vars
|
||||
|
||||
### Test Organization in Rust
|
||||
|
||||
Rust has a compiler-enforced two-tier test system that differs fundamentally from Go/TS:
|
||||
|
||||
**Unit Tests — Inline (Not Separate Files)**
|
||||
- Convention: `#[cfg(test)] mod tests` block **at the bottom of each `.rs` file**
|
||||
- Tests are compiled conditionally — stripped from production builds
|
||||
- Can access **private** functions via `use super::*`
|
||||
- This is NOT a shortcut — it is the official, idiomatic Rust convention
|
||||
- Do NOT create separate `*_test.rs` files for unit tests
|
||||
|
||||
**Integration Tests — `tests/` Directory**
|
||||
- Location: `tests/` directory **at each crate's root** (next to `src/`)
|
||||
- Each `.rs` file in `tests/` is compiled as its own **separate crate**
|
||||
- Can only access the crate's **public API** (`use my_crate::...`)
|
||||
- No `#[cfg(test)]` needed — Cargo treats `tests/` as test-only automatically
|
||||
- Shared test helpers go in `tests/common/mod.rs` (not `tests/common.rs`)
|
||||
|
||||
**Workspace-level tests:**
|
||||
- For cross-crate integration / E2E tests, place in `tests/` at the workspace root
|
||||
- Or create a dedicated test crate in the workspace (e.g. `tests-integration/` — tokio pattern)
|
||||
|
||||
**Example per-crate layout with tests:**
|
||||
```
|
||||
crates/pathfinder-search/
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs # Contains #[cfg(test)] mod tests { ... } at bottom
|
||||
search.rs # Contains #[cfg(test)] mod tests { ... } at bottom
|
||||
filter.rs # Contains #[cfg(test)] mod tests { ... } at bottom
|
||||
mock.rs # Mock implementations
|
||||
tests/ # Integration tests (separate crate, public API only)
|
||||
search_integration.rs # Tests search through public lib.rs API
|
||||
common/
|
||||
mod.rs # Shared test fixtures and helpers
|
||||
```
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Rust Idioms and Patterns @rust-idioms-and-patterns.md
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a web frontend project using Vue, React, Svelte, or similar frameworks, setting up frontend project structure
|
||||
---
|
||||
|
||||
## Vue/React Frontend Layout
|
||||
|
||||
Use this structure for web frontend applications. The vertical slice principle applies — features are self-contained modules, not scattered across global folders.
|
||||
|
||||
```
|
||||
apps/
|
||||
frontend/ # Frontend application source code
|
||||
src/
|
||||
assets/ # Fonts, Images
|
||||
features/ # Business features organized as vertical slices. Each feature is SELF-CONTAINED.
|
||||
task/ # Task management
|
||||
components/ # Task Feature-specific components go HERE, DON'T Put feature components in top-level folders
|
||||
TaskForm.vue
|
||||
TaskListItem.vue
|
||||
TaskFilters.vue
|
||||
TaskInput.vue
|
||||
TaskInput.spec.ts # Component unit tests
|
||||
store/
|
||||
task.store.ts # Pinia store
|
||||
task.store.spec.ts # Store unit tests
|
||||
api/
|
||||
task.api.ts # interface TaskAPI
|
||||
task.api.backend.ts # Production implementation
|
||||
task.api.mock.ts # Test implementation
|
||||
services/
|
||||
task.service.ts # Business logic
|
||||
task.service.spec.ts # Logic unit tests
|
||||
types/ # TS Interfaces for tasks (e.g. CreateTaskDTO interfaces)
|
||||
composables/ # Task Feature-specific hooks (e.g. useTaskFilters.ts)
|
||||
index.ts # Public exports. Export ONLY what's needed by `views/`
|
||||
order/
|
||||
composables/ # Global reactive logic (useAuth, useTheme)
|
||||
components/ # Shared Component (Buttons, Inputs) - Dumb UI, No Domain Logic. DON'T Put feature components HERE
|
||||
ui/ # UI Components (Atoms & Molecules) Pure, reusable UI primitives. NO domain logic, NO feature knowledge.
|
||||
BaseButton.vue
|
||||
BaseButton.spec.ts # Unit tests for button states
|
||||
types.ts # Shared UI types/interfaces
|
||||
index.ts # Barrel export for easy imports
|
||||
layout/ # Layout Components (Organisms) Composite UI structures that combine multiple UI components. Still reusable, but more complex.
|
||||
AppHeader.vue # Application header with nav, logo, user menu
|
||||
AppSidebar.vue # Sidebar navigation structure
|
||||
ErrorBoundary.vue # Error display wrapper
|
||||
EmptyState.vue # Empty list placeholder
|
||||
layouts/ # App shells (Sidebar, Navbar wrappers)
|
||||
MainLayout.vue # Contains Navbar, Sidebar, Footer
|
||||
AuthLayout.vue # Minimal layout for Login/Register
|
||||
views/ # Route entry points (The "Glue")
|
||||
HomeView.vue # Imports from features/analytics
|
||||
TaskView.vue # Imports from features/task
|
||||
utils/ # Pure, stateless helper functions. No domain knowledge, no Vue reactivity, (e.g. date-fns wrappers, math).
|
||||
router/ # Route definitions
|
||||
plugins/ # Library configs (Axios, I18n)
|
||||
App.vue # Root component (hosts <router-view>)
|
||||
main.ts # Entry point (bootstraps plugins & mounts app)
|
||||
...
|
||||
```
|
||||
|
||||
**Key frontend conventions:**
|
||||
- `features/` for vertical slices — each feature exports only what `views/` needs via `index.ts`
|
||||
- `components/ui/` for **shared** dumb UI (buttons, inputs) — NO domain logic, NO feature knowledge
|
||||
- `components/layout/` for composite UI structures (headers, sidebars)
|
||||
- `views/` are route entry points — they compose features, not implement them
|
||||
- Feature components live **inside** the feature, NOT in top-level `components/`
|
||||
- `composables/` at root for global reactive logic; feature-specific hooks inside the feature
|
||||
|
||||
> This structure applies equally to React (.tsx), Vue (.vue), and Svelte (.svelte). Replace component file extensions and state management (Redux/Zustand for React, Pinia for Vue) as needed.
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Vue Idioms and Patterns @vue-idioms-and-patterns.md (Composition API, Pinia, composables)
|
||||
- TypeScript Idioms and Patterns @typescript-idioms-and-patterns.md (type system, async patterns)
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
> **This file is the SINGLE SOURCE OF TRUTH for project organization.**
|
||||
> All other rules and workflows that reference paths should defer to this file.
|
||||
> To adapt the setup for a different project type, edit this file only.
|
||||
|
||||
## Project Structure
|
||||
|
||||
**Project Structure Philosophy:**
|
||||
|
||||
- **Organize by FEATURE, not by technical layer**
|
||||
- Each feature is a vertical slice
|
||||
- Enables modular growth, clear boundaries, and independent deployability
|
||||
|
||||
**Universal Rule: Context → Feature → Layer**
|
||||
|
||||
**1. Level 1: Repository Scope:** Root contains `apps/` grouping distinct applications (e.g., `apps/backend`, `apps/frontend`, `apps/mobile`).
|
||||
|
||||
**2. Level 2: Feature Organization**
|
||||
- **Rule:** Divide application into vertical business slices (e.g., `user/`, `order/`, `payment/`).
|
||||
- **Anti-Pattern:** Do NOT organize by technical layer (e.g., `controllers/`, `models/`, `services/`) at the top level.
|
||||
|
||||
### Language-Specific Layouts
|
||||
|
||||
Each layout follows the universal philosophy above. Load the relevant layout when working with a specific language or framework:
|
||||
|
||||
| Layout | File | When to Use |
|
||||
| ------------------ | ------------------------------------ | ------------------------------------ |
|
||||
| Go Backend | @project-structure-go-backend.md | Go services, APIs, CLI tools |
|
||||
| Vue/React Frontend | @project-structure-vue-frontend.md | Web frontends (Vue, React, Svelte) |
|
||||
| Flutter/Mobile | @project-structure-flutter-mobile.md | Mobile apps (Flutter, React Native) |
|
||||
| Rust/Cargo | @project-structure-rust-cargo.md | Rust binaries, libraries, workspaces |
|
||||
|
||||
> This Feature/Domain/UI/API structure is framework-agnostic. It applies equally to any language or framework. The layout files provide language-specific conventions (file naming, module systems, test locations) while preserving the vertical slice architecture.
|
||||
|
||||
### Adapting for Different Project Types
|
||||
|
||||
| Project Type | What to Change |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **Monorepo** (default) | Use as-is — `apps/backend/`, `apps/frontend/`, `apps/mobile/` |
|
||||
| **Single backend** | Flatten to root: `cmd/`, `internal/` (Go) or `src/` (Rust) at project root |
|
||||
| **Single frontend** | Flatten to root: `src/` at project root (no `apps/` wrapper) |
|
||||
| **Single mobile** | Flatten to root: `lib/` at project root (no `apps/` wrapper) |
|
||||
| **Single Rust binary** | Flatten to root: `src/`, `tests/`, `benches/` at project root with `Cargo.toml` |
|
||||
| **Rust library** | Flatten to root: `src/`, `examples/`, `tests/` at project root with `Cargo.toml` (lib target) |
|
||||
| **Rust workspace** | `crates/` at root with workspace `Cargo.toml` — each crate follows single-crate layout internally |
|
||||
| **Microservices** | One directory per service under `apps/` (each with own `go.mod`/`Cargo.toml`/`Dockerfile`) |
|
||||
| **Full-stack + mobile** | Use all relevant layout files under `apps/` |
|
||||
|
||||
**Single-app projects** don't need the `apps/` directory — put the language-specific root directories directly at the project root. The internal structure (features, platform, etc.) stays the same.
|
||||
|
||||
**Multiple entry points (CLI, workers, etc.):**
|
||||
|
||||
Go:
|
||||
```
|
||||
cmd/
|
||||
api/main.go # HTTP server entry point
|
||||
cli/main.go # CLI tool entry point
|
||||
worker/main.go # Background worker entry point
|
||||
```
|
||||
|
||||
Rust:
|
||||
```
|
||||
src/
|
||||
bin/
|
||||
api.rs # HTTP server entry point
|
||||
cli.rs # CLI tool entry point
|
||||
worker.rs # Background worker entry point
|
||||
lib.rs # Shared library code
|
||||
```
|
||||
|
||||
**Microservices notes:**
|
||||
- Each service is its own directory under `apps/` with its own `go.mod`/`Cargo.toml` and `Dockerfile`
|
||||
- Each service follows the same layout internally (see language-specific files)
|
||||
- Add `shared/` at root for cross-service contracts (protobuf, shared types) — keep this minimal
|
||||
- Services communicate via API calls or message queues, never direct imports
|
||||
|
||||
### Related Principles
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Python code, reviewing Python idioms, or working on any Python project (backend, CLI, or data pipeline)
|
||||
---
|
||||
|
||||
## Python Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Python rewards explicitness and readability over cleverness. Follow the **Zen of Python** (`import this`) — beautiful code is not a luxury, it's a professional necessity. If it reads like plain English, it's probably idiomatic Python.
|
||||
|
||||
> **Scope:** This file covers Python-specific *coding idioms*. For file layout, see `project-structure-python-backend.md`. For test naming conventions, see `testing-strategy.md`. For logging library choice, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### Type Hints — Non-Negotiable
|
||||
|
||||
**Always annotate function signatures and public APIs.** Use `from __future__ import annotations` for forward references.
|
||||
|
||||
```python
|
||||
# ✅ Fully annotated — self-documenting and mypy-verifiable
|
||||
from __future__ import annotations
|
||||
from collections.abc import Sequence
|
||||
|
||||
def calculate_discount(items: Sequence[Item], coupon: Coupon) -> float: ...
|
||||
|
||||
# ❌ Untyped — opaque to both mypy and the next developer
|
||||
def calculate_discount(items, coupon): ...
|
||||
```
|
||||
|
||||
1. **Use `X | None` over `Optional[X]`** (Python 3.10+)
|
||||
```python
|
||||
# ✅ Modern union syntax
|
||||
def find_user(user_id: str) -> User | None: ...
|
||||
|
||||
# ❌ Verbose legacy form (still valid, but prefer the above)
|
||||
from typing import Optional
|
||||
def find_user(user_id: str) -> Optional[User]: ...
|
||||
```
|
||||
|
||||
2. **Use `TypeAlias` and `TypeVar` for reusable generics**
|
||||
```python
|
||||
from typing import TypeVar, TypeAlias
|
||||
|
||||
T = TypeVar("T")
|
||||
UserId: TypeAlias = str
|
||||
```
|
||||
|
||||
3. **Use `Protocol` for structural interfaces instead of ABCs when duck-typing is sufficient**
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class TaskStorage(Protocol):
|
||||
def get_by_id(self, task_id: str) -> Task: ...
|
||||
def save(self, task: Task) -> None: ...
|
||||
```
|
||||
|
||||
4. **`TypedDict` for structured dicts crossing system boundaries (JSON, configs)**
|
||||
```python
|
||||
from typing import TypedDict
|
||||
|
||||
class CreateTaskRequest(TypedDict):
|
||||
title: str
|
||||
priority: Literal["low", "medium", "high"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Handling
|
||||
|
||||
> For general error handling principles, see `error-handling-principles.md`. This section covers Python-specific idioms.
|
||||
|
||||
1. **Prefer specific exception types over broad `except Exception`**
|
||||
```python
|
||||
# ✅ Precise — catches exactly what you expect
|
||||
try:
|
||||
task = storage.get_by_id(task_id)
|
||||
except TaskNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
# ❌ Broad — may swallow programming errors
|
||||
try:
|
||||
task = storage.get_by_id(task_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Error")
|
||||
```
|
||||
|
||||
2. **Define domain-specific exception hierarchies**
|
||||
```python
|
||||
class FathError(Exception):
|
||||
"""Base exception for all domain errors."""
|
||||
|
||||
class NotFoundError(FathError):
|
||||
def __init__(self, resource: str, resource_id: str) -> None:
|
||||
self.resource = resource
|
||||
self.resource_id = resource_id
|
||||
super().__init__(f"{resource} '{resource_id}' not found")
|
||||
|
||||
class ValidationError(FathError):
|
||||
def __init__(self, field: str, message: str) -> None:
|
||||
self.field = field
|
||||
self.message = message
|
||||
super().__init__(f"Validation failed on '{field}': {message}")
|
||||
```
|
||||
|
||||
3. **Never silence exceptions** — if an exception is caught and not re-raised, log it explicitly
|
||||
```python
|
||||
# ❌ Silent swallow
|
||||
try:
|
||||
notify_user(user_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ✅ Explicit intent + observability
|
||||
try:
|
||||
notify_user(user_id)
|
||||
except NotificationError:
|
||||
logger.warning("notification_failed", user_id=user_id, exc_info=True)
|
||||
```
|
||||
|
||||
4. **Use `contextlib.suppress` only for truly expected, inconsequential exceptions**
|
||||
```python
|
||||
from contextlib import suppress
|
||||
|
||||
with suppress(FileNotFoundError):
|
||||
cache_path.unlink() # OK — cleanup, not business logic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Dataclasses and Pydantic
|
||||
|
||||
1. **Use `dataclasses` for internal domain models** (no I/O, no validation)
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass(frozen=True) # frozen = immutable value object
|
||||
class Task:
|
||||
id: str
|
||||
title: str
|
||||
priority: str
|
||||
tags: tuple[str, ...] = field(default_factory=tuple)
|
||||
```
|
||||
|
||||
2. **Use Pydantic `BaseModel` for data crossing system boundaries** (API requests/responses, config)
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class CreateTaskRequest(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=200)
|
||||
priority: Literal["low", "medium", "high"] = "medium"
|
||||
due_date: datetime | None = None
|
||||
|
||||
model_config = ConfigDict(frozen=True) # Pydantic v2
|
||||
```
|
||||
|
||||
3. **Keep domain models separate from API schemas** — never use a Pydantic model as a domain entity
|
||||
```
|
||||
models.py → dataclasses (pure domain)
|
||||
schemas.py → Pydantic models (API boundary)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Interfaces and Dependency Injection
|
||||
|
||||
Python uses Protocols and constructor injection to achieve the same testability goal as Go interfaces.
|
||||
|
||||
1. **Define the Protocol where it is *used*, not where it is *implemented***
|
||||
```python
|
||||
# task/storage.py ← defined in the consumer feature
|
||||
from typing import Protocol
|
||||
|
||||
class TaskStorage(Protocol):
|
||||
def get_by_id(self, task_id: str) -> Task: ...
|
||||
def save(self, task: Task) -> None: ...
|
||||
def delete(self, task_id: str) -> None: ...
|
||||
```
|
||||
|
||||
2. **Inject dependencies through `__init__`** — never instantiate concrete dependencies inside a class
|
||||
```python
|
||||
# ✅ Testable — storage is injected
|
||||
class TaskService:
|
||||
def __init__(self, storage: TaskStorage) -> None:
|
||||
self._storage = storage
|
||||
|
||||
# ❌ Not testable — concrete dependency hardwired
|
||||
class TaskService:
|
||||
def __init__(self) -> None:
|
||||
self._storage = PostgresTaskStorage()
|
||||
```
|
||||
|
||||
3. **Wire dependencies in the entry point** (e.g., `main.py`, `app.py`, or DI container)
|
||||
```python
|
||||
# app/main.py
|
||||
storage = PostgresTaskStorage(db=database)
|
||||
service = TaskService(storage=storage)
|
||||
router.include_router(build_task_router(service))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Async / Await
|
||||
|
||||
> For general async principles (when to add concurrency), see `concurrency-and-threading-mandate.md`. This section covers Python-specific async idioms.
|
||||
|
||||
1. **Choose one async paradigm and stay consistent** — do not mix `asyncio.run` entry points
|
||||
```python
|
||||
# ✅ Fully async service layer
|
||||
async def get_task(self, task_id: str) -> Task:
|
||||
return await self._storage.get_by_id(task_id)
|
||||
```
|
||||
|
||||
2. **Never call blocking I/O directly in an async function**
|
||||
```python
|
||||
# ❌ Blocks the event loop
|
||||
async def load_file(path: str) -> str:
|
||||
return open(path).read()
|
||||
|
||||
# ✅ Use async I/O or run in executor
|
||||
import asyncio, aiofiles
|
||||
|
||||
async def load_file(path: str) -> str:
|
||||
async with aiofiles.open(path) as f:
|
||||
return await f.read()
|
||||
```
|
||||
|
||||
3. **Use `asyncio.gather` for concurrent independent operations**
|
||||
```python
|
||||
# ✅ Concurrent fan-out
|
||||
user, tasks = await asyncio.gather(
|
||||
get_user(user_id),
|
||||
get_tasks(user_id),
|
||||
)
|
||||
```
|
||||
|
||||
4. **Use `asyncio.TaskGroup` (Python 3.11+) for structured concurrency with cancellation safety**
|
||||
```python
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
user_task = tg.create_task(get_user(user_id))
|
||||
tasks_task = tg.create_task(get_tasks(user_id))
|
||||
user = user_task.result()
|
||||
tasks = tasks_task.result()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
Follow **PEP 8** rigorously. No exceptions.
|
||||
|
||||
| Construct | Convention | Example |
|
||||
| ----------------------- | ------------------ | ---------------------------- |
|
||||
| Module / Package | `snake_case` | `task_service.py` |
|
||||
| Class | `PascalCase` | `TaskService` |
|
||||
| Function / Method | `snake_case` | `get_by_id` |
|
||||
| Private method/attr | `_snake_case` | `_validate_title` |
|
||||
| Constant | `UPPER_SNAKE_CASE` | `MAX_TITLE_LENGTH = 200` |
|
||||
| Type alias | `PascalCase` | `UserId = str` |
|
||||
| Protocol / Interface | `PascalCase` | `TaskStorage` |
|
||||
|
||||
1. **Never use single-letter names outside list comprehensions or math** — names must be descriptive
|
||||
2. **Avoid `data`, `info`, `obj`, `result` as standalone names** — describe the *domain concept*
|
||||
3. **Boolean variables and functions should read as yes/no questions**
|
||||
```python
|
||||
# ✅
|
||||
is_active: bool
|
||||
has_permission: bool
|
||||
def can_edit(user: User, task: Task) -> bool: ...
|
||||
|
||||
# ❌
|
||||
active: bool
|
||||
permission: bool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Idiomatic Patterns
|
||||
|
||||
1. **Context managers for resource cleanup** — always prefer `with` over manual `close()`
|
||||
```python
|
||||
# ✅
|
||||
async with database.transaction() as tx:
|
||||
await tx.execute(query)
|
||||
|
||||
# ❌
|
||||
tx = database.begin()
|
||||
tx.execute(query)
|
||||
tx.commit() # easily forgotten on exception
|
||||
```
|
||||
|
||||
2. **Generator expressions over list comprehensions for lazy evaluation**
|
||||
```python
|
||||
# ✅ Lazy — does not materialise the entire list
|
||||
active_ids = (task.id for task in tasks if task.is_active)
|
||||
|
||||
# Use list comprehension only when you need the full list
|
||||
active_tasks = [task for task in tasks if task.is_active]
|
||||
```
|
||||
|
||||
3. **`dataclasses.replace()` for immutable updates** (preferred over mutating frozen dataclasses)
|
||||
```python
|
||||
from dataclasses import replace
|
||||
|
||||
updated_task = replace(task, title="New Title")
|
||||
```
|
||||
|
||||
4. **`functools.cache` / `functools.lru_cache` for pure function memoization**
|
||||
```python
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def get_config() -> AppConfig: # called once; result reused
|
||||
return _load_config_from_env()
|
||||
```
|
||||
|
||||
5. **`__slots__` on hot-path, frequently instantiated classes**
|
||||
```python
|
||||
@dataclass
|
||||
class Vector:
|
||||
__slots__ = ("x", "y")
|
||||
x: float
|
||||
y: float
|
||||
```
|
||||
|
||||
6. **`enum.Enum` (not raw strings) for domain-level constants**
|
||||
```python
|
||||
from enum import StrEnum # Python 3.11+
|
||||
|
||||
class Priority(StrEnum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test file naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Python-specific tooling only.
|
||||
|
||||
1. **Use `pytest` as the sole test runner** — never mix with `unittest.TestCase` classes
|
||||
```python
|
||||
# ✅ Idiomatic pytest
|
||||
def test_calculate_discount_returns_zero_for_no_items() -> None:
|
||||
result = calculate_discount(items=[], coupon=Coupon(code="SAVE10"))
|
||||
assert result == 0.0
|
||||
```
|
||||
|
||||
2. **Parametrize test cases with `@pytest.mark.parametrize`**
|
||||
```python
|
||||
@pytest.mark.parametrize("priority,expected_score", [
|
||||
("low", 1),
|
||||
("medium", 5),
|
||||
("high", 10),
|
||||
])
|
||||
def test_priority_score(priority: str, expected_score: int) -> None:
|
||||
assert priority_score(priority) == expected_score
|
||||
```
|
||||
|
||||
3. **Use `pytest-mock` (`mocker` fixture) for mocking — not `unittest.mock` directly**
|
||||
```python
|
||||
def test_task_service_creates_task(mocker: MockerFixture) -> None:
|
||||
mock_storage = mocker.create_autospec(TaskStorage, instance=True)
|
||||
service = TaskService(storage=mock_storage)
|
||||
|
||||
service.create(title="Test", priority="high")
|
||||
|
||||
mock_storage.save.assert_called_once()
|
||||
```
|
||||
|
||||
4. **Use a typed mock factory for Protocol-based interfaces**
|
||||
```python
|
||||
# storage_mock.py ← co-locate with storage.py
|
||||
class InMemoryTaskStorage:
|
||||
"""In-memory TaskStorage implementation for unit tests."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[str, Task] = {}
|
||||
|
||||
def get_by_id(self, task_id: str) -> Task:
|
||||
if task_id not in self._store:
|
||||
raise NotFoundError("Task", task_id)
|
||||
return self._store[task_id]
|
||||
|
||||
def save(self, task: Task) -> None:
|
||||
self._store[task.id] = task
|
||||
|
||||
def delete(self, task_id: str) -> None:
|
||||
self._store.pop(task_id, None)
|
||||
```
|
||||
|
||||
5. **Use `pytest-asyncio` for async tests** — mark the whole module or use `asyncio_mode = "auto"` in `pyproject.toml`
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_create_task() -> None:
|
||||
service = TaskService(storage=InMemoryTaskStorage())
|
||||
task = await service.create(title="Async Task", priority="low")
|
||||
assert task.title == "Async Task"
|
||||
```
|
||||
|
||||
6. **Fixtures for reusable setup** — never repeat identical `Arrange` blocks across tests
|
||||
```python
|
||||
@pytest.fixture
|
||||
def task_service() -> TaskService:
|
||||
return TaskService(storage=InMemoryTaskStorage())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Formatting and Static Analysis
|
||||
|
||||
All of the following **must pass with zero warnings/errors** before any commit. See `code-completion-mandate.md` for the full checklist.
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
| ------------ | --------------------------------- | ---------------------------------- |
|
||||
| `ruff format`| Canonical formatting (fast) | `ruff format .` |
|
||||
| `ruff check` | Lint (replaces flake8, isort, ...) | `ruff check . --fix` |
|
||||
| `mypy` | Static type checking | `mypy src/ --strict` |
|
||||
| `bandit` | Security scanning | `bandit -r src/ -c pyproject.toml` |
|
||||
| `pip-audit` | Dependency CVE scanning | `pip-audit` |
|
||||
|
||||
Configure all tools in `pyproject.toml` — never use per-file pragma comments to disable checks without a `# NOQA:` reason comment.
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "UP", "S", "B", "ANN"]
|
||||
ignore = []
|
||||
|
||||
[tool.mypy]
|
||||
strict = true
|
||||
python_version = "3.11"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
```
|
||||
|
||||
> **Logging:** Never use `print()` in production code — it produces unstructured output. Use the standard `logging` module or `structlog` for structured JSON logs. See `logging-and-observability-principles.md` for the required patterns.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Project Structure — Python Backend @project-structure-python-backend.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Security Principles @security-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working with resources requiring cleanup (files, database connections, network sockets, locks) or implementing resource pools
|
||||
---
|
||||
|
||||
## Resource and Memory Management Principles
|
||||
|
||||
### Universal Resource Management Rules
|
||||
|
||||
**1. Always Clean Up Resources**
|
||||
|
||||
**Resources requiring cleanup:**
|
||||
|
||||
- Files, network connections, database connections
|
||||
- Locks, semaphores, mutexes
|
||||
- Memory allocations (in manual-memory languages)
|
||||
- OS handles, GPU resources
|
||||
|
||||
**Clean up in ALL paths:**
|
||||
|
||||
- Success path: Normal completion
|
||||
- Error path: Exception thrown, error returned
|
||||
- Early return path: Guard clauses, validation failures
|
||||
|
||||
**Use language-appropriate patterns:**
|
||||
|
||||
- Go: defer statements
|
||||
- Rust: Drop trait (RAII)
|
||||
- Python: context managers (with statement)
|
||||
- TypeScript: try/finally
|
||||
- Java: try-with-resources
|
||||
|
||||
**2. Timeout All I/O Operations**
|
||||
|
||||
**Why timeout:**
|
||||
|
||||
- Network requests can hang indefinitely
|
||||
- Prevents resource exhaustion (connections, threads)
|
||||
- Provides predictable failure behavior
|
||||
|
||||
**Timeout recommendations:**
|
||||
|
||||
- Network requests: 30s default, shorter (5-10s) for interactive
|
||||
- Database queries: 10s default, configure per query complexity
|
||||
- File operations: Usually fast, but timeout on network filesystems
|
||||
- Message queue operations: Configurable, avoid indefinite blocking
|
||||
|
||||
**3. Pool Expensive Resources**
|
||||
|
||||
**Resources to pool:**
|
||||
|
||||
- Database connections: Pool size 5-20 per app instance
|
||||
- HTTP connections: Reuse with keep-alive
|
||||
- Thread pools: Size based on CPU count (CPU-bound) or I/O wait (I/O-bound)
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Reduces latency (no connection setup overhead)
|
||||
- Limits resource consumption (cap on max connections)
|
||||
- Improves throughput (reuse vs create new)
|
||||
|
||||
**Connection Pool Best Practices:**
|
||||
|
||||
- Minimum connections: 5 (ensures pool is warm)
|
||||
- Maximum connections: 20-50 (prevents overwhelming database)
|
||||
- Idle timeout: Close connections idle >5-10 minutes
|
||||
- Validation: Test connections before use (avoid broken connections)
|
||||
- Monitoring: Track utilization, wait times, timeout rates
|
||||
|
||||
**4. Avoid Resource Leaks**
|
||||
|
||||
**What is a leak:**
|
||||
|
||||
- Acquire resource (open file, allocate memory, get connection)
|
||||
- Never release it (forget to close, exception prevents cleanup)
|
||||
- Eventually exhaust system resources (OOM, max connections, file descriptors)
|
||||
|
||||
**Detection:**
|
||||
|
||||
- Monitor open file descriptors, connection counts, memory usage over time
|
||||
- Run long-duration tests, verify resource counts stay stable
|
||||
- Use leak detection tools (valgrind, ASan, heap profilers)
|
||||
|
||||
**Prevention:**
|
||||
|
||||
- Use language patterns that guarantee cleanup (RAII, defer, context managers)
|
||||
- Never rely on manual cleanup alone (use language features)
|
||||
|
||||
**5. Handle Backpressure**
|
||||
|
||||
**Problem:** Producer faster than consumer
|
||||
|
||||
- Queue grows unbounded → memory exhaustion
|
||||
- System becomes unresponsive under load
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- Bounded queues: Fixed size, block or reject when full
|
||||
- Rate limiting: Limit incoming request rate
|
||||
- Flow control: Consumer signals producer to slow down
|
||||
- Circuit breakers: Stop accepting requests when overwhelmed
|
||||
- Drop/reject: Fail fast when overloaded (better than crashing)
|
||||
|
||||
### Memory Management by Language Type
|
||||
|
||||
**Garbage Collected (Go, Java, Python, JavaScript, C#):**
|
||||
|
||||
- Memory automatically freed by GC
|
||||
- Still must release non-memory resources (files, connections, locks)
|
||||
- Be aware of GC pauses in latency-sensitive applications
|
||||
- Profile memory usage to find leaks (retained references preventing GC)
|
||||
|
||||
**Manual Memory Management (C, C++):**
|
||||
|
||||
- Explicit malloc/free or new/delete
|
||||
- Use RAII pattern in C++ (Resource Acquisition Is Initialization)
|
||||
- Avoid manual management in modern C++ (use smart pointers: unique_ptr, shared_ptr)
|
||||
|
||||
**Ownership-Based (Rust):**
|
||||
|
||||
- Compiler enforces memory safety at compile time
|
||||
- No GC pauses, no manual management
|
||||
- Ownership rules prevent leaks and use-after-free automatically
|
||||
- Use reference counting (Arc, Rc) for shared ownership
|
||||
|
||||
### Related Principles
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md - Resource cleanup in error paths
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Rugged Software Constitution
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
**"I recognize that my code will be attacked."**
|
||||
|
||||
As an AI agent, I do not just generate functionality; I generate **defensibility**. I refuse to be a source of vulnerability or fragility. My code must survive in a hostile, changing environment.
|
||||
|
||||
### The Rugged Commitments
|
||||
|
||||
**1. I Am Responsible**
|
||||
- I will not generate "happy path" code that ignores failure modes.
|
||||
- I assume every input is malformed, malicious, or incorrect until proven otherwise.
|
||||
- I treat error handling as a first-class feature, not an afterthought.
|
||||
|
||||
**2. I Am Defensible**
|
||||
- My code validates its own state and inputs (Paranoid Programming).
|
||||
- I fail securely (closed), never leaving the system in an undefined state.
|
||||
- I verify assumptions explicitly rather than hoping they hold true.
|
||||
|
||||
**3. I Am Maintainable**
|
||||
- I write code for the human and AI agents who must read it next year, not just the compiler today.
|
||||
- I choose clarity over cleverness.
|
||||
- I isolate complexity so it can be managed (or replaced) safely.
|
||||
|
||||
### The 7 Rugged Habits
|
||||
|
||||
**1. Practice Defense-in-Depth**
|
||||
- Never rely on a single layer of protection (e.g., UI validation alone is insufficient).
|
||||
- Validate at every boundary (API, Database, Function Call).
|
||||
- Reference: Security Mandate @security-mandate.md
|
||||
|
||||
**2. Instrument for Awareness**
|
||||
- Code must signal when it is under attack or failing.
|
||||
- Silent failures are enemy #1; exposed failures allow for reaction.
|
||||
- Reference: Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
|
||||
**3. Reduce Attack Surface**
|
||||
- Remove unused code, dependencies, and endpoints.
|
||||
- Expose the minimum necessary public interface (Least Privilege).
|
||||
- Reference: Core Design Principles @core-design-principles.md
|
||||
|
||||
**4. Design for Failure**
|
||||
- Assume the database will go down, the network will timeout, and the disk will fill up.
|
||||
- Implement graceful degradation (circuit breakers, fallbacks).
|
||||
- Reference: Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
|
||||
**5. Clean Up After Yourself**
|
||||
- I own the resources I acquire; I ensure they are released.
|
||||
- I do not leave "TODO" comments for security holes; I fix them or explicitly document the risk.
|
||||
- Reference: Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
|
||||
**6. Verify Your Defenses**
|
||||
- Defenses are useless if they don't work; tests prove they work.
|
||||
- Test the "unhappy path" (attacks, errors, edge cases) as rigorously as the happy path.
|
||||
- Reference: Testing Strategy @testing-strategy.md
|
||||
|
||||
**7. Adapt to the Ecosystem**
|
||||
- Use established, battle-tested libraries over custom implementations.
|
||||
- Follow community conventions to ensure long-term maintainability.
|
||||
- Reference: Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
|
||||
### Application to Code Generation
|
||||
|
||||
**When generating code, I will:**
|
||||
- **Refuse** to generate insecure patterns (SQLi, hardcoded secrets, shell injection), even if asked.
|
||||
- **Proactively** add validation, error handling, and timeout logic, even if not explicitly requested.
|
||||
- **Explain** *why* I added a defensive measure (e.g., "Added input validation to prevent XSS").
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Rule Priority (When Rules Conflict)
|
||||
|
||||
When two rules pull in opposite directions, use this priority to decide:
|
||||
|
||||
### Priority Order (Highest to Lowest)
|
||||
|
||||
1. **Security Mandate** — always wins. Never compromise security for velocity, simplicity, or convenience.
|
||||
2. **Rugged Software Constitution** — foundational philosophy. Code must be defensible.
|
||||
3. **Code Completion Mandate** and **Logging and Observability Mandate** — both are always-on enforcement rules. Validation and instrumentation are non-negotiable; neither can be skipped to ship faster.
|
||||
4. **Testability-First Design** — maintainability enables future improvements.
|
||||
5. **Feature-specific principles** — context-dependent guidance for the task at hand. This includes language-specific idiom files (`go-idioms-and-patterns.md`, `typescript-idioms-and-patterns.md`, `vue-idioms-and-patterns.md`, `flutter-idioms-and-patterns.md`, `rust-idioms-and-patterns.md`), `concurrency-and-threading-mandate.md`, and `ci-cd-principles.md`. When an idiom conflicts with a higher-priority rule (e.g., security or testability), the higher-priority rule always wins.
|
||||
6. **PRD-gated principles** — only apply when the PRD or technical architecture document *explicitly requires* the capability. Includes `feature-flags-principles.md` and `ci-cd-gitops-kubernetes.md`. Must not be applied based on speculation; agent must confirm requirement before activating.
|
||||
7. **YAGNI / KISS** — only when no security, reliability, or maintainability trade-off exists.
|
||||
|
||||
### Common Conflict Resolutions
|
||||
|
||||
| Conflict | Resolution |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| YAGNI vs Security ("don't add input validation, it's not needed yet") | **Security wins.** Input validation is always needed. |
|
||||
| KISS vs Testability ("adding an interface makes it more complex") | **Testability wins.** Interfaces enable testing, which enables maintainability. |
|
||||
| Performance vs YAGNI ("should I optimize this now?") | **Measure first.** Only optimize after profiling shows a real bottleneck. |
|
||||
| DRY vs Clarity ("should I abstract this into a shared utility?") | **Clarity wins** until duplication reaches 3+ instances (Rule of Three). |
|
||||
| Speed vs Logging ("skip logging to ship faster") | **Logging wins.** Silent failures are the enemy. |
|
||||
| YAGNI vs PRD-gated requirement ("feature flags aren't needed") | **PRD wins.** If explicitly required, YAGNI cannot override it. |
|
||||
|
||||
### Guiding Principle
|
||||
|
||||
When in doubt, ask: *"Which choice makes the code more defensible and maintainable?"*
|
||||
|
||||
If both options are equally defensible, choose the simpler one (KISS).
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Rugged Software Constitution @rugged-software-constitution.md
|
||||
- Code Completion Mandate @code-completion-mandate.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Rust code, working on a Rust or Cargo project, or reviewing Rust idioms and safety practices
|
||||
---
|
||||
|
||||
## Rust Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Rust's type system and ownership model are your primary tools for correctness. Lean into the compiler — it is your strongest ally. Write code that is idiomatic, safe, and expressive.
|
||||
|
||||
> **Scope:** This file covers Rust-specific *coding idioms*. For file layout, see `project-structure-rust-cargo.md`. For test naming conventions, see `testing-strategy.md`. For logging library choice, see `logging-and-observability-principles.md`.
|
||||
|
||||
### Ownership and Borrowing
|
||||
|
||||
1. **Prefer borrowing (`&T`, `&mut T`) over cloning**
|
||||
- Never `.clone()` to silence the borrow checker without a `// CLONE:` comment explaining why
|
||||
- Use `Cow<'_, T>` when a function may or may not need ownership
|
||||
- Prefer `&str` over `String` in function parameters, `&[T]` over `Vec<T>`
|
||||
|
||||
2. **Minimize owned data in structs**
|
||||
- Use references with explicit lifetimes when the struct is short-lived
|
||||
- Use owned types (`String`, `Vec<T>`) when the struct must outlive its inputs
|
||||
|
||||
3. **Avoid unnecessary `Arc<Mutex<T>>`**
|
||||
- If data flows one direction, use channels (`tokio::sync::mpsc`)
|
||||
- If data is read-heavy, consider `RwLock` over `Mutex`
|
||||
- If data is immutable after init, use `Arc<T>` without a lock
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Use the `?` operator for propagation — never `unwrap()` in production code**
|
||||
- `unwrap()` and `expect()` are acceptable only in:
|
||||
- Tests (`#[test]`, `#[tokio::test]`)
|
||||
- Infallible operations where the invariant is proven (document with `// SAFETY:` comment)
|
||||
- CLI `main()` function with clear error messages via `expect("reason")`
|
||||
|
||||
2. **Choose error crates by context:**
|
||||
- Library code: `thiserror` — define typed error enums
|
||||
- Application code: `anyhow` — ergonomic error chaining
|
||||
- Never mix: library crates should not depend on `anyhow`
|
||||
|
||||
3. **Error type design:**
|
||||
|
||||
```rust
|
||||
// ✅ Good — typed, matchable errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PathfinderError {
|
||||
#[error("file not found: {path}")]
|
||||
FileNotFound { path: PathBuf },
|
||||
#[error("AST parse failed: {0}")]
|
||||
ParseError(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
// ❌ Bad — stringly-typed, unmatchable
|
||||
fn do_thing() -> Result<(), String> { ... }
|
||||
|
||||
// ✅ Annotate public Result-producing functions to force callers to handle them
|
||||
#[must_use]
|
||||
pub fn create_task(req: CreateTaskRequest) -> Result<Task, TaskError> { ... }
|
||||
|
||||
// Triggers a compiler warning when the return value is fully ignored:
|
||||
// create_task(req); // warning: unused `Result` that must be used
|
||||
//
|
||||
// This does NOT trigger the warning (intentional discard — still valid when used deliberately):
|
||||
// let _ = create_task(req); // explicit discard, silences warning by design
|
||||
```
|
||||
|
||||
### Async and Concurrency
|
||||
|
||||
1. **Use `tokio` as the async runtime**
|
||||
- Mark async entry points with `#[tokio::main]` or `#[tokio::test]`
|
||||
- Prefer `tokio::spawn` for concurrent tasks, not `std::thread::spawn`
|
||||
- Use `tokio::select!` for racing futures, not manual polling
|
||||
|
||||
2. **Cancellation safety:**
|
||||
- Prefer `tokio::sync::mpsc` over `tokio::sync::broadcast` unless fan-out is needed
|
||||
- Document cancellation behavior on any `async fn` that holds resources across `.await`
|
||||
- Use `tokio_util::sync::CancellationToken` for graceful shutdown
|
||||
|
||||
3. **Blocking operations:**
|
||||
- Never call blocking I/O inside async context
|
||||
- Use `tokio::task::spawn_blocking` for CPU-heavy or blocking work
|
||||
- Use `tokio::fs` instead of `std::fs` inside async functions
|
||||
|
||||
### Unsafe Code
|
||||
|
||||
1. **Zero `unsafe` blocks unless in FFI boundaries**
|
||||
- Tree-sitter C bindings and similar FFI are the only valid use case
|
||||
- Every `unsafe` block must have a `// SAFETY:` comment explaining the invariant
|
||||
|
||||
2. **Minimize unsafe surface area:**
|
||||
- Encapsulate `unsafe` in a safe wrapper function
|
||||
- The wrapper's public API must be safe to call from any context
|
||||
- Write tests that exercise the boundary conditions of `unsafe` wrappers
|
||||
|
||||
3. **Never use `unsafe` to bypass the borrow checker** — restructure the code instead
|
||||
|
||||
### Lifetimes and Generics
|
||||
|
||||
1. **Prefer `'_` lifetime elision when possible**
|
||||
- Only introduce named lifetimes when the compiler requires them or when they clarify intent
|
||||
- Use `'a` for single lifetime parameters, descriptive names (`'input`, `'query`) for multiple
|
||||
|
||||
2. **Keep generic bounds simple:**
|
||||
- Prefer concrete types for prototyping, introduce generics when the pattern stabilizes
|
||||
- Use `impl Trait` in argument position for simple cases
|
||||
- Use `where` clauses for complex bounds — never inline complex bounds in `<...>`
|
||||
|
||||
3. **Avoid lifetime gymnastics:**
|
||||
- If lifetime annotations become complex, restructure to use owned data or `Arc`
|
||||
- Consider the "split borrow" pattern to avoid borrow checker issues in struct methods
|
||||
|
||||
### Idiomatic Patterns
|
||||
|
||||
1. **Builder pattern** for types with many optional fields:
|
||||
- Return `Self` from builder methods for chaining
|
||||
- `build()` returns `Result<T, BuildError>`, not `T`
|
||||
|
||||
2. **Newtype pattern** for domain types:
|
||||
- Wrap primitives: `struct UserId(u64)`, not bare `u64`
|
||||
- Implement `Deref` only when the newtype truly "is-a" the inner type
|
||||
|
||||
3. **Typestate pattern** for state machines:
|
||||
- Different states = different types — invalid transitions are compile errors
|
||||
- Use this for protocol implementations and lifecycle management
|
||||
|
||||
4. **`From`/`Into` conversions:**
|
||||
- Implement `From<A> for B` (never `Into` directly)
|
||||
- Use `impl From<X> for Error` with `thiserror`'s `#[from]` attribute
|
||||
|
||||
### Testing
|
||||
|
||||
1. **Test organization (Rust-specific — differs from Go/TS):**
|
||||
- **Unit tests:** `#[cfg(test)] mod tests` block **at the bottom of each `.rs` file** — this is the idiomatic Rust convention, not a shortcut
|
||||
- Tests can access private functions via `use super::*`
|
||||
- Code inside `#[cfg(test)]` is stripped from production builds
|
||||
- Do NOT create separate `*_test.rs` files — this breaks private access and is non-idiomatic
|
||||
- **Integration tests:** `tests/` directory at crate root (each file compiled as a separate crate)
|
||||
- Only tests public API — use `use my_crate::function;`
|
||||
- No `#[cfg(test)]` annotation needed
|
||||
- Shared helpers: `tests/common/mod.rs` (NOT `tests/common.rs`, which Cargo treats as a test file)
|
||||
- Use `#[tokio::test]` for async tests
|
||||
|
||||
2. **Test naming:** `fn test_<function>_<scenario>_<expected>()` (snake_case)
|
||||
|
||||
3. **Assertions:**
|
||||
- Use `assert_eq!` / `assert_ne!` over `assert!(a == b)` — better error messages
|
||||
- Use `assert!(matches!(result, Ok(_)))` for enum variant checking
|
||||
|
||||
4. **Property testing:** Use `proptest` or `quickcheck` for functions with wide input spaces
|
||||
|
||||
### Clippy and Formatting
|
||||
|
||||
1. **`cargo check` for fast iteration during development**
|
||||
- `cargo check`: type-checks without producing a binary — fastest feedback loop
|
||||
- `cargo clippy`: includes `cargo check` plus lint rules — use before committing
|
||||
- `cargo build`: only when you need the actual binary/library artifact
|
||||
- Never run `cargo build` during TDD cycles — it is significantly slower than `cargo check`
|
||||
|
||||
2. **`cargo clippy` must pass with zero warnings** before any commit
|
||||
- Use `#[allow(clippy::...)]` only with a `// ALLOW:` comment explaining why
|
||||
- Prefer fixing the lint over suppressing it
|
||||
|
||||
3. **`cargo fmt` is non-negotiable** — all code must be formatted
|
||||
|
||||
4. **Recommended project-level Clippy configuration (`.clippy.toml` or `Cargo.toml`):**
|
||||
|
||||
```toml
|
||||
[lints.clippy]
|
||||
pedantic = "warn"
|
||||
unwrap_used = "deny"
|
||||
expect_used = "warn"
|
||||
```
|
||||
|
||||
### Dependency Management
|
||||
|
||||
1. **Minimize dependency count** — each dependency is an attack surface and compile-time cost
|
||||
2. **Pin major versions** in `Cargo.toml` — use `dep = "1"` not `dep = "*"`
|
||||
3. **Audit regularly** — run `cargo audit` to check for known vulnerabilities
|
||||
4. **Prefer well-maintained crates** — check download count, last commit date, and issue tracker
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Performance Optimization Principles @performance-optimization-principles.md
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Security Mandate
|
||||
|
||||
**Security is a foundational requirement, not a feature.**
|
||||
|
||||
### Universal Security Principles
|
||||
|
||||
1. **Never trust user input:** All data from users, APIs, or external sources must be validated server-side
|
||||
2. **Deny by default:** Require explicit permission grants, never assume access
|
||||
3. **Fail securely:** When errors occur, fail closed (deny access) rather than open
|
||||
4. **Defense in depth:** Multiple layers of security, never rely on a single control
|
||||
|
||||
**When implementing security-sensitive features (auth, validation, queries), see Security Principles @security-principles.md for detailed implementation guidance.**
|
||||
|
||||
### Related Principles
|
||||
- Security Principles @security-principles.md
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing authentication, authorization, input validation, cryptographic operations, or handling user input and sensitive data
|
||||
---
|
||||
|
||||
## Security Principles
|
||||
|
||||
### OWASP Top 10 Enforcement
|
||||
|
||||
* **Broken Access Control:** Deny by default. Validate permissions *server-side* for every request. Do not rely on UI state.
|
||||
* **Cryptographic Failures:** Use TLS 1.2+ everywhere. Encrypt PII/Secrets at rest. Use standard algorithms (AES-256, RSA-2048, Ed25519). *Never* roll your own crypto.
|
||||
* **Injection:** ZERO TOLERANCE for string concatenation in queries. Use Parameterized Queries (SQL) or ORM bindings. Sanitize all HTML/JS output.
|
||||
* **SSRF Prevention:** Validate all user-provided URLs against an allowlist. Disable HTTP redirects in fetch clients. Block requests to internal IPs (metadata services, localhost).
|
||||
* **Insecure Design:** Threat model every new feature. Fail securely (closed), not openly.
|
||||
* **Vulnerable Components:** Pin dependency versions. Scan for CVEs in CI/CD.
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
* **Passwords:** Hash with Argon2id or Bcrypt (min cost 12). Never plain text.
|
||||
* **Tokens:**
|
||||
* *Access Tokens:* Short-lived (15-30 mins). HS256 or RS256.
|
||||
* *Refresh Tokens:* Long-lived (7-30 days). Rotate on use. Store in `HttpOnly; Secure; SameSite=Strict` cookies.
|
||||
* **Rate Limiting:** Enforce strictly on public endpoints (Login, Register, Password Reset). Standard: 5 attempts / 15 mins.
|
||||
* **MFA:** Required for Admin and Sensitive Data access.
|
||||
* **RBAC:** Map permissions to Roles, not Users. Check permissions at the Route AND Resource level.
|
||||
|
||||
### Input Validation & Sanitization
|
||||
|
||||
* **Principle:** "All Input is Evil until Proven Good."
|
||||
* **Validation:** Validate against a strict Schema (Zod/Pydantic) at the *Handler/Port* boundary.
|
||||
* **Allowlist:** Check for "Good characters" (e.g., `^[a-zA-Z0-9]+$`), do not try to filter "Bad characters."
|
||||
* **Sanitization:** Strip dangerous tags from rich text input using a proven library (e.g., DOMPurify equivalent).
|
||||
|
||||
### Logging & Monitoring (Security Focus)
|
||||
|
||||
* **Redaction:** SCRUB all PII, Secrets, Tokens, and Passwords from logs *before* writing.
|
||||
* **Events:** Log all *failed* auth attempts, access denied events, and input validation failures.
|
||||
* **Format:** JSON structured logs with `correlationId`, `user_id`, and `event_type`.
|
||||
* **Anti-Tamper:** Logs should be write-only for the application.
|
||||
|
||||
### Secrets Management
|
||||
|
||||
* **Storage:** Never commit secrets to git. Use `.env` (local) or Secret Managers (Prod - e.g., Vault/GSM).
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Configuration Management Principles @configuration-management-principles.md
|
||||
- Command Execution Principles @command-execution-principles.md
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing tests, organizing test files, implementing test doubles, or setting up testing infrastructure
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Pyramid
|
||||
|
||||
**Unit Tests (70% of tests):**
|
||||
|
||||
- **What:** Test domain logic in isolation with mocked dependencies
|
||||
- **Speed:** Fast (<100ms per test)
|
||||
- **Scope:** Single function, class, or module
|
||||
- **Dependencies:** All external dependencies mocked (repositories, APIs, time, random)
|
||||
- **Coverage Goal:** >85% of domain logic
|
||||
|
||||
**Integration Tests (20% of tests):**
|
||||
|
||||
- **What:** Test adapters against real infrastructure
|
||||
- **Speed:** Medium (100ms-5s per test)
|
||||
- **Scope:** Component interaction with infrastructure (database, cache, message queue)
|
||||
- **Dependencies:** Real infrastructure via Testcontainers
|
||||
- **Coverage Goal:** All adapter implementations, critical integration points
|
||||
|
||||
**End-to-End Tests (10% of tests):**
|
||||
|
||||
- **What:** Test complete user journeys through all layers
|
||||
- **Speed:** Slow (5s-30s per test)
|
||||
- **Scope:** Full system from HTTP request to database and back
|
||||
- **Dependencies:** Entire system running (or close approximation)
|
||||
- **Coverage Goal:** Happy paths, critical business flows
|
||||
|
||||
### Test-Driven Development (TDD)
|
||||
|
||||
**Red-Green-Refactor Cycle:**
|
||||
|
||||
1. **Red:** Write a failing test for next bit of functionality
|
||||
2. **Green:** Write minimal code to make test pass
|
||||
3. **Refactor:** Clean up code while keeping tests green
|
||||
4. **Repeat:** Next test
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Tests written first ensure testable design
|
||||
- Comprehensive test coverage (code without test doesn't exist)
|
||||
- Faster development (catch bugs immediately, not in QA)
|
||||
- Better design (forces thinking about interfaces before implementation)
|
||||
|
||||
### Test Doubles Strategy
|
||||
|
||||
**Unit Tests:** Use mocks/stubs for all driven ports
|
||||
|
||||
- Mock repositories return pre-defined data
|
||||
- Mock external APIs return successful responses
|
||||
- Mock time/random for deterministic tests
|
||||
- Control test environment completely
|
||||
|
||||
**Integration Tests:** Use real infrastructure
|
||||
|
||||
- Testcontainers spins up PostgreSQL, Redis, message queues
|
||||
- Firebase emulator spins up Firebase Authentication, Cloud Firestore, Realtime Database, Cloud Storage for Firebase, Firebase Hosting, Cloud Functions, Pub/Sub, and Firebase Extensions
|
||||
- Test actual database queries, connection handling, transactions
|
||||
- Verify adapter implementations work with real services
|
||||
|
||||
**Best Practice:**
|
||||
|
||||
- Generate at least 2 implementations per driven port:
|
||||
1. Production adapter (PostgreSQL, GCP GCS, etc.)
|
||||
2. Test adapter (in-memory, fake implementation)
|
||||
|
||||
### Test Organization
|
||||
|
||||
**Universal Rule: Co-locate implementation tests; Separate system tests.**
|
||||
|
||||
**1. Unit & Integration Tests (Co-located)**
|
||||
- **Rule:** Place tests **next to the file** they test.
|
||||
- **Why:** Keeps tests visible, encourages maintenance, and supports refactoring (moving a file moves its tests).
|
||||
- **Naming Convention Example:**
|
||||
- **TS/JS:** `*.spec.ts` (Unit), `*.integration.spec.ts` (Integration)
|
||||
- **Go:** `*_test.go` (Unit), `*_integration_test.go` (Integration)
|
||||
- **Dart/Flutter:** `*_test.dart` (Unit), `*_integration_test.dart` (Integration) — tests live in `test/` mirroring `lib/` layout (Flutter's default convention, discovered by `flutter test`)
|
||||
- **Python:** `test_*.py` (Unit), `test_*_integration.py` (Integration)
|
||||
- **Java:** `*Test.java` (Unit), `*IT.java` (Integration)
|
||||
- **Rust:** `#[cfg(test)] mod tests` inline in each `.rs` file (Unit), `tests/` directory at crate root (Integration)
|
||||
> You must strictly follow the convention for the target language. Do not mix `test` and `spec` suffixes in the same application context.
|
||||
> **Language-specific overrides:** Some ecosystems have different test location conventions. When a language-specific project structure file exists (e.g., `project-structure-flutter-mobile.md`, `project-structure-rust-cargo.md`), its test location rules take precedence over the co-location default. See `architectural-pattern.md` § Test co-location for the authoritative rule.
|
||||
> **Rust exception:** Rust unit tests are **inline** (`#[cfg(test)] mod tests` at the bottom of each `.rs` file), not separate files. This is the official Rust convention — it enables testing private functions and is how `cargo test` discovers unit tests. Integration tests go in `tests/` at the crate root, compiled as separate crates. See `rust-idioms-and-patterns.md` for details.
|
||||
|
||||
**2. End-to-End Tests (Separate)**
|
||||
- **Rule:** Place in a dedicated `e2e/` folder
|
||||
- **Single app:** `e2e/` at project root
|
||||
- **Monorepo:** `apps/e2e/` subdivided by test scope:
|
||||
- `apps/e2e/api/` for full API flow E2E tests (HTTP → Database)
|
||||
- `apps/e2e/ui/` for full-stack E2E tests (Browser → Backend → Database)
|
||||
> Path adapts to project type per `project-structure.md` § Adapting for Different Project Types.
|
||||
- **Naming:** Follow `{feature}-{ui/api}.e2e.test.{ext}` (Universal - configure test runner to match this pattern `**/*.e2e.test.*`)
|
||||
- Example:
|
||||
- `user-registration-api.e2e.test.ts` # Full API flow: HTTP → DB
|
||||
- `user-registration-ui.e2e.test.ts` # Full-stack: Browser → Backend → DB
|
||||
|
||||
**Using Playwright MCP for UI E2E Tests:**
|
||||
|
||||
When running E2E tests interactively (during development or verification), use Playwright MCP:
|
||||
|
||||
```
|
||||
# Navigate to the page
|
||||
mcp_playwright_browser_navigate(url="http://localhost:5173/login")
|
||||
|
||||
# Capture accessible state (better than screenshot for assertions)
|
||||
mcp_playwright_browser_snapshot()
|
||||
|
||||
# Interact with elements by ref from snapshot
|
||||
mcp_playwright_browser_type(ref="<ref>", text="[email protected]")
|
||||
mcp_playwright_browser_click(ref="<ref>")
|
||||
|
||||
# Wait for results
|
||||
mcp_playwright_browser_wait_for(text="Welcome")
|
||||
|
||||
# Capture snapshot for walkthrough documentation (captures full accessibility tree)
|
||||
mcp_playwright_browser_snapshot(filename="login-success.md")
|
||||
```
|
||||
|
||||
**E2E Test Requirements:**
|
||||
- Capture a snapshot (`mcp_playwright_browser_snapshot`) at each major step
|
||||
- Save snapshots to walkthrough as proof of functionality
|
||||
- If visual proof is needed, use `browser_subagent` with `RecordingName` to produce a recorded video artifact
|
||||
- Test happy path AND at least one error path
|
||||
- Clean up test data after test (or use unique identifiers)
|
||||
|
||||
**Key Principles:**
|
||||
- **Unit/Integration tests**: Co-located with implementation
|
||||
- **E2E tests**: Separate directory (crosses boundaries)
|
||||
- **Test doubles**: Co-located with interface (storage_mock.go, taskAPI.mock.ts)
|
||||
- **Pattern consistency**: All features follow same structure
|
||||
|
||||
### Test Quality Standards
|
||||
|
||||
**AAA Pattern (Arrange-Act-Assert):**
|
||||
```
|
||||
// Arrange: Set up test data and mocks
|
||||
const user = { id: '123', email: '[email protected]' };
|
||||
const mockRepo = createMockRepository();
|
||||
|
||||
// Act: Execute the code under test
|
||||
const result = await userService.createUser(user);
|
||||
|
||||
// Assert: Verify expected outcome
|
||||
expect(result.id).toBe('123');
|
||||
expect(mockRepo.save).toHaveBeenCalledWith(user);
|
||||
```
|
||||
**Test Naming:**
|
||||
|
||||
- Descriptive: `should [expected behavior] when [condition]`
|
||||
- Examples:
|
||||
- `should return 404 when user not found`
|
||||
- `should hash password before saving to database`
|
||||
- `should reject email with invalid format`
|
||||
|
||||
**Coverage Requirements:**
|
||||
|
||||
- Unit tests: >85% code coverage
|
||||
- Integration tests: All adapter implementations
|
||||
- E2E tests: Critical user journeys
|
||||
|
||||
### Related Principles
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Project Structure @project-structure.md
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing TypeScript code, reviewing TypeScript idioms, or working on any TypeScript project (backend, frontend, or CLI)
|
||||
---
|
||||
|
||||
## TypeScript Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
TypeScript's type system is your documentation, your test, and your specification — all at once. Make the type system encode the invariants of your domain so that invalid states are unrepresentable. Lean into the compiler.
|
||||
|
||||
> **Scope:** This file covers TypeScript-specific *type system and language idioms*. For Vue-specific patterns, see `vue-idioms-and-patterns.md`. For file layout, see `project-structure-vue-frontend.md`. For quality commands, see `code-completion-mandate.md`. For logging library, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### Strict Mode — Non-Negotiable
|
||||
|
||||
**Always enable strict mode** in `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These flags catch the majority of runtime errors at compile time. Never disable them on a per-file basis without a `// STRICT-DISABLE:` comment explaining the rationale.
|
||||
|
||||
---
|
||||
|
||||
### Type System Idioms
|
||||
|
||||
1. **`unknown` over `any` — always**
|
||||
```typescript
|
||||
// ✅ Forces narrowing before use
|
||||
function parse(data: unknown): User {
|
||||
if (!isUser(data)) throw new Error('Invalid user shape');
|
||||
return data;
|
||||
}
|
||||
|
||||
// ❌ Disables the type checker entirely
|
||||
function parse(data: any): User { return data; }
|
||||
```
|
||||
|
||||
2. **Use `readonly` to enforce immutability at compile time**
|
||||
```typescript
|
||||
interface TaskState {
|
||||
readonly id: string;
|
||||
readonly items: readonly Task[];
|
||||
}
|
||||
|
||||
// For function params that should not be mutated
|
||||
function process(tasks: readonly Task[]): Summary { ... }
|
||||
```
|
||||
|
||||
3. **Discriminated unions for type-safe state machines**
|
||||
```typescript
|
||||
type AsyncState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error };
|
||||
|
||||
// Exhaustive handling — compiler catches missing cases
|
||||
function render(state: AsyncState<User>): string {
|
||||
switch (state.status) {
|
||||
case 'idle': return 'Waiting...';
|
||||
case 'loading': return 'Loading...';
|
||||
case 'success': return state.data.name; // data is typed
|
||||
case 'error': return state.error.message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Const assertions for literal types**
|
||||
```typescript
|
||||
const ROLES = ['admin', 'editor', 'viewer'] as const;
|
||||
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'
|
||||
```
|
||||
|
||||
5. **Type narrowing — use type guards instead of `as` casts**
|
||||
```typescript
|
||||
// ✅ Type guard — safe narrowing
|
||||
function isError(value: unknown): value is Error {
|
||||
return value instanceof Error;
|
||||
}
|
||||
|
||||
// ❌ Type assertion — bypasses type checker
|
||||
const err = value as Error;
|
||||
```
|
||||
|
||||
6. **Never use non-null assertion `!` in production code**
|
||||
```typescript
|
||||
// ❌ Hides a potential null/undefined bug
|
||||
const name = user!.profile!.name;
|
||||
|
||||
// ✅ Explicit handling
|
||||
const name = user?.profile?.name ?? 'Anonymous';
|
||||
```
|
||||
|
||||
7. **`satisfies` operator for type-checked object literals (TS 4.9+)**
|
||||
```typescript
|
||||
// ✅ satisfies: compile-checked against interface, type stays as literal
|
||||
const config = {
|
||||
endpoint: '/api/tasks',
|
||||
retries: 3,
|
||||
} satisfies ApiConfig;
|
||||
// config.retries is typed as `3` (literal), not `number` — narrower and safer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Null Safety
|
||||
|
||||
1. **Prefer `??` (nullish coalescing) over `||` for default values**
|
||||
```typescript
|
||||
// ✅ Only falls back for null/undefined
|
||||
const count = input.count ?? 0;
|
||||
|
||||
// ❌ Also falls back for 0, '', false
|
||||
const count = input.count || 0;
|
||||
```
|
||||
|
||||
2. **Use optional chaining `?.` for safe navigation**
|
||||
```typescript
|
||||
const city = user?.address?.city;
|
||||
```
|
||||
|
||||
3. **Distinguish `undefined` (absence) from `null` (explicit empty)**
|
||||
- Use `undefined` for optional fields
|
||||
- Use `null` only when you need to represent "intentionally empty" on the wire (JSON APIs)
|
||||
|
||||
---
|
||||
|
||||
### Async/Await
|
||||
|
||||
> For general async patterns (when to use concurrency), see `concurrency-and-threading-mandate.md`. This section covers TypeScript-specific async idioms.
|
||||
|
||||
1. **Always `await` or handle returned Promises — no floating promises**
|
||||
```typescript
|
||||
// ❌ Fire-and-forget — errors are silently swallowed
|
||||
sendEmail(user);
|
||||
|
||||
// ✅ Awaited
|
||||
await sendEmail(user);
|
||||
|
||||
// ✅ Intentionally fire-and-forget needs explicit void annotation
|
||||
void sendEmail(user); // still logs errors internally
|
||||
```
|
||||
|
||||
2. **Use `Promise.all` for concurrent independent operations**
|
||||
```typescript
|
||||
// ✅ Concurrent — total time = max(individual times)
|
||||
const [user, tasks] = await Promise.all([getUser(id), getTasks(id)]);
|
||||
|
||||
// ❌ Sequential — total time = sum of individual times
|
||||
const user = await getUser(id);
|
||||
const tasks = await getTasks(id);
|
||||
```
|
||||
|
||||
3. **Use `Promise.allSettled` when partial failure is acceptable**
|
||||
```typescript
|
||||
const results = await Promise.allSettled(notifications.map(send));
|
||||
const failed = results.filter(r => r.status === 'rejected');
|
||||
```
|
||||
|
||||
4. **Never mix `async/await` with raw `.then()/.catch()` chains in the same function**
|
||||
|
||||
---
|
||||
|
||||
### Runtime Validation at Boundaries
|
||||
|
||||
**All data crossing a system boundary must be validated at runtime**, not just typed.
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Define schema as the single source of truth
|
||||
const CreateTaskSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
priority: z.enum(['low', 'medium', 'high']),
|
||||
dueDate: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
// Infer the TypeScript type from the schema — no duplication
|
||||
type CreateTaskRequest = z.infer<typeof CreateTaskSchema>;
|
||||
|
||||
// Validate at the API boundary
|
||||
function parseCreateTask(body: unknown): CreateTaskRequest {
|
||||
return CreateTaskSchema.parse(body); // throws ZodError on invalid input
|
||||
}
|
||||
```
|
||||
|
||||
- Use `zod` for runtime schema validation at API ingress and external API egress
|
||||
- Never use TypeScript's `as` operator as a substitute for runtime validation
|
||||
- Validate on ingress; trust validated types thereafter
|
||||
|
||||
---
|
||||
|
||||
### Centralized HTTP Client
|
||||
|
||||
**All outbound HTTP calls MUST go through the project's single, shared API client utility.**
|
||||
|
||||
Do not call `fetch()` or `axios()` directly in feature code. Route every request through the centralized client (e.g., `apiFetch`, `apiClient`, or equivalent).
|
||||
|
||||
**Why this matters:**
|
||||
- Consistent auth header injection (token is attached in one place)
|
||||
- Correlation-ID propagation — every request carries a traceable ID
|
||||
- Centralized error normalization — uniform error shapes for all API failures
|
||||
- Single place to add retries, timeouts, and request logging
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — bypass: no auth header, no correlation-ID, no logging
|
||||
const res = await fetch('/api/tasks');
|
||||
|
||||
// ✅ Correct — use the shared client
|
||||
import { apiFetch } from '@/infrastructure/apiFetch';
|
||||
const res = await apiFetch('/api/tasks');
|
||||
```
|
||||
|
||||
**Exception:** The centralized client itself may use raw `fetch` or `axios` internally — that is its implementation detail, not a bypass.
|
||||
|
||||
> The audit's Integration Contracts dimension (Phase 1.5, Dimension A) checks compliance with this rule. Any direct `fetch`/`axios` call outside the shared client is a `[INT]` finding.
|
||||
|
||||
---
|
||||
|
||||
### Module and Export Patterns
|
||||
|
||||
1. **Prefer named exports over default exports**
|
||||
```typescript
|
||||
// ✅ Named — explicit, refactor-safe, IDE-friendly
|
||||
export function createTask() { ... }
|
||||
export type { Task };
|
||||
|
||||
// ❌ Default — ambiguous import names, harder to auto-import
|
||||
export default function createTask() { ... }
|
||||
```
|
||||
|
||||
2. **Avoid barrel re-exports that create circular dependency risk**
|
||||
- Use feature `index.ts` files only for the public API of a feature, not as catch-all re-exports
|
||||
|
||||
3. **Import type separately to avoid bundling runtime artifacts**
|
||||
```typescript
|
||||
import type { Task } from './types';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test naming, file conventions, and pyramid proportions are defined in `testing-strategy.md`. This section covers TypeScript-specific tooling.
|
||||
|
||||
1. **Type your mocks with Vitest types** — never use `as any` in test doubles
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
import type { MockedObject } from 'vitest';
|
||||
|
||||
const mockStore: MockedObject<TaskStore> = {
|
||||
create: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
};
|
||||
```
|
||||
|
||||
2. **Assert on error types, not just error messages**
|
||||
```typescript
|
||||
await expect(service.create(invalid)).rejects.toThrow(ZodError);
|
||||
```
|
||||
|
||||
3. **Use `satisfies` operator in tests for type-checked fixtures**
|
||||
```typescript
|
||||
const fixture = {
|
||||
id: 'abc', title: 'Test task'
|
||||
} satisfies Task;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Formatting and Static Analysis
|
||||
|
||||
| Tool | Purpose | Notes |
|
||||
| -------------------------- | --------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `vue-tsc --noEmit` | Full type checking (incl. `.vue` files) | Must pass zero errors; use `tsc --noEmit` for non-Vue TS projects |
|
||||
| `eslint` | Lint rules + style | Use `@typescript-eslint/recommended-type-checked` |
|
||||
| `prettier` | Canonical formatting | Non-negotiable |
|
||||
| `npm audit` / `pnpm audit` | Dependency CVE scanning | Run in CI; fail on high severity |
|
||||
|
||||
See `code-completion-mandate.md` for the exact commands to run before committing.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Vue Idioms and Patterns @vue-idioms-and-patterns.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Vue 3 code, working on Vue components, using Pinia stores, or building Vue composables
|
||||
---
|
||||
|
||||
## Vue Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Vue 3 Composition API is the default for all new code. `<script setup>` is the canonical syntax. Think in terms of reactive *data flows*, not component lifecycle hooks. Composables (`use*` functions) are the primary unit of logic reuse.
|
||||
|
||||
> **Scope:** This file covers Vue 3 *coding idioms* for components, stores, and composables. For TypeScript type system patterns, see `typescript-idioms-and-patterns.md`. For file and folder layout, see `project-structure-vue-frontend.md`. For test naming, see `testing-strategy.md`. For logging, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### `<script setup>` — The Only Style
|
||||
|
||||
Always use `<script setup lang="ts">`. Never use the Options API or the class-style component pattern for new code.
|
||||
|
||||
```vue
|
||||
<!-- ✅ Canonical style -->
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{ title: string; count?: number }>();
|
||||
const emit = defineEmits<{ 'update:count': [value: number] }>();
|
||||
|
||||
const doubled = computed(() => (props.count ?? 0) * 2);
|
||||
</script>
|
||||
|
||||
<!-- ❌ Options API — do not use for new components -->
|
||||
<script lang="ts">
|
||||
export default { props: { title: String }, ... }
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Reactivity: `ref` vs `reactive`
|
||||
|
||||
| Use | When |
|
||||
| ------------ | ---------------------------------------------------------------------------------- |
|
||||
| `ref<T>()` | Primitives, single values, values that may be reassigned |
|
||||
| `reactive()` | Plain objects where you always access properties (never reassign the whole object) |
|
||||
| `readonly()` | Expose state that must not be mutated outside its owner |
|
||||
|
||||
```typescript
|
||||
// ✅ ref for primitives and replaceable objects
|
||||
const count = ref(0);
|
||||
const user = ref<User | null>(null);
|
||||
user.value = fetchedUser; // reassignment is fine
|
||||
|
||||
// ✅ reactive for objects where you destructure properties
|
||||
const form = reactive({ title: '', priority: 'medium' });
|
||||
|
||||
// ❌ Never destructure a reactive object — reactivity is lost
|
||||
const { title } = form; // title is now a plain string, NOT reactive
|
||||
// ✅ Use toRefs if you must destructure
|
||||
const { title } = toRefs(form);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Computed Properties
|
||||
|
||||
1. **Use `computed` for all derived state** — never recompute in the template
|
||||
```typescript
|
||||
// ✅ Cached, reactive
|
||||
const filteredTasks = computed(() =>
|
||||
tasks.value.filter(t => t.status === activeFilter.value)
|
||||
);
|
||||
|
||||
// ❌ Recomputes on every render
|
||||
// <template>{{ tasks.filter(t => t.status === filter) }}</template>
|
||||
```
|
||||
|
||||
2. **Never cause side effects inside `computed`** — computed must be pure
|
||||
```typescript
|
||||
// ❌ Side effect in computed
|
||||
const count = computed(() => {
|
||||
taskStore.logAccess(); // NO — this is a side effect
|
||||
return tasks.value.length;
|
||||
});
|
||||
```
|
||||
|
||||
3. **Use writable computed for two-way bindings**
|
||||
```typescript
|
||||
const modelValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Watch Strategy
|
||||
|
||||
Use the most precise watcher for the situation — over-watching is a performance and correctness problem.
|
||||
|
||||
| Watcher | Use When |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `watchEffect` | Side effect that should re-run whenever any of its reactive dependencies change; auto-tracks dependencies |
|
||||
| `watch` | You need the old value, lazy execution, or want to watch a specific source explicitly |
|
||||
| `computed` | You need a synchronous derived value (prefer this over `watch` for transformation) |
|
||||
|
||||
```typescript
|
||||
// ✅ watchEffect — auto-tracks dependencies
|
||||
watchEffect(() => {
|
||||
document.title = `Tasks (${count.value})`;
|
||||
});
|
||||
|
||||
// ✅ watch — explicit source, has old value
|
||||
watch(userId, async (newId, oldId) => {
|
||||
if (newId !== oldId) await loadUser(newId);
|
||||
}, { immediate: true });
|
||||
|
||||
// ❌ Avoid using watch just for computed values
|
||||
watch(tasks, () => { filteredCount.value = tasks.value.filter(...).length; });
|
||||
// ✅ Use computed instead
|
||||
const filteredCount = computed(() => tasks.value.filter(...).length);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pinia Stores
|
||||
|
||||
> The store directory structure is defined in `project-structure-vue-frontend.md`. This section covers Pinia coding idioms.
|
||||
|
||||
1. **Use the Setup Store API** (not Options API) for new stores
|
||||
```typescript
|
||||
// task/store/task.store.ts
|
||||
export const useTaskStore = defineStore('task', () => {
|
||||
// State
|
||||
const tasks = ref<Task[]>([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// Getters (computed)
|
||||
const completedTasks = computed(() =>
|
||||
tasks.value.filter(t => t.status === 'done')
|
||||
);
|
||||
|
||||
// Actions
|
||||
async function loadTasks() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
tasks.value = await taskAPI.getTasks();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { tasks, isLoading, completedTasks, loadTasks };
|
||||
});
|
||||
```
|
||||
|
||||
2. **Never mutate store state from outside the store**
|
||||
```typescript
|
||||
// ❌ Direct mutation from a component
|
||||
const store = useTaskStore();
|
||||
store.tasks.push(newTask); // NO
|
||||
|
||||
// ✅ Call an action
|
||||
await store.addTask(newTask);
|
||||
```
|
||||
|
||||
3. **Inject the API dependency — never import it directly inside the store**
|
||||
```typescript
|
||||
// ✅ Receives the API interface — testable with createTestingPinia + mock API
|
||||
export const useTaskStore = defineStore('task', () => {
|
||||
const api = inject<TaskAPI>(TASK_API_KEY);
|
||||
if (!api) throw new Error('[TaskStore] TASK_API_KEY not provided — ensure app.provide() is called before store access');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
4. **Use `storeToRefs` when destructuring a store in components**
|
||||
```typescript
|
||||
// ✅ Preserves reactivity
|
||||
const { tasks, isLoading } = storeToRefs(useTaskStore());
|
||||
const { loadTasks } = useTaskStore(); // actions don't need storeToRefs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Composables (`use*` Functions)
|
||||
|
||||
Composables are the Vue equivalent of custom hooks — self-contained, reusable units of reactive logic.
|
||||
|
||||
1. **Naming: always prefix with `use`**
|
||||
- `useTaskFilters`, `useAuth`, `usePagination`
|
||||
|
||||
2. **Return reactive refs, not raw values**
|
||||
```typescript
|
||||
// ✅ Caller can use returned values reactively
|
||||
function useCounter(initial = 0) {
|
||||
const count = ref(initial);
|
||||
const increment = () => count.value++;
|
||||
return { count, increment };
|
||||
}
|
||||
|
||||
// ❌ count is a plain number — not reactive
|
||||
function useCounter() {
|
||||
let count = 0;
|
||||
return { count };
|
||||
}
|
||||
```
|
||||
|
||||
3. **Always clean up side effects in `onUnmounted`**
|
||||
```typescript
|
||||
function useWindowResize() {
|
||||
const width = ref(window.innerWidth);
|
||||
const handler = () => (width.value = window.innerWidth);
|
||||
|
||||
onMounted(() => window.addEventListener('resize', handler));
|
||||
onUnmounted(() => window.removeEventListener('resize', handler)); // ✅ cleanup
|
||||
return { width };
|
||||
}
|
||||
```
|
||||
|
||||
4. **Template refs with `useTemplateRef` (Vue 3.5+)** — type-safe, IDE-friendly replacement for `ref(null)`
|
||||
```typescript
|
||||
// ✅ Vue 3.5+ — useTemplateRef provides fully typed access
|
||||
const inputEl = useTemplateRef<HTMLInputElement>('myInput');
|
||||
// <input ref="myInput" />
|
||||
|
||||
// ❌ Old pattern (before 3.5) — less type-safe
|
||||
const inputEl = ref<HTMLInputElement | null>(null);
|
||||
```
|
||||
|
||||
5. **Feature-specific composables live inside the feature directory** — global composables go in `src/composables/`. See `project-structure-vue-frontend.md`.
|
||||
|
||||
---
|
||||
|
||||
### Component Design
|
||||
|
||||
1. **`defineProps` with TypeScript generics — no runtime validators for typed props**
|
||||
```typescript
|
||||
const props = defineProps<{
|
||||
taskId: string;
|
||||
variant?: 'compact' | 'full';
|
||||
}>();
|
||||
|
||||
// Defaults via withDefaults
|
||||
const props = withDefaults(defineProps<{ variant?: 'compact' | 'full' }>(), {
|
||||
variant: 'full',
|
||||
});
|
||||
```
|
||||
|
||||
2. **`defineEmits` with typed event signatures**
|
||||
```typescript
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
'submit': [task: CreateTaskRequest];
|
||||
}>();
|
||||
```
|
||||
|
||||
3. **`v-model` contract: always `modelValue` prop + `update:modelValue` emit**
|
||||
|
||||
4. **`defineExpose` to selectively expose methods to parent refs**
|
||||
```typescript
|
||||
// Everything in <script setup> is private by default.
|
||||
// Use defineExpose only for intentional parent access (e.g., form.reset()).
|
||||
defineExpose({ reset, focus });
|
||||
// ❌ Without defineExpose: parent ref.value.reset() will be undefined
|
||||
```
|
||||
|
||||
5. **`v-bind="$attrs"` and `inheritAttrs: false` for forwarding attributes**
|
||||
```typescript
|
||||
// Avoid prop drilling for HTML attributes — forward them to the root element
|
||||
defineOptions({ inheritAttrs: false });
|
||||
// In template: <input v-bind="$attrs" />
|
||||
```
|
||||
|
||||
6. **One concern per component** — if the template exceeds 100 lines (excluding boilerplate), extract a sub-component
|
||||
|
||||
7. **Never put business logic in the template** — computed and composables belong in `<script setup>`
|
||||
|
||||
---
|
||||
|
||||
### Template Patterns
|
||||
|
||||
1. **Always bind `:key` with stable, unique IDs in `v-for`** — never use index as key when list order can change
|
||||
```html
|
||||
<!-- ✅ Stable key -->
|
||||
<TaskCard v-for="task in tasks" :key="task.id" :task="task" />
|
||||
|
||||
<!-- ❌ Index key — causes rerender bugs when list reordered -->
|
||||
<TaskCard v-for="(task, i) in tasks" :key="i" :task="task" />
|
||||
```
|
||||
|
||||
2. **Never combine `v-if` and `v-for` on the same element** — wrap with `<template>`
|
||||
```html
|
||||
<!-- ✅ -->
|
||||
<template v-for="task in tasks" :key="task.id">
|
||||
<TaskCard v-if="task.visible" :task="task" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Route Transitions
|
||||
|
||||
When using `<Transition>` or `<RouterView>` with transition effects, CSS frameworks that use `@layer` (Tailwind v4, Open Props, UnoCSS) can silently break SPA navigation by overriding transition properties in the cascade. This causes `transitionend` to never fire, permanently blocking the entering component.
|
||||
|
||||
1. **Avoid `mode="out-in"` when using `@layer`-based CSS frameworks** — the leaving component's `transitionend` event may never fire, blocking the entering component indefinitely. Use simultaneous transitions instead:
|
||||
```html
|
||||
<!-- ❌ Dangerous with @layer CSS frameworks -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
|
||||
<!-- ✅ Safe: simultaneous leave/enter, always mounts new component -->
|
||||
<Transition name="fade">
|
||||
<component :is="Component" :key="$route.path" />
|
||||
</Transition>
|
||||
```
|
||||
|
||||
2. **Always bind `:key="$route.path"`** on dynamic `<component>` inside `<Transition>` — forces Vue to treat each route as a distinct component instance, ensuring proper enter/leave lifecycle
|
||||
|
||||
3. **Use `!important` on route transition CSS classes** — guarantees transition properties win the `@layer` cascade:
|
||||
```css
|
||||
.fade-enter-active {
|
||||
transition: opacity 0.15s ease-in !important;
|
||||
}
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-out !important;
|
||||
position: absolute !important;
|
||||
width: 100% !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
4. **Give the transition parent `position: relative`** — contains the absolutely-positioned leaving element during the simultaneous transition overlap
|
||||
|
||||
> For full diagnosis steps when a transition-stuck blank screen occurs, see the Debugging Protocol's [Frontend module](file:///home/irahardianto/works/projects/awesome-agv/.agents/skills/debugging-protocol/languages/frontend.md) § CSS × Animation.
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Vue-specific tooling.
|
||||
|
||||
1. **Use `createTestingPinia` to stub stores in component tests**
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
|
||||
const wrapper = mount(TaskView, {
|
||||
global: {
|
||||
plugins: [createTestingPinia({ createSpy: vi.fn })],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
2. **Test component behaviour, not implementation details** — query by accessible role, not CSS class
|
||||
|
||||
3. **Test stores independently** — use `setActivePinia(createPinia())` in store unit tests
|
||||
|
||||
---
|
||||
|
||||
### Linting and Type Checking
|
||||
|
||||
| Tool | Purpose |
|
||||
| ------------------- | --------------------------- |
|
||||
| `vue-tsc --noEmit` | Full-template type checking |
|
||||
| `eslint-plugin-vue` | Vue-specific lint rules |
|
||||
| `prettier` | Canonical formatting |
|
||||
|
||||
See `code-completion-mandate.md` for exact commands.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- TypeScript Idioms and Patterns @typescript-idioms-and-patterns.md
|
||||
- Project Structure — Vue Frontend @project-structure-vue-frontend.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
Reference in new issue
Block a user