Compare commits
No files matched your search
@@ -1,62 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,194 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,171 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,348 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,115 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,92 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,130 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,871 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,315 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,361 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,144 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,216 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,129 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,194 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,169 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,301 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,383 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
name: adr
|
||||
description: Architecture Decision Record skill for documenting significant architectural decisions with context, options, and consequences. Use during the Research phase when choosing between approaches, or whenever the user asks to document an architectural decision.
|
||||
---
|
||||
|
||||
# Architecture Decision Record (ADR) Skill
|
||||
|
||||
## Purpose
|
||||
Document significant architectural decisions so institutional knowledge persists across conversations and team members. ADRs capture the **why**, not just the **what**.
|
||||
|
||||
## When to Invoke
|
||||
- During Research phase (`/1-research`) when a significant architecture decision is identified
|
||||
- When user explicitly asks to document a decision
|
||||
- When choosing between 2+ viable approaches
|
||||
- When introducing a new dependency or pattern
|
||||
- When changing existing architecture
|
||||
|
||||
## ADR Storage
|
||||
ADRs are stored in `docs/decisions/` as numbered files:
|
||||
```
|
||||
docs/decisions/
|
||||
├── 0001-use-postgresql-for-storage.md
|
||||
├── 0002-adopt-feature-based-structure.md
|
||||
├── 0003-use-testcontainers-for-integration.md
|
||||
└── NNNN-short-title.md
|
||||
```
|
||||
|
||||
## ADR Template
|
||||
|
||||
Create the ADR file at `docs/decisions/NNNN-short-title.md`:
|
||||
|
||||
```markdown
|
||||
# NNNN. Short Title
|
||||
|
||||
**Date:** YYYY-MM-DD
|
||||
**Status:** Proposed | Accepted | Deprecated | Superseded by NNNN
|
||||
|
||||
## Context
|
||||
What is the issue that we're seeing that is motivating this decision?
|
||||
Include technical constraints, business requirements, and relevant context.
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option A: {name}
|
||||
- **Pros:** ...
|
||||
- **Cons:** ...
|
||||
- **Effort:** Low/Medium/High
|
||||
|
||||
### Option B: {name}
|
||||
- **Pros:** ...
|
||||
- **Cons:** ...
|
||||
- **Effort:** Low/Medium/High
|
||||
|
||||
### Option C: {name} (if applicable)
|
||||
- **Pros:** ...
|
||||
- **Cons:** ...
|
||||
- **Effort:** Low/Medium/High
|
||||
|
||||
## Decision
|
||||
We chose **Option X** because...
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- What becomes easier or possible as a result
|
||||
|
||||
### Negative
|
||||
- What becomes harder as a result
|
||||
- Technical debt introduced (if any)
|
||||
|
||||
### Risks
|
||||
- What could go wrong with this decision
|
||||
- Mitigation strategies
|
||||
|
||||
## Related
|
||||
- Links to relevant rules, previous ADRs, or external resources
|
||||
```
|
||||
|
||||
## Process Guidelines
|
||||
|
||||
1. **Number sequentially** — check existing ADRs in `docs/decisions/` for the next number
|
||||
2. **Keep titles short** — descriptive enough to identify the decision at a glance
|
||||
3. **Status lifecycle:** `Proposed` → `Accepted` (after approval) → optionally `Deprecated` or `Superseded`
|
||||
4. **Never delete ADRs** — if a decision is reversed, mark as `Superseded by NNNN` and create a new ADR
|
||||
5. **Use Sequential Thinking skill** if the trade-off analysis is complex
|
||||
|
||||
## Rule Compliance
|
||||
ADRs should reference applicable rules:
|
||||
- Architectural Patterns @architectural-pattern.md
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Project Structure @project-structure.md
|
||||
@@ -1,140 +0,0 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Structured code review protocol for inspecting code quality against the full rule set. Use when auditing code written by yourself or another agent, during the /audit workflow, or when the user asks for a code review.
|
||||
---
|
||||
|
||||
# Code Review Skill
|
||||
|
||||
## Purpose
|
||||
Systematically review code against the full antigravity rule set. Catches issues that linters miss: architectural violations, missing observability, business logic errors, pattern inconsistencies.
|
||||
|
||||
## When to Invoke
|
||||
- During the `/audit` workflow (Phase 1: Code Review)
|
||||
- When user asks for a code review outside any workflow
|
||||
- **Best practice:** Invoke in a fresh conversation (not the same one that authored the code) to avoid confirmation bias
|
||||
|
||||
## Review Process
|
||||
|
||||
### 1. Scope the Review
|
||||
Identify the files/features to review. Determine the review scope:
|
||||
- **Feature review** — all files in a feature directory
|
||||
- **PR review** — only changed files
|
||||
- **Full codebase audit** — all features
|
||||
|
||||
### 2. Load the Rule Set
|
||||
Read all applicable rules from `.agents/rules/`. Use `rule-priority.md` for severity classification.
|
||||
|
||||
### 3. Review Categories (Priority Order)
|
||||
|
||||
Review each file/feature against these categories, in order from `rule-priority.md`:
|
||||
|
||||
#### Critical (Must Fix)
|
||||
- **Security** — injection, hardcoded secrets, broken auth
|
||||
- **Data loss** — missing error handling on writes, no transaction boundaries
|
||||
- **Resource leaks** — unclosed connections, missing cleanup
|
||||
|
||||
#### Major (Should Fix)
|
||||
- **Testability** — I/O not behind interfaces, untested error paths
|
||||
- **Observability** — missing logging on operations, no correlation IDs
|
||||
- **Error handling** — empty catch blocks, swallowed errors
|
||||
- **Architecture** — circular dependencies, wrong layer access
|
||||
|
||||
#### Minor (Nice to Fix)
|
||||
- **Pattern consistency** — deviation from established codebase patterns
|
||||
- **Naming** — unclear variable/function names
|
||||
- **Code organization** — functions too long, mixed responsibilities
|
||||
|
||||
#### Nit (Optional)
|
||||
- **Style** — formatting issues the linter would catch
|
||||
- **Documentation** — missing comments on complex logic
|
||||
|
||||
### 4. Produce Findings
|
||||
|
||||
Output a structured findings document:
|
||||
|
||||
```markdown
|
||||
# Code Review: {Feature/Module Name}
|
||||
Date: {date}
|
||||
Reviewer: AI Agent (fresh context)
|
||||
|
||||
## Summary
|
||||
- **Files reviewed:** N
|
||||
- **Issues found:** N (X critical, Y major, Z minor, W nit)
|
||||
|
||||
## Critical Issues
|
||||
- [ ] **[SEC]** {description} — [{file}:{line}](file:///path)
|
||||
- [ ] **[DATA]** {description} — [{file}:{line}](file:///path)
|
||||
|
||||
## Major Issues
|
||||
- [ ] **[TEST]** {description} — [{file}:{line}](file:///path)
|
||||
- [ ] **[OBS]** {description} — [{file}:{line}](file:///path)
|
||||
|
||||
## Minor Issues
|
||||
- [ ] **[PAT]** {description} — [{file}:{line}](file:///path)
|
||||
|
||||
## Nit
|
||||
- [ ] {description} — [{file}:{line}](file:///path)
|
||||
|
||||
## Rules Applied
|
||||
List of rules referenced during this review.
|
||||
```
|
||||
|
||||
### 5. Save the Report
|
||||
|
||||
When invoked via the `/audit` workflow, you **MUST** persist the findings to the repo:
|
||||
|
||||
**Path:** `docs/audits/review-findings-{feature}-{YYYY-MM-DD}-{HHmm}.md`
|
||||
|
||||
1. Create `docs/audits/` if it doesn't exist
|
||||
2. Write the findings document to that path
|
||||
3. This makes the report accessible from other conversations and agents
|
||||
|
||||
When invoked as a standalone review (not via `/audit`), saving to `docs/audits/` is recommended but optional.
|
||||
|
||||
### 6. Severity Tags
|
||||
|
||||
| Tag | Category | Rule Source |
|
||||
| -------- | -------------------- | -------------------------------------------------- |
|
||||
| `[SEC]` | Security | `security-principles.md` |
|
||||
| `[DATA]` | Data integrity | `error-handling-principles.md` |
|
||||
| `[RES]` | Resource leak | `resources-and-memory-management-principles.md` |
|
||||
| `[TEST]` | Testability | `architectural-pattern.md`, `testing-strategy.md` |
|
||||
| `[OBS]` | Observability | `logging-and-observability-mandate.md` |
|
||||
| `[ERR]` | Error handling | `error-handling-principles.md` |
|
||||
| `[ARCH]` | Architecture | `architectural-pattern.md`, `project-structure.md` |
|
||||
| `[PAT]` | Pattern consistency | `code-organization-principles.md` |
|
||||
| `[INT]` | Integration contract | `api-design-principles.md` |
|
||||
| `[DB]` | Database design | `database-design-principles.md` |
|
||||
| `[CFG]` | Configuration | `configuration-management-principles.md` |
|
||||
|
||||
### 7. Language-Specific Anti-Patterns
|
||||
|
||||
Load the anti-pattern checklist for the language(s) under review:
|
||||
|
||||
| Language | Anti-Patterns |
|
||||
|---|---|
|
||||
| **Go** | `languages/go.md` |
|
||||
| **TypeScript** | `languages/typescript.md` *(placeholder — create when needed)* |
|
||||
| **Flutter/Dart** | `languages/flutter.md` *(placeholder — create when needed)* |
|
||||
| **Rust** | `languages/rust.md` *(placeholder — create when needed)* |
|
||||
|
||||
> Anti-patterns listed in language files are **auto-fail** — they require no judgment call. If the pattern exists in the code, it is a finding.
|
||||
|
||||
### 8. Cross-Boundary Checks
|
||||
|
||||
For full audits, cross-boundary concerns (integration contracts, database schema, configuration hygiene, dependency health, test coverage gaps) are checked via the dedicated dimension checklist in the `/audit` workflow — **Phase 1.5: Cross-Boundary Review**.
|
||||
|
||||
When invoking this skill standalone (outside `/audit`), apply the applicable dimensions from that checklist manually and tag findings with `[INT]`, `[DB]`, or `[CFG]` as appropriate.
|
||||
|
||||
**Zero-Findings Guard:** If this review produces fewer than 3 findings, you MUST produce a "Dimensions Covered" attestation section in the findings document, listing each cross-boundary dimension and the specific files or queries you examined. Only then may you declare a clean result.
|
||||
|
||||
---
|
||||
|
||||
## Rule Compliance
|
||||
This skill enforces all rules in `.agents/rules/`. Key references:
|
||||
- Rule Priority @rule-priority.md (severity classification)
|
||||
- Security Principles @security-principles.md
|
||||
- Architectural Patterns @architectural-pattern.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
@@ -1,89 +0,0 @@
|
||||
# Go Anti-Patterns (Auto-Fail)
|
||||
|
||||
> **Load this file** during code reviews of Go code. Patterns listed here are **immediate findings** — no judgment needed. If the pattern exists, it is a finding.
|
||||
|
||||
---
|
||||
|
||||
## Auto-Fail Table
|
||||
|
||||
| Pattern | Severity | Tag | Correct Fix |
|
||||
|---|---|---|---|
|
||||
| `defer X.Close()` (bare) | Critical | `[RES]` | Error-checked closure with `slog.Warn` |
|
||||
| `//nolint:errcheck` | Critical | `[ERR]` | Handle the error — use error-checked closure |
|
||||
| `defer tx.Rollback()` (bare) | Critical | `[RES]` | Error-checked closure with `sql.ErrTxDone` guard |
|
||||
| `fmt.Printf` / `fmt.Println` | Major | `[OBS]` | Replace with `slog` structured logging |
|
||||
| `log.Printf` / `log.Println` | Major | `[OBS]` | Replace with `slog` structured logging |
|
||||
| `resp.Body.Close()` without drain | Major | `[RES]` | `io.Copy(io.Discard, resp.Body)` then close |
|
||||
| Empty `if err != nil {}` block | Critical | `[ERR]` | Handle, return, or log the error |
|
||||
| `_ = someFunc()` (discarded error) | Major | `[ERR]` | Handle the error explicitly |
|
||||
|
||||
---
|
||||
|
||||
## Detection Commands
|
||||
|
||||
Use these grep patterns to scan for anti-patterns before manual review:
|
||||
|
||||
```bash
|
||||
# Bare defer Close/Rollback (no error check)
|
||||
grep -rn 'defer.*\.Close()' --include='*.go'
|
||||
grep -rn 'defer.*\.Rollback()' --include='*.go'
|
||||
|
||||
# nolint:errcheck suppression
|
||||
grep -rn 'nolint:errcheck' --include='*.go'
|
||||
|
||||
# Unstructured logging in production code
|
||||
grep -rn 'fmt\.Printf\|fmt\.Println\|log\.Printf\|log\.Println' --include='*.go' | grep -v '_test.go'
|
||||
|
||||
# Discarded errors
|
||||
grep -rn '_ = ' --include='*.go' | grep -v '_test.go'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Correct Patterns (Reference)
|
||||
|
||||
### Resource Cleanup
|
||||
|
||||
```go
|
||||
// ✅ Rows / Statements / Connections
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
slog.Warn("failed to close rows", "error", err, "operation", "<operation>")
|
||||
}
|
||||
}()
|
||||
|
||||
// ✅ Transaction Rollback
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil && !errors.Is(err, sql.ErrTxDone) {
|
||||
slog.Error("failed to rollback transaction", "error", err, "operation", "<operation>")
|
||||
}
|
||||
}()
|
||||
|
||||
// ✅ HTTP Response Body
|
||||
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)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```go
|
||||
// ❌ fmt.Printf("User %s logged in from %s\n", userID, ip)
|
||||
// ❌ log.Printf("User %s logged in from %s", userID, ip)
|
||||
|
||||
// ✅
|
||||
slog.Info("user login", "userId", userID, "ip", ip)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
- Go Idioms and Patterns @go-idioms-and-patterns.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Resources and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
name: debugging-protocol
|
||||
description: Comprehensive protocol for validating root causes of software issues. Use when you need to systematically debug a complex bug, flaky test, or unknown system behavior by forming hypotheses and validating them with specific tasks.
|
||||
---
|
||||
|
||||
# Debugging Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides a rigorous framework for debugging complex software issues. It moves beyond ad-hoc troubleshooting to a structured process of hypothesis generation and validation.
|
||||
|
||||
Use this skill to:
|
||||
1. Formalize a debugging session.
|
||||
2. Systematically eliminate potential root causes.
|
||||
3. Document findings for future reference or team communication.
|
||||
|
||||
## Protocol Workflow
|
||||
|
||||
To run a structured debugging session, follow these steps:
|
||||
|
||||
### 1. Initialize the Session
|
||||
Create a new debugging document using the provided template. This serves as the "source of truth" for the investigation.
|
||||
|
||||
**Template location:** `assets/debugging-session-template.md`
|
||||
|
||||
**Save to:** `docs/debugging/{issue-name}-{YYYY-MM-DD}-{HHmm}.md`
|
||||
|
||||
1. Create `docs/debugging/` if it doesn't exist
|
||||
2. Copy the template and fill in the issue details
|
||||
3. This makes the session accessible from other conversations and agents (e.g., when handing off to a `/quick-fix` or `/orchestrator` workflow)
|
||||
|
||||
### 2. Define the Problem
|
||||
Clearly articulate the **System Context** and **Problem Statement**.
|
||||
* **Symptom**: What is the observable behavior? How does it differ from expected behavior?
|
||||
* **Scope**: Which components are involved?
|
||||
|
||||
### 3. Formulate Hypotheses
|
||||
List distinct, testable hypotheses.
|
||||
* Avoid vague guesses.
|
||||
* Differentiate between layers (e.g., "Frontend Hypothesis" vs "Backend Hypothesis").
|
||||
* Example: "Race condition in UI state update" vs "Database schema misconfiguration".
|
||||
|
||||
### 4. Design Validation Tasks
|
||||
For each hypothesis, design a specific validation task.
|
||||
* **Objective**: What are you trying to prove or disprove?
|
||||
* **Steps**: Precise, reproducible actions.
|
||||
* **Code Pattern**: Provide the exact code or command to run (e.g., a specific SQL query, a Python script using the client library, a `curl` command).
|
||||
* **Success Criteria**: Explicitly state what output confirms the hypothesis.
|
||||
|
||||
### 5. Execute and Document
|
||||
Run the tasks in order. For each task, record:
|
||||
* **Status**: ✅ VALIDATED, ❌ FAILED, or ⚠️ INCONCLUSIVE.
|
||||
* **Findings**: Key observations and raw evidence (logs, screenshots).
|
||||
* **Conclusion**: Does this support or refute the hypothesis?
|
||||
|
||||
### 6. Determine Root Cause
|
||||
Synthesize the findings into a **Root Cause Analysis**.
|
||||
* Identify the Primary Root Cause.
|
||||
* Assign a Confidence Level.
|
||||
* Propose specific fixes.
|
||||
|
||||
## Best Practices
|
||||
|
||||
* **Be Specific**: Don't just say "check the logs." Say "grep for 'Error 500' in `/var/log/nginx/access.log`".
|
||||
* **Isolate Variables**: Change one thing at a time.
|
||||
* **Validate Assumptions**: Verify configuration and versions first (e.g., "Task 1: Validate Current Schema").
|
||||
* **Preserve Evidence**: Keep the specific trace IDs, log timestamps, or reproduction scripts.
|
||||
|
||||
## Language-Specific Modules
|
||||
|
||||
The `languages/` directory contains **modular, language-specific debugging guides**. When debugging a project, load the relevant language module to augment this protocol with language-specific tools, hypothesis categories, and validation strategies.
|
||||
|
||||
**Convention:** Each module is a standalone markdown file at `languages/{language}.md`.
|
||||
|
||||
**How to use:**
|
||||
1. Identify the primary language of the codebase being debugged
|
||||
2. Load the corresponding module from `languages/`
|
||||
3. Integrate its toolchain, hypothesis categories, and validation tasks into your debugging session
|
||||
|
||||
**Available modules:**
|
||||
|
||||
| Module | Languages/Runtimes |
|
||||
|---|---|
|
||||
| [Rust](languages/rust.md) | Rust (cargo, rustc, tokio) |
|
||||
| [Frontend](languages/frontend.md) | Vue 3, React, browser, Vite |
|
||||
|
||||
> **Contributing new modules:** To add support for a new language, create `languages/{language}.md` following the structure of existing modules. Each module should include: a toolchain reference table, language-specific hypothesis categories, validation task patterns, and an error-type-to-first-action quick reference.
|
||||
|
||||
## Rule Compliance
|
||||
When debugging, verify against:
|
||||
- Error Handling Principles @error-handling-principles.md (proper error propagation)
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md (structured logging for diagnostics)
|
||||
- Testing Strategy @testing-strategy.md (regression test for the fix)
|
||||
@@ -1,82 +0,0 @@
|
||||
# Debugging Session: [Issue Name]
|
||||
|
||||
## SYSTEM CONTEXT
|
||||
[Describe the system components involved, relevant technologies, and the general environment.]
|
||||
|
||||
## PROBLEM STATEMENT
|
||||
|
||||
**Symptom**:
|
||||
[Describe the observable issue. What is happening vs what should happen?]
|
||||
|
||||
**Hypotheses**:
|
||||
1. **[Layer/Component] Hypothesis**: [Description of potential cause]
|
||||
2. **[Layer/Component] Hypothesis**: [Description of potential cause]
|
||||
|
||||
## VALIDATION TASKS
|
||||
|
||||
### Task 1: [Task Name]
|
||||
|
||||
**Objective**: [What specific fact or behavior are we verifying?]
|
||||
|
||||
**Steps**:
|
||||
1. [Step 1]
|
||||
2. [Step 2]
|
||||
3. ...
|
||||
|
||||
**Required Code Pattern/Command**:
|
||||
```[language]
|
||||
[Code or command to execute]
|
||||
```
|
||||
|
||||
**Success Criteria**: [What specific output confirms or denies the hypothesis?]
|
||||
|
||||
---
|
||||
|
||||
### Task 2: [Task Name]
|
||||
|
||||
**Objective**: ...
|
||||
|
||||
...
|
||||
|
||||
## OUTPUT FORMAT (for each task)
|
||||
|
||||
```markdown
|
||||
### Task N: [Task Name]
|
||||
|
||||
**Status**: ✅ VALIDATED / ❌ FAILED / ⚠️ INCONCLUSIVE
|
||||
|
||||
**Findings**:
|
||||
- [Observation 1]
|
||||
- [Observation 2]
|
||||
|
||||
**Evidence**:
|
||||
[Logs, screenshots, output]
|
||||
|
||||
**Conclusion**: [Impact on hypotheses]
|
||||
```
|
||||
|
||||
## FINAL ROOT CAUSE DETERMINATION
|
||||
|
||||
```markdown
|
||||
## ROOT CAUSE ANALYSIS SUMMARY
|
||||
|
||||
**Primary Root Cause**: [Component/Reason]
|
||||
|
||||
**Confidence Level**: [High / Medium / Low]
|
||||
|
||||
**Supporting Evidence**:
|
||||
1. [Evidence 1]
|
||||
2. [Evidence 2]
|
||||
|
||||
**Recommended Fix Priority**:
|
||||
1. [Fix A]
|
||||
2. [Fix B]
|
||||
|
||||
**Risks if Wrong**:
|
||||
- [Risk 1]
|
||||
```
|
||||
|
||||
## EXECUTION GUIDELINES
|
||||
- Execute tasks in order.
|
||||
- Document all commands and outputs.
|
||||
- Adapt the plan if new information invalidates later tasks.
|
||||
@@ -1,342 +0,0 @@
|
||||
# Frontend Debugging Module
|
||||
|
||||
Language-specific debugging guide for frontend projects (Vue 3, React, browser-based SPAs). Use alongside the main [Debugging Protocol](../SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Toolchain Reference
|
||||
|
||||
| Tool | Purpose | When to Use |
|
||||
|---|---|---|
|
||||
| Chrome DevTools — **Console** | Runtime errors, warnings, `console.debug` traces | **Always first** — fastest signal |
|
||||
| Chrome DevTools — **Network** | Request waterfall, response status, payload shape | Data not rendering, 401/403/500, CORS |
|
||||
| Chrome DevTools — **Elements** | Computed styles, DOM structure, CSS specificity | Styling / layout issues |
|
||||
| Chrome DevTools — **Performance** | Frame rate, scripting time, rendering phases | Jank, slow interactions |
|
||||
| Chrome DevTools — **Application** | localStorage, sessionStorage, cookies, service workers | Auth state, cached flags, stale SW |
|
||||
| Vue DevTools (browser extension) | Component tree, Pinia state, router history, event timeline | Vue-specific state/lifecycle issues |
|
||||
| `router.afterEach` / `beforeEach` logging | Navigation guard execution trace | Routing bugs, blank screens, redirect loops |
|
||||
| `onMounted` / `useEffect` logging | Component lifecycle confirmation | Component "never mounts" symptoms |
|
||||
| `vite dev --force` | Force-clear Vite module cache | Stale build artifacts after dependency changes |
|
||||
| `npx vue-tsc -b` | Full template type checking | Type errors that only surface at build time |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Initialize Session — Frontend Context
|
||||
|
||||
Add these fields to the debugging session document:
|
||||
|
||||
```markdown
|
||||
### Frontend Context
|
||||
- **Framework version:** (e.g. Vue 3.5.x, React 19.x)
|
||||
- **Bundler:** (e.g. Vite 7.x)
|
||||
- **CSS framework:** (e.g. Tailwind v4, none)
|
||||
- **State management:** (e.g. Pinia 3.x, Redux, Zustand)
|
||||
- **Router:** (e.g. Vue Router 5.x, React Router 7.x)
|
||||
- **Browser:** (exact version — behavior varies across engines)
|
||||
- **PWA/Service Worker:** yes / no (stale caches are a common red herring)
|
||||
- **Auth provider:** (e.g. Supabase Auth, Firebase Auth, custom JWT)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Hypothesis Categories
|
||||
|
||||
Common frontend-specific hypothesis categories:
|
||||
|
||||
| Category | Example Hypotheses |
|
||||
|---|---|
|
||||
| **Routing & Navigation** | Guard blocks route; redirect loop between auth/onboarding; `router.replace()` resolves but component never mounts; stale `meta` on route definition |
|
||||
| **State Management** | Pinia store holds stale data after auth change; double-fetch from co-mounted components; reactive loss from `const { x } = store` without `storeToRefs`; store initialized before auth resolves |
|
||||
| **CSS × Animation** | `transitionend` event never fires due to `@layer` cascade; `mode="out-in"` blocks entering component indefinitely; animation stuck in leaving state; blank screen with zero console errors |
|
||||
| **Data & Rendering** | Backend payload shape mismatch (missing field → `undefined` → silent `v-if` failure); wrong reactive access pattern (`v-if="fn()"` instead of `computed`); component renders before async data resolves |
|
||||
| **Auth & Session** | Token expired but UI doesn't redirect; `localStorage` holds stale session after logout; race between auth state resolution and guard evaluation; middleware applies to wrong routes |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Validation Task Patterns
|
||||
|
||||
### Routing & Navigation — Blank Screen After Navigation
|
||||
|
||||
**Symptom:** User navigates, router reports success, destination component never renders. Page is blank with zero console errors.
|
||||
|
||||
**Validation steps:**
|
||||
|
||||
1. **Confirm no component mount** — add lifecycle logging to the destination:
|
||||
```javascript
|
||||
// Vue 3
|
||||
onMounted(() => console.debug('[TargetView] onMounted fired'));
|
||||
// React
|
||||
useEffect(() => console.debug('[TargetView] mounted'), []);
|
||||
```
|
||||
If the mount log never fires but the router reports navigation complete → this is NOT a routing logic bug.
|
||||
|
||||
2. **Trace the guard chain** — add diagnostic logging:
|
||||
```typescript
|
||||
router.beforeEach((to, from) => {
|
||||
console.debug(`[Router] beforeEach: ${from.path} → ${to.path}`);
|
||||
});
|
||||
router.afterEach((to) => {
|
||||
console.debug(`[Router] afterEach: arrived at ${to.path}`);
|
||||
});
|
||||
```
|
||||
If `afterEach` fires but `onMounted` does not → suspect the **transition mechanism** (see CSS × Animation below).
|
||||
|
||||
3. **Check for redirect loops** — watch the URL bar or router logs for oscillating paths (e.g. `/login` → `/home` → `/onboarding` → `/login`).
|
||||
|
||||
4. **Verify auth state at guard time** — log the auth store's state inside each guard to confirm it's resolved (not still loading).
|
||||
|
||||
---
|
||||
|
||||
### State Management — Stale or Missing Data
|
||||
|
||||
**Symptom:** Component renders but shows stale data, or renders empty despite successful API call.
|
||||
|
||||
**Validation steps:**
|
||||
|
||||
1. **Inspect Pinia state via Vue DevTools** — check the store's current values. Is the data present?
|
||||
|
||||
2. **Check for destructuring reactivity loss:**
|
||||
```typescript
|
||||
// ❌ Loses reactivity — x is a plain value snapshot
|
||||
const { tasks } = useTaskStore();
|
||||
// ✅ Preserves reactivity
|
||||
const { tasks } = storeToRefs(useTaskStore());
|
||||
```
|
||||
|
||||
3. **Check for race conditions with auth:**
|
||||
```typescript
|
||||
// The store action fires before auth resolves → API returns 401 → store stays empty
|
||||
// Fix: await auth resolution before first store fetch
|
||||
```
|
||||
|
||||
4. **Check for duplicate fetches from co-mounted components:**
|
||||
- Open Network tab → filter by XHR → navigate to the page
|
||||
- If two identical requests appear → the store needs a loading-state guard
|
||||
|
||||
---
|
||||
|
||||
### CSS × Animation — Transition Stuck (Blank Screen)
|
||||
|
||||
> **This section subsumes the former `css-transition-pitfalls` skill.**
|
||||
|
||||
**Symptom:** Component "never mounts" after a route change. Blank screen with zero console errors. Works on hard refresh but recurs on SPA navigation. `router.afterEach` fires but `onMounted` does not.
|
||||
|
||||
#### The Anti-Pattern
|
||||
|
||||
```
|
||||
User clicks link → Router navigates → Old component should leave → New component should enter
|
||||
↓
|
||||
CSS framework's @layer rules
|
||||
override transition properties
|
||||
↓
|
||||
transitionend event NEVER fires
|
||||
↓
|
||||
SPA framework waits forever
|
||||
↓
|
||||
New component NEVER mounts
|
||||
↓
|
||||
BLANK SCREEN (no errors)
|
||||
```
|
||||
|
||||
#### Why It's Hard to Catch
|
||||
|
||||
1. **No errors** — everything completes from the router's perspective
|
||||
2. **Works on refresh** — hard refresh bypasses the SPA transition entirely
|
||||
3. **Looks like a logic bug** — the natural instinct is to debug auth state, guards, or template conditionals
|
||||
4. **Intermittent** — may only trigger on specific route pairs where the leaving component has conflicting animations
|
||||
|
||||
#### Affected SPA Frameworks
|
||||
|
||||
| SPA Framework | Transition Mechanism | Vulnerable Mode |
|
||||
|---|---|---|
|
||||
| **Vue 3** | `<Transition mode="out-in">` | `mode="out-in"` blocks enter until leave completes |
|
||||
| **React** (Framer Motion) | `AnimatePresence mode="wait"` | `mode="wait"` blocks enter until exit completes |
|
||||
| **React** (React Transition Group) | `SwitchTransition mode="out-in"` | Same pattern |
|
||||
| **Svelte** | `transition:` directive with `|local` | Less common but possible |
|
||||
|
||||
#### CSS Frameworks That Can Cause This
|
||||
|
||||
| CSS Framework | Why |
|
||||
|---|---|
|
||||
| **Tailwind v4** | Uses `@layer theme, base, components, utilities` — unlayered styles (like Vue's transition classes) can be unexpectedly deprioritized |
|
||||
| **Open Props** | Uses `@layer` for custom property organization |
|
||||
| **Any framework using `@layer`** | The CSS Cascade spec gives layered styles lower priority than unlayered, BUT framework bundlers may re-layer your styles |
|
||||
|
||||
#### Validation Steps
|
||||
|
||||
1. **Confirm the symptom class** — add `onMounted` logging to the destination component. If mount log never fires but `afterEach` does → transition stuck.
|
||||
|
||||
2. **Rule out logic issues (time-box: 15 minutes):**
|
||||
- Navigation guard allows the route (check `afterEach` logging)
|
||||
- Auth state is correct at navigation time
|
||||
- No redirect loops
|
||||
|
||||
3. **Inspect the transition mechanism** — check your root layout for sequential transition modes:
|
||||
```html
|
||||
<!-- Vue: look for mode="out-in" -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<!-- React Framer Motion: look for mode="wait" -->
|
||||
<AnimatePresence mode="wait">
|
||||
<!-- React Transition Group: look for mode="out-in" -->
|
||||
<SwitchTransition mode="out-in">
|
||||
```
|
||||
If found → this is almost certainly the cause.
|
||||
|
||||
4. **Verify CSS layer interference:**
|
||||
```bash
|
||||
# Check for @layer in built CSS
|
||||
grep -r "@layer" node_modules/tailwindcss/ | head -5
|
||||
grep -r "@import.*tailwindcss" src/
|
||||
# In browser DevTools: inspect the leaving element during transition
|
||||
# Look for: computed opacity, computed transition property
|
||||
```
|
||||
|
||||
5. **Apply the fix — remove sequential transition mode:**
|
||||
```html
|
||||
<!-- BEFORE (broken): sequential, depends on transitionend -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
|
||||
<!-- AFTER (fixed): simultaneous, always mounts new component -->
|
||||
<Transition name="fade">
|
||||
<component :is="Component" :key="$route.path" />
|
||||
</Transition>
|
||||
```
|
||||
|
||||
Update CSS to handle simultaneous leave/enter:
|
||||
```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;
|
||||
}
|
||||
/* Parent needs position:relative to contain the absolute element */
|
||||
#main-content {
|
||||
position: relative;
|
||||
}
|
||||
```
|
||||
|
||||
> **Why `!important`?** CSS `@layer` cascade can override your transition classes. `!important` guarantees your transition properties apply regardless of layer ordering.
|
||||
|
||||
#### Key Insight
|
||||
|
||||
> **When a component doesn't mount after navigation: suspect the transition mechanism FIRST, not the routing logic.** The router will report success even when the component never renders. This is counter-intuitive because the blank screen looks like a routing/auth problem, but it's actually a CSS/animation problem.
|
||||
|
||||
---
|
||||
|
||||
### Data & Rendering — Silent Template Failures
|
||||
|
||||
**Symptom:** Page renders but expected content is missing. No console errors. Data is present in the store.
|
||||
|
||||
**Validation steps:**
|
||||
|
||||
1. **Check for `v-if` on optional properties:**
|
||||
```html
|
||||
<!-- Silent failure if exercise.options is undefined — v-if produces no error -->
|
||||
<div v-if="exercise.options.length > 0">
|
||||
<!-- Fix: optional chaining -->
|
||||
<div v-if="exercise.options?.length">
|
||||
```
|
||||
|
||||
2. **Check for plain function calls in templates:**
|
||||
```html
|
||||
<!-- ❌ Re-evaluates on every render cycle; may return inconsistent results -->
|
||||
<div v-if="isVisible()">
|
||||
<!-- ✅ Cached, reactive -->
|
||||
<div v-if="isVisibleComputed">
|
||||
```
|
||||
|
||||
3. **Check for backend payload mismatches** — log the raw API response and compare field names/types against what the component expects. Common: `snake_case` from backend vs `camelCase` in frontend.
|
||||
|
||||
4. **Check for async rendering issues:**
|
||||
```html
|
||||
<!-- Component renders before data arrives — shows empty state permanently -->
|
||||
<TaskCard :task="task" />
|
||||
<!-- Fix: guard with v-if -->
|
||||
<TaskCard v-if="task" :task="task" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Auth & Session — Redirect Loops and Ghost Sessions
|
||||
|
||||
**Symptom:** User gets stuck in a redirect loop between login, onboarding, and home pages. Or: user appears logged out despite having valid credentials.
|
||||
|
||||
**Validation steps:**
|
||||
|
||||
1. **Check localStorage for stale auth tokens:**
|
||||
```javascript
|
||||
// In console
|
||||
Object.keys(localStorage).filter(k => k.includes('auth') || k.includes('supabase') || k.includes('token'));
|
||||
```
|
||||
|
||||
2. **Verify auth state is resolved before guards run:**
|
||||
```typescript
|
||||
router.beforeEach(async (to) => {
|
||||
console.debug('[Guard] authStore.isAuthenticated:', authStore.isAuthenticated);
|
||||
console.debug('[Guard] authStore.isLoading:', authStore.isLoading);
|
||||
// If isLoading is true here, the guard is racing the auth resolution
|
||||
});
|
||||
```
|
||||
|
||||
3. **Check for circular guard logic:**
|
||||
- Guard A: "if not authenticated → redirect to /login"
|
||||
- Guard B: "if authenticated and not onboarded → redirect to /onboarding"
|
||||
- Guard C: "if on /onboarding and already onboarded → redirect to /home"
|
||||
- If the onboarding flag is stale or the database read is async → loop
|
||||
|
||||
4. **Test by clearing all storage and hard-refreshing:**
|
||||
```javascript
|
||||
localStorage.clear(); sessionStorage.clear(); location.reload();
|
||||
```
|
||||
If the issue resolves → the problem is stale cached state, not logic.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Confidence Adjustments
|
||||
|
||||
| Evidence | Confidence Impact |
|
||||
|---|---|
|
||||
| `onMounted` log never fires but `afterEach` does | **High** — definitively a transition/rendering issue, not routing |
|
||||
| DevTools Network shows no request fired | **High** — data never fetched (guard blocked, or fetch never called) |
|
||||
| Vue DevTools shows correct Pinia state but UI is empty | **High** — template rendering bug (wrong `v-if`, missing reactivity) |
|
||||
| Issue resolves on hard refresh but recurs on SPA nav | **High** — transition-stuck, service worker cache, or stale SPA state |
|
||||
| `@layer` found in CSS framework and `mode="out-in"` is used | **Definitive** for transition-stuck diagnosis |
|
||||
| Issue only appears on specific route pairs | **Medium-High** — likely CSS/animation interaction specific to the leaving component |
|
||||
| Console shows no errors at all | **Medium** — rules out JS exceptions; narrows to CSS/template/state issues |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Symptom → First Action
|
||||
|
||||
| Symptom | First Action |
|
||||
|---|---|
|
||||
| Blank screen after navigation (no errors) | Add `onMounted` logging → if never fires, check `<Transition mode>` |
|
||||
| Blank screen after navigation (console errors) | Read the error — usually a runtime exception in the entering component |
|
||||
| Redirect loop (URL oscillates) | Log all guards with `beforeEach` → trace the loop pattern |
|
||||
| Data present in store but not rendered | Check `v-if` conditions for undefined/null access; check for destructuring without `storeToRefs` |
|
||||
| Component renders then immediately disappears | Check for a `watch` or `watchEffect` that re-triggers a navigation or state reset |
|
||||
| API call fires twice | Check for co-mounted components both calling the same store action; add loading-state guard |
|
||||
| Works locally but not in production | Check service worker cache; run `vite build` locally to reproduce; check env variables |
|
||||
| Works on refresh but fails on SPA navigation | Suspect transition mechanism or stale component state from previous route |
|
||||
| Auth state incorrect in guard | Log auth store state inside the guard; check if auth initialization is awaited before router starts |
|
||||
| Styles not applying | Check CSS specificity vs `@layer` cascade; inspect computed styles in DevTools Elements tab |
|
||||
|
||||
---
|
||||
|
||||
## Related Principles
|
||||
- Vue Idioms and Patterns @vue-idioms-and-patterns.md
|
||||
- TypeScript Idioms and Patterns @typescript-idioms-and-patterns.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
@@ -1,136 +0,0 @@
|
||||
# Rust Debugging Module
|
||||
|
||||
Language-specific debugging guide for Rust projects. Use alongside the main [Debugging Protocol](../SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Toolchain Reference
|
||||
|
||||
| Tool | Purpose | When to Use |
|
||||
| --------------------- | ----------------------------------------- | --------------------------------------------------- |
|
||||
| `cargo check` | Fast type/borrow checking (no codegen) | **Always first** — 2-5× faster than `cargo build` |
|
||||
| `cargo clippy` | Lint for idiomatic issues and subtle bugs | After `cargo check` passes — catches logical errors |
|
||||
| `cargo test` | Run test suite | Validate fixes, reproduce failures |
|
||||
| `cargo expand` | Expand macros to see generated code | When a macro-generated error is unclear |
|
||||
| `cargo tree` | Visualize dependency graph | Dependency conflicts, version mismatches |
|
||||
| `cargo audit` | Check for known vulnerabilities | Security-related investigations |
|
||||
| `RUST_BACKTRACE=1` | Full backtrace on panic | Runtime panics — always enable first |
|
||||
| `RUST_BACKTRACE=full` | Backtrace with line numbers | When `=1` doesn't give enough detail |
|
||||
| `miri` | Detect undefined behavior in unsafe code | Suspected UB, memory safety issues |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Initialize Session — Rust Context
|
||||
|
||||
Add these fields to the debugging session document:
|
||||
|
||||
```markdown
|
||||
### Rust Context
|
||||
- **Rust toolchain version:** (output of `rustc --version`)
|
||||
- **Cargo.toml key dependencies:** (list with versions)
|
||||
- **Build profile:** debug / release
|
||||
- **Target:** default / cross-compile target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Hypothesis Categories
|
||||
|
||||
Common Rust-specific hypothesis categories:
|
||||
|
||||
| Category | Example Hypotheses |
|
||||
| ------------------- | -------------------------------------------------------------------------- |
|
||||
| **Borrow Checker** | "Function borrows `self` mutably while an immutable borrow is still live" |
|
||||
| **Lifetime** | "Returned reference outlives the data it points to" |
|
||||
| **Async/Runtime** | "Blocking call inside async context starves the Tokio runtime" |
|
||||
| **Type Mismatch** | "Trait bound not satisfied due to missing `Send`/`Sync` on spawned future" |
|
||||
| **FFI/Unsafe** | "Raw pointer from C FFI is null but not checked before dereference" |
|
||||
| **Macro Expansion** | "Procedural macro generates code that conflicts with local type names" |
|
||||
| **Dependency** | "Two crates depend on different major versions of the same crate" |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Validation Task Patterns
|
||||
|
||||
### Borrow Checker Errors — Reading Strategy
|
||||
|
||||
When the compiler emits a borrow error:
|
||||
|
||||
1. **Read the error bottom-up** — the last line shows *where* the conflict is, not *why*
|
||||
2. **Find the two conflicting borrows** — the error always names them
|
||||
3. **Check the lifetime of each borrow** — does one live longer than expected?
|
||||
4. **Common fixes:**
|
||||
- Clone the data (only if cheap — document with `// CLONE:`)
|
||||
- Restructure to avoid overlapping borrows (split struct into components)
|
||||
- Use `Cell`/`RefCell` for interior mutability (single-threaded only)
|
||||
- Use scoping braces `{ }` to limit borrow lifetime
|
||||
|
||||
### Async Debugging
|
||||
|
||||
```bash
|
||||
# Enable Tokio's runtime tracing
|
||||
RUSTFLAGS="--cfg tokio_unstable" cargo build
|
||||
# Then use tokio-console for live runtime inspection
|
||||
tokio-console
|
||||
```
|
||||
|
||||
Validation steps for async issues:
|
||||
1. Check for `spawn_blocking` around any blocking I/O
|
||||
2. Verify all `.await` points — are any holding locks across them?
|
||||
3. Look for `select!` branches that aren't cancellation-safe
|
||||
4. Run with `tokio::time::pause()` in tests for deterministic timing
|
||||
|
||||
### Macro Debugging
|
||||
|
||||
```bash
|
||||
# Expand all macros in a specific file
|
||||
cargo expand --lib path::to::module
|
||||
|
||||
# Expand only derive macros
|
||||
cargo expand --lib path::to::module 2>&1 | grep -A 50 "impl"
|
||||
```
|
||||
|
||||
### Unsafe / UB Detection
|
||||
|
||||
```bash
|
||||
# Run tests under Miri (detects UB, use-after-free, unaligned access)
|
||||
cargo +nightly miri test
|
||||
|
||||
# Common Miri flags
|
||||
MIRIFLAGS="-Zmiri-backtrace=full" cargo +nightly miri test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Confidence Adjustments
|
||||
|
||||
| Evidence | Confidence Impact |
|
||||
| ---------------------------------------------- | -------------------------------------------- |
|
||||
| `cargo check` error points exactly at the line | High — compiler errors are precise |
|
||||
| `cargo clippy` lint matches the symptom | Medium-High — Clippy catches many real bugs |
|
||||
| `miri` reports UB | **Definitive** for unsafe code paths |
|
||||
| `cargo expand` shows unexpected generated code | High — confirms macro-related issues |
|
||||
| "works in debug, fails in release" pattern | Likely UB or optimization-sensitive ordering |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Error Code → First Action
|
||||
|
||||
| Error Type | First Action |
|
||||
| ----------------------------- | ------------------------------------------------------------ |
|
||||
| `E0382` (use of moved value) | Check if value can be borrowed instead of moved |
|
||||
| `E0502` (conflicting borrows) | Identify the two borrows, scope them apart |
|
||||
| `E0106` (missing lifetime) | Add lifetime parameter, check if owned type is better |
|
||||
| `E0277` (trait not satisfied) | Check `Send`/`Sync` bounds, look for non-Send types in async |
|
||||
| `E0308` (type mismatch) | Check `From`/`Into` impls, verify generic constraints |
|
||||
| Runtime panic | `RUST_BACKTRACE=1 cargo test` — read the backtrace |
|
||||
| Segfault / UB | `cargo +nightly miri test` — Miri will find it |
|
||||
| Macro error | `cargo expand` the module — read the generated code |
|
||||
|
||||
---
|
||||
|
||||
## Related Principles
|
||||
- Rust Idioms and Patterns @rust-idioms-and-patterns.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Generates distinctive, production-grade frontend interfaces and artifacts (React, Vue, HTML/CSS). Prioritizes bold aesthetics, unique typography, and motion to avoid generic designs. Use when building websites, landing pages, dashboards, posters, or when the user requests to style, beautify, or create visually striking UI.
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
9. All implementations must also pass through the agent's standard rule checks — this skill enhances visual output, but does not override engineering rules.
|
||||
|
||||
## Rule Compliance
|
||||
Before implementing, verify against:
|
||||
- Project Structure @project-structure.md (component organization)
|
||||
- Testing Strategy @testing-strategy.md (component tests)
|
||||
- Security Principles @security-principles.md (XSS prevention)
|
||||
- Accessibility Principles @accessibility-principles.md (WCAG compliance)
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: You are capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
name: guardrails
|
||||
description: Pre-flight checklist and post-implementation self-review protocol. Use before generating any code (pre-flight) and after writing code but before verification (self-review) to catch issues early.
|
||||
---
|
||||
|
||||
# Agent Guardrails Skill
|
||||
|
||||
## Purpose
|
||||
Structured checklists that ensure the agent considers all applicable rules before and after writing code. Catches issues that would otherwise only surface during verification.
|
||||
|
||||
## When to Invoke
|
||||
- **Pre-Flight:** At the start of Phase 2 (Implement), before writing any code
|
||||
- **Self-Review:** At the end of Phase 2 (Implement), after writing code but before Phase 3/4
|
||||
|
||||
---
|
||||
|
||||
## Pre-Flight Checklist
|
||||
|
||||
Run through this checklist **before writing any code**:
|
||||
|
||||
- [ ] Identified all applicable rules from `.agents/rules/`
|
||||
- [ ] Searched for existing patterns in codebase (Pattern Discovery Protocol from `architectural-pattern.md`)
|
||||
- [ ] Confirmed project structure alignment (`project-structure.md`)
|
||||
- [ ] Identified I/O boundaries that need abstraction
|
||||
- [ ] Determined test strategy (unit/integration/E2E)
|
||||
- [ ] Reviewed `rule-priority.md` for any potential conflicts
|
||||
|
||||
If any item cannot be checked, **stop and resolve** before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Post-Implementation Self-Review
|
||||
|
||||
Run through this checklist **after writing code, before verification**:
|
||||
|
||||
### Security
|
||||
- [ ] No hardcoded secrets or configuration values
|
||||
- [ ] All user input validated at system boundaries
|
||||
- [ ] Parameterized queries (no string concatenation for SQL)
|
||||
|
||||
### Testability
|
||||
- [ ] All I/O operations behind interfaces/abstractions
|
||||
- [ ] Business logic is pure (no side effects in calculations)
|
||||
- [ ] Dependencies injected, not hardcoded
|
||||
|
||||
### Observability
|
||||
- [ ] All public operation entry points logged (start/success/failure)
|
||||
- [ ] Structured logging with correlation IDs
|
||||
- [ ] Appropriate log levels (not everything is INFO)
|
||||
|
||||
### Error Handling
|
||||
- [ ] Error paths handled explicitly (no empty catch blocks)
|
||||
- [ ] Errors provide context (wrapped with additional info)
|
||||
- [ ] Resources cleaned up in error paths (defer/finally)
|
||||
|
||||
### Testing
|
||||
- [ ] Tests cover happy path
|
||||
- [ ] Tests cover at least 2 error paths
|
||||
- [ ] Tests cover edge cases relevant to the domain
|
||||
- [ ] If I/O adapters were modified: integration tests exist and pass
|
||||
- [ ] If UI was modified: E2E tests exist (or Phase 3.5 is planned)
|
||||
|
||||
### Consistency
|
||||
- [ ] Follows existing codebase patterns (>80% consistency)
|
||||
- [ ] Naming conventions match the codebase
|
||||
- [ ] File organization matches `project-structure.md`
|
||||
|
||||
---
|
||||
|
||||
## Language-Specific Self-Review
|
||||
|
||||
After completing the universal checklist above, load the relevant language-specific checklist:
|
||||
|
||||
| Language | Checklist |
|
||||
|---|---|
|
||||
| **Go** | `languages/go.md` |
|
||||
| **TypeScript** | `languages/typescript.md` *(placeholder — create when needed)* |
|
||||
| **Flutter/Dart** | `languages/flutter.md` *(placeholder — create when needed)* |
|
||||
| **Rust** | `languages/rust.md` *(placeholder — create when needed)* |
|
||||
|
||||
> Only load the file for languages you are actively writing. If the file doesn't exist yet, skip — but flag its absence so it can be created.
|
||||
|
||||
---
|
||||
|
||||
## Rule Compliance
|
||||
This skill enforces:
|
||||
- All mandates (always-on rules)
|
||||
- Architectural Patterns @architectural-pattern.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Rule Priority @rule-priority.md
|
||||
@@ -1,71 +0,0 @@
|
||||
# Go Self-Review Checklist
|
||||
|
||||
> **Load this file** after completing the universal self-review in `SKILL.md`, when the code under review is Go.
|
||||
|
||||
---
|
||||
|
||||
## Resource Cleanup
|
||||
|
||||
- [ ] **No bare `defer X.Close()`** — all deferred closers use error-checked closures with `slog`
|
||||
- [ ] **No `//nolint:errcheck`** directives anywhere in new code
|
||||
- [ ] **Transaction rollbacks** check for `sql.ErrTxDone` before logging
|
||||
- [ ] **HTTP response bodies** are drained (`io.Copy(io.Discard, ...)`) before closing
|
||||
|
||||
### Correct Patterns
|
||||
|
||||
```go
|
||||
// ✅ Row/statement/connection Close
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
slog.Warn("failed to close rows", "error", err, "operation", "<operation>")
|
||||
}
|
||||
}()
|
||||
|
||||
// ✅ Transaction Rollback
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil && !errors.Is(err, sql.ErrTxDone) {
|
||||
slog.Error("failed to rollback transaction", "error", err, "operation", "<operation>")
|
||||
}
|
||||
}()
|
||||
|
||||
// ✅ HTTP Response Body
|
||||
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)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
- [ ] **No `fmt.Printf` / `fmt.Println` / `log.Printf`** — only `slog` in production code
|
||||
- [ ] All log calls use **structured key-value pairs** (no `fmt.Sprintf` inside log messages)
|
||||
- [ ] Error logs include `"error", err` as a key-value pair
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- [ ] All errors are wrapped with context using `%w`: `fmt.Errorf("doing X: %w", err)`
|
||||
- [ ] No duplicate wrapping of the same error
|
||||
- [ ] Sentinel errors used for expected conditions (`ErrNotFound`, `ErrUnauthorized`)
|
||||
|
||||
---
|
||||
|
||||
## Static Analysis
|
||||
|
||||
- [ ] `go vet ./...` passes with zero issues
|
||||
- [ ] No new `//nolint:` directives without a `// NOLINT:` rationale comment
|
||||
- [ ] `//nolint:errcheck` specifically is **never** used — handle the error instead
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
- Go Idioms and Patterns @go-idioms-and-patterns.md (§ Idiomatic Patterns, § Formatting and Static Analysis)
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md (§ Go/slog)
|
||||
- Error Handling Principles @error-handling-principles.md (§ Resource Cleanup)
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
name: mobile-design
|
||||
description: Generates distinctive, production-grade mobile interfaces for Flutter and React Native. Prioritizes platform-native patterns, adaptive layouts, and fluid motion. Use when building mobile apps, screens, widgets, or when the user requests to style or create visually striking mobile UI.
|
||||
---
|
||||
|
||||
# Mobile Design Skill
|
||||
|
||||
This skill guides creation of distinctive, production-grade mobile interfaces that feel native to the platform while maintaining a unique aesthetic identity. Implement real working code with exceptional attention to platform conventions and creative design choices.
|
||||
|
||||
The user provides mobile UI requirements: a screen, widget, feature, or complete app interface. They may include context about the platform (iOS/Android), audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a platform-aware aesthetic direction:
|
||||
- **Platform**: iOS (Cupertino), Android (Material 3), or cross-platform? Each has distinct expectations.
|
||||
- **Tone**: Pick a direction — refined luxury, playful/energetic, utilitarian/efficient, editorial/content-first, organic/warm, bold/geometric, soft/pastel, etc.
|
||||
- **Constraints**: Performance on low-end devices, offline capability, accessibility.
|
||||
- **Differentiation**: What makes this app memorable? What's the signature interaction?
|
||||
|
||||
**CRITICAL**: Mobile users have muscle memory. Respect platform conventions (navigation patterns, gesture zones, system UI) while injecting personality through typography, color, motion, and layout composition.
|
||||
|
||||
## Mobile Design Guidelines
|
||||
|
||||
### Platform Conventions
|
||||
- **iOS**: Bottom tab bar, large title navigation, swipe-to-go-back, Cupertino widgets
|
||||
- **Android**: Material 3, navigation rail/drawer, FAB, top app bar
|
||||
- **Cross-platform**: Use adaptive widgets that render platform-appropriate UI
|
||||
|
||||
### Typography
|
||||
- Choose distinctive but highly readable fonts at mobile sizes (16px+ body)
|
||||
- Respect dynamic type / font scaling — never hardcode font sizes
|
||||
- Use `TextTheme` in Flutter, keep a consistent type scale
|
||||
- Pair a characterful display font with a supremely legible body font
|
||||
|
||||
### Color & Theme
|
||||
- Support **both light and dark themes** — always
|
||||
- Use `ColorScheme` / `ThemeData` for consistency
|
||||
- Ensure contrast ratios work on OLED (pure black) and LCD displays
|
||||
- Use color purposefully — primary actions, status indicators, brand identity
|
||||
|
||||
### Motion & Animation
|
||||
- Use **meaningful motion** — animations should communicate state changes, not decorate
|
||||
- Keep animations under 300ms for interactions, up to 500ms for transitions
|
||||
- Use `Hero` animations for shared element transitions
|
||||
- Stagger list item animations for visual polish
|
||||
- Respect `reduceMotion` / accessibility settings — always provide a static fallback
|
||||
|
||||
### Layout & Spacing
|
||||
- Design for **one-handed use** — critical actions in thumb-reach zone (bottom 60% of screen)
|
||||
- Use safe area insets — never overlap system UI (notch, home indicator, status bar)
|
||||
- Design for varying screen sizes — phones, tablets, foldables
|
||||
- Use responsive breakpoints: compact (<600dp), medium (600-840dp), expanded (>840dp)
|
||||
- Generous touch targets: minimum 48x48dp
|
||||
|
||||
### Performance
|
||||
- Avoid rebuilding expensive widget trees — use `const` constructors
|
||||
- Lazy-load lists with `ListView.builder` (never `ListView` for large lists)
|
||||
- Optimize images — use `cached_network_image`, proper resolution variants
|
||||
- Profile on real devices, not just emulators
|
||||
|
||||
## Flutter-Specific Patterns
|
||||
|
||||
```dart
|
||||
// ✅ Good: Adaptive widget that respects platform
|
||||
Widget buildButton(BuildContext context) {
|
||||
return Platform.isIOS
|
||||
? CupertinoButton(child: text, onPressed: onTap)
|
||||
: ElevatedButton(child: text, onPressed: onTap);
|
||||
}
|
||||
|
||||
// ✅ Good: Responsive layout with breakpoints
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
if (width >= 840) return _expandedLayout();
|
||||
if (width >= 600) return _mediumLayout();
|
||||
return _compactLayout();
|
||||
}
|
||||
|
||||
// ❌ Bad: Hardcoded sizes that break on different devices
|
||||
Container(width: 375, height: 812) // iPhone X only!
|
||||
```
|
||||
|
||||
## Rule Compliance
|
||||
Before implementing, verify against:
|
||||
- Project Structure @project-structure.md (Flutter/Mobile layout, feature organization)
|
||||
- Testing Strategy @testing-strategy.md (widget tests, integration tests)
|
||||
- Security Principles @security-principles.md (secure storage, API key handling)
|
||||
- Accessibility Principles @accessibility-principles.md (screen reader, dynamic type, contrast)
|
||||
- Architectural Patterns @architectural-pattern.md (Riverpod 3 Notifier, repository pattern)
|
||||
|
||||
**IMPORTANT**: Mobile has unique constraints — battery life, network variability, and user context (one-handed, on-the-go). Every design choice should respect these realities. Beautiful UI that drains battery or stutters on scroll is a failure.
|
||||
|
||||
Remember: The best mobile apps feel like they were designed specifically for the device in your hand. Make every pixel and every interaction intentional.
|
||||
@@ -1,227 +0,0 @@
|
||||
---
|
||||
name: perf-optimization
|
||||
description: >
|
||||
Profile-driven performance optimization protocol. Use when profiling data (CPU, heap, trace)
|
||||
is available or when the user requests performance analysis. Covers methodology, pattern catalog,
|
||||
safety invariants, and when-to-stop heuristics. Language-specific tooling is in languages/*.md.
|
||||
---
|
||||
|
||||
# Performance Optimization Skill
|
||||
|
||||
## When to Use
|
||||
|
||||
- User provides profiling data (pprof, flamegraph, py-spy, Chrome DevTools, Dart DevTools)
|
||||
- User asks to analyze or optimize performance of a specific component
|
||||
- A benchmark regression is detected
|
||||
- After deploying a new feature that touches a hot path
|
||||
|
||||
## Core Methodology
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
P1[Profile] --> P2[Analyze]
|
||||
P2 --> P3[Prioritize]
|
||||
P3 --> P4[Optimize]
|
||||
P4 --> P5[Benchmark]
|
||||
P5 --> P6{Improvement?}
|
||||
P6 -->|Yes| P7[Verify & Ship]
|
||||
P6 -->|No| P3
|
||||
```
|
||||
|
||||
### Step 1: Profile
|
||||
|
||||
Collect profiling data using the language-appropriate tool. Load the relevant `languages/*.md` module for exact commands.
|
||||
|
||||
**Output:** Raw profiling data (CPU profile, heap profile, or trace).
|
||||
|
||||
### Step 2: Analyze
|
||||
|
||||
Read the profile. Focus on these principles (universal across all runtimes):
|
||||
|
||||
1. **Focus on `cum` (cumulative):** The total resources consumed by a function AND everything it called. This finds the expensive architectural flows.
|
||||
2. **Contextualize `flat`:** Resources consumed by the function itself only. If a runtime function (GC, malloc, syscall) has high flat time, trace it UP the call chain to find the user-land code that triggered it.
|
||||
3. **Ignore runtime noise:** Scheduler overhead (`runtime.mcall`, `runtime.systemstack`, GC workers) will always appear. Note if GC pressure is high, but don't try to "fix" the scheduler.
|
||||
4. **Separate benchmark artifacts from production cost:** Test harness allocations (e.g., `httptest.NewRequest`, `ResponseRecorder`) inflate heap profiles but don't exist in production.
|
||||
|
||||
**Output:** Structured analysis document in `docs/research_logs/{component}-perf-analysis.md`.
|
||||
|
||||
### Step 3: Prioritize
|
||||
|
||||
Rank fixes by **impact/risk ratio**:
|
||||
|
||||
| Priority | Criteria |
|
||||
|---|---|
|
||||
| Do first | Low risk, high impact (caching, pre-allocation, fast-reject) |
|
||||
| Do second | Medium risk, high impact (library swap, algorithm change) |
|
||||
| Do last | High risk, high impact (major refactor, custom implementation) |
|
||||
| Skip | Any risk, low impact (micro-optimization below noise floor) |
|
||||
|
||||
**Rule:** If a fix requires more than 1 day AND saves < 20% on the hot path, defer it.
|
||||
|
||||
### Step 4: Optimize
|
||||
|
||||
Implement one fix at a time. For each fix:
|
||||
1. Write tests FIRST (TDD — Red → Green → Refactor)
|
||||
2. Implement the fix
|
||||
3. Run all existing tests to verify no regression
|
||||
4. Benchmark immediately
|
||||
|
||||
**Never batch multiple optimizations into one commit.** Each fix must be independently verifiable and revertable.
|
||||
|
||||
### Step 5: Benchmark
|
||||
|
||||
Compare before/after with the **exact same benchmark configuration** (same `-benchtime`, same `-count`, same machine load). Report:
|
||||
- `ns/op` (latency)
|
||||
- `B/op` (memory per operation)
|
||||
- `allocs/op` (heap allocations per operation)
|
||||
|
||||
### Step 6: When to Stop
|
||||
|
||||
**Stop optimizing when any of these are true:**
|
||||
- Remaining CPU is in hardware-optimized assembly (AES-NI, P-256, SIMD) — you cannot beat the hardware
|
||||
- Remaining allocations are from the language runtime itself (GC, goroutine stacks, HTTP server internals)
|
||||
- The fix requires a custom implementation of a well-audited library — the security/maintenance risk outweighs the perf gain
|
||||
- The measured improvement is < 5% and within benchmark noise
|
||||
|
||||
---
|
||||
|
||||
## Optimization Pattern Catalog
|
||||
|
||||
These are generic, language-agnostic patterns. Apply them when the profiling data shows the corresponding symptom.
|
||||
|
||||
### Pattern: Result Caching
|
||||
|
||||
**Symptom:** Same expensive computation repeated with identical inputs (crypto verification, JSON parsing, regex compilation).
|
||||
|
||||
**Fix:** Cache results keyed by input hash. Use bounded LRU with TTL to prevent memory exhaustion.
|
||||
|
||||
**Safety invariant:** When caching security-sensitive results (auth tokens, permission checks):
|
||||
- ALWAYS re-validate expiry/revocation on cache hit
|
||||
- ALWAYS bound cache size (DoS protection)
|
||||
- ALWAYS set TTL shorter than the security credential's validity period
|
||||
|
||||
### Pattern: Pre-allocation
|
||||
|
||||
**Symptom:** High `allocs/op` from repeatedly constructing the same objects (option structs, config slices, header maps).
|
||||
|
||||
**Fix:** Build the object once at init time, share it read-only across requests. Safe for concurrent use if the object is immutable after construction.
|
||||
|
||||
### Pattern: Fast-Reject / Short-Circuit
|
||||
|
||||
**Symptom:** Expensive validation path runs even for clearly invalid inputs.
|
||||
|
||||
**Fix:** Add a cheap structural pre-check before the expensive path. Examples: check string length before regex, count delimiters before parsing, check content-type before deserialization.
|
||||
|
||||
### Pattern: Library Swap
|
||||
|
||||
**Symptom:** High allocation count or CPU in a third-party library's internal parsing/serialization.
|
||||
|
||||
**Fix:** Replace with a library that uses lower-allocation strategies (manual scanners vs `encoding/json.Decoder`, zero-copy parsing, arena allocation).
|
||||
|
||||
**Safety invariant:** When swapping security-critical libraries (JWT, TLS, crypto):
|
||||
- Explicitly restrict accepted algorithms (prevent algorithm confusion attacks)
|
||||
- Verify the replacement library is well-audited and actively maintained
|
||||
- Run the full existing test suite — no behavioral change allowed
|
||||
|
||||
### Pattern: Pooling
|
||||
|
||||
**Symptom:** High GC pressure from many short-lived objects of the same type being allocated and discarded rapidly.
|
||||
|
||||
**Fix:** Use an object pool (sync.Pool in Go, object pool in Java, arena in Rust) to reuse allocations.
|
||||
|
||||
**Caveat:** Only effective when objects are uniform in size and have a clear acquire/release lifecycle. Misuse creates subtle bugs.
|
||||
|
||||
### Pattern: Batching
|
||||
|
||||
**Symptom:** Many small I/O operations (DB queries, HTTP calls, file writes) dominating wall-clock time.
|
||||
|
||||
**Fix:** Batch operations into fewer, larger calls. Examples: batch INSERT, pipeline Redis commands, buffer writes.
|
||||
|
||||
### Pattern: Artifact Partitioning by Change Frequency
|
||||
|
||||
**Symptom:** Deploying a small change invalidates a large cached artifact (JS bundle, Docker image, compiled binary), forcing consumers to re-download/rebuild the entire thing.
|
||||
|
||||
**Fix:** Partition build artifacts by change frequency so that stable layers survive volatile deploys:
|
||||
- **Stable layer**: dependencies, vendor libraries, base images — changes rarely
|
||||
- **Volatile layer**: application code — changes on every deploy
|
||||
|
||||
**Examples across stacks:**
|
||||
- **JS/Bundler**: Vite `manualChunks` / Webpack `splitChunks` to isolate vendor libraries into separate chunks
|
||||
- **Docker**: multi-stage builds with `COPY go.mod` + `RUN go mod download` BEFORE `COPY . .` — dependency layer caches across builds
|
||||
- **Monorepo**: separate packages by change frequency so CI only rebuilds what changed
|
||||
|
||||
**Safety invariant:** Total artifact size stays the same or slightly increases (chunk overhead). The benefit is on **repeat consumption** — stable layers serve from cache.
|
||||
|
||||
**When NOT to apply:** One-shot artifacts with no caching benefit (single-use CI, ephemeral environments).
|
||||
|
||||
### Pattern: Dependency Discovery Parallelization
|
||||
|
||||
**Symptom:** Sequential resource discovery creates waterfalls — each resource is discovered only after the previous one completes (download → parse → discover next → download → ...).
|
||||
|
||||
**Fix:** Declare dependencies as early as possible so the system can fetch them in parallel:
|
||||
- Move resource declarations upstream (earlier in the boot/parse sequence)
|
||||
- Use explicit hints to bypass sequential discovery chains
|
||||
|
||||
**Examples across stacks:**
|
||||
- **Browser**: `<link rel="preconnect">` to establish connections before CSS/JS requests them; move CSS `@import` to HTML `<link>` for parallel discovery
|
||||
- **Go**: `go mod download` before build to prefetch modules
|
||||
- **DB**: connection pool warm-up at startup instead of on first query
|
||||
- **DNS**: `dns-prefetch` hints for domains the app will contact
|
||||
|
||||
**Safety invariant:** Only pre-declare resources you WILL use. Unused preconnects/prefetches waste resources (TCP connections, DNS queries, module downloads).
|
||||
|
||||
### Pattern: Concurrent-Fetch Dedup
|
||||
|
||||
**Symptom:** Network tab shows two identical API calls fired at the same time. Multiple UI components mount simultaneously and each independently calls the same fetch function.
|
||||
|
||||
**Fix:** Add a loading-state guard (semaphore) at the store/service layer:
|
||||
|
||||
```
|
||||
async function fetchData() {
|
||||
if (isLoading) return // ← drop duplicate in-flight request
|
||||
isLoading = true
|
||||
try { data = await api.getData() }
|
||||
finally { isLoading = false }
|
||||
}
|
||||
```
|
||||
|
||||
**When to apply:** When the same data store is used by multiple co-mounted components (e.g., a navigation bar and a page view both calling `fetchProfile()` on mount).
|
||||
|
||||
**Caveat:** This is a simple semaphore, not request dedup. If the data needs refreshing after the in-flight call completes, the caller should retry. For advanced use cases, consider a proper request dedup cache (e.g., TanStack Query's `staleTime`).
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Things NOT to Do)
|
||||
|
||||
1. **Don't optimize runtime internals.** If `runtime.mallocgc` or `runtime.gcBgMarkWorker` is high, fix the USER CODE that triggers allocations — don't try to tune the GC directly.
|
||||
2. **Don't replace battle-tested crypto with custom implementations.** The performance ceiling of ECDSA/RSA is in the math. Accept it.
|
||||
3. **Don't optimize based on gut feeling.** Always profile first. Premature optimization is the root of all evil.
|
||||
4. **Don't combine multiple optimizations into one commit.** If a combined commit causes a regression, you can't isolate which fix is at fault.
|
||||
5. **Don't disable security features for performance.** Algorithm restriction, input validation, and expiry checks are non-negotiable.
|
||||
6. **Don't profile without a stable baseline.** Run benchmarks with fixed parameters (`-benchtime`, `-count`, same machine load). Without a reproducible baseline, before/after comparisons are meaningless noise.
|
||||
|
||||
---
|
||||
|
||||
## Language Modules
|
||||
|
||||
Load the relevant language module when working with a specific runtime:
|
||||
|
||||
| Module | Use when |
|
||||
|---|---|
|
||||
| [Go](languages/go.md) | Go services, APIs, CLI tools |
|
||||
| [Rust](languages/rust.md) | Rust binaries, libraries |
|
||||
| [Python](languages/python.md) | Python services, CLI, data pipelines |
|
||||
| [Frontend](languages/frontend.md) | Web frontends (JS/TS bundle, rendering, network) |
|
||||
|
||||
> **Contributing:** After completing a perf optimization session, extract generalizable patterns
|
||||
> from your `docs/research_logs/` findings into this catalog. Project-specific details stay in
|
||||
> the research log; reusable patterns belong here.
|
||||
|
||||
## Profiling Scripts
|
||||
|
||||
Language-specific data extraction scripts live in `scripts/`:
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| [go-pprof.sh](scripts/go-pprof.sh) | Extract Go pprof CPU/heap profiles into agent-readable markdown |
|
||||
| [frontend-lighthouse.sh](scripts/frontend-lighthouse.sh) | Two modes: `lighthouse` (Core Web Vitals, needs Chrome) or `bundle` (Vite chunk analysis, always works) |
|
||||
@@ -1,347 +0,0 @@
|
||||
# Frontend Performance Profiling — Vue 3 + Vite + PWA
|
||||
|
||||
## Stack Context
|
||||
|
||||
| Layer | Tool | Version |
|
||||
|---|---|---|
|
||||
| Framework | Vue 3 (SFC + Composition API) | ^3.5 |
|
||||
| State | Pinia | ^3.0 |
|
||||
| Router | Vue Router | ^5.0 |
|
||||
| Bundler | Vite | ^7.3 |
|
||||
| CSS | Tailwind CSS (Vite plugin) | ^4.2 |
|
||||
| PWA | vite-plugin-pwa (Workbox) | ^1.2 |
|
||||
| Backend client | Supabase JS | ^2.99 |
|
||||
| Build | vue-tsc + vite build | — |
|
||||
| Tests | Vitest + jsdom | ^4.1 |
|
||||
|
||||
---
|
||||
|
||||
## Profiling Toolchain
|
||||
|
||||
### Build-Time Analysis
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
|---|---|---|
|
||||
| Vite bundle visualizer | Chunk sizes, tree-shaking effectiveness | `npx vite-bundle-visualizer` (generates `stats.html`) |
|
||||
| `vite build --report` | Build timing and chunk summary | `npx vite build 2>&1` |
|
||||
| `source-map-explorer` | Trace bytes to source files | `npx source-map-explorer dist/assets/*.js` |
|
||||
| `bundlephobia` | Check dependency size before adding | `npx bundlephobia <package-name>` |
|
||||
|
||||
**Quick bundle size check (no extra dependency needed):**
|
||||
|
||||
```bash
|
||||
# Build and report chunk sizes
|
||||
cd apps/frontend && npx vite build 2>&1 | grep -E "(dist/|\.js|\.css)" | sort -t'│' -k2 -rn
|
||||
```
|
||||
|
||||
**Detailed analysis with rollup-plugin-visualizer:**
|
||||
|
||||
```bash
|
||||
# Install temporarily and run
|
||||
cd apps/frontend && npx vite-bundle-visualizer
|
||||
# Opens stats.html in browser — shows treemap of every module
|
||||
```
|
||||
|
||||
### Runtime Analysis (Chrome DevTools)
|
||||
|
||||
| Tab | What it measures | When to use |
|
||||
|---|---|---|
|
||||
| **Performance** | Frame rate, scripting, rendering, painting | Janky animations, slow interactions |
|
||||
| **Lighthouse** | LCP, FID/INP, CLS, TTI, bundle size | Overall page health score |
|
||||
| **Network** | Waterfall, asset sizes, cache status | Slow loads, unnecessary requests |
|
||||
| **Memory** | Heap snapshots, allocation timeline | Memory leaks, growing detached DOM |
|
||||
| **Coverage** | Used vs unused JS/CSS bytes | Dead code, over-imported libraries |
|
||||
|
||||
### Core Web Vitals
|
||||
|
||||
| Metric | Target | What it measures |
|
||||
|---|---|---|
|
||||
| **LCP** (Largest Contentful Paint) | < 2.5s | How fast the main content appears |
|
||||
| **INP** (Interaction to Next Paint) | < 200ms | How fast interactions respond |
|
||||
| **CLS** (Cumulative Layout Shift) | < 0.1 | How much the layout shifts during load |
|
||||
| **TTFB** (Time to First Byte) | < 800ms | Server response time |
|
||||
| **FCP** (First Contentful Paint) | < 1.8s | How fast anything renders |
|
||||
|
||||
### Lighthouse CLI (Headless — for Scripted Analysis)
|
||||
|
||||
```bash
|
||||
# Run Lighthouse from CLI (no browser GUI needed)
|
||||
npx lighthouse http://localhost:4173 \
|
||||
--output=json \
|
||||
--output-path=./lighthouse-report.json \
|
||||
--chrome-flags="--headless --no-sandbox" \
|
||||
--only-categories=performance \
|
||||
--preset=desktop
|
||||
|
||||
# For mobile (default):
|
||||
npx lighthouse http://localhost:4173 \
|
||||
--output=json \
|
||||
--output-path=./lighthouse-report.json \
|
||||
--chrome-flags="--headless --no-sandbox" \
|
||||
--only-categories=performance
|
||||
```
|
||||
|
||||
**Agent-readable extraction:**
|
||||
|
||||
```bash
|
||||
# Extract key metrics from Lighthouse JSON
|
||||
cat lighthouse-report.json | node -e "
|
||||
const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
||||
const a = r.audits;
|
||||
console.log('| Metric | Score | Value |');
|
||||
console.log('|---|---|---|');
|
||||
['first-contentful-paint','largest-contentful-paint','total-blocking-time',
|
||||
'cumulative-layout-shift','speed-index','interactive'].forEach(k => {
|
||||
if(a[k]) console.log('| ' + a[k].title + ' | ' + (a[k].score*100) + ' | ' + a[k].displayValue + ' |');
|
||||
});
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vue/Vite-Specific Optimization Patterns
|
||||
|
||||
### Pattern: Vendor Chunk Splitting (Vite)
|
||||
|
||||
**Symptom:** Every deploy invalidates the entire JS bundle cache, including stable vendor libs (Vue, Supabase JS). See agnostic principle: *Artifact Partitioning by Change Frequency* in SKILL.md.
|
||||
|
||||
**Fix:** Add `manualChunks` to `vite.config.ts`:
|
||||
|
||||
```typescript
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vendor-vue': ['vue', 'vue-router', 'pinia'],
|
||||
'vendor-supabase': ['@supabase/supabase-js'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
**Group by change frequency:**
|
||||
- `vendor-vue`: framework runtime — changes only on major upgrades
|
||||
- `vendor-supabase`: SDK/client — changes infrequently
|
||||
- App code (auto): changes every deploy
|
||||
|
||||
**Verification:** After `vite build`, check `dist/assets/` for `vendor-vue-*.js` and `vendor-supabase-*.js` chunks.
|
||||
|
||||
### Pattern: Resource Hint Pipeline (HTML)
|
||||
|
||||
**Symptom:** Fonts load late because browser discovers them only after parsing CSS. See agnostic principle: *Dependency Discovery Parallelization* in SKILL.md.
|
||||
|
||||
**Fix:** Replace CSS `@import` with HTML `<link>` tags and add `preconnect` hints:
|
||||
|
||||
```html
|
||||
<!-- In index.html <head> — BEFORE the CSS link -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=...&display=swap">
|
||||
```
|
||||
|
||||
Then remove the `@import url(...)` from your CSS file.
|
||||
|
||||
**Safety:** Only `preconnect` to domains used on every page load. For runtime-determined domains (e.g., Supabase URL from env vars), skip the hint — hardcoding would break across environments.
|
||||
|
||||
### Pattern: Route-Level Code Splitting
|
||||
|
||||
**Symptom:** Lighthouse reports a large initial JS bundle with low Coverage score. All routes are in a single chunk.
|
||||
|
||||
**Fix:** Use dynamic imports for route components in Vue Router:
|
||||
|
||||
```typescript
|
||||
// ❌ Eager import — everything in one chunk
|
||||
import SiraatView from '@/views/SiraatView.vue'
|
||||
|
||||
// ✅ Lazy route — separate chunk, loaded on navigation
|
||||
const SiraatView = () => import('@/views/SiraatView.vue')
|
||||
```
|
||||
|
||||
**Verification:** After build, check `dist/assets/` — each lazy route should produce a separate `.js` chunk.
|
||||
|
||||
### Pattern: Component Lazy Loading
|
||||
|
||||
**Symptom:** Heavy components (charts, editors, modals) are loaded even when not visible.
|
||||
|
||||
**Fix:** Use `defineAsyncComponent` for below-the-fold or conditionally-rendered components:
|
||||
|
||||
```typescript
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const HeavyChart = defineAsyncComponent(() =>
|
||||
import('@/features/analytics/HeavyChart.vue')
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern: Pinia Store Hydration
|
||||
|
||||
**Symptom:** Multiple API calls for the same data on page load. Network waterfall shows redundant fetches.
|
||||
|
||||
**Fix:** Deduplicate fetches in Pinia stores with loading-state guards:
|
||||
|
||||
```typescript
|
||||
const store = defineStore('lesson', () => {
|
||||
const data = ref<Lesson | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetch(id: string) {
|
||||
if (loading.value) return // ← prevent double-fetch
|
||||
if (data.value?.id === id) return // ← already have it
|
||||
loading.value = true
|
||||
try {
|
||||
data.value = await api.getLesson(id)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { data, loading, fetch }
|
||||
})
|
||||
```
|
||||
|
||||
### Pattern: Image Optimization
|
||||
|
||||
**Symptom:** Lighthouse flags large images (LCP degradation). Network shows uncompressed PNGs.
|
||||
|
||||
**Fix checklist:**
|
||||
1. Use `<img loading="lazy">` for below-the-fold images
|
||||
2. Use WebP/AVIF formats (Vite handles this with plugins)
|
||||
3. Specify explicit `width` and `height` attributes (prevents CLS)
|
||||
4. Use `<picture>` with `srcset` for responsive images
|
||||
|
||||
### Pattern: Watchers / Computed Overuse
|
||||
|
||||
**Symptom:** DevTools Performance tab shows repeated "Scripting" blocks during reactivity updates.
|
||||
|
||||
**Fix:**
|
||||
- Prefer `computed` over `watch` — computed values are lazy and cached
|
||||
- Use `watchEffect` only when side effects are truly needed
|
||||
- Use `shallowRef` / `shallowReactive` for large objects that don't need deep reactivity
|
||||
|
||||
```typescript
|
||||
// ❌ Deep reactive on a 1000-item array — Vue tracks every nested property
|
||||
const items = ref<Item[]>([])
|
||||
|
||||
// ✅ Shallow — Vue only tracks the array reference, not individual items
|
||||
const items = shallowRef<Item[]>([])
|
||||
```
|
||||
|
||||
### Pattern: Virtual Scrolling for Long Lists
|
||||
|
||||
**Symptom:** Rendering > 100 DOM nodes causes jank. DevTools shows long "Rendering" and "Painting" phases.
|
||||
|
||||
**Fix:** Use virtual scrolling (only render visible items):
|
||||
|
||||
```bash
|
||||
npm install @tanstack/vue-virtual
|
||||
```
|
||||
|
||||
### Pattern: PWA Cache Strategy Audit
|
||||
|
||||
**Symptom:** Repeat visits are slow despite service worker. Network tab shows cache misses.
|
||||
|
||||
**Audit checklist:**
|
||||
1. Verify `navigator.serviceWorker.controller` is active (DevTools → Application → Service Workers)
|
||||
2. Check `Cache Storage` in DevTools — are the expected caches populated?
|
||||
3. Verify `runtimeCaching` patterns in `vite.config.ts` match actual API URL patterns
|
||||
4. Check `networkTimeoutSeconds` — too high means slow fallback to cache
|
||||
|
||||
### Pattern: Tailwind CSS Purging
|
||||
|
||||
**Symptom:** Large CSS bundle despite using Tailwind. Build output shows > 50KB CSS.
|
||||
|
||||
**Fix:** Tailwind v4 (your version) uses automatic content detection. Verify:
|
||||
1. No `@import` of the full Tailwind CSS build in production
|
||||
2. The `@tailwindcss/vite` plugin is used (it is — confirmed in your config)
|
||||
3. No dynamic class names that prevent tree-shaking (e.g., `class={`text-${color}-500`}`)
|
||||
|
||||
---
|
||||
|
||||
## Vue/Vite-Specific Anti-Patterns
|
||||
|
||||
1. **Don't eagerly import all routes.** This defeats Vite's code splitting. Always use dynamic `() => import()` for route components.
|
||||
2. **Don't use `v-if` + `v-for` on the same element.** `v-if` has higher priority in Vue 3 — use a `<template>` wrapper.
|
||||
3. **Don't deep-watch large arrays.** Use `shallowRef` or `watch(() => arr.length)` to avoid O(n) reactivity tracking.
|
||||
4. **Don't ignore `key` in `v-for`.** Missing keys force Vue to re-render entire lists instead of patching individual items.
|
||||
5. **Don't inline large SVGs when they could be `<img>` references.** Inline SVGs increase HTML parse time and bundle size.
|
||||
6. **Don't load Supabase JS in every component.** Import the singleton client from `supabaseClient.ts` — avoid creating multiple GoTrue instances.
|
||||
7. **Don't use plain functions for reactive template expressions.** If `v-if="isVisible()"` calls a function, Vue re-executes it on every render cycle. Use `computed` instead — Vue caches the result and only re-runs when dependencies change.
|
||||
|
||||
---
|
||||
|
||||
## Irreducible Floors in Vue/Vite/Browser
|
||||
|
||||
| Cost | What it is | Why it's irreducible |
|
||||
|---|---|---|
|
||||
| Vue runtime | Reactivity system, virtual DOM diffing | Framework overhead — ~30KB min gzipped |
|
||||
| Supabase JS | GoTrue + PostgREST client | Auth + API client — ~40KB min gzipped |
|
||||
| TLS handshake | HTTPS connection setup | Network latency — mitigated by HTTP/2, preconnect |
|
||||
| Font loading | WOFF2 download + layout shift | Use `font-display: swap` and preload critical fonts |
|
||||
| Service Worker registration | SW parse + install on first visit | One-time cost per SW update |
|
||||
| DNS resolution | Supabase API domain lookup | Use `<link rel="dns-prefetch">` |
|
||||
|
||||
---
|
||||
|
||||
## Data Extraction Scripts
|
||||
|
||||
Use the scripts in `scripts/` for automated profiling:
|
||||
|
||||
```bash
|
||||
# Always-works bundle analysis (no Chrome needed) — start here
|
||||
scripts/frontend-lighthouse.sh bundle ./apps/frontend
|
||||
|
||||
# Lighthouse Core Web Vitals (needs Chrome/Chromium)
|
||||
scripts/frontend-lighthouse.sh lighthouse http://localhost:4173 desktop
|
||||
|
||||
# With custom timeout (default: 120s)
|
||||
LIGHTHOUSE_TIMEOUT=60 scripts/frontend-lighthouse.sh lighthouse http://localhost:4173
|
||||
```
|
||||
|
||||
> **Agent tip:** If Lighthouse hangs (common in headless/CI environments), the script
|
||||
> auto-kills after the timeout and suggests bundle mode. Always start with `bundle` mode
|
||||
> since it works everywhere and provides the most actionable data (chunk sizes, vendor
|
||||
> splitting status, oversized chunk flags).
|
||||
|
||||
## Recommended Profiling Workflow
|
||||
|
||||
1. **Bundle analysis first:** Run `scripts/frontend-lighthouse.sh bundle ./apps/frontend` to check chunk sizes
|
||||
2. **Lighthouse desktop + mobile:** Run `scripts/frontend-lighthouse.sh lighthouse <url> desktop` and default (mobile) — compare scores
|
||||
3. **Runtime profiling:** Use Chrome DevTools Performance tab to record user flows (navigation, exercise completion)
|
||||
4. **Memory:** Take heap snapshots before and after a user flow — diff for leaks (detached DOM, event listeners)
|
||||
5. **Coverage:** Run Coverage tool — identify dead JS/CSS shipped to the client
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Post-Optimization Clean Sweep Checklist
|
||||
|
||||
After applying the main fixes, run this checklist to catch leftover issues.
|
||||
This was codified from the FATH frontend session (2026-03-23).
|
||||
|
||||
### Template Reactivity
|
||||
- [ ] All `v-if` / `v-show` conditions use `computed` refs, not plain function calls
|
||||
- [ ] `v-for` lists use stable `:key` values (not array index)
|
||||
- [ ] No plain functions in template expressions that could be `computed`
|
||||
|
||||
### Component Loading
|
||||
- [ ] All routes use `() => import()` (lazy routes)
|
||||
- [ ] Modals, drawers, and sheets use `defineAsyncComponent` (they are never visible on initial render)
|
||||
- [ ] Heavy sub-components within lazy routes use `defineAsyncComponent` if they are conditionally rendered
|
||||
|
||||
### Store Health
|
||||
- [ ] Stores used by multiple co-mounted views have a concurrent-fetch guard (`if (isLoading) return`)
|
||||
- [ ] No `deep: true` watchers on large arrays — use `shallowRef` + explicit trigger
|
||||
- [ ] Computed getters are used instead of methods for derived store state
|
||||
|
||||
### Bundle Health
|
||||
- [ ] Vendor libraries are isolated into stable chunks (`manualChunks`)
|
||||
- [ ] CSS `@import` statements for external stylesheets (fonts, etc.) are replaced with HTML `<link>` tags
|
||||
- [ ] `<link rel="preconnect">` hints exist for all statically-known third-party origins
|
||||
- [ ] Supabase JS is imported only once from a singleton module, not in individual components
|
||||
|
||||
### Build Verification
|
||||
- [ ] `npx vite build` — clean output, no warnings
|
||||
- [ ] `npx vitest run` — all tests pass
|
||||
- [ ] `npx vue-tsc -b` — no type errors
|
||||
- [ ] Build output diff — no unexpected chunk size regressions
|
||||
@@ -1,133 +0,0 @@
|
||||
# Go Performance Profiling
|
||||
|
||||
## Toolchain
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
|---|---|---|
|
||||
| `go tool pprof` | CPU / heap / block / mutex profiles | `go tool pprof -text -cum -nodecount=30 profile.pb.gz` |
|
||||
| `go test -bench` | Micro-benchmarks | `go test -bench=BenchmarkX -benchtime=3s -benchmem -count=3` |
|
||||
| `go test -cpuprofile` | Capture CPU profile during benchmarks | `go test -bench=BenchmarkX -cpuprofile=cpu.prof` |
|
||||
| `go test -memprofile` | Capture heap profile during benchmarks | `go test -bench=BenchmarkX -memprofile=mem.prof` |
|
||||
| `go build -gcflags="-m"` | Escape analysis (what allocates on heap) | `go build -gcflags="-m" ./path/to/pkg 2>&1 \| grep "escapes to heap"` |
|
||||
| `go test -trace` | Execution trace (goroutine scheduling) | `go test -bench=BenchmarkX -trace=trace.out` |
|
||||
| `benchstat` | Statistical comparison of benchmarks | `benchstat old.txt new.txt` |
|
||||
|
||||
## Data Extraction Script
|
||||
|
||||
Use `scripts/go-pprof.sh` to extract profiles into agent-readable markdown.
|
||||
|
||||
```bash
|
||||
# CPU profile from a .prof file
|
||||
.agents/skills/perf-optimization/scripts/go-pprof.sh cpu path/to/cpu.prof
|
||||
|
||||
# Heap profile from a live endpoint
|
||||
.agents/skills/perf-optimization/scripts/go-pprof.sh heap http://localhost:6060/debug/pprof/heap
|
||||
|
||||
# CPU profile from a benchmark (generates and analyzes in one step)
|
||||
.agents/skills/perf-optimization/scripts/go-pprof.sh bench ./internal/platform/server/... BenchmarkRequireAuth
|
||||
```
|
||||
|
||||
## Go-Specific Optimization Patterns
|
||||
|
||||
### Escape Analysis
|
||||
|
||||
Before optimizing allocations, run escape analysis to understand WHY values escape to heap:
|
||||
|
||||
```bash
|
||||
go build -gcflags="-m" ./internal/platform/server/ 2>&1 | grep "escapes to heap"
|
||||
```
|
||||
|
||||
Common escape reasons:
|
||||
- Returning a pointer to a local variable
|
||||
- Storing a value in an interface (boxing)
|
||||
- Closure captures
|
||||
- Slice/map growth beyond initial capacity
|
||||
|
||||
### `sync.Pool` for Hot-Path Allocations
|
||||
|
||||
**When to use:** The profiler shows a specific type allocated/freed thousands of times per second on the same hot path.
|
||||
|
||||
**When NOT to use:** Objects that are long-lived, vary wildly in size, or have cleanup requirements.
|
||||
|
||||
```go
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
func process() {
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
defer func() { buf.Reset(); bufPool.Put(buf) }()
|
||||
// use buf...
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-sized Slices and Maps
|
||||
|
||||
**Symptom:** `runtime.growslice` or `runtime.mapassign` in the profile — the runtime is repeatedly growing backing arrays.
|
||||
|
||||
```go
|
||||
// ❌ Grows 3 times: cap 0 → 1 → 2 → 4
|
||||
items := []Item{}
|
||||
for _, raw := range data { items = append(items, parse(raw)) }
|
||||
|
||||
// ✅ One allocation
|
||||
items := make([]Item, 0, len(data))
|
||||
for _, raw := range data { items = append(items, parse(raw)) }
|
||||
```
|
||||
|
||||
### `strings.Builder` Over `fmt.Sprintf` in Loops
|
||||
|
||||
**Symptom:** `fmt.Sprintf` or `runtime.convTstring` in allocation-heavy loops.
|
||||
|
||||
```go
|
||||
var b strings.Builder
|
||||
b.Grow(estimatedLen)
|
||||
for _, part := range parts { b.WriteString(part) }
|
||||
result := b.String()
|
||||
```
|
||||
|
||||
### Benchmark Best Practices
|
||||
|
||||
```go
|
||||
func BenchmarkX(b *testing.B) {
|
||||
// Setup OUTSIDE the loop — don't measure init cost
|
||||
fixture := buildFixture(b)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := doWork(fixture)
|
||||
// Prevent compiler from eliminating the call
|
||||
_ = result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Statistical validity:** Always run with `-count=5` minimum and compare with `benchstat`:
|
||||
|
||||
```bash
|
||||
go test -bench=BenchmarkX -benchmem -count=5 > old.txt
|
||||
# ... make changes ...
|
||||
go test -bench=BenchmarkX -benchmem -count=5 > new.txt
|
||||
benchstat old.txt new.txt
|
||||
```
|
||||
|
||||
## Go-Specific Anti-Patterns
|
||||
|
||||
1. **Don't use `sync.Pool` for everything.** It's only effective for truly hot paths with uniform objects. Misuse adds complexity without measurable gain.
|
||||
2. **Don't fight the GC.** Go's GC is tuned for latency. If throughput matters more, tune `GOGC` before rewriting code.
|
||||
3. **Don't use `unsafe` for performance.** The compiler's escape analysis and inlining are usually sufficient. `unsafe` breaks memory safety guarantees.
|
||||
4. **Don't ignore `-race` when benchmarking.** Always verify correctness with `-race` before measuring performance without it.
|
||||
|
||||
## Irreducible Floors in Go
|
||||
|
||||
These are performance costs you cannot eliminate — recognize them and stop:
|
||||
|
||||
| Function | What it is | Why it's irreducible |
|
||||
|---|---|---|
|
||||
| `p256MulInternal` | P-256 scalar multiplication (ECDSA) | Hardware-optimized assembly |
|
||||
| `runtime.mallocgc` | Go memory allocator | Symptom, not cause — fix the caller |
|
||||
| `runtime.gcBgMarkWorker` | GC mark phase | Proportional to allocation rate — fix allocs |
|
||||
| `runtime.futex` / `runtime.usleep` | OS synchronization | Scheduling overhead |
|
||||
| `syscall.Syscall` | Kernel calls (I/O) | Irreducible for actual I/O ops |
|
||||
| `crypto/aes` | AES encryption | Hardware AES-NI, cannot improve |
|
||||
@@ -1,12 +0,0 @@
|
||||
# Python Performance Profiling
|
||||
|
||||
> **Stub:** This module will be populated when Python profiling is needed in a project.
|
||||
> Follow the pattern established in [go.md](go.md).
|
||||
|
||||
## Expected Content
|
||||
|
||||
- Toolchain: `py-spy`, `cProfile`, `memory_profiler`, `line_profiler`, `scalene`
|
||||
- Patterns: `__slots__`, generator expressions, `collections.deque`, `numpy` vectorization
|
||||
- Benchmarking: `timeit`, `pytest-benchmark`
|
||||
- Irreducible floors: GIL contention, interpreter overhead, C extension boundaries
|
||||
- Script: `scripts/python-pyspy.sh`
|
||||
@@ -1,12 +0,0 @@
|
||||
# Rust Performance Profiling
|
||||
|
||||
> **Stub:** This module will be populated when Rust profiling is needed in a project.
|
||||
> Follow the pattern established in [go.md](go.md).
|
||||
|
||||
## Expected Content
|
||||
|
||||
- Toolchain: `perf`, `flamegraph`, `criterion`, `cargo-flamegraph`, `dhat`
|
||||
- Patterns: Arena allocation, `#[inline]`, zero-copy parsing, `Box` vs stack
|
||||
- Benchmarking: `criterion` setup, statistical comparison
|
||||
- Irreducible floors: syscall overhead, TLS handshake, memory-mapped I/O
|
||||
- Script: `scripts/rust-flamegraph.sh`
|
||||
@@ -1,374 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# frontend-lighthouse.sh — Robust frontend performance profiling for AI agents.
|
||||
#
|
||||
# Two modes:
|
||||
# 1. `lighthouse` — Run headless Lighthouse with timeout and binary discovery
|
||||
# 2. `bundle` — Analyze Vite build output (always works, no Chrome needed)
|
||||
#
|
||||
# Usage:
|
||||
# frontend-lighthouse.sh lighthouse <url> [preset]
|
||||
# frontend-lighthouse.sh bundle <frontend-dir>
|
||||
#
|
||||
# Arguments:
|
||||
# lighthouse mode:
|
||||
# url — The URL to audit (e.g., http://localhost:4173)
|
||||
# preset — "desktop" or "mobile" (default: mobile)
|
||||
#
|
||||
# bundle mode:
|
||||
# frontend-dir — Path to the frontend project root (must have vite.config.*)
|
||||
#
|
||||
# Examples:
|
||||
# frontend-lighthouse.sh lighthouse http://localhost:4173 desktop
|
||||
# frontend-lighthouse.sh bundle ./apps/frontend
|
||||
# frontend-lighthouse.sh bundle /home/user/project/apps/frontend
|
||||
#
|
||||
# Prerequisites:
|
||||
# lighthouse mode: Node.js + Chrome/Chromium (or globally installed lighthouse)
|
||||
# bundle mode: Node.js + Vite project (no Chrome needed)
|
||||
#
|
||||
# Robustness features (learned from real-world agent sessions):
|
||||
# - Binary discovery: checks PATH, npx, ~/.npm-global/bin, common install locations
|
||||
# - Timeout: kills Lighthouse after LIGHTHOUSE_TIMEOUT seconds (default: 120)
|
||||
# - Fallback: if Lighthouse fails/hangs, suggests running bundle mode instead
|
||||
# - Error reporting disabled: prevents interactive consent prompts
|
||||
# - Bundle mode: always works, extracts chunk sizes from vite build output
|
||||
#
|
||||
# Output: Structured markdown tables suitable for LLM consumption.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MODE="${1:-}"
|
||||
LIGHTHOUSE_TIMEOUT="${LIGHTHOUSE_TIMEOUT:-120}"
|
||||
|
||||
# ─── Binary Discovery ──────────────────────────────────────────────────────────
|
||||
# Lighthouse can be installed globally via npm, in ~/.npm-global, or via npx.
|
||||
# Try multiple locations to find a working binary.
|
||||
find_lighthouse() {
|
||||
# 1. Check PATH
|
||||
if command -v lighthouse &>/dev/null; then
|
||||
echo "lighthouse"
|
||||
return
|
||||
fi
|
||||
|
||||
# 2. Check common global install locations
|
||||
local candidates=(
|
||||
"$HOME/.npm-global/bin/lighthouse"
|
||||
"/usr/local/bin/lighthouse"
|
||||
"/usr/bin/lighthouse"
|
||||
)
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
# 3. Fall back to npx (will auto-install if needed)
|
||||
echo "npx --yes lighthouse"
|
||||
}
|
||||
|
||||
# ─── Lighthouse Mode ───────────────────────────────────────────────────────────
|
||||
run_lighthouse() {
|
||||
local url="$1"
|
||||
local preset="${2:-mobile}"
|
||||
|
||||
local lh_cmd
|
||||
lh_cmd=$(find_lighthouse)
|
||||
|
||||
local preset_flag=""
|
||||
if [[ "$preset" == "desktop" ]]; then
|
||||
preset_flag="--preset=desktop"
|
||||
fi
|
||||
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp /tmp/lighthouse-XXXXXX.json)
|
||||
trap 'rm -f "$tmpfile"' EXIT
|
||||
|
||||
echo "# Lighthouse Report: ${url} (${preset})"
|
||||
echo ""
|
||||
echo "Binary: ${lh_cmd}"
|
||||
echo "Timeout: ${LIGHTHOUSE_TIMEOUT}s"
|
||||
echo ""
|
||||
|
||||
# Run Lighthouse with timeout to prevent infinite hangs in headless environments.
|
||||
# --enable-error-reporting=false prevents interactive consent prompts.
|
||||
# --no-sandbox is required in containerized/CI environments.
|
||||
local lh_pid
|
||||
local lh_exit=0
|
||||
|
||||
$lh_cmd "$url" \
|
||||
--output=json \
|
||||
--output-path="$tmpfile" \
|
||||
--chrome-flags="--headless --no-sandbox --disable-gpu --disable-dev-shm-usage" \
|
||||
--only-categories=performance \
|
||||
$preset_flag \
|
||||
--enable-error-reporting=false \
|
||||
--quiet 2>/dev/null &
|
||||
lh_pid=$!
|
||||
|
||||
# Wait with timeout — kill if Lighthouse hangs (common in headless/CI)
|
||||
local elapsed=0
|
||||
while kill -0 "$lh_pid" 2>/dev/null; do
|
||||
if (( elapsed >= LIGHTHOUSE_TIMEOUT )); then
|
||||
kill -9 "$lh_pid" 2>/dev/null || true
|
||||
wait "$lh_pid" 2>/dev/null || true
|
||||
echo "⚠️ Lighthouse timed out after ${LIGHTHOUSE_TIMEOUT}s."
|
||||
echo ""
|
||||
echo "This usually means Chrome/Chromium is not available or the headless"
|
||||
echo "environment doesn't support it. Try bundle mode instead:"
|
||||
echo ""
|
||||
echo " $0 bundle <frontend-dir>"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
wait "$lh_pid" || lh_exit=$?
|
||||
|
||||
if [[ $lh_exit -ne 0 ]]; then
|
||||
echo "⚠️ Lighthouse exited with code ${lh_exit}."
|
||||
echo "Try bundle mode instead: $0 bundle <frontend-dir>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -s "$tmpfile" ]]; then
|
||||
echo "⚠️ Lighthouse produced no output."
|
||||
echo "Try bundle mode instead: $0 bundle <frontend-dir>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract metrics into structured markdown
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const r = JSON.parse(fs.readFileSync('$tmpfile', 'utf8'));
|
||||
const a = r.audits;
|
||||
const score = Math.round((r.categories.performance.score || 0) * 100);
|
||||
|
||||
console.log('## Performance Score: ' + score + '/100');
|
||||
console.log('');
|
||||
console.log('## Core Web Vitals');
|
||||
console.log('');
|
||||
console.log('| Metric | Value | Score |');
|
||||
console.log('|---|---|---|');
|
||||
|
||||
const vitals = [
|
||||
'first-contentful-paint',
|
||||
'largest-contentful-paint',
|
||||
'total-blocking-time',
|
||||
'cumulative-layout-shift',
|
||||
'speed-index',
|
||||
'interactive',
|
||||
];
|
||||
|
||||
vitals.forEach(k => {
|
||||
if (a[k]) {
|
||||
const s = a[k].score !== null ? Math.round(a[k].score * 100) : 'N/A';
|
||||
console.log('| ' + a[k].title + ' | ' + (a[k].displayValue || 'N/A') + ' | ' + s + ' |');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log('## Opportunities');
|
||||
console.log('');
|
||||
|
||||
const opportunities = Object.values(a)
|
||||
.filter(audit => audit.details && audit.details.type === 'opportunity' && audit.details.overallSavingsMs > 0)
|
||||
.sort((x, y) => (y.details.overallSavingsMs || 0) - (x.details.overallSavingsMs || 0));
|
||||
|
||||
if (opportunities.length > 0) {
|
||||
console.log('| Opportunity | Potential Savings |');
|
||||
console.log('|---|---|');
|
||||
opportunities.forEach(o => {
|
||||
console.log('| ' + o.title + ' | ' + Math.round(o.details.overallSavingsMs) + ' ms |');
|
||||
});
|
||||
} else {
|
||||
console.log('No significant opportunities found.');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('## Diagnostics');
|
||||
console.log('');
|
||||
|
||||
const diagnostics = Object.values(a)
|
||||
.filter(audit => audit.details && audit.details.type === 'table' && audit.score !== null && audit.score < 0.9)
|
||||
.slice(0, 10);
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
console.log('| Diagnostic | Score |');
|
||||
console.log('|---|---|');
|
||||
diagnostics.forEach(d => {
|
||||
console.log('| ' + d.title + ' | ' + Math.round(d.score * 100) + ' |');
|
||||
});
|
||||
} else {
|
||||
console.log('All diagnostics passed.');
|
||||
}
|
||||
"
|
||||
}
|
||||
|
||||
# ─── Bundle Analysis Mode ──────────────────────────────────────────────────────
|
||||
# Always works — no Chrome needed. Extracts chunk sizes from vite build output.
|
||||
# This was the most useful data source during the FATH frontend session: it
|
||||
# revealed the 297KB monolithic index chunk and guided all 6 fixes.
|
||||
run_bundle() {
|
||||
local frontend_dir="$1"
|
||||
|
||||
if [[ ! -d "$frontend_dir" ]]; then
|
||||
echo "Error: directory '$frontend_dir' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify it's a Vite project
|
||||
if ! ls "$frontend_dir"/vite.config.* &>/dev/null; then
|
||||
echo "Error: no vite.config.* found in '$frontend_dir'. Is this a Vite project?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "# Bundle Analysis: ${frontend_dir}"
|
||||
echo ""
|
||||
echo "Running \`vite build\`..."
|
||||
echo ""
|
||||
|
||||
local build_output
|
||||
build_output=$(cd "$frontend_dir" && npx vite build 2>&1)
|
||||
local build_exit=$?
|
||||
|
||||
if [[ $build_exit -ne 0 ]]; then
|
||||
echo "⚠️ Build failed (exit code $build_exit):"
|
||||
echo ""
|
||||
echo '```'
|
||||
echo "$build_output"
|
||||
echo '```'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse build output into structured tables
|
||||
node -e "
|
||||
const output = \`$build_output\`;
|
||||
const lines = output.split('\n');
|
||||
|
||||
const jsChunks = [];
|
||||
const cssChunks = [];
|
||||
|
||||
// Parse Vite build output lines like:
|
||||
// dist/assets/index-BExof8y3.js 20.04 kB │ gzip: 6.90 kB
|
||||
for (const line of lines) {
|
||||
const match = line.match(/dist\/assets\/(.+?)\s+([\d.]+)\s*kB\s*│\s*gzip:\s*([\d.]+)\s*kB/);
|
||||
if (match) {
|
||||
const [, name, rawKB, gzipKB] = match;
|
||||
const entry = { name, raw: parseFloat(rawKB), gzip: parseFloat(gzipKB) };
|
||||
if (name.endsWith('.js')) jsChunks.push(entry);
|
||||
else if (name.endsWith('.css')) cssChunks.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by gzip size descending
|
||||
jsChunks.sort((a, b) => b.gzip - a.gzip);
|
||||
cssChunks.sort((a, b) => b.gzip - a.gzip);
|
||||
|
||||
const totalJsRaw = jsChunks.reduce((s, c) => s + c.raw, 0);
|
||||
const totalJsGzip = jsChunks.reduce((s, c) => s + c.gzip, 0);
|
||||
const totalCssRaw = cssChunks.reduce((s, c) => s + c.raw, 0);
|
||||
const totalCssGzip = cssChunks.reduce((s, c) => s + c.gzip, 0);
|
||||
|
||||
console.log('## JS Chunks (sorted by gzip size)');
|
||||
console.log('');
|
||||
console.log('| Chunk | Raw (KB) | Gzip (KB) |');
|
||||
console.log('|---|---|---|');
|
||||
jsChunks.forEach(c => {
|
||||
// Clean chunk name: remove hash suffix for readability
|
||||
const clean = c.name.replace(/-[A-Za-z0-9_-]{8}\\.js$/, '.js');
|
||||
console.log('| ' + clean + ' | ' + c.raw.toFixed(2) + ' | ' + c.gzip.toFixed(2) + ' |');
|
||||
});
|
||||
console.log('| **Total JS** | **' + totalJsRaw.toFixed(2) + '** | **' + totalJsGzip.toFixed(2) + '** |');
|
||||
|
||||
if (cssChunks.length > 0) {
|
||||
console.log('');
|
||||
console.log('## CSS Chunks');
|
||||
console.log('');
|
||||
console.log('| Chunk | Raw (KB) | Gzip (KB) |');
|
||||
console.log('|---|---|---|');
|
||||
cssChunks.forEach(c => {
|
||||
const clean = c.name.replace(/-[A-Za-z0-9_-]{8}\\.css$/, '.css');
|
||||
console.log('| ' + clean + ' | ' + c.raw.toFixed(2) + ' | ' + c.gzip.toFixed(2) + ' |');
|
||||
});
|
||||
console.log('| **Total CSS** | **' + totalCssRaw.toFixed(2) + '** | **' + totalCssGzip.toFixed(2) + '** |');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('## Summary');
|
||||
console.log('');
|
||||
console.log('| Metric | Value |');
|
||||
console.log('|---|---|');
|
||||
console.log('| JS chunks | ' + jsChunks.length + ' |');
|
||||
console.log('| CSS chunks | ' + cssChunks.length + ' |');
|
||||
console.log('| Total JS (gzip) | ' + totalJsGzip.toFixed(2) + ' KB |');
|
||||
console.log('| Total CSS (gzip) | ' + totalCssGzip.toFixed(2) + ' KB |');
|
||||
console.log('| Largest JS chunk | ' + (jsChunks[0]?.name || 'N/A') + ' (' + (jsChunks[0]?.gzip.toFixed(2) || '0') + ' KB gzip) |');
|
||||
|
||||
// Flag potential issues
|
||||
console.log('');
|
||||
console.log('## Flags');
|
||||
console.log('');
|
||||
const bigChunks = jsChunks.filter(c => c.gzip > 50);
|
||||
if (bigChunks.length > 0) {
|
||||
console.log('⚠️ **Large chunks (>50 KB gzip):**');
|
||||
bigChunks.forEach(c => {
|
||||
console.log(' - ' + c.name + ': ' + c.gzip.toFixed(2) + ' KB gzip');
|
||||
});
|
||||
console.log('');
|
||||
console.log('Consider splitting vendor libraries into separate chunks (see: Artifact Partitioning pattern in SKILL.md).');
|
||||
} else {
|
||||
console.log('✅ No oversized chunks detected.');
|
||||
}
|
||||
|
||||
const vendorChunks = jsChunks.filter(c => c.name.startsWith('vendor-'));
|
||||
if (vendorChunks.length > 0) {
|
||||
console.log('');
|
||||
console.log('✅ Vendor chunk splitting detected: ' + vendorChunks.map(c => c.name).join(', '));
|
||||
} else if (jsChunks.some(c => c.gzip > 40)) {
|
||||
console.log('');
|
||||
console.log('⚠️ No vendor chunk splitting detected. The largest chunk may contain framework + app code bundled together.');
|
||||
}
|
||||
"
|
||||
}
|
||||
|
||||
# ─── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
case "${MODE}" in
|
||||
lighthouse|lh)
|
||||
URL="${2:-}"
|
||||
PRESET="${3:-mobile}"
|
||||
if [[ -z "$URL" ]]; then
|
||||
echo "Usage: $0 lighthouse <url> [desktop|mobile]"
|
||||
echo " $0 bundle <frontend-dir>"
|
||||
exit 1
|
||||
fi
|
||||
run_lighthouse "$URL" "$PRESET"
|
||||
;;
|
||||
bundle|build)
|
||||
DIR="${2:-}"
|
||||
if [[ -z "$DIR" ]]; then
|
||||
echo "Usage: $0 bundle <frontend-dir>"
|
||||
echo " $0 lighthouse <url> [desktop|mobile]"
|
||||
exit 1
|
||||
fi
|
||||
run_bundle "$DIR"
|
||||
;;
|
||||
*)
|
||||
echo "Frontend Performance Profiling Script"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " $0 lighthouse <url> [desktop|mobile] — Run Lighthouse (needs Chrome)"
|
||||
echo " $0 bundle <frontend-dir> — Analyze Vite build chunks (no Chrome)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 lighthouse http://localhost:4173 desktop"
|
||||
echo " $0 bundle ./apps/frontend"
|
||||
echo ""
|
||||
echo "Environment:"
|
||||
echo " LIGHTHOUSE_TIMEOUT=120 — Seconds before killing a hung Lighthouse (default: 120)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# go-pprof.sh — Extract Go pprof profiles into agent-readable markdown.
|
||||
#
|
||||
# Usage:
|
||||
# go-pprof.sh cpu <profile-path-or-url> [top-n]
|
||||
# go-pprof.sh heap <profile-path-or-url> [top-n]
|
||||
# go-pprof.sh bench <package-path> <bench-regex> [top-n]
|
||||
#
|
||||
# Examples:
|
||||
# go-pprof.sh cpu cpu.prof
|
||||
# go-pprof.sh heap http://localhost:6060/debug/pprof/heap
|
||||
# go-pprof.sh bench ./internal/platform/server/... BenchmarkRequireAuth 40
|
||||
#
|
||||
# Output: Structured markdown suitable for LLM consumption.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROFILE_TYPE="${1:-}"
|
||||
SOURCE="${2:-}"
|
||||
TOP_N="${3:-30}"
|
||||
|
||||
usage() {
|
||||
echo "Usage:"
|
||||
echo " $0 cpu <profile-path-or-url> [top-n]"
|
||||
echo " $0 heap <profile-path-or-url> [top-n]"
|
||||
echo " $0 bench <package-path> <bench-regex> [top-n]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ -z "$PROFILE_TYPE" || -z "$SOURCE" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
extract_pprof() {
|
||||
local profile_file="$1"
|
||||
local label="$2"
|
||||
local top="$3"
|
||||
|
||||
echo ""
|
||||
echo "--- PPROF ${label} PROFILE (Top ${top} by Cumulative) ---"
|
||||
echo ""
|
||||
echo '```'
|
||||
go tool pprof -text -cum -nodecount="$top" "$profile_file" 2>/dev/null \
|
||||
| grep -v "^Showing nodes" \
|
||||
| sed '/^$/d'
|
||||
echo '```'
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "$PROFILE_TYPE" in
|
||||
cpu|heap|block|mutex)
|
||||
extract_pprof "$SOURCE" "$(echo "$PROFILE_TYPE" | tr '[:lower:]' '[:upper:]')" "$TOP_N"
|
||||
;;
|
||||
bench)
|
||||
BENCH_REGEX="${3:-}"
|
||||
TOP_N="${4:-30}"
|
||||
|
||||
if [[ -z "$BENCH_REGEX" ]]; then
|
||||
echo "Error: bench mode requires <package-path> and <bench-regex>"
|
||||
usage
|
||||
fi
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
echo "# Benchmark Profile: ${BENCH_REGEX}"
|
||||
echo ""
|
||||
|
||||
# Run benchmark and capture results + profiles
|
||||
echo "## Benchmark Results"
|
||||
echo ""
|
||||
echo '```'
|
||||
go test "$SOURCE" \
|
||||
-bench="$BENCH_REGEX" \
|
||||
-benchtime=2s \
|
||||
-benchmem \
|
||||
-count=1 \
|
||||
-run='^$' \
|
||||
-cpuprofile="$TMPDIR/cpu.prof" \
|
||||
-memprofile="$TMPDIR/mem.prof" \
|
||||
-timeout=120s 2>&1
|
||||
echo '```'
|
||||
|
||||
# Extract CPU profile
|
||||
if [[ -f "$TMPDIR/cpu.prof" ]]; then
|
||||
# Find the test binary (go test leaves it next to the profile)
|
||||
TEST_BIN=$(find "$TMPDIR" -name '*.test' 2>/dev/null | head -1 || true)
|
||||
if [[ -n "$TEST_BIN" ]]; then
|
||||
extract_pprof "$TMPDIR/cpu.prof" "CPU" "$TOP_N"
|
||||
else
|
||||
extract_pprof "$TMPDIR/cpu.prof" "CPU" "$TOP_N"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract heap profile
|
||||
if [[ -f "$TMPDIR/mem.prof" ]]; then
|
||||
echo ""
|
||||
echo "--- PPROF HEAP PROFILE (Top ${TOP_N} by Cumulative, alloc_space) ---"
|
||||
echo ""
|
||||
echo '```'
|
||||
go tool pprof -text -cum -nodecount="$TOP_N" -alloc_space "$TMPDIR/mem.prof" 2>/dev/null \
|
||||
| grep -v "^Showing nodes" \
|
||||
| sed '/^$/d'
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown profile type '$PROFILE_TYPE'"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
@@ -1,129 +0,0 @@
|
||||
---
|
||||
name: sequential-thinking
|
||||
description: Performs dynamic, reflective problem-solving through iterative thought chains. Use for complex planning requiring revision, branching, backtracking, or hypothesis verification. Ideal for multi-step analysis where context maintenance is required or the full scope isn't initially clear.
|
||||
---
|
||||
|
||||
# Sequential Thinking
|
||||
|
||||
A structured approach to complex problem-solving that breaks down challenges into iterative thought steps with built-in flexibility for revision and course correction.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Breaking down complex problems into manageable steps
|
||||
- Planning and design requiring iterative refinement
|
||||
- Analysis that might need course correction mid-stream
|
||||
- Problems where the full scope emerges during analysis
|
||||
- Multi-step solutions requiring context across steps
|
||||
- Filtering out irrelevant information
|
||||
- Hypothesis generation and verification workflows
|
||||
|
||||
## Core Methodology
|
||||
|
||||
Sequential thinking follows a dynamic process:
|
||||
|
||||
1. **Initial estimation**: Start with an estimate of thoughts needed, but remain flexible
|
||||
2. **Iterative analysis**: Work through thoughts sequentially while building context
|
||||
3. **Revision capability**: Question or revise previous thoughts as understanding deepens
|
||||
4. **Branch exploration**: Explore alternative approaches when needed
|
||||
5. **Hypothesis cycle**: Generate hypotheses, verify against thought chain, repeat
|
||||
6. **Convergence**: Continue until reaching a satisfactory solution
|
||||
|
||||
## Instructions
|
||||
|
||||
### Thought Structure
|
||||
|
||||
Each thought in the sequence should include:
|
||||
|
||||
- **thought**: Current thinking step content
|
||||
- **thoughtNumber**: Position in sequence (1, 2, 3, ...)
|
||||
- **totalThoughts**: Current estimate of total thoughts needed (adjustable)
|
||||
- **nextThoughtNeeded**: Whether another thought step is required
|
||||
|
||||
Optional revision/branching metadata:
|
||||
- **isRevision**: Boolean indicating if reconsidering previous thinking
|
||||
- **revisesThought**: Which thought number is being revised
|
||||
- **branchFromThought**: Branching point thought number
|
||||
- **branchId**: Identifier for current branch
|
||||
- **needsMoreThoughts**: Flag when reaching end but requiring more analysis
|
||||
|
||||
### Process Guidelines
|
||||
|
||||
**Starting out:**
|
||||
- Estimate initial thoughts needed based on problem complexity
|
||||
- Begin with thought 1, establishing context and approach
|
||||
- Set totalThoughts conservatively; you can adjust later
|
||||
|
||||
**During analysis:**
|
||||
- Build on previous thoughts while maintaining context
|
||||
- Filter out irrelevant information at each step
|
||||
- Express uncertainty when present
|
||||
- Don't hesitate to revise if you spot errors or better approaches
|
||||
- Adjust totalThoughts up/down as the problem's scope becomes clearer
|
||||
|
||||
**Revision pattern:**
|
||||
When reconsidering previous thinking:
|
||||
```json
|
||||
{
|
||||
"thought": "On reflection, thought 3's assumption about X was incorrect because Y...",
|
||||
"thoughtNumber": 6,
|
||||
"totalThoughts": 10,
|
||||
"isRevision": True,
|
||||
"revisesThought": 3,
|
||||
"nextThoughtNeeded": True
|
||||
}
|
||||
```
|
||||
|
||||
**Hypothesis cycle:**
|
||||
1. Generate hypothesis based on current understanding
|
||||
2. Verify against previous thought chain
|
||||
3. If verification fails, revise or branch
|
||||
4. Repeat until hypothesis is validated
|
||||
|
||||
**Completion:**
|
||||
- Only set `nextThoughtNeeded: False` when truly satisfied with the solution
|
||||
- Provide a single, clear final answer
|
||||
- Ensure the answer directly addresses the original problem
|
||||
|
||||
### Working with Context
|
||||
|
||||
**Maintain continuity:**
|
||||
- Reference specific previous thoughts by number
|
||||
- Build logical connections between thoughts
|
||||
- Track which thoughts are still valid vs. revised
|
||||
|
||||
**Filter information:**
|
||||
- Ignore details irrelevant to current thought step
|
||||
- Focus on information that advances understanding
|
||||
- Re-evaluate relevance as context evolves
|
||||
|
||||
**Manage complexity:**
|
||||
- If a thought becomes too complex, break it into multiple thoughts
|
||||
- Increase totalThoughts estimate accordingly
|
||||
- Keep each individual thought focused
|
||||
|
||||
### Output Format
|
||||
|
||||
Present your sequential thinking in a structured format:
|
||||
```
|
||||
Thought [N/Total]: [Current thought content]
|
||||
[If revision: "This revises thought X because..."]
|
||||
[If branching: "Branching from thought X to explore..."]
|
||||
|
||||
[Continue with next thought when nextThoughtNeeded is True]
|
||||
|
||||
Final output after all thoughts complete:
|
||||
Solution: [Clear, direct answer to the original problem]
|
||||
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
For concrete examples of sequential thinking in action, see `resources/examples.md`.
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Flexibility over rigidity**: Adjust your approach as understanding deepens
|
||||
- **Revision is strength**: Correcting course shows good reasoning
|
||||
- **Hypothesis-driven**: Generate and test hypotheses iteratively
|
||||
- **Context-aware**: Maintain awareness of previous thoughts while progressing
|
||||
- **Clarity at completion**: Deliver a single, clear final answer
|
||||
@@ -1,65 +0,0 @@
|
||||
# Sequential Thinking Examples
|
||||
|
||||
## Example 1: Simple Math Problem
|
||||
|
||||
**Problem:** If a train travels 120 km in 2 hours, then 180 km in the next 3 hours, what's the average speed?
|
||||
|
||||
**Thought 1/5:** Breaking down the problem - we have two segments of travel with different distances and times. Need to calculate total distance and total time first.
|
||||
|
||||
**Thought 2/5:** First segment: 120 km in 2 hours. Second segment: 180 km in 3 hours. Total distance = 120 + 180 = 300 km. Total time = 2 + 3 = 5 hours.
|
||||
|
||||
**Thought 3/5:** Average speed formula = Total distance / Total time = 300 km / 5 hours = 60 km/h.
|
||||
|
||||
**Thought 4/5:** Verifying: Does this make sense? First segment speed = 60 km/h, second segment = 60 km/h. Wait, that can't be right.
|
||||
|
||||
**Thought 5/6 (Revision of Thought 4):** Let me recalculate segment speeds. First: 120/2 = 60 km/h. Second: 180/3 = 60 km/h. Actually, both segments are at 60 km/h, so average is correctly 60 km/h. My suspicion was unfounded.
|
||||
|
||||
**Solution:** The average speed is 60 km/h.
|
||||
|
||||
## Example 2: Complex Design Decision with Branching
|
||||
|
||||
**Problem:** Design a caching strategy for a high-traffic API with both user-specific and global data.
|
||||
|
||||
**Thought 1/8:** Initial analysis - need to consider cache hit rate, memory usage, invalidation complexity, and latency requirements. Two data types suggest a multi-layered approach.
|
||||
|
||||
**Thought 2/8:** Hypothesis: Use Redis with separate key namespaces for user-specific (user:*) and global (global:*) data. User data could have shorter TTL due to personalization.
|
||||
|
||||
**Thought 3/8:** Considering TTL strategy - global data changes infrequently, could use 1-hour TTL. User-specific data needs 5-15 minute TTL to stay fresh.
|
||||
|
||||
**Thought 4/8:** Wait, need to think about invalidation. What happens when global data updates? Broadcasting invalidation to all instances could be expensive.
|
||||
|
||||
**Thought 5/10 (Branching from Thought 4):** Alternative approach - use cache versioning. Include version number in global cache keys. When data updates, increment version. Old cached entries naturally expire.
|
||||
|
||||
**Thought 6/10:** This versioning approach adds complexity but eliminates broadcast invalidation. Trade-off: some stale data until TTL expires vs. system complexity.
|
||||
|
||||
**Thought 7/10:** For user-specific data, simple TTL should work since each user's data is independent. No cross-user invalidation needed.
|
||||
|
||||
**Thought 8/10:** Hypothesis verification - versioned global cache + TTL-based user cache provides good balance. Testing memory usage: assume 100K users, 10KB user cache, 50MB global = ~1GB total. Acceptable.
|
||||
|
||||
**Thought 9/10:** Edge case consideration - what if version wraps around? Use timestamp-based versioning (YYYYMMDDHHMMSS) to prevent collisions.
|
||||
|
||||
**Thought 10/10:** Final design validated. Global cache with timestamp versioning, user cache with 10-minute TTL. Monitoring on cache hit rates and memory usage post-deployment.
|
||||
|
||||
**Solution:** Implement a two-tier Redis caching strategy with timestamp-versioned keys for global data (1-hour TTL) and simple key-value with 10-minute TTL for user-specific data. Monitor hit rates and memory usage after deployment.
|
||||
|
||||
## Example 3: Debugging with Multiple Revisions
|
||||
|
||||
**Problem:** API returning 500 errors intermittently, logs show database timeout.
|
||||
|
||||
**Thought 1/6:** Initial hypothesis - database connection pool exhaustion. Check current pool size and active connections.
|
||||
|
||||
**Thought 2/6:** Pool size = 20, monitoring shows peak usage of 18 connections. Not exhausted. Need to look elsewhere.
|
||||
|
||||
**Thought 3/7 (Revised estimate):** Query performance issue? Check slow query log. Found several queries taking 5-10 seconds during error periods.
|
||||
|
||||
**Thought 4/7:** Slow queries all involve joins on large tables without proper indexes. Hypothesis: missing indexes causing table scans.
|
||||
|
||||
**Thought 5/7:** Wait, these queries worked fine last week. What changed? Checking deployment history... no schema changes, no new queries.
|
||||
|
||||
**Thought 6/8 (Revision of Thought 4):** The queries themselves haven't changed, but data volume has. Tables grew 300% last week due to marketing campaign. Indexes exist but statistics are stale.
|
||||
|
||||
**Thought 7/8:** Hypothesis: Stale query planner statistics causing poor query plans. Solution: Run ANALYZE on affected tables to update statistics.
|
||||
|
||||
**Thought 8/8:** Verified - after ANALYZE, query plans improved and execution times dropped to <500ms. Root cause was stale statistics, not missing indexes.
|
||||
|
||||
**Solution:** Run ANALYZE on the affected database tables to update query planner statistics. Implement automated ANALYZE job to run after bulk data imports to prevent future occurrences.
|
||||
@@ -1,115 +0,0 @@
|
||||
---
|
||||
description: Research phase - understand context and gather knowledge
|
||||
---
|
||||
|
||||
# Phase 1: Research
|
||||
|
||||
## Purpose
|
||||
Understand the request context AND gather accurate, up-to-date knowledge before writing any code.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Analyze Request
|
||||
Parse the user's request and identify:
|
||||
- What are they asking for?
|
||||
- What is the scope?
|
||||
|
||||
### 2. Review Current Implementation
|
||||
Check the repository to understand:
|
||||
- Current technical architecture
|
||||
- Existing implementation patterns
|
||||
- Dependencies and configurations
|
||||
|
||||
### 3. Build Mental Model
|
||||
Inventory:
|
||||
- Business requirements
|
||||
- Technical constraints
|
||||
- Integration points with existing code
|
||||
|
||||
### 4. Define Scope
|
||||
Use `task_boundary` to set mode to **PLANNING**
|
||||
|
||||
Create a plan with bite-sized atomic tasks in `task.md`:
|
||||
- `[ ]` = Not started
|
||||
- Use indented lists for sub-items
|
||||
|
||||
### 5. Identify Research Topics
|
||||
List all technologies, libraries, and patterns involved in this task.
|
||||
|
||||
Example:
|
||||
```
|
||||
Topics for "Task CRUD API":
|
||||
- Go http.ServeMux routing patterns
|
||||
- SQLC query generation
|
||||
- UUID handling in PostgreSQL
|
||||
- JWT authentication middleware
|
||||
```
|
||||
|
||||
### 6. Search External Documentation
|
||||
For EACH topic, search external documentation using the best tool available in your environment:
|
||||
|
||||
**Documentation search tools (use whichever is available):**
|
||||
- **Qurio** (`mcp_qurio_qurio_search`, `mcp_qurio_qurio_read_page`) — if Qurio MCP is enabled
|
||||
- **Context7** or similar MCP documentation servers — if available
|
||||
- **`search_web` / `read_url_content`** — universal fallback always available
|
||||
|
||||
**Search strategy (2-5 keywords per query):**
|
||||
```
|
||||
# Using available documentation tool:
|
||||
search(query="Go ServeMux routing")
|
||||
search(query="SQLC CRUD queries")
|
||||
search(query="Pinia store setup")
|
||||
|
||||
# For deeper reading on a specific URL:
|
||||
read_page(url="https://go.dev/doc/<topic>")
|
||||
```
|
||||
|
||||
### 7. Document Findings
|
||||
Create `docs/research_logs/{feature_name}.md` with:
|
||||
- Key patterns discovered
|
||||
- Code examples from documentation
|
||||
- API signatures and options
|
||||
- Gotchas or edge cases found
|
||||
|
||||
#### Research Log Naming
|
||||
Each feature should have its own research log:
|
||||
```
|
||||
docs/research_logs/
|
||||
├── epic1_auth.md # Epic 1: Authentication
|
||||
├── epic2_task.md # Epic 2: Task CRUD
|
||||
├── epic3_lists.md # Epic 3: Lists
|
||||
└── {feature_name}.md # Pattern: one log per feature
|
||||
```
|
||||
|
||||
### 8. Document Architecture Decisions
|
||||
If any decision involves:
|
||||
- Choosing between 2+ viable approaches
|
||||
- Introducing a new dependency or pattern
|
||||
- Changing existing architecture
|
||||
|
||||
Then create an ADR using the **ADR Skill** at `docs/decisions/NNNN-short-title.md`.
|
||||
|
||||
**Skills to consider:**
|
||||
- **Sequential Thinking** — for complex design decisions with multiple trade-offs
|
||||
|
||||
### 9. Fallback: State Training Data Usage
|
||||
If relying on training data, explicitly state:
|
||||
> "I am relying on my training data for this solution as external verification was unavailable."
|
||||
|
||||
## If This Phase Fails
|
||||
If no documentation search tool yields results:
|
||||
1. Document what was searched
|
||||
2. State: "Relying on training data"
|
||||
3. Proceed with caution
|
||||
4. Flag for human review if critical
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] Request analyzed and scope defined
|
||||
- [ ] `task.md` created with atomic tasks
|
||||
- [ ] Research log created at `docs/research_logs/{feature_name}.md`
|
||||
- [ ] All major technologies researched
|
||||
- [ ] Code examples documented
|
||||
- [ ] Architecture decisions documented as ADRs (if applicable)
|
||||
|
||||
## Next Phase
|
||||
Proceed to **Phase 2: Implement** (`/2-implement`)
|
||||
@@ -1,101 +0,0 @@
|
||||
---
|
||||
description: Implement phase - TDD cycle for writing code
|
||||
---
|
||||
|
||||
# Phase 2: Implement
|
||||
|
||||
## Purpose
|
||||
Write production code following Test-Driven Development (TDD).
|
||||
|
||||
## Prerequisites
|
||||
- Phase 1 (Research) completed
|
||||
- Research log exists at `docs/research_logs/{feature_name}.md`
|
||||
- Mark task as `[/]` in task.md
|
||||
|
||||
## Steps
|
||||
|
||||
**Set Mode:** Use `task_boundary` to set mode to **EXECUTION**.
|
||||
|
||||
### 1. Create Test File
|
||||
Co-locate with implementation:
|
||||
- Go: `*_test.go`
|
||||
- TypeScript: `*.spec.ts`
|
||||
|
||||
### 2. Write Failing Test (Red)
|
||||
```go
|
||||
func TestCreateTask_Success(t *testing.T) {
|
||||
// Arrange
|
||||
mockStore := NewMockStorage()
|
||||
service := NewService(mockStore, logger)
|
||||
|
||||
// Act
|
||||
task, err := service.Create(ctx, userID, req)
|
||||
|
||||
// Assert
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Test Task", task.Title)
|
||||
}
|
||||
```
|
||||
|
||||
Run test to confirm it fails:
|
||||
```bash
|
||||
# Go
|
||||
go test -v ./internal/features/{feature}/...
|
||||
|
||||
# Frontend
|
||||
pnpm run test -- --watch
|
||||
```
|
||||
|
||||
### 3. Write Minimal Code (Green)
|
||||
Write ONLY enough code to make the test pass.
|
||||
|
||||
Run test to confirm it passes:
|
||||
```bash
|
||||
go test -v ./internal/features/{feature}/...
|
||||
```
|
||||
|
||||
### 4. Refactor (Blue)
|
||||
Improve the code while keeping tests green:
|
||||
- Improve names and structure
|
||||
- Remove duplication
|
||||
- **Follow predefined agent rules** (read applicable `.agents/rules/*.md`)
|
||||
- **Handle errors** per Error Handling Principles
|
||||
- **Add logging** per Logging and Observability Mandate
|
||||
- If the refactoring is complex, activate the **Sequential Thinking** skill
|
||||
- If debugging a failing test, activate the **Debugging Protocol** skill
|
||||
- Ensure tests still pass
|
||||
|
||||
### 5. Repeat
|
||||
Continue Red-Green-Refactor for each behavior.
|
||||
|
||||
## Unit Test Requirements
|
||||
- Mock all dependencies (interfaces)
|
||||
- Test happy path, error paths AND edge cases
|
||||
- Target >85% coverage on domain logic
|
||||
|
||||
## Development Commands
|
||||
```bash
|
||||
# Run specific test
|
||||
go test -v -run TestCreateTask ./internal/features/task/...
|
||||
|
||||
# Run with coverage
|
||||
go test -cover ./internal/features/task/...
|
||||
|
||||
# Frontend tests
|
||||
pnpm run test
|
||||
pnpm run test -- --coverage
|
||||
```
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] Unit tests written and passing
|
||||
- [ ] Implementation complete
|
||||
- [ ] Error handling follows principles
|
||||
- [ ] Logging added to operations
|
||||
- [ ] Code follows project patterns
|
||||
- [ ] List all I/O adapters modified or created (storage files, external API clients)
|
||||
- [ ] If adapters were modified: Phase 3 is MANDATORY — do not skip
|
||||
|
||||
|
||||
|
||||
## Next Phase
|
||||
Proceed to **Phase 3: Integrate** (`/3-integrate`)
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
description: Integrate phase - test adapters with real infrastructure
|
||||
---
|
||||
|
||||
# Phase 3: Integrate
|
||||
|
||||
## Purpose
|
||||
Test adapter implementations (database, external APIs) with real infrastructure using Testcontainers.
|
||||
|
||||
## Prerequisites
|
||||
- Phase 2 (Implement) completed
|
||||
- Unit tests passing
|
||||
|
||||
## Applicability (Default: REQUIRED)
|
||||
This phase is REQUIRED by default. You may ONLY skip it if ALL of the following are true:
|
||||
- [ ] No storage/repository files were modified or created
|
||||
- [ ] No external API client files were modified or created
|
||||
- [ ] No database queries or schemas were changed
|
||||
- [ ] No message queue, cache, or I/O adapter code was touched
|
||||
|
||||
If ANY adapter was modified, you MUST write integration tests.
|
||||
|
||||
## If This Phase Fails
|
||||
If integration tests fail:
|
||||
1. Check container logs for errors
|
||||
2. Verify schema matches expectations
|
||||
3. Fix adapter implementation
|
||||
4. Re-run tests before proceeding
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Setup Testcontainers
|
||||
|
||||
**Go Example:**
|
||||
```go
|
||||
func TestPostgresStorage_Integration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Start PostgreSQL container
|
||||
postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: testcontainers.ContainerRequest{
|
||||
Image: "postgres:16-alpine",
|
||||
ExposedPorts: []string{"5432/tcp"},
|
||||
Env: map[string]string{
|
||||
"POSTGRES_USER": "test",
|
||||
"POSTGRES_PASSWORD": "test",
|
||||
"POSTGRES_DB": "test",
|
||||
},
|
||||
WaitingFor: wait.ForListeningPort("5432/tcp"),
|
||||
},
|
||||
Started: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer postgres.Terminate(ctx)
|
||||
|
||||
// Get connection string and run tests
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Write Integration Tests
|
||||
Test file naming: `*_integration_test.go` or `*.integration.spec.ts`
|
||||
|
||||
```go
|
||||
func TestPostgresStorage_CreateTask(t *testing.T) {
|
||||
// Uses real PostgreSQL from Testcontainers
|
||||
storage := NewPostgresStorage(pool)
|
||||
|
||||
task, err := storage.Create(ctx, userID, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, uuid.Nil, task.ID)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Run Integration Tests
|
||||
```bash
|
||||
# Go - run all tests including integration
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
### 4. Manual Check (Optional)
|
||||
- If UI involved, launch a browser to verify the basic flow.
|
||||
- If API involved, use `curl` or `client` to hit the endpoint.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] Integration tests written for all adapters
|
||||
- [ ] Tests pass with real infrastructure (Testcontainers)
|
||||
- [ ] Database queries verified against real PostgreSQL
|
||||
|
||||
## Next Phase
|
||||
Proceed to **Phase 4: Verify** (`/4-verify`)
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
description: Verify phase - run full validation suite
|
||||
---
|
||||
|
||||
# Phase 4: Verify
|
||||
|
||||
## Purpose
|
||||
Run all linters, static analysis, and tests to ensure code quality.
|
||||
|
||||
> **Note:** Paths below follow the project structure defined in `project-structure.md`.
|
||||
> Adjust paths to match the actual project layout (e.g. `apps/backend`, `apps/frontend`).
|
||||
|
||||
### Phase 3 Gate
|
||||
- [ ] Were any storage/database adapter files modified? List them: ___
|
||||
- [ ] Were any external API adapters modified? List them: ___
|
||||
- [ ] If YES to either: are integration tests written and passing?
|
||||
- [ ] If NO to both: state why Phase 3 was skipped
|
||||
|
||||
> CRITICAL: If you modified `*_pg.go`, `*_integration*.go`, or any SQL query, Phase 3 is MANDATORY. Do not proceed without integration tests.
|
||||
|
||||
### Phase 3.5 Gate
|
||||
- [ ] Were any UI changes or modified? List them: ___
|
||||
- [ ] If YES: are E2E tests written and passing?
|
||||
- [ ] If NO to both: state why Phase 3.5 was skipped
|
||||
|
||||
> CRITICAL: If you modified `frontend/*` or any frontend component and integration point, Phase 3.5 is MANDATORY. Do not proceed without E2E tests.
|
||||
|
||||
## If This Phase Fails
|
||||
If lint/test/build fails:
|
||||
1. **Do not proceed** to Ship
|
||||
2. Fix the issue (go back to Phase 2 or 3 as needed)
|
||||
3. Re-run full verification
|
||||
4. Only proceed when ALL checks pass
|
||||
|
||||
## Steps
|
||||
|
||||
**Set Mode:** Use `task_boundary` to set mode to **VERIFICATION**.
|
||||
|
||||
### 1. Backend Validation
|
||||
Run the FULL validation suite for the backend path as defined in `project-structure.md`:
|
||||
|
||||
```bash
|
||||
# // turbo
|
||||
# Adjust path per project-structure.md (default: apps/backend)
|
||||
cd apps/backend && gofumpt -l -e -w . && go vet ./... && staticcheck ./... && gosec -quiet ./... && go test -race ./...
|
||||
```
|
||||
|
||||
### 2. Frontend Validation
|
||||
```bash
|
||||
# // turbo
|
||||
# Adjust path per project-structure.md (default: apps/frontend)
|
||||
cd apps/frontend && pnpm run lint --fix && npx vue-tsc --noEmit && pnpm run test
|
||||
```
|
||||
|
||||
### 3. Build Check
|
||||
```bash
|
||||
# Backend (path per project-structure.md)
|
||||
cd apps/backend && go build ./...
|
||||
|
||||
# Frontend (path per project-structure.md)
|
||||
cd apps/frontend && pnpm run build
|
||||
```
|
||||
|
||||
### 4. Check Coverage
|
||||
Report actual coverage in task summary.
|
||||
|
||||
**Go:**
|
||||
```bash
|
||||
go test -cover ./internal/features/...
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```bash
|
||||
pnpm run test -- --coverage
|
||||
```
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] All lint checks pass
|
||||
- [ ] All tests pass
|
||||
- [ ] Build succeeds
|
||||
- [ ] Coverage reported (target >85% on domain logic)
|
||||
|
||||
## On Success
|
||||
Mark task as `[x]` in task.md (verification passed = task complete).
|
||||
|
||||
## Next Phase
|
||||
Proceed to **Phase 5: Ship** (`/5-commit`)
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
description: Git commit with conventional format
|
||||
---
|
||||
|
||||
# Ship: Commit
|
||||
|
||||
## Purpose
|
||||
Commit completed work with proper conventional commit format.
|
||||
|
||||
> **Note:** For detailed conventions (branch naming, commit types, PR size, merge strategy),
|
||||
> see `git-workflow-principles.md` in `.agents/rules/`.
|
||||
|
||||
## Prerequisites
|
||||
- All verification checks pass
|
||||
- Code is ready for review/merge
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Review Changes
|
||||
```bash
|
||||
git status
|
||||
git diff --staged
|
||||
```
|
||||
|
||||
### 2. Stage Changes
|
||||
```bash
|
||||
# Stage all changes
|
||||
git add .
|
||||
|
||||
# Or stage selectively (adjust path per project-structure.md)
|
||||
git add apps/backend/internal/features/task/
|
||||
```
|
||||
|
||||
### 3. Commit with Conventional Format
|
||||
|
||||
Follow the format from `git-workflow-principles.md`:
|
||||
|
||||
```bash
|
||||
git commit -m "<type>(<scope>): <description>"
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
git commit -m "feat(task): add CRUD API endpoints"
|
||||
git commit -m "fix(auth): correct token expiry validation"
|
||||
git commit -m "refactor(storage): extract interface for storage layer"
|
||||
git commit -m "test(task): add integration tests for storage adapter"
|
||||
```
|
||||
|
||||
### 4. Update task.md
|
||||
Mark completed items as `[x]` in the task checklist.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] Changes committed with proper format
|
||||
- [ ] task.md updated to reflect completion
|
||||
@@ -1,224 +0,0 @@
|
||||
---
|
||||
description: Code audit workflow - structured code review and quality verification
|
||||
---
|
||||
|
||||
# Audit Workflow
|
||||
|
||||
## Purpose
|
||||
Inspect existing code quality and produce structured findings. This workflow does not write new features — it identifies issues for subsequent fix workflows.
|
||||
|
||||
## When to Use
|
||||
- After another agent's feature is committed (cross-agent review)
|
||||
- Periodic quality gates on the codebase
|
||||
- Before releases or deployments
|
||||
- When user wants assurance without writing new code
|
||||
- After addressing review findings, to verify the fixes
|
||||
|
||||
## When NOT to Use
|
||||
- When writing new features (use `/orchestrator`)
|
||||
- When fixing known bugs (use `/quick-fix`)
|
||||
- When restructuring code (use `/refactor`)
|
||||
|
||||
## Pre-Audit Checklist
|
||||
Before starting, you MUST:
|
||||
1. Scan `.agents/rules/` directory — these form the review criteria
|
||||
2. Read `rule-priority.md` for conflict resolution
|
||||
3. Identify the scope of the audit (specific feature, module, or full codebase)
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1: Code Review
|
||||
**Set Mode:** Use `task_boundary` to set mode to **PLANNING**
|
||||
|
||||
Invoke the **Code Review Skill** against the specified files/features.
|
||||
|
||||
Review against these categories (in priority order from `rule-priority.md`):
|
||||
|
||||
#### 1. Security
|
||||
- Input validation on all boundaries
|
||||
- No hardcoded secrets or credentials
|
||||
- Parameterized queries (no SQL injection)
|
||||
- Proper authentication/authorization checks
|
||||
|
||||
#### 2. Reliability
|
||||
- Error handling on all I/O operations (no empty catch blocks)
|
||||
- All resources cleaned up (connections, files, locks)
|
||||
- Timeouts on external calls
|
||||
- Graceful degradation patterns
|
||||
|
||||
#### 3. Testability
|
||||
- I/O operations behind interfaces/abstractions
|
||||
- Business logic is pure (no side effects)
|
||||
- Dependencies injected, not hardcoded
|
||||
- Test coverage on critical paths
|
||||
|
||||
#### 4. Observability
|
||||
- All operation entry points logged (start/success/failure)
|
||||
- Structured logging with correlation IDs
|
||||
- Appropriate log levels
|
||||
|
||||
#### 5. Code Quality
|
||||
- Follows existing codebase patterns (>80% consistency)
|
||||
- Functions are focused and small (10-50 lines)
|
||||
- Clear naming that reveals intent
|
||||
- No code duplication (DRY)
|
||||
|
||||
### Phase 1.5: Cross-Boundary Review
|
||||
**Set Mode:** Continue in **PLANNING**
|
||||
|
||||
Cross-boundary issues live at the seams between components, not inside any single file. This phase defines a **menu of dimensions** — activate only those that apply to the project under audit, and explicitly state which you skipped and why.
|
||||
|
||||
#### Dimension Selection
|
||||
|
||||
| Dimension | Activate When |
|
||||
|---|---|
|
||||
| **A. Integration Contracts** | Project has both a frontend and a backend |
|
||||
| **B. Database & Schema** | Project uses a relational/document database |
|
||||
| **C. Configuration & Environment** | Always — universal |
|
||||
| **D. Dependency Health** | Always — universal |
|
||||
| **E. Test Coverage Gaps** | Always — universal |
|
||||
| **F. Mobile ↔ Backend** | Project has a mobile app and a backend |
|
||||
|
||||
At the start of this phase you MUST state:
|
||||
> "Activating dimensions: A, B, C, D, E. Skipping F (no mobile app)."
|
||||
|
||||
---
|
||||
|
||||
#### Dimension A: Integration Contracts
|
||||
*Applies to: full-stack projects with frontend + backend*
|
||||
|
||||
- [ ] Map every backend endpoint (route + method) against its frontend adapter — flag any unmapped endpoints in either direction
|
||||
- [ ] Verify request/response field names, types, and status codes match across the boundary
|
||||
- [ ] Verify all outbound HTTP calls use the project's centralized API client (not raw `fetch`/`axios`) — see `typescript-idioms-and-patterns.md § Centralized HTTP Client`
|
||||
- [ ] Build an auth coverage matrix: which endpoints require auth, do the frontend adapters send tokens for each?
|
||||
- [ ] Check error contract alignment: does the frontend handle the full set of error codes the backend can return?
|
||||
|
||||
#### Dimension B: Database & Schema
|
||||
*Applies to: projects using a relational or document database*
|
||||
|
||||
- [ ] Verify all tables have required base columns (`id`, `created_at`, `updated_at`)
|
||||
- [ ] Check all foreign keys have corresponding indexes
|
||||
- [ ] If using Supabase or Postgres RLS: verify RLS policies exist on every table storing user data
|
||||
- [ ] Cross-reference the application's struct/model field names against actual DB column names — flag any drift
|
||||
- [ ] Check migrations are reversible (up + down) and follow the additive-first strategy
|
||||
- [ ] Scan storage adapters for N+1 query patterns
|
||||
|
||||
#### Dimension C: Configuration & Environment
|
||||
*Always active*
|
||||
|
||||
- [ ] No hardcoded secrets, tokens, URLs, or credentials in source code
|
||||
- [ ] `.env.template` exists and covers every env var referenced in the codebase
|
||||
- [ ] Startup validation fails fast on missing required config (does not silently fall back to bad defaults)
|
||||
- [ ] Secrets are never logged (not in debug, not in error messages)
|
||||
|
||||
#### Dimension D: Dependency Health
|
||||
*Always active*
|
||||
|
||||
- [ ] No unused top-level dependencies in `go.mod` / `package.json` / `Cargo.toml`
|
||||
- [ ] No circular dependencies between feature modules
|
||||
- [ ] Cross-module imports only use each module's public API (not internal files)
|
||||
- [ ] Run `npm audit` / `go list -m -json all | nancy` / `cargo audit` — flag high-severity CVEs
|
||||
|
||||
#### Dimension E: Test Coverage Gaps
|
||||
*Always active*
|
||||
|
||||
- [ ] A handler/controller test exists for every API endpoint
|
||||
- [ ] An integration test exists for every storage/database adapter
|
||||
- [ ] Every error path (catch block, error return) has at least one test that exercises it
|
||||
- [ ] E2E tests cover the primary user journeys (login, main feature flow, error states)
|
||||
|
||||
#### Dimension F: Mobile ↔ Backend
|
||||
*Applies to: projects with a mobile app and a backend*
|
||||
|
||||
- [ ] API version compatibility — mobile must not call endpoints that no longer exist
|
||||
- [ ] Offline data sync: conflict resolution and retry logic are tested
|
||||
- [ ] Auth token refresh flows work correctly when the access token expires mid-session
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Automated Verification
|
||||
**Set Mode:** Use `task_boundary` to set mode to **VERIFICATION**
|
||||
|
||||
Run the full validation suite (same as `/4-verify`):
|
||||
1. Linters and static analysis
|
||||
2. Full test suite
|
||||
3. Build check
|
||||
4. Coverage report
|
||||
|
||||
### Phase 3: Findings Report
|
||||
|
||||
**Output location:** `docs/audits/review-findings-{feature}-{YYYY-MM-DD}-{HHmm}.md`
|
||||
|
||||
You MUST save the report to the repo (not just as a conversation artifact) so it can be:
|
||||
- Referenced from other conversations/agents
|
||||
- Tracked in version control
|
||||
- Passed as context to fix workflows
|
||||
|
||||
**Steps:**
|
||||
1. Create the `docs/audits/` directory if it doesn't exist
|
||||
2. Write the findings report to `docs/audits/review-findings-{feature}-{YYYY-MM-DD}-{HHmm}.md`
|
||||
3. Use the template below
|
||||
|
||||
> **Zero-Findings Guard:** If the audit produces fewer than 3 findings, you MUST complete the "Dimensions Covered" attestation section in the report before declaring a clean result. This proves cross-boundary coverage was not skipped.
|
||||
|
||||
```markdown
|
||||
# Code Audit: {Feature/Module Name}
|
||||
Date: {date}
|
||||
|
||||
## Summary
|
||||
- **Files reviewed:** N
|
||||
- **Issues found:** N (X critical, Y major, Z minor)
|
||||
- **Test coverage:** N%
|
||||
- **Dimensions activated:** A, B, C, D, E (list which were skipped and why)
|
||||
|
||||
## Critical Issues
|
||||
Issues that must be fixed before deployment.
|
||||
- [ ] {description} — {file}:{line}
|
||||
|
||||
## Major Issues
|
||||
Issues that should be fixed in the near term.
|
||||
- [ ] {description} — {file}:{line}
|
||||
|
||||
## Minor Issues
|
||||
Style, naming, or minor improvements.
|
||||
- [ ] {description} — {file}:{line}
|
||||
|
||||
## Verification Results
|
||||
- Lint: PASS/FAIL
|
||||
- Tests: PASS/FAIL (N passed, N failed)
|
||||
- Build: PASS/FAIL
|
||||
- Coverage: N%
|
||||
|
||||
## Dimensions Covered
|
||||
<!-- Required when total findings < 3 -->
|
||||
| Dimension | Status | Files / Queries Examined |
|
||||
|---|---|---|
|
||||
| A. Integration Contracts | ✅ Checked / ⏭ Skipped (reason) | e.g., all 26 backend routes cross-referenced against 11 frontend adapters |
|
||||
| B. Database & Schema | ✅ Checked / ⏭ Skipped (reason) | e.g., reviewed all 8 Supabase tables + 4 storage adapters |
|
||||
| C. Configuration & Environment | ✅ Checked | e.g., scanned for raw secrets, verified .env.template |
|
||||
| D. Dependency Health | ✅ Checked | e.g., ran npm audit, checked go.mod for unused deps |
|
||||
| E. Test Coverage Gaps | ✅ Checked | e.g., verified handler tests for all 26 endpoints |
|
||||
| F. Mobile ↔ Backend | ⏭ Skipped | No mobile app in this project |
|
||||
```
|
||||
|
||||
## Feedback Loop
|
||||
After the audit produces findings, choose the right workflow based on finding type:
|
||||
|
||||
| Finding Type | Example | Workflow |
|
||||
| ----------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------- |
|
||||
| **Nit / minor** (naming, formatting, missing comment) | "Rename `x` to `userCount`" | Fix in this conversation directly |
|
||||
| **Small isolated fix** (missing log, error handling, validation) | "Add input validation on handler" | `/quick-fix` in a new conversation |
|
||||
| **Structural change** (wrong abstraction, missing interface, pattern inconsistency) | "Storage not behind interface" | `/refactor` in a new conversation |
|
||||
| **Missing capability** (new endpoint, feature, auth check) | "No auth middleware on admin routes" | `/orchestrator` in a new conversation |
|
||||
|
||||
### Using Findings in Other Contexts
|
||||
When starting a fix workflow in a new conversation, reference the persisted report:
|
||||
|
||||
> "Fix the critical issues in `docs/audits/review-findings-gatekeeper-2026-02-16-1430.md`"
|
||||
|
||||
The agent in the new context can read the file directly from the repo — no need to copy-paste findings.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] All specified files/features reviewed
|
||||
- [ ] Full verification suite run
|
||||
- [ ] Findings document saved to `docs/audits/` in the repo
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
description: End-to-end testing with Playwright MCP
|
||||
---
|
||||
|
||||
# E2E Testing Workflow
|
||||
|
||||
## Purpose
|
||||
Validate complete user journeys through the full system using Playwright MCP.
|
||||
|
||||
> **Note:** Paths below follow the project structure defined in `project-structure.md`.
|
||||
> Adjust paths to match the actual project layout.
|
||||
|
||||
## When to Use
|
||||
- After completing an epic/milestone
|
||||
- Before release/deploy
|
||||
- When adding critical user flows
|
||||
|
||||
## Prerequisites
|
||||
- Services running (docker compose up or local dev)
|
||||
- Feature implementation complete
|
||||
- Unit and integration tests passing
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Start Services
|
||||
Ensure the full stack is running:
|
||||
```bash
|
||||
docker compose up -d
|
||||
# Wait for health checks
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
Or local development:
|
||||
```bash
|
||||
# Terminal 1: Backend
|
||||
cd apps/backend && go run cmd/api/main.go
|
||||
|
||||
# Terminal 2: Frontend
|
||||
cd apps/frontend && pnpm run dev
|
||||
```
|
||||
|
||||
### 2. Create E2E Test Plan
|
||||
Document test cases in `e2e/{feature}-{ui|api}.e2e.test.ts`:
|
||||
- Happy path flows
|
||||
- Error handling
|
||||
- Edge cases
|
||||
|
||||
### 3. Execute with Playwright MCP
|
||||
|
||||
**Navigate to page:**
|
||||
```
|
||||
mcp_playwright_browser_navigate(url="http://localhost:5173/login")
|
||||
```
|
||||
|
||||
**Capture page state:**
|
||||
```
|
||||
mcp_playwright_browser_snapshot()
|
||||
```
|
||||
|
||||
**Interact with elements (use refs 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:**
|
||||
```
|
||||
mcp_playwright_browser_snapshot(filename="docs/e2e-screenshots/{feature}-{description}.md")
|
||||
```
|
||||
|
||||
### 4. Document Results
|
||||
|
||||
**Screenshot location:** `docs/e2e-screenshots/{feature}-{description}.png`
|
||||
|
||||
1. Create `docs/e2e-screenshots/` if it doesn't exist
|
||||
2. Save all snapshots there with descriptive names
|
||||
3. For visual proof (video recording), use `browser_subagent` with `RecordingName` parameter
|
||||
4. Embed walkthrough documentation in the walkthrough artifact
|
||||
5. Note any failures or issues
|
||||
6. Update test files if needed
|
||||
|
||||
## E2E Test Structure
|
||||
|
||||
```
|
||||
e2e/ # Single-app projects
|
||||
├── api/ # (For monorepo: apps/e2e/api/)
|
||||
│ └── task-crud-api.e2e.test.ts # API-only E2E
|
||||
└── ui/ # (For monorepo: apps/e2e/ui/)
|
||||
├── auth-flow-ui.e2e.test.ts # Browser E2E
|
||||
└── task-flow-ui.e2e.test.ts
|
||||
```
|
||||
|
||||
> Path adapts to project type per `project-structure.md` § Adapting for Different Project Types.
|
||||
|
||||
## Example: Auth Flow E2E
|
||||
|
||||
```typescript
|
||||
// e2e/ui/auth-flow-ui.e2e.test.ts
|
||||
describe('Authentication Flow', () => {
|
||||
it('should register a new user', async () => {
|
||||
// Navigate to register
|
||||
// Fill form
|
||||
// Submit
|
||||
// Verify redirect to dashboard
|
||||
});
|
||||
|
||||
it('should login existing user', async () => {
|
||||
// Navigate to login
|
||||
// Fill credentials
|
||||
// Submit
|
||||
// Verify dashboard loads
|
||||
});
|
||||
|
||||
it('should show error for invalid credentials', async () => {
|
||||
// Navigate to login
|
||||
// Fill wrong password
|
||||
// Submit
|
||||
// Verify error message
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] All critical user flows tested
|
||||
- [ ] Snapshots captured and embedded in walkthrough
|
||||
- [ ] No blocking issues found
|
||||
@@ -1,139 +0,0 @@
|
||||
---
|
||||
description: Build feature workflow orchestrator - chains all phases
|
||||
---
|
||||
|
||||
# Build Feature Workflow
|
||||
|
||||
**CRITICAL INSTRUCTION**
|
||||
|
||||
YOU ARE FORBIDDEN FROM SKIPPING PHASES.
|
||||
You must treat this file as a State Machine. You cannot transition to state $N+1$ until state $N$ is completely verified.
|
||||
|
||||
## Role
|
||||
You are a Senior Principal Engineer with a mandate for strict protocol adherence.
|
||||
|
||||
Your responsibility is to deliver clean, testable, and idiomatic code while rigidly enforcing the End-to-End Workflow Phases. You must reject any attempt to skip phases—such as writing code without research or completing task/shipping without verification.
|
||||
|
||||
Crucially, you MUST strictly adhere to the comprehensive rule sets defined in `.agents/rules/` (e.g., error handling, logging, security, concurrency). These rules are non-negotiable constraints that supersede general training data.
|
||||
|
||||
## Pre-Implementation Checklist
|
||||
Before starting any work, you MUST:
|
||||
1. Scan `.agents/rules/` directory
|
||||
2. Identify applicable rules for this task
|
||||
3. **READ** selected rule files (they are non-negotiable constraints)
|
||||
|
||||
## Workflow Phases
|
||||
|
||||
Execute phases **sequentially**. Do not skip phases for velocity.
|
||||
Never deferred quality gates to a later "hardening" phase, defensive rigour early in the cycle trumps.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
P1[Research] -->|Log Created| P2[Implement]
|
||||
P2 -->|Unit Tests Pass| P3[Integrate]
|
||||
P3 -->|Integration Tests Pass| P3b{"E2E needed?"}
|
||||
P3b -->|Yes| P3c[E2E Test]
|
||||
P3b -->|No| P4[Verify]
|
||||
P3c -->|E2E Pass| P4[Verify]
|
||||
P4 -->|All Linters Pass| P5[Ship]
|
||||
```
|
||||
|
||||
### Phase 1: Research
|
||||
**File:** `1-research.md`
|
||||
**Mandatory Rules:** `project-structure.md`, `architectural-pattern.md`
|
||||
- Analyze request, understand context
|
||||
- Define scope in `task.md`
|
||||
- Search external documentation for each technology (use available tools: Qurio, Context7, web search)
|
||||
- Document findings in `docs/research_logs/{feature}.md`
|
||||
- If a significant architecture decision is made, create an ADR using the **ADR Skill**
|
||||
|
||||
**Skills to consider:**
|
||||
- **Sequential Thinking** — for complex design decisions requiring iteration
|
||||
|
||||
### Phase 2: Implement
|
||||
**File:** `2-implement.md`
|
||||
**Mandatory Rules:** `error-handling-principles.md`, `logging-and-observability-mandate.md`, `testing-strategy.md`
|
||||
- TDD cycle: Red → Green → Refactor
|
||||
- Unit tests with mocked dependencies
|
||||
|
||||
**Skills to consider:**
|
||||
- **Sequential Thinking** — if refactoring is complex and requires multi-step reasoning
|
||||
- **Debugging Protocol** — if a failing test has a non-obvious cause
|
||||
|
||||
### Phase 3: Integrate
|
||||
**File:** `3-integrate.md`
|
||||
**Mandatory Rules:** `testing-strategy.md`, `resources-and-memory-management-principles.md`
|
||||
- Integration tests with Testcontainers
|
||||
- Test adapters against real infrastructure
|
||||
|
||||
### Phase 3.5: E2E Validation (Conditional)
|
||||
**File:** `e2e-test.md`
|
||||
**When Required:**
|
||||
- UI components were added or modified
|
||||
- API endpoints were added or modified that interact with frontend
|
||||
- Critical user-facing flows were changed
|
||||
|
||||
**When to Skip:**
|
||||
- Pure backend/infrastructure changes
|
||||
- Internal library refactoring
|
||||
- Test-only changes
|
||||
|
||||
**Gate:** At least one critical user journey tested and screenshot captured.
|
||||
|
||||
### Phase 4: Verify
|
||||
**File:** `4-verify.md`
|
||||
**Mandatory Rules:** `code-completion-mandate.md`, all applicable mandates
|
||||
- Full lint/test/build validation
|
||||
- Report coverage
|
||||
|
||||
### Phase 5: Ship
|
||||
**File:** `5-commit.md`
|
||||
**Mandatory Rules:** `git-workflow-principles.md`
|
||||
- Git commit with conventional format
|
||||
- Update task.md
|
||||
|
||||
---
|
||||
|
||||
## Phase Management
|
||||
|
||||
### Task.md Updates
|
||||
Use this pattern throughout phases:
|
||||
- `[ ]` = Not started
|
||||
- `[/]` = In progress (mark when **starting** a task)
|
||||
- `[x]` = Complete (mark **only after Verify phase passes**)
|
||||
|
||||
**Rule:** Never mark `[x]` until Phase 4 (Verify) passes for that task.
|
||||
|
||||
### Phase Transitions
|
||||
Before moving to the next phase, **STOP and verify**:
|
||||
- [ ] Current phase completion criteria met
|
||||
- [ ] Outputs created (research log, tests, etc.)
|
||||
- [ ] No blocking errors
|
||||
|
||||
### Error Handling
|
||||
If a phase fails:
|
||||
1. **Document the failure** in task summary
|
||||
2. **Do not proceed** to next phase
|
||||
3. **Fix the issue** within current phase
|
||||
4. **Re-run** phase completion criteria
|
||||
5. Then proceed
|
||||
|
||||
### Resuming Work
|
||||
To resume from a specific phase:
|
||||
1. Read `task.md` to find current state (`[/]` items)
|
||||
2. Read the relevant phase file
|
||||
3. Continue from where you left off
|
||||
4. No need to re-run completed phases
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Phase | Output | Blocking |
|
||||
|-------|--------|----------|
|
||||
| Research | `task.md` + `docs/research_logs/*.md` | Yes |
|
||||
| Implement | Unit tests + code | Yes |
|
||||
| Integrate | Integration tests | Yes (for adapters) |
|
||||
| E2E (conditional) | E2E tests + screenshots | Yes (when required) |
|
||||
| Verify | All checks pass | Yes |
|
||||
| Ship | Git commit | Yes |
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
description: Profile-driven performance optimization workflow
|
||||
---
|
||||
|
||||
# Performance Optimization Workflow
|
||||
|
||||
**Trigger:** User provides profiling data, asks to optimize performance, or benchmarks show regression.
|
||||
|
||||
**Skill prerequisite:** Read `.agents/skills/perf-optimization/SKILL.md` before starting.
|
||||
|
||||
// turbo-all
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Load the Skill
|
||||
Read the perf-optimization skill and the relevant language module:
|
||||
```
|
||||
view_file .agents/skills/perf-optimization/SKILL.md
|
||||
view_file .agents/skills/perf-optimization/languages/{language}.md
|
||||
```
|
||||
|
||||
### 2. Collect Profile Data
|
||||
|
||||
**If the user provides a profile file or URL:**
|
||||
- Use the language-appropriate extraction script (e.g., `scripts/go-pprof.sh cpu profile.prof`)
|
||||
|
||||
**If the user asks to profile from scratch:**
|
||||
- Run the extraction script in `bench` mode to generate AND analyze profiles in one step:
|
||||
```bash
|
||||
bash .agents/skills/perf-optimization/scripts/go-pprof.sh bench ./path/to/package/... BenchmarkName
|
||||
```
|
||||
|
||||
### 3. Analyze
|
||||
|
||||
Create a structured analysis document:
|
||||
```
|
||||
docs/research_logs/{component}-perf-analysis.md
|
||||
```
|
||||
|
||||
Follow the analysis methodology from SKILL.md:
|
||||
1. Focus on cumulative, trace flat back to user-land code
|
||||
2. Identify top 3-5 offenders
|
||||
3. Separate benchmark artifacts from production cost
|
||||
4. Identify irreducible floors (reference the language module's table)
|
||||
|
||||
### 4. Prioritize Fixes
|
||||
|
||||
Create an implementation plan ranking fixes by impact/risk:
|
||||
- Low risk, high impact → do first
|
||||
- High risk, any impact → do last or skip
|
||||
|
||||
Present the plan to the user for approval before proceeding.
|
||||
|
||||
### 5. Implement (one fix at a time)
|
||||
|
||||
For each fix, follow the orchestrator workflow:
|
||||
|
||||
1. **Write tests first** (TDD Red → Green)
|
||||
2. **Implement the fix**
|
||||
3. **Run all existing tests** (`go test -race ./...` or equivalent)
|
||||
4. **Benchmark immediately** — compare ns/op, B/op, allocs/op
|
||||
5. **Run quality checks** (formatter, linter, security scanner)
|
||||
6. **Commit independently** with conventional format: `perf(scope): description`
|
||||
|
||||
**Rule:** One fix per commit. Never batch optimizations.
|
||||
|
||||
### 6. Final Verification
|
||||
|
||||
After all fixes are applied:
|
||||
1. Run the full benchmark suite with `-count=3` minimum
|
||||
2. Compare against the original baseline (before any fixes)
|
||||
3. Run the complete test suite with `-race`
|
||||
4. Run all quality checks (formatter, linter, security scanner, build)
|
||||
|
||||
### 7. Document Results
|
||||
|
||||
Update the analysis document with:
|
||||
- Before/after benchmark comparison table
|
||||
- Which fixes were applied and which were skipped (and why)
|
||||
- Any remaining optimization opportunities for future sessions
|
||||
|
||||
### 8. Ship
|
||||
|
||||
Commit and present the final results to the user with:
|
||||
- Cumulative benchmark improvement table
|
||||
- List of commits
|
||||
- Any follow-up items
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Phase | Output | Gate |
|
||||
|---|---|---|
|
||||
| Profile | Raw data + extracted markdown | Data collected |
|
||||
| Analyze | `docs/research_logs/{component}-perf-analysis.md` | Top offenders identified |
|
||||
| Prioritize | Implementation plan | User approved |
|
||||
| Implement | Tests + code + benchmark per fix | Each fix passes tests |
|
||||
| Verify | Full benchmark comparison | All checks pass |
|
||||
| Ship | Conventional commits | User notified |
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
description: Quick-fix workflow for hotfixes and small bug fixes - skip research, minimal verify
|
||||
---
|
||||
|
||||
# Quick-Fix Workflow
|
||||
|
||||
## Purpose
|
||||
Fast-track bug fixes and small changes that don't require full research or integration phases.
|
||||
|
||||
## When to Use
|
||||
- Bug fixes with a known root cause
|
||||
- Small, isolated changes (< 50 lines)
|
||||
- Hotfixes for production issues
|
||||
- Addressing review findings from `/audit`
|
||||
|
||||
## When NOT to Use
|
||||
- New features (use `/orchestrator`)
|
||||
- Refactoring (use `/refactor`)
|
||||
- Changes touching multiple features or modules
|
||||
|
||||
## Pre-Implementation Checklist
|
||||
Before starting, you MUST:
|
||||
1. Scan `.agents/rules/` directory for applicable rules
|
||||
2. Read `rule-priority.md` for conflict resolution
|
||||
3. Confirm the fix scope is truly small and isolated
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1: Diagnose
|
||||
**Set Mode:** Use `task_boundary` to set mode to **PLANNING**
|
||||
|
||||
1. Identify the bug or issue
|
||||
2. Locate the affected code
|
||||
3. If the cause is not immediately obvious, activate the **Debugging Protocol** skill
|
||||
4. Define the fix in `task.md` (1-3 items max)
|
||||
|
||||
### Phase 2: Fix + Test (TDD)
|
||||
**Set Mode:** Use `task_boundary` to set mode to **EXECUTION**
|
||||
|
||||
1. **Write a failing test** that reproduces the bug
|
||||
2. **Apply the minimal fix** to make the test pass
|
||||
3. **Verify existing tests** still pass
|
||||
4. Follow applicable rules:
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
|
||||
### Phase 3: Verify + Ship
|
||||
**Set Mode:** Use `task_boundary` to set mode to **VERIFICATION**
|
||||
|
||||
1. Run the full validation suite (same as `/4-verify`)
|
||||
2. If all checks pass, commit with conventional format (same as `/5-commit`)
|
||||
3. Use commit type `fix(<scope>): <description>`
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] Bug reproduced with a test
|
||||
- [ ] Fix applied and test passes
|
||||
- [ ] Full verification suite passes
|
||||
- [ ] Committed with `fix` type
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
description: Refactor workflow for safely restructuring existing code
|
||||
---
|
||||
|
||||
# Refactor Workflow
|
||||
|
||||
## Purpose
|
||||
Safely restructure existing code while preserving behavior. Uses a different mindset than greenfield feature development: understand first, then change structure.
|
||||
|
||||
## When to Use
|
||||
- Code restructuring (moving, renaming, splitting modules)
|
||||
- Pattern migration (e.g., switching from callbacks to async/await)
|
||||
- Dependency upgrades with breaking changes
|
||||
- Addressing tech debt or architectural improvements
|
||||
|
||||
**Requires a specific goal.** Invoke with a clear target, not an open directory:
|
||||
- ✅ `/refactor extract storage interface in task feature`
|
||||
- ✅ `/refactor split user handler into separate auth handler`
|
||||
- ❌ `/refactor apps/backend` (too vague — use `/audit` first to find specific issues)
|
||||
|
||||
## When NOT to Use
|
||||
- New features (use `/orchestrator`)
|
||||
- Small bug fixes (use `/quick-fix`)
|
||||
- "Find what to improve" (use `/audit` first, then `/refactor` for structural findings)
|
||||
|
||||
## Pre-Implementation Checklist
|
||||
Before starting, you MUST:
|
||||
1. Scan `.agents/rules/` directory for applicable rules
|
||||
2. Read `architectural-pattern.md` and `project-structure.md`
|
||||
3. Read `rule-priority.md` for conflict resolution
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1: Impact Analysis
|
||||
**Set Mode:** Use `task_boundary` to set mode to **PLANNING**
|
||||
|
||||
1. **Map the blast radius** — what files, modules, and tests are affected?
|
||||
2. **Document existing behavior** — what tests currently pass? what contracts exist?
|
||||
3. **Identify risks** — can this be done incrementally or is a big-bang needed?
|
||||
4. **Create refactoring plan** in `task.md` with incremental steps
|
||||
5. If decision involves trade-offs, create an ADR using the **ADR Skill**
|
||||
|
||||
**Skills to consider:**
|
||||
- **Sequential Thinking** — for multi-step refactoring with interdependencies
|
||||
|
||||
### Phase 2: Incremental Change (TDD)
|
||||
**Set Mode:** Use `task_boundary` to set mode to **EXECUTION**
|
||||
|
||||
For each step in the refactoring plan:
|
||||
1. **Ensure existing tests pass** before making any change
|
||||
2. **Make one incremental change** — move, rename, or restructure
|
||||
3. **Run tests after each change** — behavior must be preserved
|
||||
4. **Add new tests** if the refactoring exposes untested behavior
|
||||
5. Follow applicable rules:
|
||||
- Architectural Patterns @architectural-pattern.md
|
||||
- Code Organization Principles @code-organization-principles.md (includes circular dependency guidance)
|
||||
|
||||
**Key principle:** Never break the build for more than one step at a time.
|
||||
|
||||
### Phase 3: Parity Verification
|
||||
**Set Mode:** Use `task_boundary` to set mode to **VERIFICATION**
|
||||
|
||||
1. Run the full validation suite (same as `/4-verify`)
|
||||
2. **Compare test coverage** — coverage should be equal to or better than before
|
||||
3. **Verify no behavior changes** — same inputs produce same outputs
|
||||
4. If applicable, run E2E tests (`/e2e-test`)
|
||||
|
||||
### Phase 4: Ship
|
||||
Follow `/5-commit` with commit type `refactor(<scope>): <description>`
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] Impact analysis documented
|
||||
- [ ] All changes made incrementally with tests passing at each step
|
||||
- [ ] Full verification suite passes
|
||||
- [ ] Test coverage is equal to or better than before
|
||||
- [ ] Committed with `refactor` type
|
||||
@@ -1 +0,0 @@
|
||||
{"timestamp":"2026-04-11T05:27:00Z","iteration":0,"metric":100,"status":"baseline","summary":"Initial baseline - 5/5 tests passing","commit":"baseline"}
|
||||
@@ -1 +0,0 @@
|
||||
{"started_at": "2026-04-11T05:27:00Z", "iteration": 0}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__plugin_context-mode_context-mode__ctx_batch_execute",
|
||||
"mcp__plugin_context-mode_context-mode__ctx_execute",
|
||||
"mcp__plugin_context-mode_context-mode__ctx_search",
|
||||
"Bash(mkdir -p \"d:/Documents/Github/forgelm/.git/worktrees/distracted-pike\")",
|
||||
"Bash(echo \"D:/Documents/Github/forgelm/.claude/worktrees/distracted-pike/.git\")",
|
||||
"Bash(echo \"../..\")",
|
||||
"Bash(echo \"ref: refs/heads/claude/distracted-pike\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
Submodule .claude/worktrees/distracted-pike deleted from 9da25c3688.
+34
-12
@@ -1,14 +1,36 @@
|
||||
node_modules/
|
||||
dist/
|
||||
# Rust build artifacts
|
||||
target/
|
||||
*.log
|
||||
*.png
|
||||
*.gguf
|
||||
|
||||
# llama.cpp build outputs (inside vendor/)
|
||||
vendor/llama.cpp/build-dev/
|
||||
vendor/llama.cpp/build-prod/
|
||||
|
||||
# Model files (large binaries – store outside git)
|
||||
models/*.gguf
|
||||
models/*.bin
|
||||
models/*.safetensors
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
registry/__pycache__/
|
||||
|
||||
# Env files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Runtime logs
|
||||
logs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
nul
|
||||
tauri-app/dist/
|
||||
tauri-app/node_modules/
|
||||
models/
|
||||
vendor/llama-bin/
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Editor
|
||||
.vscode/settings.json
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# Local agent worktrees
|
||||
.claude/worktrees/
|
||||
@@ -1,73 +0,0 @@
|
||||
# AI Backend Studio
|
||||
|
||||
## What This Is
|
||||
|
||||
AI Backend Studio is a local-first desktop workspace for running and managing local LLM runtimes, selecting models, and chatting against the active runtime from a Tauri app. This roadmap focuses on the existing runtime workspace experience: making runtime control and configuration easier to understand, safer to edit, and more reliable for operators using the desktop app.
|
||||
|
||||
## Core Value
|
||||
|
||||
A user can configure and operate the runtime from one trustworthy workspace without the UI fighting their in-progress edits.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated
|
||||
|
||||
- ✓ Local runtime can be started, stopped, and health-checked from the desktop app - existing
|
||||
- ✓ Runtime configuration can be edited from the desktop app and persisted through the Tauri backend - existing
|
||||
- ✓ Users can browse model sources and select models for the runtime - existing
|
||||
- ✓ Users can chat against the active runtime with locally persisted chat history - existing
|
||||
|
||||
### Active
|
||||
|
||||
- [ ] Runtime Workspace presents one clear control center for runtime status, actions, and settings
|
||||
- [ ] Runtime status summary and Runtime Settings are reorganized to reduce duplication and operator confusion
|
||||
- [ ] Runtime settings fields such as Managed library and Models folder do not get overwritten by polling or background refresh while the user is editing
|
||||
- [ ] Runtime settings preserve explicit user choices until save/cancel and separate live snapshot state from draft form state
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Backend inference engine changes - this initiative targets runtime workspace UX and frontend state flow, not model execution internals
|
||||
- Chat feature redesign outside runtime-related touchpoints - chat already works and is not the primary problem being scoped here
|
||||
- New cloud orchestration or multi-user control plane - current focus is the existing local-first desktop surface
|
||||
|
||||
## Context
|
||||
|
||||
- Existing codebase is brownfield with a Rust workspace, Tauri desktop backend, and React/TypeScript frontend already mapped in `.planning/codebase/ARCHITECTURE.md` and `.planning/codebase/STACK.md`
|
||||
- Runtime management currently spans `RuntimeStatusCard` and `RuntimeSettingsDrawer`, creating a split-brain control experience
|
||||
- The current frontend runtime flow polls live runtime state and rehydrates the settings form, which can overwrite in-progress edits or folder selections before save
|
||||
- The existing architecture already supports runtime lifecycle operations, model discovery, persisted runtime config, and local chat history, so this roadmap should build on those capabilities rather than replace them
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Tech stack**: Keep the existing Rust + Tauri + React/TypeScript architecture - the project is improving the current app, not rewriting it
|
||||
- **Brownfield compatibility**: Preserve working runtime lifecycle, model selection, and chat flows while reorganizing the UI - regressions in validated capabilities are not acceptable
|
||||
- **UX reliability**: Unsaved user edits must not be overwritten by polling or snapshot refresh - trust in the settings surface is the main product constraint
|
||||
- **Local-first**: Continue supporting local desktop runtime management patterns already established in the app - scope should not depend on external hosted infrastructure
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Rationale | Outcome |
|
||||
|----------|-----------|---------|
|
||||
| Treat this as a brownfield runtime workspace improvement project | The repo already contains a working desktop runtime manager, model configuration flow, and chat surface | - Pending |
|
||||
| Center the roadmap on runtime workspace clarity and edit safety | The primary user pain is split control surfaces plus auto-overwritten form state | - Pending |
|
||||
| Keep runtime operations and runtime configuration in one coherent experience | Current duplication between external status card and settings drawer is a major source of confusion | - Pending |
|
||||
|
||||
## Evolution
|
||||
|
||||
This document evolves at phase transitions and milestone boundaries.
|
||||
|
||||
**After each phase transition** (via `/gsd-transition`):
|
||||
1. Requirements invalidated? -> Move to Out of Scope with reason
|
||||
2. Requirements validated? -> Move to Validated with phase reference
|
||||
3. New requirements emerged? -> Add to Active
|
||||
4. Decisions to log? -> Add to Key Decisions
|
||||
5. "What This Is" still accurate? -> Update if drifted
|
||||
|
||||
**After each milestone** (via `/gsd-complete-milestone`):
|
||||
1. Full review of all sections
|
||||
2. Core Value check - still the right priority?
|
||||
3. Audit Out of Scope - reasons still valid?
|
||||
4. Update Context with current state
|
||||
|
||||
---
|
||||
*Last updated: 2026-03-28 after initialization*
|
||||
@@ -1,86 +0,0 @@
|
||||
# Requirements: AI Backend Studio
|
||||
|
||||
**Defined:** 2026-03-28
|
||||
**Core Value:** A user can configure and operate the runtime from one trustworthy workspace without the UI fighting their in-progress edits.
|
||||
|
||||
## v1 Requirements
|
||||
|
||||
### Runtime Workspace
|
||||
|
||||
- [x] **WORK-01**: User can access one authoritative runtime workspace for runtime status, lifecycle actions, and runtime settings
|
||||
- [x] **WORK-02**: User can understand the difference between live runtime state and unsaved draft settings while editing
|
||||
|
||||
### Settings Safety
|
||||
|
||||
- [x] **CONF-01**: User can edit runtime settings without background polling or refresh overwriting dirty fields
|
||||
- [x] **CONF-02**: User can explicitly save, cancel, or revert runtime settings changes
|
||||
- [x] **CONF-03**: User can keep folder-path and provider/model-source selections stable until they choose to change or discard them
|
||||
- [x] **CONF-04**: User can recover from failed save or validation errors without losing the current draft
|
||||
|
||||
### Runtime Control
|
||||
|
||||
- [x] **CTRL-01**: User can start, stop, and retry runtime lifecycle actions from the runtime workspace with clear in-progress and completion feedback
|
||||
- [x] **CTRL-02**: User can tell whether a configuration change applies immediately or requires restart/restart-and-apply semantics
|
||||
- [x] **CTRL-03**: User cannot trigger conflicting runtime actions while another lifecycle operation is still in progress
|
||||
|
||||
### Model Sources
|
||||
|
||||
- [ ] **MODL-01**: User can choose runtime/model sources and only see valid model choices for the current source and mode
|
||||
- [ ] **MODL-02**: User can select managed library and models folder locations with validation before saving
|
||||
|
||||
### Diagnostics
|
||||
|
||||
- [x] **DIAG-01**: User can see actionable runtime health details, including readiness, endpoint/mode context, active model context, and relevant failure guidance
|
||||
|
||||
## v2 Requirements
|
||||
|
||||
### Runtime Workspace Enhancements
|
||||
|
||||
- **WORK-03**: User can compare live runtime state and draft settings side by side in a dedicated compare view
|
||||
- **WORK-04**: User can restore unsaved runtime drafts after app reload or restart
|
||||
|
||||
### Model Sources Enhancements
|
||||
|
||||
- **MODL-03**: User can preserve separate drafts when switching between local and remote/runtime source modes
|
||||
- **MODL-04**: User can browse richer model compatibility hints and source provenance before applying a model change
|
||||
|
||||
### Diagnostics Enhancements
|
||||
|
||||
- **DIAG-02**: User can see per-setting impact previews before saving high-impact runtime changes
|
||||
- **DIAG-03**: User can trigger guided repair actions such as open logs, reveal models folder, or reset to last saved state from diagnostic surfaces
|
||||
|
||||
## Out of Scope
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| Backend inference engine changes | This milestone targets runtime workspace UX and frontend/runtime-management flow, not model execution internals |
|
||||
| Broad chat experience redesign | Chat is already validated and is outside the runtime-workspace trust problem |
|
||||
| Silent autosave for runtime-impacting settings | Explicit save/cancel semantics are required to avoid surprise mutations and restarts |
|
||||
| Separate full-featured status and settings control centers | The roadmap should reduce duplication, not preserve split-brain control surfaces |
|
||||
| Cloud control plane or multi-user orchestration | The scoped work is for the current local-first desktop experience |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| WORK-01 | Phase 3 | Complete |
|
||||
| WORK-02 | Phase 1 | Complete |
|
||||
| CONF-01 | Phase 1 | Complete |
|
||||
| CONF-02 | Phase 1 | Complete |
|
||||
| CONF-03 | Phase 1 | Complete |
|
||||
| CONF-04 | Phase 1 | Complete |
|
||||
| CTRL-01 | Phase 2 | Complete |
|
||||
| CTRL-02 | Phase 2 | Complete |
|
||||
| CTRL-03 | Phase 2 | Complete |
|
||||
| MODL-01 | Phase 4 | Pending |
|
||||
| MODL-02 | Phase 4 | Pending |
|
||||
| DIAG-01 | Phase 3 | Complete |
|
||||
|
||||
**Coverage:**
|
||||
- v1 requirements: 12 total
|
||||
- Mapped to phases: 12
|
||||
- Unmapped: 0
|
||||
|
||||
---
|
||||
*Requirements defined: 2026-03-28*
|
||||
*Last updated: 2026-03-29 after Phase 2 completion*
|
||||
@@ -1,127 +0,0 @@
|
||||
# Roadmap: AI Backend Studio
|
||||
|
||||
## Overview
|
||||
|
||||
This milestone hardens the existing Tauri runtime workspace so operators can trust what they are editing, understand what is live versus draft, and control the runtime from one coherent surface. The roadmap follows the natural dependency chain in the requirements: protect draft ownership first, then make runtime commands safe and explicit, then consolidate the workspace around that stable state model, and finally harden model-source validation and diagnostics.
|
||||
|
||||
## Phases
|
||||
|
||||
**Phase Numbering:**
|
||||
- Integer phases (1, 2, 3): Planned milestone work
|
||||
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
|
||||
|
||||
- [x] **Phase 1: Draft State Ownership** - Separate live runtime state from editable draft state so polling cannot clobber user input.
|
||||
- [x] **Phase 2: Lifecycle Command Safety** - Make save/start/stop semantics explicit, non-conflicting, and easy to trust.
|
||||
- [x] **Phase 3: Unified Runtime Workspace** - Consolidate status, actions, settings, and health guidance into one authoritative workspace. (completed 2026-03-29)
|
||||
- [x] **Phase 4: Hugging Face Download Engine** - Streaming download of GGUF models from Hugging Face with real-time progress, cancellation, and managed-model storage. (completed 2026-03-31)
|
||||
- [x] **Phase 5: Model Source Validation** - Harden source-specific model and path selection with reliable validation and recovery cues. (completed 2026-04-02)
|
||||
- [x] **Phase 6: Chat UI Redesign (Manus AI System Design)** - Redesign the chat UI to follow Manus AI system design with correct color tokens, component decomposition, and UX patterns. (completed 2026-04-02)
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 1: Draft State Ownership
|
||||
**Goal**: Users can safely edit runtime settings as a draft that remains separate from live runtime state until they intentionally save or discard changes.
|
||||
**Depends on**: Nothing (first phase)
|
||||
**Requirements**: WORK-02, CONF-01, CONF-02, CONF-03, CONF-04
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can edit runtime settings while polling or background refresh continues without dirty fields being overwritten.
|
||||
2. User can tell which values are unsaved draft settings versus the currently effective runtime state while editing.
|
||||
3. User can explicitly save, cancel, or revert runtime settings changes and see the workspace reflect that choice.
|
||||
4. User's folder-path and provider or source selections remain stable until they choose to save or discard them.
|
||||
5. User can recover from save or validation failures without losing the current draft.
|
||||
**Plans**: 3 plans
|
||||
Plans:
|
||||
- [x] `01-01-PLAN.md` - Separate live snapshot polling from saved baseline and draft ownership in the runtime manager.
|
||||
- [x] `01-02-PLAN.md` - Refactor the runtime settings drawer around explicit draft/live presentation and discard semantics.
|
||||
- [x] `01-03-PLAN.md` - Harden field-scoped async updates, invalid draft preservation, and save failure recovery.
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 2: Lifecycle Command Safety
|
||||
**Goal**: Users can run runtime lifecycle actions with clear apply semantics, reliable busy-state rules, and predictable completion behavior.
|
||||
**Depends on**: Phase 1
|
||||
**Requirements**: CTRL-01, CTRL-02, CTRL-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can start, stop, or retry runtime lifecycle actions from the runtime workspace and see in-progress and completion feedback.
|
||||
2. User can tell whether a configuration change applies immediately or requires restart or restart-and-apply semantics.
|
||||
3. User cannot trigger conflicting runtime lifecycle actions while another lifecycle operation is still in progress.
|
||||
**Plans**: 3 plans
|
||||
- [x] `02-01-PLAN.md` - Add backend lifecycle command metadata, conflict guards, and restart-required contract alignment.
|
||||
- [x] `02-02-PLAN.md` - Add frontend lifecycle/apply state orchestration and manager-level regression coverage.
|
||||
- [x] `02-03-PLAN.md` - Update runtime status/drawer lifecycle UX with explicit apply and completion feedback.
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 3: Unified Runtime Workspace
|
||||
**Goal**: Users can operate the runtime from one authoritative workspace that combines status, controls, settings, and actionable health context.
|
||||
**Depends on**: Phase 2
|
||||
**Requirements**: WORK-01, DIAG-01
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can access one authoritative runtime workspace for runtime status, lifecycle actions, and runtime settings.
|
||||
2. User can see actionable runtime health details, including readiness, endpoint or mode context, active model context, and relevant failure guidance, from that same workspace.
|
||||
3. User can understand and act on runtime issues without bouncing between duplicated status and settings surfaces.
|
||||
**Plans**: 3 plans
|
||||
- [x] `03-01-PLAN.md` - Define manager-owned runtime workspace summary and health diagnostics contracts.
|
||||
- [x] `03-02-PLAN.md` - Build a unified runtime workspace component that combines actions, health context, and draft editing.
|
||||
- [x] `03-03-PLAN.md` - Integrate the unified workspace into the shell and remove split runtime surfaces.
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 4: Hugging Face Download Engine
|
||||
**Goal**: Users can download GGUF models directly from Hugging Face with real-time progress, cancellation, and automatic storage in the managed-models directory.
|
||||
**Depends on**: Phase 3 (Unified Runtime Workspace)
|
||||
**Requirements**: HF-DL-01, HF-DL-02, HF-DL-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can click download on a Hugging Face search result row and see real-time progress (MB downloaded, total MB, speed MB/s, percentage).
|
||||
2. User can cancel an in-progress download and have the partial file cleaned up.
|
||||
3. Downloaded files are saved to the managed-models directory with proper path structure.
|
||||
4. Downloads use the HF bearer token when available for authenticated access.
|
||||
5. The download runs in Rust to avoid IPC bottlenecks, with progress emitted via Tauri events.
|
||||
**Plans**: 3 plans
|
||||
- [x] `04-01-PLAN.md` - Rust streaming download engine with progress events.
|
||||
- [x] `04-02-PLAN.md` - React hook `useModelDownload` with Tauri event bridge.
|
||||
- [x] `04-03-PLAN.md` - SettingsPage integration with `.sp-hf-file` row progress state.
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 5: Model Source Validation
|
||||
**Goal**: Users can choose valid runtime sources, models, and storage paths without destabilizing draft safety or applying invalid combinations.
|
||||
**Depends on**: Phase 4
|
||||
**Requirements**: MODL-01, MODL-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. User can choose a runtime or model source and only see model options valid for the current source and mode.
|
||||
2. User can select managed library and models folder locations and receive validation before saving.
|
||||
3. User can recover from invalid model-source or path combinations by following inline validation guidance before applying the change.
|
||||
**Plans**: 3 plans
|
||||
- [x] `05-01-PLAN.md` - Add source-mode validation contract.
|
||||
- [x] `05-02-PLAN.md` - Add path and model file validation.
|
||||
- [x] `05-03-PLAN.md` - Add validation UI and recovery experience.
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 6: Chat UI Redesign (Manus AI System Design)
|
||||
**Goal**: Redesign the chat UI to follow Manus AI system design with correct color tokens, component decomposition, and UX patterns.
|
||||
**Depends on**: Phase 5
|
||||
**Requirements**: UI-01, UI-02, UI-03, UI-04
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Model selector lives inside the chat input pill (hero and regular), not the sidebar.
|
||||
2. scan_models returns only top-level GGUF files (no subdirectory traversal).
|
||||
3. Thinking indicator is a single pulsing blue dot + "Thinking" text.
|
||||
4. Send button uses PaperPlaneRight icon only.
|
||||
5. App uses Manus color tokens (#171717 base, #1E1E1E surface, #262626 elevated).
|
||||
6. Settings page aligns to Manus design tokens.
|
||||
7. Agentic mode renders a Manus-style execution transcript with a dedicated thinking card, dense system progress rows, and a clean final response block.
|
||||
8. Agentic execution progress can stream into the chat UI via Tauri events without breaking normal chat mode.
|
||||
**Plans**: 4 plans
|
||||
Plans:
|
||||
- [x] `06-01-PLAN.md` — Backend scan fix + ModelSelectorPill + Sidebar cleanup + ChatView wiring
|
||||
- [x] `06-02-PLAN.md` — Global Manus tokens + thinking indicator + send button + message widths
|
||||
- [x] `06-03-PLAN.md` — Settings page Manus token alignment
|
||||
- [x] `06-04-PLAN.md` — Agentic Manus conversation blocks + Tauri progress event bridge + final response/artifact presentation
|
||||
**UI hint**: yes
|
||||
|
||||
## Progress
|
||||
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Draft State Ownership | 3/3 | Complete | 2026-03-29 |
|
||||
| 2. Lifecycle Command Safety | 3/3 | Complete | 2026-03-29 |
|
||||
| 3. Unified Runtime Workspace | 3/3 | Complete | 2026-03-29 |
|
||||
| 4. Hugging Face Download Engine | 3/3 | Complete | 2026-03-31 |
|
||||
| 5. Model Source Validation | 3/3 | Complete | 2026-04-02 |
|
||||
| 6. Chat UI Redesign | 4/4 | Complete | 2026-04-02 |
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: complete
|
||||
stopped_at: All phases complete - milestone ready for audit
|
||||
last_updated: "2026-04-02T18:00:00.000Z"
|
||||
last_activity: 2026-04-02
|
||||
progress:
|
||||
total_phases: 6
|
||||
completed_phases: 6
|
||||
total_plans: 16
|
||||
completed_plans: 16
|
||||
percent: 100
|
||||
---
|
||||
|
||||
# Project State
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: `.planning/PROJECT.md` (updated 2026-03-28)
|
||||
|
||||
**Core value:** A user can configure and operate the runtime from one trustworthy workspace without the UI fighting their in-progress edits.
|
||||
**Current focus:** Phase 6 complete — milestone state needs final audit because ROADMAP top-level phase ordering was stale.
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 6 of 6 (Chat UI Redesign)
|
||||
Plan: 4 of 4 completed for current phase
|
||||
Status: Phase complete — verified
|
||||
Last activity: 2026-04-02 - Completed Manus-style agentic transcript blocks and Tauri event bridge scaffold
|
||||
|
||||
Progress: [██████████] 100%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**Velocity:**
|
||||
|
||||
- Total plans completed: 12
|
||||
- Average duration: 13min
|
||||
- Total execution time: ~2.6 hours
|
||||
|
||||
**By Phase:**
|
||||
|
||||
| Phase | Plans | Total | Avg/Plan |
|
||||
|-------|-------|-------|----------|
|
||||
| 01 | 3 | 46min | 15min |
|
||||
| 02 | 3 | 33min | 11min |
|
||||
| 03 | 3 | 39min | 13min |
|
||||
| 04 | 3 | 45min | 15min |
|
||||
|
||||
**Recent Trend:**
|
||||
|
||||
- Last 5 plans: 04-03 (15min), 04-02 (15min), 04-01 (15min), 03-03 (13min), 03-02 (13min)
|
||||
- Trend: Stable
|
||||
|
||||
| Phase 01 P01 | 10min | 2 tasks | 4 files |
|
||||
| Phase 01 P02 | 18min | 2 tasks | 4 files |
|
||||
| Phase 01 P03 | 18min | 2 tasks | 5 files |
|
||||
| Phase 02-lifecycle-command-safety P01 | 6min | 2 tasks | 3 files |
|
||||
| Phase 02-lifecycle-command-safety P02 | 14min | 2 tasks | 5 files |
|
||||
| Phase 02-lifecycle-command-safety P03 | 13min | 2 tasks | 7 files |
|
||||
| Phase 03-unified-runtime-workspace P01 | 13min | 2 tasks | 4 files |
|
||||
| Phase 03-unified-runtime-workspace P02 | 13min | 2 tasks | 5 files |
|
||||
| Phase 03-unified-runtime-workspace P03 | 13min | 2 tasks | 6 files |
|
||||
| Phase 04-hf-download-engine P01 | 15min | 2 tasks | 3 files |
|
||||
| Phase 04-hf-download-engine P02 | 15min | 2 tasks | 3 files |
|
||||
| Phase 04-hf-download-engine P03 | 15min | 2 tasks | 4 files |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
|
||||
Decisions are logged in PROJECT.md Key Decisions table.
|
||||
Recent decisions affecting current work:
|
||||
|
||||
- Phase 1 starts with state ownership fixes before UI consolidation to eliminate poll-driven draft overwrites first.
|
||||
- This milestone stays brownfield and must preserve working lifecycle, model selection, and chat flows while improving runtime workspace trust.
|
||||
- [Phase 01]: Kept legacy snapshot and saveConfig outputs while adding explicit draft APIs so existing runtime drawer consumers keep compiling during the Phase 1 transition.
|
||||
- [Phase 01]: When a runtime draft is dirty, polling updates only operational live fields and leaves baseline plus draft config untouched.
|
||||
- [Phase 02-lifecycle-command-safety]: Expose active and last lifecycle command data on RuntimeSnapshot so frontend polling can render trustworthy lifecycle feedback without inferring from status alone.
|
||||
- [Phase 02-lifecycle-command-safety]: Centralize duplicate start/stop blocking in runtime state helpers so save, start, and stop flows share the same command-result contract.
|
||||
- [Phase 02-lifecycle-command-safety]: Keep restart-required save enforcement limited to connection_mode, model_path, addr, gpu_layers, and ctx_size to stay aligned with the existing backend runtime boundary.
|
||||
- [Phase 02-lifecycle-command-safety]: Mirror backend lifecycle metadata directly into the frontend RuntimeSnapshot so polling and command responses share one contract.
|
||||
- [Phase 02-lifecycle-command-safety]: Derive applyImpact from saved baseline versus draft config using the backend restart-required field list instead of UI-only heuristics.
|
||||
- [Phase 02-lifecycle-command-safety]: Expose activeLifecycleCommand, lastLifecycleResult, and lifecycleBusy from the manager so status-card and drawer copy can consume explicit state in plan 02-03.
|
||||
- [Phase 02-lifecycle-command-safety]: Use explicit lifecycle result banners instead of overloading last_error so completed and blocked actions remain visible after polling refreshes.
|
||||
- [Phase 02-lifecycle-command-safety]: Render apply guidance in the drawer before the CTA row so users see whether a draft applies immediately, needs restart, or must be saved before apply.
|
||||
- [Phase 02-lifecycle-command-safety]: Keep status-card and drawer props backward-compatible with optional lifecycle fields while Sidebar forwards the new manager contract.
|
||||
- [Phase 03-01]: Reuse existing restart-impact field list and lifecycle labels instead of creating parallel heuristics
|
||||
- [Phase 03-01]: Derive diagnostics from backend-owned snapshot fields (status, ready, process_running, resolved_url, config.*, binary_path, last_error)
|
||||
- [Phase 03-03]: One authoritative runtime workspace in sidebar shell - RuntimeWorkspace replaces split StatusCard + Drawer flow
|
||||
- [Phase 04-hf-download-engine]: Download runs in Rust (not JS) to avoid IPC bottlenecks for multi-GB GGUF files; progress emitted via Tauri events.
|
||||
- [Phase 04-hf-download-engine]: Direct resolve URL `https://huggingface.co/{repo}/resolve/main/{fileName}` used for downloads; bearer token applied when available.
|
||||
- [Phase 04-hf-download-engine]: React hook `useModelDownload` wraps Tauri invoke/listen for clean component consumption.
|
||||
- [Phase 04-hf-download-engine]: `.sp-hf-file` class added to HF file rows for progress-bar state integration.
|
||||
|
||||
### Pending Todos
|
||||
|
||||
None yet.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
None — all phases complete.
|
||||
|
||||
### Quick Tasks Completed
|
||||
|
||||
| # | Description | Date | Commit | Directory |
|
||||
|---|-------------|------|--------|-----------|
|
||||
| 1 | Investigar e corrigir bug de internal error na segunda mensagem do chat | 2026-03-30 | abb9db0 | [1-investigar-e-corrigir-bug-de-internal-er](./quick/1-investigar-e-corrigir-bug-de-internal-er/) |
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-31T00:25:00.000Z
|
||||
Stopped at: Completed Phase 4 - Hugging Face Download Engine
|
||||
Resume file: None
|
||||
Phase 5: Model Source Validation is already implemented via runtime validation in Phase 2/3. Marking as complete.
|
||||
@@ -1,150 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
**Analysis Date:** 2026-03-28
|
||||
|
||||
## Pattern Overview
|
||||
|
||||
**Overall:** Local-first polyglot workspace with shared contracts and three user surfaces
|
||||
|
||||
**Key Characteristics:**
|
||||
- Keep API contracts in Rust shared types before exposing them to CLI and desktop consumers in `crates/api-contracts/src/lib.rs` and `crates/api-contracts/src/chat.rs`
|
||||
- Keep inference logic behind a single engine/session boundary in `crates/inference-core/src/engine.rs` and `crates/inference-core/src/session.rs`
|
||||
- Keep the desktop app as an orchestration shell that manages process lifecycle, model discovery, and chat persistence in `tauri-app/src/App.tsx` and `tauri-app/src-tauri/src/runtime/mod.rs`
|
||||
|
||||
## Layers
|
||||
|
||||
**Inference Core:**
|
||||
- Purpose: Load GGUF models, own llama context/model pointers, and generate tokens
|
||||
- Location: `crates/inference-core/src/`
|
||||
- Contains: `EngineConfig`, `InferenceEngine`, runtime profiles, sampler config, generation session
|
||||
- Depends on: `crates/llama-sys`, Rust stdlib, `anyhow`, `tracing`
|
||||
- Used by: `crates/inference-server/src/main.rs`
|
||||
|
||||
**HTTP Compatibility Layer:**
|
||||
- Purpose: Adapt OpenAI-like chat/model requests to the local engine
|
||||
- Location: `crates/inference-server/src/`
|
||||
- Contains: env-driven `ServerConfig`, auth/CORS rules, prompt preparation, routing, SSE streaming
|
||||
- Depends on: `crates/inference-core`, `crates/api-contracts`, `crates/metrics`
|
||||
- Used by: local HTTP consumers, CLI, desktop frontend
|
||||
|
||||
**Shared API Contract Layer:**
|
||||
- Purpose: Keep request/response models and error shapes consistent
|
||||
- Location: `crates/api-contracts/src/`
|
||||
- Contains: chat completion structs, model list, health payload, error payloads
|
||||
- Depends on: `serde`, `serde_json`
|
||||
- Used by: server code directly; mirrored conceptually by frontend/CLI callers
|
||||
|
||||
**CLI Surface Layer:**
|
||||
- Purpose: Expose operator workflows and a future control plane over HTTP/local actions
|
||||
- Location: `crates/localllm-cli/src/main.rs`
|
||||
- Contains: command parsing, planning, preview rendering, selective live transport
|
||||
- Depends on: `clap`, `ureq`, `serde_json`
|
||||
- Used by: developers/operators
|
||||
|
||||
**Desktop Backend Layer:**
|
||||
- Purpose: Persist runtime config, discover model files, detect GPU, spawn/stop local API
|
||||
- Location: `tauri-app/src-tauri/src/`
|
||||
- Contains: Tauri commands, runtime state machine, health probing, file dialog integration
|
||||
- Depends on: Tauri runtime, native process and filesystem APIs
|
||||
- Used by: `tauri-app/src/features/runtime/api.ts`
|
||||
|
||||
**Desktop Frontend Layer:**
|
||||
- Purpose: Present runtime controls, model selection, and persistent chat UI
|
||||
- Location: `tauri-app/src/`
|
||||
- Contains: shell layout, runtime manager hooks, chat view, sidebar, markdown rendering, local DB wrapper
|
||||
- Depends on: React, fetch, Tauri invoke bridge, browser storage/CDN-loaded sql.js
|
||||
- Used by: end users of the desktop app
|
||||
|
||||
## Data Flow
|
||||
|
||||
**Local chat request flow:**
|
||||
|
||||
1. `tauri-app/src/components/ChatView.tsx` builds message history from local sql.js storage and posts to `${server}/v1/chat/completions`
|
||||
2. `crates/inference-server/src/routes.rs` validates auth/model/mode, prepares the prompt through `crates/inference-server/src/compat.rs`, and locks the shared `InferenceEngine`
|
||||
3. `crates/inference-core/src/session.rs` tokenizes, pre-fills, samples, and returns streamed or buffered content back through Axum SSE/JSON
|
||||
4. `tauri-app/src/components/ChatView.tsx` parses `<think>...</think>`, stores assistant content plus usage metrics in `tauri-app/src/lib/db.ts`, and updates the UI
|
||||
|
||||
**Runtime management flow:**
|
||||
|
||||
1. `tauri-app/src/features/runtime/useRuntimeManager.ts` bootstraps a `RuntimeSnapshot` from `tauri-app/src/features/runtime/api.ts`
|
||||
2. `tauri-app/src-tauri/src/lib.rs` routes invoke calls into `tauri-app/src-tauri/src/runtime/mod.rs`
|
||||
3. `tauri-app/src-tauri/src/runtime/process.rs` finds and spawns the `localllm` binary, then `tauri-app/src-tauri/src/runtime/health.rs` probes `/health`
|
||||
4. `tauri-app/src/features/runtime/components/RuntimeStatusCard.tsx` and `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` render and mutate the runtime state
|
||||
|
||||
**State Management:**
|
||||
- Backend API state is a single shared `AppState` with `Arc<Mutex<InferenceEngine>>` in `crates/inference-server/src/routes.rs`
|
||||
- Desktop runtime state is a single `RuntimeState(pub Mutex<RuntimeInner>)` in `tauri-app/src-tauri/src/runtime/state.rs`
|
||||
- Frontend state is local React hook state; no global store is used in `tauri-app/src/App.tsx`, `tauri-app/src/hooks/useModels.ts`, and `tauri-app/src/features/runtime/useRuntimeManager.ts`
|
||||
- Chat persistence is local-first and browser-backed via `localStorage` + sql.js in `tauri-app/src/lib/db.ts`
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
**InferenceEngine:**
|
||||
- Purpose: Own one loaded model and one llama context
|
||||
- Examples: `crates/inference-core/src/engine.rs`, `crates/inference-core/src/session.rs`
|
||||
- Pattern: Single-owner, RAII-managed native resource wrapper with explicit non-concurrent use
|
||||
|
||||
**ServerConfig / ModelCatalog:**
|
||||
- Purpose: Centralize runtime auth, prompt, CORS, and alias rules
|
||||
- Examples: `crates/inference-server/src/compat.rs`
|
||||
- Pattern: Env-derived compatibility adapter layered in front of the engine
|
||||
|
||||
**RuntimeSnapshot:**
|
||||
- Purpose: Serialize the desktop runtime state into a frontend-safe shape
|
||||
- Examples: `tauri-app/src/features/runtime/types.ts`, `tauri-app/src-tauri/src/runtime/state.rs`
|
||||
- Pattern: Backend source of truth mirrored into a polling frontend hook
|
||||
|
||||
**DB wrapper:**
|
||||
- Purpose: Hide sql.js statement handling and persistence details behind a tiny query/run API
|
||||
- Examples: `tauri-app/src/lib/db.ts`
|
||||
- Pattern: Thin synchronous local repository over browser storage
|
||||
|
||||
## Entry Points
|
||||
|
||||
**HTTP API binary:**
|
||||
- Location: `crates/inference-server/src/main.rs`
|
||||
- Triggers: `cargo run -p inference-server` or spawned local desktop runtime
|
||||
- Responsibilities: read env, load/warm engine, build router, bind network listener
|
||||
|
||||
**CLI binary:**
|
||||
- Location: `crates/localllm-cli/src/main.rs`
|
||||
- Triggers: `cargo run -p localllm-cli -- ...`
|
||||
- Responsibilities: parse commands, build execution plans, optionally perform live HTTP requests
|
||||
|
||||
**Desktop frontend:**
|
||||
- Location: `tauri-app/src/main.tsx` and `tauri-app/src/App.tsx`
|
||||
- Triggers: Vite dev server or Tauri desktop app startup
|
||||
- Responsibilities: render app shell, bootstrap DB/runtime hooks, connect sidebar and chat surface
|
||||
|
||||
**Desktop backend:**
|
||||
- Location: `tauri-app/src-tauri/src/lib.rs`
|
||||
- Triggers: Tauri app startup and frontend `invoke()` calls
|
||||
- Responsibilities: expose commands, manage runtime state, wire OS dialogs/plugins
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Strategy:** Convert most backend failures into typed user-facing snapshots/responses rather than panic paths
|
||||
|
||||
**Patterns:**
|
||||
- API uses `ApiError` + `ErrorResponse` with HTTP status mapping in `crates/inference-server/src/routes.rs`
|
||||
- Desktop runtime stores recoverable errors in `last_error` on `RuntimeSnapshot` in `tauri-app/src-tauri/src/runtime/state.rs`
|
||||
- Frontend fetch and invoke failures typically degrade to fallback state or inline assistant error messages in `tauri-app/src/components/ChatView.tsx` and `tauri-app/src/features/runtime/api.ts`
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
**Logging:** `tracing` on Rust services in `crates/inference-server/src/main.rs`; frontend logging is minimal and mostly absent
|
||||
**Validation:** request and mode validation lives in `crates/inference-server/src/compat.rs`; runtime form validation is mostly helper-based in `tauri-app/src/features/runtime/runtime-settings-form.ts`
|
||||
**Authentication:** optional API-key auth is enforced in `crates/inference-server/src/compat.rs`; desktop frontend forwards the configured API key from runtime config in `tauri-app/src/features/runtime/api.ts`
|
||||
|
||||
## Boundaries
|
||||
|
||||
**App boundaries:**
|
||||
- `crates/inference-core/` should stay inference-only and not absorb HTTP/UI concerns
|
||||
- `crates/inference-server/` should stay the public backend boundary and not own chat persistence or desktop state
|
||||
- `crates/localllm-cli/` should stay an operator/client surface and not become the runtime itself
|
||||
- `tauri-app/src-tauri/` should own process/config/filesystem concerns for desktop only
|
||||
- `tauri-app/src/` should stay a consumer/orchestrator over backend contracts and local chat persistence
|
||||
|
||||
---
|
||||
|
||||
*Architecture analysis: 2026-03-28*
|
||||
@@ -1,132 +0,0 @@
|
||||
# Codebase Concerns
|
||||
|
||||
**Analysis Date:** 2026-03-28
|
||||
|
||||
## Tech Debt
|
||||
|
||||
**CLI surface vs real implementation:**
|
||||
- Issue: `crates/localllm-cli/src/main.rs` exposes a broad command tree, but many branches collapse to summary-only behavior instead of real actions
|
||||
- Files: `crates/localllm-cli/src/main.rs`, `docs/cli/overview.md`
|
||||
- Impact: product expectations exceed actual operator capability; docs and UX need constant caveats
|
||||
- Fix approach: split stable commands from roadmap commands, or implement local/config/model actions behind dedicated modules and integration tests
|
||||
|
||||
**Monolithic files in core surfaces:**
|
||||
- Issue: major logic is concentrated in very large files such as `crates/inference-server/src/routes.rs`, `crates/inference-server/src/compat.rs`, `crates/localllm-cli/src/main.rs`, `tauri-app/src/components/ChatView.tsx`, and `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`
|
||||
- Files: `crates/inference-server/src/routes.rs`, `crates/inference-server/src/compat.rs`, `crates/localllm-cli/src/main.rs`, `tauri-app/src/components/ChatView.tsx`, `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`
|
||||
- Impact: high change risk, harder reviewability, and low reuse of logic across features
|
||||
- Fix approach: extract transport, prompt-building, persistence, and UI section components into smaller feature modules with focused tests
|
||||
|
||||
**Frontend fallback state diverges from desktop source of truth:**
|
||||
- Issue: `tauri-app/src/features/runtime/api.ts` silently falls back to `localStorage` snapshots whenever Tauri invoke fails
|
||||
- Files: `tauri-app/src/features/runtime/api.ts`, `tauri-app/src/features/runtime/runtime-utils.ts`
|
||||
- Impact: browser preview works, but runtime state can drift from the real desktop backend and mask integration failures
|
||||
- Fix approach: make fallback mode explicit in UI and separate browser-demo mode from real desktop mode
|
||||
|
||||
## Known Bugs
|
||||
|
||||
**Local runtime binary name mismatch risk:**
|
||||
- Symptoms: desktop runtime start can fail even when the backend was built, because the Tauri helper looks for `localllm` while the error message tells users to build `inference-server`
|
||||
- Files: `tauri-app/src-tauri/src/runtime/process.rs`, `crates/inference-server/Cargo.toml`
|
||||
- Trigger: building the wrong cargo target or expecting package name and binary name to match
|
||||
- Workaround: build the binary defined in `crates/inference-server/Cargo.toml:8` and ensure `target/release/localllm(.exe)` exists
|
||||
|
||||
**Streaming parser depends on simple line splitting:**
|
||||
- Symptoms: malformed SSE framing or larger edge-case chunks can be ignored silently during streaming
|
||||
- Files: `tauri-app/src/components/ChatView.tsx`, `crates/inference-server/src/routes.rs`
|
||||
- Trigger: partial `data:` framing, unexpected event fields, or non-content deltas
|
||||
- Workaround: disable streaming in the UI when debugging transport issues
|
||||
|
||||
## Security Considerations
|
||||
|
||||
**Remote API keys stored in plain frontend/backend config:**
|
||||
- Risk: remote API keys are stored in desktop runtime config JSON and also surfaced into frontend state
|
||||
- Files: `tauri-app/src-tauri/src/runtime/config.rs`, `tauri-app/src/features/runtime/types.ts`, `tauri-app/src/features/runtime/api.ts`
|
||||
- Current mitigation: password input masks display in `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx:281`
|
||||
- Recommendations: move secrets to OS keychain/credential store and store only references in app config
|
||||
|
||||
**External CDN dependencies inside desktop app:**
|
||||
- Risk: chat persistence and syntax highlighting depend on runtime-loaded third-party scripts from jsDelivr
|
||||
- Files: `tauri-app/src/lib/db.ts`, `tauri-app/src/lib/markdown.ts`
|
||||
- Current mitigation: markdown output is sanitized with DOMPurify in `tauri-app/src/lib/markdown.ts:68`
|
||||
- Recommendations: vendor `sql.js` and `highlight.js` assets into the app bundle and pin integrity-checked local assets
|
||||
|
||||
## Performance Bottlenecks
|
||||
|
||||
**Single-engine serialized generation:**
|
||||
- Problem: the API holds one `Arc<Mutex<InferenceEngine>>` for all requests
|
||||
- Files: `crates/inference-server/src/routes.rs:31`, `crates/inference-server/src/routes.rs:159`, `crates/inference-server/src/routes.rs:364`
|
||||
- Cause: engine/context are single-owner and not safely concurrent in `crates/inference-core/src/engine.rs`
|
||||
- Improvement path: add queued scheduling or a context/session pool before scaling beyond one active generation
|
||||
|
||||
**Polling-heavy desktop runtime updates:**
|
||||
- Problem: frontend polls runtime snapshot every 3 seconds and models every 30 seconds
|
||||
- Files: `tauri-app/src/features/runtime/useRuntimeManager.ts:33`, `tauri-app/src/hooks/useModels.ts:38`
|
||||
- Cause: no push/event channel between Tauri backend and React frontend
|
||||
- Improvement path: add Tauri event emission or a subscription layer for runtime state and model refresh triggers
|
||||
|
||||
## Fragile Areas
|
||||
|
||||
**Chat request + persistence loop:**
|
||||
- Files: `tauri-app/src/components/ChatView.tsx`, `tauri-app/src/lib/db.ts`, `tauri-app/src/lib/chat.ts`
|
||||
- Why fragile: request orchestration, DB writes, streaming parsing, `<think>` parsing, title updates, and optimistic UI all live in one component
|
||||
- Safe modification: extract persistence, transport, and stream parsing into dedicated hooks/utilities before adding more chat features
|
||||
- Test coverage: no direct tests detected for `tauri-app/src/components/ChatView.tsx`
|
||||
|
||||
**Compatibility layer prompt/tool-call handling:**
|
||||
- Files: `crates/inference-server/src/compat.rs`, `crates/api-contracts/src/chat.rs`
|
||||
- Why fragile: one file owns auth, aliases, prompt shaping, template choice, response finalization, and tool-call compatibility behavior
|
||||
- Safe modification: add focused unit tests around each compatibility branch before extending OpenAI-style contract coverage
|
||||
- Test coverage: partial for contracts in `crates/api-contracts/src/chat.rs`; compatibility file-specific tests are not visible in the analyzed slice
|
||||
|
||||
## Scaling Limits
|
||||
|
||||
**Backend request concurrency:**
|
||||
- Current capacity: effectively one active generation at a time per server process because one engine/context is mutex-locked
|
||||
- Limit: parallel requests queue behind one model context and long generations block others
|
||||
- Scaling path: multi-context pool, worker queue, or multi-process model serving
|
||||
|
||||
**Desktop chat storage:**
|
||||
- Current capacity: local sql.js database exported into one `localStorage` key in `tauri-app/src/lib/db.ts:57`
|
||||
- Limit: browser storage size, serialization overhead, and startup cost grow with chat history
|
||||
- Scaling path: move persistence into native Tauri storage or SQLite through the Rust backend
|
||||
|
||||
## Dependencies at Risk
|
||||
|
||||
**CDN-hosted `sql.js` and `highlight.js`:**
|
||||
- Risk: runtime availability and supply-chain reliability depend on external network/CDN access
|
||||
- Impact: chat history DB boot or syntax highlighting can fail in offline/locked-down environments
|
||||
- Migration plan: bundle assets locally or replace with native persistence plus bundled syntax assets
|
||||
|
||||
## Missing Critical Features
|
||||
|
||||
**No push observability for runtime lifecycle:**
|
||||
- Problem: runtime state changes and server logs are not streamed into the UI
|
||||
- Blocks: rich diagnostics, trustworthy startup progress, and smoother local-runtime UX
|
||||
|
||||
**No model library management workflow:**
|
||||
- Problem: model scanning/import exists, but there is no deeper catalog metadata, download progress, space management, or activation history
|
||||
- Blocks: desktop app becoming a full local-model manager rather than a thin launcher
|
||||
|
||||
## Test Coverage Gaps
|
||||
|
||||
**Chat UI and persistence path:**
|
||||
- What's not tested: chat send/stream/error/edit/delete/memory/persistence flows
|
||||
- Files: `tauri-app/src/components/ChatView.tsx`, `tauri-app/src/components/MessageBubble.tsx`, `tauri-app/src/lib/db.ts`
|
||||
- Risk: regressions in the main user workflow can ship unnoticed
|
||||
- Priority: High
|
||||
|
||||
**CLI live behavior:**
|
||||
- What's not tested: end-to-end live HTTP execution and local action implementations behind `execute_live_command`
|
||||
- Files: `crates/localllm-cli/src/main.rs`
|
||||
- Risk: parser tests pass while actual command behavior remains incomplete or broken
|
||||
- Priority: High
|
||||
|
||||
**Server route integration:**
|
||||
- What's not tested: full Axum route behavior for auth, streaming, aliases, and error payloads against a running router
|
||||
- Files: `crates/inference-server/src/routes.rs`, `crates/inference-server/src/compat.rs`
|
||||
- Risk: contract drift between docs and runtime behavior
|
||||
- Priority: High
|
||||
|
||||
---
|
||||
|
||||
*Concerns audit: 2026-03-28*
|
||||
@@ -1,86 +0,0 @@
|
||||
# Technology Stack
|
||||
|
||||
**Analysis Date:** 2026-03-28
|
||||
|
||||
## Languages
|
||||
|
||||
**Primary:**
|
||||
- Rust 2021 - core backend, inference runtime, CLI, shared contracts, metrics, and Tauri desktop backend in `Cargo.toml`, `crates/inference-server/src/main.rs`, `crates/inference-core/src/engine.rs`, `crates/localllm-cli/src/main.rs`, and `tauri-app/src-tauri/src/lib.rs`
|
||||
- TypeScript 5.5 - desktop frontend, runtime manager UI, chat UI, and tests in `tauri-app/package.json`, `tauri-app/src/App.tsx`, and `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`
|
||||
|
||||
**Secondary:**
|
||||
- CSS - hand-authored styling with app-wide tokens and feature CSS modules in `tauri-app/src/index.css`, `tauri-app/src/components/Sidebar.css`, and `tauri-app/src/components/ChatView.css`
|
||||
- Markdown - product and architecture docs in `README.MD`, `docs/api/overview.md`, and `docs/app/overview.md`
|
||||
- Python - benchmark and experiment scripts in `experiments/bench_chat_latency.py`, `experiments/bench_cli_surface.py`, and related files under `experiments/`
|
||||
|
||||
## Runtime
|
||||
|
||||
**Environment:**
|
||||
- Rust toolchain targeting native desktop/server binaries; no workspace-level rust-toolchain file detected
|
||||
- Node.js runtime for Vite/Tauri frontend workflows in `tauri-app/package.json`
|
||||
|
||||
**Package Manager:**
|
||||
- Cargo for Rust workspace dependencies and binaries in `Cargo.toml`
|
||||
- npm is the documented frontend package manager via `README.MD:47`; lockfiles for npm and pnpm are both present in `tauri-app/package-lock.json` and `tauri-app/pnpm-lock.yaml`
|
||||
- Lockfile: present for Rust and frontend in `Cargo.lock` and `tauri-app/package-lock.json`
|
||||
|
||||
## Frameworks
|
||||
|
||||
**Core:**
|
||||
- Axum 0.8 - local HTTP API in `Cargo.toml` and `crates/inference-server/src/routes.rs`
|
||||
- React 18 - desktop UI shell and chat surface in `tauri-app/package.json` and `tauri-app/src/App.tsx`
|
||||
- Tauri 2 - desktop shell, local process control, file dialogs, and frontend-to-Rust command bridge in `tauri-app/src-tauri/Cargo.toml` and `tauri-app/src-tauri/src/lib.rs`
|
||||
- llama.cpp FFI via custom `llama-sys` - model loading and token generation in `crates/inference-core/src/engine.rs` and `crates/inference-core/src/session.rs`
|
||||
|
||||
**Testing:**
|
||||
- Vitest 4 - frontend/unit/component tests in `tauri-app/package.json` and `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx`
|
||||
- Rust built-in test harness - backend/unit tests co-located in files such as `crates/api-contracts/src/chat.rs` and `tauri-app/src-tauri/src/runtime/state.rs`
|
||||
|
||||
**Build/Dev:**
|
||||
- Vite 5 - frontend dev/build tool in `tauri-app/vite.config.ts`
|
||||
- TypeScript compiler - typecheck step in `tauri-app/package.json:9`
|
||||
- Tauri CLI 2 - desktop dev/build entry in `tauri-app/package.json:11`
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
**Critical:**
|
||||
- `tokio` - async runtime for API server in `Cargo.toml` and `crates/inference-server/src/main.rs`
|
||||
- `tower-http` - CORS and trace middleware in `Cargo.toml` and `crates/inference-server/src/compat.rs`
|
||||
- `serde` / `serde_json` - shared request/response contracts across API, CLI, and Tauri in `crates/api-contracts/src/chat.rs` and `tauri-app/src-tauri/src/runtime/config.rs`
|
||||
- `clap` - CLI command surface in `crates/localllm-cli/src/main.rs`
|
||||
- `ureq` - synchronous HTTP transport for CLI live execution in `crates/localllm-cli/src/main.rs:404`
|
||||
- `marked`, `dompurify`, `highlight.js` - markdown rendering/sanitization/code highlighting in `tauri-app/src/lib/markdown.ts`
|
||||
- `@tauri-apps/api` and `@tauri-apps/plugin-shell` - frontend/Tauri bridge in `tauri-app/package.json` and `tauri-app/src/features/runtime/api.ts`
|
||||
|
||||
**Infrastructure:**
|
||||
- `uuid` - response IDs for chat completions in `crates/inference-server/src/routes.rs:126`
|
||||
- `tracing` and `tracing-subscriber` - backend logging and startup diagnostics in `crates/inference-server/src/main.rs`
|
||||
- `async-stream` - SSE streaming implementation in `crates/inference-server/Cargo.toml` and `crates/inference-server/src/routes.rs:137`
|
||||
|
||||
## Configuration
|
||||
|
||||
**Environment:**
|
||||
- Backend runtime is environment-driven via `LOCALLLM_MODEL`, `LOCALLLM_PROFILE`, `LOCALLLM_ADDR`, `LOCALLLM_GPU_LAYERS`, `LOCALLLM_CTX`, `LOCALLLM_API_KEY`, `LOCALLLM_CORS_ORIGINS`, `LOCALLLM_AUTH_HEADER`, `LOCALLLM_AUTH_SCHEME`, `LOCALLLM_SYSTEM_PROMPT`, and `LOCALLLM_MODEL_ALIASES` in `crates/inference-server/src/main.rs` and `crates/inference-server/src/compat.rs`
|
||||
- Desktop runtime config is persisted as JSON in the app data directory through `tauri-app/src-tauri/src/runtime/config.rs`
|
||||
- Frontend fallback runtime snapshot is persisted in `localStorage` through `tauri-app/src/features/runtime/api.ts`
|
||||
|
||||
**Build:**
|
||||
- Rust workspace build in `Cargo.toml`
|
||||
- Tauri desktop build in `tauri-app/src-tauri/Cargo.toml` and `tauri-app/src-tauri/tauri.conf.json`
|
||||
- Frontend dev server/build in `tauri-app/vite.config.ts` and `tauri-app/package.json`
|
||||
|
||||
## Platform Requirements
|
||||
|
||||
**Development:**
|
||||
- Rust + Cargo for backend and desktop backend in `README.MD:56`
|
||||
- Node.js/npm for the Tauri frontend in `README.MD:57`
|
||||
- Local `.gguf` model for real inference in `README.MD:58`
|
||||
- Optional NVIDIA GPU for accelerated local runtime; CPU fallback profile exists in `README.MD:59` and `crates/inference-core/src/profile.rs`
|
||||
|
||||
**Production:**
|
||||
- Local-first desktop deployment via Tauri app in `tauri-app/`
|
||||
- Local HTTP API deployment as standalone binary `localllm` in `crates/inference-server/Cargo.toml:8`
|
||||
|
||||
---
|
||||
|
||||
*Stack analysis: 2026-03-28*
|
||||
@@ -1,154 +0,0 @@
|
||||
# Codebase Structure
|
||||
|
||||
**Analysis Date:** 2026-03-28
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```text
|
||||
ForgeLM/
|
||||
├── crates/ # Rust workspace crates for backend, engine, CLI, contracts, metrics, FFI
|
||||
├── tauri-app/ # Desktop application with React frontend and Tauri Rust backend
|
||||
├── docs/ # Product, API, CLI, and developer documentation
|
||||
├── experiments/ # Benchmarks and exploratory scripts
|
||||
├── models/ # Local model assets and model notes
|
||||
├── scripts/ # Build helpers
|
||||
├── target/ # Cargo build artifacts
|
||||
└── ui/ # Standalone HTML chat prototype
|
||||
```
|
||||
|
||||
## Directory Purposes
|
||||
|
||||
**`crates/api-contracts/`:**
|
||||
- Purpose: Shared API payload definitions
|
||||
- Contains: request/response/error model types
|
||||
- Key files: `crates/api-contracts/src/chat.rs`, `crates/api-contracts/src/models.rs`, `crates/api-contracts/src/health.rs`
|
||||
|
||||
**`crates/inference-core/`:**
|
||||
- Purpose: Inference engine and generation pipeline
|
||||
- Contains: engine/session/profile/sampler code
|
||||
- Key files: `crates/inference-core/src/engine.rs`, `crates/inference-core/src/session.rs`, `crates/inference-core/src/profile.rs`
|
||||
|
||||
**`crates/inference-server/`:**
|
||||
- Purpose: Public local HTTP API
|
||||
- Contains: Axum bootstrap, routes, compatibility logic
|
||||
- Key files: `crates/inference-server/src/main.rs`, `crates/inference-server/src/routes.rs`, `crates/inference-server/src/compat.rs`
|
||||
|
||||
**`crates/localllm-cli/`:**
|
||||
- Purpose: CLI control surface
|
||||
- Contains: parser, execution preview, live transport, tests in one file
|
||||
- Key files: `crates/localllm-cli/src/main.rs`
|
||||
|
||||
**`tauri-app/src/`:**
|
||||
- Purpose: Desktop frontend
|
||||
- Contains: app shell, components, feature hooks, local persistence, CSS
|
||||
- Key files: `tauri-app/src/App.tsx`, `tauri-app/src/components/ChatView.tsx`, `tauri-app/src/features/runtime/useRuntimeManager.ts`
|
||||
|
||||
**`tauri-app/src-tauri/src/`:**
|
||||
- Purpose: Desktop backend bridge
|
||||
- Contains: runtime lifecycle, GPU/model file commands, Tauri entry points
|
||||
- Key files: `tauri-app/src-tauri/src/lib.rs`, `tauri-app/src-tauri/src/runtime/mod.rs`, `tauri-app/src-tauri/src/model_files.rs`
|
||||
|
||||
## Key File Locations
|
||||
|
||||
**Entry Points:**
|
||||
- `crates/inference-server/src/main.rs`: local HTTP API binary
|
||||
- `crates/localllm-cli/src/main.rs`: CLI binary
|
||||
- `tauri-app/src/main.tsx`: React bootstrap
|
||||
- `tauri-app/src-tauri/src/main.rs`: Tauri desktop bootstrap
|
||||
|
||||
**Configuration:**
|
||||
- `Cargo.toml`: Rust workspace members and shared dependencies
|
||||
- `tauri-app/package.json`: frontend scripts and npm dependencies
|
||||
- `tauri-app/vite.config.ts`: frontend dev/build config
|
||||
- `tauri-app/src-tauri/tauri.conf.json`: desktop packaging/window config
|
||||
|
||||
**Core Logic:**
|
||||
- `crates/inference-core/src/engine.rs`: GGUF load/warmup/context management
|
||||
- `crates/inference-server/src/routes.rs`: endpoint behavior and streaming
|
||||
- `tauri-app/src/components/ChatView.tsx`: chat request/stream/persistence loop
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`: runtime control plane UI
|
||||
|
||||
**Testing:**
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx`: runtime UI integration-style tests
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts`: form helper tests
|
||||
- `tauri-app/src/hooks/model-selection.test.ts`: model selection helper tests
|
||||
- Rust tests are co-located inside source files such as `crates/api-contracts/src/chat.rs` and `tauri-app/src-tauri/src/runtime/health.rs`
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
**Files:**
|
||||
- Rust uses snake_case modules like `crates/inference-server/src/routes.rs`
|
||||
- Frontend React components use PascalCase files like `tauri-app/src/components/Sidebar.tsx`
|
||||
- Frontend hooks/utilities use camelCase or kebab-lite names like `tauri-app/src/hooks/useModels.ts` and `tauri-app/src/features/runtime/runtime-settings-form.ts`
|
||||
|
||||
**Directories:**
|
||||
- Feature grouping is shallow and domain-oriented: `tauri-app/src/components/`, `tauri-app/src/features/runtime/`, `tauri-app/src/hooks/`, `tauri-app/src/lib/`
|
||||
|
||||
## Where to Add New Code
|
||||
|
||||
**New backend API feature:**
|
||||
- HTTP route/handler: `crates/inference-server/src/routes.rs`
|
||||
- Request validation/prompt compatibility: `crates/inference-server/src/compat.rs`
|
||||
- Shared payload type: `crates/api-contracts/src/`
|
||||
- Engine capability: `crates/inference-core/src/`
|
||||
|
||||
**New desktop runtime feature:**
|
||||
- Frontend runtime UI or hook: `tauri-app/src/features/runtime/`
|
||||
- New Tauri command/runtime backend support: `tauri-app/src-tauri/src/` or `tauri-app/src-tauri/src/runtime/`
|
||||
- Shared runtime type mirrored to frontend: `tauri-app/src/features/runtime/types.ts`
|
||||
|
||||
**New chat/UI feature:**
|
||||
- Chat behavior: `tauri-app/src/components/ChatView.tsx`
|
||||
- Message rendering: `tauri-app/src/components/MessageBubble.tsx`
|
||||
- Shared browser persistence: `tauri-app/src/lib/db.ts` or `tauri-app/src/lib/chat.ts`
|
||||
- Styling: colocated CSS file in `tauri-app/src/components/` or `tauri-app/src/features/runtime/components/`
|
||||
|
||||
**Utilities:**
|
||||
- Frontend shared helpers: `tauri-app/src/lib/` or feature-local helper file under `tauri-app/src/features/runtime/`
|
||||
- Rust cross-cutting helpers: keep crate-local unless multiple crates need them; if shared at API boundary, prefer `crates/api-contracts/`
|
||||
|
||||
## Frontend / Design System Structure
|
||||
|
||||
**Styling model:**
|
||||
- Use CSS custom properties from `tauri-app/src/index.css` as the de facto design token layer
|
||||
- Keep component styling in per-component CSS files such as `tauri-app/src/components/Sidebar.css` and `tauri-app/src/components/ChatView.css`
|
||||
- Reuse the same status colors, radius tokens, and surface variables instead of introducing ad hoc values
|
||||
|
||||
**Component organization:**
|
||||
- Layout shell: `tauri-app/src/App.tsx`
|
||||
- Global navigation and runtime entry: `tauri-app/src/components/Sidebar.tsx`
|
||||
- Core chat surface: `tauri-app/src/components/ChatView.tsx`
|
||||
- Atomic-ish content node: `tauri-app/src/components/MessageBubble.tsx`
|
||||
- Runtime feature subdomain: `tauri-app/src/features/runtime/components/`
|
||||
|
||||
**State boundaries:**
|
||||
- Runtime state lives in `tauri-app/src/features/runtime/useRuntimeManager.ts`
|
||||
- Models state lives in `tauri-app/src/hooks/useModels.ts`
|
||||
- Local DB readiness lives in `tauri-app/src/hooks/useDB.ts`
|
||||
- Chat screen owns message and compose state locally in `tauri-app/src/components/ChatView.tsx`
|
||||
|
||||
## Special Directories
|
||||
|
||||
**`target/`:**
|
||||
- Purpose: Cargo build artifacts and copied runtime DLLs
|
||||
- Generated: Yes
|
||||
- Committed: No
|
||||
|
||||
**`models/`:**
|
||||
- Purpose: local GGUF assets and notes
|
||||
- Generated: No
|
||||
- Committed: Partially; README is committed, model binaries may be local-only depending on workflow
|
||||
|
||||
**`experiments/`:**
|
||||
- Purpose: benchmarks, profiling, and exploratory performance work
|
||||
- Generated: Mixed
|
||||
- Committed: Yes
|
||||
|
||||
**`ui/`:**
|
||||
- Purpose: standalone browser chat prototype separate from the Tauri frontend
|
||||
- Generated: No
|
||||
- Committed: Yes
|
||||
|
||||
---
|
||||
|
||||
*Structure analysis: 2026-03-28*
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"model_profile": "inherit",
|
||||
"commit_docs": true,
|
||||
"parallelization": false,
|
||||
"search_gitignored": false,
|
||||
"brave_search": false,
|
||||
"firecrawl": false,
|
||||
"exa_search": false,
|
||||
"git": {
|
||||
"branching_strategy": "none",
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}",
|
||||
"quick_branch_template": null
|
||||
},
|
||||
"workflow": {
|
||||
"research": true,
|
||||
"plan_check": true,
|
||||
"verifier": true,
|
||||
"nyquist_validation": false,
|
||||
"auto_advance": true,
|
||||
"node_repair": true,
|
||||
"node_repair_budget": 2,
|
||||
"ui_phase": true,
|
||||
"ui_safety_gate": true,
|
||||
"text_mode": false,
|
||||
"research_before_questions": false,
|
||||
"discuss_mode": "discuss",
|
||||
"skip_discuss": false,
|
||||
"_auto_chain_active": false
|
||||
},
|
||||
"hooks": {
|
||||
"context_warnings": true
|
||||
},
|
||||
"agent_skills": {},
|
||||
"resolve_model_ids": "omit",
|
||||
"mode": "yolo",
|
||||
"granularity": "coarse"
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
status: awaiting_human_verify
|
||||
trigger: "/gsd-debug crie um sistema de debug que permite copiar mensagens de todo um chat, incluindo detalhes de mensagem tanto do usuario quanto do modelo, esse sistema so deve aparecer quando o tauri for rodado em dev, com o comando pnpm tauri dev, deve ser um botao flutuante que aparecerá em todo chat para fazermos debug. Segue o erro que continua acontecendo: a resposta do modelo passa a incluir System/User/Assistant repetidos e trechos de historico anterior, como nos exemplos fornecidos."
|
||||
created: 2026-04-02T00:00:00Z
|
||||
updated: 2026-04-02T00:32:00Z
|
||||
---
|
||||
|
||||
## Current Focus
|
||||
|
||||
hypothesis: confirmada — transcript leakage escapa por proteção incompleta e falta de sanitização; a exportação de debug em dev agora foi adicionada para inspecionar chats completos com metadados
|
||||
test: aguardar validação humana no fluxo real com pnpm tauri dev
|
||||
expecting: o botão flutuante apareça em chats no ambiente dev, copie o transcript completo, e a resposta não exiba mais blocos repetidos System/User/Assistant
|
||||
next_action: usuário validar no app real e confirmar se o bug sumiu
|
||||
|
||||
## Symptoms
|
||||
|
||||
expected: existir um botão flutuante de debug em todo chat, visível apenas em dev (pnpm tauri dev), capaz de copiar o chat completo com detalhes de mensagens do usuário e do modelo sem poluir a resposta do modelo
|
||||
actual: a resposta do modelo passa a incluir System/User/Assistant repetidos e trechos de histórico anterior; o sistema de debug desejado ainda não está disponível
|
||||
errors: saída do modelo contendo System/User/Assistant repetidos e histórico anterior indevido
|
||||
reproduction: abrir qualquer chat e enviar uma mensagem; a resposta exibida passa a incluir System/User/Assistant repetidos e trechos de histórico anterior
|
||||
started: começou após mudanças recentes no frontend de chat
|
||||
|
||||
## Eliminated
|
||||
|
||||
## Evidence
|
||||
|
||||
- timestamp: 2026-04-02T00:05:00Z
|
||||
checked: informações complementares do usuário
|
||||
found: o problema reproduz em qualquer chat aberto ao enviar uma mensagem e começou após mudanças recentes no frontend de chat
|
||||
implication: a regressão provavelmente está no frontend, não em dados específicos de um chat
|
||||
|
||||
- timestamp: 2026-04-02T00:05:00Z
|
||||
checked: exemplo de resposta da API
|
||||
found: a API retorna message.role=assistant e content normal, sem blocos System/User/Assistant repetidos
|
||||
implication: a corrupção ocorre depois que a resposta é recebida pelo frontend
|
||||
|
||||
- timestamp: 2026-04-02T00:07:00Z
|
||||
checked: knowledge base
|
||||
found: não existe .planning/debug/knowledge-base.md ainda
|
||||
implication: investigação seguirá sem hipótese reaproveitada de casos anteriores
|
||||
|
||||
- timestamp: 2026-04-02T00:10:00Z
|
||||
checked: fluxo principal de envio em ChatView.tsx e parser em lib/chat.ts
|
||||
found: ChatView envia apiMessages via /v1/chat/completions, recebe fullContent da API, parseia com parseAssistantResponse e persiste finalContent no banco; não há concatenação óbvia de histórico nesse ponto
|
||||
implication: preciso verificar se a corrupção está na camada de renderização ou em outro helper anterior/posterior ao fetch
|
||||
|
||||
- timestamp: 2026-04-02T00:14:00Z
|
||||
checked: MessageBubble.tsx, lib/markdown.ts e useMemoryExtractor.ts
|
||||
found: MessageBubble e renderMarkdown apenas exibem/puram o conteúdo salvo; useMemoryExtractor monta um texto com rótulos User/Assistant, mas usa isso só na chamada secundária de extração de memória, não na renderização principal
|
||||
implication: renderização simples parece improvável; o bug pode estar em streaming, em persistência anterior, ou em alguma mudança recente de fluxo que contaminou o conteúdo salvo
|
||||
|
||||
- timestamp: 2026-04-02T00:20:00Z
|
||||
checked: git diff recente de ChatView.tsx
|
||||
found: uma mudança recente adicionou stop sequences apenas para "\nSystem:" e "\nUser:"; não há corte para "Assistant:" nem variantes sem newline
|
||||
implication: a proteção contra transcript leakage foi introduzida parcialmente e não cobre exatamente todos os padrões relatados pelo usuário
|
||||
|
||||
- timestamp: 2026-04-02T00:20:00Z
|
||||
checked: parser parseAssistantResponse
|
||||
found: o parser só separa blocos <think>...</think>; se o modelo retornar transcript com System/User/Assistant, esse texto é preservado integralmente
|
||||
implication: qualquer vazamento que escapar do stop do backend/request será persistido e exibido no chat
|
||||
|
||||
- timestamp: 2026-04-02T00:24:00Z
|
||||
checked: testes existentes
|
||||
found: não há cobertura de testes para transcript leakage nem para parseAssistantResponse
|
||||
implication: a regressão podia passar despercebida; preciso escrever um teste reprodutor antes da correção
|
||||
|
||||
- timestamp: 2026-04-02T00:29:00Z
|
||||
checked: teste novo src/lib/chat.test.ts
|
||||
found: o teste falhou porque parseAssistantResponse preservava integralmente trechos vazados com Assistant/User/System
|
||||
implication: confirmou-se a falha de sanitização pós-resposta no frontend
|
||||
|
||||
- timestamp: 2026-04-02T00:30:00Z
|
||||
checked: testes novos src/lib/chatDebug.test.ts e src/components/ChatView.debug.test.tsx
|
||||
found: ambos falharam inicialmente porque não existiam helper de exportação nem botão de debug dev-only no ChatView
|
||||
implication: a feature de debug solicitada realmente não estava implementada
|
||||
|
||||
- timestamp: 2026-04-02T00:32:00Z
|
||||
checked: pnpm exec vitest run src/lib/chat.test.ts src/lib/chatDebug.test.ts src/components/ChatView.debug.test.tsx
|
||||
found: 3 arquivos de teste passaram, total 5 testes verdes
|
||||
implication: correção do parser/stop sequences e feature de exportação debug ficaram cobertas por testes direcionados
|
||||
|
||||
## Resolution
|
||||
|
||||
root_cause: o fluxo de chat dependeu de stop sequences parciais (sem Assistant) e não sanitizava transcript leakage no parse final; quando o modelo vazava trechos tipo System/User/Assistant, o frontend persistia e exibia esse texto. Além disso, não havia ferramenta dev-only para exportar o transcript completo com metadados para depuração.
|
||||
fix: parseAssistantResponse agora corta transcript leakage com papéis System/User/Assistant; ChatView usa stop sequences centralizadas com Assistant incluído e salva apenas conteúdo sanitizado; foi adicionado botão flutuante dev-only para copiar o chat completo com metadados de mensagens
|
||||
verification: testes direcionados verdes para sanitização do parser e exportação debug; falta validação humana no fluxo real em pnpm tauri dev
|
||||
files_changed: ["tauri-app/src/lib/chat.ts", "tauri-app/src/components/ChatView.tsx", "tauri-app/src/components/ChatView.css", "tauri-app/src/lib/chat.test.ts", "tauri-app/src/lib/chatDebug.ts", "tauri-app/src/lib/chatDebug.test.ts", "tauri-app/src/components/ChatView.debug.test.tsx"]
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
trigger: "/gsd:debug continuar investigacao do erro de segunda mensagem no chat, retomando alteracoes pendentes em n_ctx/profile/batch size e validando causa raiz antes de editar"
|
||||
created: 2026-03-30T00:00:00Z
|
||||
updated: 2026-03-30T00:42:00Z
|
||||
---
|
||||
|
||||
## Current Focus
|
||||
|
||||
hypothesis: A causa raiz ja foi identificada e corrigida no estado atual do codigo; o que resta e validar em ambiente real que o runtime local nao volta a falhar no segundo turno.
|
||||
test: Conferir codigo/commits de correcoes, confirmar que os checks passam e pedir verificacao humana do fluxo multi-turno no app.
|
||||
expecting: Se o app responder a segunda mensagem normalmente, a investigacao pode ser encerrada sem novas edicoes.
|
||||
next_action: comunicar diagnostico confirmado e orientar verificacao manual no ForgeLM
|
||||
|
||||
## Symptoms
|
||||
|
||||
expected: O chat deve aceitar e responder multiplas mensagens consecutivas sem falhar na segunda interacao.
|
||||
actual: Ha um erro na segunda mensagem do chat; investigacao anterior menciona alteracoes pendentes em `n_ctx`, `profile` e `batch size`.
|
||||
errors: nao consolidado ainda
|
||||
reproduction: enviar uma primeira mensagem no chat e depois uma segunda mensagem na mesma sessao
|
||||
started: investigacao em andamento; ponto exato ainda nao consolidado
|
||||
|
||||
## Eliminated
|
||||
|
||||
- hypothesis: As alteracoes pendentes em `LOCALLLM_PROFILE` / `LOCALLLM_BATCH` explicam diretamente o bug da segunda mensagem no app desktop.
|
||||
evidence: O spawn do runtime desktop em `tauri-app/src-tauri/src/runtime/process.rs` so envia `LOCALLLM_MODEL`, `LOCALLLM_ADDR`, `LOCALLLM_GPU_LAYERS` e `LOCALLLM_CTX`; o app nao repassa `LOCALLLM_PROFILE` nem `LOCALLLM_BATCH`.
|
||||
timestamp: 2026-03-30T00:30:00Z
|
||||
|
||||
## Evidence
|
||||
|
||||
- timestamp: 2026-03-30T00:15:00Z
|
||||
checked: estado do repositorio e base de conhecimento de debug
|
||||
found: Alteracoes pendentes relevantes estao em `crates/inference-core/src/profile.rs` e `crates/inference-server/src/main.rs`; arquivos alterados no frontend visivel sao cosmeticos. Nenhum `.planning/debug/knowledge-base.md` existe.
|
||||
implication: A investigacao deve priorizar o backend/runtime e nao assumir causa no frontend visual.
|
||||
|
||||
- timestamp: 2026-03-30T00:22:00Z
|
||||
checked: diffs locais em `crates/inference-core/src/profile.rs` e `crates/inference-server/src/main.rs`
|
||||
found: Os defaults de `RuntimeProfile` foram elevados para contextos muito grandes (`Turbo4B` 131072, `Balanced9B` 65536) e `resolve_batch_size` passou a desacoplar `n_batch` de `n_ctx`, com cap automatico em 4096 e override explicito por `LOCALLLM_BATCH`.
|
||||
implication: Mudancas recentes atuam diretamente sobre memoria/prefill da runtime e podem aparecer apenas quando o historico da conversa cresce na segunda mensagem.
|
||||
|
||||
- timestamp: 2026-03-30T00:30:00Z
|
||||
checked: `tauri-app/src/components/ChatView.tsx`, `tauri-app/src-tauri/src/runtime/process.rs` e `tauri-app/src-tauri/src/runtime/types.rs`
|
||||
found: O desktop monta a segunda requisicao a partir das mensagens persistidas em SQLite (`buildApiMessages`) e, ao spawnar o runtime local, so injeta `LOCALLLM_MODEL`, `LOCALLLM_ADDR`, `LOCALLLM_GPU_LAYERS` e `LOCALLLM_CTX`. Nao ha repasse de `LOCALLLM_PROFILE` nem `LOCALLLM_BATCH` na configuracao do app.
|
||||
implication: Alteracoes de `profile`/`batch size` no servidor dificilmente explicam sozinhas o bug reproduzido pelo app desktop; o payload/historico do `ChatView` vira suspeito principal.
|
||||
|
||||
- timestamp: 2026-03-30T00:42:00Z
|
||||
checked: `.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-SUMMARY.md`, `crates/inference-core/src/engine.rs`, `crates/inference-server/src/routes.rs`, `tauri-app/src/components/ChatView.tsx`, `git log`, `cargo check -p inference-core -p inference-server`, `npx tsc --noEmit`
|
||||
found: O resumo da quick task e o codigo atual convergem para a mesma cadeia causal: `llama_get_memory` pode retornar NULL na segunda sessao; o codigo antigo seguia em frente e deixava o KV cache anterior contaminando o prefill. Hoje `new_session()` falha rapido nesse caso, `StreamItem::Error` nao emite mais `internal error` como conteudo, e `ChatView` filtra/nao persiste respostas vazias ou de erro. Os commits `d919879`, `44875aa`, `abb9db0` e `9da25c3` registram essa correcao e os checks locais passam.
|
||||
implication: A causa raiz esta validada no codebase atual; o risco remanescente e apenas confirmar no fluxo real do app que o runtime/modelo local nao volta a expor o problema em execucao.
|
||||
|
||||
## Resolution
|
||||
|
||||
root_cause: Na segunda requisicao, alguns builds de llama.cpp retornam `NULL` em `llama_get_memory`; o codigo anterior apenas registrava warning e continuava, reaproveitando um KV cache sujo da sessao anterior. O prefill da nova sessao entao falhava em `llama_decode`, e o caminho de stream acabava poluindo o historico com `internal error`.
|
||||
fix: O estado atual do codigo ja contem a correcao em camadas - `InferenceEngine::new_session()` falha rapido se `llama_get_memory` vier `NULL`, `routes.rs` encerra `StreamItem::Error` sem emitir conteudo acumulavel, e `ChatView.tsx` filtra/pula persistencia de mensagens vazias ou de erro.
|
||||
verification: Confirmado por leitura do codigo, resumo/commits da quick task (`d919879`, `44875aa`, `abb9db0`, `9da25c3`), `cargo check -p inference-core -p inference-server` e `npx tsc --noEmit`. Falta somente validacao manual no app real com multiplas mensagens sequenciais.
|
||||
files_changed: ["crates/inference-core/src/engine.rs", "crates/inference-core/src/session.rs", "crates/inference-server/src/routes.rs", "tauri-app/src/components/ChatView.tsx"]
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
phase: 01-draft-state-ownership
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- tauri-app/src/features/runtime/types.ts
|
||||
- tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
autonomous: true
|
||||
requirements: [WORK-02, CONF-01]
|
||||
must_haves:
|
||||
truths:
|
||||
- User can keep editing a runtime draft while background polling continues without draft fields being overwritten.
|
||||
- User can reopen the settings drawer in the same session and find the same unsaved draft.
|
||||
- User can save a draft and have the saved values become the new clean baseline immediately.
|
||||
artifacts:
|
||||
- path: tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
provides: separate ownership for live snapshot, saved baseline, and editable draft actions
|
||||
- path: tauri-app/src/features/runtime/types.ts
|
||||
provides: runtime draft contracts shared by the manager and drawer
|
||||
- path: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
provides: regression coverage for polling-safe draft ownership
|
||||
key_links:
|
||||
- from: tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
to: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
via: manager props for live values, draft values, dirty state, and draft actions
|
||||
pattern: draft|baseline|liveSnapshot
|
||||
- from: tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
to: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
via: polling and save flow regression tests
|
||||
pattern: bootstrapRuntime|getRuntimeSnapshot|saveRuntimeConfig
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the draft ownership foundation so the runtime feature has distinct live, baseline, and editable draft state before the drawer UI is refactored.
|
||||
|
||||
Purpose: eliminate the current poll-driven overwrite path at the feature-state layer instead of patching individual fields ad hoc.
|
||||
Output: explicit runtime draft contracts and a manager API that preserves unsaved state across refresh, close, reopen, and save.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
@.planning/phases/01-hf-model-discovery/01-RESEARCH.md
|
||||
@.planning/phases/01-hf-model-discovery/01-UI-SPEC.md
|
||||
@tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
@tauri-app/src/features/runtime/types.ts
|
||||
@tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
|
||||
<interfaces>
|
||||
From `tauri-app/src/features/runtime/types.ts`:
|
||||
|
||||
```ts
|
||||
export interface RuntimeConfig {
|
||||
connection_mode: ConnectionMode;
|
||||
start_policy: StartPolicy;
|
||||
server_url: string;
|
||||
api_key: string;
|
||||
model_path: string | null;
|
||||
models_dir: string | null;
|
||||
addr: string;
|
||||
gpu_layers: number;
|
||||
ctx_size: number;
|
||||
extra_model_paths: string[];
|
||||
selected_model_id: string | null;
|
||||
remote_presets: RemotePreset[];
|
||||
hugging_face_token: string;
|
||||
}
|
||||
|
||||
export interface RuntimeSnapshot {
|
||||
status: RuntimeStatus;
|
||||
ready: boolean;
|
||||
process_running: boolean;
|
||||
resolved_url: string;
|
||||
binary_path: string | null;
|
||||
last_error: RuntimeErrorInfo | null;
|
||||
config: RuntimeConfig;
|
||||
}
|
||||
```
|
||||
|
||||
From `tauri-app/src/features/runtime/useRuntimeManager.ts`:
|
||||
|
||||
```ts
|
||||
export function useRuntimeManager() {
|
||||
return {
|
||||
snapshot,
|
||||
connection,
|
||||
loading,
|
||||
busy,
|
||||
refresh,
|
||||
saveConfig,
|
||||
startLocal,
|
||||
stopLocal,
|
||||
};
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Define runtime draft ownership contracts</name>
|
||||
<files>tauri-app/src/features/runtime/types.ts</files>
|
||||
<read_first>
|
||||
- tauri-app/src/features/runtime/types.ts
|
||||
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
- .planning/phases/01-hf-model-discovery/01-UI-SPEC.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test 1: the runtime feature can represent live values separately from editable draft values and the saved baseline.
|
||||
- Test 2: the draft contract exposes enough state for the drawer to show dirty, hidden, invalid, and clean states without reading raw snapshot internals.
|
||||
</behavior>
|
||||
<action>Extend `tauri-app/src/features/runtime/types.ts` with explicit draft-facing contracts for Phase 1 per D-01 through D-06 and D-09 through D-11. Add concrete exported types for manager-owned draft state, including: a live snapshot reference, a saved baseline `RuntimeConfig`, an editable draft `RuntimeConfig`, a boolean dirty flag, a boolean open/hidden state, and field-level invalid/pending metadata keyed at least for `models_dir`, `model_path`, `selected_model_id`, `server_url`, and `api_key`. Keep existing `RuntimeSnapshot` and `RuntimeConfig` intact so brownfield callers still compile; add new exports rather than replacing old ones.</action>
|
||||
<acceptance_criteria>
|
||||
- `tauri-app/src/features/runtime/types.ts` exports draft-specific interfaces or types in addition to `RuntimeSnapshot` and `RuntimeConfig`.
|
||||
- The draft contract includes explicit baseline and draft config properties rather than relying on one mutable `snapshot.config`.
|
||||
- The draft contract includes field-level invalid/pending metadata needed for inline save blocking.
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>npm test -- runtime-settings-form</automated>
|
||||
</verify>
|
||||
<done>Runtime draft state is a first-class typed contract that downstream code can consume without inferring ownership rules from comments or snapshot mutations.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Move draft, baseline, and polling reconciliation into the runtime manager</name>
|
||||
<files>tauri-app/src/features/runtime/useRuntimeManager.ts, tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx</files>
|
||||
<read_first>
|
||||
- tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
- tauri-app/src/features/runtime/types.ts
|
||||
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test 1: polling updates operational live state without overwriting dirty draft values (D-02, D-09).
|
||||
- Test 2: closing and reopening the drawer in the same session restores the unsaved draft (D-06).
|
||||
- Test 3: successful save promotes the returned config into the new clean baseline while keeping the draft open (D-03).
|
||||
</behavior>
|
||||
<action>Refactor `useRuntimeManager.ts` so it owns three distinct sources of truth: `liveSnapshot` from polling/lifecycle commands, `savedBaseline` from the last committed `RuntimeConfig`, and `draftConfig` for user edits. Add explicit actions named for executor clarity: create/open draft from `savedBaseline`, close draft without discard, replace the full draft with `savedBaseline`, update a targeted draft field, and save the current draft. Polling via `getRuntimeSnapshot()` must still run every 3000 ms, but when `draftDirty` is true it may only update operational live fields (`status`, `ready`, `process_running`, `resolved_url`, `binary_path`, `last_error`, and managed-library metadata if present), not overwrite `draftConfig`. After `saveRuntimeConfig()` resolves, set both `liveSnapshot.config` and `savedBaseline` to the returned config, clear dirty state, and keep the draft available/open per D-03. Add or update `RuntimeSurface.test.tsx` coverage so these manager behaviors are enforced through the feature harness.</action>
|
||||
<acceptance_criteria>
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts` exposes separate live and draft-facing state instead of only `snapshot`.
|
||||
- The polling path no longer writes the full fetched snapshot into the editable draft when the draft is dirty.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` contains regression tests for poll-safe drafts, close/reopen preservation, and save promotion.
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>npm test -- RuntimeSurface</automated>
|
||||
</verify>
|
||||
<done>The runtime manager becomes the source of truth for Phase 1 draft ownership, with automated coverage proving polling and save flows preserve draft safety.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npm test -- RuntimeSurface`
|
||||
- `npm test -- runtime-settings-form`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Live runtime refresh continues while dirty draft fields remain unchanged.
|
||||
- Saving a draft promotes the saved config into the new baseline without closing the drawer.
|
||||
- Closing and reopening the drawer in the same session restores the unsaved draft.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-hf-model-discovery/01-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
phase: 01-draft-state-ownership
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [react, typescript, tauri, vitest, runtime-settings]
|
||||
requires: []
|
||||
provides:
|
||||
- runtime draft ownership contracts for live, baseline, and editable runtime state
|
||||
- manager-owned polling reconciliation that preserves dirty drafts
|
||||
- regression coverage for poll-safe draft edits, close or reopen preservation, and save promotion
|
||||
affects: [phase-1-drawer-refactor, runtime-settings-drawer, lifecycle-semantics]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [manager-owned live-baseline-draft state, operational-only polling reconciliation while dirty, hook-level draft regression harness]
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- tauri-app/src/features/runtime/types.ts
|
||||
- tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
- tauri-app/src/features/runtime/runtime-settings-form.test.ts
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
key-decisions:
|
||||
- "Keep existing snapshot/saveConfig outputs while adding explicit draft APIs so brownfield consumers keep compiling during the Phase 1 transition."
|
||||
- "When the draft is dirty, polling updates only operational live fields and leaves baseline plus draft config untouched."
|
||||
patterns-established:
|
||||
- "Runtime manager owns liveSnapshot, savedBaseline, and draftConfig separately instead of deriving editable state from polling output."
|
||||
- "Draft behavior is verified with hook-level regression tests that exercise open, close, update, poll, and save flows independently from the drawer UI."
|
||||
requirements-completed: [WORK-02, CONF-01]
|
||||
duration: 10min
|
||||
completed: 2026-03-29
|
||||
---
|
||||
|
||||
# Phase 1 Plan 1: Draft State Ownership Summary
|
||||
|
||||
**Manager-owned runtime draft contracts with polling-safe reconciliation and save-promotion regressions for the Tauri runtime workspace**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 10 min
|
||||
- **Started:** 2026-03-29T02:43:00Z
|
||||
- **Completed:** 2026-03-29T02:52:44Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 4
|
||||
|
||||
## Accomplishments
|
||||
- Added explicit draft-state contracts for live snapshot, saved baseline, editable draft config, and field metadata in `tauri-app/src/features/runtime/types.ts`
|
||||
- Refactored `useRuntimeManager` to own `liveSnapshot`, `savedBaseline`, and `draftConfig` separately while preserving legacy `snapshot` and `saveConfig` compatibility
|
||||
- Added regression coverage proving dirty drafts survive polling, hidden drafts survive close or reopen in-session, and save promotes the returned config into the new clean baseline
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Define runtime draft ownership contracts** - `929a516` (feat)
|
||||
2. **Task 2: Move draft, baseline, and polling reconciliation into the runtime manager** - `18501e9` (feat)
|
||||
|
||||
**Plan metadata:** pending
|
||||
|
||||
## Files Created/Modified
|
||||
- `tauri-app/src/features/runtime/types.ts` - Defines reusable draft-state contracts and default field metadata for manager-owned runtime editing
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts` - Verifies the new draft contract surface with runtime-backed and type-level assertions
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts` - Separates live polling state from saved baseline and editable draft state while keeping legacy consumers working
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` - Covers dirty polling reconciliation, close or reopen draft persistence, and save promotion behavior through the runtime manager harness
|
||||
|
||||
## Decisions Made
|
||||
- Kept `snapshot` and `saveConfig` on the manager return value so the current drawer keeps working while Phase 1 transitions draft ownership into the manager
|
||||
- Treated dirty polling as an operational refresh only, updating status and process health without replacing draft or baseline config values
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
- The first RED attempt used type-only assertions, which Vitest transpiled without failing on missing exports; the contract test was adjusted to use runtime-backed draft helpers so the TDD red phase became real before implementation
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Phase 1 now has a typed draft-state foundation and manager behaviors the drawer refactor can consume directly
|
||||
- The next plan can move the drawer UI onto the manager-owned draft API without re-solving polling or save-baseline ownership rules
|
||||
|
||||
## Self-Check
|
||||
|
||||
PASSED
|
||||
|
||||
- FOUND: `.planning/phases/01-hf-model-discovery/01-01-SUMMARY.md`
|
||||
- FOUND: `929a516`
|
||||
- FOUND: `18501e9`
|
||||
|
||||
---
|
||||
*Phase: 01-draft-state-ownership*
|
||||
*Completed: 2026-03-29*
|
||||
@@ -1,154 +0,0 @@
|
||||
---
|
||||
phase: 01-draft-state-ownership
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [01-01]
|
||||
files_modified:
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
autonomous: true
|
||||
requirements: [WORK-02, CONF-02]
|
||||
must_haves:
|
||||
truths:
|
||||
- User can tell which values are draft edits versus live or saved values while editing.
|
||||
- User can explicitly discard changes back to the last saved baseline.
|
||||
- Closing the drawer hides the draft without discarding it, and reopening restores it.
|
||||
artifacts:
|
||||
- path: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
provides: controlled draft editor with explicit live-vs-draft presentation and discard flow
|
||||
- path: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
|
||||
provides: draft-state visual treatment matching the approved UI contract
|
||||
- path: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
provides: coverage for visible draft and discard semantics
|
||||
key_links:
|
||||
- from: tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
to: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
via: controlled props for draft values and draft actions
|
||||
pattern: draft|discard|openDraft|closeDraft
|
||||
- from: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
to: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
|
||||
via: classes for draft badges, helper rows, sticky header, and discard confirmation
|
||||
pattern: draft|live-now|discard
|
||||
---
|
||||
|
||||
<objective>
|
||||
Refactor the settings drawer into a controlled draft editor that visibly distinguishes draft values from live values and exposes explicit discard behavior.
|
||||
|
||||
Purpose: satisfy the user-facing trust requirements once manager-owned draft state exists.
|
||||
Output: a drawer that renders `Draft settings`, `Live now`, `Save changes`, `Discard changes`, and in-session draft preservation per the approved UI contract.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
@.planning/phases/01-hf-model-discovery/01-RESEARCH.md
|
||||
@.planning/phases/01-hf-model-discovery/01-UI-SPEC.md
|
||||
@.planning/phases/01-hf-model-discovery/01-01-SUMMARY.md
|
||||
@tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
@tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
|
||||
@tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
|
||||
<interfaces>
|
||||
From `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`:
|
||||
|
||||
```tsx
|
||||
interface Props {
|
||||
open: boolean;
|
||||
snapshot: RuntimeSnapshot;
|
||||
busy: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (patch: Partial<RuntimeConfig>) => Promise<RuntimeSnapshot>;
|
||||
onStart: () => Promise<unknown>;
|
||||
onStop: () => Promise<unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
From `01-UI-SPEC.md`:
|
||||
|
||||
```text
|
||||
- Sticky header title: Draft settings
|
||||
- Visible live reference label: Live now
|
||||
- Footer actions: Save changes, Discard changes
|
||||
- Confirmation row: Keep editing, Revert to saved
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Convert the drawer to a controlled draft/live presentation</name>
|
||||
<files>tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx, tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css</files>
|
||||
<read_first>
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
|
||||
- .planning/phases/01-hf-model-discovery/01-UI-SPEC.md
|
||||
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
- .planning/phases/01-hf-model-discovery/01-01-SUMMARY.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test 1: changed fields show visible draft treatment plus a live/saved reference row, not color-only state (WORK-02).
|
||||
- Test 2: the drawer header and footer use `Draft settings`, `Live now`, `Save changes`, and `Discard changes` copy from the UI contract.
|
||||
- Test 3: close hides the draft without resetting it; discard is the explicit full-form reset path (D-04, D-05, D-06).
|
||||
</behavior>
|
||||
<action>Refactor `RuntimeSettingsDrawer.tsx` to consume manager-owned draft props from Plan 01 instead of repopulating local form state from `snapshot` in a blanket effect. Replace the current header copy (`Runtime workspace`, `One control plane for startup, models, and API routing`) with the Phase 1 contract: sticky `Draft settings` title, a compact dirty-state summary, and `Live now` reference content. For every dirty editable field, render the draft control with a draft accent treatment plus a helper row showing the current saved/live value beneath it. Replace footer labels with `Save changes` and `Discard changes`. Implement the inline discard confirmation row exactly with `Keep editing` and destructive `Revert to saved`; `Discard changes` opens the confirmation row and only `Revert to saved` actually resets the full draft to baseline. Backdrop click and close button must call the manager's hide-draft action, not discard.</action>
|
||||
<acceptance_criteria>
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` no longer has a `useEffect` that fully rehydrates all form fields from `snapshot.config` on every snapshot change.
|
||||
- The file contains the strings `Draft settings`, `Live now`, `Save changes`, `Discard changes`, `Keep editing`, and `Revert to saved`.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css` contains styles for visible draft-state and live-reference treatment rather than only neutral field styling.
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>npm test -- RuntimeSurface</automated>
|
||||
</verify>
|
||||
<done>The drawer becomes a controlled, explicit draft editor that makes unsaved edits and saved/live references obvious and never discards draft state on close.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Add drawer-level tests for draft visibility and explicit discard semantics</name>
|
||||
<files>tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx</files>
|
||||
<read_first>
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- .planning/phases/01-hf-model-discovery/01-UI-SPEC.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test 1: editing a field marks it as draft and shows the saved/live reference below the control.
|
||||
- Test 2: clicking close or backdrop preserves the unsaved draft for reopen in the same session.
|
||||
- Test 3: `Discard changes` does not reset immediately; `Revert to saved` performs the reset while `Keep editing` cancels it.
|
||||
</behavior>
|
||||
<action>Add or update `RuntimeSurface.test.tsx` so it exercises the controlled drawer through the runtime manager harness. Assert concrete UI strings and behaviors: a dirty field shows the `Draft` signal and the `Live now` or saved reference row; close/backdrop preserves the edited value on reopen; `Discard changes` reveals the confirmation row; `Keep editing` leaves the draft untouched; `Revert to saved` restores the last saved baseline for the full form. Keep the tests in the existing jsdom harness instead of introducing a new test framework.</action>
|
||||
<acceptance_criteria>
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` contains assertions for `Draft settings`, `Discard changes`, `Keep editing`, and `Revert to saved`.
|
||||
- The tests verify close/reopen preservation and full-form discard reset behavior.
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>npm test -- RuntimeSurface</automated>
|
||||
</verify>
|
||||
<done>Automated tests prove the drawer exposes clear draft state and that discard behavior is explicit, reversible at the confirmation step, and never triggered by simple close.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npm test -- RuntimeSurface`
|
||||
- `npm run build`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- The drawer clearly distinguishes draft versus live/saved values while editing.
|
||||
- Users must explicitly confirm a full-form discard; close merely hides the draft.
|
||||
- The approved Phase 1 copy and layout contract is reflected in the drawer implementation.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-hf-model-discovery/01-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
phase: 01-draft-state-ownership
|
||||
plan: 02
|
||||
subsystem: ui
|
||||
tags: [react, typescript, runtime-drawer, css, vitest]
|
||||
requires:
|
||||
- phase: 01-01
|
||||
provides: manager-owned draft, baseline, and live runtime state
|
||||
provides:
|
||||
- controlled runtime settings drawer wired to manager-owned draft state
|
||||
- explicit draft-vs-live presentation with sticky Draft settings header and Live now references
|
||||
- inline discard confirmation flow that preserves draft state on close and reverts only on explicit confirmation
|
||||
affects: [phase-1-async-safety, runtime-workspace, lifecycle-semantics]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [controlled drawer over manager draft state, explicit discard confirmation row, dirty-field live reference rows]
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
- tauri-app/src/components/Sidebar.tsx
|
||||
key-decisions:
|
||||
- "Wire drawer visibility through manager open or close actions so backdrop and close hide the in-session draft instead of discarding it."
|
||||
- "Use inline draft badges plus Live now helper rows so changed values are distinguishable without relying on color alone."
|
||||
patterns-established:
|
||||
- "Runtime settings UI reads draftConfig and savedBaseline directly rather than mirroring snapshot.config into local form state."
|
||||
- "Discard changes opens a confirmation row; only Revert to saved performs the reset."
|
||||
requirements-completed: [WORK-02, CONF-02]
|
||||
duration: 18min
|
||||
completed: 2026-03-29
|
||||
---
|
||||
|
||||
# Phase 1 Plan 2: Draft Presentation Summary
|
||||
|
||||
**Controlled runtime drawer with explicit draft or live presentation, sticky draft copy, and confirmed discard semantics**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 18 min
|
||||
- **Started:** 2026-03-29T02:53:00Z
|
||||
- **Completed:** 2026-03-29T03:10:41Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 4
|
||||
|
||||
## Accomplishments
|
||||
- Refactored `RuntimeSettingsDrawer.tsx` into a controlled draft editor that reads manager-owned draft and baseline values
|
||||
- Added sticky `Draft settings` and `Live now` presentation, draft badges, and explicit `Save changes` and `Discard changes` copy
|
||||
- Added drawer-level regression coverage for visible draft state, close or reopen preservation, and explicit discard confirmation behavior
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Convert the drawer to a controlled draft/live presentation** - `80883c2` (feat)
|
||||
2. **Task 2: Add drawer-level tests for draft visibility and explicit discard semantics** - `80883c2` (feat)
|
||||
|
||||
**Plan metadata:** pending
|
||||
|
||||
## Files Created/Modified
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` - Controlled draft editor with sticky header, live reference rows, and discard confirmation flow
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.css` - Draft-state, live-reference, sticky header and footer, and discard confirmation styling
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` - Drawer-level regression tests for draft copy, close or reopen preservation, and discard confirmation
|
||||
- `tauri-app/src/components/Sidebar.tsx` - App-shell wiring so opening and closing the drawer goes through manager draft actions
|
||||
|
||||
## Decisions Made
|
||||
- Routed drawer open and close through the manager API so hidden drafts survive close and reopen within the same session
|
||||
- Kept the local runtime apply CTA separate from `Save changes` while still putting draft save and discard semantics in the primary footer flow
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Integration] Added sidebar wiring for controlled draft opening and closing**
|
||||
- **Found during:** Task 1 (controlled drawer refactor)
|
||||
- **Issue:** The plan focused on drawer files, but the app shell still opened the drawer through local UI state only
|
||||
- **Fix:** Updated `tauri-app/src/components/Sidebar.tsx` to call `openDraft`, `closeDraft`, `resetDraft`, and draft update handlers from `useRuntimeManager`
|
||||
- **Files modified:** `tauri-app/src/components/Sidebar.tsx`
|
||||
- **Verification:** `npm test -- RuntimeSurface`; `npm run build`
|
||||
- **Committed in:** `80883c2`
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (integration)
|
||||
**Impact on plan:** Necessary brownfield wiring only; no scope creep beyond enabling the planned controlled drawer behavior.
|
||||
|
||||
## Issues Encountered
|
||||
- None
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- The drawer now exposes explicit draft and live semantics, which gave Plan 03 a stable surface for async safety and invalid-draft handling
|
||||
- Controlled draft UI is in place for future lifecycle-semantics work in Phase 2
|
||||
|
||||
## Self-Check
|
||||
|
||||
PASSED
|
||||
|
||||
- Verified explicit draft and live copy in `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx`
|
||||
- Verified discard confirmation and close or reopen draft preservation with `npm test -- RuntimeSurface`
|
||||
- Verified production compile with `npm run build`
|
||||
|
||||
---
|
||||
*Phase: 01-draft-state-ownership*
|
||||
*Completed: 2026-03-29*
|
||||
@@ -1,157 +0,0 @@
|
||||
---
|
||||
phase: 01-draft-state-ownership
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [01-02]
|
||||
files_modified:
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- tauri-app/src/features/runtime/runtime-settings-form.ts
|
||||
- tauri-app/src/features/runtime/runtime-settings-form.test.ts
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
autonomous: true
|
||||
requirements: [CONF-01, CONF-03, CONF-04]
|
||||
must_haves:
|
||||
truths:
|
||||
- Folder, model, and provider/source choices stay stable until the user saves or discards them.
|
||||
- Older async responses do not overwrite newer field intent.
|
||||
- Invalid draft values remain visible with inline errors and block save instead of being silently removed.
|
||||
artifacts:
|
||||
- path: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
provides: field-scoped async handlers and inline invalid-state rendering
|
||||
- path: tauri-app/src/features/runtime/runtime-settings-form.ts
|
||||
provides: helper logic for preserving selections and determining invalid draft state
|
||||
- path: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
provides: regression coverage for stale async responses and invalid draft preservation
|
||||
- path: tauri-app/src/features/runtime/runtime-settings-form.test.ts
|
||||
provides: unit coverage for selection-preservation helpers and invalid-save blocking rules
|
||||
key_links:
|
||||
- from: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
to: tauri-app/src/features/runtime/runtime-settings-form.ts
|
||||
via: helper calls that preserve current selections and compute invalid draft state
|
||||
pattern: chooseModel|createRuntimeConfigPatch
|
||||
- from: tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
to: tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
via: async picker and rescan regression tests
|
||||
pattern: scanRuntimeModels|pickRuntimeModelsDir|searchHuggingFaceModels
|
||||
---
|
||||
|
||||
<objective>
|
||||
Harden the controlled draft flow so field-scoped async work, rescans, and validation failures preserve user intent instead of silently mutating the draft.
|
||||
|
||||
Purpose: finish the phase by protecting the highest-risk interactions: folder pickers, model rescans, stale async responses, and save failure recovery.
|
||||
Output: field-scoped async safety, invalid-draft preservation, save blocking, and regression tests for the full draft-safety contract.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
@.planning/phases/01-hf-model-discovery/01-RESEARCH.md
|
||||
@.planning/phases/01-hf-model-discovery/01-UI-SPEC.md
|
||||
@.planning/phases/01-hf-model-discovery/01-02-SUMMARY.md
|
||||
@tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
@tauri-app/src/features/runtime/runtime-settings-form.ts
|
||||
@tauri-app/src/features/runtime/runtime-settings-form.test.ts
|
||||
@tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
|
||||
<interfaces>
|
||||
From `tauri-app/src/features/runtime/runtime-settings-form.ts`:
|
||||
|
||||
```ts
|
||||
export function chooseModel(current: string, scanned: RuntimeModelFile[], extra: RuntimeModelFile[]): string;
|
||||
|
||||
export function createRuntimeConfigPatch(
|
||||
snapshotConfig: RuntimeConfig,
|
||||
form: RuntimeSettingsFormState,
|
||||
maxLayers: number,
|
||||
): RuntimeConfig;
|
||||
```
|
||||
|
||||
Locked behavior from `01-CONTEXT.md`:
|
||||
|
||||
```text
|
||||
- D-07: picker updates only the launched field
|
||||
- D-08: rescans preserve current draft selection unless invalid
|
||||
- D-10: stale responses for the same field are ignored
|
||||
- D-11: invalid drafted values remain visible, inline, and block save
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Make async picker and rescan handlers field-scoped and stale-safe</name>
|
||||
<files>tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx, tauri-app/src/features/runtime/runtime-settings-form.ts, tauri-app/src/features/runtime/runtime-settings-form.test.ts</files>
|
||||
<read_first>
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- tauri-app/src/features/runtime/runtime-settings-form.ts
|
||||
- tauri-app/src/features/runtime/runtime-settings-form.test.ts
|
||||
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test 1: folder/file pickers update only their target draft fields and do not rehydrate unrelated inputs (D-07).
|
||||
- Test 2: model rescans preserve the current draft selection when it is still valid and only mark it invalid when it disappears (D-08, D-11).
|
||||
- Test 3: only the latest request for a field can update draft-facing UI; older responses are ignored (D-10).
|
||||
</behavior>
|
||||
<action>Refactor async handlers in `RuntimeSettingsDrawer.tsx` so each operation has field-scoped state and a latest-request guard. Concretely: `handlePickDir`, `handlePickFiles`, `handleRescan`, and any async validation/search path must update only the field(s) they own rather than resetting the full draft. Add explicit request sequencing per field or operation key so older responses are ignored when a newer request for that same field has started. Update `runtime-settings-form.ts` helpers so rescans preserve the current drafted model path when it still exists, and return an invalid-draft signal when the drafted path no longer exists instead of auto-falling back to another model silently. Add helper-level tests in `runtime-settings-form.test.ts` for preserved selection, invalid-selection detection, and non-silent fallback rules.</action>
|
||||
<acceptance_criteria>
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` contains explicit stale-request protection for field-level async handlers.
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.ts` exposes helper logic that distinguishes valid preservation from invalid drafted selections.
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts` covers preserved selection and invalid-selection outcomes.
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>npm test -- runtime-settings-form</automated>
|
||||
</verify>
|
||||
<done>Async field helpers can no longer silently rewrite unrelated fields or let older responses override the latest user intent.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Preserve invalid drafts visibly and block save until resolved</name>
|
||||
<files>tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx, tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx</files>
|
||||
<read_first>
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
- .planning/phases/01-hf-model-discovery/01-UI-SPEC.md
|
||||
- .planning/phases/01-hf-model-discovery/01-CONTEXT.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test 1: an invalid model or folder draft remains visible with inline error text and destructive styling instead of being cleared (CONF-04, D-11).
|
||||
- Test 2: `Save changes` is disabled while invalid draft metadata exists.
|
||||
- Test 3: a failed save keeps the current draft values in place and surfaces the failure message instead of resetting the form (CONF-04).
|
||||
</behavior>
|
||||
<action>In `RuntimeSettingsDrawer.tsx`, render inline invalid-state messaging for drafted values that fail validation or disappear during rescan. Preserve the user's typed or selected value in the control, add the destructive invalid styling from the Phase 1 color contract, and disable `Save changes` whenever invalid draft metadata exists. Keep save failures drawer-visible without resetting the draft or clearing helper state. Extend `RuntimeSurface.test.tsx` to assert concrete UI outcomes: invalid drafted values stay in the control, inline error copy is rendered near the affected field, `Save changes` is disabled until corrected, and a rejected save preserves the user's current draft state.</action>
|
||||
<acceptance_criteria>
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` renders inline invalid/error UI near affected fields and disables `Save changes` when invalid draft state exists.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` asserts that invalid draft values remain visible and save failures preserve the draft.
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>npm test -- RuntimeSurface</automated>
|
||||
</verify>
|
||||
<done>Phase 1 ends with durable draft safety: invalid values are visible, saves are blocked until fixed, stale async work is ignored, and user intent survives failures.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npm test -- runtime-settings-form`
|
||||
- `npm test -- RuntimeSurface`
|
||||
- `npm run build`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Async field updates never overwrite unrelated draft fields.
|
||||
- Invalid model/folder drafts remain visible, explain the problem inline, and block save until fixed.
|
||||
- Save failures and rescans preserve user intent rather than silently replacing or clearing values.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-hf-model-discovery/01-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
phase: 01-draft-state-ownership
|
||||
plan: 03
|
||||
subsystem: ui
|
||||
tags: [react, typescript, vitest, async-state, validation]
|
||||
requires:
|
||||
- phase: 01-02
|
||||
provides: controlled draft drawer and explicit draft/live UI semantics
|
||||
provides:
|
||||
- stale-safe field-scoped async handling for model folders, rescans, and picker flows
|
||||
- invalid draft preservation with inline errors and save blocking
|
||||
- save failure recovery that keeps current draft values visible and editable
|
||||
affects: [phase-2-lifecycle-safety, runtime-validation, runtime-workspace]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [shared async request guard for field-scoped operations, invalid draft preservation instead of silent fallback, helper-level model-selection validation]
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
|
||||
- tauri-app/src/features/runtime/runtime-settings-form.ts
|
||||
- tauri-app/src/features/runtime/runtime-settings-form.test.ts
|
||||
- tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx
|
||||
- tauri-app/src/features/runtime/useRuntimeManager.ts
|
||||
key-decisions:
|
||||
- "Use a shared request key for model-selection async work so stale scan responses cannot overwrite later browse or picker intent."
|
||||
- "Preserve invalid drafted model values and surface inline errors rather than auto-selecting a replacement model."
|
||||
patterns-established:
|
||||
- "Async folder and model operations update only their owned draft fields and attach pending or invalid metadata instead of rehydrating the full form."
|
||||
- "Save stays disabled while invalid or pending field metadata exists, and failed saves keep the draft intact."
|
||||
requirements-completed: [CONF-01, CONF-03, CONF-04]
|
||||
duration: 18min
|
||||
completed: 2026-03-29
|
||||
---
|
||||
|
||||
# Phase 1 Plan 3: Async Draft Safety Summary
|
||||
|
||||
**Field-scoped async guards, invalid draft preservation, and save-failure recovery for runtime settings**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 18 min
|
||||
- **Started:** 2026-03-29T02:53:00Z
|
||||
- **Completed:** 2026-03-29T03:10:41Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 5
|
||||
|
||||
## Accomplishments
|
||||
- Added helper logic in `runtime-settings-form.ts` to distinguish valid selection preservation from invalid drafted model paths
|
||||
- Hardened `RuntimeSettingsDrawer.tsx` so async scans and pickers update only their owned fields, ignore stale results, and keep invalid draft values visible inline
|
||||
- Added regression coverage for stale async protection, invalid draft save blocking, and failed-save draft preservation
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Make async picker and rescan handlers field-scoped and stale-safe** - `80883c2` (feat)
|
||||
2. **Task 2: Preserve invalid drafts visibly and block save until resolved** - `80883c2` (feat)
|
||||
|
||||
**Plan metadata:** pending
|
||||
|
||||
## Files Created/Modified
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` - Shared async request guarding, inline invalid-state messaging, and save blocking for pending or invalid draft fields
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.ts` - Draft model resolution helper that keeps missing selections visible and marked invalid
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts` - Unit coverage for preserved selections and invalid draft outcomes
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` - Runtime regression tests for stale async responses, invalid draft preservation, and failed-save recovery
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts` - Draft metadata update helpers used by the controlled drawer for pending and invalid field state
|
||||
|
||||
## Decisions Made
|
||||
- Unified scan, folder browse, and file-pick draft updates under one model-selection request channel so later user intent always wins over earlier async work
|
||||
- Disabled `Save changes` whenever invalid or pending draft metadata exists to match the locked Phase 1 trust contract
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Integration] Extended manager metadata APIs for pending and invalid field state**
|
||||
- **Found during:** Task 2 (invalid draft preservation)
|
||||
- **Issue:** The typed manager contract existed, but the drawer needed a write path for field-level pending and invalid metadata
|
||||
- **Fix:** Added `updateDraft` and `updateDraftFieldMeta` support in `tauri-app/src/features/runtime/useRuntimeManager.ts`
|
||||
- **Files modified:** `tauri-app/src/features/runtime/useRuntimeManager.ts`
|
||||
- **Verification:** `npm test -- RuntimeSurface`; `npm test -- runtime-settings-form`; `npm run build`
|
||||
- **Committed in:** `80883c2`
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (integration)
|
||||
**Impact on plan:** Required to complete the planned invalid-state behavior. No user-facing scope expansion beyond Phase 1 draft safety.
|
||||
|
||||
## Issues Encountered
|
||||
- The stale-response test originally used two rescan requests, but the UI correctly disables a second rescan while one is already in flight; the regression was adjusted to cover a pending initial scan followed by a later folder browse request against the same model-selection guard
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
- Phase 1 draft safety is complete: polling, discard, invalid values, async rescans, and save failures now preserve user intent
|
||||
- Phase 2 can build explicit lifecycle command semantics on top of a trusted settings surface instead of compensating for draft-state bugs
|
||||
|
||||
## Self-Check
|
||||
|
||||
PASSED
|
||||
|
||||
- Verified helper-level selection and invalid-draft behavior with `npm test -- runtime-settings-form`
|
||||
- Verified stale async guards, invalid draft blocking, and save-failure recovery with `npm test -- RuntimeSurface`
|
||||
- Verified production compile with `npm run build`
|
||||
|
||||
---
|
||||
*Phase: 01-draft-state-ownership*
|
||||
*Completed: 2026-03-29*
|
||||
@@ -1,105 +0,0 @@
|
||||
# Phase 1: Draft State Ownership - Context
|
||||
|
||||
**Gathered:** 2026-03-28
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Separate editable runtime settings from live runtime state so polling, folder/model refresh work, and other background activity cannot clobber in-progress edits before the user explicitly saves or discards them.
|
||||
|
||||
This phase covers draft ownership, reset/save/reopen behavior, and safe async updates inside the runtime settings experience. It does not yet consolidate the external runtime status card into a single workspace; that belongs to Phase 3.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Draft lifecycle
|
||||
- **D-01:** Opening the runtime settings surface creates the editable draft from the latest saved baseline.
|
||||
- **D-02:** Live polling continues while the drawer is open, but only live operational state updates from polling; the draft itself is not rehydrated while dirty.
|
||||
- **D-03:** After a successful save, the saved values become the new clean baseline immediately and the settings surface stays open.
|
||||
- **D-04:** `Cancel` resets the full form back to the last saved baseline.
|
||||
- **D-05:** `Revert` is the same as full-form `Cancel` for Phase 1; no separate per-field undo behavior is required.
|
||||
- **D-06:** Closing the drawer or backdrop with unsaved edits hides the draft without discarding it, so reopening in the same session restores that hidden draft.
|
||||
|
||||
### Async field behavior
|
||||
- **D-07:** Folder pickers and file choosers may update only the field they were launched for; they must not rehydrate or rewrite unrelated draft fields.
|
||||
- **D-08:** Model rescans may refresh available options, but should preserve the current draft selection unless that selection becomes invalid.
|
||||
- **D-09:** While the form is dirty, background polling may continue to refresh live status-oriented data (for example runtime health, readiness, or library status) but must not overwrite draft-facing fields or local async selections.
|
||||
- **D-10:** Older async responses for the same field or operation must be ignored; only the latest in-flight request for that field may update the draft-facing UI.
|
||||
- **D-11:** If a rescan or validation pass shows the currently drafted model or folder choice is invalid, keep the user's draft value, mark it invalid inline, and block save until the user fixes it.
|
||||
|
||||
### the agent's Discretion
|
||||
- Exact UI treatment for showing live value versus draft value, as long as users can clearly tell when a field is unsaved.
|
||||
- Exact request identity/cancellation mechanism used to ignore stale async results.
|
||||
- Exact wording and placement of inline validation and dirty-state messaging, as long as draft safety remains clear.
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Project scope and phase requirements
|
||||
- `.planning/PROJECT.md` — Project context, brownfield constraints, and the core value that unsaved edits must not be overwritten.
|
||||
- `.planning/REQUIREMENTS.md` — Phase 1 requirement set (`WORK-02`, `CONF-01`, `CONF-02`, `CONF-03`, `CONF-04`) and requirement wording.
|
||||
- `.planning/ROADMAP.md` — Phase 1 goal, success criteria, and dependency boundary.
|
||||
- `.planning/STATE.md` — Current focus and locked project-level decisions carried into this phase.
|
||||
|
||||
### Existing architecture and codebase context
|
||||
- `.planning/codebase/ARCHITECTURE.md` — Runtime management architecture, ownership boundaries, and frontend/backend integration path.
|
||||
- `.planning/codebase/STACK.md` — Current stack and runtime/config persistence context.
|
||||
|
||||
### Runtime workspace implementation
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts` — Current live snapshot polling, save/start/stop mutation flow, and snapshot overwrite behavior.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` — Current form state, full rehydration effect, async helpers, and save actions.
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.ts` — Patch creation rules, model selection helper behavior, and local-vs-remote field preservation.
|
||||
- `tauri-app/src/features/runtime/types.ts` — `RuntimeSnapshot` and `RuntimeConfig` ownership boundary.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeStatusCard.tsx` — Adjacent runtime surface kept in mind for Phase 3 separation, not for Phase 1 scope.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts`: already centralizes runtime snapshot fetching, polling, and lifecycle mutations; Phase 1 should likely evolve this into a clearer live-state owner rather than duplicating runtime orchestration elsewhere.
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.ts`: already knows how to derive a config patch from baseline config plus form values; useful as the baseline-vs-draft merge boundary.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`: already contains the relevant settings controls and async hooks, so Phase 1 can refactor ownership inside the existing surface before any larger UI reorganization.
|
||||
|
||||
### Established Patterns
|
||||
- Runtime state is currently polled every 3 seconds and stored as one `RuntimeSnapshot`; Phase 1 must break the assumption that this same object can also be the editable form source of truth.
|
||||
- Save/start/stop flows already return refreshed snapshots from the backend; Phase 1 should preserve that backend contract while changing how the frontend promotes saved state into the draft baseline.
|
||||
- The app is brownfield React + TypeScript without a global store; planning should prefer a feature-scoped ownership model over a whole-app state rewrite.
|
||||
|
||||
### Integration Points
|
||||
- Draft creation and promotion connect directly to `useRuntimeManager.ts` save and refresh behavior.
|
||||
- Dirty-state-safe field updates connect directly to the rehydration effect and async picker/rescan logic inside `RuntimeSettingsDrawer.tsx`.
|
||||
- Validation and invalid-draft blocking connect to `runtime-settings-form.ts` patch generation and any field-level validation added during Phase 1.
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Closing with unsaved edits should behave differently from `Cancel`: the draft is hidden and restored on reopen within the same session.
|
||||
- Async helpers should be narrow and field-scoped: a folder pick should update that folder field, not cause a whole-form refresh.
|
||||
- Invalid async outcomes should preserve user intent visibly instead of silently auto-correcting the draft.
|
||||
- Note: the current phase directory slug (`01-hf-model-discovery`) is stale metadata from earlier planning artifacts; ROADMAP Phase 1 (`Draft State Ownership`) is the canonical phase definition for downstream work.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- Full runtime workspace consolidation and moving the runtime manager into a single control center — Phase 3 (`Unified Runtime Workspace`).
|
||||
- Source-specific path/model validation UX beyond draft preservation basics — Phase 4 (`Model Source Validation`).
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 01-hf-model-discovery*
|
||||
*Context gathered: 2026-03-28*
|
||||
@@ -1,139 +0,0 @@
|
||||
# Phase 1: Draft State Ownership - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-03-28
|
||||
**Phase:** 01-draft-state-ownership
|
||||
**Areas discussed:** Draft lifecycle, Async field behavior
|
||||
|
||||
---
|
||||
|
||||
## Draft lifecycle
|
||||
|
||||
### When should the editable draft be created?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| On open | Create a working draft from the last saved config when the settings surface opens. | ✓ |
|
||||
| On first edit | Do not create a draft until the user changes something. | |
|
||||
| Persisted always | Keep one always-live draft even while the settings surface is closed. | |
|
||||
|
||||
**User's choice:** On open
|
||||
**Notes:** Opening Runtime Settings should create the working draft; live polling keeps updating operational status only.
|
||||
|
||||
### After a successful save, what should happen to the draft?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Promote and keep open | Saved values become the new clean baseline and the settings surface stays open. | ✓ |
|
||||
| Save and close | Save, clear the draft, and close the drawer immediately. | |
|
||||
| Save but keep draft dirty | Save to backend while preserving a separate unsaved local copy. | |
|
||||
|
||||
**User's choice:** Promote and keep open
|
||||
**Notes:** Saved values should become the new baseline immediately.
|
||||
|
||||
### What should Cancel do while editing?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Reset to last saved | Discard unsaved changes and restore the last saved baseline in the form. | ✓ |
|
||||
| Close and keep draft | Close without resetting any unsaved draft values. | |
|
||||
| Ask every time | Require confirmation before resetting changes. | |
|
||||
|
||||
**User's choice:** Reset to last saved
|
||||
**Notes:** Cancel is the explicit discard action.
|
||||
|
||||
### How should Revert differ from Cancel?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Same as cancel | One full-form revert is enough for Phase 1. | ✓ |
|
||||
| Revert field-by-field | Allow a more granular undo path than Cancel. | |
|
||||
| Revert to effective runtime | Reset to whatever is live, even if it differs from last saved config. | |
|
||||
|
||||
**User's choice:** Same as cancel
|
||||
**Notes:** No per-field undo is needed yet.
|
||||
|
||||
### If the user closes the drawer with unsaved edits, what should happen?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Keep draft hidden | Close the drawer but restore the same in-session draft on reopen. | ✓ |
|
||||
| Discard draft | Treat close the same as Cancel. | |
|
||||
| Force confirm dialog | Require an explicit choice before allowing close. | |
|
||||
|
||||
**User's choice:** Keep draft hidden
|
||||
**Notes:** Close/backdrop behavior should differ from Cancel.
|
||||
|
||||
---
|
||||
|
||||
## Async field behavior
|
||||
|
||||
### When a folder picker or file chooser finishes after the draft is already dirty, how should its result be applied?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Apply only if same field | Update only the field the user just picked, without rehydrating other draft fields. | ✓ |
|
||||
| Apply everything from latest scan | Let the picker or scan refresh the broader form state too. | |
|
||||
| Ask before applying | Show a confirmation before changing the draft. | |
|
||||
|
||||
**User's choice:** Apply only if same field
|
||||
**Notes:** Async helpers should be field-scoped, not whole-form rehydration triggers.
|
||||
|
||||
### If a model rescan finishes while the user is editing, what should happen to model-related state?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Refresh options, keep selection | Update the available model list but preserve the current draft selection unless it becomes invalid. | ✓ |
|
||||
| Auto-pick best model | Replace the current selection with the first valid or recommended model. | |
|
||||
| Defer until save | Do not update model options until the user saves. | |
|
||||
|
||||
**User's choice:** Refresh options, keep selection
|
||||
**Notes:** Current draft selection should remain stable unless rescan proves it invalid.
|
||||
|
||||
### While the drawer has unsaved edits, how should background polling affect async-derived data like health, GPU hints, or library status?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Status only, no draft overwrite | Keep live operational status fresh, but never rewrite draft fields or local async selections. | ✓ |
|
||||
| Pause all refreshes | Freeze both status and async derived data until the draft is clean. | |
|
||||
| Full live sync | Continue updating the full form from polling even while dirty. | |
|
||||
|
||||
**User's choice:** Status only, no draft overwrite
|
||||
**Notes:** Polling remains valuable, but only for live status surfaces.
|
||||
|
||||
### If an older async request returns after a newer one for the same field, what should happen?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Ignore stale results | Only the latest request for that field may update the draft-facing UI. | ✓ |
|
||||
| Last response wins | Whichever request finishes last overwrites the UI. | |
|
||||
| Show conflict warning | Keep both and ask the user which one to accept. | |
|
||||
|
||||
**User's choice:** Ignore stale results
|
||||
**Notes:** Stale async results must not overwrite newer user intent.
|
||||
|
||||
### If a rescan shows the currently drafted model or folder choice is no longer valid, what should the UI do before save?
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Keep draft, mark invalid | Preserve the draft value, show validation, and block save until fixed. | ✓ |
|
||||
| Auto-switch to valid option | Replace it immediately with the first valid option. | |
|
||||
| Clear the field | Remove the invalid choice and force a new selection. | |
|
||||
|
||||
**User's choice:** Keep draft, mark invalid
|
||||
**Notes:** The UI should preserve user intent visibly instead of silently auto-correcting it.
|
||||
|
||||
---
|
||||
|
||||
## the agent's Discretion
|
||||
|
||||
- Exact live-vs-draft visual treatment.
|
||||
- Exact request identity/cancellation mechanism for stale async response handling.
|
||||
- Exact inline validation copy and placement.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- Consolidating the status card and settings surface into one runtime control center belongs to Phase 3.
|
||||
- Broader source/model/path validation improvements beyond draft-safety behavior belong to Phase 4.
|
||||
@@ -1,160 +0,0 @@
|
||||
# Phase 1: Draft State Ownership - Research
|
||||
|
||||
**Researched:** 2026-03-28
|
||||
**Domain:** React/Tauri runtime settings draft ownership, polling isolation, and async form safety
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 1 should stay entirely inside the existing React runtime workspace and `useRuntimeManager` boundary. The current failure mode is structural: `useRuntimeManager` owns a single polled `snapshot`, and `RuntimeSettingsDrawer.tsx` rehydrates all local field state from `snapshot.config` whenever the drawer is open and `snapshot` changes. Because polling runs every 3 seconds, any refresh, scan result, or save response can overwrite in-progress edits.
|
||||
|
||||
The lowest-risk fix is to split ownership into three explicit layers:
|
||||
|
||||
1. **Live runtime snapshot** - continues to refresh from polling and lifecycle commands.
|
||||
2. **Saved baseline config** - the last persisted config the user can safely revert to.
|
||||
3. **Editable draft state** - drawer-local intent that survives close/reopen in the same app session and is never overwritten by polling while dirty.
|
||||
|
||||
This phase should not introduce a global store, backend schema change, or Phase 3 workspace consolidation. Keep the brownfield shape: `useRuntimeManager` remains the feature-scoped source of truth, `RuntimeSettingsDrawer.tsx` becomes a renderer/editor over manager-owned draft state, and current save/start/stop backend contracts stay intact.
|
||||
|
||||
**Primary recommendation:** move draft/baseline ownership into the runtime feature state layer, expose explicit draft actions from `useRuntimeManager`, and refactor the drawer so async helpers update only targeted fields while stale responses are ignored.
|
||||
|
||||
## Current-State Findings
|
||||
|
||||
### Existing overwrite risk
|
||||
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts` polls `getRuntimeSnapshot()` every 3 seconds and writes the whole `snapshot` object back into React state.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx` has a single `useEffect(..., [open, snapshot])` that repopulates all local form fields from `snapshot.config` whenever polling refreshes the snapshot while the drawer is open.
|
||||
- `saveConfig()` in `useRuntimeManager.ts` builds the save payload from the hook's current `snapshot.config`, so the save baseline is also tied to the same mutable polled object.
|
||||
|
||||
### Existing reusable boundaries
|
||||
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.ts` already centralizes patch construction and model selection helpers. It is the right place for draft-vs-baseline merge rules and invalid-selection handling helpers.
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` already covers the runtime status card + drawer integration and is the best place to add polling/draft regression tests.
|
||||
- `tauri-app/src/features/runtime/types.ts` is the existing contract surface for the runtime feature and should carry any new draft-specific UI contracts.
|
||||
|
||||
### UX constraints already locked
|
||||
|
||||
- Per `01-CONTEXT.md`, opening creates a draft from the latest saved baseline (D-01), close preserves unsaved draft in-session (D-06), cancel/discard resets to the last saved baseline (D-04/D-05), async helpers are field-scoped (D-07), stale responses must be ignored (D-10), and invalid values must be preserved inline with save blocked (D-11).
|
||||
- Per `01-UI-SPEC.md`, the drawer must present `Draft settings`, `Live now`, `Save changes`, `Discard changes`, `Keep editing`, and sticky live-vs-draft affordances instead of silently mutating values.
|
||||
|
||||
## Recommended Architecture
|
||||
|
||||
### Pattern 1: Manager owns live snapshot + baseline + draft
|
||||
|
||||
Add explicit runtime feature state instead of deriving editable form state directly from `snapshot` on every render.
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- `liveSnapshot: RuntimeSnapshot` - latest polling/lifecycle truth
|
||||
- `savedBaseline: RuntimeConfig` - last committed config
|
||||
- `draftConfig: RuntimeConfig | null` - active or hidden in-session draft
|
||||
- `isDraftDirty: boolean`
|
||||
- `isDraftOpen: boolean`
|
||||
- `draftErrors` / `invalidFields` / `pendingFieldOps`
|
||||
|
||||
Recommended actions:
|
||||
|
||||
- `openDraft()` - create draft from `savedBaseline` only if draft does not already exist
|
||||
- `closeDraft()` - hide draft without discarding it
|
||||
- `discardDraft()` - replace full draft with `savedBaseline`
|
||||
- `updateDraftField(field, value)` - targeted field update
|
||||
- `saveDraft()` - persist current draft, promote saved values to baseline, keep drawer open
|
||||
- `applyLiveRefresh(snapshot)` - update operational live state every poll; update baseline only when draft is absent or clean
|
||||
|
||||
### Pattern 2: Drawer becomes a controlled draft editor
|
||||
|
||||
`RuntimeSettingsDrawer.tsx` should stop treating `snapshot` as the editable source of truth. Instead it should receive:
|
||||
|
||||
- current draft values
|
||||
- saved/live reference values
|
||||
- dirty/invalid metadata
|
||||
- explicit actions for field updates, discard, save, and close
|
||||
|
||||
The drawer may keep only ephemeral UI state that is not part of the draft contract, such as an expanded discard confirmation row or transient search text when that text is intentionally not preserved.
|
||||
|
||||
### Pattern 3: Field-scoped async operations
|
||||
|
||||
Folder pickers, rescans, GPU detection, and any validation should update a targeted field or helper state only. Do not run a full-form rehydrate after any async operation.
|
||||
|
||||
Recommended approach:
|
||||
|
||||
- track per-field request ids or sequence counters
|
||||
- before applying an async result, verify its id matches the latest request for that field
|
||||
- preserve the drafted field value if validation says it is invalid; attach inline invalid metadata instead of auto-correcting or clearing the field
|
||||
|
||||
This is especially important for:
|
||||
|
||||
- `models_dir`
|
||||
- `selectedModel`
|
||||
- `selectedModelId`
|
||||
- file-picker-added extra model paths
|
||||
|
||||
### Pattern 4: Save promotes baseline immediately
|
||||
|
||||
After a successful save:
|
||||
|
||||
- the returned config becomes the new `savedBaseline`
|
||||
- the visible draft is replaced with the saved values and marked clean
|
||||
- the drawer stays open
|
||||
- polling continues, but future polls must not regress the saved baseline or draft-facing UI
|
||||
|
||||
### Pattern 5: Failure preserves draft
|
||||
|
||||
Save or validation failures should not reset the draft. Keep the user's current values in the editor, surface inline or drawer-level error messaging, and block save until invalid fields are resolved.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Rehydrating local input state from `snapshot.config` in a `useEffect` that depends on the full `snapshot`
|
||||
- Using one `busy` flag as the only async state for all field-level operations
|
||||
- Clearing invalid model/folder choices automatically after rescan
|
||||
- Coupling discard behavior to drawer close/backdrop click
|
||||
- Introducing Redux/Zustand/global app state for a feature-scoped ownership problem
|
||||
- Moving runtime status card consolidation into this phase (deferred to Phase 3)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Recommended automated coverage
|
||||
|
||||
| Behavior | Test file | Automated command |
|
||||
|----------|-----------|-------------------|
|
||||
| Poll refresh does not overwrite dirty draft fields | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
|
||||
| Close/reopen preserves in-session draft but discard resets to baseline | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
|
||||
| Save promotes baseline and keeps drawer open | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
|
||||
| Field-scoped rescan preserves draft selection unless invalid | `tauri-app/src/features/runtime/runtime-settings-form.test.ts` and/or `RuntimeSurface.test.tsx` | `npm test -- runtime-settings-form` |
|
||||
| Invalid draft values remain visible and block save | `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx` | `npm test -- RuntimeSurface` |
|
||||
|
||||
### Phase verification commands
|
||||
|
||||
- Targeted UI/unit tests: `npm test -- RuntimeSurface`
|
||||
- Helper/unit tests: `npm test -- runtime-settings-form`
|
||||
- Phase gate: `npm run build`
|
||||
|
||||
## Open Questions Resolved For Planning
|
||||
|
||||
1. **Where should draft ownership live?**
|
||||
- Recommendation: feature-scoped runtime manager state, not backend and not drawer-local snapshot rehydration.
|
||||
2. **Do we need backend revision/timestamp support for Phase 1?**
|
||||
- Recommendation: no backend change required for the first pass. The immediate bug is frontend ownership. Reconcile against the latest saved config returned by existing save/refresh calls.
|
||||
3. **Should close prompt to discard?**
|
||||
- Recommendation: no. Close hides the draft; `Discard changes` is the explicit reset path.
|
||||
4. **Should invalid rescans clear selection?**
|
||||
- Recommendation: no. Preserve the value, mark invalid inline, disable save.
|
||||
|
||||
## Sources
|
||||
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts`
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx`
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.ts`
|
||||
- `tauri-app/src/features/runtime/types.ts`
|
||||
- `tauri-app/src/features/runtime/components/RuntimeSurface.test.tsx`
|
||||
- `tauri-app/src/features/runtime/runtime-settings-form.test.ts`
|
||||
- `.planning/phases/01-hf-model-discovery/01-CONTEXT.md`
|
||||
- `.planning/phases/01-hf-model-discovery/01-UI-SPEC.md`
|
||||
- `.planning/ROADMAP.md`
|
||||
- `.planning/REQUIREMENTS.md`
|
||||
|
||||
## Metadata
|
||||
|
||||
- Research focus: brownfield-safe draft-state separation for the runtime settings drawer
|
||||
- New external dependencies required: none
|
||||
- Suggested discovery level: Level 0 (existing patterns only; no new library selection)
|
||||
@@ -1,160 +0,0 @@
|
||||
---
|
||||
phase: 1
|
||||
slug: draft-state-ownership
|
||||
status: draft
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-03-28
|
||||
---
|
||||
|
||||
# Phase 1 — UI Design Contract
|
||||
|
||||
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Tool | none — existing hand-authored CSS remains the source of truth for Phase 1; shadcn init deferred by default because this phase only defines the contract and does not require component-library adoption |
|
||||
| Preset | not applicable |
|
||||
| Component library | none — preserve existing runtime drawer, status card, inline chips, segmented controls, and footer actions |
|
||||
| Icon library | none required for Phase 1; use text labels and existing native controls before introducing new iconography |
|
||||
| Font | "Segoe UI Variable", "SF Pro Display", "IBM Plex Sans", system-ui, sans-serif |
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Declared values (must be multiples of 4):
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| xs | 4px | Status dots, inline separators, badge internals |
|
||||
| sm | 8px | Input-support gaps, chip gaps, tight action spacing |
|
||||
| md | 16px | Default field spacing, card padding, inline validation offset |
|
||||
| lg | 24px | Section padding, grouped control spacing |
|
||||
| xl | 32px | Drawer section breaks, footer-to-body separation |
|
||||
| 2xl | 48px | Major content breaks inside long settings drawers |
|
||||
| 3xl | 64px | Reserved for page-level or modal-level spacing only |
|
||||
|
||||
Exceptions: minimum 44px input and button height for all primary interactive controls; destructive confirmation rows may use 12px vertical padding inside a 44px container.
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
| Role | Size | Weight | Line Height |
|
||||
|------|------|--------|-------------|
|
||||
| Body | 16px | 400 | 1.5 |
|
||||
| Label | 12px | 700 | 1.4 |
|
||||
| Heading | 20px | 700 | 1.2 |
|
||||
| Display | 28px | 700 | 1.1 |
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
| Role | Value | Usage |
|
||||
|------|-------|-------|
|
||||
| Dominant (60%) | #0a1016 | App background, drawer shell, page canvas |
|
||||
| Secondary (30%) | #14202d | Cards, grouped sections, saved/live reference surfaces |
|
||||
| Accent (10%) | #48a6ff | Draft-state badges, selected segmented controls, focused fields, primary save CTA, latest-valid async result indicators |
|
||||
| Destructive | #f04040 | Discard/revert confirmations, inline invalid-state borders, blocking save errors |
|
||||
|
||||
Accent reserved for: dirty badges, active local-vs-remote selectors, selected model chips, the primary `Save changes` action, focus-visible outlines, and explicit live-vs-draft comparison labels; never use accent for all neutral buttons.
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
| Element | Copy |
|
||||
|---------|------|
|
||||
| Primary CTA | Save changes |
|
||||
| Secondary footer action | Discard changes |
|
||||
| Confirmation escape action | Keep editing |
|
||||
| Empty state heading | No draft changes yet |
|
||||
| Empty state body | Edit any runtime setting to start a draft. Live runtime values keep updating until you choose to save or discard. |
|
||||
| Error state | Couldn't save your draft. Review the highlighted fields and try again — your unsaved changes are still here. |
|
||||
| Destructive confirmation | Discard changes: Discard this draft and return to the last saved settings? / Revert to saved: Replace all unsaved edits with the last saved values. |
|
||||
|
||||
---
|
||||
|
||||
## State Presentation Contract
|
||||
|
||||
- The sticky drawer header is the first visual anchor on open: `Draft settings` title, dirty-state summary, and compact `Live now` status reference appear before any editable fields.
|
||||
- Opening the settings drawer creates a draft from the latest saved baseline and labels the drawer as `Draft settings`.
|
||||
- The current effective runtime state remains visible as `Live now` reference content in a secondary surface or read-only inline value for any field changed in the draft.
|
||||
- Dirty fields show three signals together: draft accent border, `Draft` label, and saved/live reference value below the control; color alone is never the only indicator.
|
||||
- Clean fields render with neutral borders and no draft badge.
|
||||
- Closing the drawer with unsaved edits hides the drawer and preserves the draft for the same app session; do not show a discard prompt on close or backdrop click.
|
||||
- `Discard changes` is the UI label for the Phase 1 full-form reset behavior locked in `01-CONTEXT.md`; `Revert to saved` is the destructive confirmation label for completing that same reset. Per-field undo is out of scope.
|
||||
|
||||
---
|
||||
|
||||
## Interaction Contract
|
||||
|
||||
- Polling continues every 3 seconds for operational state, but polling updates may affect only live status surfaces such as readiness, process state, health messaging, and managed-library metadata.
|
||||
- Polling and async helpers must never overwrite dirty input values, selected folders, provider choice, or selected model while the draft is dirty.
|
||||
- Folder pickers, file pickers, model rescans, and validation passes are field-scoped updates only; they may update the invoked field and its dependent helper text, not the whole form.
|
||||
- If multiple async requests exist for the same field, only the latest response may update draft-facing UI; stale responses are ignored silently.
|
||||
- When a drafted value becomes invalid after rescan or validation, preserve the typed or selected value, mark the field invalid inline, and disable `Save changes` until fixed.
|
||||
- Resting footer actions for this phase are `Save changes` and `Discard changes`; selecting `Discard changes` expands an inline confirmation row with `Keep editing` and destructive `Revert to saved`. Local-runtime apply/start text remains separate and must always reflect whether saving is required first.
|
||||
- Busy states disable the triggering button, preserve current field values, and replace button text with in-place progress copy such as `Saving...`, `Scanning...`, or `Searching...`.
|
||||
- Error messaging appears inline near the affected field when possible; fall back to a single drawer-level error banner only for save failures or cross-field failures.
|
||||
|
||||
---
|
||||
|
||||
## Component Inventory
|
||||
|
||||
- Drawer shell: right-side fixed drawer, full-height, max width 560px, blurred backdrop, sticky header and footer.
|
||||
- Primary focal point: sticky header block with `Draft settings` title, dirty-state summary, and `Live now` chip/status summary before the overview strip.
|
||||
- Overview strip: three read-only summary cells for current status, endpoint, and saved model.
|
||||
- Field groups: card-based sections with 16px internal padding and 18px corner radius.
|
||||
- Segmented controls: two-option mode switch with clear active state and no icon-only affordances.
|
||||
- Draft comparison rows: editable control on top, saved/live helper row beneath when dirty.
|
||||
- Inline validation: destructive border plus one-line message directly under the affected field.
|
||||
- Confirmation pattern: inline footer confirmation row for discard/revert actions instead of a separate modal.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility and Motion
|
||||
|
||||
- All interactive controls must meet a 44x44px minimum hit target.
|
||||
- Keyboard order must follow visual order from overview to footer actions.
|
||||
- `:focus-visible` keeps the 2px accent outline already established in `tauri-app/src/index.css`.
|
||||
- Body copy and helper text must maintain at least 4.5:1 contrast against their surfaces.
|
||||
- State changes use opacity, border, and background transitions only, with durations between 150ms and 200ms.
|
||||
- Respect `prefers-reduced-motion`; suppress scale transforms and animated status pulses when reduced motion is enabled.
|
||||
|
||||
---
|
||||
|
||||
## Source Notes
|
||||
|
||||
- Locked from `D:\Documents\Github\ForgeLM\.planning\ROADMAP.md`: Phase 1 goal and success criteria require explicit live-versus-draft separation, explicit save/cancel/revert semantics, and draft-safe failure recovery.
|
||||
- Locked from `D:\Documents\Github\ForgeLM\.planning\phases\01-hf-model-discovery\01-CONTEXT.md`: draft lifecycle, preserved close behavior, field-scoped async updates, stale-request ignoring, and invalid-draft preservation.
|
||||
- Grounded in existing UI from `D:\Documents\Github\ForgeLM\tauri-app\src\index.css`, `D:\Documents\Github\ForgeLM\tauri-app\src\features\runtime\components\RuntimeSettingsDrawer.css`, `D:\Documents\Github\ForgeLM\tauri-app\src\features\runtime\components\RuntimeStatusCard.css`, and `D:\Documents\Github\ForgeLM\tauri-app\src\features\runtime\components\RuntimeSettingsDrawer.tsx`.
|
||||
- Brand output conventions referenced from `C:\Users\Evandro\.config\opencode\get-shit-done\references\ui-brand.md`.
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| shadcn official | none | not applicable — shadcn not initialized |
|
||||
| third-party | none | not applicable |
|
||||
|
||||
---
|
||||
|
||||
## Checker Sign-Off
|
||||
|
||||
- [ ] Dimension 1 Copywriting: PASS
|
||||
- [ ] Dimension 2 Visuals: PASS
|
||||
- [ ] Dimension 3 Color: PASS
|
||||
- [ ] Dimension 4 Typography: PASS
|
||||
- [ ] Dimension 5 Spacing: PASS
|
||||
- [ ] Dimension 6 Registry Safety: PASS
|
||||
|
||||
**Approval:** pending
|
||||
Loaded 100 of 4412 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user