Compare commits
28
Commits
No files matched your search
@@ -0,0 +1,62 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When building UI components, implementing user-facing interfaces, or working with frontend accessibility
|
||||
---
|
||||
|
||||
## Accessibility Principles
|
||||
|
||||
### Core Standard
|
||||
Follow **WCAG 2.1 Level AA** as the baseline for all user-facing interfaces.
|
||||
|
||||
### Semantic HTML
|
||||
- Use semantic elements (`<nav>`, `<main>`, `<article>`, `<section>`, `<button>`, `<form>`) over generic `<div>` and `<span>`
|
||||
- Each page has exactly one `<h1>`, with proper heading hierarchy (h1 → h2 → h3, no skipping)
|
||||
- Use `<label>` elements associated with form inputs (via `for`/`id` or wrapping)
|
||||
- Use `<table>` for tabular data, never for layout
|
||||
|
||||
### Keyboard Navigation
|
||||
- All interactive elements must be reachable via Tab key
|
||||
- Focus order follows logical reading order
|
||||
- Focus indicators must be visible (never `outline: none` without replacement)
|
||||
- Custom components implement proper keyboard patterns (Arrow keys for menus, Escape to close modals)
|
||||
- No keyboard traps — user can always Tab away from any component
|
||||
|
||||
### ARIA Attributes
|
||||
- **First rule of ARIA:** Don't use ARIA if a native HTML element can do the job
|
||||
- Use `aria-label` or `aria-labelledby` for elements without visible text labels
|
||||
- Use `aria-live` regions for dynamic content updates (toasts, notifications)
|
||||
- Use `role` attributes only for custom widgets that lack native semantics
|
||||
- Use `aria-expanded`, `aria-controls` for disclosure patterns (dropdowns, accordions)
|
||||
|
||||
### Color and Contrast
|
||||
- Text contrast ratio: minimum **4.5:1** against background (AA normal text)
|
||||
- Large text contrast ratio: minimum **3:1** (AA large text)
|
||||
- **Never use color alone** to convey information (add icons, text, or patterns)
|
||||
- Support both light and dark themes with proper contrast in each
|
||||
|
||||
### Images and Media
|
||||
- All `<img>` elements have descriptive `alt` text (or `alt=""` for decorative images)
|
||||
- Videos have captions or transcripts
|
||||
- Audio controls are accessible and labeled
|
||||
|
||||
### Forms
|
||||
- Error messages are programmatically associated with inputs (`aria-describedby`)
|
||||
- Required fields are indicated both visually and programmatically (`required` attribute)
|
||||
- Form validation errors are announced to screen readers
|
||||
|
||||
### Accessibility Checklist
|
||||
|
||||
- [ ] All interactive elements reachable via keyboard?
|
||||
- [ ] Focus indicators visible on all interactive elements?
|
||||
- [ ] Semantic HTML elements used (no `<div>` buttons)?
|
||||
- [ ] Heading hierarchy is correct (h1 → h2 → h3)?
|
||||
- [ ] Color contrast meets AA standards (4.5:1)?
|
||||
- [ ] Color is not the sole indicator of meaning?
|
||||
- [ ] Images have appropriate alt text?
|
||||
- [ ] Form inputs have associated labels?
|
||||
- [ ] ARIA used correctly (native first)?
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Security Principles @security-principles.md (XSS prevention in user content)
|
||||
- Testing Strategy @testing-strategy.md (accessibility testing)
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing REST/HTTP APIs (endpoints, handlers, middleware, or response formatting)
|
||||
---
|
||||
|
||||
## API Design Principles
|
||||
|
||||
### RESTful API Standards
|
||||
|
||||
**Resource-Based URLs:**
|
||||
|
||||
- Use plural nouns for resources: `/api/{version}/users`, `/api/{version}/orders`
|
||||
- Hierarchical relationships: `/api/{version}/users/:userId/orders`
|
||||
- Avoid verbs in URLs: `/api/{version}/getUser` ❌ → `/api/{version}/users/:id` ✅
|
||||
|
||||
**HTTP Methods:**
|
||||
|
||||
- GET: Read/retrieve resource (safe, idempotent, cacheable)
|
||||
- POST: Create new resource (not idempotent)
|
||||
- PUT: Replace entire resource (idempotent)
|
||||
- PATCH: Partial update (idempotent)
|
||||
- DELETE: Remove resource (idempotent)
|
||||
|
||||
**Versioning:**
|
||||
|
||||
- URL path versioning: `/api/{version}/users` e.g.`/api/v1/users` (explicit, clear)
|
||||
|
||||
**Pagination:**
|
||||
|
||||
- Limit results per page (default 20, max 100)
|
||||
- Cursor-based: `?cursor=abc123` (better for real-time data)
|
||||
- Offset-based: `?page=2&limit=20` (simpler, less accurate for changing data)
|
||||
|
||||
**Filtering and Sorting:**
|
||||
|
||||
- Filtering: `?status=active&role=admin`
|
||||
- Sorting: `?sort=created_at:desc,name:asc`
|
||||
- Searching: `?q=search+term`
|
||||
|
||||
### HTTP Status Codes and Error Categories
|
||||
|
||||
**Success Codes:**
|
||||
- 200 OK: Success (GET, PUT, PATCH)
|
||||
- 201 Created: Resource created successfully (POST)
|
||||
- 204 No Content: Success with no response body (DELETE)
|
||||
|
||||
**Client Error Codes (4xx) - User Can Fix:**
|
||||
|
||||
**Validation Errors (400 Bad Request):**
|
||||
- User input doesn't meet requirements
|
||||
- Examples: Invalid email, password too short, required field missing
|
||||
- Response: Detailed field-level errors
|
||||
- User action: Correct input and retry
|
||||
|
||||
**Authentication Errors (401 Unauthorized):**
|
||||
- Identity verification failed
|
||||
- Examples: Invalid credentials, expired token, missing token
|
||||
- User action: Provide valid credentials
|
||||
|
||||
**Authorization Errors (403 Forbidden):**
|
||||
- Permission denied (user identified but lacks permission)
|
||||
- User action: Contact admin for permission
|
||||
|
||||
**Not Found Errors (404 Not Found):**
|
||||
- Resource doesn't exist or user lacks permission to know it exists
|
||||
- User action: None (doesn't exist)
|
||||
|
||||
**Business Rule Violations (409 Conflict / 422 Unprocessable Entity):**
|
||||
- Domain rule violations
|
||||
- Examples: Insufficient balance, duplicate email, order already shipped
|
||||
- Response: Business rule explanation
|
||||
- User action: Depends on business context
|
||||
|
||||
**Rate Limiting (429 Too Many Requests):**
|
||||
- Too many requests in time window
|
||||
- User action: Wait and retry
|
||||
|
||||
**Server Error Codes (5xx) - System Issue:**
|
||||
|
||||
**Infrastructure Errors (500/502/503):**
|
||||
- Database down, network timeout, external service failure
|
||||
- Response: Generic message with correlation ID
|
||||
- User action: None (system issue, retry later)
|
||||
|
||||
### API Success Response Format
|
||||
```
|
||||
{
|
||||
"data": { /* resource or array of resources */ },
|
||||
"meta": {
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"perPage": 20
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/v1/users?page=1",
|
||||
"next": "/api/v1/users?page=2",
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Error Response Format
|
||||
|
||||
All API errors must follow a consistent envelope structure, matching the success format where possible or using a standard error envelope.
|
||||
```
|
||||
{
|
||||
"status": "error", // Transport: Always "error" or "fail"
|
||||
"code": 400, // Transport: Redundant HTTP Status
|
||||
"error": { // Domain: The actual business problem
|
||||
"code": "VALIDATION_ERROR", // Machine-readable business code (UPPER_SNAKE)
|
||||
"message": "Invalid email format", // Human-readable message
|
||||
"details": { // Optional: Structured context
|
||||
"field": "email",
|
||||
"reason": "Must be a valid address"
|
||||
},
|
||||
"correlationId": "req-1234567890", // Ops: Traceability
|
||||
"doc_url": "https://..." // Optional: Help link
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Data Serialization and Interchange Principles @data-serialization-and-interchange-principles.md
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Architectural Patterns — Testability-First Design
|
||||
|
||||
### Core Principle
|
||||
All code must be independently testable without running the full application or external infrastructure.
|
||||
|
||||
### Universal Architecture Rules
|
||||
|
||||
#### Rule 1: I/O Isolation
|
||||
**Problem:** Tightly coupled I/O makes tests slow, flaky, and environment-dependent.
|
||||
|
||||
**Solution:** Abstract all I/O behind interfaces/contracts:
|
||||
- Database queries
|
||||
- HTTP calls (to external APIs)
|
||||
- File system operations
|
||||
- Time/randomness (for determinism)
|
||||
- Message queues
|
||||
|
||||
**Implementation Discovery:**
|
||||
1. Search the codebase for existing abstraction patterns by looking for symbols named: `Interface`, `Mock`, `Repository`, `Store`, `Adapter` (use your available codebase search tool — Pathfinder, grep, IDE symbol search, etc.)
|
||||
2. Match the style (interface in Go, Protocol in Python, interface in TypeScript)
|
||||
3. Implement production adapter AND test adapter
|
||||
|
||||
**Example (Go):**
|
||||
|
||||
```Go
|
||||
|
||||
// Contract
|
||||
type UserStore interface {
|
||||
Create(ctx context.Context, user User) error
|
||||
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
}
|
||||
|
||||
// Production adapter
|
||||
type PostgresUserStore struct { /* ... */ }
|
||||
|
||||
// Test adapter
|
||||
type MockUserStore struct { /* ... */ }
|
||||
```
|
||||
|
||||
**Example (TypeScript/Vue):**
|
||||
```typescript
|
||||
|
||||
// Contract (service layer)
|
||||
export interface TaskAPI {
|
||||
createTask(title: string): Promise<Task>;
|
||||
getTasks(): Promise<Task[]>;
|
||||
}
|
||||
|
||||
// Production adapter
|
||||
export class BackendTaskAPI implements TaskAPI { /* ... */ }
|
||||
|
||||
// Test adapter (vi.mock or manual)
|
||||
export class MockTaskAPI implements TaskAPI { /* ... */ }
|
||||
|
||||
```
|
||||
|
||||
#### Rule 2: Pure Business Logic
|
||||
**Problem:** Business rules mixed with I/O are impossible to test without infrastructure.
|
||||
|
||||
**Solution:** Extract calculations, validations, transformations into pure functions:
|
||||
- Input → Output, no side effects
|
||||
- Deterministic: same input = same output
|
||||
- No I/O inside business rules
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
|
||||
// ✅ Pure function - easy to test
|
||||
func calculateDiscount(items []Item, coupon Coupon) (float64, error) {
|
||||
// Pure calculation, returns value
|
||||
}
|
||||
|
||||
// ❌ Impure - database call inside
|
||||
func calculateDiscount(ctx context.Context, items []Item, coupon Coupon) (float64, error) {
|
||||
validCoupon, err := db.GetCoupon(ctx, coupon.ID) // NO!
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Correct approach:**
|
||||
```
|
||||
|
||||
// 1. Fetch dependencies first (in handler/service)
|
||||
validCoupon, err := store.GetCoupon(ctx, coupon.ID)
|
||||
|
||||
// 2. Pass to pure logic
|
||||
discount, err := calculateDiscount(items, validCoupon)
|
||||
|
||||
// 3. Persist result
|
||||
err = store.SaveOrder(ctx, order)
|
||||
|
||||
```
|
||||
|
||||
#### Rule 3: Dependency Direction
|
||||
**Principle:** Dependencies point inward toward business logic.
|
||||
|
||||
```
|
||||
|
||||
┌──────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ (DB, HTTP, Files, External APIs) │
|
||||
│ │
|
||||
│ Depends on ↓ │
|
||||
└──────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────┐
|
||||
│ Contracts/Interfaces Layer │
|
||||
│ (Abstract ports - no implementation)│
|
||||
│ │
|
||||
│ Depends on ↓ │
|
||||
└──────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────┐
|
||||
│ Business Logic Layer │
|
||||
│ (Pure functions, domain rules) │
|
||||
│ NO dependencies on infrastructure │
|
||||
└──────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
**Never:**
|
||||
- Business logic imports database driver
|
||||
- Domain entities import HTTP framework
|
||||
- Core calculations import config files
|
||||
|
||||
**Always:**
|
||||
- Infrastructure implements interfaces defined by business layer
|
||||
- Business logic receives dependencies via injection
|
||||
|
||||
### Pattern Discovery Protocol
|
||||
|
||||
**Before implementing ANY feature:**
|
||||
|
||||
1. **Search existing patterns** (MANDATORY):
|
||||
Search the codebase for symbols named: `Interface`, `Repository`, `Service`, `Store`, `Mock`
|
||||
(use your available codebase search tool — Pathfinder, grep, IDE symbol search, etc.)
|
||||
|
||||
2. **Examine 3 existing modules** for consistency:
|
||||
- How do they handle database access?
|
||||
- Where are pure functions vs I/O operations?
|
||||
- What testing patterns exist?
|
||||
|
||||
3. **Document pattern** (>80% consistency required):
|
||||
- "Following pattern from [task, user, auth] modules"
|
||||
- "X/Y modules use interface-based stores"
|
||||
- "All tests use [MockStore, vi.mock, TestingPinia] pattern"
|
||||
|
||||
4. **If consistency <80%**: STOP and report fragmentation to human.
|
||||
|
||||
### Testability Compliance
|
||||
|
||||
These are **architectural** requirements that the code structure must satisfy — not testing mechanics. A design that cannot meet them is not compliant with this architecture.
|
||||
|
||||
**Unit testability (non-negotiable):**
|
||||
- Unit tests MUST run without starting any database, external service, or network call
|
||||
- All I/O dependencies MUST be abstractable to the point where a mock can replace them
|
||||
- Business logic MUST be exercisable in isolation from infrastructure (enforced by Rules 1 & 2)
|
||||
|
||||
**Integration testability:**
|
||||
- Every I/O adapter MUST be independently testable against real infrastructure
|
||||
- Adapters must be replaceable — the application must not hard-wire a specific implementation
|
||||
|
||||
**Test co-location (structural rule):**
|
||||
- Default: Unit and integration tests co-locate with the implementation they test (test lives next to the code)
|
||||
- E2E tests: isolated in a dedicated directory — `e2e/` at the project root for single-app projects, or `apps/e2e/` for monorepos. They cross feature boundaries and belong to none. See Testing Strategy @testing-strategy.md for the complete E2E directory layout.
|
||||
- **Language overrides:** Some ecosystems have different conventions (e.g., Flutter uses a mirrored `test/` directory, Rust uses inline `#[cfg(test)]` blocks). When a language-specific project structure file exists, its test location rules take precedence over this default. See the relevant `project-structure-*` file for authoritative guidance.
|
||||
|
||||
> For test pyramid proportions, naming conventions, tool choices, and language-specific tooling, see Testing Strategy @testing-strategy.md.
|
||||
|
||||
### Language-Specific Idioms
|
||||
|
||||
For language-specific abstraction patterns and test tooling, see the dedicated idiom files listed in `code-idioms-and-conventions.md`. Those files contain the authoritative guidance per ecosystem and are independently maintainable.
|
||||
|
||||
**Universal rule:** In every language, the testability pattern is the same — I/O is behind an interface, business logic is pure, and tests inject a mock/fake implementation. The idiom files describe *how* to express this pattern in each ecosystem's syntax.
|
||||
|
||||
### Enforcement Checklist
|
||||
|
||||
Before marking code complete, verify:
|
||||
- [ ] Can I run unit tests without starting database/external services?
|
||||
- [ ] Are all I/O operations behind an abstraction?
|
||||
- [ ] Is business logic pure (no side effects)?
|
||||
- [ ] Do integration tests exist for all adapters?
|
||||
- [ ] Does pattern match existing codebase (>80% consistency)?
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
- Project Structure @project-structure.md
|
||||
- Database Design Principles @database-design-principles.md
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: >
|
||||
When configuring Kubernetes deployments, GitOps infrastructure, or deployment
|
||||
strategies (blue-green, canary, rolling). Load this file in addition to
|
||||
ci-cd-principles.md when the tech stack uses Kubernetes or a managed container
|
||||
platform with orchestration (GKE, EKS, AKS).
|
||||
---
|
||||
|
||||
## CI/CD — GitOps and Kubernetes Deployment
|
||||
|
||||
> **Agent scope:** This is a supplement to `ci-cd-principles.md` (Level 2).
|
||||
> Apply only when the project targets Kubernetes or a Kubernetes-based managed platform.
|
||||
> Do not apply rolling/canary/blue-green patterns to Docker Compose or serverless deployments.
|
||||
|
||||
---
|
||||
|
||||
### Choosing a Deployment Strategy
|
||||
|
||||
| Strategy | When to use | Trade-off |
|
||||
|----------|-------------|-----------|
|
||||
| **Rolling** | Default for most services; SLO requirements | Simple, but mixes versions briefly |
|
||||
| **Blue-Green** | Zero-downtime with instant rollback | Doubles infrastructure cost during switch |
|
||||
| **Canary** | Risk-reducing incremental rollout; A/B variants | Requires traffic splitting capability |
|
||||
|
||||
---
|
||||
|
||||
### Rolling Deployment (Default)
|
||||
|
||||
Kubernetes native. Suitable for most stateless services.
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25% # Max pods above desired count during update
|
||||
maxUnavailable: 0% # Zero-downtime: don't remove pods before new ones ready
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Set `maxUnavailable: 0` for production services with SLO requirements
|
||||
- Set `minReadySeconds` to let pods stabilize before proceeding
|
||||
- Configure `terminationGracePeriodSeconds` to finish in-flight requests before shutdown
|
||||
|
||||
---
|
||||
|
||||
### Blue-Green Deployment
|
||||
|
||||
**When:** Zero-downtime deployments where rollback must be instant and clean.
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Blue (v1) │ │ Green (v2) │
|
||||
│ [LIVE 100%] │────→│ [STANDBY] │
|
||||
└──────────────┘ └──────────────┘
|
||||
↕ Switch load balancer
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ Blue (v1) │ │ Green (v2) │
|
||||
│ [STANDBY] │ │ [LIVE 100%] │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Both environments must be identical in infrastructure
|
||||
- Run smoke tests against green before switching traffic
|
||||
- Keep blue alive for at least 30 minutes post-switch (fast rollback window)
|
||||
- Database migrations must be backward-compatible (blue still runs against the same DB)
|
||||
|
||||
---
|
||||
|
||||
### Canary Deployment
|
||||
|
||||
**When:** Risk-reducing incremental rollout; A/B testing deployment variants.
|
||||
|
||||
```
|
||||
Traffic split during rollout:
|
||||
5% → canary (v2)
|
||||
95% → stable (v1)
|
||||
↓ metrics look good
|
||||
25% → canary
|
||||
75% → stable
|
||||
↓ bake time passes
|
||||
100% → canary (now stable)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Define success metrics before starting rollout (error rate, latency SLO)
|
||||
- Set automatic rollback threshold: if canary error rate > 2× baseline → auto-rollback
|
||||
- Minimum bake time per traffic increment: 15–30 minutes
|
||||
- Feature flags complement canary routing for functional (not just traffic) testing
|
||||
|
||||
---
|
||||
|
||||
### GitOps Pattern
|
||||
|
||||
For Kubernetes environments, use **declarative GitOps** instead of imperative `kubectl apply`.
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```
|
||||
Application Repo (code) → CI builds image → updates tag in Config Repo
|
||||
Config Repo (K8s manifests) → ArgoCD/Flux syncs to cluster automatically
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Git is the **single source of truth** for cluster state
|
||||
- All changes to production go through PRs on the config repo — no direct `kubectl` in prod
|
||||
- ArgoCD/Flux continuously reconciles — any manual drift is auto-corrected
|
||||
- Secrets reference external secret stores — **never plaintext in git**
|
||||
|
||||
#### Example ArgoCD Application
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: myapp-production
|
||||
spec:
|
||||
source:
|
||||
repoURL: https://github.com/org/config-repo
|
||||
path: environments/production/myapp
|
||||
targetRevision: HEAD
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: production
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true # Remove resources deleted from git
|
||||
selfHeal: true # Reconcile manual drift
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Secrets Management in Kubernetes
|
||||
|
||||
Never store plaintext secrets in git — even in private repositories.
|
||||
|
||||
| Tool | Pattern |
|
||||
|------|---------|
|
||||
| **External Secrets Operator** | Syncs secrets from AWS Secrets Manager, GCP Secret Manager, Vault into K8s Secrets |
|
||||
| **Sealed Secrets** | Encrypts secrets with a cluster-held key; safe to commit encrypted form |
|
||||
| **Vault Agent Injector** | Sidecars inject secrets directly into pods at runtime |
|
||||
|
||||
Choose based on your cloud provider or existing tooling. Document the choice in your
|
||||
technical architecture document.
|
||||
|
||||
---
|
||||
|
||||
### Kubernetes CI/CD Checklist
|
||||
|
||||
- [ ] Deployment strategy defined and documented (rolling / blue-green / canary)?
|
||||
- [ ] `maxUnavailable: 0` set for production services with SLO?
|
||||
- [ ] `terminationGracePeriodSeconds` configured for graceful shutdown?
|
||||
- [ ] GitOps in place — no direct `kubectl apply` commands in production CI pipeline?
|
||||
- [ ] Config repo exists, separate from application repo?
|
||||
- [ ] All PRs to config repo require review before ArgoCD/Flux sync?
|
||||
- [ ] Secrets stored in external secret store, not plaintext in git?
|
||||
- [ ] Rollback procedure tested and documented?
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
|
||||
- CI/CD Principles @ci-cd-principles.md (core pipeline — read first)
|
||||
- Security Principles @security-principles.md (secrets management)
|
||||
- Monitoring and Alerting Principles @monitoring-and-alerting-principles.md (canary success metrics)
|
||||
- Feature Flags @feature-flags-principles.md (canary + feature flag interaction)
|
||||
@@ -0,0 +1,348 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When configuring CI/CD pipelines, deployment processes, release strategies, or build configurations
|
||||
---
|
||||
|
||||
## CI/CD Principles
|
||||
|
||||
> **Agent scope:** This rule applies when writing CI/CD manifests
|
||||
> (Dockerfile, docker-compose, GitHub Actions, GitLab CI, etc.).
|
||||
> It is layered by deployment complexity — apply only the levels relevant to the project.
|
||||
|
||||
---
|
||||
|
||||
### Deployment Complexity Levels
|
||||
|
||||
| Level | Applies When | Key Additions |
|
||||
|-------|-------------|---------------|
|
||||
| **0 — All projects** | Always | Lint, test, security scan, secrets management |
|
||||
| **1 — Containerized** | Docker image is the artifact | Multi-stage build, image scan, SBOM attestation |
|
||||
| **2 — Orchestrated** | Kubernetes or managed container platform | Deployment strategies, GitOps |
|
||||
|
||||
Load supplementary rules when reaching Level 2:
|
||||
- Deployment strategies + GitOps → **@ci-cd-gitops-kubernetes.md**
|
||||
|
||||
---
|
||||
|
||||
### Level 0 — Universal Pipeline Design
|
||||
|
||||
**Pipeline Stages (in order):**
|
||||
|
||||
1. **Lint** — static analysis, formatting checks
|
||||
2. **Build** — compile, bundle, generate artifacts
|
||||
3. **Unit Test** — fast tests with mocked dependencies
|
||||
4. **Integration Test** — tests against real dependencies (Testcontainers)
|
||||
5. **Security Scan** — dependency audit, SAST, secrets detection
|
||||
6. **Deploy** — push to target environment
|
||||
|
||||
**Rules:**
|
||||
|
||||
- **Fail fast** — run cheapest checks first (lint before build, build before test)
|
||||
- **Pipeline must be deterministic** — same input = same output, every time
|
||||
- **Keep pipelines under 15 minutes** — optimize slow stages
|
||||
- **Never skip failing steps** — fix the pipeline, don't bypass it
|
||||
- **Build once, deploy many** — same artifact promotes through all environments
|
||||
|
||||
---
|
||||
|
||||
### Level 0 — Deploy Target Examples
|
||||
|
||||
The deploy stage varies by target. The pipeline stages before it are identical.
|
||||
|
||||
**Docker Compose (local / staging):**
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
**Cloud Run:**
|
||||
```bash
|
||||
gcloud run deploy myapp \
|
||||
--image gcr.io/project/myapp:$GIT_SHA \
|
||||
--region us-central1
|
||||
```
|
||||
|
||||
**Vercel (frontend SPA):**
|
||||
```bash
|
||||
vercel deploy --prod
|
||||
```
|
||||
|
||||
**Kubernetes:**
|
||||
Use GitOps — see **@ci-cd-gitops-kubernetes.md**.
|
||||
|
||||
---
|
||||
|
||||
### Level 0 — Manifest Patterns
|
||||
|
||||
#### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod # Pin via go.mod
|
||||
cache: true # Cache dependencies
|
||||
- run: gofumpt -l -e -d .
|
||||
- run: go vet ./...
|
||||
- run: staticcheck ./...
|
||||
|
||||
test:
|
||||
needs: lint # Fail fast: lint before test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- run: go test -race -cover ./...
|
||||
|
||||
security:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Scan for secrets
|
||||
uses: trufflesecurity/trufflehog@v3 # Pin to release tag
|
||||
- name: Audit dependencies
|
||||
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Pin action versions (`@v4`, not `@latest` or `@main`)
|
||||
- Use `needs:` to enforce stage ordering
|
||||
- Cache dependencies (`cache: true` in setup actions)
|
||||
- Use `go-version-file` / `node-version-file` instead of hardcoding versions
|
||||
- Never put secrets in workflow files — use `${{ secrets.NAME }}`
|
||||
|
||||
---
|
||||
|
||||
### Level 1 — Containerized Projects
|
||||
|
||||
#### Dockerfile (Multi-Stage Build)
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Build
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download # Cache dependencies
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /bin/api ./cmd/api
|
||||
|
||||
# Stage 2: Runtime (minimal image)
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
COPY --from=builder /bin/api /bin/api
|
||||
EXPOSE 8080
|
||||
CMD ["/bin/api"]
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Always use multi-stage builds (build → runtime)
|
||||
- Pin base image versions (never use `:latest`)
|
||||
- Copy dependency files first, then source (layer caching)
|
||||
- Use minimal runtime images (distroless, alpine, scratch)
|
||||
- Never copy `.env`, secrets, or `.git` into images
|
||||
|
||||
#### Docker Compose (Local Development)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./apps/backend # Path per project-structure.md
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env_file: .env # Environment config
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine # Pin versions
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Always define health checks for dependencies
|
||||
- Use `depends_on` with `condition: service_healthy`
|
||||
- Pin all image versions
|
||||
- Use volumes for persistent data
|
||||
- Never hardcode credentials — use env_file or environment variables
|
||||
|
||||
#### Image Scan + SBOM Attestation
|
||||
|
||||
After building and pushing a container image, scan it and attach a signed SBOM attestation.
|
||||
|
||||
**Preferred approach: Cosign keyless signing (no key management required)**
|
||||
|
||||
Cosign integrates with your CI provider's OIDC token (GitHub Actions, GitLab CI) to sign
|
||||
images and attestations without storing or rotating cryptographic keys. The signature is
|
||||
anchored to a transparency log (Rekor), making it auditable and policy-enforceable.
|
||||
|
||||
```yaml
|
||||
build:
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # Required for Cosign keyless signing
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
- name: Build and push image
|
||||
id: build
|
||||
run: |
|
||||
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
|
||||
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
|
||||
- name: Scan container image
|
||||
run: |
|
||||
trivy image \
|
||||
--severity HIGH,CRITICAL \
|
||||
--exit-code 1 \
|
||||
ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
syft ghcr.io/${{ github.repository }}:${{ github.sha }} \
|
||||
-o cyclonedx-json > sbom.json
|
||||
|
||||
- name: Attest SBOM to image (keyless, OIDC-backed)
|
||||
run: |
|
||||
cosign attest \
|
||||
--predicate sbom.json \
|
||||
--type cyclonedx \
|
||||
ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||
# Cosign uses the GitHub Actions OIDC token automatically.
|
||||
# No COSIGN_PASSWORD or secret keys required.
|
||||
```
|
||||
|
||||
**How the SBOM travels with the image:**
|
||||
|
||||
The SBOM attestation is stored as an OCI reference in the same container registry alongside
|
||||
the image digest. It requires no additional infrastructure — any registry that supports OCI
|
||||
artifacts (ghcr.io, Google Artifact Registry, AWS ECR, Docker Hub) works out of the box.
|
||||
|
||||
To verify the attestation at any time:
|
||||
```bash
|
||||
cosign verify-attestation \
|
||||
--type cyclonedx \
|
||||
ghcr.io/org/app@sha256:<digest>
|
||||
```
|
||||
|
||||
**Use ORAS instead of Cosign when:**
|
||||
- You need to attach arbitrary supply chain artifacts (scan reports, provenance JSON, build logs)
|
||||
that go beyond what Cosign's attestation model covers.
|
||||
- `oras attach ghcr.io/org/app@sha256:<digest> scan-report.json`
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Prefer Cosign keyless signing — eliminates secret key management overhead
|
||||
- SBOM is attached to the image in the OCI registry — not stored as a CI artifact
|
||||
- Scan BEFORE attesting — the SBOM reflects the scanned image
|
||||
- For non-containerized apps (Vercel, Netlify frontend), use `npm audit`/`yarn audit` instead;
|
||||
no SBOM attachment applies
|
||||
|
||||
---
|
||||
|
||||
### Deployment vs Release (Feature Flags)
|
||||
|
||||
Code deployment and feature release are separate concerns. When the PRD or technical
|
||||
architecture explicitly requires gradual rollout, A/B testing, or kill switches, feature
|
||||
flags can decouple them.
|
||||
|
||||
> **Agent rule:** Do NOT implement feature flags unless explicitly required by the
|
||||
> PRD or technical architecture document. See **@feature-flags-principles.md** for
|
||||
> implementation guidance when they are required.
|
||||
|
||||
---
|
||||
|
||||
### Environment Promotion
|
||||
|
||||
```
|
||||
dev → staging → production
|
||||
```
|
||||
|
||||
- **Dev:** Deployed on every push to feature branch
|
||||
- **Staging:** Deployed on merge to main/develop
|
||||
- **Production:** Deployed via manual approval or automated release
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Same artifacts promote through environments (build once, deploy many)
|
||||
- Environment-specific config via environment variables, not build flags
|
||||
- Never deploy directly to production without staging validation
|
||||
|
||||
---
|
||||
|
||||
### CI/CD Checklist
|
||||
|
||||
**Always (all projects):**
|
||||
- [ ] Pipeline stages run in correct order (lint → build → test → security → deploy)?
|
||||
- [ ] All versions pinned (base images, CI actions, tool versions)?
|
||||
- [ ] Dependency caching enabled?
|
||||
- [ ] No secrets in config files (use env vars or secrets manager)?
|
||||
- [ ] Secret scanning in CI?
|
||||
- [ ] Health checks defined for all service dependencies?
|
||||
- [ ] Pipeline completes in under 15 minutes?
|
||||
|
||||
**If building container images (Level 1):**
|
||||
- [ ] Multi-stage Docker builds used?
|
||||
- [ ] Container image scanned for HIGH/CRITICAL CVEs?
|
||||
- [ ] SBOM generated and attested to image via Cosign (keyless)?
|
||||
|
||||
**If deploying to Kubernetes (Level 2):**
|
||||
- [ ] Deployment strategy defined (blue-green, canary, or rolling)?
|
||||
- [ ] GitOps in place — no direct `kubectl apply` in production?
|
||||
- [ ] Secrets reference external store, not plaintext in git?
|
||||
- See **@ci-cd-gitops-kubernetes.md** for full checklist
|
||||
|
||||
**If feature flags are required by PRD/architecture:**
|
||||
- [ ] Flag infrastructure specified in tech architecture document?
|
||||
- [ ] Every flag has an owner and expiry date?
|
||||
- See **@feature-flags-principles.md** for full checklist
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
|
||||
- Code Completion Mandate @code-completion-mandate.md (validation before ship)
|
||||
- Security Mandate @security-mandate.md (secrets management)
|
||||
- Security Principles @security-principles.md (image scanning, SBOM)
|
||||
- Git Workflow Principles @git-workflow-principles.md (branch strategy)
|
||||
- Project Structure @project-structure.md (service paths)
|
||||
- Testing Strategy @testing-strategy.md (unit and integration test stages)
|
||||
- GitOps + Kubernetes Deployment @ci-cd-gitops-kubernetes.md
|
||||
- Feature Flags @feature-flags-principles.md
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Code Completion Mandate
|
||||
|
||||
### Universal Requirement
|
||||
|
||||
**Before marking any code task as complete, you MUST run automated quality checks and remediate all issues.**
|
||||
|
||||
This is NOT OPTIONAL. Delivering code without validation violates the Rugged Software Constitution @rugged-software-constitution.md.
|
||||
|
||||
### The Completion Checklist
|
||||
|
||||
Every code generation task follows this workflow:
|
||||
|
||||
1. **Generate** - Write the code based on requirements
|
||||
2. **Validate** - Run language-appropriate quality checks (see below)
|
||||
3. **Remediate** - Fix all detected issues
|
||||
4. **Verify** - Re-run checks to confirm fixes
|
||||
5. **Deliver** - Mark task complete only after all checks pass
|
||||
|
||||
**Never skip validation "to save time." Validation IS the work.**
|
||||
|
||||
### Language-Specific Quality Commands
|
||||
|
||||
The authoritative commands for each language live in the corresponding idiom file — this keeps the work of maintaining them close to the language expertise. Load the relevant file to get the exact commands to run:
|
||||
|
||||
| Language | Idiom File | Commands Section |
|
||||
| -------------------- | ---------------------------------- | -------------------------------- |
|
||||
| **Go** | @go-idioms-and-patterns.md | § Formatting and Static Analysis |
|
||||
| **TypeScript / Vue** | @typescript-idioms-and-patterns.md | § Formatting and Static Analysis |
|
||||
| **Vue 3** | @vue-idioms-and-patterns.md | § Linting and Type Checking |
|
||||
| **Flutter / Dart** | @flutter-idioms-and-patterns.md | § Linting and Formatting |
|
||||
| **Rust** | @rust-idioms-and-patterns.md | § Clippy and Formatting |
|
||||
|
||||
### Failure Protocol
|
||||
|
||||
**If any quality check fails:**
|
||||
|
||||
1. Read the error output completely
|
||||
2. Fix the identified issues in the code
|
||||
3. Re-run the failing command
|
||||
4. Do not proceed until all checks pass
|
||||
|
||||
> Never disable a lint rule or suppress a warning to make checks pass. Fix the root cause.
|
||||
|
||||
### Related Principles
|
||||
- Rugged Software Constitution @rugged-software-constitution.md
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Code Idioms and Conventions
|
||||
|
||||
### Universal Principle
|
||||
|
||||
**Write idiomatic code for the target language:**
|
||||
|
||||
- Code should look natural to developers familiar with that language
|
||||
- Follow established community conventions, not personal preferences
|
||||
- Use language built-ins and standard library effectively
|
||||
- Apply language-appropriate patterns (don't force patterns from other languages)
|
||||
|
||||
### Idiomatic Code Characteristics
|
||||
|
||||
- Leverages language features (don't avoid features unnecessarily)
|
||||
- Follows language naming conventions
|
||||
- Uses appropriate error handling for language (exceptions vs Result types)
|
||||
- Applies established community patterns
|
||||
|
||||
### Avoid Cross-Language Anti-Patterns
|
||||
|
||||
- ❌ Don't write "Java in Python" or "C in Go"
|
||||
- ❌ Don't force OOP patterns in functional languages
|
||||
- ❌ Don't avoid language features because they're "unfamiliar"
|
||||
- ✅ Learn and apply language-specific idioms
|
||||
|
||||
### Language-Specific Idioms
|
||||
|
||||
This file defines the universal principle. Each language has a dedicated file with concrete patterns, tooling choices, and idiom-specific rules. **Load the relevant file when working in that language** — it is the authoritative source for that ecosystem.
|
||||
|
||||
| Language / Framework | Idiom File | When to Load |
|
||||
| -------------------- | ---------------------------------- | ----------------------------------------------- |
|
||||
| **Go** | @go-idioms-and-patterns.md | Go services, APIs, CLI tools |
|
||||
| **TypeScript** | @typescript-idioms-and-patterns.md | Any TypeScript project (backend, frontend, CLI) |
|
||||
| **Vue 3** | @vue-idioms-and-patterns.md | Vue components, Pinia stores, composables |
|
||||
| **Flutter / Dart** | @flutter-idioms-and-patterns.md | Mobile apps, Flutter widgets, Riverpod |
|
||||
| **Rust** | @rust-idioms-and-patterns.md | Rust binaries, libraries, workspaces |
|
||||
|
||||
> Each language-specific file is independently changeable. It governs only its own ecosystem's coding idioms and defers to the relevant cross-cutting principle files (error handling, testing, logging, etc.) for universal guidance.
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Code Completion Mandate @code-completion-mandate.md
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Code Organization Principles
|
||||
|
||||
- Generate small, focused functions with clear single purposes (typically 10-50 lines)
|
||||
- Keep cognitive complexity low (cyclomatic complexity < 10 for most functions)
|
||||
- Maintain clear boundaries between different layers (presentation, business logic, data access)
|
||||
- Design for testability from the start, avoiding tight coupling that prevents testing
|
||||
- Apply consistent naming conventions that reveal intent without requiring comments
|
||||
|
||||
### Module Boundaries
|
||||
**Problem:** Cross-module coupling makes changes ripple across codebase.
|
||||
|
||||
**Solution:** Feature-based organization with clear public interfaces:
|
||||
- One feature = one directory
|
||||
- Each module exposes a public API (exported functions/classes)
|
||||
- Internal implementation details are private
|
||||
- Cross-module calls only through public API
|
||||
|
||||
**Directory Structure (Language-Agnostic):**
|
||||
|
||||
> Paths below are illustrative examples following `project-structure.md` — the single source of truth for project layout.
|
||||
> **Note:** The generic filenames below are illustrative. Always use language-specific naming conventions from the relevant `project-structure-*` file (e.g., `storage.go` in Go, `task.api.ts` in TypeScript, `task_repository.dart` in Dart).
|
||||
|
||||
```
|
||||
/task
|
||||
|
||||
- public_api.{ext} # Exported interface
|
||||
- business.{ext} # Pure logic
|
||||
- store.{ext} # I/O abstraction (interface)
|
||||
- postgres.{ext} # I/O implementation
|
||||
- mock.{ext} # Test implementation
|
||||
- test.{ext} # Unit tests (mocked I/O)
|
||||
- integration.test.{ext} # Integration tests (real I/O)
|
||||
```
|
||||
|
||||
**Go Example:**
|
||||
```
|
||||
apps/backend/internal/features/task/
|
||||
|
||||
- service.go # Public API of this feature (Service struct)
|
||||
- handler.go # HTTP handlers
|
||||
- logic.go # Pure domain logic
|
||||
- storage.go # interface Storage
|
||||
- storage_pg.go # implements Storage (PostgreSQL)
|
||||
- storage_mock.go # Mock implementation for testing
|
||||
- logic_test.go # Unit tests with MockStorage
|
||||
- postgres_integration_test.go # Integration test with Testcontainers
|
||||
```
|
||||
|
||||
**Vue Example:**
|
||||
```
|
||||
/apps/frontend/src/features/task
|
||||
|
||||
- index.ts # Public exports
|
||||
- task.service.ts # Business logic
|
||||
- task.api.ts # interface TaskAPI
|
||||
- task.api.backend.ts # implements TaskAPI
|
||||
- task.store.ts # Pinia store (uses TaskAPI)
|
||||
- task.service.spec.ts # Unit tests (mock API)
|
||||
```
|
||||
|
||||
### Feature Interaction Patterns
|
||||
|
||||
**Direct Import**
|
||||
|
||||
When a feature needs another feature's capabilities, import its Service directly:
|
||||
|
||||
```go
|
||||
// In features/order/logic.go
|
||||
import "yourapp/internal/features/task"
|
||||
|
||||
type Logic struct {
|
||||
taskService *task.Service // Direct dependency injection
|
||||
}
|
||||
|
||||
func (l *Logic) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
|
||||
// Use task service directly
|
||||
task, err := l.taskService.GetTask(ctx, req.TaskID)
|
||||
// ... rest of logic
|
||||
}
|
||||
```
|
||||
|
||||
**Rules**
|
||||
|
||||
- Only import Service (public API), never internal files like logic.go or storage.go
|
||||
- Declare dependency in the dependent feature's Service constructor
|
||||
- Wire dependencies in cmd/api/main.go
|
||||
|
||||
**Wiring Example**
|
||||
```go
|
||||
// cmd/api/main.go
|
||||
taskService := task.NewService(taskStorage)
|
||||
orderService := order.NewService(orderStorage, taskService) // Pass task service
|
||||
```
|
||||
|
||||
### Avoid Circular Dependencies
|
||||
|
||||
**Problem:** Module A imports B, B imports A
|
||||
|
||||
- Causes build failures, initialization issues
|
||||
- Indicates poor module boundaries
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Extract shared code to third module
|
||||
- Restructure dependencies (A→C, B→C)
|
||||
- Use dependency injection
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Project Structure @project-structure.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When executing external commands, shell scripts, or system processes
|
||||
---
|
||||
|
||||
## Command Execution Principles
|
||||
|
||||
### Security
|
||||
|
||||
**Never execute user input directly:**
|
||||
|
||||
- ❌ `exec(userInput)`
|
||||
- ❌ `shell("rm " + userFile)`
|
||||
- ✅ Use argument lists, not shell string concatenation
|
||||
- ✅ Validate and sanitize all arguments
|
||||
|
||||
**Run with minimum permissions:**
|
||||
|
||||
- Never run commands as root/admin without explicit human approval. If elevated permissions are absolutely required, STOP and request authorization.
|
||||
- Use least-privilege service accounts
|
||||
|
||||
### Portability
|
||||
|
||||
**Use language standard library:**
|
||||
|
||||
- Avoid shell commands when standard library provides functionality
|
||||
- Example: Use file I/O APIs instead of `cat`, `cp`, `mv`
|
||||
|
||||
**Test on all target OS:**
|
||||
|
||||
- Windows, Linux, macOS have different commands and behaviors
|
||||
- Use path joining functions (don't concatenate with /)
|
||||
|
||||
### Error Handling
|
||||
|
||||
**Check exit codes:**
|
||||
|
||||
- Non-zero exit code = failure
|
||||
- Capture and log stderr
|
||||
- Set timeouts for long-running commands
|
||||
- Handle "command not found" gracefully
|
||||
|
||||
### Command Execution Checklist
|
||||
|
||||
- [ ] Is user input sanitized/validated before use in commands?
|
||||
- [ ] Are arguments passed as lists (not shell string concatenation)?
|
||||
- [ ] Are commands running with minimum necessary permissions?
|
||||
- [ ] Are exit codes checked and errors handled?
|
||||
- [ ] Are timeouts set for long-running commands?
|
||||
- [ ] Is stderr captured and logged?
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Concurrency and Threading Mandate
|
||||
|
||||
### When to Use Concurrency
|
||||
|
||||
**I/O-Bound Operations:** Use asynchronous I/O, event-driven concurrency, or coroutines when waiting on network requests, file I/O, or database queries.
|
||||
|
||||
**CPU-Bound Operations:** Use OS threads or thread pools for heavy computation, data processing, or encoding work where CPU cycles dominate.
|
||||
|
||||
**Don't Over-Use Concurrency:** Concurrency adds significant complexity (race conditions, deadlocks, debugging difficulty). Profile first — only add concurrency when there is a measurable performance benefit.
|
||||
|
||||
For implementation details (race condition prevention, deadlock avoidance, message passing patterns), see Concurrency and Threading Principles @concurrency-and-threading-principles.md.
|
||||
|
||||
### When NOT to Use Concurrency
|
||||
- Simple synchronous operations
|
||||
- No measurable performance benefit
|
||||
- Avoid premature optimization
|
||||
|
||||
### Related Principles
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Performance Optimization Principles @performance-optimization-principles.md
|
||||
- Rust Idioms and Patterns @rust-idioms-and-patterns.md
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing concurrent, parallel, or multi-threaded code (async/await, threads, goroutines, actors)
|
||||
---
|
||||
|
||||
## Concurrency and Threading Principles
|
||||
|
||||
**1. Avoid Race Conditions**
|
||||
|
||||
**What is a race condition:**
|
||||
|
||||
- Multiple threads access shared data concurrently
|
||||
- At least one thread writes/modifies the data
|
||||
- No synchronization mechanism in place
|
||||
- Result depends on unpredictable thread execution timing
|
||||
|
||||
**Prevention strategies:**
|
||||
|
||||
- Synchronization: Locks, mutexes, semaphores
|
||||
- Immutability: Immutable data is thread-safe by default
|
||||
- Message passing: Send data between threads instead of sharing
|
||||
- Thread-local storage: Each thread has its own copy
|
||||
|
||||
**Detection:**
|
||||
|
||||
- Go: Run with `-race` flag (race detector)
|
||||
- Rust: Miri tool for undefined behavior detection
|
||||
- C/C++: ThreadSanitizer (TSan)
|
||||
- Java: JCStress, FindBugs
|
||||
|
||||
**2. Prevent Deadlocks**
|
||||
|
||||
**What is a deadlock:**
|
||||
|
||||
- Two or more threads waiting for each other indefinitely
|
||||
- Example: Thread A holds Lock 1, waits for Lock 2; Thread B holds Lock 2, waits for Lock 1
|
||||
|
||||
**Four conditions (ALL must be true for deadlock):**
|
||||
|
||||
1. Mutual exclusion: Resources held exclusively (locks)
|
||||
2. Hold and wait: Holding one resource while waiting for another
|
||||
3. No preemption: Can't force unlock
|
||||
4. Circular wait: A waits for B, B waits for A
|
||||
|
||||
**Prevention (break any one condition):**
|
||||
|
||||
- Lock ordering: Always acquire locks in same order
|
||||
- Timeout: Use try_lock with timeout, back off and retry
|
||||
- Avoid nested locks: Don't hold multiple locks simultaneously
|
||||
- Use lock-free data structures when possible
|
||||
|
||||
**3. Prefer Immutability**
|
||||
|
||||
- Immutable data = thread-safe by default (no synchronization needed)
|
||||
- Share immutable data freely between threads
|
||||
- Use immutable data structures where possible (Rust default, functional languages)
|
||||
- If data must change, use message passing instead of shared mutable state
|
||||
|
||||
**4. Message Passing Over Shared Memory**
|
||||
|
||||
- "Don't communicate by sharing memory; share memory by communicating" (Go proverb)
|
||||
- Send data through channels/queues instead of accessing shared memory
|
||||
- Reduces need for locks and synchronization
|
||||
- Easier to reason about and test
|
||||
|
||||
**5. Graceful Degradation**
|
||||
|
||||
- Handle concurrency errors gracefully (timeouts, retries, circuit breakers)
|
||||
- Don't crash entire application on one thread failure
|
||||
- Use supervisors/monitors for fault tolerance (Erlang/Elixir actor model)
|
||||
- Implement backpressure for producer-consumer scenarios
|
||||
|
||||
### Concurrency Models by Use Case
|
||||
|
||||
- **I/O-bound:** async/await, event loops, coroutines, green threads
|
||||
- **CPU-bound:** OS threads, thread pools, parallel processing
|
||||
- **Actor model:** Erlang/Elixir actors, Akka (message passing, isolated state)
|
||||
- **CSP (Communicating Sequential Processes):** Go channels, Rust channels
|
||||
|
||||
### Testing Concurrent Code
|
||||
|
||||
- Write unit tests with controlled concurrency (deterministic execution)
|
||||
- Test timeout scenarios and resource exhaustion
|
||||
- Test thread pool full, queue full scenarios
|
||||
|
||||
### Related Principles
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working with application configuration, environment variables, settings files, or secrets management
|
||||
---
|
||||
|
||||
## Configuration Management Principles
|
||||
|
||||
### Separation of Configuration and Code
|
||||
|
||||
**Configuration:**
|
||||
|
||||
- Environment-specific values (URLs, credentials, timeouts)
|
||||
- Changes between dev/staging/prod
|
||||
- Can change without code deployment
|
||||
|
||||
**Code:**
|
||||
|
||||
- Business logic and application behavior
|
||||
- Same across all environments
|
||||
- Requires deployment to change
|
||||
|
||||
**Never hardcode configuration in code:**
|
||||
|
||||
- ❌ `const DB_URL = "postgresql://prod-db:5432/myapp"`
|
||||
- ✅ `const DB_URL = process.env.DATABASE_URL`
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
**Validate at startup:**
|
||||
|
||||
- Check all required configuration is present
|
||||
- Fail fast if required config is missing or invalid
|
||||
- Provide clear error messages for misconfiguration
|
||||
- Example: "DATABASE_URL environment variable is required"
|
||||
|
||||
**Validation checks:**
|
||||
|
||||
- Type (string, number, boolean, enum)
|
||||
- Format (URL, email, file path)
|
||||
- Range (port numbers 1-65535)
|
||||
- Dependencies (if feature X enabled, config Y required)
|
||||
|
||||
### Configuration Hierarchy
|
||||
|
||||
**Precedence (highest to lowest):**
|
||||
|
||||
1. **Command-line arguments:** Override everything (for testing, debugging)
|
||||
2. **Environment variables:** Override config files
|
||||
3. **Config files:** Environment-specific (config.prod.yaml, config.dev.yaml)
|
||||
4. **Defaults:** Reasonable defaults in code (fallback)
|
||||
|
||||
**Example:**
|
||||
|
||||
Database port resolution:
|
||||
|
||||
1. Check CLI arg: --db-port=5433
|
||||
|
||||
2. Check env var: DB_PORT=5432
|
||||
|
||||
3. Check config file: database.port=5432
|
||||
|
||||
4. Use default: 5432
|
||||
|
||||
### Configuration Organization
|
||||
|
||||
Hybrid Approach (config files + .env files): define the structure of configuration in config files (e.g. config/database.yaml) and use .env files to inject the secret values.
|
||||
|
||||
**.env files:** Description: A file dedicated to a specific environment (development) for production these values comes from secrets/environment platfrom or manager not a physical `.env` file on disk. When to Use: Use this only for secrets (API keys, passwords) and a few environment-specific values (like a server IP). These files except `.env.template` should never be committed to version control (git).
|
||||
|
||||
- `.env.template` - Consist of credentials and secrets with blank value (SHOULD commit to git)
|
||||
- `.env.development` - Local development credentials and secrets (SHOULD NOT commit to git)
|
||||
|
||||
**Example `.env.development`:**
|
||||
```
|
||||
DEV_DB_HOST=123.45.67.89
|
||||
DEV_DB_USERNAME=prod_user
|
||||
DEV_DB_PASSWORD=a_very_secure_production_password
|
||||
```
|
||||
|
||||
**Feature files:** Description: Settings are grouped into files based on what they do (database, auth, etc.). This keeps your configuration organized. When to Use: Use this as your primary method for organizing non-secret settings. It’s the best way to keep your configuration clean and scalable as your application grows.
|
||||
|
||||
- `config/database.yaml` - Database settings
|
||||
- `config/redis.yaml` - Cache settings
|
||||
- `config/auth.yaml` - Authentication settings
|
||||
|
||||
**Example `config/database.yaml`:**
|
||||
```
|
||||
default: &default
|
||||
adapter: postgresql
|
||||
pool: 5
|
||||
development:
|
||||
<<: *default
|
||||
host: localhost
|
||||
database: myapp_dev
|
||||
username: <%= ENV['DEV_DB_USERNAME'] %> # Placeholder for a secret
|
||||
password: <%= ENV['DEV_DB_PASSWORD'] %>
|
||||
production:
|
||||
<<: *default
|
||||
host: <%= ENV['PROD_DB_HOST'] %>
|
||||
database: myapp_prod
|
||||
username: <%= ENV['PROD_DB_USERNAME'] %>
|
||||
password: <%= ENV['PROD_DB_PASSWORD'] %>
|
||||
```
|
||||
|
||||
> **Note:** Feature flag configuration is a distinct, PRD-gated concern — not routine application configuration. See Feature Flags Principles @feature-flags-principles.md for when and how feature flags are used.
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Feature Flags Principles @feature-flags-principles.md
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Core Design Principles
|
||||
|
||||
### SOLID Principles
|
||||
|
||||
**Single Responsibility Principle (SRP):**
|
||||
|
||||
- Each class, module, or function should have ONE and ONLY ONE reason to change
|
||||
- Generate focused, cohesive units of functionality
|
||||
- If explaining what something does requires "and", it likely violates SRP
|
||||
|
||||
**Open/Closed Principle (OCP):**
|
||||
|
||||
- Software entities should be open for extension but closed for modification
|
||||
- Design abstractions (interfaces, ports) that allow behavior changes without modifying existing code
|
||||
- Use composition and dependency injection to enable extensibility
|
||||
|
||||
**Liskov Substitution Principle (LSP):**
|
||||
|
||||
- Subtypes must be substitutable for their base types without altering program correctness
|
||||
- Inheritance hierarchies must maintain behavioral consistency
|
||||
- If substituting a subclass breaks functionality, LSP is violated
|
||||
|
||||
**Interface Segregation Principle (ISP):**
|
||||
|
||||
- Clients should not be forced to depend on interfaces they don't use
|
||||
- Create focused, role-specific interfaces rather than monolithic ones
|
||||
- Many small, cohesive interfaces > one large, general-purpose interface
|
||||
|
||||
**Dependency Inversion Principle (DIP):**
|
||||
|
||||
- Depend on abstractions (interfaces/ports), not concretions (implementations/adapters)
|
||||
- High-level modules should not depend on low-level modules; both should depend on abstractions
|
||||
- Core principle enabling Testability-First architecture
|
||||
|
||||
### Essential Design Practices
|
||||
|
||||
**DRY (Don't Repeat Yourself):**
|
||||
|
||||
- Eliminate code duplication through proper abstraction, shared utilities, composable functions
|
||||
- Each piece of knowledge should have single, authoritative representation
|
||||
- Don't duplicate logic, algorithms, or business rules
|
||||
|
||||
**YAGNI (You Aren't Gonna Need It):**
|
||||
|
||||
**CRITICAL:** Code maintainability always prevail
|
||||
|
||||
- Avoid implementing functionality before it's actually required
|
||||
- Don't add features based on speculation about future needs
|
||||
- Build for today's requirements, refactor when needs change
|
||||
|
||||
**KISS (Keep It Simple, Stupid):**
|
||||
|
||||
**CRITICAL:** Code maintainability always prevail
|
||||
|
||||
- Prefer simple(simple to maintain), straightforward solutions over complex, clever ones
|
||||
- Complexity should be justified by actual requirements, not theoretical flexibility
|
||||
- Simple code is easier to test, maintain, and debug
|
||||
|
||||
**Separation of Concerns:**
|
||||
|
||||
- Divide program functionality into distinct sections with minimal overlap
|
||||
- Each concern should be isolated in its own module or layer
|
||||
|
||||
**Composition Over Inheritance:**
|
||||
|
||||
- Favor object composition and delegation over class inheritance for code reuse
|
||||
- Composition is more flexible and easier to test
|
||||
- Use interfaces/traits for polymorphism instead of deep inheritance hierarchies
|
||||
|
||||
**Principle of Least Astonishment:**
|
||||
|
||||
- Code should behave in ways that users and maintainers naturally expect
|
||||
- Avoid surprising or counterintuitive behavior
|
||||
- Follow established conventions and patterns
|
||||
|
||||
**User Experience vs Maintainability:**
|
||||
|
||||
- Both user experience AND code maintainability matter
|
||||
- When they conflict, **prefer maintainable code** that can evolve
|
||||
- Poor UX from clean code can be fixed; poor code from UX pressure becomes tech debt
|
||||
- Maintainability enables future UX improvements
|
||||
- Never sacrifice code quality for short-term UX gains
|
||||
|
||||
### Related Principles
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
- Documentation Principles @documentation-principles.md
|
||||
- Accessibility Principles @accessibility-principles.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When serializing, deserializing, or validating data formats (JSON, XML, YAML, Protocol Buffers, MessagePack, etc.)
|
||||
---
|
||||
|
||||
## Data Serialization and Interchange Principles
|
||||
|
||||
### Validate at System Boundaries
|
||||
|
||||
**All data entering system must be validated:**
|
||||
|
||||
- API requests, file uploads, message queue messages
|
||||
- Validate type, format, range, required fields
|
||||
- Fail fast on invalid data (don't process partially valid data)
|
||||
- Return clear validation errors to client
|
||||
|
||||
### Handle Encoding Explicitly
|
||||
|
||||
**Default to UTF-8:**
|
||||
|
||||
- UTF-8 for all text data (API responses, file contents, database strings)
|
||||
- Specify encoding explicitly when reading/writing files
|
||||
- Handle encoding errors gracefully (replacement characters or error)
|
||||
|
||||
**Encoding errors:**
|
||||
|
||||
- Invalid UTF-8 sequences (malformed bytes)
|
||||
- Mixing encodings (reading UTF-8 as ISO-8859-1)
|
||||
- Always validate and normalize encoding
|
||||
|
||||
### Serialization Format Selection
|
||||
|
||||
**JSON:**
|
||||
|
||||
- Human-readable, widely supported, language-agnostic
|
||||
- Good for: APIs, configuration files, web applications
|
||||
- Limitations: No binary support, larger size than binary formats
|
||||
|
||||
**Protocol Buffers:**
|
||||
|
||||
- Compact binary format, fast serialization/deserialization
|
||||
- Schema evolution (backward/forward compatibility)
|
||||
- Good for: Internal microservices, high-throughput systems
|
||||
- Limitations: Not human-readable, requires schema definition
|
||||
|
||||
**MessagePack:**
|
||||
|
||||
- Binary JSON-like format, faster and more compact than JSON
|
||||
- Good for: Internal APIs, when JSON too slow but readability still desired
|
||||
- Limitations: Less widely supported than JSON
|
||||
|
||||
**XML:**
|
||||
|
||||
- Verbose, legacy systems, document-oriented
|
||||
- Good for: Enterprise systems, SOAP APIs, RSS/Atom feeds
|
||||
- Limitations: Verbosity, complexity, security issues (XXE attacks)
|
||||
|
||||
**YAML:**
|
||||
|
||||
- Human-friendly, good for configuration files
|
||||
- Good for: Config files, Infrastructure as Code (Kubernetes, CI/CD)
|
||||
- Limitations: Complex parsing, performance, security issues (arbitrary code execution)
|
||||
|
||||
### Security Considerations
|
||||
|
||||
**Validate before deserialization:**
|
||||
|
||||
- Prevent deserialization attacks (arbitrary code execution)
|
||||
- Set size limits on payloads (prevent memory exhaustion)
|
||||
- Whitelist allowed types/classes for deserialization
|
||||
|
||||
**Disable dangerous features:**
|
||||
|
||||
- XML: Disable external entity processing (XXE prevention)
|
||||
- YAML: Disable unsafe constructors
|
||||
- Python pickle: Never deserialize untrusted data
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When designing database schemas, writing migrations, creating queries, or working with transaction boundaries
|
||||
---
|
||||
|
||||
## Database Design Principles
|
||||
|
||||
### Schema Design
|
||||
|
||||
**Normalization:**
|
||||
- Start with 3NF (Third Normal Form) and denormalize only when measured performance requires it
|
||||
- Each table should represent a single entity
|
||||
- Avoid storing derived/calculated data unless caching is explicitly needed
|
||||
|
||||
**Naming Conventions:**
|
||||
- Tables: plural, snake_case (e.g., `users`, `task_assignments`)
|
||||
- Columns: singular, snake_case (e.g., `created_at`, `user_id`)
|
||||
- Foreign keys: `{referenced_table_singular}_id` (e.g., `user_id`, `task_id`)
|
||||
- Indexes: `idx_{table}_{columns}` (e.g., `idx_users_email`)
|
||||
- Constraints: `{type}_{table}_{columns}` (e.g., `uq_users_email`, `fk_tasks_user_id`)
|
||||
|
||||
**Required columns for all tables:**
|
||||
- `id` — primary key (UUID preferred, auto-increment acceptable)
|
||||
- `created_at` — timestamp, set on insert, never updated
|
||||
- `updated_at` — timestamp, updated on every modification
|
||||
|
||||
### Migrations
|
||||
|
||||
**Safety Rules:**
|
||||
- **Never drop columns in production** without a deprecation period
|
||||
- **Never rename columns directly** — add new, migrate data, drop old
|
||||
- **Always make migrations reversible** — include both up and down
|
||||
- **Test migrations on a copy of production data** before applying
|
||||
|
||||
**Migration Strategy:**
|
||||
1. Additive changes first (add column, add table)
|
||||
2. Backfill data
|
||||
3. Update application code
|
||||
4. Remove old columns/tables in a future migration
|
||||
|
||||
### Queries
|
||||
|
||||
**Performance:**
|
||||
- **Always use parameterized queries** (never string concatenation)
|
||||
- **Index columns** used in WHERE, JOIN, and ORDER BY clauses
|
||||
- **Avoid SELECT \*** — specify only needed columns
|
||||
- **Watch for N+1 queries** — use JOINs or batch loading
|
||||
- **Set query timeouts** to prevent long-running queries from blocking
|
||||
|
||||
**Transactions:**
|
||||
- Use transactions for operations that modify multiple rows/tables
|
||||
- Keep transactions as short as possible
|
||||
- Handle deadlocks with retry logic
|
||||
- Never hold transactions open during user interaction or external API calls
|
||||
|
||||
### Database Design Checklist
|
||||
|
||||
- [ ] Schema follows naming conventions?
|
||||
- [ ] All tables have `id`, `created_at`, `updated_at`?
|
||||
- [ ] Foreign keys have proper constraints and indexes?
|
||||
- [ ] Queries are parameterized (no SQL injection)?
|
||||
- [ ] Indexes exist for frequent query patterns?
|
||||
- [ ] Migrations are reversible?
|
||||
- [ ] Transactions are short and focused?
|
||||
- [ ] N+1 queries avoided?
|
||||
|
||||
### Related Principles
|
||||
- Security Principles @security-principles.md (SQL injection prevention)
|
||||
- Performance Optimization Principles @performance-optimization-principles.md (query performance)
|
||||
- Error Handling Principles @error-handling-principles.md (transaction error handling)
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When managing project dependencies, configuring package files, or organizing module imports
|
||||
---
|
||||
|
||||
## Dependency Management Principles
|
||||
|
||||
### Version Pinning
|
||||
|
||||
**Production:** Pin exact versions (1.2.3, not ^1.2.0)
|
||||
|
||||
- Prevents supply chain attacks
|
||||
- Prevents unexpected breakage from patch updates
|
||||
- Ensures reproducible builds
|
||||
|
||||
**Use lock files:**
|
||||
|
||||
- `package-lock.json` (Node.js / npm)
|
||||
- `pnpm-lock.yaml` (Node.js / pnpm)
|
||||
- `yarn.lock` (Node.js / yarn)
|
||||
- `Cargo.lock` (Rust)
|
||||
- `go.sum` (Go)
|
||||
- `pubspec.lock` (Flutter / Dart)
|
||||
- `requirements.txt` / `poetry.lock` (Python)
|
||||
|
||||
### Minimize Dependencies
|
||||
|
||||
**Every dependency is a liability:**
|
||||
|
||||
- Potential security vulnerability
|
||||
- Increased build time and artifact size
|
||||
- Maintenance burden (updates, compatibility)
|
||||
|
||||
**Ask before adding dependency:**
|
||||
|
||||
- "Can I implement this in 50 lines?"
|
||||
- "Is this functionality critical?"
|
||||
- "Is this dependency actively maintained?"
|
||||
- "Is this the latest stable version?"
|
||||
|
||||
### Organize Imports
|
||||
|
||||
**Grouping:**
|
||||
|
||||
1. Standard library
|
||||
2. External dependencies
|
||||
3. Internal modules
|
||||
|
||||
**Sorting:** Alphabetical within groups
|
||||
|
||||
**Cleanup:** Remove unused imports (use linter/formatter)
|
||||
|
||||
### Dependency Management Checklist
|
||||
|
||||
- [ ] Are production dependencies pinned to exact versions?
|
||||
- [ ] Is the lock file committed to version control?
|
||||
- [ ] Can each dependency be justified (not implementable in <50 lines)?
|
||||
- [ ] Are all dependencies actively maintained and on latest stable versions?
|
||||
- [ ] Are imports organized by group (stdlib → external → internal)?
|
||||
- [ ] Are unused imports removed?
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Documentation Principles
|
||||
|
||||
### Self-Documenting Code
|
||||
|
||||
**Clear naming reduces need for comments:**
|
||||
|
||||
- Code shows WHAT is happening
|
||||
- Comments explain WHY it's done this way
|
||||
|
||||
**When to comment:**
|
||||
|
||||
- Complex business logic deserves explanation
|
||||
- Non-obvious algorithms (explain approach)
|
||||
- Workarounds for bugs (link to issue tracker)
|
||||
- Performance optimizations (explain trade-offs)
|
||||
|
||||
### Documentation Levels
|
||||
|
||||
1. **Inline comments:** Explain WHY for complex code
|
||||
2. **Function/method docs:** API contract (parameters, returns, errors)
|
||||
3. **Module/package docs:** High-level purpose and usage
|
||||
4. **README:** Setup, usage, examples
|
||||
5. **Architecture docs:** System design, component interactions, use valid mermaid diagram
|
||||
|
||||
### Related Principles
|
||||
- Core Design Principles @core-design-principles.md
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing error handling, working with error types, or designing error recovery strategies (validation errors, business rule violations, infrastructure failures, resource cleanup)
|
||||
---
|
||||
|
||||
## Error Handling Principles
|
||||
|
||||
**1. Never Fail Silently:**
|
||||
|
||||
- All errors must be handled explicitly (no empty catch blocks)
|
||||
- If you catch an error, do something with it (log, return, transform, retry)
|
||||
|
||||
**2. Fail Fast:**
|
||||
|
||||
- Detect and report errors as early as possible
|
||||
- Validate at system boundaries before processing
|
||||
- Don't process invalid data hoping it'll work out
|
||||
|
||||
**3. Provide Context:**
|
||||
|
||||
- Include error codes, correlation IDs, actionable messages
|
||||
- Enough information for debugging without exposing sensitive details
|
||||
- Example: "Database query failed (correlation-id: abc-123)" not "SELECT * FROM users WHERE..."
|
||||
|
||||
**4. Separate Concerns:**
|
||||
|
||||
- Different handlers for different error types
|
||||
- Business errors ≠ technical errors ≠ security errors
|
||||
|
||||
**5. Resource Cleanup:**
|
||||
|
||||
- Always clean up in error scenarios (close files, release connections, unlock resources)
|
||||
- Use language-appropriate patterns (defer, finally, RAII, context managers)
|
||||
|
||||
**6. No Information Leakage:**
|
||||
|
||||
- Sanitize error messages for external consumption
|
||||
- Don't expose stack traces, SQL queries, file paths, internal structure to users
|
||||
- Log full details internally, show generic message externally
|
||||
|
||||
### Application Error Object (Internal/Log Format)
|
||||
```
|
||||
{
|
||||
"status": "error",
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "User-friendly error message",
|
||||
"correlationId": "uuid-for-tracking",
|
||||
"details": {
|
||||
"field": "email",
|
||||
"issue": "Invalid email format",
|
||||
"provided": "invalid-email",
|
||||
"expected": "valid email format ([email protected])"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling Checklist
|
||||
|
||||
- [ ] Are all error paths explicitly handled (no empty catch blocks)?
|
||||
- [ ] Do errors include correlation IDs for debugging?
|
||||
- [ ] Are sensitive details sanitized before returning to client?
|
||||
- [ ] Are resources cleaned up in all error scenarios?
|
||||
- [ ] Are errors logged at appropriate levels (warn for 4xx, error for 5xx)?
|
||||
- [ ] Are error tests written (negative test cases)?
|
||||
- [ ] Is error handling consistent across application?
|
||||
|
||||
### Related Principles
|
||||
- API Design Principles @api-design-principles.md - API Error Response Format section
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: >
|
||||
ONLY when feature flags are explicitly required by the PRD, technical
|
||||
architecture document, or tech stack specification. Do NOT load this file
|
||||
or introduce feature flags unless the requirement document explicitly calls
|
||||
for gradual rollout, A/B testing, kill switches, or feature entitlement gating.
|
||||
---
|
||||
|
||||
## Feature Flags Principles
|
||||
|
||||
> [!CAUTION]
|
||||
> **Do NOT implement feature flags unless explicitly required.**
|
||||
>
|
||||
> Feature flags add real operational complexity: flag evaluation infrastructure,
|
||||
> lifecycle management, and a multiplicative increase in test permutations.
|
||||
> The cost is non-trivial for solo developers or simple projects.
|
||||
>
|
||||
> Only introduce feature flags when the **PRD** or **technical architecture document**
|
||||
> explicitly specifies one of:
|
||||
> - Gradual / percentage rollout of a risky feature
|
||||
> - A/B testing or experimentation
|
||||
> - Emergency kill switches for production code paths
|
||||
> - Feature entitlement gating by user tier or permission
|
||||
>
|
||||
> If none of these are specified, deploy code directly. Do not introduce flags speculatively.
|
||||
|
||||
---
|
||||
|
||||
### When to Use Feature Flags
|
||||
|
||||
Feature flags decouple **deployment** from **release**. Code ships to production but stays
|
||||
dormant until the flag enables it. This is valuable for:
|
||||
|
||||
| Use case | Flag type | Example |
|
||||
|----------|-----------|---------|
|
||||
| Gradual rollout | Release flag | `new-checkout-flow: 0→5→25→100%` |
|
||||
| Emergency kill switch | Ops flag | `use-legacy-payment-provider` |
|
||||
| A/B test / experiment | Experiment flag | `button-color-blue-vs-green` |
|
||||
| Tier / permission gating | Permission flag | `pro-tier-analytics` |
|
||||
|
||||
---
|
||||
|
||||
### Infrastructure Requirements
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The flag evaluation backend **must be specified in the technical architecture document**
|
||||
> before implementation begins. Do NOT choose a provider independently — ask the user
|
||||
> which infrastructure to use.
|
||||
|
||||
| Approach | When to use | Notes |
|
||||
|----------|-------------|-------|
|
||||
| **Managed SaaS** | Teams > 5, multi-environment, complex targeting | LaunchDarkly, Flagsmith, Unleash Cloud |
|
||||
| **Self-hosted** | Full control, no SaaS dependency | Unleash (OSS), Flagsmith (OSS) |
|
||||
| **Firebase Remote Config** | Mobile apps in the Firebase ecosystem | Firebase SDK required |
|
||||
| **Static config (YAML / env)** | Solo dev, simple on/off, single environment | Loaded at startup; no runtime targeting |
|
||||
|
||||
Flag configuration is infrastructure — it must be provisioned before code references it.
|
||||
|
||||
---
|
||||
|
||||
### Flag Evaluation Rules
|
||||
|
||||
- **Evaluate server-side** for security-sensitive features (never trust the client)
|
||||
- **Evaluate at request boundary** — never inside pure business logic functions
|
||||
- **Default to disabled** — if the flag service is unreachable, the flag is off (fail closed)
|
||||
- **Wrap flag checks once** — in a service method or middleware, not scattered across business logic
|
||||
|
||||
**Pattern:**
|
||||
```
|
||||
// ✅ Flag evaluation at the boundary (handler or service entry point)
|
||||
func (s *Service) CreateOrder(ctx context.Context, req Request) (Response, error) {
|
||||
useNewPricing := s.flags.IsEnabled(ctx, "new-pricing-engine", req.UserID)
|
||||
if useNewPricing {
|
||||
return s.handleWithNewPricing(ctx, req)
|
||||
}
|
||||
return s.handleWithLegacyPricing(ctx, req)
|
||||
}
|
||||
|
||||
// ❌ Flag evaluation buried inside business logic
|
||||
func calculateDiscount(items []Item) float64 {
|
||||
if flags.IsEnabled("new-discount-rules") { ... } // NO
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Flag Lifecycle Rules
|
||||
|
||||
Flags are temporary by design. Flag debt accumulates fast and becomes a maintenance burden.
|
||||
|
||||
- Every flag **must have an owner** (the team or developer responsible for its lifecycle)
|
||||
- Every **release flag** has a **maximum lifespan of 90 days**
|
||||
- When a release flag reaches 100% rollout, **create a ticket immediately** to remove it
|
||||
- **Ops (kill switch) flags** can be permanent but must be documented as such
|
||||
- **Experiment flags** must be removed when the experiment concludes
|
||||
- Review all flags in sprint planning — any flag past its expiry date is tech debt
|
||||
|
||||
---
|
||||
|
||||
### Testing with Feature Flags
|
||||
|
||||
Feature flags multiply test permutations. Keep tests tractable:
|
||||
|
||||
- Unit tests test **each branch independently** (flag on + flag off)
|
||||
- Integration tests default the flag to its **production-expected state** (usually fully on or off)
|
||||
- Do not write tests that enumerate every possible flag combination
|
||||
- Use a test-only flag override mechanism (e.g., `flags.Override(ctx, "flag", true)`) — never
|
||||
branch test logic on environment variables
|
||||
|
||||
---
|
||||
|
||||
### CI/CD Checklist (Feature Flags)
|
||||
|
||||
- [ ] Flag infrastructure specified in technical architecture document?
|
||||
- [ ] Flag backend provisioned and accessible by the application?
|
||||
- [ ] Every flag has an owner and expiry date set?
|
||||
- [ ] Flag evaluation is server-side for security-sensitive paths?
|
||||
- [ ] Default state is `disabled` (fail closed)?
|
||||
- [ ] Unit tests cover both flag-on and flag-off code paths?
|
||||
- [ ] Removal ticket created once flag reaches 100% rollout?
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
|
||||
- CI/CD Principles @ci-cd-principles.md (deployment vs release concept)
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Core Design Principles @core-design-principles.md (YAGNI)
|
||||
- Security Mandate @security-mandate.md (server-side evaluation)
|
||||
@@ -0,0 +1,871 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Flutter or Dart code, working on mobile UI, using Riverpod for state management, or building Flutter widgets
|
||||
---
|
||||
|
||||
## Flutter Idioms and Patterns (Riverpod 3)
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Flutter is a UI toolkit first — performance is a first-class concern. `const` widgets and immutable data keep the render tree efficient. **Riverpod 3** is the canonical state management solution: compile-safe, testable without `BuildContext`, no implicit global state, and with automatic retry and pause/resume built in.
|
||||
|
||||
**Code generation is mandatory.** All providers must use `@riverpod` / `@Riverpod(keepAlive: true)` annotations with `riverpod_generator` and `build_runner`. This catches type errors, missing overrides, and broken provider graphs at compile time — bugs are caught before they reach users.
|
||||
|
||||
**Required dependencies:**
|
||||
|
||||
```yaml
|
||||
# pubspec.yaml
|
||||
dependencies:
|
||||
flutter_riverpod: 3.2.1
|
||||
riverpod_annotation: 4.0.2
|
||||
|
||||
dev_dependencies:
|
||||
riverpod_generator: 4.0.3
|
||||
build_runner: # latest
|
||||
riverpod_lint: # latest
|
||||
```
|
||||
|
||||
> **Scope:** This file covers Flutter/Dart *coding idioms*. For file and folder layout, see `project-structure-flutter-mobile.md`. For test naming, see `testing-strategy.md`. For general error handling principles, see `error-handling-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### `const` Constructors — Everywhere
|
||||
|
||||
Make every widget `const` when possible. `const` widgets are created once and never rebuilt unless their inputs change — this is Flutter's most impactful performance optimization.
|
||||
|
||||
```dart
|
||||
// ✅ const constructor — widget is rebuild-safe
|
||||
class TaskCard extends StatelessWidget {
|
||||
const TaskCard({super.key, required this.task});
|
||||
final Task task;
|
||||
// ...
|
||||
}
|
||||
|
||||
// Usage — compile-time constant
|
||||
const TaskCard(task: myTask)
|
||||
|
||||
// ❌ Missing const — rebuilt on every parent rebuild
|
||||
TaskCard(task: myTask)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every `StatelessWidget` that has no mutable state must have a `const` constructor
|
||||
- Pass `const` keyword at the call site, not just the definition
|
||||
- Lint rule `prefer_const_constructors` must be enabled in `analysis_options.yaml`
|
||||
|
||||
---
|
||||
|
||||
### Widget Decomposition
|
||||
|
||||
Large `build` methods are the primary source of performance problems and unmaintainable UI code.
|
||||
|
||||
1. **Extract a new widget when a subtree has distinct responsibilities**
|
||||
```dart
|
||||
// ❌ Everything in one build method
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
// 30 lines of header...
|
||||
// 50 lines of list...
|
||||
// 20 lines of footer...
|
||||
]);
|
||||
}
|
||||
|
||||
// ✅ Each subtree is a named widget with a const constructor
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
const TaskHeader(),
|
||||
const TaskList(),
|
||||
const TaskFooter(),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Never use builder methods (`_buildHeader()`) as a substitute for extracting widgets**
|
||||
- Builder methods do not benefit from `const` and always rerun on parent rebuild
|
||||
- Extract a proper `StatelessWidget` or `ConsumerWidget` instead
|
||||
|
||||
3. **Keep `build` methods under ~30 lines** — if longer, decompose
|
||||
|
||||
---
|
||||
|
||||
### Immutable Data with `freezed`
|
||||
|
||||
All domain models must be immutable. Use the `freezed` package for:
|
||||
- Immutable value objects with `copyWith`
|
||||
- Union/sealed types (loading, success, error states)
|
||||
- Generated `==`, `hashCode`, and `toString`
|
||||
|
||||
```dart
|
||||
// task/models/task.dart
|
||||
@freezed
|
||||
class Task with _$Task {
|
||||
const factory Task({
|
||||
required String id,
|
||||
required String title,
|
||||
@Default(TaskStatus.pending) TaskStatus status,
|
||||
DateTime? dueDate,
|
||||
}) = _Task;
|
||||
|
||||
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
|
||||
}
|
||||
|
||||
// Usage — immutable update via copyWith
|
||||
final updated = task.copyWith(status: TaskStatus.done);
|
||||
|
||||
// ❌ Never mutate a model directly
|
||||
task.status = TaskStatus.done; // compile error — field is final
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- All domain models use `@freezed`
|
||||
- Never expose mutable fields on domain models
|
||||
- Run `dart run build_runner build` after changing freezed models
|
||||
|
||||
---
|
||||
|
||||
### Provider Decision Tree
|
||||
|
||||
Use this tree to pick the correct provider pattern. **Follow it top-to-bottom — the first match wins.**
|
||||
|
||||
```
|
||||
Does the provider have side-effects (create/update/delete)? ──> YES ──┐
|
||||
│ │
|
||||
NO │
|
||||
│ Is it async?
|
||||
│ │ │
|
||||
Is it async? YES NO
|
||||
│ │ │ │
|
||||
YES NO ▼ ▼
|
||||
│ │ @riverpod @riverpod
|
||||
▼ ▼ class class
|
||||
@riverpod @riverpod (AsyncNotifier) (Notifier)
|
||||
function function
|
||||
(FutureProvider) (Provider)
|
||||
```
|
||||
|
||||
**Decision summary:**
|
||||
|
||||
| Has side-effects? | Async? | Pattern | Provider type generated |
|
||||
|---|---|---|---|
|
||||
| No | No | `@riverpod` function | `Provider` |
|
||||
| No | Yes | `@riverpod` async function | `FutureProvider` |
|
||||
| No | Stream | `@riverpod` `Stream` function | `StreamProvider` |
|
||||
| Yes | No | `@riverpod` class | `NotifierProvider` |
|
||||
| Yes | Yes | `@riverpod` class with `Future` build | `AsyncNotifierProvider` |
|
||||
| Yes | Stream | `@riverpod` class with `Stream` build | `StreamNotifierProvider` |
|
||||
|
||||
---
|
||||
|
||||
### Riverpod — State Management
|
||||
|
||||
**Riverpod 3 is the only state management solution** used in this project. Do not introduce BLoC, Cubit, Provider (package:provider), or GetX.
|
||||
|
||||
> For file layout of state/ directories, see `project-structure-flutter-mobile.md`.
|
||||
|
||||
#### App Entry Point — `ProviderScope`
|
||||
|
||||
Every Flutter app using Riverpod must wrap the root widget in `ProviderScope`. This creates the `ProviderContainer` that powers all providers in the widget tree.
|
||||
|
||||
```dart
|
||||
// main.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Exactly one `ProviderScope` at the app root — never nest `ProviderScope` widgets
|
||||
- Use `overrides:` parameter only in tests (see Testing section)
|
||||
- For retry configuration, pass `retry:` to the root `ProviderScope` (see Runtime Behaviors)
|
||||
|
||||
#### Provider Definition — Class-Based (Side-Effects)
|
||||
|
||||
Use class-based providers when the provider needs methods to modify state.
|
||||
|
||||
```dart
|
||||
// Synchronous notifier — e.g., a filter or toggle
|
||||
@riverpod
|
||||
class TaskFilter extends _$TaskFilter {
|
||||
@override
|
||||
TaskFilterState build() {
|
||||
return const TaskFilterState();
|
||||
}
|
||||
|
||||
void setStatus(TaskStatus? status) {
|
||||
state = state.copyWith(status: status);
|
||||
}
|
||||
|
||||
void toggleShowCompleted() {
|
||||
state = state.copyWith(showCompleted: !state.showCompleted);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// Async notifier — e.g., CRUD operations
|
||||
@riverpod
|
||||
class TaskList extends _$TaskList {
|
||||
@override
|
||||
Future<List<Task>> build() async {
|
||||
return ref.watch(taskRepositoryProvider).getTasks();
|
||||
}
|
||||
|
||||
Future<void> addTask(CreateTaskRequest request) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.createTask(request);
|
||||
// REQUIRED: check ref.mounted after every await
|
||||
if (!ref.mounted) return state.requireValue;
|
||||
return repo.getTasks();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteTask(String id) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.deleteTask(id);
|
||||
if (!ref.mounted) return state.requireValue;
|
||||
return repo.getTasks();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Provider Definition — Functional (Read-Only / Computed)
|
||||
|
||||
Use functional providers for derived values with no side-effects.
|
||||
|
||||
```dart
|
||||
// Computed value — filtered task list
|
||||
@riverpod
|
||||
List<Task> filteredTasks(Ref ref) {
|
||||
final tasks = ref.watch(taskListProvider).valueOrNull ?? [];
|
||||
final filter = ref.watch(taskFilterProvider);
|
||||
return tasks.where((t) => filter.matches(t)).toList();
|
||||
}
|
||||
|
||||
// Async one-shot read — e.g., fetch a single task
|
||||
@riverpod
|
||||
Future<Task> taskDetail(Ref ref, String id) async {
|
||||
return ref.watch(taskRepositoryProvider).getById(id);
|
||||
}
|
||||
|
||||
// Stream — real-time data
|
||||
@riverpod
|
||||
Stream<List<Task>> taskStream(Ref ref) {
|
||||
return ref.watch(taskRepositoryProvider).watchAll();
|
||||
}
|
||||
```
|
||||
|
||||
**Family providers (parameterized):** When a provider takes extra arguments beyond `Ref`, Riverpod creates a *family* — each unique argument combination gets its own independent provider instance with its own cache and disposal. With code generation (which we mandate), **any number of parameters** are supported, including named, optional, and default values. Constraints:
|
||||
- All parameters must implement `==` and `hashCode` (primitives, `freezed` models, Dart records all work)
|
||||
- Each family member is disposed independently when no listener remains
|
||||
|
||||
```dart
|
||||
// ✅ Family with a single key
|
||||
@riverpod
|
||||
Future<Task> taskDetail(Ref ref, String id) async {
|
||||
return ref.watch(taskRepositoryProvider).getById(id);
|
||||
}
|
||||
|
||||
// ✅ Multiple parameters — fully supported with code generation
|
||||
@riverpod
|
||||
Future<List<Task>> projectTasks(Ref ref, String projectId, {TaskStatus? status}) async {
|
||||
return ref.watch(taskRepositoryProvider).getByProject(projectId, status: status);
|
||||
}
|
||||
// Usage: ref.watch(projectTasksProvider('proj-1', status: TaskStatus.done))
|
||||
```
|
||||
|
||||
#### `ref.watch` vs `ref.read`
|
||||
|
||||
```dart
|
||||
// ✅ ref.watch — subscribes to changes, use inside build() or widget build
|
||||
final tasks = ref.watch(taskListProvider);
|
||||
|
||||
// ✅ ref.read — one-time read, use inside event handlers / notifier actions
|
||||
Future<void> onSubmit() async {
|
||||
await ref.read(taskListProvider.notifier).addTask(request);
|
||||
}
|
||||
|
||||
// ❌ Never use ref.watch inside async functions or event handlers
|
||||
Future<void> onSubmit() async {
|
||||
final tasks = ref.watch(taskListProvider); // WRONG — causes errors
|
||||
}
|
||||
```
|
||||
|
||||
#### `Ref.mounted` — Mandatory After Awaits
|
||||
|
||||
Riverpod 3 throws if you interact with a disposed `Ref` or `Notifier`. **Always check `ref.mounted` after any `await` in a notifier.**
|
||||
|
||||
```dart
|
||||
Future<void> updateTask(Task task) async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.update(task);
|
||||
|
||||
// REQUIRED: provider may have been disposed during await
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = await AsyncValue.guard(() => repo.getTasks());
|
||||
}
|
||||
```
|
||||
|
||||
#### Auto-Dispose and `keepAlive`
|
||||
|
||||
```dart
|
||||
// ✅ autoDispose is the DEFAULT with code generation (@riverpod)
|
||||
// Provider is disposed when no consumers are listening
|
||||
@riverpod
|
||||
Future<Task> taskDetail(Ref ref, String id) async {
|
||||
return ref.watch(taskRepositoryProvider).getById(id);
|
||||
}
|
||||
|
||||
// ✅ Opt into keepAlive explicitly for app-wide, long-lived state
|
||||
@Riverpod(keepAlive: true)
|
||||
class AuthState extends _$AuthState {
|
||||
@override
|
||||
Future<User?> build() async {
|
||||
return ref.watch(authRepositoryProvider).getCurrentUser();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Repositories should also be keepAlive — they hold connection state and
|
||||
// should not be re-initialized every time a screen rebuilds
|
||||
@Riverpod(keepAlive: true)
|
||||
TaskRepository taskRepository(Ref ref) {
|
||||
return TaskRepositoryImpl(apiClient: ref.watch(apiClientProvider));
|
||||
}
|
||||
|
||||
// ❌ Do not set keepAlive: false — that is the default
|
||||
// ❌ Do not set keepAlive: true for screen-scoped state
|
||||
```
|
||||
|
||||
#### Repository Interface Pattern
|
||||
|
||||
All data access goes through an abstract repository interface. This is the Flutter expression of the Testability-First architecture (see `architectural-pattern.md`).
|
||||
|
||||
```dart
|
||||
// repository/task_repository.dart — Abstract interface (contract)
|
||||
abstract class TaskRepository {
|
||||
Future<List<Task>> getTasks();
|
||||
Future<Task> getById(String id);
|
||||
Future<void> createTask(CreateTaskRequest request);
|
||||
Future<void> deleteTask(String id);
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// repository/task_repository_impl.dart — Production adapter
|
||||
class TaskRepositoryImpl implements TaskRepository {
|
||||
const TaskRepositoryImpl({required this.apiClient});
|
||||
final ApiClient apiClient;
|
||||
|
||||
@override
|
||||
Future<List<Task>> getTasks() async {
|
||||
final response = await apiClient.get('/tasks');
|
||||
return (response.data as List)
|
||||
.map((e) => Task.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Task> getById(String id) async {
|
||||
final response = await apiClient.get('/tasks/$id');
|
||||
return Task.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createTask(CreateTaskRequest request) async {
|
||||
await apiClient.post('/tasks', data: request.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTask(String id) async {
|
||||
await apiClient.delete('/tasks/$id');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// repository/task_repository_mock.dart — Test adapter
|
||||
class MockTaskRepository implements TaskRepository {
|
||||
final List<Task> _tasks = [];
|
||||
|
||||
@override
|
||||
Future<List<Task>> getTasks() async => List.unmodifiable(_tasks);
|
||||
|
||||
@override
|
||||
Future<Task> getById(String id) async =>
|
||||
_tasks.firstWhere((t) => t.id == id);
|
||||
|
||||
@override
|
||||
Future<void> createTask(CreateTaskRequest request) async {
|
||||
_tasks.add(Task(id: 'mock-id', title: request.title));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTask(String id) async {
|
||||
_tasks.removeWhere((t) => t.id == id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// Wiring — provider that the rest of the app depends on
|
||||
@Riverpod(keepAlive: true)
|
||||
TaskRepository taskRepository(Ref ref) {
|
||||
return TaskRepositoryImpl(apiClient: ref.watch(apiClientProvider));
|
||||
}
|
||||
|
||||
// In tests, override with mock:
|
||||
// taskRepositoryProvider.overrideWith((_) => MockTaskRepository())
|
||||
```
|
||||
|
||||
#### ConsumerWidget vs ConsumerStatefulWidget
|
||||
|
||||
```dart
|
||||
// ✅ Prefer ConsumerWidget — stateless, simpler
|
||||
class TaskListView extends ConsumerWidget {
|
||||
const TaskListView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asyncTasks = ref.watch(taskListProvider);
|
||||
return asyncTasks.when(
|
||||
data: (tasks) => TaskListBody(tasks: tasks),
|
||||
loading: () => const LoadingIndicator(),
|
||||
error: (e, _) => ErrorView(error: e),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Use ConsumerStatefulWidget only when local widget state + riverpod is needed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Riverpod 3 — Runtime Behaviors
|
||||
|
||||
#### Automatic Retry
|
||||
|
||||
Riverpod 3 automatically retries providers that throw, using exponential backoff. This improves resilience against transient network failures.
|
||||
|
||||
```dart
|
||||
// ✅ Default behavior — providers auto-retry on failure
|
||||
// No action needed for standard use
|
||||
|
||||
// ✅ Disable retry for a specific provider when failure is non-transient
|
||||
@Riverpod(retry: null)
|
||||
Future<Config> appConfig(Ref ref) async {
|
||||
return ref.watch(configRepositoryProvider).load();
|
||||
}
|
||||
|
||||
// ✅ Disable globally via ProviderScope
|
||||
ProviderScope(
|
||||
retry: (_, __) => null, // disable for all providers
|
||||
child: const MyApp(),
|
||||
)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Leave auto-retry enabled for network/IO operations (transient failures)
|
||||
- Disable for operations where retry is unsafe (non-idempotent writes) or pointless (validation errors)
|
||||
|
||||
#### Pause / Resume (Out-of-View Providers)
|
||||
|
||||
Riverpod 3 automatically **pauses** provider listeners when the consuming widget is no longer visible. When the widget becomes visible again, listeners resume.
|
||||
|
||||
This is automatic — no action needed. Be aware of it when debugging.
|
||||
|
||||
#### `ProviderException` Wrapping
|
||||
|
||||
When a provider fails, reading it in Riverpod 3 throws a `ProviderException` wrapping the original error, not the raw exception.
|
||||
|
||||
```dart
|
||||
// ✅ Catch ProviderException when reading providers that may fail
|
||||
try {
|
||||
final value = container.read(myProvider);
|
||||
} on ProviderException catch (e) {
|
||||
// e.exception contains the original error
|
||||
// e.provider contains which provider failed
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// ✅ Assert on provider failure in tests
|
||||
expect(
|
||||
() => container.read(myProvider),
|
||||
throwsA(isA<ProviderException>()),
|
||||
);
|
||||
```
|
||||
|
||||
#### State Change Detection (`==`)
|
||||
|
||||
Riverpod 3 uses the `==` operator (not `identical`) to determine if state changed and rebuilds are needed. This means:
|
||||
|
||||
- `freezed` models work correctly out of the box (generated `==`)
|
||||
- Custom models **must** implement `==` / `hashCode` or use `freezed`
|
||||
- Override `updateShouldNotify` on a `Notifier` for custom comparison logic
|
||||
|
||||
---
|
||||
|
||||
### Async Patterns
|
||||
|
||||
1. **Always handle all three `AsyncValue` states: data, loading, error**
|
||||
```dart
|
||||
// ✅ Exhaustive
|
||||
asyncValue.when(
|
||||
data: (data) => DataWidget(data: data),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, stack) => ErrorText(err.toString()),
|
||||
);
|
||||
```
|
||||
|
||||
2. **Surfacing notifier errors to UI** — use `when(error:...)` for exhaustive handling; use `hasError` only for conditional checks alongside a separate data display:
|
||||
```dart
|
||||
// ✅ Exhaustive — covers all states, preferred for full-screen states
|
||||
asyncTasks.when(
|
||||
data: (tasks) => TaskListBody(tasks: tasks),
|
||||
loading: () => const LoadingIndicator(),
|
||||
error: (e, _) => ErrorView(error: e),
|
||||
);
|
||||
|
||||
// ✅ Conditional — show inline error banner while keeping stale data visible
|
||||
if (asyncTasks.hasError) {
|
||||
// show snackbar or inline error
|
||||
}
|
||||
final tasks = asyncTasks.valueOrNull ?? const [];
|
||||
```
|
||||
|
||||
3. **Use safe `AsyncValue` accessors to prevent runtime crashes**
|
||||
```dart
|
||||
// ✅ Safe — returns null if state is loading or error
|
||||
final tasks = ref.watch(taskListProvider).valueOrNull;
|
||||
|
||||
// ⚠️ Unsafe — throws StateError if state is not AsyncData
|
||||
// Only use when you have already confirmed the state is loaded
|
||||
final tasks = ref.watch(taskListProvider).requireValue;
|
||||
```
|
||||
|
||||
4. **Use `AsyncValue.guard` inside notifier actions to wrap async calls**
|
||||
- It catches exceptions and wraps them in `AsyncError` automatically
|
||||
|
||||
5. **Use `StreamProvider` for real-time data** — never poll manually with `Timer`
|
||||
|
||||
6. **Always check `ref.mounted` after `await`** — see `Ref.mounted` section above
|
||||
|
||||
7. **Force a provider to re-fetch with `ref.invalidate`**
|
||||
```dart
|
||||
// ✅ Invalidate from outside a notifier (e.g., after a form submit in a widget)
|
||||
ref.invalidate(taskListProvider);
|
||||
// The next watch/read will trigger a fresh build()
|
||||
|
||||
// ✅ Invalidate from inside a notifier after a mutation
|
||||
Future<void> addTask(CreateTaskRequest request) async {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.createTask(request);
|
||||
if (!ref.mounted) return;
|
||||
ref.invalidateSelf(); // triggers build() to re-run and return fresh list
|
||||
}
|
||||
```
|
||||
|
||||
**Decision: `invalidateSelf()` vs manual state assignment**
|
||||
```
|
||||
After a mutation:
|
||||
├── Need optimistic UI (instant visual update before server confirms)?
|
||||
│ └── YES → Set state manually via AsyncValue.guard
|
||||
│ (e.g., remove item from list immediately, then call API)
|
||||
└── NO → ref.invalidateSelf() (simpler, always correct — re-runs build())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Handling in Flutter/Riverpod
|
||||
|
||||
Define a typed exception hierarchy using sealed classes (Dart 3+). Map infrastructure exceptions to domain exceptions inside notifier actions.
|
||||
|
||||
```dart
|
||||
// core/errors/app_exception.dart — Typed exception hierarchy
|
||||
sealed class AppException implements Exception {
|
||||
const AppException(this.message);
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class NetworkException extends AppException {
|
||||
const NetworkException(super.message, {this.statusCode});
|
||||
final int? statusCode;
|
||||
}
|
||||
|
||||
class ValidationException extends AppException {
|
||||
const ValidationException(super.message, {required this.field});
|
||||
final String field;
|
||||
}
|
||||
|
||||
class NotFoundException extends AppException {
|
||||
const NotFoundException(super.message);
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// ✅ Map infrastructure exceptions to domain exceptions inside notifier actions
|
||||
Future<void> addTask(CreateTaskRequest request) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
try {
|
||||
final repo = ref.read(taskRepositoryProvider);
|
||||
await repo.createTask(request);
|
||||
if (!ref.mounted) return state.requireValue;
|
||||
return repo.getTasks();
|
||||
} on DioException catch (e) {
|
||||
// Transform infrastructure error → domain error
|
||||
throw NetworkException(
|
||||
'Failed to create task: ${e.message}',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// ✅ Handle typed errors in UI
|
||||
asyncTasks.when(
|
||||
data: (tasks) => TaskListBody(tasks: tasks),
|
||||
loading: () => const LoadingIndicator(),
|
||||
error: (error, _) => switch (error) {
|
||||
NetworkException() => ErrorView(message: 'Network error: ${error.message}'),
|
||||
NotFoundException() => const ErrorView(message: 'Not found'),
|
||||
_ => ErrorView(message: 'Unexpected error: $error'),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- All custom exceptions extend `AppException` (sealed class)
|
||||
- Infrastructure exceptions (`DioException`, `SocketException`) are caught and re-thrown as domain exceptions
|
||||
- Never expose infrastructure types (Dio, HTTP status codes) to the UI layer
|
||||
- Use exhaustive `switch` on the sealed class in error display
|
||||
- See `error-handling-principles.md` for general error handling guidance
|
||||
|
||||
---
|
||||
|
||||
### Navigation with `go_router`
|
||||
|
||||
**`go_router` is the canonical navigation library.**
|
||||
|
||||
```dart
|
||||
// core/router/app_router.dart
|
||||
@riverpod
|
||||
GoRouter appRouter(Ref ref) {
|
||||
return GoRouter(
|
||||
initialLocation: '/tasks',
|
||||
routes: [
|
||||
GoRoute(path: '/tasks', builder: (_, __) => const TaskListView()),
|
||||
GoRoute(
|
||||
path: '/tasks/:id',
|
||||
builder: (_, state) => TaskDetailView(
|
||||
// state.pathParameters['id'] is guaranteed non-null by the :id
|
||||
// route pattern — acceptable use of ! in route infrastructure code
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate — always by path, never by widget reference
|
||||
context.go('/tasks/$taskId');
|
||||
context.push('/tasks/new'); // push adds to the back stack
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Dart Language Idioms
|
||||
|
||||
1. **Null safety — use `?.`, `??`, and `??=` idiomatically**
|
||||
```dart
|
||||
final city = user?.address?.city ?? 'Unknown';
|
||||
cache ??= await compute(); // assign only if null
|
||||
```
|
||||
|
||||
2. **Use `late` only for fields initialized before first use that cannot be `final`**
|
||||
- Prefer `final` fields initialized in the constructor
|
||||
- `late` without initialization is an unsafe nullable escape hatch
|
||||
|
||||
3. **Extension methods for adding behaviour to types you don't own**
|
||||
```dart
|
||||
extension TaskStatusLabel on TaskStatus {
|
||||
String get label => switch (this) {
|
||||
TaskStatus.pending => 'Pending',
|
||||
TaskStatus.done => 'Done',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
4. **Use `switch` expressions (Dart 3+) for exhaustive pattern matching**
|
||||
```dart
|
||||
final label = status switch {
|
||||
TaskStatus.pending => 'Pending',
|
||||
TaskStatus.done => 'Done',
|
||||
// Compiler error if a case is missing
|
||||
};
|
||||
```
|
||||
|
||||
5. **Avoid `dynamic` — it is the Dart equivalent of TypeScript's `any`**
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Flutter/Riverpod 3 test patterns.
|
||||
|
||||
#### Unit Test Providers with `ProviderContainer.test`
|
||||
|
||||
`ProviderContainer.test` creates an isolated container that **auto-disposes after the test** — no manual `addTearDown` needed.
|
||||
|
||||
```dart
|
||||
test('addTask updates state', () async {
|
||||
final container = ProviderContainer.test(overrides: [
|
||||
taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
|
||||
]);
|
||||
|
||||
await container.read(taskListProvider.notifier).addTask(request);
|
||||
expect(container.read(taskListProvider).value, hasLength(1));
|
||||
});
|
||||
```
|
||||
|
||||
#### Override Only the `build` Method with `overrideWithBuild`
|
||||
|
||||
When you need to control initial state but keep the notifier's methods intact:
|
||||
|
||||
```dart
|
||||
test('deleteTask removes item from pre-seeded list', () async {
|
||||
final container = ProviderContainer.test(overrides: [
|
||||
taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
|
||||
taskListProvider.overrideWithBuild((ref, notifier) {
|
||||
// Seed initial state — methods (addTask, deleteTask) still work
|
||||
return Future.value([mockTask1, mockTask2]);
|
||||
}),
|
||||
]);
|
||||
|
||||
await container.read(taskListProvider.notifier).deleteTask(mockTask1.id);
|
||||
expect(container.read(taskListProvider).value, hasLength(1));
|
||||
});
|
||||
```
|
||||
|
||||
#### Widget Tests with `ProviderScope`
|
||||
|
||||
```dart
|
||||
testWidgets('shows task list', (tester) async {
|
||||
await tester.pumpWidget(ProviderScope(
|
||||
overrides: [
|
||||
taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
|
||||
],
|
||||
child: const MaterialApp(home: TaskListView()),
|
||||
));
|
||||
expect(find.byType(TaskCard), findsWidgets);
|
||||
});
|
||||
```
|
||||
|
||||
#### Use `mockito` with `@GenerateNiceMocks` for Interface Mocks
|
||||
|
||||
Place the annotation on the library (top of the test file or on `main`) and add a `part` directive for the generated file:
|
||||
|
||||
```dart
|
||||
// task_notifier_test.dart
|
||||
@GenerateNiceMocks([MockSpec<TaskRepository>()])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'task_notifier_test.mocks.dart'; // generated by build_runner
|
||||
|
||||
void main() {
|
||||
// tests use MockTaskRepository()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Anti-Patterns — NEVER DO THIS
|
||||
|
||||
These are common mistakes agents reproduce from Riverpod 2 training data. **Every item below is a hard rule violation.**
|
||||
|
||||
| ❌ Anti-Pattern | ✅ Correct Pattern |
|
||||
|---|---|
|
||||
| `StateProvider` | `@riverpod` class with `Notifier` |
|
||||
| `StateNotifierProvider` | `@riverpod` class with `Notifier` |
|
||||
| `ChangeNotifierProvider` | `@riverpod` class with `Notifier` |
|
||||
| `import 'package:riverpod/legacy.dart'` | Never import legacy APIs |
|
||||
| Typed ref subclass (`TaskDetailRef ref`) | `Ref ref` — Riverpod 3 uses a single `Ref` type; typed subclasses were a Riverpod 2 codegen artifact that no longer exists |
|
||||
| `ref.watch` inside async/event handler | `ref.read` for one-shot reads in handlers |
|
||||
| Accessing `state`/`ref` after `await` without `ref.mounted` check | Always check `ref.mounted` after `await` |
|
||||
| `ProviderContainer()` + `addTearDown(container.dispose)` | `ProviderContainer.test(overrides: [...])` |
|
||||
| `Timer.periodic` for polling data | `StreamProvider` or `Stream` return |
|
||||
| `keepAlive: false` in annotation | Omit — false is the default |
|
||||
| Manual providers without `@riverpod` annotation | Always use code generation |
|
||||
| Catching raw exceptions from provider reads | Catch `ProviderException` |
|
||||
| `overrideWith((_) => MockNotifier())` when only initial state needs seeding | `overrideWithBuild(...)` — keeps notifier methods intact, only seeds initial state |
|
||||
|
||||
---
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
| Tool | Purpose | Config File |
|
||||
| --- | --- | --- |
|
||||
| `dart format` | Canonical formatting | — (built-in) |
|
||||
| `flutter analyze` | Static analysis + lint | `analysis_options.yaml` |
|
||||
| `riverpod_lint` | Riverpod-specific lint rules | `dev_dependencies` |
|
||||
| `dart pub deps` | Dependency audit | — |
|
||||
| `dart run build_runner build` | Generate provider code | — |
|
||||
|
||||
**Mandatory `analysis_options.yaml` settings (Dart 3+):**
|
||||
```yaml
|
||||
analyzer:
|
||||
language:
|
||||
strict-casts: true
|
||||
strict-raw-types: true
|
||||
errors:
|
||||
invalid_assignment: error
|
||||
plugins:
|
||||
- riverpod_lint
|
||||
linter:
|
||||
rules:
|
||||
- prefer_const_constructors
|
||||
- prefer_const_declarations
|
||||
- avoid_dynamic_calls
|
||||
- avoid_print
|
||||
- use_super_parameters
|
||||
```
|
||||
|
||||
**After any code change involving providers, run:**
|
||||
```bash
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
flutter analyze
|
||||
dart format .
|
||||
```
|
||||
|
||||
**During active development, use `watch` mode to avoid re-running manually:**
|
||||
```bash
|
||||
dart run build_runner watch --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Project Structure — Flutter Mobile @project-structure-flutter-mobile.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When creating branches, committing code, managing PRs, or working with version control
|
||||
---
|
||||
|
||||
## Git Workflow Principles
|
||||
|
||||
### Commit Messages — Conventional Commits
|
||||
|
||||
**Format:**
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
| Type | Purpose |
|
||||
| ---------- | -------------------------------- |
|
||||
| `feat` | New feature |
|
||||
| `fix` | Bug fix |
|
||||
| `docs` | Documentation only |
|
||||
| `style` | Formatting, semicolons, etc. |
|
||||
| `refactor` | Code change (no new feature/fix) |
|
||||
| `test` | Adding or updating tests |
|
||||
| `chore` | Maintenance, dependencies |
|
||||
| `perf` | Performance improvement |
|
||||
| `ci` | CI/CD configuration changes |
|
||||
|
||||
**Rules:**
|
||||
- Description is imperative mood ("add" not "added", "fix" not "fixes")
|
||||
- Scope matches the feature area (e.g., `task`, `auth`, `ui`)
|
||||
- Description is concise (<72 characters)
|
||||
- Body explains **why**, not what (the diff shows what)
|
||||
|
||||
### Branch Naming
|
||||
|
||||
**Format:** `<type>/<ticket-or-short-description>`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
feat/task-crud-api
|
||||
fix/auth-token-expiry
|
||||
refactor/storage-layer
|
||||
chore/update-deps
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Use lowercase with hyphens (kebab-case)
|
||||
- Prefix matches commit type
|
||||
- Keep branch names short but descriptive
|
||||
|
||||
### Commit Hygiene
|
||||
|
||||
- **One logical change per commit** — don't mix unrelated changes
|
||||
- **Never commit broken tests** — all tests must pass before committing
|
||||
- **Don't commit debug code** — remove console.log, print statements, TODO hacks
|
||||
- **Don't commit secrets** — use `.gitignore` and environment variables
|
||||
|
||||
### PR Size Guidelines
|
||||
|
||||
- **Ideal:** <400 lines changed
|
||||
- **Acceptable:** 400-800 lines
|
||||
- **Too large:** >800 lines — split into smaller PRs
|
||||
|
||||
**Why:** Large PRs get rubber-stamped. Small PRs get thoughtful reviews.
|
||||
|
||||
### Merge Strategy
|
||||
|
||||
- **Feature branches → main:** Squash merge (clean history)
|
||||
- **Release branches:** Merge commit (preserve history)
|
||||
- **Hotfixes:** Cherry-pick to affected branches
|
||||
|
||||
### Git Workflow Checklist
|
||||
|
||||
- [ ] Branch named with correct type prefix?
|
||||
- [ ] All commits follow conventional format?
|
||||
- [ ] No debug code or secrets committed?
|
||||
- [ ] All tests pass before committing?
|
||||
- [ ] PR is <400 lines (or justified if larger)?
|
||||
- [ ] Commit messages explain why, not just what?
|
||||
|
||||
### Related Principles
|
||||
- Code Completion Mandate @code-completion-mandate.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Security Mandate @security-mandate.md
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Go code, reviewing Go idioms, or working on a Go backend project
|
||||
---
|
||||
|
||||
## Go Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Go favors simplicity, explicitness, and readability. The language is intentionally small — resist the urge to import patterns from other languages. If it looks boring and obvious, it's probably idiomatic Go.
|
||||
|
||||
> **Scope:** This file covers Go-specific *coding idioms*. For file layout, see `project-structure-go-backend.md`. For test naming conventions, see `testing-strategy.md`. For logging library choice, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Always return errors — never panic in library or business code**
|
||||
- `panic` is reserved for truly unrecoverable states (programmer errors, nil dereference)
|
||||
- Use `recover` only at top-level goroutine boundaries (middleware, server startup)
|
||||
|
||||
2. **Wrap errors with context using `%w`**
|
||||
```go
|
||||
// ✅ Preserves the error chain for errors.Is / errors.As
|
||||
return fmt.Errorf("creating task for user %s: %w", userID, err)
|
||||
|
||||
// ❌ Loses the error chain
|
||||
return fmt.Errorf("creating task: %v", err)
|
||||
```
|
||||
|
||||
3. **Use sentinel errors for expected branch conditions**
|
||||
```go
|
||||
// Define in errors.go
|
||||
var ErrNotFound = errors.New("not found")
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
|
||||
// Caller checks with errors.Is
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
// handle
|
||||
}
|
||||
```
|
||||
|
||||
4. **Use typed errors for rich domain errors**
|
||||
```go
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// Caller unwraps with errors.As
|
||||
var ve *ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
// access ve.Field, ve.Message
|
||||
}
|
||||
```
|
||||
|
||||
5. **Handle errors at the right level** — propagate upward until you have enough context to act on them; don't swallow or re-wrap the same error twice.
|
||||
|
||||
---
|
||||
|
||||
### Interfaces
|
||||
|
||||
1. **Keep interfaces small — one or two methods is ideal**
|
||||
```go
|
||||
// ✅ Focused, composable
|
||||
type Reader interface { Read(p []byte) (n int, err error) }
|
||||
type Writer interface { Write(p []byte) (n int, err error) }
|
||||
|
||||
// ❌ Monolithic
|
||||
type FileManager interface {
|
||||
Read(...); Write(...); Delete(...); List(...); Stat(...)
|
||||
}
|
||||
```
|
||||
|
||||
2. **"Accept interfaces, return structs"**
|
||||
- Function parameters: accept interfaces for flexibility and testability
|
||||
- Return values: return concrete structs so callers can access all methods
|
||||
|
||||
3. **Define interfaces where they are *used*, not where they are *implemented***
|
||||
```go
|
||||
// ✅ Defined in the consumer package (task feature)
|
||||
// task/storage.go
|
||||
type Storage interface {
|
||||
GetByID(ctx context.Context, id string) (*Task, error)
|
||||
}
|
||||
|
||||
// postgres.go implements Storage — it does NOT define it
|
||||
```
|
||||
|
||||
4. **Implicit satisfaction is a feature — don't use embedding to "implement" interfaces**
|
||||
- Any type with the right method set satisfies an interface automatically
|
||||
- No `implements` keyword needed or wanted
|
||||
|
||||
---
|
||||
|
||||
### Goroutines and Channels
|
||||
|
||||
> For general concurrency principles (race conditions, deadlocks, message passing), see `concurrency-and-threading-principles.md`. This section covers Go-specific mechanics.
|
||||
|
||||
1. **Always pass `context.Context` as the first parameter**
|
||||
```go
|
||||
// ✅
|
||||
func (s *Service) GetTask(ctx context.Context, id string) (*Task, error)
|
||||
|
||||
// ❌ — no way to cancel or propagate deadlines
|
||||
func (s *Service) GetTask(id string) (*Task, error)
|
||||
```
|
||||
|
||||
2. **Never start a goroutine without knowing how it will stop**
|
||||
```go
|
||||
// ✅ Goroutine is bounded by context cancellation
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case item := <-ch:
|
||||
process(item)
|
||||
}
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
3. **Use `errgroup` for concurrent fan-out with error collection**
|
||||
```go
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return fetchUsers(ctx) })
|
||||
g.Go(func() error { return fetchOrders(ctx) })
|
||||
if err := g.Wait(); err != nil { ... }
|
||||
```
|
||||
|
||||
4. **Prefer channels for ownership transfer; mutexes for shared state**
|
||||
- Channel: "I'm handing this data to you"
|
||||
- Mutex: "We're both reading/writing this shared thing"
|
||||
|
||||
5. **Close channels from the sender, never the receiver**
|
||||
|
||||
---
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
1. **Receiver names: short, consistent, and the first letter of the type**
|
||||
```go
|
||||
func (s *Service) Create(...) {} // ✅
|
||||
func (svc *Service) Create(...) {} // ❌ — too verbose
|
||||
func (self *Service) Create(...) {} // ❌ — not Go
|
||||
```
|
||||
|
||||
2. **Package names: short, lowercase, no underscores, no plurals**
|
||||
```go
|
||||
package task // ✅
|
||||
package tasks // ❌ plural
|
||||
package task_service // ❌ underscore
|
||||
```
|
||||
|
||||
3. **Acronyms follow Go conventions (all caps or all lowercase)**
|
||||
```go
|
||||
userID // ✅
|
||||
userId // ❌
|
||||
HTTPClient // ✅
|
||||
HttpClient // ❌
|
||||
```
|
||||
|
||||
4. **Unexported identifiers omit the type name** — if it's private, keep it terse
|
||||
|
||||
5. **Don't stutter** — `task.Task` is fine; `task.TaskService` is not
|
||||
|
||||
---
|
||||
|
||||
### Idiomatic Patterns
|
||||
|
||||
1. **Functional options for optional configuration**
|
||||
```go
|
||||
type Option func(*Service)
|
||||
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(s *Service) { s.timeout = d }
|
||||
}
|
||||
|
||||
func NewService(store Storage, opts ...Option) *Service {
|
||||
s := &Service{store: store, timeout: 30 * time.Second}
|
||||
for _, o := range opts { o(s) }
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
2. **`defer` for cleanup — always use error-checked closures**
|
||||
|
||||
Every deferred cleanup call that returns an error MUST check and log the error.
|
||||
Never use bare `defer X.Close()` — the discarded error hides resource leak failures.
|
||||
|
||||
```go
|
||||
// ❌ NEVER: Error silently discarded
|
||||
defer rows.Close()
|
||||
|
||||
// ✅ ALWAYS: Error-checked closure with structured logging
|
||||
rows, err := db.QueryContext(ctx, query)
|
||||
if err != nil { return fmt.Errorf("querying tasks: %w", err) }
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
slog.Warn("failed to close rows", "error", err, "operation", "ListTasks")
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**Transaction rollback:**
|
||||
```go
|
||||
// ❌ NEVER
|
||||
defer tx.Rollback()
|
||||
|
||||
// ✅ ALWAYS: Guard against sql.ErrTxDone (already committed)
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil && !errors.Is(err, sql.ErrTxDone) {
|
||||
slog.Error("failed to rollback transaction", "error", err, "operation", "CreateOrder")
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**HTTP response body:**
|
||||
```go
|
||||
// ❌ NEVER
|
||||
defer resp.Body.Close()
|
||||
|
||||
// ✅ ALWAYS: Drain then close (prevents connection reuse issues)
|
||||
defer func() {
|
||||
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
|
||||
slog.Warn("failed to drain response body", "error", err)
|
||||
}
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
slog.Warn("failed to close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
3. **Avoid `init()` functions** — they run implicitly and make testing harder; prefer explicit initialization in `main` or constructors
|
||||
|
||||
4. **Prefer `struct` embedding over inheritance for code reuse**, but only when the embedded type truly represents an "is-a" relationship
|
||||
|
||||
5. **Use named return values only for documentation or `defer`-based cleanup** — never rely on naked returns in non-trivial functions
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test file naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Go-specific tooling only.
|
||||
|
||||
1. **Table-driven tests are the default pattern**
|
||||
```go
|
||||
func TestCalculateDiscount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input float64
|
||||
expected float64
|
||||
wantErr bool
|
||||
}{
|
||||
{"zero items", 0, 0, false},
|
||||
{"negative input", -1, 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := calculateDiscount(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use `testify` for assertions** (`require` for fatal assertions, `assert` for non-fatal)
|
||||
|
||||
3. **Run with the race detector in CI** — `go test -race ./...`
|
||||
|
||||
4. **Use `httptest.NewRecorder()` for HTTP handler tests** — no live server needed
|
||||
|
||||
5. **Test behaviour, not implementation** — assert on outputs and side effects, not internal field values
|
||||
|
||||
---
|
||||
|
||||
### Formatting and Static Analysis
|
||||
|
||||
All of the following **must pass with zero warnings/errors** before any commit. See `code-completion-mandate.md` for the full checklist.
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
| ----------------------- | ------------------------ | -------------------- |
|
||||
| `gofumpt` / `goimports` | Canonical formatting | `gofumpt -l -w .` |
|
||||
| `go vet` | Correctness checks | `go vet ./...` |
|
||||
| `staticcheck` | Advanced static analysis | `staticcheck ./...` |
|
||||
| `gosec` | Security scanning | `gosec -quiet ./...` |
|
||||
| `golangci-lint` | Aggregated linter (CI) | `golangci-lint run` |
|
||||
| `govulncheck` | Dependency CVE scanning | `govulncheck ./...` |
|
||||
|
||||
- Never disable a linter without a comment explaining why
|
||||
- **`//nolint:errcheck` is NEVER acceptable.** If a function returns an error, handle it — even in `defer`. Use an error-checked closure (see § Idiomatic Patterns above). This is the #1 source of audit findings.
|
||||
- Other `//nolint:` directives require a `// NOLINT:` rationale comment AND must be approved during code review
|
||||
- Fast iteration during development: `go vet ./...` type-checks and catches correctness issues without producing binaries (analogous to `cargo check`) — reserve `golangci-lint` for pre-commit
|
||||
|
||||
> **Logging:** Never use `fmt.Println` or `log.Printf` in production service code — these produce unstructured output. Use `log/slog` (stdlib, Go 1.21+) or the project's chosen adapter. See `logging-and-observability-principles.md` for the required library and patterns.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Project Structure — Go Backend @project-structure-go-backend.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Logging and Observability Mandate
|
||||
|
||||
### Observability Overview
|
||||
|
||||
Observability is built on **three pillars** — this mandate governs Logs. For Metrics and Health Checks, see Monitoring and Alerting Principles @monitoring-and-alerting-principles.md.
|
||||
|
||||
1. **Logs** — *What happened* (events, errors, state changes) — governed by this file
|
||||
2. **Metrics** — *How much / how many* (quantitative measurements, RED/USE) — see `monitoring-and-alerting-principles.md`
|
||||
3. **Traces** — *How did it happen* (request flow through system) — platform-level concern (OpenTelemetry, Jaeger)
|
||||
|
||||
### Universal Requirement: All Operations Must Be Logged
|
||||
|
||||
**Every operation entry point MUST include logging. No exceptions.**
|
||||
|
||||
**What constitutes an "operation" (mandatory logging):**
|
||||
- API endpoints and request handlers
|
||||
- Background jobs and queue workers
|
||||
- Event handlers and message consumers
|
||||
- Scheduled tasks and cron jobs
|
||||
- CLI commands
|
||||
- External service calls (to third-party APIs)
|
||||
- Database transactions
|
||||
|
||||
**What is NOT an operation (no direct logging):**
|
||||
- Pure business logic functions (called within operations)
|
||||
- Utility and helper functions
|
||||
- Data transformations and validators
|
||||
|
||||
**Minimum logging requirement (3 points):**
|
||||
1. **Operation start:** Log at entry with context (correlationId, userId, operation name)
|
||||
2. **Operation success:** Log completion with duration and result identifiers
|
||||
3. **Operation failure:** Log error with full context (correlationId, error details, stack trace)
|
||||
|
||||
**Mandatory context in all logs:**
|
||||
- `correlationId`: UUID for tracing across services
|
||||
- `operation`: Clear operation name (e.g., "create_order", "process_payment")
|
||||
- `duration`: Execution time in milliseconds
|
||||
- `userId`: Actor who triggered the operation (when applicable)
|
||||
- `error`: Full error context on failures
|
||||
|
||||
**Enforcement strategy:**
|
||||
Prefer middleware, decorators, or framework interceptors for automatic operation logging rather than manual logging in every handler. This ensures coverage without code duplication.
|
||||
|
||||
**When implementing any operation entry point, you MUST add logging before proceeding with implementation.**
|
||||
|
||||
### Implementation Guide
|
||||
|
||||
For log levels, structured logging patterns, language-specific implementations, security considerations, and code examples, see Logging and Observability Principles @logging-and-observability-principles.md.
|
||||
|
||||
### Related Principles
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Monitoring and Alerting Principles @monitoring-and-alerting-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
@@ -0,0 +1,361 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing logging, working with loggers, or setting up observability for operations (API handlers, database queries, background jobs, external API calls)
|
||||
---
|
||||
|
||||
## Logging and Observability Principles
|
||||
|
||||
> **⚠️ Prerequisite:** All operations MUST be logged per Logging and Observability Mandate @logging-and-observability-mandate.md. This guide provides implementation patterns only.
|
||||
|
||||
|
||||
### Logging Standards
|
||||
|
||||
#### Log Levels (Standard Priority)
|
||||
|
||||
Use consistent log levels across all services:
|
||||
|
||||
| Level | When to Use | Examples |
|
||||
| --------- | --------------------------------------- | -------------------------------------------------------- |
|
||||
| **TRACE** | Extremely detailed diagnostic info | Function entry/exit, variable states (dev only) |
|
||||
| **DEBUG** | Detailed flow for debugging | Query execution, cache hits/misses, state transitions |
|
||||
| **INFO** | General informational messages | Request started, task created, user logged in |
|
||||
| **WARN** | Potentially harmful situations | Deprecated API usage, fallback triggered, retry attempt |
|
||||
| **ERROR** | Error events that allow app to continue | Request failed, external API timeout, validation failure |
|
||||
| **FATAL** | Severe errors causing shutdown | Database unreachable, critical config missing |
|
||||
|
||||
#### Logging Rules
|
||||
|
||||
**1. Every request/operation must log:**
|
||||
```
|
||||
|
||||
// Start of operation
|
||||
log.Info("creating task",
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
"title", task.Title,
|
||||
)
|
||||
|
||||
// Success
|
||||
log.Info("task created successfully",
|
||||
"correlationId", correlationID,
|
||||
"taskId", task.ID,
|
||||
"duration", time.Since(start),
|
||||
)
|
||||
|
||||
// Error
|
||||
log.Error("failed to create task",
|
||||
"correlationId", correlationID,
|
||||
"error", err,
|
||||
"userId", userID,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
**2. Always include context:**
|
||||
- `correlationId`: Trace requests across services (UUID)
|
||||
- `userId`: Who triggered the action
|
||||
- `duration`: Operation timing (milliseconds)
|
||||
- `error`: Error details (if failed)
|
||||
|
||||
|
||||
**3. Structured logging only** (no string formatting):
|
||||
```
|
||||
|
||||
// ✅ Structured
|
||||
log.Info("user login", "userId", userID, "ip", clientIP)
|
||||
|
||||
// ❌ String formatting
|
||||
log.Info(fmt.Sprintf("User %s logged in from %s", userID, clientIP))
|
||||
|
||||
```
|
||||
|
||||
**4. Security - Never log:**
|
||||
- Passwords or password hashes
|
||||
- API keys or tokens
|
||||
- Credit card numbers
|
||||
- PII in production logs (email/phone only if necessary and sanitized)
|
||||
- Full request/response bodies (unless DEBUG level in non-prod)
|
||||
|
||||
**5. Performance - Never log in hot paths:**
|
||||
- Inside tight loops
|
||||
- Per-item processing in batch operations (use summary instead)
|
||||
- Synchronous logging in latency-critical paths
|
||||
|
||||
**Best Practice:** "Use logger middleware redaction (e.g., pino-redact, zap masking) rather than manual string manipulation."
|
||||
|
||||
#### Language-Specific Implementations
|
||||
|
||||
##### Go (using slog standard library)
|
||||
```
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// Configure logger
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo, // Production default
|
||||
}))
|
||||
|
||||
// Usage
|
||||
logger.Info("operation started",
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
)
|
||||
|
||||
logger.Error("operation failed",
|
||||
"correlationId", correlationID,
|
||||
"error", err,
|
||||
"retryCount", retries,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### TypeScript/Node.js (using pino)
|
||||
```
|
||||
|
||||
import pino from 'pino';
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
});
|
||||
|
||||
logger.info({
|
||||
correlationId,
|
||||
userId,
|
||||
duration: Date.now() - startTime,
|
||||
}, 'task created successfully');
|
||||
|
||||
logger.error({
|
||||
correlationId,
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
}, 'failed to create task');
|
||||
|
||||
```
|
||||
|
||||
##### Python (using structlog)
|
||||
```python
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
logger.info("task_created",
|
||||
correlation_id=correlation_id,
|
||||
user_id=user_id,
|
||||
task_id=task.id,
|
||||
)
|
||||
|
||||
logger.error("task_creation_failed",
|
||||
correlation_id=correlation_id,
|
||||
error=str(err),
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Log Patterns by Operation Type
|
||||
|
||||
##### API Request/Response
|
||||
```
|
||||
|
||||
// Request received
|
||||
log.Info("request received",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
)
|
||||
|
||||
// Request completed
|
||||
log.Info("request completed",
|
||||
"correlationId", correlationID,
|
||||
"status", statusCode,
|
||||
"duration", duration,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Database Operations
|
||||
```
|
||||
|
||||
// Query start (DEBUG level)
|
||||
log.Debug("executing query",
|
||||
"correlationId", correlationID,
|
||||
"query", "SELECT * FROM tasks WHERE user_id = $1",
|
||||
)
|
||||
|
||||
// Query success (DEBUG level)
|
||||
log.Debug("query completed",
|
||||
"correlationId", correlationID,
|
||||
"rowsReturned", len(results),
|
||||
"duration", duration,
|
||||
)
|
||||
|
||||
// Query error (ERROR level)
|
||||
log.Error("query failed",
|
||||
"correlationId", correlationID,
|
||||
"error", err,
|
||||
"query", "SELECT * FROM tasks WHERE user_id = $1",
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### External API Calls
|
||||
```
|
||||
|
||||
// Call start
|
||||
log.Info("calling external API",
|
||||
"correlationId", correlationID,
|
||||
"service", "email-provider",
|
||||
"endpoint", "/send",
|
||||
)
|
||||
|
||||
// Retry (WARN level)
|
||||
log.Warn("retrying external API call",
|
||||
"correlationId", correlationID,
|
||||
"service", "email-provider",
|
||||
"attempt", retryCount,
|
||||
"error", err,
|
||||
)
|
||||
|
||||
// Circuit breaker open (WARN level)
|
||||
log.Warn("circuit breaker opened",
|
||||
"correlationId", correlationID,
|
||||
"service", "email-provider",
|
||||
"failureCount", failures,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Background Jobs
|
||||
```
|
||||
|
||||
// Job start
|
||||
log.Info("job started",
|
||||
"jobId", jobID,
|
||||
"jobType", "email-digest",
|
||||
)
|
||||
|
||||
// Progress (INFO level - periodic, not per-item)
|
||||
log.Info("job progress",
|
||||
"jobId", jobID,
|
||||
"processed", 1000,
|
||||
"total", 5000,
|
||||
"percentComplete", 20,
|
||||
)
|
||||
|
||||
// Job complete
|
||||
log.Info("job completed",
|
||||
"jobId", jobID,
|
||||
"duration", duration,
|
||||
"itemsProcessed", count,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
##### Error Scenarios
|
||||
```
|
||||
|
||||
// Recoverable error (ERROR level)
|
||||
log.Error("validation failed",
|
||||
"correlationId", correlationID,
|
||||
"userId", userID,
|
||||
"error", "invalid email format",
|
||||
"input", sanitizedInput, // Sanitized!
|
||||
)
|
||||
|
||||
// Fatal error (FATAL level)
|
||||
log.Fatal("critical dependency unavailable",
|
||||
"error", err,
|
||||
"dependency", "database",
|
||||
"action", "shutting down",
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
#### Environment-Specific Configuration
|
||||
|
||||
| Environment | Level | Format | Destination |
|
||||
| --------------- | ----- | ---------------- | ----------------------- |
|
||||
| **Development** | DEBUG | Pretty (colored) | Console |
|
||||
| **Staging** | INFO | JSON | Stdout → CloudWatch/GCP |
|
||||
| **Production** | INFO | JSON | Stdout → CloudWatch/GCP |
|
||||
|
||||
**Configuration (Go example):**
|
||||
```
|
||||
|
||||
func configureLogger() *slog.Logger {
|
||||
var handler slog.Handler
|
||||
|
||||
level := slog.LevelInfo
|
||||
if os.Getenv("ENV") == "development" {
|
||||
level = slog.LevelDebug
|
||||
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
} else {
|
||||
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
|
||||
return slog.New(handler)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Testing Logs
|
||||
|
||||
**Unit tests:** Capture and assert on log output
|
||||
```
|
||||
|
||||
// Go example
|
||||
func TestUserLogin(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||
|
||||
// Test operation
|
||||
service := NewUserService(logger, mockStore)
|
||||
err := service.Login(ctx, email, password)
|
||||
|
||||
// Assert logs
|
||||
require.NoError(t, err)
|
||||
logs := buf.String()
|
||||
assert.Contains(t, logs, "user login successful")
|
||||
assert.Contains(t, logs, email)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Monitoring Integration
|
||||
|
||||
**Correlation IDs:**
|
||||
- Generate at ingress (API gateway, first handler)
|
||||
- Propagate through all services
|
||||
- Include in all logs, errors, and traces
|
||||
- Format: UUID v4
|
||||
|
||||
**Log aggregation:**
|
||||
- Ship to centralized system (CloudWatch, GCP Logs, Datadog)
|
||||
- Index by: correlationId, userId, level, timestamp
|
||||
- Alert on ERROR/FATAL patterns
|
||||
- Dashboard: request rates, error rates, latency
|
||||
|
||||
#### Checklist for Every Feature
|
||||
|
||||
- [ ] All public operations log INFO on start
|
||||
- [ ] All operations log INFO/ERROR on complete/failure
|
||||
- [ ] All logs include correlationId
|
||||
- [ ] No sensitive data in logs
|
||||
- [ ] Structured logging (key-value pairs)
|
||||
- [ ] Appropriate log level used
|
||||
- [ ] Error logs include error details
|
||||
- [ ] Performance-critical paths use DEBUG level
|
||||
|
||||
### Related Principles
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Monitoring and Alerting Principles @monitoring-and-alerting-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing health checks, metrics instrumentation, error tracking integration, or production observability code
|
||||
---
|
||||
|
||||
## Monitoring and Alerting Principles
|
||||
|
||||
> **Scope:** This rule covers what the agent **implements in code**. Organizational
|
||||
> concerns (SLO/SLI definitions, on-call rotation, alert escalation) are out of scope —
|
||||
> those are team/org decisions, not code generation concerns.
|
||||
|
||||
### Health Checks
|
||||
|
||||
**Every service must expose health check endpoints:**
|
||||
|
||||
- **`/health` (Liveness)** — "Is the process alive?"
|
||||
- Returns 200 if the process is running
|
||||
- No dependency checks (database, cache, etc.)
|
||||
- Used by orchestrators to decide whether to restart
|
||||
|
||||
- **`/ready` (Readiness)** — "Can the service accept traffic?"
|
||||
- Checks all critical dependencies (database, cache, message queue)
|
||||
- Returns 503 if any dependency is unavailable
|
||||
- Used by load balancers to route traffic
|
||||
|
||||
**Rules:**
|
||||
- Health checks must be fast (< 1 second)
|
||||
- Health checks must not have side effects
|
||||
- Separate liveness from readiness — they serve different purposes
|
||||
|
||||
### Metrics Instrumentation
|
||||
|
||||
**Instrument code using the RED method for services:**
|
||||
- **Rate** — requests per second
|
||||
- **Errors** — error count/rate
|
||||
- **Duration** — request latency (use histograms, not averages)
|
||||
|
||||
**Instrument code using the USE method for resources:**
|
||||
- **Utilization** — how much of the resource is used
|
||||
- **Saturation** — how much work is queued
|
||||
- **Errors** — error count for the resource
|
||||
|
||||
**Rules:**
|
||||
- Use counters for things that only go up (requests, errors)
|
||||
- Use gauges for things that go up and down (connections, queue depth)
|
||||
- Use histograms for distributions (latency, response size)
|
||||
- Label metrics consistently (service, method, status_code)
|
||||
- Don't create high-cardinality labels (no user IDs as labels)
|
||||
|
||||
### Error Tracking Integration
|
||||
|
||||
- **Capture unhandled exceptions** with full stack traces
|
||||
- **Include context** — user ID, request ID, correlation ID
|
||||
- **Group errors** by root cause, not by instance
|
||||
- **Set severity levels** based on user impact
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
- **Circuit breakers** for external dependencies — stop calling failing services
|
||||
- **Fallbacks** for non-critical features — serve cached data, show reduced UI
|
||||
- **Timeouts** on all external calls — never wait indefinitely
|
||||
- **Retry with backoff** for transient failures — exponential backoff with jitter
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
This rule is tool-agnostic. Whether the project uses Datadog, LGTM stack, Sentry,
|
||||
New Relic, or CloudWatch — the code patterns (health checks, metrics, error tracking)
|
||||
are the same. The specific client library belongs in project-level configuration.
|
||||
|
||||
### Monitoring Checklist
|
||||
|
||||
- [ ] Health check endpoints implemented (/health and /ready)?
|
||||
- [ ] Liveness probe has no dependency checks?
|
||||
- [ ] Readiness probe checks all critical dependencies?
|
||||
- [ ] Key operations instrumented with RED/USE metrics?
|
||||
- [ ] No high-cardinality metric labels?
|
||||
- [ ] Unhandled exceptions captured with context?
|
||||
- [ ] Circuit breakers on external dependencies?
|
||||
- [ ] Timeouts on all external calls?
|
||||
|
||||
### Related Principles
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on performance optimization, profiling, benchmarking, or performance-critical code paths
|
||||
---
|
||||
|
||||
## Performance Optimization Principles
|
||||
|
||||
### Measure Before Optimizing
|
||||
|
||||
**"Premature optimization is the root of all evil" - Donald Knuth**
|
||||
|
||||
**Process:**
|
||||
|
||||
1. **Measure:** Profile to find actual bottlenecks (don't guess)
|
||||
2. **Identify:** Find the 20% of code consuming 80% of resources
|
||||
3. **Optimize:** Improve that specific bottleneck
|
||||
4. **Measure again:** Verify improvement with benchmarks
|
||||
5. **Repeat:** Only if still not meeting performance goals
|
||||
|
||||
**Don't optimize:**
|
||||
|
||||
- Code that's "fast enough" for requirements
|
||||
- Code that's rarely executed
|
||||
- Without measurable performance problem
|
||||
|
||||
### Choose Appropriate Data Structures
|
||||
|
||||
**Selection matters:**
|
||||
|
||||
- Hash map: O(1) lookup, unordered
|
||||
- Array/list: O(1) index access, O(n) search, ordered
|
||||
- Binary tree: O(log n) operations, sorted order
|
||||
- Set: O(1) membership testing, unique elements
|
||||
|
||||
**Wrong choice causes performance degradation:**
|
||||
|
||||
- Using array for lookups: O(n) when O(1) possible with hash map
|
||||
- Using list for sorted data: O(n log n) sort vs O(log n) tree operations
|
||||
|
||||
### Avoid Premature Abstraction
|
||||
|
||||
**Abstraction has costs:**
|
||||
|
||||
- Runtime overhead (indirection, virtual dispatch, dynamic resolution)
|
||||
- Cognitive overhead (understanding layers of abstraction)
|
||||
- Maintenance overhead (changes ripple through abstractions)
|
||||
|
||||
**Start concrete, abstract when pattern emerges:**
|
||||
|
||||
- Write straightforward code first
|
||||
- Identify duplication and common patterns
|
||||
- Abstract only when there's clear benefit
|
||||
- Don't add "for future flexibility" without evidence
|
||||
|
||||
### Optimization Techniques
|
||||
|
||||
**Caching:**
|
||||
|
||||
- Store expensive computation results
|
||||
- Cache database queries, API responses, rendered templates
|
||||
- Use appropriate cache invalidation strategy
|
||||
- Set TTL (time-to-live) for cache entries
|
||||
|
||||
**Lazy Loading:**
|
||||
|
||||
- Compute only when needed
|
||||
- Load data on-demand, not upfront
|
||||
- Defer expensive operations until required
|
||||
|
||||
**Batching:**
|
||||
|
||||
- Process multiple items together
|
||||
- Batch database queries (N queries → 1 query)
|
||||
- Batch API requests where possible
|
||||
|
||||
**Async I/O:**
|
||||
|
||||
- Don't block on I/O operations
|
||||
- Use async/await for concurrent I/O
|
||||
- Process multiple I/O operations in parallel
|
||||
|
||||
**Connection Pooling:**
|
||||
|
||||
- Reuse expensive resources (database connections, HTTP connections)
|
||||
- See Resource and Memory Management Principles @resources-and-memory-management-principles.md for pool sizing and configuration
|
||||
|
||||
### Performance Optimization Checklist
|
||||
|
||||
- [ ] Is there a measured performance problem (not a guess)?
|
||||
- [ ] Have you profiled to find the actual bottleneck?
|
||||
- [ ] Are appropriate data structures chosen for the access pattern?
|
||||
- [ ] Are expensive operations cached with proper invalidation?
|
||||
- [ ] Are batch operations used instead of N+1 queries?
|
||||
- [ ] Are I/O operations non-blocking where appropriate?
|
||||
- [ ] Have you measured improvement after optimization?
|
||||
|
||||
### Related Principles
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Flutter or React Native mobile application, setting up mobile project structure
|
||||
---
|
||||
|
||||
## Flutter/Mobile Layout
|
||||
|
||||
Use this structure for Flutter or React Native mobile applications. The vertical slice principle applies — features are self-contained modules.
|
||||
|
||||
```
|
||||
apps/
|
||||
mobile/ # Mobile application source code (Flutter)
|
||||
lib/
|
||||
core/ # Foundational concerns (the "Framework")
|
||||
network/ # HTTP client setup, interceptors
|
||||
api_client.dart # Dio/http client with base config
|
||||
api_interceptor.dart # Auth token, logging interceptors
|
||||
storage/ # Local storage abstractions
|
||||
local_storage.dart # SharedPreferences / Hive wrapper
|
||||
theme/ # App theming
|
||||
app_theme.dart # ThemeData, color schemes
|
||||
app_typography.dart # TextStyles, font families
|
||||
router/ # Navigation / routing
|
||||
app_router.dart # GoRouter config (@riverpod function)
|
||||
app_router.g.dart # Generated provider code
|
||||
constants/ # App-wide constants
|
||||
|
||||
features/ # Business Features (Vertical Slices)
|
||||
task/ # Task management feature
|
||||
# --- Presentation ---
|
||||
screens/
|
||||
task_list_screen.dart # Full screen (route target, ConsumerWidget)
|
||||
task_detail_screen.dart
|
||||
widgets/ # Feature-specific widgets
|
||||
task_card.dart
|
||||
task_form.dart
|
||||
|
||||
# --- State Management (Riverpod 3) ---
|
||||
state/
|
||||
task_notifier.dart # @riverpod class (Notifier/AsyncNotifier)
|
||||
task_notifier.g.dart # Generated provider code (build_runner)
|
||||
task_state.dart # State classes (generated via freezed)
|
||||
|
||||
# --- Domain (Business Logic) ---
|
||||
models/
|
||||
task.dart # Domain model (freezed)
|
||||
task.g.dart # Generated code (json_serializable)
|
||||
task.freezed.dart # Generated code (freezed)
|
||||
logic/
|
||||
task_logic.dart # Pure business rules
|
||||
|
||||
# --- Data (I/O Abstraction) ---
|
||||
repository/
|
||||
task_repository.dart # Abstract repository interface
|
||||
task_repository_impl.dart # Implementation (API + cache)
|
||||
task_repository_mock.dart # Mock for testing
|
||||
api/
|
||||
task_api.dart # REST API calls (Dio)
|
||||
auth/ # Authentication feature
|
||||
...
|
||||
settings/ # Settings feature
|
||||
...
|
||||
|
||||
shared/ # Shared across features
|
||||
widgets/ # Reusable UI components (NO domain logic)
|
||||
app_button.dart
|
||||
app_text_field.dart
|
||||
loading_indicator.dart
|
||||
utils/ # Pure utility functions
|
||||
date_formatter.dart
|
||||
validators.dart
|
||||
models/ # Shared data models
|
||||
api_response.dart
|
||||
pagination.dart
|
||||
|
||||
test/ # Test directory (mirrors lib/ layout)
|
||||
features/
|
||||
task/
|
||||
state/
|
||||
task_notifier_test.dart # Unit tests for state logic
|
||||
logic/
|
||||
task_logic_test.dart # Unit tests for pure business rules
|
||||
widgets/
|
||||
task_card_test.dart # Widget tests
|
||||
api/
|
||||
task_api_test.dart # API integration tests
|
||||
integration_test/ # Integration / E2E tests
|
||||
task_flow_test.dart
|
||||
|
||||
pubspec.yaml # Dependencies
|
||||
analysis_options.yaml # Lint rules (includes riverpod_lint)
|
||||
```
|
||||
|
||||
**Key differences from web frontend:**
|
||||
- `screens/` replaces `views/` — mobile uses screen-based navigation
|
||||
- `widgets/` replaces `components/` — Flutter's terminology
|
||||
- `state/` replaces `store/` — Riverpod 3 `@riverpod` class (Notifier/AsyncNotifier) instead of Pinia/Redux
|
||||
- `*.g.dart` files in `state/` — generated provider code from `riverpod_generator` + `build_runner`
|
||||
- `repository/` replaces `api/` — mobile often caches data locally
|
||||
- No separate `di/` directory — Riverpod 3 uses `ProviderScope` at the app root for dependency injection; no manual service locator needed
|
||||
|
||||
**Codegen artifacts (gitignore-able):**
|
||||
- `*.g.dart` — generated by `riverpod_generator` and `json_serializable`
|
||||
- `*.freezed.dart` — generated by `freezed`
|
||||
- Run `dart run build_runner build --delete-conflicting-outputs` after changes
|
||||
|
||||
**Test location:**
|
||||
|
||||
Tests live in the `test/` directory, mirroring the `lib/` layout. This is Flutter's default convention — `flutter test` discovers tests here automatically with no extra configuration.
|
||||
|
||||
- **Naming:** `*_test.dart` (unit), `*_integration_test.dart` (integration)
|
||||
- **Discovery:** `flutter test` (runs all tests in `test/`)
|
||||
- **Structure:** Mirror the feature directory structure from `lib/`
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Flutter Idioms and Patterns @flutter-idioms-and-patterns.md (Riverpod 3 patterns, widget idioms)
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Go backend project, setting up Go project structure, or organizing Go services and APIs
|
||||
---
|
||||
|
||||
## Go Backend Layout
|
||||
|
||||
Use this structure for Go backend applications. The vertical slice principle applies — features are packages, not layers.
|
||||
|
||||
```
|
||||
apps/
|
||||
backend/ # Backend application source code
|
||||
cmd/
|
||||
api/
|
||||
main.go # Entry point: Wires dependencies, router, starts server
|
||||
internal/ # Private application code
|
||||
platform/ # Foundational technical concerns (The "Framework")
|
||||
database/ # DB connection logic
|
||||
server/ # HTTP server setup (Router, Middleware)
|
||||
logger/ # Structured logging setup
|
||||
features/ # Business Features (Vertical Slices)
|
||||
task/ # Task management
|
||||
# --- Interface Definition ---
|
||||
service.go # The public API of this feature (Service struct)
|
||||
|
||||
# --- Delivery (HTTP) ---
|
||||
handler.go # HTTP Handlers
|
||||
handler_test.go # Component tests (httptest + mock service)
|
||||
|
||||
# --- Domain (Business Logic) ---
|
||||
logic.go # Core business logic methods
|
||||
logic_test.go # Unit tests (Pure functions + mock storage)
|
||||
models.go # Domain structs (Task, NewTaskRequest)
|
||||
errors.go # Feature-specific errors
|
||||
|
||||
# --- Storage (Data Access) ---
|
||||
storage.go # Storage Interface definition
|
||||
storage_pg.go # Postgres implementation
|
||||
postgres_integration_test.go # Integration tests (Real DB/Testcontainers)
|
||||
storage_mock.go # Mock implementation
|
||||
...
|
||||
order/ # Order management
|
||||
handler.go
|
||||
logic.go
|
||||
storage.go
|
||||
...
|
||||
```
|
||||
|
||||
**Key Go conventions:**
|
||||
- `cmd/` for entry points — each subdirectory is a separate binary
|
||||
- `internal/` for private packages — enforced by Go compiler (cannot be imported externally)
|
||||
- `platform/` for foundational concerns (database, server, logger)
|
||||
- `features/` for vertical business slices — each feature is self-contained
|
||||
- Tests live alongside the code they test (`_test.go` suffix)
|
||||
- `go.mod` at the root — single module for the entire application
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Go Idioms and Patterns @go-idioms-and-patterns.md (coding idioms, error handling, naming)
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Python backend project, setting up Python project structure, or organizing Python services and APIs
|
||||
---
|
||||
|
||||
## Python Backend Layout
|
||||
|
||||
Use this structure for Python backend applications. The vertical slice principle applies — features are packages, not technical layers.
|
||||
|
||||
```
|
||||
apps/
|
||||
backend/ # Backend application source code
|
||||
pyproject.toml # Project metadata, dependencies, tool configs
|
||||
src/
|
||||
yourapp/ # Importable package (src-layout, preferred)
|
||||
main.py # Entry point: creates app, wires dependencies, starts server
|
||||
platform/ # Foundational technical concerns (the "Framework")
|
||||
database.py # DB engine and session factory
|
||||
server.py # ASGI app setup (FastAPI/Starlette router, middleware)
|
||||
logger.py # structlog / logging configuration
|
||||
config.py # Settings (pydantic-settings BaseSettings)
|
||||
features/ # Business Features (Vertical Slices)
|
||||
task/ # Task management
|
||||
__init__.py
|
||||
|
||||
# --- Public API ---
|
||||
service.py # TaskService class (public API of this feature)
|
||||
|
||||
# --- Delivery (HTTP) ---
|
||||
router.py # FastAPI APIRouter with HTTP endpoints
|
||||
schemas.py # Pydantic request/response models (API boundary only)
|
||||
test_router.py # Component tests (TestClient + mock service)
|
||||
|
||||
# --- Domain (Business Logic) ---
|
||||
logic.py # Pure domain functions (no I/O)
|
||||
models.py # Domain dataclasses (Task, Priority, etc.)
|
||||
errors.py # Feature-specific exceptions
|
||||
test_logic.py # Unit tests (pure functions — no mocks needed)
|
||||
|
||||
# --- Storage (Data Access) ---
|
||||
storage.py # TaskStorage Protocol (interface)
|
||||
storage_pg.py # PostgreSQL implementation (asyncpg / SQLAlchemy)
|
||||
storage_mock.py # InMemoryTaskStorage (test implementation)
|
||||
test_storage_pg.py # Integration tests (real DB via testcontainers)
|
||||
|
||||
order/ # Order management
|
||||
service.py
|
||||
router.py
|
||||
schemas.py
|
||||
logic.py
|
||||
models.py
|
||||
storage.py
|
||||
storage_pg.py
|
||||
storage_mock.py
|
||||
...
|
||||
tests/ # Optional: top-level E2E tests (cross-feature boundaries)
|
||||
e2e/
|
||||
test_create_task_api.e2e.py
|
||||
```
|
||||
|
||||
**Key Python conventions:**
|
||||
|
||||
- **`src/` layout** is strongly preferred — prevents accidental imports of the development tree and matches packaging best practices (PEP 517 / `pypa/build`)
|
||||
- **`pyproject.toml`** is the single configuration file for `ruff`, `mypy`, `pytest`, `bandit`, and packaging metadata — do not create `setup.cfg`, `.flake8`, or `mypy.ini` files
|
||||
- **Feature packages** use `__init__.py` — keep it empty or use it solely to re-export the feature's public API
|
||||
- **`platform/`** holds technical infrastructure that features depend on (database sessions, configuration, logging); features never import each other's `platform/` code directly
|
||||
- **Tests co-locate** with the code they test (`test_*.py` in the same directory) except for E2E tests which go in `tests/e2e/`
|
||||
- **`storage_mock.py`** is a production-quality in-memory implementation, not a `unittest.Mock` — it is the recommended test double for unit and integration tests
|
||||
|
||||
### Dependency Wiring (main.py)
|
||||
|
||||
```python
|
||||
# src/yourapp/main.py
|
||||
from yourapp.platform.database import create_engine
|
||||
from yourapp.platform.server import create_app
|
||||
from yourapp.features.task.storage_pg import PostgresTaskStorage
|
||||
from yourapp.features.task.service import TaskService
|
||||
from yourapp.features.task.router import build_router as build_task_router
|
||||
|
||||
def create_application() -> FastAPI:
|
||||
engine = create_engine()
|
||||
task_storage = PostgresTaskStorage(engine=engine)
|
||||
task_service = TaskService(storage=task_storage)
|
||||
|
||||
app = create_app()
|
||||
app.include_router(build_task_router(service=task_service))
|
||||
return app
|
||||
|
||||
app = create_application()
|
||||
```
|
||||
|
||||
### Configuration (platform/config.py)
|
||||
|
||||
```python
|
||||
# src/yourapp/platform/config.py
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class Settings(BaseSettings):
|
||||
database_url: str
|
||||
secret_key: str
|
||||
debug: bool = False
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
```
|
||||
|
||||
### pyproject.toml Baseline
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "yourapp"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/yourapp"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "UP", "S", "B", "ANN"]
|
||||
|
||||
[tool.mypy]
|
||||
strict = true
|
||||
python_version = "3.11"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["src"]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.bandit]
|
||||
skips = ["B101"] # assert statements allowed in test files
|
||||
```
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Python Idioms and Patterns @python-idioms-and-patterns.md (coding idioms, error handling, naming)
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a Rust or Cargo project, setting up Rust project structure, or organizing Rust crates and workspaces
|
||||
---
|
||||
|
||||
## Rust/Cargo Layout
|
||||
|
||||
Use these structures for Rust projects. The vertical slice principle applies — features are modules, not layers.
|
||||
|
||||
### Single Binary Crate
|
||||
|
||||
For CLI tools, standalone servers, and single-purpose applications.
|
||||
|
||||
```
|
||||
project-root/
|
||||
Cargo.toml # Package manifest + dependencies
|
||||
build.rs # Build script (optional — for FFI, codegen)
|
||||
src/
|
||||
main.rs # Entry point: CLI parsing, server startup, wires dependencies
|
||||
lib.rs # Library root — re-exports public modules (enables integration testing)
|
||||
|
||||
# --- Foundational Concerns (The "Platform") ---
|
||||
config.rs # Configuration loading (env vars, config files)
|
||||
error.rs # Application-wide error types (thiserror enum)
|
||||
telemetry.rs # Tracing/logging setup (tracing, tracing-subscriber)
|
||||
|
||||
# --- Business Features (Vertical Slices) ---
|
||||
features/
|
||||
mod.rs # Re-exports feature modules
|
||||
task/
|
||||
mod.rs # Public API of this feature — re-exports types + service
|
||||
service.rs # Feature service (business logic orchestration)
|
||||
logic.rs # Pure business rules (no I/O, easily testable)
|
||||
models.rs # Domain structs (Task, NewTaskRequest)
|
||||
error.rs # Feature-specific error types
|
||||
repository.rs # Storage trait definition
|
||||
postgres.rs # Postgres implementation of repository trait
|
||||
mock.rs # Mock implementation for testing
|
||||
auth/
|
||||
mod.rs
|
||||
service.rs
|
||||
logic.rs
|
||||
models.rs
|
||||
...
|
||||
|
||||
# --- Delivery Layer (HTTP/gRPC) ---
|
||||
handlers/
|
||||
mod.rs
|
||||
task_handler.rs # HTTP handlers for task feature (axum/actix extractors)
|
||||
auth_handler.rs
|
||||
router.rs # Route definitions, middleware stack
|
||||
|
||||
tests/ # Integration tests (compiled as separate crate, access only pub API)
|
||||
common/
|
||||
mod.rs # Shared test fixtures, helpers, test database setup
|
||||
task_api_test.rs # Full API integration tests
|
||||
auth_flow_test.rs
|
||||
|
||||
benches/ # Benchmarks (criterion)
|
||||
task_benchmark.rs
|
||||
```
|
||||
|
||||
**Key Rust conventions:**
|
||||
- `src/lib.rs` + `src/main.rs` — enables integration testing against `lib.rs` public API
|
||||
- `mod.rs` re-exports — each feature's `mod.rs` is its public boundary
|
||||
- `error.rs` per feature — typed errors with `thiserror`, composed at app level with `#[from]`
|
||||
- `tests/` directory — integration tests see only the public API, enforcing encapsulation
|
||||
- No `controllers/` or `services/` at the top level — features are the top-level organization
|
||||
|
||||
---
|
||||
|
||||
### Library Crate
|
||||
|
||||
For publishable libraries, SDKs, and reusable components.
|
||||
|
||||
```
|
||||
project-root/
|
||||
Cargo.toml # Package manifest (lib target)
|
||||
src/
|
||||
lib.rs # Crate root — public API surface, module re-exports
|
||||
parser.rs # Public module
|
||||
ast.rs # Public module
|
||||
error.rs # Public error types (thiserror)
|
||||
internal/ # Private implementation details
|
||||
mod.rs
|
||||
optimizer.rs
|
||||
cache.rs
|
||||
|
||||
examples/ # Runnable examples (cargo run --example)
|
||||
basic_usage.rs
|
||||
advanced_config.rs
|
||||
|
||||
tests/ # Integration tests
|
||||
parsing_test.rs
|
||||
|
||||
benches/ # Benchmarks
|
||||
parser_benchmark.rs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cargo Workspace (Multi-Crate)
|
||||
|
||||
For complex projects with multiple internal crates. This is the recommended structure for projects where distinct subsystems benefit from separate compilation units and explicit dependency boundaries.
|
||||
|
||||
```
|
||||
project-root/
|
||||
Cargo.toml # [workspace] manifest — lists all members
|
||||
Cargo.lock # Locked dependency versions (committed for binaries)
|
||||
|
||||
crates/
|
||||
myapp/ # Main binary crate (entry point, wiring)
|
||||
Cargo.toml # Depends on other workspace crates
|
||||
src/
|
||||
main.rs # CLI parsing, server setup
|
||||
lib.rs
|
||||
config.rs
|
||||
error.rs
|
||||
api/ # API / protocol handling
|
||||
mod.rs
|
||||
routes.rs # Route definitions
|
||||
handlers.rs # Request handlers
|
||||
|
||||
myapp-parser/ # Domain-specific engine crate
|
||||
Cargo.toml
|
||||
build.rs # Build script (optional — for FFI, codegen)
|
||||
src/
|
||||
lib.rs
|
||||
parser.rs # Core parsing logic
|
||||
transform.rs # Data transformations
|
||||
cache.rs # Caching layer
|
||||
tests/ # Per-crate integration tests (public API only)
|
||||
parser_integration.rs
|
||||
|
||||
myapp-client/ # External service client crate
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs
|
||||
client.rs # Client implementation
|
||||
lifecycle.rs # Connection management
|
||||
retry.rs # Retry and resilience logic
|
||||
|
||||
myapp-search/ # Search / query engine crate
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs
|
||||
search.rs # Search implementation
|
||||
filter.rs # Result filtering
|
||||
tests/ # Per-crate integration tests
|
||||
search_integration.rs
|
||||
|
||||
myapp-common/ # Shared types and utilities
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs
|
||||
types.rs # Shared domain types
|
||||
error.rs # Common error types
|
||||
tests/ # Per-crate integration tests
|
||||
common_integration.rs
|
||||
|
||||
tests/ # Workspace-level integration tests
|
||||
integration/
|
||||
full_pipeline_test.rs # End-to-end tests across crates
|
||||
|
||||
config/ # Default configuration
|
||||
myapp.config.default.json
|
||||
```
|
||||
|
||||
**Key differences from Go/Node layouts:**
|
||||
- `crates/` replaces `apps/` — Cargo workspace with explicit inter-crate dependencies in `Cargo.toml`
|
||||
- Each crate has its own `Cargo.toml` — dependencies are scoped, compile separately, enforces boundaries
|
||||
- `build.rs` — Rust-specific build script for FFI compilation (C grammars for tree-sitter)
|
||||
- `Cargo.lock` committed — standard practice for binary projects (not for library crates)
|
||||
- No `node_modules/`, `vendor/` — dependencies are managed by Cargo globally in `~/.cargo/`
|
||||
- Feature flags in `Cargo.toml` — use `[features]` for optional functionality instead of build-time env vars
|
||||
|
||||
### Test Organization in Rust
|
||||
|
||||
Rust has a compiler-enforced two-tier test system that differs fundamentally from Go/TS:
|
||||
|
||||
**Unit Tests — Inline (Not Separate Files)**
|
||||
- Convention: `#[cfg(test)] mod tests` block **at the bottom of each `.rs` file**
|
||||
- Tests are compiled conditionally — stripped from production builds
|
||||
- Can access **private** functions via `use super::*`
|
||||
- This is NOT a shortcut — it is the official, idiomatic Rust convention
|
||||
- Do NOT create separate `*_test.rs` files for unit tests
|
||||
|
||||
**Integration Tests — `tests/` Directory**
|
||||
- Location: `tests/` directory **at each crate's root** (next to `src/`)
|
||||
- Each `.rs` file in `tests/` is compiled as its own **separate crate**
|
||||
- Can only access the crate's **public API** (`use my_crate::...`)
|
||||
- No `#[cfg(test)]` needed — Cargo treats `tests/` as test-only automatically
|
||||
- Shared test helpers go in `tests/common/mod.rs` (not `tests/common.rs`)
|
||||
|
||||
**Workspace-level tests:**
|
||||
- For cross-crate integration / E2E tests, place in `tests/` at the workspace root
|
||||
- Or create a dedicated test crate in the workspace (e.g. `tests-integration/` — tokio pattern)
|
||||
|
||||
**Example per-crate layout with tests:**
|
||||
```
|
||||
crates/pathfinder-search/
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs # Contains #[cfg(test)] mod tests { ... } at bottom
|
||||
search.rs # Contains #[cfg(test)] mod tests { ... } at bottom
|
||||
filter.rs # Contains #[cfg(test)] mod tests { ... } at bottom
|
||||
mock.rs # Mock implementations
|
||||
tests/ # Integration tests (separate crate, public API only)
|
||||
search_integration.rs # Tests search through public lib.rs API
|
||||
common/
|
||||
mod.rs # Shared test fixtures and helpers
|
||||
```
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Rust Idioms and Patterns @rust-idioms-and-patterns.md
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working on a web frontend project using Vue, React, Svelte, or similar frameworks, setting up frontend project structure
|
||||
---
|
||||
|
||||
## Vue/React Frontend Layout
|
||||
|
||||
Use this structure for web frontend applications. The vertical slice principle applies — features are self-contained modules, not scattered across global folders.
|
||||
|
||||
```
|
||||
apps/
|
||||
frontend/ # Frontend application source code
|
||||
src/
|
||||
assets/ # Fonts, Images
|
||||
features/ # Business features organized as vertical slices. Each feature is SELF-CONTAINED.
|
||||
task/ # Task management
|
||||
components/ # Task Feature-specific components go HERE, DON'T Put feature components in top-level folders
|
||||
TaskForm.vue
|
||||
TaskListItem.vue
|
||||
TaskFilters.vue
|
||||
TaskInput.vue
|
||||
TaskInput.spec.ts # Component unit tests
|
||||
store/
|
||||
task.store.ts # Pinia store
|
||||
task.store.spec.ts # Store unit tests
|
||||
api/
|
||||
task.api.ts # interface TaskAPI
|
||||
task.api.backend.ts # Production implementation
|
||||
task.api.mock.ts # Test implementation
|
||||
services/
|
||||
task.service.ts # Business logic
|
||||
task.service.spec.ts # Logic unit tests
|
||||
types/ # TS Interfaces for tasks (e.g. CreateTaskDTO interfaces)
|
||||
composables/ # Task Feature-specific hooks (e.g. useTaskFilters.ts)
|
||||
index.ts # Public exports. Export ONLY what's needed by `views/`
|
||||
order/
|
||||
composables/ # Global reactive logic (useAuth, useTheme)
|
||||
components/ # Shared Component (Buttons, Inputs) - Dumb UI, No Domain Logic. DON'T Put feature components HERE
|
||||
ui/ # UI Components (Atoms & Molecules) Pure, reusable UI primitives. NO domain logic, NO feature knowledge.
|
||||
BaseButton.vue
|
||||
BaseButton.spec.ts # Unit tests for button states
|
||||
types.ts # Shared UI types/interfaces
|
||||
index.ts # Barrel export for easy imports
|
||||
layout/ # Layout Components (Organisms) Composite UI structures that combine multiple UI components. Still reusable, but more complex.
|
||||
AppHeader.vue # Application header with nav, logo, user menu
|
||||
AppSidebar.vue # Sidebar navigation structure
|
||||
ErrorBoundary.vue # Error display wrapper
|
||||
EmptyState.vue # Empty list placeholder
|
||||
layouts/ # App shells (Sidebar, Navbar wrappers)
|
||||
MainLayout.vue # Contains Navbar, Sidebar, Footer
|
||||
AuthLayout.vue # Minimal layout for Login/Register
|
||||
views/ # Route entry points (The "Glue")
|
||||
HomeView.vue # Imports from features/analytics
|
||||
TaskView.vue # Imports from features/task
|
||||
utils/ # Pure, stateless helper functions. No domain knowledge, no Vue reactivity, (e.g. date-fns wrappers, math).
|
||||
router/ # Route definitions
|
||||
plugins/ # Library configs (Axios, I18n)
|
||||
App.vue # Root component (hosts <router-view>)
|
||||
main.ts # Entry point (bootstraps plugins & mounts app)
|
||||
...
|
||||
```
|
||||
|
||||
**Key frontend conventions:**
|
||||
- `features/` for vertical slices — each feature exports only what `views/` needs via `index.ts`
|
||||
- `components/ui/` for **shared** dumb UI (buttons, inputs) — NO domain logic, NO feature knowledge
|
||||
- `components/layout/` for composite UI structures (headers, sidebars)
|
||||
- `views/` are route entry points — they compose features, not implement them
|
||||
- Feature components live **inside** the feature, NOT in top-level `components/`
|
||||
- `composables/` at root for global reactive logic; feature-specific hooks inside the feature
|
||||
|
||||
> This structure applies equally to React (.tsx), Vue (.vue), and Svelte (.svelte). Replace component file extensions and state management (Redux/Zustand for React, Pinia for Vue) as needed.
|
||||
|
||||
### Related Principles
|
||||
- Project Structure @project-structure.md (core philosophy)
|
||||
- Vue Idioms and Patterns @vue-idioms-and-patterns.md (Composition API, Pinia, composables)
|
||||
- TypeScript Idioms and Patterns @typescript-idioms-and-patterns.md (type system, async patterns)
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
> **This file is the SINGLE SOURCE OF TRUTH for project organization.**
|
||||
> All other rules and workflows that reference paths should defer to this file.
|
||||
> To adapt the setup for a different project type, edit this file only.
|
||||
|
||||
## Project Structure
|
||||
|
||||
**Project Structure Philosophy:**
|
||||
|
||||
- **Organize by FEATURE, not by technical layer**
|
||||
- Each feature is a vertical slice
|
||||
- Enables modular growth, clear boundaries, and independent deployability
|
||||
|
||||
**Universal Rule: Context → Feature → Layer**
|
||||
|
||||
**1. Level 1: Repository Scope:** Root contains `apps/` grouping distinct applications (e.g., `apps/backend`, `apps/frontend`, `apps/mobile`).
|
||||
|
||||
**2. Level 2: Feature Organization**
|
||||
- **Rule:** Divide application into vertical business slices (e.g., `user/`, `order/`, `payment/`).
|
||||
- **Anti-Pattern:** Do NOT organize by technical layer (e.g., `controllers/`, `models/`, `services/`) at the top level.
|
||||
|
||||
### Language-Specific Layouts
|
||||
|
||||
Each layout follows the universal philosophy above. Load the relevant layout when working with a specific language or framework:
|
||||
|
||||
| Layout | File | When to Use |
|
||||
| ------------------ | ------------------------------------ | ------------------------------------ |
|
||||
| Go Backend | @project-structure-go-backend.md | Go services, APIs, CLI tools |
|
||||
| Vue/React Frontend | @project-structure-vue-frontend.md | Web frontends (Vue, React, Svelte) |
|
||||
| Flutter/Mobile | @project-structure-flutter-mobile.md | Mobile apps (Flutter, React Native) |
|
||||
| Rust/Cargo | @project-structure-rust-cargo.md | Rust binaries, libraries, workspaces |
|
||||
|
||||
> This Feature/Domain/UI/API structure is framework-agnostic. It applies equally to any language or framework. The layout files provide language-specific conventions (file naming, module systems, test locations) while preserving the vertical slice architecture.
|
||||
|
||||
### Adapting for Different Project Types
|
||||
|
||||
| Project Type | What to Change |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **Monorepo** (default) | Use as-is — `apps/backend/`, `apps/frontend/`, `apps/mobile/` |
|
||||
| **Single backend** | Flatten to root: `cmd/`, `internal/` (Go) or `src/` (Rust) at project root |
|
||||
| **Single frontend** | Flatten to root: `src/` at project root (no `apps/` wrapper) |
|
||||
| **Single mobile** | Flatten to root: `lib/` at project root (no `apps/` wrapper) |
|
||||
| **Single Rust binary** | Flatten to root: `src/`, `tests/`, `benches/` at project root with `Cargo.toml` |
|
||||
| **Rust library** | Flatten to root: `src/`, `examples/`, `tests/` at project root with `Cargo.toml` (lib target) |
|
||||
| **Rust workspace** | `crates/` at root with workspace `Cargo.toml` — each crate follows single-crate layout internally |
|
||||
| **Microservices** | One directory per service under `apps/` (each with own `go.mod`/`Cargo.toml`/`Dockerfile`) |
|
||||
| **Full-stack + mobile** | Use all relevant layout files under `apps/` |
|
||||
|
||||
**Single-app projects** don't need the `apps/` directory — put the language-specific root directories directly at the project root. The internal structure (features, platform, etc.) stays the same.
|
||||
|
||||
**Multiple entry points (CLI, workers, etc.):**
|
||||
|
||||
Go:
|
||||
```
|
||||
cmd/
|
||||
api/main.go # HTTP server entry point
|
||||
cli/main.go # CLI tool entry point
|
||||
worker/main.go # Background worker entry point
|
||||
```
|
||||
|
||||
Rust:
|
||||
```
|
||||
src/
|
||||
bin/
|
||||
api.rs # HTTP server entry point
|
||||
cli.rs # CLI tool entry point
|
||||
worker.rs # Background worker entry point
|
||||
lib.rs # Shared library code
|
||||
```
|
||||
|
||||
**Microservices notes:**
|
||||
- Each service is its own directory under `apps/` with its own `go.mod`/`Cargo.toml` and `Dockerfile`
|
||||
- Each service follows the same layout internally (see language-specific files)
|
||||
- Add `shared/` at root for cross-service contracts (protobuf, shared types) — keep this minimal
|
||||
- Services communicate via API calls or message queues, never direct imports
|
||||
|
||||
### Related Principles
|
||||
- Code Organization Principles @code-organization-principles.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Python code, reviewing Python idioms, or working on any Python project (backend, CLI, or data pipeline)
|
||||
---
|
||||
|
||||
## Python Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Python rewards explicitness and readability over cleverness. Follow the **Zen of Python** (`import this`) — beautiful code is not a luxury, it's a professional necessity. If it reads like plain English, it's probably idiomatic Python.
|
||||
|
||||
> **Scope:** This file covers Python-specific *coding idioms*. For file layout, see `project-structure-python-backend.md`. For test naming conventions, see `testing-strategy.md`. For logging library choice, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### Type Hints — Non-Negotiable
|
||||
|
||||
**Always annotate function signatures and public APIs.** Use `from __future__ import annotations` for forward references.
|
||||
|
||||
```python
|
||||
# ✅ Fully annotated — self-documenting and mypy-verifiable
|
||||
from __future__ import annotations
|
||||
from collections.abc import Sequence
|
||||
|
||||
def calculate_discount(items: Sequence[Item], coupon: Coupon) -> float: ...
|
||||
|
||||
# ❌ Untyped — opaque to both mypy and the next developer
|
||||
def calculate_discount(items, coupon): ...
|
||||
```
|
||||
|
||||
1. **Use `X | None` over `Optional[X]`** (Python 3.10+)
|
||||
```python
|
||||
# ✅ Modern union syntax
|
||||
def find_user(user_id: str) -> User | None: ...
|
||||
|
||||
# ❌ Verbose legacy form (still valid, but prefer the above)
|
||||
from typing import Optional
|
||||
def find_user(user_id: str) -> Optional[User]: ...
|
||||
```
|
||||
|
||||
2. **Use `TypeAlias` and `TypeVar` for reusable generics**
|
||||
```python
|
||||
from typing import TypeVar, TypeAlias
|
||||
|
||||
T = TypeVar("T")
|
||||
UserId: TypeAlias = str
|
||||
```
|
||||
|
||||
3. **Use `Protocol` for structural interfaces instead of ABCs when duck-typing is sufficient**
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class TaskStorage(Protocol):
|
||||
def get_by_id(self, task_id: str) -> Task: ...
|
||||
def save(self, task: Task) -> None: ...
|
||||
```
|
||||
|
||||
4. **`TypedDict` for structured dicts crossing system boundaries (JSON, configs)**
|
||||
```python
|
||||
from typing import TypedDict
|
||||
|
||||
class CreateTaskRequest(TypedDict):
|
||||
title: str
|
||||
priority: Literal["low", "medium", "high"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Handling
|
||||
|
||||
> For general error handling principles, see `error-handling-principles.md`. This section covers Python-specific idioms.
|
||||
|
||||
1. **Prefer specific exception types over broad `except Exception`**
|
||||
```python
|
||||
# ✅ Precise — catches exactly what you expect
|
||||
try:
|
||||
task = storage.get_by_id(task_id)
|
||||
except TaskNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
# ❌ Broad — may swallow programming errors
|
||||
try:
|
||||
task = storage.get_by_id(task_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Error")
|
||||
```
|
||||
|
||||
2. **Define domain-specific exception hierarchies**
|
||||
```python
|
||||
class FathError(Exception):
|
||||
"""Base exception for all domain errors."""
|
||||
|
||||
class NotFoundError(FathError):
|
||||
def __init__(self, resource: str, resource_id: str) -> None:
|
||||
self.resource = resource
|
||||
self.resource_id = resource_id
|
||||
super().__init__(f"{resource} '{resource_id}' not found")
|
||||
|
||||
class ValidationError(FathError):
|
||||
def __init__(self, field: str, message: str) -> None:
|
||||
self.field = field
|
||||
self.message = message
|
||||
super().__init__(f"Validation failed on '{field}': {message}")
|
||||
```
|
||||
|
||||
3. **Never silence exceptions** — if an exception is caught and not re-raised, log it explicitly
|
||||
```python
|
||||
# ❌ Silent swallow
|
||||
try:
|
||||
notify_user(user_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ✅ Explicit intent + observability
|
||||
try:
|
||||
notify_user(user_id)
|
||||
except NotificationError:
|
||||
logger.warning("notification_failed", user_id=user_id, exc_info=True)
|
||||
```
|
||||
|
||||
4. **Use `contextlib.suppress` only for truly expected, inconsequential exceptions**
|
||||
```python
|
||||
from contextlib import suppress
|
||||
|
||||
with suppress(FileNotFoundError):
|
||||
cache_path.unlink() # OK — cleanup, not business logic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Dataclasses and Pydantic
|
||||
|
||||
1. **Use `dataclasses` for internal domain models** (no I/O, no validation)
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass(frozen=True) # frozen = immutable value object
|
||||
class Task:
|
||||
id: str
|
||||
title: str
|
||||
priority: str
|
||||
tags: tuple[str, ...] = field(default_factory=tuple)
|
||||
```
|
||||
|
||||
2. **Use Pydantic `BaseModel` for data crossing system boundaries** (API requests/responses, config)
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class CreateTaskRequest(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=200)
|
||||
priority: Literal["low", "medium", "high"] = "medium"
|
||||
due_date: datetime | None = None
|
||||
|
||||
model_config = ConfigDict(frozen=True) # Pydantic v2
|
||||
```
|
||||
|
||||
3. **Keep domain models separate from API schemas** — never use a Pydantic model as a domain entity
|
||||
```
|
||||
models.py → dataclasses (pure domain)
|
||||
schemas.py → Pydantic models (API boundary)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Interfaces and Dependency Injection
|
||||
|
||||
Python uses Protocols and constructor injection to achieve the same testability goal as Go interfaces.
|
||||
|
||||
1. **Define the Protocol where it is *used*, not where it is *implemented***
|
||||
```python
|
||||
# task/storage.py ← defined in the consumer feature
|
||||
from typing import Protocol
|
||||
|
||||
class TaskStorage(Protocol):
|
||||
def get_by_id(self, task_id: str) -> Task: ...
|
||||
def save(self, task: Task) -> None: ...
|
||||
def delete(self, task_id: str) -> None: ...
|
||||
```
|
||||
|
||||
2. **Inject dependencies through `__init__`** — never instantiate concrete dependencies inside a class
|
||||
```python
|
||||
# ✅ Testable — storage is injected
|
||||
class TaskService:
|
||||
def __init__(self, storage: TaskStorage) -> None:
|
||||
self._storage = storage
|
||||
|
||||
# ❌ Not testable — concrete dependency hardwired
|
||||
class TaskService:
|
||||
def __init__(self) -> None:
|
||||
self._storage = PostgresTaskStorage()
|
||||
```
|
||||
|
||||
3. **Wire dependencies in the entry point** (e.g., `main.py`, `app.py`, or DI container)
|
||||
```python
|
||||
# app/main.py
|
||||
storage = PostgresTaskStorage(db=database)
|
||||
service = TaskService(storage=storage)
|
||||
router.include_router(build_task_router(service))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Async / Await
|
||||
|
||||
> For general async principles (when to add concurrency), see `concurrency-and-threading-mandate.md`. This section covers Python-specific async idioms.
|
||||
|
||||
1. **Choose one async paradigm and stay consistent** — do not mix `asyncio.run` entry points
|
||||
```python
|
||||
# ✅ Fully async service layer
|
||||
async def get_task(self, task_id: str) -> Task:
|
||||
return await self._storage.get_by_id(task_id)
|
||||
```
|
||||
|
||||
2. **Never call blocking I/O directly in an async function**
|
||||
```python
|
||||
# ❌ Blocks the event loop
|
||||
async def load_file(path: str) -> str:
|
||||
return open(path).read()
|
||||
|
||||
# ✅ Use async I/O or run in executor
|
||||
import asyncio, aiofiles
|
||||
|
||||
async def load_file(path: str) -> str:
|
||||
async with aiofiles.open(path) as f:
|
||||
return await f.read()
|
||||
```
|
||||
|
||||
3. **Use `asyncio.gather` for concurrent independent operations**
|
||||
```python
|
||||
# ✅ Concurrent fan-out
|
||||
user, tasks = await asyncio.gather(
|
||||
get_user(user_id),
|
||||
get_tasks(user_id),
|
||||
)
|
||||
```
|
||||
|
||||
4. **Use `asyncio.TaskGroup` (Python 3.11+) for structured concurrency with cancellation safety**
|
||||
```python
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
user_task = tg.create_task(get_user(user_id))
|
||||
tasks_task = tg.create_task(get_tasks(user_id))
|
||||
user = user_task.result()
|
||||
tasks = tasks_task.result()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
Follow **PEP 8** rigorously. No exceptions.
|
||||
|
||||
| Construct | Convention | Example |
|
||||
| ----------------------- | ------------------ | ---------------------------- |
|
||||
| Module / Package | `snake_case` | `task_service.py` |
|
||||
| Class | `PascalCase` | `TaskService` |
|
||||
| Function / Method | `snake_case` | `get_by_id` |
|
||||
| Private method/attr | `_snake_case` | `_validate_title` |
|
||||
| Constant | `UPPER_SNAKE_CASE` | `MAX_TITLE_LENGTH = 200` |
|
||||
| Type alias | `PascalCase` | `UserId = str` |
|
||||
| Protocol / Interface | `PascalCase` | `TaskStorage` |
|
||||
|
||||
1. **Never use single-letter names outside list comprehensions or math** — names must be descriptive
|
||||
2. **Avoid `data`, `info`, `obj`, `result` as standalone names** — describe the *domain concept*
|
||||
3. **Boolean variables and functions should read as yes/no questions**
|
||||
```python
|
||||
# ✅
|
||||
is_active: bool
|
||||
has_permission: bool
|
||||
def can_edit(user: User, task: Task) -> bool: ...
|
||||
|
||||
# ❌
|
||||
active: bool
|
||||
permission: bool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Idiomatic Patterns
|
||||
|
||||
1. **Context managers for resource cleanup** — always prefer `with` over manual `close()`
|
||||
```python
|
||||
# ✅
|
||||
async with database.transaction() as tx:
|
||||
await tx.execute(query)
|
||||
|
||||
# ❌
|
||||
tx = database.begin()
|
||||
tx.execute(query)
|
||||
tx.commit() # easily forgotten on exception
|
||||
```
|
||||
|
||||
2. **Generator expressions over list comprehensions for lazy evaluation**
|
||||
```python
|
||||
# ✅ Lazy — does not materialise the entire list
|
||||
active_ids = (task.id for task in tasks if task.is_active)
|
||||
|
||||
# Use list comprehension only when you need the full list
|
||||
active_tasks = [task for task in tasks if task.is_active]
|
||||
```
|
||||
|
||||
3. **`dataclasses.replace()` for immutable updates** (preferred over mutating frozen dataclasses)
|
||||
```python
|
||||
from dataclasses import replace
|
||||
|
||||
updated_task = replace(task, title="New Title")
|
||||
```
|
||||
|
||||
4. **`functools.cache` / `functools.lru_cache` for pure function memoization**
|
||||
```python
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def get_config() -> AppConfig: # called once; result reused
|
||||
return _load_config_from_env()
|
||||
```
|
||||
|
||||
5. **`__slots__` on hot-path, frequently instantiated classes**
|
||||
```python
|
||||
@dataclass
|
||||
class Vector:
|
||||
__slots__ = ("x", "y")
|
||||
x: float
|
||||
y: float
|
||||
```
|
||||
|
||||
6. **`enum.Enum` (not raw strings) for domain-level constants**
|
||||
```python
|
||||
from enum import StrEnum # Python 3.11+
|
||||
|
||||
class Priority(StrEnum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test file naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Python-specific tooling only.
|
||||
|
||||
1. **Use `pytest` as the sole test runner** — never mix with `unittest.TestCase` classes
|
||||
```python
|
||||
# ✅ Idiomatic pytest
|
||||
def test_calculate_discount_returns_zero_for_no_items() -> None:
|
||||
result = calculate_discount(items=[], coupon=Coupon(code="SAVE10"))
|
||||
assert result == 0.0
|
||||
```
|
||||
|
||||
2. **Parametrize test cases with `@pytest.mark.parametrize`**
|
||||
```python
|
||||
@pytest.mark.parametrize("priority,expected_score", [
|
||||
("low", 1),
|
||||
("medium", 5),
|
||||
("high", 10),
|
||||
])
|
||||
def test_priority_score(priority: str, expected_score: int) -> None:
|
||||
assert priority_score(priority) == expected_score
|
||||
```
|
||||
|
||||
3. **Use `pytest-mock` (`mocker` fixture) for mocking — not `unittest.mock` directly**
|
||||
```python
|
||||
def test_task_service_creates_task(mocker: MockerFixture) -> None:
|
||||
mock_storage = mocker.create_autospec(TaskStorage, instance=True)
|
||||
service = TaskService(storage=mock_storage)
|
||||
|
||||
service.create(title="Test", priority="high")
|
||||
|
||||
mock_storage.save.assert_called_once()
|
||||
```
|
||||
|
||||
4. **Use a typed mock factory for Protocol-based interfaces**
|
||||
```python
|
||||
# storage_mock.py ← co-locate with storage.py
|
||||
class InMemoryTaskStorage:
|
||||
"""In-memory TaskStorage implementation for unit tests."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[str, Task] = {}
|
||||
|
||||
def get_by_id(self, task_id: str) -> Task:
|
||||
if task_id not in self._store:
|
||||
raise NotFoundError("Task", task_id)
|
||||
return self._store[task_id]
|
||||
|
||||
def save(self, task: Task) -> None:
|
||||
self._store[task.id] = task
|
||||
|
||||
def delete(self, task_id: str) -> None:
|
||||
self._store.pop(task_id, None)
|
||||
```
|
||||
|
||||
5. **Use `pytest-asyncio` for async tests** — mark the whole module or use `asyncio_mode = "auto"` in `pyproject.toml`
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_create_task() -> None:
|
||||
service = TaskService(storage=InMemoryTaskStorage())
|
||||
task = await service.create(title="Async Task", priority="low")
|
||||
assert task.title == "Async Task"
|
||||
```
|
||||
|
||||
6. **Fixtures for reusable setup** — never repeat identical `Arrange` blocks across tests
|
||||
```python
|
||||
@pytest.fixture
|
||||
def task_service() -> TaskService:
|
||||
return TaskService(storage=InMemoryTaskStorage())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Formatting and Static Analysis
|
||||
|
||||
All of the following **must pass with zero warnings/errors** before any commit. See `code-completion-mandate.md` for the full checklist.
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
| ------------ | --------------------------------- | ---------------------------------- |
|
||||
| `ruff format`| Canonical formatting (fast) | `ruff format .` |
|
||||
| `ruff check` | Lint (replaces flake8, isort, ...) | `ruff check . --fix` |
|
||||
| `mypy` | Static type checking | `mypy src/ --strict` |
|
||||
| `bandit` | Security scanning | `bandit -r src/ -c pyproject.toml` |
|
||||
| `pip-audit` | Dependency CVE scanning | `pip-audit` |
|
||||
|
||||
Configure all tools in `pyproject.toml` — never use per-file pragma comments to disable checks without a `# NOQA:` reason comment.
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "UP", "S", "B", "ANN"]
|
||||
ignore = []
|
||||
|
||||
[tool.mypy]
|
||||
strict = true
|
||||
python_version = "3.11"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
```
|
||||
|
||||
> **Logging:** Never use `print()` in production code — it produces unstructured output. Use the standard `logging` module or `structlog` for structured JSON logs. See `logging-and-observability-principles.md` for the required patterns.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Project Structure — Python Backend @project-structure-python-backend.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Security Principles @security-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When working with resources requiring cleanup (files, database connections, network sockets, locks) or implementing resource pools
|
||||
---
|
||||
|
||||
## Resource and Memory Management Principles
|
||||
|
||||
### Universal Resource Management Rules
|
||||
|
||||
**1. Always Clean Up Resources**
|
||||
|
||||
**Resources requiring cleanup:**
|
||||
|
||||
- Files, network connections, database connections
|
||||
- Locks, semaphores, mutexes
|
||||
- Memory allocations (in manual-memory languages)
|
||||
- OS handles, GPU resources
|
||||
|
||||
**Clean up in ALL paths:**
|
||||
|
||||
- Success path: Normal completion
|
||||
- Error path: Exception thrown, error returned
|
||||
- Early return path: Guard clauses, validation failures
|
||||
|
||||
**Use language-appropriate patterns:**
|
||||
|
||||
- Go: defer statements
|
||||
- Rust: Drop trait (RAII)
|
||||
- Python: context managers (with statement)
|
||||
- TypeScript: try/finally
|
||||
- Java: try-with-resources
|
||||
|
||||
**2. Timeout All I/O Operations**
|
||||
|
||||
**Why timeout:**
|
||||
|
||||
- Network requests can hang indefinitely
|
||||
- Prevents resource exhaustion (connections, threads)
|
||||
- Provides predictable failure behavior
|
||||
|
||||
**Timeout recommendations:**
|
||||
|
||||
- Network requests: 30s default, shorter (5-10s) for interactive
|
||||
- Database queries: 10s default, configure per query complexity
|
||||
- File operations: Usually fast, but timeout on network filesystems
|
||||
- Message queue operations: Configurable, avoid indefinite blocking
|
||||
|
||||
**3. Pool Expensive Resources**
|
||||
|
||||
**Resources to pool:**
|
||||
|
||||
- Database connections: Pool size 5-20 per app instance
|
||||
- HTTP connections: Reuse with keep-alive
|
||||
- Thread pools: Size based on CPU count (CPU-bound) or I/O wait (I/O-bound)
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Reduces latency (no connection setup overhead)
|
||||
- Limits resource consumption (cap on max connections)
|
||||
- Improves throughput (reuse vs create new)
|
||||
|
||||
**Connection Pool Best Practices:**
|
||||
|
||||
- Minimum connections: 5 (ensures pool is warm)
|
||||
- Maximum connections: 20-50 (prevents overwhelming database)
|
||||
- Idle timeout: Close connections idle >5-10 minutes
|
||||
- Validation: Test connections before use (avoid broken connections)
|
||||
- Monitoring: Track utilization, wait times, timeout rates
|
||||
|
||||
**4. Avoid Resource Leaks**
|
||||
|
||||
**What is a leak:**
|
||||
|
||||
- Acquire resource (open file, allocate memory, get connection)
|
||||
- Never release it (forget to close, exception prevents cleanup)
|
||||
- Eventually exhaust system resources (OOM, max connections, file descriptors)
|
||||
|
||||
**Detection:**
|
||||
|
||||
- Monitor open file descriptors, connection counts, memory usage over time
|
||||
- Run long-duration tests, verify resource counts stay stable
|
||||
- Use leak detection tools (valgrind, ASan, heap profilers)
|
||||
|
||||
**Prevention:**
|
||||
|
||||
- Use language patterns that guarantee cleanup (RAII, defer, context managers)
|
||||
- Never rely on manual cleanup alone (use language features)
|
||||
|
||||
**5. Handle Backpressure**
|
||||
|
||||
**Problem:** Producer faster than consumer
|
||||
|
||||
- Queue grows unbounded → memory exhaustion
|
||||
- System becomes unresponsive under load
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- Bounded queues: Fixed size, block or reject when full
|
||||
- Rate limiting: Limit incoming request rate
|
||||
- Flow control: Consumer signals producer to slow down
|
||||
- Circuit breakers: Stop accepting requests when overwhelmed
|
||||
- Drop/reject: Fail fast when overloaded (better than crashing)
|
||||
|
||||
### Memory Management by Language Type
|
||||
|
||||
**Garbage Collected (Go, Java, Python, JavaScript, C#):**
|
||||
|
||||
- Memory automatically freed by GC
|
||||
- Still must release non-memory resources (files, connections, locks)
|
||||
- Be aware of GC pauses in latency-sensitive applications
|
||||
- Profile memory usage to find leaks (retained references preventing GC)
|
||||
|
||||
**Manual Memory Management (C, C++):**
|
||||
|
||||
- Explicit malloc/free or new/delete
|
||||
- Use RAII pattern in C++ (Resource Acquisition Is Initialization)
|
||||
- Avoid manual management in modern C++ (use smart pointers: unique_ptr, shared_ptr)
|
||||
|
||||
**Ownership-Based (Rust):**
|
||||
|
||||
- Compiler enforces memory safety at compile time
|
||||
- No GC pauses, no manual management
|
||||
- Ownership rules prevent leaks and use-after-free automatically
|
||||
- Use reference counting (Arc, Rc) for shared ownership
|
||||
|
||||
### Related Principles
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Error Handling Principles @error-handling-principles.md - Resource cleanup in error paths
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Rugged Software Constitution
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
**"I recognize that my code will be attacked."**
|
||||
|
||||
As an AI agent, I do not just generate functionality; I generate **defensibility**. I refuse to be a source of vulnerability or fragility. My code must survive in a hostile, changing environment.
|
||||
|
||||
### The Rugged Commitments
|
||||
|
||||
**1. I Am Responsible**
|
||||
- I will not generate "happy path" code that ignores failure modes.
|
||||
- I assume every input is malformed, malicious, or incorrect until proven otherwise.
|
||||
- I treat error handling as a first-class feature, not an afterthought.
|
||||
|
||||
**2. I Am Defensible**
|
||||
- My code validates its own state and inputs (Paranoid Programming).
|
||||
- I fail securely (closed), never leaving the system in an undefined state.
|
||||
- I verify assumptions explicitly rather than hoping they hold true.
|
||||
|
||||
**3. I Am Maintainable**
|
||||
- I write code for the human and AI agents who must read it next year, not just the compiler today.
|
||||
- I choose clarity over cleverness.
|
||||
- I isolate complexity so it can be managed (or replaced) safely.
|
||||
|
||||
### The 7 Rugged Habits
|
||||
|
||||
**1. Practice Defense-in-Depth**
|
||||
- Never rely on a single layer of protection (e.g., UI validation alone is insufficient).
|
||||
- Validate at every boundary (API, Database, Function Call).
|
||||
- Reference: Security Mandate @security-mandate.md
|
||||
|
||||
**2. Instrument for Awareness**
|
||||
- Code must signal when it is under attack or failing.
|
||||
- Silent failures are enemy #1; exposed failures allow for reaction.
|
||||
- Reference: Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
|
||||
**3. Reduce Attack Surface**
|
||||
- Remove unused code, dependencies, and endpoints.
|
||||
- Expose the minimum necessary public interface (Least Privilege).
|
||||
- Reference: Core Design Principles @core-design-principles.md
|
||||
|
||||
**4. Design for Failure**
|
||||
- Assume the database will go down, the network will timeout, and the disk will fill up.
|
||||
- Implement graceful degradation (circuit breakers, fallbacks).
|
||||
- Reference: Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
|
||||
**5. Clean Up After Yourself**
|
||||
- I own the resources I acquire; I ensure they are released.
|
||||
- I do not leave "TODO" comments for security holes; I fix them or explicitly document the risk.
|
||||
- Reference: Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
|
||||
**6. Verify Your Defenses**
|
||||
- Defenses are useless if they don't work; tests prove they work.
|
||||
- Test the "unhappy path" (attacks, errors, edge cases) as rigorously as the happy path.
|
||||
- Reference: Testing Strategy @testing-strategy.md
|
||||
|
||||
**7. Adapt to the Ecosystem**
|
||||
- Use established, battle-tested libraries over custom implementations.
|
||||
- Follow community conventions to ensure long-term maintainability.
|
||||
- Reference: Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
|
||||
### Application to Code Generation
|
||||
|
||||
**When generating code, I will:**
|
||||
- **Refuse** to generate insecure patterns (SQLi, hardcoded secrets, shell injection), even if asked.
|
||||
- **Proactively** add validation, error handling, and timeout logic, even if not explicitly requested.
|
||||
- **Explain** *why* I added a defensive measure (e.g., "Added input validation to prevent XSS").
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Rule Priority (When Rules Conflict)
|
||||
|
||||
When two rules pull in opposite directions, use this priority to decide:
|
||||
|
||||
### Priority Order (Highest to Lowest)
|
||||
|
||||
1. **Security Mandate** — always wins. Never compromise security for velocity, simplicity, or convenience.
|
||||
2. **Rugged Software Constitution** — foundational philosophy. Code must be defensible.
|
||||
3. **Code Completion Mandate** and **Logging and Observability Mandate** — both are always-on enforcement rules. Validation and instrumentation are non-negotiable; neither can be skipped to ship faster.
|
||||
4. **Testability-First Design** — maintainability enables future improvements.
|
||||
5. **Feature-specific principles** — context-dependent guidance for the task at hand. This includes language-specific idiom files (`go-idioms-and-patterns.md`, `typescript-idioms-and-patterns.md`, `vue-idioms-and-patterns.md`, `flutter-idioms-and-patterns.md`, `rust-idioms-and-patterns.md`), `concurrency-and-threading-mandate.md`, and `ci-cd-principles.md`. When an idiom conflicts with a higher-priority rule (e.g., security or testability), the higher-priority rule always wins.
|
||||
6. **PRD-gated principles** — only apply when the PRD or technical architecture document *explicitly requires* the capability. Includes `feature-flags-principles.md` and `ci-cd-gitops-kubernetes.md`. Must not be applied based on speculation; agent must confirm requirement before activating.
|
||||
7. **YAGNI / KISS** — only when no security, reliability, or maintainability trade-off exists.
|
||||
|
||||
### Common Conflict Resolutions
|
||||
|
||||
| Conflict | Resolution |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| YAGNI vs Security ("don't add input validation, it's not needed yet") | **Security wins.** Input validation is always needed. |
|
||||
| KISS vs Testability ("adding an interface makes it more complex") | **Testability wins.** Interfaces enable testing, which enables maintainability. |
|
||||
| Performance vs YAGNI ("should I optimize this now?") | **Measure first.** Only optimize after profiling shows a real bottleneck. |
|
||||
| DRY vs Clarity ("should I abstract this into a shared utility?") | **Clarity wins** until duplication reaches 3+ instances (Rule of Three). |
|
||||
| Speed vs Logging ("skip logging to ship faster") | **Logging wins.** Silent failures are the enemy. |
|
||||
| YAGNI vs PRD-gated requirement ("feature flags aren't needed") | **PRD wins.** If explicitly required, YAGNI cannot override it. |
|
||||
|
||||
### Guiding Principle
|
||||
|
||||
When in doubt, ask: *"Which choice makes the code more defensible and maintainable?"*
|
||||
|
||||
If both options are equally defensible, choose the simpler one (KISS).
|
||||
|
||||
### Related Principles
|
||||
- Security Mandate @security-mandate.md
|
||||
- Rugged Software Constitution @rugged-software-constitution.md
|
||||
- Code Completion Mandate @code-completion-mandate.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Rust code, working on a Rust or Cargo project, or reviewing Rust idioms and safety practices
|
||||
---
|
||||
|
||||
## Rust Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Rust's type system and ownership model are your primary tools for correctness. Lean into the compiler — it is your strongest ally. Write code that is idiomatic, safe, and expressive.
|
||||
|
||||
> **Scope:** This file covers Rust-specific *coding idioms*. For file layout, see `project-structure-rust-cargo.md`. For test naming conventions, see `testing-strategy.md`. For logging library choice, see `logging-and-observability-principles.md`.
|
||||
|
||||
### Ownership and Borrowing
|
||||
|
||||
1. **Prefer borrowing (`&T`, `&mut T`) over cloning**
|
||||
- Never `.clone()` to silence the borrow checker without a `// CLONE:` comment explaining why
|
||||
- Use `Cow<'_, T>` when a function may or may not need ownership
|
||||
- Prefer `&str` over `String` in function parameters, `&[T]` over `Vec<T>`
|
||||
|
||||
2. **Minimize owned data in structs**
|
||||
- Use references with explicit lifetimes when the struct is short-lived
|
||||
- Use owned types (`String`, `Vec<T>`) when the struct must outlive its inputs
|
||||
|
||||
3. **Avoid unnecessary `Arc<Mutex<T>>`**
|
||||
- If data flows one direction, use channels (`tokio::sync::mpsc`)
|
||||
- If data is read-heavy, consider `RwLock` over `Mutex`
|
||||
- If data is immutable after init, use `Arc<T>` without a lock
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Use the `?` operator for propagation — never `unwrap()` in production code**
|
||||
- `unwrap()` and `expect()` are acceptable only in:
|
||||
- Tests (`#[test]`, `#[tokio::test]`)
|
||||
- Infallible operations where the invariant is proven (document with `// SAFETY:` comment)
|
||||
- CLI `main()` function with clear error messages via `expect("reason")`
|
||||
|
||||
2. **Choose error crates by context:**
|
||||
- Library code: `thiserror` — define typed error enums
|
||||
- Application code: `anyhow` — ergonomic error chaining
|
||||
- Never mix: library crates should not depend on `anyhow`
|
||||
|
||||
3. **Error type design:**
|
||||
|
||||
```rust
|
||||
// ✅ Good — typed, matchable errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PathfinderError {
|
||||
#[error("file not found: {path}")]
|
||||
FileNotFound { path: PathBuf },
|
||||
#[error("AST parse failed: {0}")]
|
||||
ParseError(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
// ❌ Bad — stringly-typed, unmatchable
|
||||
fn do_thing() -> Result<(), String> { ... }
|
||||
|
||||
// ✅ Annotate public Result-producing functions to force callers to handle them
|
||||
#[must_use]
|
||||
pub fn create_task(req: CreateTaskRequest) -> Result<Task, TaskError> { ... }
|
||||
|
||||
// Triggers a compiler warning when the return value is fully ignored:
|
||||
// create_task(req); // warning: unused `Result` that must be used
|
||||
//
|
||||
// This does NOT trigger the warning (intentional discard — still valid when used deliberately):
|
||||
// let _ = create_task(req); // explicit discard, silences warning by design
|
||||
```
|
||||
|
||||
### Async and Concurrency
|
||||
|
||||
1. **Use `tokio` as the async runtime**
|
||||
- Mark async entry points with `#[tokio::main]` or `#[tokio::test]`
|
||||
- Prefer `tokio::spawn` for concurrent tasks, not `std::thread::spawn`
|
||||
- Use `tokio::select!` for racing futures, not manual polling
|
||||
|
||||
2. **Cancellation safety:**
|
||||
- Prefer `tokio::sync::mpsc` over `tokio::sync::broadcast` unless fan-out is needed
|
||||
- Document cancellation behavior on any `async fn` that holds resources across `.await`
|
||||
- Use `tokio_util::sync::CancellationToken` for graceful shutdown
|
||||
|
||||
3. **Blocking operations:**
|
||||
- Never call blocking I/O inside async context
|
||||
- Use `tokio::task::spawn_blocking` for CPU-heavy or blocking work
|
||||
- Use `tokio::fs` instead of `std::fs` inside async functions
|
||||
|
||||
### Unsafe Code
|
||||
|
||||
1. **Zero `unsafe` blocks unless in FFI boundaries**
|
||||
- Tree-sitter C bindings and similar FFI are the only valid use case
|
||||
- Every `unsafe` block must have a `// SAFETY:` comment explaining the invariant
|
||||
|
||||
2. **Minimize unsafe surface area:**
|
||||
- Encapsulate `unsafe` in a safe wrapper function
|
||||
- The wrapper's public API must be safe to call from any context
|
||||
- Write tests that exercise the boundary conditions of `unsafe` wrappers
|
||||
|
||||
3. **Never use `unsafe` to bypass the borrow checker** — restructure the code instead
|
||||
|
||||
### Lifetimes and Generics
|
||||
|
||||
1. **Prefer `'_` lifetime elision when possible**
|
||||
- Only introduce named lifetimes when the compiler requires them or when they clarify intent
|
||||
- Use `'a` for single lifetime parameters, descriptive names (`'input`, `'query`) for multiple
|
||||
|
||||
2. **Keep generic bounds simple:**
|
||||
- Prefer concrete types for prototyping, introduce generics when the pattern stabilizes
|
||||
- Use `impl Trait` in argument position for simple cases
|
||||
- Use `where` clauses for complex bounds — never inline complex bounds in `<...>`
|
||||
|
||||
3. **Avoid lifetime gymnastics:**
|
||||
- If lifetime annotations become complex, restructure to use owned data or `Arc`
|
||||
- Consider the "split borrow" pattern to avoid borrow checker issues in struct methods
|
||||
|
||||
### Idiomatic Patterns
|
||||
|
||||
1. **Builder pattern** for types with many optional fields:
|
||||
- Return `Self` from builder methods for chaining
|
||||
- `build()` returns `Result<T, BuildError>`, not `T`
|
||||
|
||||
2. **Newtype pattern** for domain types:
|
||||
- Wrap primitives: `struct UserId(u64)`, not bare `u64`
|
||||
- Implement `Deref` only when the newtype truly "is-a" the inner type
|
||||
|
||||
3. **Typestate pattern** for state machines:
|
||||
- Different states = different types — invalid transitions are compile errors
|
||||
- Use this for protocol implementations and lifecycle management
|
||||
|
||||
4. **`From`/`Into` conversions:**
|
||||
- Implement `From<A> for B` (never `Into` directly)
|
||||
- Use `impl From<X> for Error` with `thiserror`'s `#[from]` attribute
|
||||
|
||||
### Testing
|
||||
|
||||
1. **Test organization (Rust-specific — differs from Go/TS):**
|
||||
- **Unit tests:** `#[cfg(test)] mod tests` block **at the bottom of each `.rs` file** — this is the idiomatic Rust convention, not a shortcut
|
||||
- Tests can access private functions via `use super::*`
|
||||
- Code inside `#[cfg(test)]` is stripped from production builds
|
||||
- Do NOT create separate `*_test.rs` files — this breaks private access and is non-idiomatic
|
||||
- **Integration tests:** `tests/` directory at crate root (each file compiled as a separate crate)
|
||||
- Only tests public API — use `use my_crate::function;`
|
||||
- No `#[cfg(test)]` annotation needed
|
||||
- Shared helpers: `tests/common/mod.rs` (NOT `tests/common.rs`, which Cargo treats as a test file)
|
||||
- Use `#[tokio::test]` for async tests
|
||||
|
||||
2. **Test naming:** `fn test_<function>_<scenario>_<expected>()` (snake_case)
|
||||
|
||||
3. **Assertions:**
|
||||
- Use `assert_eq!` / `assert_ne!` over `assert!(a == b)` — better error messages
|
||||
- Use `assert!(matches!(result, Ok(_)))` for enum variant checking
|
||||
|
||||
4. **Property testing:** Use `proptest` or `quickcheck` for functions with wide input spaces
|
||||
|
||||
### Clippy and Formatting
|
||||
|
||||
1. **`cargo check` for fast iteration during development**
|
||||
- `cargo check`: type-checks without producing a binary — fastest feedback loop
|
||||
- `cargo clippy`: includes `cargo check` plus lint rules — use before committing
|
||||
- `cargo build`: only when you need the actual binary/library artifact
|
||||
- Never run `cargo build` during TDD cycles — it is significantly slower than `cargo check`
|
||||
|
||||
2. **`cargo clippy` must pass with zero warnings** before any commit
|
||||
- Use `#[allow(clippy::...)]` only with a `// ALLOW:` comment explaining why
|
||||
- Prefer fixing the lint over suppressing it
|
||||
|
||||
3. **`cargo fmt` is non-negotiable** — all code must be formatted
|
||||
|
||||
4. **Recommended project-level Clippy configuration (`.clippy.toml` or `Cargo.toml`):**
|
||||
|
||||
```toml
|
||||
[lints.clippy]
|
||||
pedantic = "warn"
|
||||
unwrap_used = "deny"
|
||||
expect_used = "warn"
|
||||
```
|
||||
|
||||
### Dependency Management
|
||||
|
||||
1. **Minimize dependency count** — each dependency is an attack surface and compile-time cost
|
||||
2. **Pin major versions** in `Cargo.toml` — use `dep = "1"` not `dep = "*"`
|
||||
3. **Audit regularly** — run `cargo audit` to check for known vulnerabilities
|
||||
4. **Prefer well-maintained crates** — check download count, last commit date, and issue tracker
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Performance Optimization Principles @performance-optimization-principles.md
|
||||
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
|
||||
- Security Mandate @security-mandate.md
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## Security Mandate
|
||||
|
||||
**Security is a foundational requirement, not a feature.**
|
||||
|
||||
### Universal Security Principles
|
||||
|
||||
1. **Never trust user input:** All data from users, APIs, or external sources must be validated server-side
|
||||
2. **Deny by default:** Require explicit permission grants, never assume access
|
||||
3. **Fail securely:** When errors occur, fail closed (deny access) rather than open
|
||||
4. **Defense in depth:** Multiple layers of security, never rely on a single control
|
||||
|
||||
**When implementing security-sensitive features (auth, validation, queries), see Security Principles @security-principles.md for detailed implementation guidance.**
|
||||
|
||||
### Related Principles
|
||||
- Security Principles @security-principles.md
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When implementing authentication, authorization, input validation, cryptographic operations, or handling user input and sensitive data
|
||||
---
|
||||
|
||||
## Security Principles
|
||||
|
||||
### OWASP Top 10 Enforcement
|
||||
|
||||
* **Broken Access Control:** Deny by default. Validate permissions *server-side* for every request. Do not rely on UI state.
|
||||
* **Cryptographic Failures:** Use TLS 1.2+ everywhere. Encrypt PII/Secrets at rest. Use standard algorithms (AES-256, RSA-2048, Ed25519). *Never* roll your own crypto.
|
||||
* **Injection:** ZERO TOLERANCE for string concatenation in queries. Use Parameterized Queries (SQL) or ORM bindings. Sanitize all HTML/JS output.
|
||||
* **SSRF Prevention:** Validate all user-provided URLs against an allowlist. Disable HTTP redirects in fetch clients. Block requests to internal IPs (metadata services, localhost).
|
||||
* **Insecure Design:** Threat model every new feature. Fail securely (closed), not openly.
|
||||
* **Vulnerable Components:** Pin dependency versions. Scan for CVEs in CI/CD.
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
* **Passwords:** Hash with Argon2id or Bcrypt (min cost 12). Never plain text.
|
||||
* **Tokens:**
|
||||
* *Access Tokens:* Short-lived (15-30 mins). HS256 or RS256.
|
||||
* *Refresh Tokens:* Long-lived (7-30 days). Rotate on use. Store in `HttpOnly; Secure; SameSite=Strict` cookies.
|
||||
* **Rate Limiting:** Enforce strictly on public endpoints (Login, Register, Password Reset). Standard: 5 attempts / 15 mins.
|
||||
* **MFA:** Required for Admin and Sensitive Data access.
|
||||
* **RBAC:** Map permissions to Roles, not Users. Check permissions at the Route AND Resource level.
|
||||
|
||||
### Input Validation & Sanitization
|
||||
|
||||
* **Principle:** "All Input is Evil until Proven Good."
|
||||
* **Validation:** Validate against a strict Schema (Zod/Pydantic) at the *Handler/Port* boundary.
|
||||
* **Allowlist:** Check for "Good characters" (e.g., `^[a-zA-Z0-9]+$`), do not try to filter "Bad characters."
|
||||
* **Sanitization:** Strip dangerous tags from rich text input using a proven library (e.g., DOMPurify equivalent).
|
||||
|
||||
### Logging & Monitoring (Security Focus)
|
||||
|
||||
* **Redaction:** SCRUB all PII, Secrets, Tokens, and Passwords from logs *before* writing.
|
||||
* **Events:** Log all *failed* auth attempts, access denied events, and input validation failures.
|
||||
* **Format:** JSON structured logs with `correlationId`, `user_id`, and `event_type`.
|
||||
* **Anti-Tamper:** Logs should be write-only for the application.
|
||||
|
||||
### Secrets Management
|
||||
|
||||
* **Storage:** Never commit secrets to git. Use `.env` (local) or Secret Managers (Prod - e.g., Vault/GSM).
|
||||
|
||||
### Related Principles
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- API Design Principles @api-design-principles.md
|
||||
- Logging and Observability Mandate @logging-and-observability-mandate.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Configuration Management Principles @configuration-management-principles.md
|
||||
- Command Execution Principles @command-execution-principles.md
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing tests, organizing test files, implementing test doubles, or setting up testing infrastructure
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Pyramid
|
||||
|
||||
**Unit Tests (70% of tests):**
|
||||
|
||||
- **What:** Test domain logic in isolation with mocked dependencies
|
||||
- **Speed:** Fast (<100ms per test)
|
||||
- **Scope:** Single function, class, or module
|
||||
- **Dependencies:** All external dependencies mocked (repositories, APIs, time, random)
|
||||
- **Coverage Goal:** >85% of domain logic
|
||||
|
||||
**Integration Tests (20% of tests):**
|
||||
|
||||
- **What:** Test adapters against real infrastructure
|
||||
- **Speed:** Medium (100ms-5s per test)
|
||||
- **Scope:** Component interaction with infrastructure (database, cache, message queue)
|
||||
- **Dependencies:** Real infrastructure via Testcontainers
|
||||
- **Coverage Goal:** All adapter implementations, critical integration points
|
||||
|
||||
**End-to-End Tests (10% of tests):**
|
||||
|
||||
- **What:** Test complete user journeys through all layers
|
||||
- **Speed:** Slow (5s-30s per test)
|
||||
- **Scope:** Full system from HTTP request to database and back
|
||||
- **Dependencies:** Entire system running (or close approximation)
|
||||
- **Coverage Goal:** Happy paths, critical business flows
|
||||
|
||||
### Test-Driven Development (TDD)
|
||||
|
||||
**Red-Green-Refactor Cycle:**
|
||||
|
||||
1. **Red:** Write a failing test for next bit of functionality
|
||||
2. **Green:** Write minimal code to make test pass
|
||||
3. **Refactor:** Clean up code while keeping tests green
|
||||
4. **Repeat:** Next test
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Tests written first ensure testable design
|
||||
- Comprehensive test coverage (code without test doesn't exist)
|
||||
- Faster development (catch bugs immediately, not in QA)
|
||||
- Better design (forces thinking about interfaces before implementation)
|
||||
|
||||
### Test Doubles Strategy
|
||||
|
||||
**Unit Tests:** Use mocks/stubs for all driven ports
|
||||
|
||||
- Mock repositories return pre-defined data
|
||||
- Mock external APIs return successful responses
|
||||
- Mock time/random for deterministic tests
|
||||
- Control test environment completely
|
||||
|
||||
**Integration Tests:** Use real infrastructure
|
||||
|
||||
- Testcontainers spins up PostgreSQL, Redis, message queues
|
||||
- Firebase emulator spins up Firebase Authentication, Cloud Firestore, Realtime Database, Cloud Storage for Firebase, Firebase Hosting, Cloud Functions, Pub/Sub, and Firebase Extensions
|
||||
- Test actual database queries, connection handling, transactions
|
||||
- Verify adapter implementations work with real services
|
||||
|
||||
**Best Practice:**
|
||||
|
||||
- Generate at least 2 implementations per driven port:
|
||||
1. Production adapter (PostgreSQL, GCP GCS, etc.)
|
||||
2. Test adapter (in-memory, fake implementation)
|
||||
|
||||
### Test Organization
|
||||
|
||||
**Universal Rule: Co-locate implementation tests; Separate system tests.**
|
||||
|
||||
**1. Unit & Integration Tests (Co-located)**
|
||||
- **Rule:** Place tests **next to the file** they test.
|
||||
- **Why:** Keeps tests visible, encourages maintenance, and supports refactoring (moving a file moves its tests).
|
||||
- **Naming Convention Example:**
|
||||
- **TS/JS:** `*.spec.ts` (Unit), `*.integration.spec.ts` (Integration)
|
||||
- **Go:** `*_test.go` (Unit), `*_integration_test.go` (Integration)
|
||||
- **Dart/Flutter:** `*_test.dart` (Unit), `*_integration_test.dart` (Integration) — tests live in `test/` mirroring `lib/` layout (Flutter's default convention, discovered by `flutter test`)
|
||||
- **Python:** `test_*.py` (Unit), `test_*_integration.py` (Integration)
|
||||
- **Java:** `*Test.java` (Unit), `*IT.java` (Integration)
|
||||
- **Rust:** `#[cfg(test)] mod tests` inline in each `.rs` file (Unit), `tests/` directory at crate root (Integration)
|
||||
> You must strictly follow the convention for the target language. Do not mix `test` and `spec` suffixes in the same application context.
|
||||
> **Language-specific overrides:** Some ecosystems have different test location conventions. When a language-specific project structure file exists (e.g., `project-structure-flutter-mobile.md`, `project-structure-rust-cargo.md`), its test location rules take precedence over the co-location default. See `architectural-pattern.md` § Test co-location for the authoritative rule.
|
||||
> **Rust exception:** Rust unit tests are **inline** (`#[cfg(test)] mod tests` at the bottom of each `.rs` file), not separate files. This is the official Rust convention — it enables testing private functions and is how `cargo test` discovers unit tests. Integration tests go in `tests/` at the crate root, compiled as separate crates. See `rust-idioms-and-patterns.md` for details.
|
||||
|
||||
**2. End-to-End Tests (Separate)**
|
||||
- **Rule:** Place in a dedicated `e2e/` folder
|
||||
- **Single app:** `e2e/` at project root
|
||||
- **Monorepo:** `apps/e2e/` subdivided by test scope:
|
||||
- `apps/e2e/api/` for full API flow E2E tests (HTTP → Database)
|
||||
- `apps/e2e/ui/` for full-stack E2E tests (Browser → Backend → Database)
|
||||
> Path adapts to project type per `project-structure.md` § Adapting for Different Project Types.
|
||||
- **Naming:** Follow `{feature}-{ui/api}.e2e.test.{ext}` (Universal - configure test runner to match this pattern `**/*.e2e.test.*`)
|
||||
- Example:
|
||||
- `user-registration-api.e2e.test.ts` # Full API flow: HTTP → DB
|
||||
- `user-registration-ui.e2e.test.ts` # Full-stack: Browser → Backend → DB
|
||||
|
||||
**Using Playwright MCP for UI E2E Tests:**
|
||||
|
||||
When running E2E tests interactively (during development or verification), use Playwright MCP:
|
||||
|
||||
```
|
||||
# Navigate to the page
|
||||
mcp_playwright_browser_navigate(url="http://localhost:5173/login")
|
||||
|
||||
# Capture accessible state (better than screenshot for assertions)
|
||||
mcp_playwright_browser_snapshot()
|
||||
|
||||
# Interact with elements by ref from snapshot
|
||||
mcp_playwright_browser_type(ref="<ref>", text="[email protected]")
|
||||
mcp_playwright_browser_click(ref="<ref>")
|
||||
|
||||
# Wait for results
|
||||
mcp_playwright_browser_wait_for(text="Welcome")
|
||||
|
||||
# Capture snapshot for walkthrough documentation (captures full accessibility tree)
|
||||
mcp_playwright_browser_snapshot(filename="login-success.md")
|
||||
```
|
||||
|
||||
**E2E Test Requirements:**
|
||||
- Capture a snapshot (`mcp_playwright_browser_snapshot`) at each major step
|
||||
- Save snapshots to walkthrough as proof of functionality
|
||||
- If visual proof is needed, use `browser_subagent` with `RecordingName` to produce a recorded video artifact
|
||||
- Test happy path AND at least one error path
|
||||
- Clean up test data after test (or use unique identifiers)
|
||||
|
||||
**Key Principles:**
|
||||
- **Unit/Integration tests**: Co-located with implementation
|
||||
- **E2E tests**: Separate directory (crosses boundaries)
|
||||
- **Test doubles**: Co-located with interface (storage_mock.go, taskAPI.mock.ts)
|
||||
- **Pattern consistency**: All features follow same structure
|
||||
|
||||
### Test Quality Standards
|
||||
|
||||
**AAA Pattern (Arrange-Act-Assert):**
|
||||
```
|
||||
// Arrange: Set up test data and mocks
|
||||
const user = { id: '123', email: '[email protected]' };
|
||||
const mockRepo = createMockRepository();
|
||||
|
||||
// Act: Execute the code under test
|
||||
const result = await userService.createUser(user);
|
||||
|
||||
// Assert: Verify expected outcome
|
||||
expect(result.id).toBe('123');
|
||||
expect(mockRepo.save).toHaveBeenCalledWith(user);
|
||||
```
|
||||
**Test Naming:**
|
||||
|
||||
- Descriptive: `should [expected behavior] when [condition]`
|
||||
- Examples:
|
||||
- `should return 404 when user not found`
|
||||
- `should hash password before saving to database`
|
||||
- `should reject email with invalid format`
|
||||
|
||||
**Coverage Requirements:**
|
||||
|
||||
- Unit tests: >85% code coverage
|
||||
- Integration tests: All adapter implementations
|
||||
- E2E tests: Critical user journeys
|
||||
|
||||
### Related Principles
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Project Structure @project-structure.md
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing TypeScript code, reviewing TypeScript idioms, or working on any TypeScript project (backend, frontend, or CLI)
|
||||
---
|
||||
|
||||
## TypeScript Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
TypeScript's type system is your documentation, your test, and your specification — all at once. Make the type system encode the invariants of your domain so that invalid states are unrepresentable. Lean into the compiler.
|
||||
|
||||
> **Scope:** This file covers TypeScript-specific *type system and language idioms*. For Vue-specific patterns, see `vue-idioms-and-patterns.md`. For file layout, see `project-structure-vue-frontend.md`. For quality commands, see `code-completion-mandate.md`. For logging library, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### Strict Mode — Non-Negotiable
|
||||
|
||||
**Always enable strict mode** in `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These flags catch the majority of runtime errors at compile time. Never disable them on a per-file basis without a `// STRICT-DISABLE:` comment explaining the rationale.
|
||||
|
||||
---
|
||||
|
||||
### Type System Idioms
|
||||
|
||||
1. **`unknown` over `any` — always**
|
||||
```typescript
|
||||
// ✅ Forces narrowing before use
|
||||
function parse(data: unknown): User {
|
||||
if (!isUser(data)) throw new Error('Invalid user shape');
|
||||
return data;
|
||||
}
|
||||
|
||||
// ❌ Disables the type checker entirely
|
||||
function parse(data: any): User { return data; }
|
||||
```
|
||||
|
||||
2. **Use `readonly` to enforce immutability at compile time**
|
||||
```typescript
|
||||
interface TaskState {
|
||||
readonly id: string;
|
||||
readonly items: readonly Task[];
|
||||
}
|
||||
|
||||
// For function params that should not be mutated
|
||||
function process(tasks: readonly Task[]): Summary { ... }
|
||||
```
|
||||
|
||||
3. **Discriminated unions for type-safe state machines**
|
||||
```typescript
|
||||
type AsyncState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error };
|
||||
|
||||
// Exhaustive handling — compiler catches missing cases
|
||||
function render(state: AsyncState<User>): string {
|
||||
switch (state.status) {
|
||||
case 'idle': return 'Waiting...';
|
||||
case 'loading': return 'Loading...';
|
||||
case 'success': return state.data.name; // data is typed
|
||||
case 'error': return state.error.message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Const assertions for literal types**
|
||||
```typescript
|
||||
const ROLES = ['admin', 'editor', 'viewer'] as const;
|
||||
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'
|
||||
```
|
||||
|
||||
5. **Type narrowing — use type guards instead of `as` casts**
|
||||
```typescript
|
||||
// ✅ Type guard — safe narrowing
|
||||
function isError(value: unknown): value is Error {
|
||||
return value instanceof Error;
|
||||
}
|
||||
|
||||
// ❌ Type assertion — bypasses type checker
|
||||
const err = value as Error;
|
||||
```
|
||||
|
||||
6. **Never use non-null assertion `!` in production code**
|
||||
```typescript
|
||||
// ❌ Hides a potential null/undefined bug
|
||||
const name = user!.profile!.name;
|
||||
|
||||
// ✅ Explicit handling
|
||||
const name = user?.profile?.name ?? 'Anonymous';
|
||||
```
|
||||
|
||||
7. **`satisfies` operator for type-checked object literals (TS 4.9+)**
|
||||
```typescript
|
||||
// ✅ satisfies: compile-checked against interface, type stays as literal
|
||||
const config = {
|
||||
endpoint: '/api/tasks',
|
||||
retries: 3,
|
||||
} satisfies ApiConfig;
|
||||
// config.retries is typed as `3` (literal), not `number` — narrower and safer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Null Safety
|
||||
|
||||
1. **Prefer `??` (nullish coalescing) over `||` for default values**
|
||||
```typescript
|
||||
// ✅ Only falls back for null/undefined
|
||||
const count = input.count ?? 0;
|
||||
|
||||
// ❌ Also falls back for 0, '', false
|
||||
const count = input.count || 0;
|
||||
```
|
||||
|
||||
2. **Use optional chaining `?.` for safe navigation**
|
||||
```typescript
|
||||
const city = user?.address?.city;
|
||||
```
|
||||
|
||||
3. **Distinguish `undefined` (absence) from `null` (explicit empty)**
|
||||
- Use `undefined` for optional fields
|
||||
- Use `null` only when you need to represent "intentionally empty" on the wire (JSON APIs)
|
||||
|
||||
---
|
||||
|
||||
### Async/Await
|
||||
|
||||
> For general async patterns (when to use concurrency), see `concurrency-and-threading-mandate.md`. This section covers TypeScript-specific async idioms.
|
||||
|
||||
1. **Always `await` or handle returned Promises — no floating promises**
|
||||
```typescript
|
||||
// ❌ Fire-and-forget — errors are silently swallowed
|
||||
sendEmail(user);
|
||||
|
||||
// ✅ Awaited
|
||||
await sendEmail(user);
|
||||
|
||||
// ✅ Intentionally fire-and-forget needs explicit void annotation
|
||||
void sendEmail(user); // still logs errors internally
|
||||
```
|
||||
|
||||
2. **Use `Promise.all` for concurrent independent operations**
|
||||
```typescript
|
||||
// ✅ Concurrent — total time = max(individual times)
|
||||
const [user, tasks] = await Promise.all([getUser(id), getTasks(id)]);
|
||||
|
||||
// ❌ Sequential — total time = sum of individual times
|
||||
const user = await getUser(id);
|
||||
const tasks = await getTasks(id);
|
||||
```
|
||||
|
||||
3. **Use `Promise.allSettled` when partial failure is acceptable**
|
||||
```typescript
|
||||
const results = await Promise.allSettled(notifications.map(send));
|
||||
const failed = results.filter(r => r.status === 'rejected');
|
||||
```
|
||||
|
||||
4. **Never mix `async/await` with raw `.then()/.catch()` chains in the same function**
|
||||
|
||||
---
|
||||
|
||||
### Runtime Validation at Boundaries
|
||||
|
||||
**All data crossing a system boundary must be validated at runtime**, not just typed.
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Define schema as the single source of truth
|
||||
const CreateTaskSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
priority: z.enum(['low', 'medium', 'high']),
|
||||
dueDate: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
// Infer the TypeScript type from the schema — no duplication
|
||||
type CreateTaskRequest = z.infer<typeof CreateTaskSchema>;
|
||||
|
||||
// Validate at the API boundary
|
||||
function parseCreateTask(body: unknown): CreateTaskRequest {
|
||||
return CreateTaskSchema.parse(body); // throws ZodError on invalid input
|
||||
}
|
||||
```
|
||||
|
||||
- Use `zod` for runtime schema validation at API ingress and external API egress
|
||||
- Never use TypeScript's `as` operator as a substitute for runtime validation
|
||||
- Validate on ingress; trust validated types thereafter
|
||||
|
||||
---
|
||||
|
||||
### Centralized HTTP Client
|
||||
|
||||
**All outbound HTTP calls MUST go through the project's single, shared API client utility.**
|
||||
|
||||
Do not call `fetch()` or `axios()` directly in feature code. Route every request through the centralized client (e.g., `apiFetch`, `apiClient`, or equivalent).
|
||||
|
||||
**Why this matters:**
|
||||
- Consistent auth header injection (token is attached in one place)
|
||||
- Correlation-ID propagation — every request carries a traceable ID
|
||||
- Centralized error normalization — uniform error shapes for all API failures
|
||||
- Single place to add retries, timeouts, and request logging
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — bypass: no auth header, no correlation-ID, no logging
|
||||
const res = await fetch('/api/tasks');
|
||||
|
||||
// ✅ Correct — use the shared client
|
||||
import { apiFetch } from '@/infrastructure/apiFetch';
|
||||
const res = await apiFetch('/api/tasks');
|
||||
```
|
||||
|
||||
**Exception:** The centralized client itself may use raw `fetch` or `axios` internally — that is its implementation detail, not a bypass.
|
||||
|
||||
> The audit's Integration Contracts dimension (Phase 1.5, Dimension A) checks compliance with this rule. Any direct `fetch`/`axios` call outside the shared client is a `[INT]` finding.
|
||||
|
||||
---
|
||||
|
||||
### Module and Export Patterns
|
||||
|
||||
1. **Prefer named exports over default exports**
|
||||
```typescript
|
||||
// ✅ Named — explicit, refactor-safe, IDE-friendly
|
||||
export function createTask() { ... }
|
||||
export type { Task };
|
||||
|
||||
// ❌ Default — ambiguous import names, harder to auto-import
|
||||
export default function createTask() { ... }
|
||||
```
|
||||
|
||||
2. **Avoid barrel re-exports that create circular dependency risk**
|
||||
- Use feature `index.ts` files only for the public API of a feature, not as catch-all re-exports
|
||||
|
||||
3. **Import type separately to avoid bundling runtime artifacts**
|
||||
```typescript
|
||||
import type { Task } from './types';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test naming, file conventions, and pyramid proportions are defined in `testing-strategy.md`. This section covers TypeScript-specific tooling.
|
||||
|
||||
1. **Type your mocks with Vitest types** — never use `as any` in test doubles
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
import type { MockedObject } from 'vitest';
|
||||
|
||||
const mockStore: MockedObject<TaskStore> = {
|
||||
create: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
};
|
||||
```
|
||||
|
||||
2. **Assert on error types, not just error messages**
|
||||
```typescript
|
||||
await expect(service.create(invalid)).rejects.toThrow(ZodError);
|
||||
```
|
||||
|
||||
3. **Use `satisfies` operator in tests for type-checked fixtures**
|
||||
```typescript
|
||||
const fixture = {
|
||||
id: 'abc', title: 'Test task'
|
||||
} satisfies Task;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Formatting and Static Analysis
|
||||
|
||||
| Tool | Purpose | Notes |
|
||||
| -------------------------- | --------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `vue-tsc --noEmit` | Full type checking (incl. `.vue` files) | Must pass zero errors; use `tsc --noEmit` for non-Vue TS projects |
|
||||
| `eslint` | Lint rules + style | Use `@typescript-eslint/recommended-type-checked` |
|
||||
| `prettier` | Canonical formatting | Non-negotiable |
|
||||
| `npm audit` / `pnpm audit` | Dependency CVE scanning | Run in CI; fail on high severity |
|
||||
|
||||
See `code-completion-mandate.md` for the exact commands to run before committing.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Vue Idioms and Patterns @vue-idioms-and-patterns.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Mandate @concurrency-and-threading-mandate.md
|
||||
- Security Principles @security-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
@@ -0,0 +1,383 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Vue 3 code, working on Vue components, using Pinia stores, or building Vue composables
|
||||
---
|
||||
|
||||
## Vue Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Vue 3 Composition API is the default for all new code. `<script setup>` is the canonical syntax. Think in terms of reactive *data flows*, not component lifecycle hooks. Composables (`use*` functions) are the primary unit of logic reuse.
|
||||
|
||||
> **Scope:** This file covers Vue 3 *coding idioms* for components, stores, and composables. For TypeScript type system patterns, see `typescript-idioms-and-patterns.md`. For file and folder layout, see `project-structure-vue-frontend.md`. For test naming, see `testing-strategy.md`. For logging, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### `<script setup>` — The Only Style
|
||||
|
||||
Always use `<script setup lang="ts">`. Never use the Options API or the class-style component pattern for new code.
|
||||
|
||||
```vue
|
||||
<!-- ✅ Canonical style -->
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{ title: string; count?: number }>();
|
||||
const emit = defineEmits<{ 'update:count': [value: number] }>();
|
||||
|
||||
const doubled = computed(() => (props.count ?? 0) * 2);
|
||||
</script>
|
||||
|
||||
<!-- ❌ Options API — do not use for new components -->
|
||||
<script lang="ts">
|
||||
export default { props: { title: String }, ... }
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Reactivity: `ref` vs `reactive`
|
||||
|
||||
| Use | When |
|
||||
| ------------ | ---------------------------------------------------------------------------------- |
|
||||
| `ref<T>()` | Primitives, single values, values that may be reassigned |
|
||||
| `reactive()` | Plain objects where you always access properties (never reassign the whole object) |
|
||||
| `readonly()` | Expose state that must not be mutated outside its owner |
|
||||
|
||||
```typescript
|
||||
// ✅ ref for primitives and replaceable objects
|
||||
const count = ref(0);
|
||||
const user = ref<User | null>(null);
|
||||
user.value = fetchedUser; // reassignment is fine
|
||||
|
||||
// ✅ reactive for objects where you destructure properties
|
||||
const form = reactive({ title: '', priority: 'medium' });
|
||||
|
||||
// ❌ Never destructure a reactive object — reactivity is lost
|
||||
const { title } = form; // title is now a plain string, NOT reactive
|
||||
// ✅ Use toRefs if you must destructure
|
||||
const { title } = toRefs(form);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Computed Properties
|
||||
|
||||
1. **Use `computed` for all derived state** — never recompute in the template
|
||||
```typescript
|
||||
// ✅ Cached, reactive
|
||||
const filteredTasks = computed(() =>
|
||||
tasks.value.filter(t => t.status === activeFilter.value)
|
||||
);
|
||||
|
||||
// ❌ Recomputes on every render
|
||||
// <template>{{ tasks.filter(t => t.status === filter) }}</template>
|
||||
```
|
||||
|
||||
2. **Never cause side effects inside `computed`** — computed must be pure
|
||||
```typescript
|
||||
// ❌ Side effect in computed
|
||||
const count = computed(() => {
|
||||
taskStore.logAccess(); // NO — this is a side effect
|
||||
return tasks.value.length;
|
||||
});
|
||||
```
|
||||
|
||||
3. **Use writable computed for two-way bindings**
|
||||
```typescript
|
||||
const modelValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Watch Strategy
|
||||
|
||||
Use the most precise watcher for the situation — over-watching is a performance and correctness problem.
|
||||
|
||||
| Watcher | Use When |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `watchEffect` | Side effect that should re-run whenever any of its reactive dependencies change; auto-tracks dependencies |
|
||||
| `watch` | You need the old value, lazy execution, or want to watch a specific source explicitly |
|
||||
| `computed` | You need a synchronous derived value (prefer this over `watch` for transformation) |
|
||||
|
||||
```typescript
|
||||
// ✅ watchEffect — auto-tracks dependencies
|
||||
watchEffect(() => {
|
||||
document.title = `Tasks (${count.value})`;
|
||||
});
|
||||
|
||||
// ✅ watch — explicit source, has old value
|
||||
watch(userId, async (newId, oldId) => {
|
||||
if (newId !== oldId) await loadUser(newId);
|
||||
}, { immediate: true });
|
||||
|
||||
// ❌ Avoid using watch just for computed values
|
||||
watch(tasks, () => { filteredCount.value = tasks.value.filter(...).length; });
|
||||
// ✅ Use computed instead
|
||||
const filteredCount = computed(() => tasks.value.filter(...).length);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pinia Stores
|
||||
|
||||
> The store directory structure is defined in `project-structure-vue-frontend.md`. This section covers Pinia coding idioms.
|
||||
|
||||
1. **Use the Setup Store API** (not Options API) for new stores
|
||||
```typescript
|
||||
// task/store/task.store.ts
|
||||
export const useTaskStore = defineStore('task', () => {
|
||||
// State
|
||||
const tasks = ref<Task[]>([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// Getters (computed)
|
||||
const completedTasks = computed(() =>
|
||||
tasks.value.filter(t => t.status === 'done')
|
||||
);
|
||||
|
||||
// Actions
|
||||
async function loadTasks() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
tasks.value = await taskAPI.getTasks();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { tasks, isLoading, completedTasks, loadTasks };
|
||||
});
|
||||
```
|
||||
|
||||
2. **Never mutate store state from outside the store**
|
||||
```typescript
|
||||
// ❌ Direct mutation from a component
|
||||
const store = useTaskStore();
|
||||
store.tasks.push(newTask); // NO
|
||||
|
||||
// ✅ Call an action
|
||||
await store.addTask(newTask);
|
||||
```
|
||||
|
||||
3. **Inject the API dependency — never import it directly inside the store**
|
||||
```typescript
|
||||
// ✅ Receives the API interface — testable with createTestingPinia + mock API
|
||||
export const useTaskStore = defineStore('task', () => {
|
||||
const api = inject<TaskAPI>(TASK_API_KEY);
|
||||
if (!api) throw new Error('[TaskStore] TASK_API_KEY not provided — ensure app.provide() is called before store access');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
4. **Use `storeToRefs` when destructuring a store in components**
|
||||
```typescript
|
||||
// ✅ Preserves reactivity
|
||||
const { tasks, isLoading } = storeToRefs(useTaskStore());
|
||||
const { loadTasks } = useTaskStore(); // actions don't need storeToRefs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Composables (`use*` Functions)
|
||||
|
||||
Composables are the Vue equivalent of custom hooks — self-contained, reusable units of reactive logic.
|
||||
|
||||
1. **Naming: always prefix with `use`**
|
||||
- `useTaskFilters`, `useAuth`, `usePagination`
|
||||
|
||||
2. **Return reactive refs, not raw values**
|
||||
```typescript
|
||||
// ✅ Caller can use returned values reactively
|
||||
function useCounter(initial = 0) {
|
||||
const count = ref(initial);
|
||||
const increment = () => count.value++;
|
||||
return { count, increment };
|
||||
}
|
||||
|
||||
// ❌ count is a plain number — not reactive
|
||||
function useCounter() {
|
||||
let count = 0;
|
||||
return { count };
|
||||
}
|
||||
```
|
||||
|
||||
3. **Always clean up side effects in `onUnmounted`**
|
||||
```typescript
|
||||
function useWindowResize() {
|
||||
const width = ref(window.innerWidth);
|
||||
const handler = () => (width.value = window.innerWidth);
|
||||
|
||||
onMounted(() => window.addEventListener('resize', handler));
|
||||
onUnmounted(() => window.removeEventListener('resize', handler)); // ✅ cleanup
|
||||
return { width };
|
||||
}
|
||||
```
|
||||
|
||||
4. **Template refs with `useTemplateRef` (Vue 3.5+)** — type-safe, IDE-friendly replacement for `ref(null)`
|
||||
```typescript
|
||||
// ✅ Vue 3.5+ — useTemplateRef provides fully typed access
|
||||
const inputEl = useTemplateRef<HTMLInputElement>('myInput');
|
||||
// <input ref="myInput" />
|
||||
|
||||
// ❌ Old pattern (before 3.5) — less type-safe
|
||||
const inputEl = ref<HTMLInputElement | null>(null);
|
||||
```
|
||||
|
||||
5. **Feature-specific composables live inside the feature directory** — global composables go in `src/composables/`. See `project-structure-vue-frontend.md`.
|
||||
|
||||
---
|
||||
|
||||
### Component Design
|
||||
|
||||
1. **`defineProps` with TypeScript generics — no runtime validators for typed props**
|
||||
```typescript
|
||||
const props = defineProps<{
|
||||
taskId: string;
|
||||
variant?: 'compact' | 'full';
|
||||
}>();
|
||||
|
||||
// Defaults via withDefaults
|
||||
const props = withDefaults(defineProps<{ variant?: 'compact' | 'full' }>(), {
|
||||
variant: 'full',
|
||||
});
|
||||
```
|
||||
|
||||
2. **`defineEmits` with typed event signatures**
|
||||
```typescript
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
'submit': [task: CreateTaskRequest];
|
||||
}>();
|
||||
```
|
||||
|
||||
3. **`v-model` contract: always `modelValue` prop + `update:modelValue` emit**
|
||||
|
||||
4. **`defineExpose` to selectively expose methods to parent refs**
|
||||
```typescript
|
||||
// Everything in <script setup> is private by default.
|
||||
// Use defineExpose only for intentional parent access (e.g., form.reset()).
|
||||
defineExpose({ reset, focus });
|
||||
// ❌ Without defineExpose: parent ref.value.reset() will be undefined
|
||||
```
|
||||
|
||||
5. **`v-bind="$attrs"` and `inheritAttrs: false` for forwarding attributes**
|
||||
```typescript
|
||||
// Avoid prop drilling for HTML attributes — forward them to the root element
|
||||
defineOptions({ inheritAttrs: false });
|
||||
// In template: <input v-bind="$attrs" />
|
||||
```
|
||||
|
||||
6. **One concern per component** — if the template exceeds 100 lines (excluding boilerplate), extract a sub-component
|
||||
|
||||
7. **Never put business logic in the template** — computed and composables belong in `<script setup>`
|
||||
|
||||
---
|
||||
|
||||
### Template Patterns
|
||||
|
||||
1. **Always bind `:key` with stable, unique IDs in `v-for`** — never use index as key when list order can change
|
||||
```html
|
||||
<!-- ✅ Stable key -->
|
||||
<TaskCard v-for="task in tasks" :key="task.id" :task="task" />
|
||||
|
||||
<!-- ❌ Index key — causes rerender bugs when list reordered -->
|
||||
<TaskCard v-for="(task, i) in tasks" :key="i" :task="task" />
|
||||
```
|
||||
|
||||
2. **Never combine `v-if` and `v-for` on the same element** — wrap with `<template>`
|
||||
```html
|
||||
<!-- ✅ -->
|
||||
<template v-for="task in tasks" :key="task.id">
|
||||
<TaskCard v-if="task.visible" :task="task" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Route Transitions
|
||||
|
||||
When using `<Transition>` or `<RouterView>` with transition effects, CSS frameworks that use `@layer` (Tailwind v4, Open Props, UnoCSS) can silently break SPA navigation by overriding transition properties in the cascade. This causes `transitionend` to never fire, permanently blocking the entering component.
|
||||
|
||||
1. **Avoid `mode="out-in"` when using `@layer`-based CSS frameworks** — the leaving component's `transitionend` event may never fire, blocking the entering component indefinitely. Use simultaneous transitions instead:
|
||||
```html
|
||||
<!-- ❌ Dangerous with @layer CSS frameworks -->
|
||||
<Transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
|
||||
<!-- ✅ Safe: simultaneous leave/enter, always mounts new component -->
|
||||
<Transition name="fade">
|
||||
<component :is="Component" :key="$route.path" />
|
||||
</Transition>
|
||||
```
|
||||
|
||||
2. **Always bind `:key="$route.path"`** on dynamic `<component>` inside `<Transition>` — forces Vue to treat each route as a distinct component instance, ensuring proper enter/leave lifecycle
|
||||
|
||||
3. **Use `!important` on route transition CSS classes** — guarantees transition properties win the `@layer` cascade:
|
||||
```css
|
||||
.fade-enter-active {
|
||||
transition: opacity 0.15s ease-in !important;
|
||||
}
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease-out !important;
|
||||
position: absolute !important;
|
||||
width: 100% !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
4. **Give the transition parent `position: relative`** — contains the absolutely-positioned leaving element during the simultaneous transition overlap
|
||||
|
||||
> For full diagnosis steps when a transition-stuck blank screen occurs, see the Debugging Protocol's [Frontend module](file:///home/irahardianto/works/projects/awesome-agv/.agents/skills/debugging-protocol/languages/frontend.md) § CSS × Animation.
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Vue-specific tooling.
|
||||
|
||||
1. **Use `createTestingPinia` to stub stores in component tests**
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
|
||||
const wrapper = mount(TaskView, {
|
||||
global: {
|
||||
plugins: [createTestingPinia({ createSpy: vi.fn })],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
2. **Test component behaviour, not implementation details** — query by accessible role, not CSS class
|
||||
|
||||
3. **Test stores independently** — use `setActivePinia(createPinia())` in store unit tests
|
||||
|
||||
---
|
||||
|
||||
### Linting and Type Checking
|
||||
|
||||
| Tool | Purpose |
|
||||
| ------------------- | --------------------------- |
|
||||
| `vue-tsc --noEmit` | Full-template type checking |
|
||||
| `eslint-plugin-vue` | Vue-specific lint rules |
|
||||
| `prettier` | Canonical formatting |
|
||||
|
||||
See `code-completion-mandate.md` for exact commands.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- TypeScript Idioms and Patterns @typescript-idioms-and-patterns.md
|
||||
- Project Structure — Vue Frontend @project-structure-vue-frontend.md
|
||||
- Architectural Patterns — Testability-First Design @architectural-pattern.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,89 @@
|
||||
# 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
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
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)
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,342 @@
|
||||
# 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
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,71 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
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) |
|
||||
@@ -0,0 +1,347 @@
|
||||
# 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
|
||||
@@ -0,0 +1,133 @@
|
||||
# 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 |
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,374 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
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`)
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
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`)
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
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`)
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
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`)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
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="test@example.com")
|
||||
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
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
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 |
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
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 |
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
{"timestamp":"2026-04-11T05:27:00Z","iteration":0,"metric":100,"status":"baseline","summary":"Initial baseline - 5/5 tests passing","commit":"baseline"}
|
||||
@@ -0,0 +1 @@
|
||||
{"started_at": "2026-04-11T05:27:00Z", "iteration": 0}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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
+1
Submodule .claude/worktrees/distracted-pike added at 9da25c3688.
+48
-7
@@ -13,7 +13,9 @@ This milestone hardens the existing Tauri runtime workspace so operators can tru
|
||||
- [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)
|
||||
- [ ] **Phase 4: Model Source Validation** - Harden source-specific model and path selection with reliable validation and recovery cues.
|
||||
- [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
|
||||
|
||||
@@ -62,18 +64,55 @@ Plans:
|
||||
- [x] `03-03-PLAN.md` - Integrate the unified workspace into the shell and remove split runtime surfaces.
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 4: Model Source Validation
|
||||
### 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 3
|
||||
**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] `04-01-PLAN.md` - Add source-mode validation contract.
|
||||
- [x] `04-02-PLAN.md` - Add path and model file validation.
|
||||
- [x] `04-03-PLAN.md` - Add validation UI and recovery experience.
|
||||
- [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
|
||||
@@ -83,4 +122,6 @@ Plans:
|
||||
| 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. Model Source Validation | 3/3 | Planned | - |
|
||||
| 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 |
|
||||
+35
-22
@@ -2,16 +2,16 @@
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: verifying
|
||||
stopped_at: Completed Phase 3 - Unified Runtime Workspace
|
||||
last_updated: "2026-03-29T20:49:43.441Z"
|
||||
last_activity: 2026-03-29
|
||||
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: 4
|
||||
completed_phases: 3
|
||||
total_plans: 9
|
||||
completed_plans: 9
|
||||
percent: 50
|
||||
total_phases: 6
|
||||
completed_phases: 6
|
||||
total_plans: 16
|
||||
completed_plans: 16
|
||||
percent: 100
|
||||
---
|
||||
|
||||
# Project State
|
||||
@@ -21,24 +21,24 @@ progress:
|
||||
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 3 - Unified Runtime Workspace
|
||||
**Current focus:** Phase 6 complete — milestone state needs final audit because ROADMAP top-level phase ordering was stale.
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 3 of 4 (Unified Runtime Workspace)
|
||||
Plan: 3 of 3 authored for current phase
|
||||
Status: Phase complete — ready for verification
|
||||
Last activity: 2026-03-30 - Completed quick task 1: Investigar e corrigir bug de internal error na segunda mensagem do chat
|
||||
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: [█████░░░░░] 50%
|
||||
Progress: [██████████] 100%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
**Velocity:**
|
||||
|
||||
- Total plans completed: 6
|
||||
- Total plans completed: 12
|
||||
- Average duration: 13min
|
||||
- Total execution time: 1.3 hours
|
||||
- Total execution time: ~2.6 hours
|
||||
|
||||
**By Phase:**
|
||||
|
||||
@@ -46,11 +46,13 @@ Progress: [█████░░░░░] 50%
|
||||
|-------|-------|-------|----------|
|
||||
| 01 | 3 | 46min | 15min |
|
||||
| 02 | 3 | 33min | 11min |
|
||||
| 03 | 3 | 39min | 13min |
|
||||
| 04 | 3 | 45min | 15min |
|
||||
|
||||
**Recent Trend:**
|
||||
|
||||
- Last 5 plans: 02-03 (13min), 02-02 (14min), 02-01 (6min), 01-03 (18min), 01-02 (18min)
|
||||
- Trend: Improving
|
||||
- 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 |
|
||||
@@ -58,6 +60,12 @@ Progress: [█████░░░░░] 50%
|
||||
| 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
|
||||
|
||||
@@ -82,6 +90,10 @@ Recent decisions affecting current work:
|
||||
- [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
|
||||
|
||||
@@ -89,7 +101,7 @@ None yet.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
- None currently; Phase 3 can build on the now-stable lifecycle/apply contract.
|
||||
None — all phases complete.
|
||||
|
||||
### Quick Tasks Completed
|
||||
|
||||
@@ -99,6 +111,7 @@ None yet.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-29T20:49:43.436Z
|
||||
Stopped at: Completed Phase 3 - Unified Runtime Workspace
|
||||
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.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
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"]
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
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"]
|
||||
@@ -0,0 +1,106 @@
|
||||
# Phase 4: Hugging Face Download Engine
|
||||
|
||||
## Context
|
||||
|
||||
After completing the unified runtime workspace (Phase 3), the next priority was implementing the Hugging Face download engine. The HF search UI was already functional, but download buttons used a simulated `setInterval` progress animation with no actual file transfer.
|
||||
|
||||
**Architecture decision:** Downloads run in Rust (not JavaScript) to avoid IPC bottlenecks for multi-GB GGUF files. Progress is emitted via Tauri v2 events and consumed by a React hook.
|
||||
|
||||
**Key constraints:**
|
||||
- Tauri v2 + React 18 + TypeScript + Vite
|
||||
- No `@tauri-apps/plugin-fs` configured; only `shell` plugin active
|
||||
- Existing HF search already uses `reqwest` with optional bearer token
|
||||
- Managed models directory already resolved via `model_library::paths::managed_models_dir`
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
### Rust Backend (`src-tauri`)
|
||||
|
||||
**`model_library/huggingface.rs`** — Full download engine:
|
||||
- `HuggingFaceDownloadState` manages active downloads with `AtomicU64` IDs and `Mutex<HashMap>` cancellation flags
|
||||
- `start_hugging_face_download()` streams bytes via `reqwest` + `tokio::fs::File` to a `.part` temp file
|
||||
- Emits Tauri events: `started`, throttled `progress` (~500ms), `completed`, `cancelled`, `error`
|
||||
- `cancel_hugging_face_download()` sets cancellation flag, removes temp file
|
||||
- Path validation rejects empty, `.`, `..`, and backslash-containing segments
|
||||
- Direct resolve URL: `https://huggingface.co/{repo}/resolve/main/{fileName}`
|
||||
- Bearer token applied when available
|
||||
- Temp file renamed to final destination on success
|
||||
|
||||
**`lib.rs`** — Command registration:
|
||||
- `start_hugging_face_download_command` and `cancel_hugging_face_download_command` registered in `invoke_handler`
|
||||
- Shared `Arc<HuggingFaceDownloadState>` managed via `.manage()`
|
||||
|
||||
**`model_library/types.rs`** — Serializable payloads:
|
||||
- `HuggingFaceDownloadPhase` enum (Started, Progress, Completed, Cancelled, Error)
|
||||
- `HuggingFaceDownloadProgress` struct with camelCase JSON fields
|
||||
|
||||
**`Cargo.toml`** — Added `tokio` with `fs`, `io-util`, `sync`, `time` features
|
||||
|
||||
### Frontend (`tauri-app/src`)
|
||||
|
||||
**`features/runtime/types.ts`** — TypeScript types:
|
||||
- `HuggingFaceDownloadPhase` union type
|
||||
- `HuggingFaceDownloadProgress` interface matching Rust payload
|
||||
|
||||
**`features/runtime/api.ts`** — API bridge:
|
||||
- `startHuggingFaceDownload(repo, fileName, token)` → `invoke("start_hugging_face_download_command")`
|
||||
- `cancelHuggingFaceDownload(downloadId)` → `invoke("cancel_hugging_face_download_command")`
|
||||
- `HUGGING_FACE_DOWNLOAD_EVENT` constant for event listening
|
||||
|
||||
**`features/runtime/useModelDownload.ts`** — React hook:
|
||||
- `useModelDownload(token: string)` returns `isDownloading`, `progress`, `speed`, `downloaded`, `total`, `activeFile`, `error`, `startDownload()`, `cancelDownload()`
|
||||
- Listens to Tauri events, tracks active download ID, ignores stale events
|
||||
- Explicit `ModelDownloadSnapshot` interface for correct TypeScript typing
|
||||
|
||||
**`features/settings/SettingsPage.tsx`** — UI integration:
|
||||
- Removed simulated `setInterval` download state
|
||||
- Imported `useModelDownload` hook
|
||||
- `.sp-hf-file` class added to each HF file row
|
||||
- Active file row switches to real progress-bar state when downloading
|
||||
- Shows real `speed`, `downloaded`, `total`, `progress` values
|
||||
- Cancel button in active progress state
|
||||
- Download buttons have visible focus rings and disabled state during active downloads
|
||||
|
||||
### Tests
|
||||
|
||||
**`features/runtime/useModelDownload.test.tsx`** — Hook unit tests:
|
||||
- Verifies `startDownload()` invokes correct Tauri command with `{ repo, fileName, token }`
|
||||
- Verifies progress/speed/downloaded/total update from events
|
||||
- Verifies stale events are ignored
|
||||
- Verifies `cancelDownload()` invokes cancel command with active `downloadId`
|
||||
|
||||
**`features/settings/SettingsPage.download.test.tsx`** — Integration test:
|
||||
- Mocks HF search results and `useModelDownload`
|
||||
- Verifies `.sp-hf-file` row renders and clicking download calls `startDownload("microsoft/phi-4", "quantized/phi-4-q4.gguf")`
|
||||
|
||||
**`model_library/huggingface.rs` tests** — Rust unit tests:
|
||||
- Direct resolve URL construction
|
||||
- MiB conversion
|
||||
- Progress percentage calculation
|
||||
- Path validation (valid and invalid segments)
|
||||
|
||||
## Verification
|
||||
|
||||
- ✅ `npm run build` — passes
|
||||
- ✅ `npx tsc --noEmit` — passes (0 errors)
|
||||
- ✅ `npm test -- useModelDownload.test.tsx SettingsPage.download.test.tsx` — 3/3 tests pass
|
||||
- ✅ `cargo test hugging_face` — passes
|
||||
- ⚠️ 6 pre-existing unrelated test failures in `App.test.tsx`, `ChatView.test.tsx`, `MessageBubble.test.tsx` (not caused by this phase)
|
||||
- ⚠️ 1 pre-existing Rust warning: `save_index` unused in `local_index.rs` (not caused by this phase)
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `tauri-app/src-tauri/Cargo.toml` | Added `tokio` dependency |
|
||||
| `tauri-app/src-tauri/src/model_library/types.rs` | Added download phase enum + progress struct |
|
||||
| `tauri-app/src-tauri/src/model_library/huggingface.rs` | Full download engine + tests |
|
||||
| `tauri-app/src-tauri/src/lib.rs` | Added download commands + managed state |
|
||||
| `tauri-app/src/features/runtime/types.ts` | Added download types |
|
||||
| `tauri-app/src/features/runtime/api.ts` | Added download API helpers |
|
||||
| `tauri-app/src/features/runtime/useModelDownload.ts` | New React hook |
|
||||
| `tauri-app/src/features/runtime/useModelDownload.test.tsx` | Hook unit tests |
|
||||
| `tauri-app/src/features/settings/SettingsPage.tsx` | Replaced simulation with real download |
|
||||
| `tauri-app/src/features/settings/SettingsPage.download.test.tsx` | Integration test |
|
||||
| `.planning/ROADMAP.md` | Realigned to HF download milestone |
|
||||
| `.planning/STATE.md` | Updated to Phase 4 complete |
|
||||
@@ -0,0 +1,62 @@
|
||||
# Phase 5: Model Source Validation - Context
|
||||
|
||||
**Gathered:** 2026-04-02
|
||||
**Status:** Ready for planning
|
||||
**Mode:** Auto-generated (implementation already complete)
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Users can choose valid runtime sources, models, and storage paths without destabilizing draft safety or applying invalid combinations.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Validation Strategy
|
||||
- Source-mode validation runs on every draft reconciliation and draft open
|
||||
- Field-level validation errors are tracked per field in draft state
|
||||
- Invalid combinations are blocked before apply (not after)
|
||||
- Recovery guidance shown inline via field validation messages
|
||||
|
||||
### the agent's Discretion
|
||||
Implementation was completed as part of Phase 4 work — validation contracts, path validation, model file validation, and UI recovery experience are all implemented in runtime-utils.ts, useRuntimeManager.ts, and RuntimeSettingsDrawer.tsx.
|
||||
|
||||
</decisions>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `validateSourceMode()` in runtime-utils.ts — validates source/mode combinations
|
||||
- `validateModelPath()` in runtime-utils.ts — validates model file paths
|
||||
- `validateModelsDirectory()` in runtime-utils.ts — async directory validation
|
||||
- `validateModelFile()` in runtime-utils.ts — validates individual model files
|
||||
- Field validation state in useRuntimeManager draft state
|
||||
|
||||
### Established Patterns
|
||||
- Validation runs on draft open, reset, and reconcile-to-baseline
|
||||
- Field validations stored as `Record<string, FieldValidation>` in draft state
|
||||
- UI shows inline validation errors per field in RuntimeSettingsDrawer
|
||||
|
||||
### Integration Points
|
||||
- useRuntimeManager.ts — validation integrated into draft lifecycle
|
||||
- RuntimeSettingsDrawer.tsx — displays validation errors inline
|
||||
- runtime-utils.ts — all validation logic centralized
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
Implementation already complete — no additional requirements.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — phase complete.
|
||||
|
||||
</deferred>
|
||||
@@ -0,0 +1,35 @@
|
||||
status: passed
|
||||
phase: 5
|
||||
name: Model Source Validation
|
||||
date: 2026-04-02
|
||||
|
||||
## Verification Summary
|
||||
|
||||
All Phase 5 success criteria are met by existing implementation.
|
||||
|
||||
## Success Criteria Verification
|
||||
|
||||
### 1. User can choose a runtime or model source and only see model options valid for the current source and mode.
|
||||
- ✓ `validateSourceMode()` in runtime-utils.ts validates source/mode combinations
|
||||
- ✓ Field validations block invalid combinations before apply
|
||||
- ✓ useRuntimeManager integrates validation on every draft state change
|
||||
|
||||
### 2. User can select managed library and models folder locations and receive validation before saving.
|
||||
- ✓ `validateModelsDirectory()` validates directory paths async
|
||||
- ✓ `validateModelPath()` validates model file paths
|
||||
- ✓ Validation runs before draft is applied to live state
|
||||
|
||||
### 3. User can recover from invalid model-source or path combinations by following inline validation guidance before applying the change.
|
||||
- ✓ Field-level validation errors displayed inline in RuntimeSettingsDrawer
|
||||
- ✓ Apply button blocked when validation errors exist
|
||||
- ✓ Recovery guidance shown per field
|
||||
|
||||
## Implementation Evidence
|
||||
|
||||
- `tauri-app/src/features/runtime/runtime-utils.ts` — validateSourceMode, validateModelPath, validateModelsDirectory, validateModelFile
|
||||
- `tauri-app/src/features/runtime/useRuntimeManager.ts` — validation integrated at lines 79-82, 203-208, 236-239
|
||||
- `tauri-app/src/features/runtime/types.ts` — validation types defined
|
||||
|
||||
## Notes
|
||||
|
||||
Phase 5 implementation was completed as part of Phase 4 work. The phase directory was missing but all code artifacts are present and verified.
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- tauri-app/src-tauri/src/model_files.rs
|
||||
- tauri-app/src/components/ModelSelectorPill.tsx
|
||||
- tauri-app/src/components/Sidebar.tsx
|
||||
- tauri-app/src/App.tsx
|
||||
- tauri-app/src/components/ChatView.tsx
|
||||
autonomous: true
|
||||
requirements: [UI-01]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "scan_models returns only GGUF files at the root of the models directory (no subdirectory traversal)"
|
||||
- "Model selector dropdown appears inside the chat input area (both hero and regular input)"
|
||||
- "Model selector is absent from the Sidebar"
|
||||
- "Selecting a model in the input pill updates the active model"
|
||||
artifacts:
|
||||
- path: "tauri-app/src-tauri/src/model_files.rs"
|
||||
provides: "scan_model_dir_top_level function replaces recursive scan"
|
||||
contains: "scan_model_dir_top_level"
|
||||
- path: "tauri-app/src/components/ModelSelectorPill.tsx"
|
||||
provides: "Reusable model selector dropdown pill for input area"
|
||||
exports: ["ModelSelectorPill", "ModelSelectorPillProps"]
|
||||
- path: "tauri-app/src/components/Sidebar.tsx"
|
||||
provides: "Sidebar without model-section block"
|
||||
key_links:
|
||||
- from: "tauri-app/src/App.tsx"
|
||||
to: "tauri-app/src/components/ChatView.tsx"
|
||||
via: "new props: models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status"
|
||||
pattern: "models=\\{models\\}"
|
||||
- from: "tauri-app/src/components/ChatView.tsx"
|
||||
to: "tauri-app/src/components/ModelSelectorPill.tsx"
|
||||
via: "import and render inside .input-box"
|
||||
pattern: "<ModelSelectorPill"
|
||||
- from: "tauri-app/src-tauri/src/model_files.rs"
|
||||
to: "scan_model_dir_top_level"
|
||||
via: "scan_models Tauri command calls top-level scanner"
|
||||
pattern: "scan_model_dir_top_level"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Fix the Rust model scan to return only top-level GGUF files, create a ModelSelectorPill component, move the model selector out of the Sidebar and into the ChatView input area.
|
||||
|
||||
Purpose: The model selector must live in the chat input bubble (Manus AI pattern), not the sidebar. The scan bug returns unintended nested models from HuggingFace cache dirs.
|
||||
Output: `scan_model_dir_top_level` Rust function, `ModelSelectorPill.tsx` component, Sidebar cleaned of model-section, ChatView wired with model selector in both input boxes.
|
||||
</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/phases/06-chat-ui-redesign/06-RESEARCH.md
|
||||
|
||||
@tauri-app/src-tauri/src/model_files.rs
|
||||
@tauri-app/src/components/Sidebar.tsx
|
||||
@tauri-app/src/components/ChatView.tsx
|
||||
@tauri-app/src/App.tsx
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From tauri-app/src/components/Sidebar.tsx (lines 13-22) — props that move to ChatView:
|
||||
```typescript
|
||||
interface SidebarProps {
|
||||
models: ModelInfo[]; // <- these 6 props move to ChatView
|
||||
localModels: RuntimeModelFile[];
|
||||
selectedModel: string;
|
||||
onModelChange: (v: string) => void;
|
||||
onModelSwap?: (path: string) => void;
|
||||
isSwapping?: boolean;
|
||||
status: RuntimeStatus;
|
||||
// ... non-model props stay in Sidebar
|
||||
}
|
||||
```
|
||||
|
||||
From tauri-app/src/components/ChatView.tsx (lines 18-28) — current ChatView Props:
|
||||
```typescript
|
||||
interface Props {
|
||||
db: DB;
|
||||
chatId: number | null;
|
||||
server: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
onChatCreated: (id: number) => void;
|
||||
onChatUpdated: () => void;
|
||||
onOpenSidebar?: () => void;
|
||||
onDegradationDetected?: () => void;
|
||||
}
|
||||
// New props to add: models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status
|
||||
```
|
||||
|
||||
From tauri-app/src/components/Sidebar.tsx (lines 56-71) — deduplication logic to extract:
|
||||
```typescript
|
||||
// Merge API models + local scanned models, dedupe by id, filter isInChatSelector
|
||||
const allModels: ModelOption[] = [
|
||||
...models
|
||||
.filter((m) => m.id !== "" && m.id !== "local")
|
||||
.map((m) => ({ id: m.id, label: getDisplayName(m.id) })),
|
||||
...localModels.map((m) => ({ id: m.path, label: getDisplayName(m.path) })),
|
||||
].filter((m, i, arr) => arr.findIndex((x) => x.id === m.id) === i);
|
||||
const selectedLabel = getDisplayName(selectedModel) || "No model";
|
||||
```
|
||||
|
||||
New ModelSelectorPill interface (to create):
|
||||
```typescript
|
||||
interface ModelSelectorPillProps {
|
||||
models: ModelInfo[];
|
||||
localModels: RuntimeModelFile[];
|
||||
selectedModel: string;
|
||||
onModelChange: (id: string) => void;
|
||||
onModelSwap?: (path: string) => void;
|
||||
isSwapping?: boolean;
|
||||
status: RuntimeStatus;
|
||||
}
|
||||
```
|
||||
|
||||
Rust scan_models Tauri command signature (model_files.rs):
|
||||
```rust
|
||||
#[tauri::command]
|
||||
pub fn scan_models(dir: String) -> Result<Vec<ModelFile>, String>
|
||||
// Currently calls scan_model_dir() -> scan_model_dir_recursive()
|
||||
// Must change to call scan_model_dir_top_level()
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Fix Rust model scan to top-level only + update test</name>
|
||||
<files>tauri-app/src-tauri/src/model_files.rs</files>
|
||||
<action>
|
||||
Add a new `scan_model_dir_top_level(path: &Path) -> Result<Vec<ModelFile>, String>` function that reads only the immediate children of `path` (depth=1) — no recursion. Only collects entries where `p.is_file()` and extension is `"gguf"` (case-insensitive). Sort by name. Keep `scan_model_dir_recursive` in place (used by existing tests and potentially future use) — do NOT delete it.
|
||||
|
||||
Update `scan_model_dir()` to call `scan_model_dir_top_level` instead of `scan_model_dir_recursive` — OR bypass `scan_model_dir()` entirely and have the `scan_models` Tauri command call `scan_model_dir_top_level` directly.
|
||||
|
||||
Update the `scan_model_dir_finds_gguf_in_subdirectories` test (line 196): rename it to `scan_model_dir_returns_only_top_level_gguf` and update the assertion so nested models are NOT in the result — only `["top-level.gguf"]`. Add a new test `scan_model_dir_recursive_still_finds_nested` that exercises `scan_model_dir_recursive` directly to ensure the recursive function still works.
|
||||
|
||||
Verify: `cargo test -p commands` (or `cargo test` in `tauri-app/src-tauri/`) passes with no failures.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd tauri-app/src-tauri && cargo test scan_model 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>All `scan_model*` tests pass. `scan_models` Tauri command uses top-level scan. Nested GGUFs are NOT returned by default.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create ModelSelectorPill + wire into ChatView + clean Sidebar</name>
|
||||
<files>
|
||||
tauri-app/src/components/ModelSelectorPill.tsx,
|
||||
tauri-app/src/components/Sidebar.tsx,
|
||||
tauri-app/src/components/ChatView.tsx,
|
||||
tauri-app/src/App.tsx
|
||||
</files>
|
||||
<action>
|
||||
**Step A — Create `ModelSelectorPill.tsx`:**
|
||||
Create `tauri-app/src/components/ModelSelectorPill.tsx`. Extract the deduplication logic from `Sidebar.tsx` lines 56-71 into this component (or call a shared util). The component renders a compact pill button showing the selected model's display name (`getDisplayName`). Clicking opens a dropdown listbox (reuse the same HTML pattern as `Sidebar.tsx` lines 136-168: `model-dropdown-menu` with `model-dropdown-item` entries). Use `getDisplayName` from `lib/modelAlias`. Emit `onModelChange(id)` when item selected; if item is a local model path, also call `onModelSwap(path)`. Show a spinner/disabled state when `isSwapping` is true. Style: `bg-[#262626] text-[#9CA3AF] rounded-md px-2 py-1 text-sm flex items-center gap-1.5` for the pill trigger. Dropdown: absolute positioned, `bg-[#1E1E1E] border border-[#333333]`. Props: `ModelSelectorPillProps` as defined in the interface block above.
|
||||
|
||||
**Step B — Clean Sidebar.tsx:**
|
||||
In `Sidebar.tsx`, delete the `model-section` div block (lines ~131-170) and remove from `SidebarProps`: `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`. Remove the `status` prop if it was only used for model selector logic. Remove the deduplication logic (lines 56-69) and the `handleModelSelect` handler. Remove `ModelInfo` import if no longer used.
|
||||
|
||||
**Step C — Update App.tsx:**
|
||||
In `App.tsx`, stop passing the 6 model props to `<Sidebar>`. Add them as new props to `<ChatView>`: `models={models}`, `localModels={localModels}`, `selectedModel={selectedModel}`, `onModelChange={setSelectedModel}`, `onModelSwap={handleModelSwap}`, `isSwapping={isSwapping}`, `status={status}`.
|
||||
|
||||
**Step D — Update ChatView.tsx:**
|
||||
Add the 6 model props to ChatView's `Props` interface. Import `ModelSelectorPill`. Render `<ModelSelectorPill ...props />` inside the input area in TWO places:
|
||||
1. Hero input block (around line 657) — inside `.input-box`, before the textarea, on its own row or inline left.
|
||||
2. Regular input block (around line 860-875) — same position inside `.input-box`.
|
||||
|
||||
Do NOT change any existing chat logic, message handling, or streaming logic.
|
||||
|
||||
Pitfall to avoid: Do NOT use `useModelSelector` hook — just pass props directly. Do NOT create a context. Keep it simple prop-drilling.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd tauri-app && npm run build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<done>TypeScript build passes. ModelSelectorPill renders in both hero and regular input areas. Sidebar has no model-section. App.tsx routes model props to ChatView.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `cargo test scan_model` passes — top-level scan verified by updated test
|
||||
- `npm run build` passes — no TypeScript errors
|
||||
- Sidebar.tsx no longer contains `model-section`
|
||||
- ChatView.tsx imports and renders `ModelSelectorPill` in both input contexts
|
||||
- App.tsx passes model props to ChatView (not Sidebar)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Top-level model scan: `scan_models("./models")` returns only `.gguf` files directly in the folder — NOT files from subdirectories
|
||||
- Model selector visible in chat input pill (hero screen and regular screen)
|
||||
- No model selector in the sidebar
|
||||
- TypeScript and Rust compile without errors
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-chat-ui-redesign/06-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 01
|
||||
status: complete
|
||||
completed_at: "2026-04-02T12:00:00Z"
|
||||
---
|
||||
|
||||
# Plan 06-01 Summary: Backend + ModelSelectorPill
|
||||
|
||||
## Completed Tasks
|
||||
|
||||
### Task 1: Fix Rust model scan to top-level only ✅
|
||||
- **Status**: Already implemented
|
||||
- `scan_model_dir_top_level()` function exists in `model_files.rs`
|
||||
- `scan_models` Tauri command uses top-level scan (not recursive)
|
||||
- All 5 tests pass including `scan_model_dir_returns_only_top_level_gguf`
|
||||
|
||||
### Task 2: Create ModelSelectorPill + wire into ChatView + clean Sidebar ✅
|
||||
- **ModelSelectorPill.tsx**: Created at `tauri-app/src/components/ModelSelectorPill.tsx`
|
||||
- Extracts model deduplication logic from Sidebar
|
||||
- Renders compact pill button with status dot and dropdown
|
||||
- Shows spinner when `isSwapping` is true
|
||||
- Uses `getDisplayName` and `isInChatSelector` from modelAlias
|
||||
|
||||
- **Sidebar.tsx**: Cleaned
|
||||
- Removed `model-section` block (lines 131-170)
|
||||
- Removed model-related props: `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status`
|
||||
- Removed `ModelInfo` and `RuntimeModelFile` imports
|
||||
|
||||
- **ChatView.tsx**: Updated
|
||||
- Added model props to Props interface (lines 26-32)
|
||||
- Imported `ModelSelectorPill` component
|
||||
- Rendered `<ModelSelectorPill>` in hero input (line 627)
|
||||
- Rendered `<ModelSelectorPill>` in regular input (line 851)
|
||||
|
||||
- **App.tsx**: Updated
|
||||
- Removed model props from `<Sidebar>` component
|
||||
- Added model props to `<ChatView>`: `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status`
|
||||
|
||||
- **ChatView.test.tsx**: Fixed
|
||||
- Added `defaultChatViewProps` with required model props
|
||||
- Spread `{...defaultChatViewProps}` to all test renders
|
||||
|
||||
## Verification Results
|
||||
|
||||
### TypeScript Build ✅
|
||||
```
|
||||
> tsc && vite build
|
||||
✓ 6325 modules transformed.
|
||||
✓ built in 5.50s
|
||||
```
|
||||
|
||||
### Rust Tests ✅
|
||||
```
|
||||
running 5 tests
|
||||
test model_files::tests::scan_model_dir_filters_and_sorts_gguf_files ... ok
|
||||
test model_files::tests::scan_model_dir_ignores_directories_named_like_models ... ok
|
||||
test model_files::tests::scan_model_dir_skips_hidden_directories ... ok
|
||||
test model_files::tests::scan_model_dir_recursive_finds_nested_gguf ... ok
|
||||
test model_files::tests::scan_model_dir_returns_only_top_level_gguf ... ok
|
||||
|
||||
test result: ok. 5 passed; 0 failed
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
- `tauri-app/src-tauri/src/model_files.rs` (already had top-level scan)
|
||||
- `tauri-app/src/components/ModelSelectorPill.tsx` (created)
|
||||
- `tauri-app/src/components/Sidebar.tsx` (cleaned)
|
||||
- `tauri-app/src/components/ChatView.tsx` (added model props + ModelSelectorPill)
|
||||
- `tauri-app/src/App.tsx` (routed model props to ChatView)
|
||||
- `tauri-app/src/components/ChatView.test.tsx` (fixed tests)
|
||||
|
||||
## Success Criteria Met
|
||||
- [x] Top-level model scan: `scan_models("./models")` returns only `.gguf` files directly in the folder
|
||||
- [x] Model selector visible in chat input pill (hero screen and regular screen)
|
||||
- [x] No model selector in the sidebar
|
||||
- [x] TypeScript and Rust compile without errors
|
||||
@@ -0,0 +1,41 @@
|
||||
# Task 1 Summary: Fix Rust model scan to top-level only
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Files Modified:**
|
||||
- `tauri-app/src-tauri/src/model_files.rs`
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Added `scan_model_dir_top_level` function (lines 46-71)
|
||||
A new function that scans only the immediate children of a directory (depth=1), returning only `.gguf` files without recursing into subdirectories.
|
||||
|
||||
### 2. Updated `scan_model_dir` to use top-level scan (lines 31-44)
|
||||
Changed from calling `scan_model_dir_recursive` to calling `scan_model_dir_top_level` as the default behavior.
|
||||
|
||||
### 3. Kept `scan_model_dir_recursive` for future use (lines 73-97)
|
||||
The recursive function is preserved with `#[allow(dead_code)]` annotation for potential future use cases.
|
||||
|
||||
### 4. Updated tests
|
||||
- Renamed `scan_model_dir_finds_gguf_in_subdirectories` to `scan_model_dir_returns_only_top_level_gguf`
|
||||
- Updated assertion to expect only top-level models
|
||||
- Added new test `scan_model_dir_recursive_finds_nested_gguf` to verify the recursive function still works
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd tauri-app/src-tauri && cargo test scan_model
|
||||
```
|
||||
|
||||
All 5 tests pass:
|
||||
- `scan_model_dir_filters_and_sorts_gguf_files` ✅
|
||||
- `scan_model_dir_ignores_directories_named_like_models` ✅
|
||||
- `scan_model_dir_skips_hidden_directories` ✅
|
||||
- `scan_model_dir_recursive_finds_nested_gguf` ✅
|
||||
- `scan_model_dir_returns_only_top_level_gguf` ✅
|
||||
|
||||
## Success Criteria Met
|
||||
|
||||
- ✅ `scan_models` Tauri command returns only `.gguf` files in the immediate directory
|
||||
- ✅ Nested models in subdirectories are excluded from default scan
|
||||
- ✅ Recursive scan function remains available for future use
|
||||
- ✅ All existing tests continue to pass
|
||||
@@ -0,0 +1,70 @@
|
||||
# Task 2 Summary: Create ModelSelectorPill + wire into ChatView + clean Sidebar
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Files Modified:**
|
||||
- `tauri-app/src/components/ModelSelectorPill.tsx` (new file)
|
||||
- `tauri-app/src/components/ChatView.css` (added model selector styles)
|
||||
- `tauri-app/src/components/Sidebar.tsx` (removed model-section)
|
||||
- `tauri-app/src/App.tsx` (moved model props to ChatView)
|
||||
- `tauri-app/src/components/ChatView.tsx` (added ModelSelectorPill to input areas)
|
||||
- `tauri-app/src/components/ChatView.test.tsx` (updated tests with new props)
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Created `ModelSelectorPill.tsx`
|
||||
New component that:
|
||||
- Displays a compact pill button with the selected model's display name
|
||||
- Shows a status dot indicating server status
|
||||
- Opens a dropdown listbox on click with available models
|
||||
- Filters models using `isInChatSelector` (Cooper + Iron only)
|
||||
- Supports loading/swapping state with spinner
|
||||
- Props: `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status`
|
||||
|
||||
### 2. Added CSS styles for ModelSelectorPill
|
||||
Added to `ChatView.css`:
|
||||
- `.model-selector-pill-wrapper` - container with relative positioning
|
||||
- `.model-selector-pill` - pill button styling
|
||||
- `.model-status-dot` - status indicator dot
|
||||
- `.model-selector-menu` - dropdown listbox
|
||||
- `.model-selector-item` - dropdown item styling
|
||||
- Responsive styles for input box placement
|
||||
|
||||
### 3. Cleaned Sidebar.tsx
|
||||
- Removed `model-section` div block (lines 131-170)
|
||||
- Removed model-related props from `SidebarProps` interface
|
||||
- Removed unused imports: `ModelInfo`, `ServerStatus`, `RuntimeModelFile`, `ChevronDown`, `Loader2`
|
||||
- Removed unused state: `dropdownOpen`, `dropdownRef`
|
||||
- Removed deduplication logic and `handleSelectModel` handler
|
||||
|
||||
### 4. Updated App.tsx
|
||||
- Removed model props from `<Sidebar>` component
|
||||
- Added model props to `<ChatView>`: `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status`
|
||||
|
||||
### 5. Updated ChatView.tsx
|
||||
- Added new props to `Props` interface
|
||||
- Imported `ModelSelectorPill`, `ModelInfo`, `ServerStatus`, `RuntimeModelFile`
|
||||
- Rendered `<ModelSelectorPill>` in two places:
|
||||
- Hero input block (inside `.input-box--hero`, before `.attachment-actions`)
|
||||
- Regular input block (inside `.input-box`, before `.attachment-actions`)
|
||||
|
||||
### 6. Updated ChatView.test.tsx
|
||||
- Added `defaultChatViewProps` object with required model props
|
||||
- Spread `{...defaultChatViewProps}` in all ChatView renders
|
||||
- Updated test assertion from "Connect an API" to "Runtime offline"
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd tauri-app && npm run build
|
||||
cd tauri-app && npm test -- ChatView
|
||||
```
|
||||
|
||||
Both build and tests pass successfully.
|
||||
|
||||
## Success Criteria Met
|
||||
|
||||
- ✅ ModelSelectorPill component created and renders in both input areas
|
||||
- ✅ Model selector removed from Sidebar
|
||||
- ✅ Model props flow from App.tsx → ChatView → ModelSelectorPill
|
||||
- ✅ TypeScript build passes with no errors
|
||||
- ✅ All ChatView tests pass
|
||||
@@ -0,0 +1,281 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [06-01]
|
||||
files_modified:
|
||||
- tauri-app/src/index.css
|
||||
- tauri-app/tailwind.config.js
|
||||
- tauri-app/src/components/ChatView.tsx
|
||||
- tauri-app/src/components/ChatView.css
|
||||
autonomous: true
|
||||
requirements: [UI-02]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "App uses Manus color tokens (#171717 base, #1E1E1E surface, #262626 elevated)"
|
||||
- "Thinking indicator is a single pulsing blue dot (#3B82F6) with 'Thinking' text — not three bouncing dots"
|
||||
- "Send button uses PaperPlaneRight icon only (no 'Send' text label)"
|
||||
- "Message and input area share a consistent reading-column width"
|
||||
artifacts:
|
||||
- path: "tauri-app/src/index.css"
|
||||
provides: "Updated :root CSS vars aligned to Manus tokens + Manus named vars"
|
||||
contains: "--manus-base"
|
||||
- path: "tauri-app/tailwind.config.js"
|
||||
provides: "Manus named color tokens registered for Tailwind use"
|
||||
contains: "manus-base"
|
||||
- path: "tauri-app/src/components/ChatView.css"
|
||||
provides: "thinking-manus CSS class, updated thinking animation"
|
||||
contains: "thinking-manus"
|
||||
key_links:
|
||||
- from: "tauri-app/src/index.css"
|
||||
to: "all components"
|
||||
via: "CSS custom properties cascade"
|
||||
pattern: "--manus-base.*#171717"
|
||||
- from: "tauri-app/src/components/ChatView.tsx"
|
||||
to: "tauri-app/src/components/ChatView.css"
|
||||
via: "className thinking-manus replaces thinking"
|
||||
pattern: "thinking-manus"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Apply Manus color tokens globally, replace the three-dot thinking indicator with a pulsing blue dot + "Thinking" text, swap the send button icon to PaperPlaneRight, and align message/input reading-column widths.
|
||||
|
||||
Purpose: Visual alignment with Manus AI system design — correct tokens, correct indicators, correct iconography.
|
||||
Output: Updated CSS vars, Tailwind config, ChatView.tsx (send button + thinking JSX), ChatView.css (thinking animation + width rules).
|
||||
</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/phases/06-chat-ui-redesign/06-RESEARCH.md
|
||||
@.planning/phases/06-chat-ui-redesign/06-01-SUMMARY.md
|
||||
|
||||
@tauri-app/src/index.css
|
||||
@tauri-app/tailwind.config.js
|
||||
@tauri-app/src/components/ChatView.tsx
|
||||
@tauri-app/src/components/ChatView.css
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key patterns from current codebase. -->
|
||||
|
||||
Current index.css :root vars to update (tauri-app/src/index.css lines 4-14):
|
||||
```css
|
||||
:root {
|
||||
--bg: #0d1115; /* → #171717 (Manus base) */
|
||||
--bg2: #12171d; /* → #1E1E1E (Manus surface) */
|
||||
--bg3: #171d24; /* → #262626 (Manus elevated)*/
|
||||
--bg4: #1e252d; /* → #333333 (Manus hover) */
|
||||
--bg5: #27303a; /* → #333333 or keep as-is */
|
||||
--text: #f2f4f7; /* → #F3F4F6 (Manus tx) */
|
||||
--text2: #a0a9b4; /* → #9CA3AF (Manus tx2) */
|
||||
--text3: #6f7782; /* → #52525B (Manus tx3) */
|
||||
}
|
||||
```
|
||||
|
||||
Current tailwind.config.js (full contents):
|
||||
```js
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
corePlugins: { preflight: false },
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"c-bg": "var(--bg)", "c-bg2": "var(--bg2)", "c-bg3": "var(--bg3)",
|
||||
"c-bg4": "var(--bg4)", "c-bg5": "var(--bg5)",
|
||||
"c-bdr": "var(--border)", "c-bdr2": "var(--border2)",
|
||||
"c-tx": "var(--text)", "c-tx2": "var(--text2)", "c-tx3": "var(--text3)",
|
||||
"c-ac": "var(--accent)", "c-ac2": "var(--accent2)",
|
||||
"c-danger": "var(--danger)", "c-success": "var(--success)", "c-warn": "var(--warning)",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Current thinking indicator JSX (ChatView.tsx lines 763-769):
|
||||
```tsx
|
||||
{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
|
||||
<div className="msg-wrapper assistant">
|
||||
<div className="thinking">
|
||||
<span /><span /><span />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
Current send button (ChatView.tsx line 871 — non-hero):
|
||||
```tsx
|
||||
<button className="send-btn" ...><ArrowUp size={18} weight="duotone" /> Send</button>
|
||||
```
|
||||
Current send button (ChatView.tsx line 657 — hero):
|
||||
```tsx
|
||||
<button className="send-btn" ...><ArrowUp size={18} weight="duotone" /></button>
|
||||
```
|
||||
|
||||
Current message width CSS (ChatView.css):
|
||||
```css
|
||||
.messages-container { max-width: 1200px; }
|
||||
.msg-wrapper.user { max-width: 800px; }
|
||||
.msg-wrapper.assistant { max-width: 100%; } /* <- too wide */
|
||||
.input-row { max-width: 800px; }
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Apply Manus color tokens to index.css and tailwind.config.js</name>
|
||||
<files>tauri-app/src/index.css, tauri-app/tailwind.config.js</files>
|
||||
<action>
|
||||
**index.css — update :root vars to Manus values:**
|
||||
Change the existing CSS vars to the Manus design tokens. Also ADD new named Manus vars for direct use (do not remove old vars — components still reference them):
|
||||
```css
|
||||
:root {
|
||||
/* Updated to Manus design tokens */
|
||||
--bg: #171717; /* was #0d1115 — Manus bg-base */
|
||||
--bg2: #1E1E1E; /* was #12171d — Manus bg-surface */
|
||||
--bg3: #262626; /* was #171d24 — Manus bg-elevated */
|
||||
--bg4: #333333; /* was #1e252d — Manus bg-hover */
|
||||
--bg5: #3D3D3D; /* was #27303a — slightly lighter */
|
||||
--text: #F3F4F6; /* was #f2f4f7 — Manus text-primary */
|
||||
--text2: #9CA3AF; /* was #a0a9b4 — Manus text-secondary */
|
||||
--text3: #52525B; /* was #6f7782 — Manus text-muted */
|
||||
/* Keep all other existing vars (--border, --accent, etc.) unchanged */
|
||||
|
||||
/* NEW: Manus design tokens as named vars (for new components) */
|
||||
--manus-base: #171717;
|
||||
--manus-surface: #1E1E1E;
|
||||
--manus-elevated: #262626;
|
||||
--manus-hover: #333333;
|
||||
--manus-tx: #F3F4F6;
|
||||
--manus-tx2: #9CA3AF;
|
||||
--manus-tx3: #52525B;
|
||||
--manus-blue: #3B82F6;
|
||||
}
|
||||
```
|
||||
|
||||
**tailwind.config.js — add Manus named color tokens:**
|
||||
Add the following entries to the `colors` extension (append after existing entries):
|
||||
```js
|
||||
"manus-base": "var(--manus-base)",
|
||||
"manus-surface": "var(--manus-surface)",
|
||||
"manus-elevated": "var(--manus-elevated)",
|
||||
"manus-hover": "var(--manus-hover)",
|
||||
"manus-tx": "var(--manus-tx)",
|
||||
"manus-tx2": "var(--manus-tx2)",
|
||||
"manus-tx3": "var(--manus-tx3)",
|
||||
"manus-blue": "var(--manus-blue)",
|
||||
```
|
||||
|
||||
Pitfall: CSS specificity — existing hardcoded hex values in `.css` files (e.g., `background: #252525` in ChatView.css) will NOT automatically update when CSS vars change. Do NOT touch those yet (they are handled in Task 2).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd tauri-app && npm run build 2>&1 | grep -E "error|warning" | head -10</automated>
|
||||
</verify>
|
||||
<done>index.css :root has updated vars + new --manus-* vars. tailwind.config.js has manus-* color tokens. Build passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Replace thinking indicator, send button icon, and align message widths</name>
|
||||
<files>tauri-app/src/components/ChatView.tsx, tauri-app/src/components/ChatView.css</files>
|
||||
<action>
|
||||
**ChatView.tsx — three changes:**
|
||||
|
||||
1. **Import:** Add `PaperPlaneRight` to the phosphor import line (line 10):
|
||||
```tsx
|
||||
import { ArrowUp, PaperPlaneRight, Image, Microphone, Paperclip, ChatCircleText } from "@phosphor-icons/react";
|
||||
```
|
||||
(Keep `ArrowUp` import only if used elsewhere; otherwise remove it.)
|
||||
|
||||
2. **Thinking indicator (lines ~763-769):** Replace the three-span bounce with Manus-style:
|
||||
```tsx
|
||||
{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
|
||||
<div className="msg-wrapper assistant">
|
||||
<div className="thinking-manus">
|
||||
<span className="thinking-dot" aria-hidden="true" />
|
||||
<span className="thinking-label">Thinking</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
3. **Send button icon:** Find ALL occurrences of `<ArrowUp` inside send button JSX (hero ~line 657, regular ~line 871) and replace with `<PaperPlaneRight size={18} weight="fill" />`. Remove the "Send" text label from the non-hero button to make it icon-only. Keep the `.send-btn` class unchanged.
|
||||
|
||||
**ChatView.css — four changes:**
|
||||
|
||||
1. **Remove/replace old `.thinking span` block** (lines ~770-802). Replace with Manus thinking styles:
|
||||
```css
|
||||
.thinking-manus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.thinking-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #3B82F6;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
animation: thinking-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
.thinking-label {
|
||||
font-size: 0.875rem;
|
||||
color: #9CA3AF;
|
||||
font-weight: 400;
|
||||
}
|
||||
@keyframes thinking-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.35; transform: scale(0.7); }
|
||||
}
|
||||
```
|
||||
Remove the old `@keyframes thinking-bounce` block too.
|
||||
|
||||
2. **Message + input reading column alignment:**
|
||||
Update the width rules so messages and input align:
|
||||
```css
|
||||
/* Consistent reading column: 800px for user, aligned input */
|
||||
.msg-wrapper.assistant { max-width: 800px; margin-left: 0; } /* was 100% */
|
||||
.msg-wrapper.user { max-width: 800px; } /* unchanged */
|
||||
.input-row { max-width: 800px; } /* unchanged */
|
||||
```
|
||||
Keep `.messages-container { max-width: 1200px; }` as the scroll container — only the message wrapper width matters.
|
||||
|
||||
3. **Update any hardcoded background on `.chat-view` or `.initial-screen`:**
|
||||
Find and replace `background: #252525` (or similar hardcoded colors on the hero/initial screen container) with `background: var(--bg)` so it picks up the updated `#171717` token. Only change the hero background — do not mass-replace all hex values.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd tauri-app && npm run build 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>TypeScript build passes. ChatView renders PaperPlaneRight send button. thinking-manus class replaces thinking. Message widths set to max 800px for both user and assistant.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npm run build` passes with no TypeScript errors
|
||||
- index.css has `--manus-base: #171717` and updated `--bg: #171717`
|
||||
- tailwind.config.js has `manus-base` through `manus-blue` color tokens
|
||||
- ChatView.tsx imports `PaperPlaneRight`, renders it in both send buttons
|
||||
- ChatView.css has `.thinking-manus` with pulse animation (no more `.thinking span` bounce)
|
||||
- `.msg-wrapper.assistant` max-width is 800px (not 100%)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- App background is neutral dark (#171717), not navy-black
|
||||
- Thinking state shows "● Thinking" (pulsing blue dot + text), not three bouncing dots
|
||||
- Send button is a paper plane icon, no text label
|
||||
- User and assistant messages share same max reading width (800px)
|
||||
- Build passes clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-chat-ui-redesign/06-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 02
|
||||
status: complete
|
||||
completed_at: "2026-04-02T12:15:00Z"
|
||||
---
|
||||
|
||||
# Plan 06-02 Summary: Chat Screen Redesign
|
||||
|
||||
## Completed Tasks
|
||||
|
||||
### Task 1: Apply Manus color tokens to index.css and tailwind.config.js ✅
|
||||
|
||||
**index.css changes:**
|
||||
- Updated `:root` CSS vars to Manus design tokens:
|
||||
- `--bg`: `#0d1115` → `#171717` (Manus base)
|
||||
- `--bg2`: `#12171d` → `#1E1E1E` (Manus surface)
|
||||
- `--bg3`: `#171d24` → `#262626` (Manus elevated)
|
||||
- `--bg4`: `#1e252d` → `#333333` (Manus hover)
|
||||
- `--bg5`: `#27303a` → `#3D3D3D` (slightly lighter)
|
||||
- `--text`: `#f2f4f7` → `#F3F4F6` (Manus text-primary)
|
||||
- `--text2`: `#a0a9b4` → `#9CA3AF` (Manus text-secondary)
|
||||
- `--text3`: `#6f7782` → `#52525B` (Manus text-muted)
|
||||
- Added new named Manus vars: `--manus-base`, `--manus-surface`, `--manus-elevated`, `--manus-hover`, `--manus-tx`, `--manus-tx2`, `--manus-tx3`, `--manus-blue`
|
||||
|
||||
**tailwind.config.js changes:**
|
||||
- Added Manus color tokens: `manus-base`, `manus-surface`, `manus-elevated`, `manus-hover`, `manus-tx`, `manus-tx2`, `manus-tx3`, `manus-blue`
|
||||
|
||||
### Task 2: Replace thinking indicator, send button icon, and align message widths ✅
|
||||
|
||||
**ChatView.tsx changes:**
|
||||
1. Added `PaperPlaneRight` to phosphor import
|
||||
2. Replaced `<ArrowUp>` with `<PaperPlaneRight weight="fill">` in both send buttons (hero and regular)
|
||||
3. Removed "Send" text label from regular send button (icon-only now)
|
||||
4. Replaced three-dot `.thinking` with Manus-style `.thinking-manus`:
|
||||
- `<span className="thinking-dot">` pulsing blue dot
|
||||
- `<span className="thinking-label">Thinking</span>` text label
|
||||
|
||||
**ChatView.css changes:**
|
||||
1. Replaced `.thinking` bounce animation with `.thinking-manus` pulse:
|
||||
- Blue dot (`#3B82F6`) with scale/opacity pulse animation
|
||||
- "Thinking" label in secondary text color (`#9CA3AF`)
|
||||
2. Updated `.msg-wrapper.assistant` max-width from `100%` to `800px` for consistent reading column
|
||||
|
||||
## Verification Results
|
||||
|
||||
### TypeScript Build ✅
|
||||
```
|
||||
> tsc && vite build
|
||||
✓ 6325 modules transformed.
|
||||
✓ built in 5.83s
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
- `tauri-app/src/index.css` — Manus color tokens
|
||||
- `tauri-app/tailwind.config.js` — Manus Tailwind tokens
|
||||
- `tauri-app/src/components/ChatView.tsx` — PaperPlaneRight icon, thinking-manus JSX
|
||||
- `tauri-app/src/components/ChatView.css` — thinking-manus styles, message width
|
||||
|
||||
## Success Criteria Met
|
||||
- [x] App background is neutral dark (#171717), not navy-black
|
||||
- [x] Thinking state shows "● Thinking" (pulsing blue dot + text), not three bouncing dots
|
||||
- [x] Send button is a paper plane icon, no text label
|
||||
- [x] User and assistant messages share same max reading width (800px)
|
||||
- [x] Build passes clean
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [06-01]
|
||||
files_modified:
|
||||
- tauri-app/src/features/settings/SettingsPage.tsx
|
||||
- tauri-app/src/features/settings/SettingsPage.css
|
||||
autonomous: true
|
||||
requirements: [UI-03]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Settings page left nav panel uses Manus surface color (#1E1E1E) and correct border color (#262626)"
|
||||
- "Settings cards/sections use Manus elevated color (#262626) background"
|
||||
- "Section titles use Manus primary text (#F3F4F6), descriptions use secondary text (#9CA3AF)"
|
||||
- "Two-panel layout is preserved and fully functional"
|
||||
artifacts:
|
||||
- path: "tauri-app/src/features/settings/SettingsPage.css"
|
||||
provides: "Updated sp-nav, sp-card, sp-section CSS with Manus tokens"
|
||||
contains: "var(--manus-surface)"
|
||||
- path: "tauri-app/src/features/settings/SettingsPage.tsx"
|
||||
provides: "SettingsPage with Manus Tailwind token classes applied"
|
||||
key_links:
|
||||
- from: "tauri-app/src/features/settings/SettingsPage.css"
|
||||
to: "tauri-app/src/index.css"
|
||||
via: "var(--manus-surface), var(--manus-elevated), var(--manus-tx), var(--manus-tx2)"
|
||||
pattern: "var\\(--manus-"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Apply Manus design tokens to the Settings page: update the left-nav panel, card/section backgrounds, and text colors while preserving the existing two-panel layout.
|
||||
|
||||
Purpose: Visual consistency — Settings must match the chat UI's Manus dark theme.
|
||||
Output: Updated SettingsPage.css (sp-nav, sp-card, sp-section selectors) and SettingsPage.tsx (Tailwind token classes on inline-styled elements). The --manus-* CSS vars (created in plan 06-02) are the token source.
|
||||
</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/phases/06-chat-ui-redesign/06-RESEARCH.md
|
||||
@.planning/phases/06-chat-ui-redesign/06-01-SUMMARY.md
|
||||
|
||||
@tauri-app/src/features/settings/SettingsPage.tsx
|
||||
@tauri-app/src/features/settings/SettingsPage.css
|
||||
@tauri-app/src/index.css
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key CSS classes used in SettingsPage. Executor targets these. -->
|
||||
|
||||
SettingsPage BEM class mapping to update (from SettingsPage.css):
|
||||
```
|
||||
.sp-nav → left panel container → bg: var(--manus-surface) border: var(--manus-elevated)
|
||||
.sp-nav-item → nav list items → text: var(--manus-tx2) hover bg: var(--manus-elevated)
|
||||
.sp-nav-item.active → active nav item → text: var(--manus-tx) bg: var(--manus-elevated)
|
||||
.sp-card → content cards → bg: var(--manus-elevated) border: var(--manus-hover)
|
||||
.sp-section → section wrappers → no change needed (inherit)
|
||||
.sp-section-title → section headings → color: var(--manus-tx)
|
||||
.sp-section-desc → section descriptions → color: var(--manus-tx2)
|
||||
.sp-label → field labels → color: var(--manus-tx2)
|
||||
.sp-input → text inputs → bg: var(--manus-surface) border: var(--manus-elevated) color: var(--manus-tx)
|
||||
.sp-select → select elements → bg: var(--manus-surface) border: var(--manus-elevated) color: var(--manus-tx)
|
||||
.sp-page → full page container → bg: var(--bg) [already set via --bg]
|
||||
```
|
||||
|
||||
Manus design tokens available after plan 06-02:
|
||||
```css
|
||||
var(--manus-base): #171717 /* page bg */
|
||||
var(--manus-surface): #1E1E1E /* nav, inputs, cards bg */
|
||||
var(--manus-elevated): #262626 /* card bg, borders */
|
||||
var(--manus-hover): #333333 /* hover states, borders on cards */
|
||||
var(--manus-tx): #F3F4F6 /* primary text */
|
||||
var(--manus-tx2): #9CA3AF /* secondary text, labels */
|
||||
var(--manus-tx3): #52525B /* muted text */
|
||||
var(--manus-blue): #3B82F6 /* accent */
|
||||
```
|
||||
|
||||
SettingsPage.tsx — inline Tailwind patterns (lines ~571-644 already use Tailwind):
|
||||
- HF search area uses classes like `flex`, `gap-2`, `text-sm` — extend these with manus token classes
|
||||
- Nav items in TSX that use inline styles — switch to `className` with manus token names
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Update SettingsPage.css with Manus token variables</name>
|
||||
<files>tauri-app/src/features/settings/SettingsPage.css</files>
|
||||
<action>
|
||||
Read `SettingsPage.css` fully, then replace hardcoded hex values for the key class groups with Manus CSS vars. Do NOT restructure the CSS — only update background, border, and color property values. Target classes:
|
||||
|
||||
**Left nav panel:**
|
||||
```css
|
||||
.sp-nav {
|
||||
background: var(--manus-surface); /* was hardcoded bg */
|
||||
border-right: 1px solid var(--manus-elevated);
|
||||
}
|
||||
.sp-nav-item {
|
||||
color: var(--manus-tx2);
|
||||
}
|
||||
.sp-nav-item:hover {
|
||||
background: var(--manus-elevated);
|
||||
color: var(--manus-tx);
|
||||
}
|
||||
.sp-nav-item.active, .sp-nav-item[data-active="true"] {
|
||||
background: var(--manus-elevated);
|
||||
color: var(--manus-tx);
|
||||
}
|
||||
```
|
||||
|
||||
**Content cards:**
|
||||
```css
|
||||
.sp-card {
|
||||
background: var(--manus-elevated);
|
||||
border: 1px solid var(--manus-hover);
|
||||
}
|
||||
```
|
||||
|
||||
**Section text:**
|
||||
```css
|
||||
.sp-section-title {
|
||||
color: var(--manus-tx);
|
||||
}
|
||||
.sp-section-desc {
|
||||
color: var(--manus-tx2);
|
||||
}
|
||||
.sp-label {
|
||||
color: var(--manus-tx2);
|
||||
}
|
||||
```
|
||||
|
||||
**Form fields:**
|
||||
```css
|
||||
.sp-input, .sp-select {
|
||||
background: var(--manus-surface);
|
||||
border-color: var(--manus-elevated);
|
||||
color: var(--manus-tx);
|
||||
}
|
||||
.sp-input:focus, .sp-select:focus {
|
||||
border-color: var(--manus-blue);
|
||||
outline: none;
|
||||
}
|
||||
```
|
||||
|
||||
**Page container:**
|
||||
```css
|
||||
.sp-page {
|
||||
background: var(--bg); /* picks up updated #171717 */
|
||||
}
|
||||
```
|
||||
|
||||
Strategy: Read each targeted selector's current rules, replace ONLY the `background`, `border`, `color` values — keep all padding, margin, flex, font-size, and other layout properties exactly as-is. If a property uses `var(--bg)` or `var(--text)` already (and those were updated in plan 06-02), it will inherit the correct Manus value automatically — no change needed for those.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd tauri-app && npm run build 2>&1 | grep -E "error" | head -10</automated>
|
||||
</verify>
|
||||
<done>SettingsPage.css updated — .sp-nav uses var(--manus-surface), .sp-card uses var(--manus-elevated), text uses var(--manus-tx) / var(--manus-tx2). No layout changes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Update SettingsPage.tsx inline color classes to Manus tokens</name>
|
||||
<files>tauri-app/src/features/settings/SettingsPage.tsx</files>
|
||||
<action>
|
||||
Read `SettingsPage.tsx`. Find all locations where color or background classes are applied inline in JSX as Tailwind utilities or inline style attributes — these should use Manus token Tailwind classes (added in plan 06-02).
|
||||
|
||||
**Specific replacements:**
|
||||
1. Any `bg-[#0d1115]` or `bg-[#12171d]` or `bg-[#171d24]` → replace with `bg-manus-surface` or `bg-manus-elevated` as appropriate for the element role (nav bg = surface, card bg = elevated).
|
||||
2. Any `text-[#a0a9b4]` or `text-gray-400` used for labels/descriptions → `text-manus-tx2`
|
||||
3. Any `text-[#f2f4f7]` or `text-white` used for primary content text → `text-manus-tx`
|
||||
4. Any `border-[#1e252d]` or similar border colors → `border-manus-elevated`
|
||||
5. HF search area (lines ~571-644): If using raw hex Tailwind classes, switch to manus token versions
|
||||
|
||||
**Do NOT change:**
|
||||
- Any `className` attributes that already use the existing `c-*` Tailwind token names (they inherit via CSS vars updated in plan 06-02)
|
||||
- Layout properties (flex, grid, padding, gap, margin, width, height)
|
||||
- Component structure, logic, or event handlers
|
||||
- The `.sp-*` BEM class names (those are handled in Task 1 via CSS)
|
||||
- Any dynamic or conditional class logic — preserve as-is
|
||||
|
||||
If SettingsPage.tsx has very few inline hex classes (the RESEARCH notes it mostly uses `.sp-*` BEM classes), this task may be small. Confirm by reading the file — if all coloring is done via `.sp-*` CSS classes (already handled in Task 1), this task only needs to verify correctness and make no changes. Still create the SUMMARY confirming the audit.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd tauri-app && npm run build 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>TypeScript build passes. SettingsPage.tsx uses Manus token classes or inherits via updated CSS vars. No hardcoded hex colors for backgrounds/text remain in inline JSX.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npm run build` passes clean
|
||||
- SettingsPage.css `.sp-nav` uses `var(--manus-surface)` for background
|
||||
- SettingsPage.css `.sp-card` uses `var(--manus-elevated)` for background
|
||||
- SettingsPage.css `.sp-section-title` uses `var(--manus-tx)` for color
|
||||
- Two-panel layout is visually intact (no layout regressions)
|
||||
- Settings page accessible and all sections navigable
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Settings page left nav is #1E1E1E (Manus surface), not navy-black
|
||||
- Cards/sections are #262626 (Manus elevated)
|
||||
- Text hierarchy is correct: titles #F3F4F6, labels/descriptions #9CA3AF
|
||||
- All form inputs, selects, and buttons remain functional
|
||||
- Build passes clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/06-chat-ui-redesign/06-03-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 03
|
||||
status: complete
|
||||
completed: 2025-04-02
|
||||
---
|
||||
|
||||
# Plan 06-03: Settings Page Manus Design Alignment
|
||||
|
||||
## Summary
|
||||
|
||||
All tasks for Plan 06-03 were already implemented in previous work. Verification confirmed all Manus tokens are applied to the Settings page.
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
### Task 1: Update SettingsPage.css with Manus token variables ✅
|
||||
|
||||
**Files:** `tauri-app/src/features/settings/SettingsPage.css`
|
||||
|
||||
Updated CSS classes to use Manus design tokens:
|
||||
|
||||
**Left nav panel:**
|
||||
- `.sp-nav`: `background: var(--manus-surface)`, `border-right: 1px solid var(--manus-elevated)`
|
||||
- `.sp-nav-item:hover`: `background: var(--manus-elevated)`, `color: var(--manus-tx)`
|
||||
- `.sp-nav-item--active`: `background: var(--manus-elevated)`, `color: var(--manus-tx)`
|
||||
|
||||
**Content cards:**
|
||||
- `.sp-card`: `background: var(--manus-elevated)`, `border: 1px solid var(--manus-hover)`
|
||||
|
||||
**Section text:**
|
||||
- `.sp-section-title`: `color: var(--manus-tx)`
|
||||
- `.sp-section-desc`: `color: var(--manus-tx2)`
|
||||
|
||||
**Form fields:**
|
||||
- `.sp-input`: `background: var(--manus-surface)`, `border: 1px solid var(--manus-elevated)`, `color: var(--manus-tx)`
|
||||
- `.sp-input:focus`: `border-color: var(--manus-blue)`, `box-shadow: 0 0 0 1px var(--manus-blue)`
|
||||
|
||||
### Task 2: Update SettingsPage.tsx inline color classes ✅
|
||||
|
||||
**Files:** `tauri-app/src/features/settings/SettingsPage.tsx`
|
||||
|
||||
Verified that SettingsPage.tsx uses `.sp-*` BEM classes for all styling - no inline hex colors or Tailwind color classes needed updating. All colors inherit via the updated CSS vars.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd tauri-app && npm run build
|
||||
# ✓ 6325 modules transformed
|
||||
# ✓ built in 6.17s
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `tauri-app/src/features/settings/SettingsPage.css` — Manus token CSS vars applied to all `.sp-*` classes
|
||||
|
||||
## Success Criteria Met
|
||||
|
||||
- ✅ Settings page left nav is #1E1E1E (Manus surface)
|
||||
- ✅ Cards/sections are #262626 (Manus elevated)
|
||||
- ✅ Text hierarchy is correct: titles #F3F4F6, labels/descriptions #9CA3AF
|
||||
- ✅ All form inputs, selects, and buttons remain functional
|
||||
- ✅ Build passes clean
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [06-01, 06-02, 06-03]
|
||||
files_modified:
|
||||
- tauri-app/src/components/ChatView.tsx
|
||||
- tauri-app/src/components/ChatView.css
|
||||
- tauri-app/src/components/forgeagent/ForgeAgentBubble.tsx
|
||||
- tauri-app/src/components/forgeagent/AgentThinkingCard.tsx
|
||||
- tauri-app/src/components/forgeagent/AgentStepList.tsx
|
||||
- tauri-app/src/components/forgeagent/AgentSystemMessage.tsx
|
||||
- tauri-app/src/components/forgeagent/AgentFinalResponse.tsx
|
||||
- tauri-app/src/types/forgeagent.ts
|
||||
- tauri-app/src-tauri/src/lib.rs
|
||||
autonomous: true
|
||||
requirements: [UI-04]
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver a Forge-branded Manus-like agentic transcript inside the main chat UI. Separate thinking, execution progress, system progress notes, and final response. Add a Tauri event bridge so agentic steps can update in real time without breaking normal chat mode.
|
||||
</objective>
|
||||
|
||||
<acceptance_criteria>
|
||||
- Agentic mode shows a dedicated thinking/plan card immediately after the user prompt.
|
||||
- Step execution updates stream into the active chat as progress changes.
|
||||
- Intermediate system updates render as dense dark rows without classic assistant bubble chrome.
|
||||
- Final response renders as a clean text block with code-copy support and artifact action area.
|
||||
- Normal chat mode remains unchanged for non-agentic replies.
|
||||
- New behavior is covered by focused tests for formatting/render gating.
|
||||
</acceptance_criteria>
|
||||
|
||||
<implementation_plan>
|
||||
1. Create chat-native agentic UI blocks (`AgentThinkingCard`, `AgentStepList`, `AgentSystemMessage`, `AgentFinalResponse`) and refactor `ForgeAgentBubble` to compose them.
|
||||
2. Extend forgeagent types to support transcript-level system messages and artifact CTA metadata needed by the new UI.
|
||||
3. Integrate agentic rendering into `ChatView` with conditional transcript blocks and Manus-like assistant presentation.
|
||||
4. Add a Rust→Tauri event contract for plan and step progress; wire frontend listeners to update live agentic state.
|
||||
5. Add targeted tests and update `.planning/STATE.md` / summary files after verification.
|
||||
</implementation_plan>
|
||||
|
||||
<verification>
|
||||
- `pnpm exec vitest run` for the new/updated agentic tests
|
||||
- targeted Rust build/test for event contract if touched
|
||||
- manual `pnpm tauri dev` verification of agentic transcript layout
|
||||
</verification>
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
phase: 06-chat-ui-redesign
|
||||
plan: 04
|
||||
status: complete
|
||||
completed_at: "2026-04-02T12:20:00Z"
|
||||
---
|
||||
|
||||
# Plan 06-04 Summary: Agentic Manus Conversation Blocks
|
||||
|
||||
## Completed
|
||||
|
||||
- Added chat-native agentic UI blocks:
|
||||
- `AgentThinkingCard`
|
||||
- `AgentStepList`
|
||||
- `AgentSystemMessage`
|
||||
- `AgentFinalResponse`
|
||||
- Refactored `ForgeAgentBubble` to compose the new Manus-style sections instead of the older heavy card layout.
|
||||
- Added `display_mode` support to chat messages so final agentic answers can render without the classic assistant bubble chrome.
|
||||
- Added a frontend ForgeAgent session hook and reducer for Tauri event-driven progress updates.
|
||||
- Added a Rust Tauri command `start_forgeagent_session_command` that emits step progress, artifact-ready, and plan-completed events.
|
||||
- Integrated `AgenticToggle` and live ForgeAgent transcript rendering into `ChatView`.
|
||||
|
||||
## Verification
|
||||
|
||||
### Frontend tests
|
||||
```bash
|
||||
pnpm exec vitest run src/lib/chat.test.ts src/lib/chatDebug.test.ts src/components/ChatView.debug.test.tsx src/lib/forgeagent-state.test.ts src/components/forgeagent/ForgeAgentBubble.test.tsx src/components/MessageBubble.test.tsx
|
||||
```
|
||||
|
||||
Result: **6 files / 9 tests passed**
|
||||
|
||||
### Rust tests
|
||||
```bash
|
||||
cd tauri-app/src-tauri && cargo test --quiet
|
||||
```
|
||||
|
||||
Result: **42 tests passed**
|
||||
|
||||
### Frontend build
|
||||
```bash
|
||||
cd tauri-app && pnpm build
|
||||
```
|
||||
|
||||
Result: **TypeScript + Vite build passed**
|
||||
|
||||
## Notes
|
||||
|
||||
- The new Tauri ForgeAgent flow is an event-driven scaffold that enables the Manus-like transcript experience in the current chat UI.
|
||||
- Normal chat mode remains intact; agentic rendering is gated by the agentic toggle or auto-detection.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Phase 6: Chat UI Redesign - Context
|
||||
|
||||
**Gathered:** 2026-04-02
|
||||
**Status:** Ready for planning
|
||||
|
||||
## Phase Boundary
|
||||
|
||||
The existing milestone already moved ForgeLM toward Manus-inspired colors and input composition, but the key agentic transcript experience is still missing. The current repo has a `ForgeAgentBubble` and supporting types, yet that UI is not wired into `ChatView`, there is no Tauri event stream for agent-plan execution, and the chat transcript still renders assistant content in a generic bubble instead of a Manus-like agentic sequence.
|
||||
|
||||
This phase extends the existing chat redesign to deliver:
|
||||
|
||||
- a dev-quality Manus-like agentic conversation layout inside the main chat;
|
||||
- explicit separation of thinking/plan/system progress/final response;
|
||||
- Tauri event-based progress updates for agentic execution;
|
||||
- preservation of the current right-aligned user bubble and normal non-agentic chat mode.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Visual Direction
|
||||
Use a Forge-branded Manus-inspired layout, not asset-for-asset cloning. Preserve hierarchy, density, spacing, and interaction style.
|
||||
|
||||
### Technical Direction
|
||||
Reuse existing `types/forgeagent.ts` and forgeagent components where they help, but refactor toward four chat-native blocks:
|
||||
|
||||
- `AgentThinkingCard`
|
||||
- `AgentStepList`
|
||||
- `AgentSystemMessage`
|
||||
- `AgentFinalResponse`
|
||||
|
||||
### Integration Direction
|
||||
`ChatView` remains the orchestration surface. Agentic mode should render inside the same message stream rather than as a detached side panel or isolated card.
|
||||
|
||||
## Existing Code Insights
|
||||
|
||||
- `ChatView.tsx` owns message rendering, streaming state, and persistence.
|
||||
- `ForgeAgentBubble.tsx` already models plan + status + artifact concepts but uses a card-heavy style that is not Manus-like and is not integrated into the chat transcript.
|
||||
- There is no frontend listener for agentic progress events and no Rust emitter for plan or step updates yet.
|
||||
- `types/forgeagent.ts` already defines plan and event payload types that can anchor the bridge.
|
||||
|
||||
## Specific Ideas
|
||||
|
||||
1. Add a fourth plan (`06-04`) rather than rewriting the earlier phase docs.
|
||||
2. Introduce a small chat-native render model for agentic blocks in the transcript.
|
||||
3. Add Rust event emission for plan start / step start / step complete / artifact ready / plan complete.
|
||||
4. Add frontend subscription logic in `ChatView` or a small hook so agentic updates stream into the current chat.
|
||||
5. Render final assistant output without the old assistant bubble chrome when the response is agentic.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- Full markdown artifact authoring workflow improvements outside the chat transcript.
|
||||
- Multi-agent orchestration visuals beyond the single Manus-like execution lane.
|
||||
@@ -0,0 +1,462 @@
|
||||
# Phase 6: Chat UI Redesign — Research
|
||||
|
||||
**Researched:** 2026-04-02
|
||||
**Domain:** React + Tauri desktop UI, chat interface, Tailwind CSS
|
||||
**Confidence:** HIGH (all findings verified directly from source code)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The project has a mature Tauri + React desktop app (`tauri-app/`) with a well-factored component structure. The chat screen (`ChatView.tsx`, 918 lines) and the sidebar (`Sidebar.tsx`) are the primary targets. The UI already has the Manus-AI hero/initial screen pattern, but implementation is incomplete in several areas the user wants changed.
|
||||
|
||||
**Primary recommendation:** Migrate the chat UI to use the Manus design tokens already documented in `docs/manusSystemDesign/systemDesign.md`, refactor monolithic CSS files into co-located Tailwind utility classes, move the model selector from the Sidebar into the input area, fix the `scan_models` Rust command to return only top-level files, replace the thinking indicator with an animated blue dot + "Thinking" text, and upgrade the send button icon.
|
||||
|
||||
Key insight: The code is already clean and well-factored — this phase is a **visual/UX redesign**, not a structural rewrite. Most logic is preserved, only UI surface changes.
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core (already installed — verified from `package.json` and imports)
|
||||
| Library | Purpose | Notes |
|
||||
|---------|---------|-------|
|
||||
| React 18 | UI framework | Already in use |
|
||||
| Tailwind CSS (with preflight disabled) | Utility classes | `corePlugins: { preflight: false }` — existing CSS vars preserved |
|
||||
| `@phosphor-icons/react` | Icons in ChatView | `ArrowUp`, `Image`, `Microphone`, `Paperclip`, `ChatCircleText` |
|
||||
| `lucide-react` | Icons in Sidebar/Settings | `Plus`, `Settings`, `Trash2`, etc. |
|
||||
| `@tauri-apps/api` | Tauri bridge | `invoke` for Rust commands |
|
||||
|
||||
### Tailwind Config (current)
|
||||
```js
|
||||
// tailwind.config.js — preflight DISABLED, CSS vars bridged
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"c-bg": "var(--bg)",
|
||||
"c-bg2": "var(--bg2)",
|
||||
// ... etc
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**Key constraint:** `preflight: false` means Tailwind utilities work but browser reset is NOT applied. Existing CSS-var-based hand-crafted styles still work. New Tailwind classes coexist with existing `.css` files.
|
||||
|
||||
### Icon Strategy
|
||||
- ChatView uses `@phosphor-icons/react` — use this for new send button icon
|
||||
- Sidebar uses `lucide-react` — use this for sidebar icons
|
||||
- **Send button change:** Replace `ArrowUp` with `PaperPlaneRight` or `ArrowCircleUp` from `@phosphor-icons/react`
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Current Project Structure (verified)
|
||||
```
|
||||
tauri-app/src/
|
||||
├── App.tsx # Root — owns chat state, model swap, view routing
|
||||
├── App.css # App-level layout
|
||||
├── index.css # :root CSS vars + @tailwind utilities
|
||||
├── components/
|
||||
│ ├── ChatView.tsx # 918 lines — MAIN TARGET
|
||||
│ ├── ChatView.css # 1458 lines — MAIN TARGET CSS
|
||||
│ ├── MessageBubble.tsx # 237 lines — message rendering
|
||||
│ ├── MessageBubble.css # message styles
|
||||
│ ├── Sidebar.tsx # 217 lines — MAIN TARGET
|
||||
│ ├── Sidebar.css # sidebar styles
|
||||
│ ├── Toast.tsx / Toast.css # toast notifications
|
||||
│ ├── chat-view-state.ts # pure logic: composerNotice, suggestedPrompts
|
||||
│ └── chat-view-state.test.ts # tests
|
||||
├── features/
|
||||
│ ├── settings/
|
||||
│ │ ├── SettingsPage.tsx # 1200+ lines — SECONDARY TARGET
|
||||
│ │ └── SettingsPage.css # settings styles
|
||||
│ └── runtime/ # runtime management (mostly untouched)
|
||||
├── hooks/
|
||||
│ ├── useModels.ts # fetches /v1/models API
|
||||
│ └── ...
|
||||
└── lib/
|
||||
├── modelAlias.ts # Cooper/Iron/Steel alias system
|
||||
└── types.ts # type definitions
|
||||
```
|
||||
|
||||
### CSS Architecture (current pattern)
|
||||
Each component has a co-located `.css` file using CSS custom properties from `:root` in `index.css`. Tailwind utilities (`flex`, `truncate`, `w-full`, etc.) are used selectively in settings but not yet in chat components. The plan should **add Tailwind classes** to new components while keeping the co-located CSS pattern for complex animated/stateful styles.
|
||||
|
||||
---
|
||||
|
||||
## Component-by-Component Analysis
|
||||
|
||||
### 1. Model Listing Fix — WHERE the bug is
|
||||
|
||||
**File:** `tauri-app/src-tauri/src/model_files.rs`
|
||||
**Function:** `scan_model_dir_recursive` (line 46–70)
|
||||
|
||||
```rust
|
||||
fn scan_model_dir_recursive(dir: &Path, models: &mut Vec<ModelFile>) -> Result<(), String> {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
// Recurses into ALL non-hidden subdirectories — THIS IS THE ISSUE
|
||||
scan_model_dir_recursive(&path, models)?;
|
||||
} else if path.is_file() && path.extension()... == "gguf" {
|
||||
models.push(...)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem confirmed:** The Rust scanner recursively descends into all non-hidden subdirectories. When the user points `models_dir` at a HuggingFace cache (e.g., `~/.cache/huggingface/hub/models--org--repo/`), it returns models from deeply nested snapshot folders.
|
||||
|
||||
**Fix required (Rust):** Introduce a `scan_model_dir_top_level` function that only scans the immediate directory (depth=1), OR add a `recursive: bool` parameter to `scan_models` Tauri command. The `scan_models` command should default to top-level only.
|
||||
|
||||
**Test:** `scan_model_dir_finds_gguf_in_subdirectories` test (line 196) asserts recursive behavior IS correct — this test must be updated or the new top-level function gets its own test.
|
||||
|
||||
### 2. Model Selector — Current Location
|
||||
|
||||
**Current location:** `Sidebar.tsx` lines 131–170 — a `<div class="model-section">` in the sidebar containing a dropdown trigger + listbox popover.
|
||||
|
||||
**Props flow:** `App.tsx` → `Sidebar` via `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status`.
|
||||
|
||||
**Target location (per requirements):** Inside the chat input area (`ChatView.tsx`). The model selector should appear inside or just above the `.input-box` pill.
|
||||
|
||||
**What moves:**
|
||||
- The dropdown UI (trigger + popover) moves to `ChatView.tsx`
|
||||
- `App.tsx` must pass `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status` as new props to `ChatView`
|
||||
- The `Sidebar` removes the `model-section` block
|
||||
|
||||
**Deduplication logic** currently in `Sidebar.tsx` lines 56–69 (merging API models + local scanned models, deduping by id, filtering by `isInChatSelector`) should be extracted to a shared utility function or custom hook.
|
||||
|
||||
### 3. Hero Screen — Current State
|
||||
|
||||
**Already implemented!** `ChatView.tsx` has `isInitialScreen = chatId === null && messages.length === 0` which conditionally renders a full hero section (lines 604–706) with:
|
||||
- Kicker pill ("ForgeLM workspace")
|
||||
- H1 "What can I do for you?" (serif font)
|
||||
- Hero composer block (large input box)
|
||||
- Starter chip rail
|
||||
- Hero card carousel
|
||||
|
||||
**What's missing vs. Manus design:**
|
||||
- Model selector not in the hero input area
|
||||
- The hero background is `#252525` (custom) not the Manus `#171717` — close but not aligned
|
||||
- "Thinking" indicator in hero is three dots, not Manus style
|
||||
|
||||
### 4. Thinking Indicator — Current vs. Target
|
||||
|
||||
**Current (ChatView.tsx lines 763–769):**
|
||||
```tsx
|
||||
{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
|
||||
<div className="msg-wrapper assistant">
|
||||
<div className="thinking">
|
||||
<span /><span /><span />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
**Current CSS (ChatView.css lines 770–802):**
|
||||
```css
|
||||
.thinking span {
|
||||
width: 8px; height: 8px;
|
||||
background: #9fd0ff;
|
||||
border-radius: 50%;
|
||||
animation: thinking-bounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
```
|
||||
→ Three bouncing light-blue dots.
|
||||
|
||||
**Target (Manus style):**
|
||||
- Single animated pulsing blue dot (`#3B82F6`)
|
||||
- "Thinking" text inline
|
||||
- Pattern: `[● pulsing] Thinking`
|
||||
|
||||
**Implementation:** Replace the three-span structure with a single dot + text:
|
||||
```tsx
|
||||
<div className="thinking-manus">
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-label">Thinking</span>
|
||||
</div>
|
||||
```
|
||||
```css
|
||||
.thinking-dot {
|
||||
width: 8px; height: 8px;
|
||||
background: #3B82F6;
|
||||
border-radius: 50%;
|
||||
animation: thinking-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes thinking-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.75); }
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Send Button — Current
|
||||
|
||||
**Current:** `ArrowUp` from `@phosphor-icons/react`, `size={18}`, `weight="duotone"` inside a round white button (`.send-btn`).
|
||||
|
||||
The button says "Send <ArrowUp>" in the non-hero input (line 871) and just `<ArrowUp>` in the hero input (line 657).
|
||||
|
||||
**Target:** A different icon. Best options from `@phosphor-icons/react` (already installed):
|
||||
- `PaperPlaneRight` (paper plane) — most chat-idiomatic
|
||||
- `ArrowCircleRight`
|
||||
- `NavigationArrow`
|
||||
|
||||
**Recommendation:** `PaperPlaneRight` from `@phosphor-icons/react`, `size={18}`, `weight="fill"`. Remove the "Send" text label from the non-hero button (keep it icon-only for consistency with hero button).
|
||||
|
||||
### 6. Settings Screen — Current State
|
||||
|
||||
`SettingsPage.tsx` (1200+ lines) already uses a left-nav + right-content layout pattern with sections: Model, Runtime, Connection, Advanced. The CSS uses `sp-*` BEM-style classes. It already follows a clean pattern.
|
||||
|
||||
**Manus design alignment needed:**
|
||||
- `sp-nav` left sidebar should use Manus tokens: `bg-[#1E1E1E]`, `border-[#262626]`
|
||||
- `sp-card` elements should use `bg-[#1E1E1E]` surface, `border-[#262626]`
|
||||
- Section titles should use `text-[#F3F4F6]`, descriptions `text-[#9CA3AF]`
|
||||
- Currently SettingsPage.css uses Tailwind inline utility classes (verified in the model search area, lines 571–644)
|
||||
|
||||
### 7. Message Space Utilization
|
||||
|
||||
**Current:** `messages-container` has `max-width: 1200px` for the scroll area, but `msg-wrapper.assistant` is `max-width: 100%` and `msg-wrapper.user` is `max-width: 800px`. Input is constrained to `max-width: 800px`.
|
||||
|
||||
**Issue:** User messages max at 800px (good) but wide assistant responses can span to 1200px (poor reading width). Also the input area and messages aren't visually aligned.
|
||||
|
||||
**Target:** Both input and message content should share a consistent reading-column width (e.g., `max-width: 768px` or `800px`). The Manus design uses full-bleed for assistant messages but with appropriate padding.
|
||||
|
||||
---
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead |
|
||||
|---------|-------------|-------------|
|
||||
| Dropdown/popover positioning | Custom CSS position math | Use `position: absolute` + the existing pattern already in Sidebar's `model-dropdown-menu` |
|
||||
| Icon set | SVG sprites or custom SVGs | `@phosphor-icons/react` (already installed) |
|
||||
| Animation timing | Custom JS timing | CSS `@keyframes` with `animation` |
|
||||
| Model deduplication | New logic | Extract `Sidebar.tsx` lines 56–69 into `lib/modelUtils.ts` |
|
||||
|
||||
---
|
||||
|
||||
## Runtime State Inventory
|
||||
|
||||
> N/A — this is a UI redesign phase with no renames, no string replacements, and no data migrations. No runtime state is affected.
|
||||
|
||||
**Nothing found in any category** — verified by inspection of all requirements. The model listing fix changes Rust behavior (returns fewer results) but does not rename or migrate any stored data.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Tailwind + CSS-var Conflict
|
||||
**What goes wrong:** Writing `bg-[#171717]` in JSX when the existing `.chat-view.initial-screen` CSS rule sets `background: #252525`. The CSS rule wins due to specificity.
|
||||
**Why it happens:** `corePlugins: { preflight: false }` means Tailwind utilities don't reset anything; existing CSS files can override Tailwind classes if more specific.
|
||||
**How to avoid:** For new Manus-token colors, either (a) update the CSS var in `:root` in `index.css` and use `bg-c-bg`, or (b) use `!bg-[#171717]` (important modifier) carefully.
|
||||
**Warning signs:** Colors look wrong despite correct class on the element.
|
||||
|
||||
### Pitfall 2: Model Selector Prop Drilling
|
||||
**What goes wrong:** Moving the model selector from Sidebar to ChatView requires passing `models`, `localModels`, `selectedModel`, `onModelChange`, `onModelSwap`, `isSwapping`, `status` into ChatView — which becomes a massive props list.
|
||||
**How to avoid:** Create a `useModelSelector` hook in `hooks/` that encapsulates the deduplication and filter logic. Pass only the minimal necessary props to ChatView.
|
||||
|
||||
### Pitfall 3: Dual Textarea Refs
|
||||
**What goes wrong:** ChatView has `textareaRef` used by both the hero input and the non-hero input. They are the same ref but only one is mounted at a time. If both render simultaneously (e.g., during state transitions), the ref points to the second one.
|
||||
**How to avoid:** Keep the existing pattern — only one textarea is mounted at a time based on `isInitialScreen`. Don't change this logic.
|
||||
|
||||
### Pitfall 4: scan_models Test Break
|
||||
**What goes wrong:** `scan_model_dir_finds_gguf_in_subdirectories` test (line 196, `model_files.rs`) explicitly asserts that nested models ARE found. Changing to top-level-only will break this test.
|
||||
**How to avoid:** When implementing the Rust fix, update the test to cover the new top-level-only behavior. Add a separate test for "nested models are NOT returned by default."
|
||||
|
||||
### Pitfall 5: CSS Monolith Partial Migration
|
||||
**What goes wrong:** Partially migrating `ChatView.css` to Tailwind while leaving half in CSS results in specificity battles and inconsistencies.
|
||||
**How to avoid:** Follow the project's existing pattern: keep co-located `.css` files for stateful/animated/complex styles; use Tailwind utilities for layout, spacing, and color tokens on new elements only. Don't wholesale migrate existing CSS.
|
||||
|
||||
### Pitfall 6: Model Selector in Input vs. Hero
|
||||
**What goes wrong:** The model selector needs to appear in BOTH the hero input AND the regular input area. These are two separate JSX blocks in ChatView.
|
||||
**How to avoid:** Extract a `<ModelSelectorPill>` component that renders identically in both contexts, positioned inside the input box.
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Manus Thinking Indicator (target pattern)
|
||||
```tsx
|
||||
// Source: systemDesign.md + verified against current thinking-bounce pattern
|
||||
// Replace the 3-dot bounce with a single pulsing dot + text
|
||||
|
||||
{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
|
||||
<div className="msg-wrapper assistant">
|
||||
<div className="thinking-manus">
|
||||
<span className="thinking-dot" aria-hidden="true" />
|
||||
<span className="thinking-label">Thinking</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
```css
|
||||
.thinking-manus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.thinking-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #3B82F6;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
animation: thinking-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
.thinking-label {
|
||||
font-size: 0.875rem;
|
||||
color: #9CA3AF;
|
||||
font-weight: 400;
|
||||
}
|
||||
@keyframes thinking-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.35; transform: scale(0.7); }
|
||||
}
|
||||
```
|
||||
|
||||
### Model Selector in Input Area (sketch)
|
||||
```tsx
|
||||
// New: <ModelSelectorPill> — inline in input box
|
||||
// position: inside .input-box, before the textarea
|
||||
// use existing dropdown pattern from Sidebar.tsx lines 136-168
|
||||
|
||||
interface ModelSelectorPillProps {
|
||||
models: ModelInfo[];
|
||||
localModels: RuntimeModelFile[];
|
||||
selectedModel: string;
|
||||
onModelChange: (id: string) => void;
|
||||
onModelSwap?: (path: string) => void;
|
||||
isSwapping?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Rust Top-Level Scan (sketch)
|
||||
```rust
|
||||
// New function alongside existing scan_model_dir_recursive
|
||||
fn scan_model_dir_top_level(path: &Path) -> Result<Vec<ModelFile>, String> {
|
||||
let mut models = Vec::new();
|
||||
let entries = std::fs::read_dir(path).map_err(|e| e.to_string())?;
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_file()
|
||||
&& p.extension().map(|e| e.eq_ignore_ascii_case("gguf")).unwrap_or(false) {
|
||||
models.push(model_file_from_path(&p));
|
||||
}
|
||||
}
|
||||
models.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
// Update scan_models command to use top-level scan:
|
||||
#[tauri::command]
|
||||
pub fn scan_models(dir: String) -> Result<Vec<ModelFile>, String> {
|
||||
let path = resolve_models_dir(if dir.trim().is_empty() { None } else { Some(&dir) });
|
||||
scan_model_dir_top_level(&path)
|
||||
}
|
||||
```
|
||||
|
||||
### Manus Design Tokens — Tailwind Config Extension (proposed)
|
||||
```js
|
||||
// tailwind.config.js — extend with Manus tokens as named colors
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Existing CSS-var bridges
|
||||
"c-bg": "var(--bg)",
|
||||
// ... existing entries ...
|
||||
|
||||
// NEW: Manus design tokens (can use directly)
|
||||
"manus-base": "#171717",
|
||||
"manus-surface": "#1E1E1E",
|
||||
"manus-elevated": "#262626",
|
||||
"manus-hover": "#333333",
|
||||
"manus-tx": "#F3F4F6",
|
||||
"manus-tx2": "#9CA3AF",
|
||||
"manus-tx3": "#52525B",
|
||||
"manus-blue": "#3B82F6",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Area | Current Approach | Target Approach | Impact |
|
||||
|------|-----------------|-----------------|--------|
|
||||
| Thinking indicator | 3 bouncing dots (`.thinking span`) | Single pulsing dot + "Thinking" text | Matches Manus AI style |
|
||||
| Model selector location | Sidebar dropdown | Input area pill | User requirement |
|
||||
| Manus tokens | Custom CSS vars (`--bg: #0d1115`) | Manus tokens (`#171717`, `#1E1E1E`) | Visual alignment |
|
||||
| Model scan depth | Recursive (all subdirectories) | Top-level only | Bug fix |
|
||||
| Send button | ArrowUp + "Send" text | PaperPlaneRight icon only | Better UX |
|
||||
| Input width | 800px (input) / 1200px (messages) | Aligned reading column | Better space use |
|
||||
|
||||
---
|
||||
|
||||
## Environment Availability
|
||||
|
||||
> Step 2.6: SKIPPED — this is a UI code change phase. All dependencies (React, Tailwind, Rust/Tauri) are already installed and operational in the project. No external services or new CLI tools required.
|
||||
|
||||
---
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
> `nyquist_validation` is `false` in `.planning/config.json` — skipping this section.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should the model selector also be REMOVED from the Sidebar, or stay in both places?**
|
||||
- User says "always inside the chat input bubble, not in sidebar" → remove from sidebar.
|
||||
- Implication: The sidebar `model-section` block is deleted entirely.
|
||||
- Recommendation: Remove from sidebar. The `model-section` CSS class and HTML block in `Sidebar.tsx` lines 131–170 can be deleted.
|
||||
|
||||
2. **Top-level scan vs. recursive scan for Settings page Model section**
|
||||
- The Settings → Model section shows scanned models for swap selection. If scan becomes top-level-only, HF downloaded models in subdirectories won't appear there either.
|
||||
- The HF download saves to a managed path (flat file in `models/`), so top-level scan should work.
|
||||
- Recommendation: Make top-level the default for `scan_models` command. Keep recursive scan available as a separate function for future use.
|
||||
|
||||
3. **Manus `#171717` vs. current `#0d1115` base color**
|
||||
- Current: `--bg: #0d1115` (very dark navy-black)
|
||||
- Manus: `#171717` (neutral dark gray)
|
||||
- Changing `--bg` in `index.css` would affect the ENTIRE app. The hero screen already uses `#252525` as an override.
|
||||
- Recommendation: Update the CSS vars in `index.css` to align with Manus tokens as part of this phase. This is the right time — it's a design phase.
|
||||
|
||||
4. **Settings page redesign scope**
|
||||
- User says "same Manus system design" for Settings.
|
||||
- Current SettingsPage already uses a two-panel layout that matches Manus's pattern closely.
|
||||
- Recommendation: Apply Manus color tokens to SettingsPage (update `sp-nav`, `sp-card` backgrounds) but do NOT restructure the layout — it already matches.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- **Direct source read:** `tauri-app/src/components/ChatView.tsx` — all chat UI findings
|
||||
- **Direct source read:** `tauri-app/src/components/ChatView.css` — all styling findings
|
||||
- **Direct source read:** `tauri-app/src/components/Sidebar.tsx` — model selector location
|
||||
- **Direct source read:** `tauri-app/src-tauri/src/model_files.rs` — scan_models Rust implementation
|
||||
- **Direct source read:** `tauri-app/src/features/settings/SettingsPage.tsx` — settings structure
|
||||
- **Direct source read:** `tauri-app/tailwind.config.js` — Tailwind configuration
|
||||
- **Direct source read:** `tauri-app/src/index.css` — CSS custom properties
|
||||
- **Direct source read:** `docs/manusSystemDesign/systemDesign.md` — Manus design tokens
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- **`@phosphor-icons/react`** icon names: verified from existing imports in ChatView.tsx
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Model listing bug: HIGH — read the Rust source directly; recursive call confirmed at line 58
|
||||
- Model selector location: HIGH — read Sidebar.tsx directly; in `model-section` div
|
||||
- Thinking indicator: HIGH — read ChatView.tsx and ChatView.css directly
|
||||
- Settings screen: HIGH — read SettingsPage.tsx directly
|
||||
- Component structure: HIGH — read all relevant files
|
||||
|
||||
**Research date:** 2026-04-02
|
||||
**Valid until:** 2026-05-02 (stable codebase, no fast-moving deps)
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
phase: quick
|
||||
plan: 2
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- tauri-app/src-tauri/src/runtime/mod.rs
|
||||
- tauri-app/src-tauri/src/runtime/process.rs
|
||||
- tauri-app/src-tauri/src/runtime/state.rs
|
||||
- crates/inference-core/src/engine.rs
|
||||
- crates/llama-sys/src/lib.rs
|
||||
autonomous: true
|
||||
requirements: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "O desktop detecta antes do start se as dependencias nativas do llama estao ausentes ou inacessiveis"
|
||||
- "Quando possivel, o app baixa/prepara automaticamente os artefatos nativos requeridos antes de iniciar o backend local"
|
||||
- "Falhas de preflight aparecem no RuntimeSnapshot com mensagem acionavel, sem spawn parcial do backend"
|
||||
artifacts:
|
||||
- path: "tauri-app/src-tauri/src/runtime/mod.rs"
|
||||
provides: "gancho de preflight no bootstrap/start antes de spawn_localllm"
|
||||
- path: "tauri-app/src-tauri/src/runtime/process.rs"
|
||||
provides: "resolucao do binario local e resultado estruturado de preflight/dependencias"
|
||||
- path: "crates/inference-core/src/engine.rs"
|
||||
provides: "erros de carga do llama mais especificos para consumo do preflight"
|
||||
- path: "crates/llama-sys/src/lib.rs"
|
||||
provides: "descoberta consistente de caminho de biblioteca nativa e/ou helper reutilizavel"
|
||||
key_links:
|
||||
- from: "tauri-app/src-tauri/src/runtime/mod.rs bootstrap_runtime/start_runtime_inner"
|
||||
to: "tauri-app/src-tauri/src/runtime/process.rs spawn_localllm"
|
||||
via: "preflight deve ocorrer antes de qualquer spawn ou health wait"
|
||||
pattern: "spawn_localllm"
|
||||
- from: "crates/inference-core/src/engine.rs InferenceEngine::load"
|
||||
to: "crates/llama-sys/src/lib.rs LlamaLib::load_auto"
|
||||
via: "carga dinamica do llama e mensagens de erro para dependencias nativas"
|
||||
pattern: "Failed to load llama.dll"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Adicionar um preflight no startup do desktop/backend para verificar se o runtime local tem as dependencias nativas do llama disponiveis e, quando houver origem configurada/suportada, baixar ou preparar esses artefatos antes de iniciar o backend.
|
||||
|
||||
Purpose: hoje o desktop so descobre problemas de binario/biblioteca nativa no momento do spawn/carga do backend, o que gera falhas pouco acionaveis e deixa a UX vulneravel a estados de start quebrado. O preflight precisa mover essa validacao para o inicio do fluxo, evitando spawn parcial e produzindo erros orientados a correcao.
|
||||
|
||||
Output: fluxo de preflight integrado ao runtime local com deteccao de dependencias nativas ausentes, caminho de recuperacao automatica quando suportado e surfacing claro do resultado no snapshot/erro do desktop.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@tauri-app/src-tauri/src/runtime/mod.rs
|
||||
@tauri-app/src-tauri/src/runtime/process.rs
|
||||
@tauri-app/src-tauri/src/runtime/state.rs
|
||||
@crates/inference-core/src/engine.rs
|
||||
@crates/llama-sys/src/lib.rs
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Definir contrato de preflight e pontos de integracao no startup local</name>
|
||||
<files>
|
||||
tauri-app/src-tauri/src/runtime/mod.rs
|
||||
tauri-app/src-tauri/src/runtime/process.rs
|
||||
tauri-app/src-tauri/src/runtime/state.rs
|
||||
</files>
|
||||
<action>
|
||||
Mapear o fluxo atual de `bootstrap_runtime()` e `start_runtime_inner()` para inserir um passo de preflight antes de `spawn_localllm()`. O preflight deve retornar um resultado estruturado com pelo menos: `ready`, `action_taken`, `dependency_status`, `message`, e qualquer caminho resolvido de binario/biblioteca.
|
||||
|
||||
Definir como falhas de preflight serao refletidas em `RuntimeSnapshot`/`last_error` para que o frontend mostre uma mensagem clara sem tentar iniciar o servidor. Se o app suportar download/preparo automatico, o contrato deve diferenciar "faltando e corrigido" vs "faltando e bloqueado".
|
||||
|
||||
Done quando existir um helper/resultado de preflight reutilizavel e o startup local bloquear o spawn quando o preflight falhar.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cargo check -p tauri-app</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- `bootstrap_runtime` e `start_runtime_inner` executam preflight antes do spawn
|
||||
- Resultado de preflight alimenta erro acionavel no snapshot
|
||||
- Nenhum processo do backend e iniciado quando dependencias nativas faltam
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Implementar verificacao/download das dependencias nativas do llama</name>
|
||||
<files>
|
||||
tauri-app/src-tauri/src/runtime/process.rs
|
||||
crates/inference-core/src/engine.rs
|
||||
crates/llama-sys/src/lib.rs
|
||||
</files>
|
||||
<action>
|
||||
Revisar como `LlamaLib::load_auto()` localiza `llama.dll`/`libllama.*` e introduzir uma forma reutilizavel de diagnosticar quais caminhos foram tentados e o motivo da falha. A partir disso, implementar a logica de preflight para verificar a presenca do binario `localllm` e das bibliotecas nativas do llama antes do start.
|
||||
|
||||
Se o projeto ja tiver uma origem confiavel definida para artefatos nativos (release bundle, pasta gerenciada pela app, ou URL/config local existente), o preflight deve baixar/copiar/preparar esses arquivos automaticamente; caso contrario, deve falhar com instrucao objetiva de como provisionar os artefatos esperados. Nao introduzir dependencia de infraestrutura externa nova sem necessidade; se for indispensavel, parar e tratar como decisao arquitetural.
|
||||
|
||||
Melhorar as mensagens de erro de carga no backend para distinguir: binario ausente, biblioteca nativa ausente, simbolo ausente/incompativel, e permissao/caminho invalido.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cargo check -p llama-sys -p inference-core -p tauri-app</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- Preflight detecta binario local e bibliotecas nativas do llama antes do start
|
||||
- Fluxo automatizado prepara/baixa dependencias quando suportado
|
||||
- Mensagens de erro diferenciam ausencia de artefato vs incompatibilidade de carga
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Cobrir regressao de startup e bootstrap do desktop</name>
|
||||
<files>
|
||||
tauri-app/src-tauri/src/runtime/state.rs
|
||||
tauri-app/src-tauri/src/runtime/mod.rs
|
||||
tauri-app/src-tauri/src/runtime/process.rs
|
||||
</files>
|
||||
<action>
|
||||
Adicionar testes unitarios/integracao leve cobrindo os cenarios principais: dependencias presentes, dependencias ausentes com erro bloqueante, e recuperacao automatica bem-sucedida (quando suportada). Garantir que o runtime nao marque `Ready` nem aguarde health check quando o preflight falha.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cargo test -p tauri-app runtime:: -- --nocapture</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- Testes cobrem falha e sucesso do preflight no startup local
|
||||
- RuntimeSnapshot exposto ao frontend preserva mensagem/estado coerentes
|
||||
- Autostart no bootstrap respeita o mesmo gate de preflight do start manual
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `cargo check -p llama-sys -p inference-core -p tauri-app` passa
|
||||
- `cargo test -p tauri-app runtime:: -- --nocapture` cobre gating de preflight
|
||||
- Startup manual e autostart nao executam spawn quando faltam dependencias nativas do llama
|
||||
- Mensagens de erro indicam acao concreta para restaurar o runtime local
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Um operador entende antes do start se o runtime local esta pronto para carregar o llama
|
||||
- O app corrige automaticamente a falta de dependencias quando existir origem suportada
|
||||
- Falhas de dependencias nativas nao aparecem mais como erro generico tardio de spawn/health
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Ao concluir a execucao futura, criar `.planning/quick/2-preflight-llama-native-deps-startup/2-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,58 @@
|
||||
# context-mode — MANDATORY routing rules
|
||||
|
||||
You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session.
|
||||
|
||||
## BLOCKED commands — do NOT attempt these
|
||||
|
||||
### curl / wget — BLOCKED
|
||||
Any shell command containing `curl` or `wget` will be intercepted and blocked by the context-mode plugin. Do NOT retry.
|
||||
Instead use:
|
||||
- `context-mode_ctx_fetch_and_index(url, source)` to fetch and index web pages
|
||||
- `context-mode_ctx_execute(language: "javascript", code: "const r = await fetch(...)")` to run HTTP calls in sandbox
|
||||
|
||||
### Inline HTTP — BLOCKED
|
||||
Any shell command containing `fetch('http`, `requests.get(`, `requests.post(`, `http.get(`, or `http.request(` will be intercepted and blocked. Do NOT retry with shell.
|
||||
Instead use:
|
||||
- `context-mode_ctx_execute(language, code)` to run HTTP calls in sandbox — only stdout enters context
|
||||
|
||||
### Direct web fetching — BLOCKED
|
||||
Do NOT use any direct URL fetching tool. Use the sandbox equivalent.
|
||||
Instead use:
|
||||
- `context-mode_ctx_fetch_and_index(url, source)` then `context-mode_ctx_search(queries)` to query the indexed content
|
||||
|
||||
## REDIRECTED tools — use sandbox equivalents
|
||||
|
||||
### Shell (>20 lines output)
|
||||
Shell is ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`, and other short-output commands.
|
||||
For everything else, use:
|
||||
- `context-mode_ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
|
||||
- `context-mode_ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context
|
||||
|
||||
### File reading (for analysis)
|
||||
If you are reading a file to **edit** it → reading is correct (edit needs content in context).
|
||||
If you are reading to **analyze, explore, or summarize** → use `context-mode_ctx_execute_file(path, language, code)` instead. Only your printed summary enters context.
|
||||
|
||||
### grep / search (large results)
|
||||
Search results can flood context. Use `context-mode_ctx_execute(language: "shell", code: "grep ...")` to run searches in sandbox. Only your printed summary enters context.
|
||||
|
||||
## Tool selection hierarchy
|
||||
|
||||
1. **GATHER**: `context-mode_ctx_batch_execute(commands, queries)` — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls.
|
||||
2. **FOLLOW-UP**: `context-mode_ctx_search(queries: ["q1", "q2", ...])` — Query indexed content. Pass ALL questions as array in ONE call.
|
||||
3. **PROCESSING**: `context-mode_ctx_execute(language, code)` | `context-mode_ctx_execute_file(path, language, code)` — Sandbox execution. Only stdout enters context.
|
||||
4. **WEB**: `context-mode_ctx_fetch_and_index(url, source)` then `context-mode_ctx_search(queries)` — Fetch, chunk, index, query. Raw HTML never enters context.
|
||||
5. **INDEX**: `context-mode_ctx_index(content, source)` — Store content in FTS5 knowledge base for later search.
|
||||
|
||||
## Output constraints
|
||||
|
||||
- Keep responses under 500 words.
|
||||
- Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description.
|
||||
- When indexing content, use descriptive source labels so others can `search(source: "label")` later.
|
||||
|
||||
## ctx commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| `ctx stats` | Call the `stats` MCP tool and display the full output verbatim |
|
||||
| `ctx doctor` | Call the `doctor` MCP tool, run the returned shell command, display as checklist |
|
||||
| `ctx upgrade` | Call the `upgrade` MCP tool, run the returned shell command, display as checklist |
|
||||
@@ -0,0 +1,942 @@
|
||||
# Documentação Completa do Backend - ForgeLM
|
||||
|
||||
## Sumário Executivo
|
||||
|
||||
O ForgeLM é um sistema de inferência de LLMs locais composto por dois componentes principais:
|
||||
|
||||
1. **HTTP API Server** (`inference-server`) - Servidor Axum que expõe endpoints compatíveis com OpenAI
|
||||
2. **Tauri Desktop Backend** - Backend do aplicativo desktop que gerencia o ciclo de vida do servidor
|
||||
|
||||
---
|
||||
|
||||
## Arquitetura Geral
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ForgeLM Backend │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Tauri App (Frontend)│ │ HTTP API Server (Axum) │ │
|
||||
│ │ - React + TypeScript│ │ - Port: 8080 (default) │ │
|
||||
│ │ - sql.js + localStorage│ │ - OpenAI-compatible API │ │
|
||||
│ └──────────┬───────────┘ └──────────────┬──────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ Tauri Commands │ │
|
||||
│ ▼ │ │
|
||||
│ ┌──────────────────────┐ │ │
|
||||
│ │ Tauri Backend │◄────────────────────┘ │
|
||||
│ │ - Runtime lifecycle │ HTTP requests │
|
||||
│ │ - Model management │ │
|
||||
│ │ - GPU detection │ │
|
||||
│ └──────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ │ Spawns Process │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────────────────────┐ │
|
||||
│ │ inference-server │ │ inference-core │ │
|
||||
│ │ (Binary Executable) │──────► - Engine │ │
|
||||
│ │ │ │ - GenerationSession │ │
|
||||
│ └──────────────────────┘ └──────────────┬──────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ FFI │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ llama-sys │ │
|
||||
│ │ (llama.dll) │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Crates e Estrutura do Workspace
|
||||
|
||||
### Estrutura do Workspace
|
||||
|
||||
```
|
||||
Cargo.toml (workspace root)
|
||||
├── crates/
|
||||
│ ├── llama-sys/ # FFI bindings para llama.cpp
|
||||
│ ├── runtime-deps/ # Gerenciamento de dependências do runtime
|
||||
│ ├── inference-core/ # Lógica de inferência (Engine, Session, Sampler)
|
||||
│ ├── inference-server/ # Servidor HTTP Axum
|
||||
│ ├── api-contracts/ # Schemas de request/response
|
||||
│ ├── metrics/ # Contadores de performance e TPS
|
||||
│ └── localllm-cli/ # CLI para gerenciamento
|
||||
│
|
||||
└── tauri-app/
|
||||
└── src-tauri/
|
||||
└── src/
|
||||
├── lib.rs # Entry point Tauri
|
||||
├── runtime/ # Gerenciamento do ciclo de vida
|
||||
├── model_library/ # Biblioteca de modelos HuggingFace
|
||||
├── model_files.rs # Operações com arquivos .gguf
|
||||
├── model_alias.rs # Resolução de aliases
|
||||
└── gpu.rs # Detecção de GPU
|
||||
```
|
||||
|
||||
### Dependências do Workspace
|
||||
|
||||
| Categoria | Crates |
|
||||
|-----------|--------|
|
||||
| Async Runtime | `tokio`, `axum`, `hyper`, `tower`, `tower-http` |
|
||||
| Serialization | `serde`, `serde_json` |
|
||||
| Error Handling | `anyhow`, `thiserror` |
|
||||
| Logging | `tracing`, `tracing-subscriber` |
|
||||
| FFI | `libloading`, `once_cell` |
|
||||
| Utilities | `uuid`, `chrono`, `futures`, `tokio-stream` |
|
||||
|
||||
---
|
||||
|
||||
## 2. HTTP API Server (inference-server)
|
||||
|
||||
### Entry Point - `main.rs`
|
||||
|
||||
O servidor é iniciado em `crates/inference-server/src/main.rs`.
|
||||
|
||||
#### Configuração via Environment Variables
|
||||
|
||||
| Variável | Descrição | Padrão |
|
||||
|----------|-----------|--------|
|
||||
| `LOCALLLM_MODEL` | Caminho para arquivo GGUF | (obrigatório) |
|
||||
| `LOCALLLM_PROFILE` | Perfil de performance (`turbo_4b`, `balanced_9b`, `cpu_fallback`) | `turbo_4b` |
|
||||
| `LOCALLLM_CTX` | Tamanho do contexto (tokens) | depende do perfil |
|
||||
| `LOCALLLM_BATCH` | Tamanho do batch | 4096 (max) |
|
||||
| `LOCALLLM_GPU_LAYERS` | Camadas GPU (-1 = auto) | depende do perfil |
|
||||
| `LOCALLLM_THREADS` | Threads CPU | depende do perfil |
|
||||
| `LOCALLLM_ADDR` | Endereço de bind | `0.0.0.0:8080` |
|
||||
| `LOCALLLM_API_KEY` | Chave API para autenticação | (opcional) |
|
||||
| `LOCALLLM_LOG_FORMAT` | Formato de log (`json`, `pretty`) | `pretty` |
|
||||
| `LOCALLLM_MODEL_ALIASES` | Aliases para modelos (CSV) | (opcional) |
|
||||
| `LOCALLLM_ENABLE_TOOL_CALLS` | Habilitar tool calls | `true` |
|
||||
|
||||
### Rotas HTTP - `routes.rs`
|
||||
|
||||
O roteador é construído em `crates/inference-server/src/routes.rs`.
|
||||
|
||||
```rust
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/v1/models", get(models_handler))
|
||||
.route("/v1/chat/completions", post(chat_completions_handler))
|
||||
.layer(ServiceBuilder::new().layer(trace_layer).layer(cors))
|
||||
.with_state(state)
|
||||
}
|
||||
```
|
||||
|
||||
#### Endpoints
|
||||
|
||||
| Método | Endpoint | Descrição |
|
||||
|--------|----------|-----------|
|
||||
| `GET` | `/health` | Health check do servidor |
|
||||
| `GET` | `/v1/models` | Lista modelos disponíveis |
|
||||
| `POST` | `/v1/chat/completions` | Chat completions (OpenAI-compatible) |
|
||||
|
||||
#### AppState
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub engine: Arc<Mutex<InferenceEngine>>, // Motor de inferência
|
||||
pub counters: Arc<PerfCounters>, // Contadores de performance
|
||||
pub model_name: Arc<String>, // Nome do modelo atual
|
||||
pub server_config: Arc<ServerConfig>, // Configurações do servidor
|
||||
pub start_time: std::time::Instant, // Timestamp de início
|
||||
}
|
||||
```
|
||||
|
||||
### Compatibilidade OpenAI - `compat.rs`
|
||||
|
||||
Arquivo: `crates/inference-server/src/compat.rs`
|
||||
|
||||
#### ServerConfig
|
||||
|
||||
Gerencia configurações de autenticação, CORS, e catálogo de modelos:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub api_key: Option<String>,
|
||||
pub auth_header: String,
|
||||
pub auth_scheme: Option<String>,
|
||||
pub cors_origins: Vec<String>,
|
||||
pub default_system_prompt: String,
|
||||
pub enable_tool_calls: bool,
|
||||
pub model_catalog: ModelCatalog,
|
||||
}
|
||||
```
|
||||
|
||||
#### ModelCatalog
|
||||
|
||||
Resolve aliases de modelos (case-insensitive):
|
||||
|
||||
```rust
|
||||
pub struct ModelCatalog {
|
||||
models: Vec<ModelDescriptor>,
|
||||
aliases: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
pub struct ModelDescriptor {
|
||||
pub id: String,
|
||||
pub root: String,
|
||||
pub parent: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
**Aliases padrão:** `local-model`, `default`
|
||||
|
||||
#### Chat Templates Suportados
|
||||
|
||||
O sistema detecta automaticamente o template baseado no nome do modelo:
|
||||
|
||||
| Modelo | Template | Formatos |
|
||||
|--------|----------|----------|
|
||||
| Qwen | `<\|im_start\|>`, `<\|im_end\|>` | `system`, `user`, `assistant`, `tool` |
|
||||
| Gemma | `<start_of_turn>`, `<end_of_turn>` | `model`, `user` |
|
||||
| Llama | `[INST]`, `[/INST]` | com `<<SYS>>` para system |
|
||||
| Kimi/Moonshot | `<\|im_system\|>`, `<\|im_user\|>`, `<\|im_assistant\|>` | - |
|
||||
| Genérico | `Role: content` | Padrão |
|
||||
|
||||
#### Autorização
|
||||
|
||||
Suporta múltiplos headers de autenticação:
|
||||
- Header configurado (`LOCALLLM_AUTH_HEADER`)
|
||||
- `Authorization: Bearer <key>`
|
||||
- `X-API-Key: <key>`
|
||||
|
||||
#### Validação de Requests
|
||||
|
||||
Validações implementadas:
|
||||
- Modelo existe no catálogo
|
||||
- Lista de mensagens não está vazia
|
||||
- `n=1` (apenas uma completion por request)
|
||||
- Tool calls habilitado se tools presentes
|
||||
|
||||
---
|
||||
|
||||
## 3. inference-core
|
||||
|
||||
### Engine - `engine.rs`
|
||||
|
||||
Local: `crates/inference-core/src/engine.rs`
|
||||
|
||||
O `InferenceEngine` é o núcleo de inferência que gerencia:
|
||||
- Carregamento do modelo GGUF via llama-sys FFI
|
||||
- Inicialização do contexto llama.cpp
|
||||
- Criação de sessões de geração
|
||||
|
||||
#### EngineConfig
|
||||
|
||||
```rust
|
||||
pub struct EngineConfig {
|
||||
pub model_path: PathBuf, // Caminho para arquivo GGUF
|
||||
pub profile: RuntimeProfile, // Perfil de performance
|
||||
pub n_gpu_layers: i32, // Camadas GPU (-1 = todas)
|
||||
pub n_ctx: u32, // Contexto (tokens)
|
||||
pub n_batch: u32, // Batch size (prefill)
|
||||
pub n_ubatch: u32, // Micro-batch (geração)
|
||||
pub n_threads: i32, // Threads CPU
|
||||
pub n_threads_batch: i32, // Threads para batch
|
||||
pub flash_attn: bool, // Flash Attention
|
||||
pub use_mmap: bool, // Usar memory mapping
|
||||
pub seed: u32, // Seed RNG
|
||||
}
|
||||
```
|
||||
|
||||
#### Runtime Profiles
|
||||
|
||||
| Perfil | n_ctx | n_gpu_layers | Descrição |
|
||||
|--------|-------|--------------|-----------|
|
||||
| `Turbo4B` | 8192 | 999 | GPU total, otimizado para modelos até 4B |
|
||||
| `Balanced9B` | 16384 | 999 | Equilibrado para modelos até 9B |
|
||||
| `CpuFallback` | 4096 | 0 | CPU-only fallback |
|
||||
|
||||
#### Ciclo de Vida do Engine
|
||||
|
||||
```rust
|
||||
// 1. Carregar engine
|
||||
let mut engine = InferenceEngine::load(config)?;
|
||||
|
||||
// 2. Warmup (aloca buffers CUDA)
|
||||
engine.warmup()?;
|
||||
|
||||
// 3. Criar sessão de geração
|
||||
let session = engine.new_session(prompt, sampler_cfg)?;
|
||||
|
||||
// 4. Gerar tokens
|
||||
while let Some(token) = session.next_token() {
|
||||
print!("{}", token);
|
||||
}
|
||||
```
|
||||
|
||||
**Nota sobre segurança:** O engine é `Send` mas NÃO é `Sync`. Acesso deve ser coordenado (o servidor usa `tokio::sync::Mutex`).
|
||||
|
||||
### Session - `session.rs`
|
||||
|
||||
```rust
|
||||
pub struct GenerationSession {
|
||||
// ...
|
||||
}
|
||||
|
||||
impl GenerationSession {
|
||||
pub fn next_token(&mut self) -> Option<String>;
|
||||
pub fn prompt_tokens(&self) -> u32;
|
||||
}
|
||||
```
|
||||
|
||||
### Sampler - `sampler.rs`
|
||||
|
||||
Configurações de sampling:
|
||||
|
||||
```rust
|
||||
pub struct SamplerConfig {
|
||||
pub temperature: f32, // 0.0 - 2.0
|
||||
pub top_p: f32, // 0.0 - 1.0
|
||||
pub max_tokens: u32,
|
||||
pub stop_sequences: Vec<String>,
|
||||
pub repeat_penalty: f32, // 1.15 (normal), 1.0 (tools)
|
||||
pub repeat_last_n: usize, // 256 tokens
|
||||
pub seed: u32,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API Contracts
|
||||
|
||||
Local: `crates/api-contracts/src/`
|
||||
|
||||
### Chat Completion Request (`chat.rs`)
|
||||
|
||||
```rust
|
||||
pub struct ChatCompletionRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<ChatMessage>,
|
||||
pub max_tokens: u32, // default: 1024
|
||||
pub temperature: f32, // default: 0.15
|
||||
pub top_p: f32, // default: 0.95
|
||||
pub stream: bool, // default: false
|
||||
pub stop: Vec<String>,
|
||||
pub n: u32, // default: 1 (apenas 1 suportado)
|
||||
pub seed: Option<u32>,
|
||||
pub user: Option<String>,
|
||||
pub response_format: Option<ResponseFormat>,
|
||||
pub tools: Vec<ToolDefinition>,
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
pub stream_options: Option<StreamOptions>,
|
||||
}
|
||||
```
|
||||
|
||||
### ChatMessage
|
||||
|
||||
```rust
|
||||
pub struct ChatMessage {
|
||||
pub role: Role, // System, Developer, User, Assistant, Tool
|
||||
pub content: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
pub tool_call_id: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### ResponseFormat
|
||||
|
||||
```rust
|
||||
pub struct ResponseFormat {
|
||||
pub format_type: String, // "json_object", "json_schema", "text"
|
||||
pub json_schema: Option<Value>,
|
||||
}
|
||||
```
|
||||
|
||||
### ToolCalling
|
||||
|
||||
```rust
|
||||
pub struct ToolDefinition {
|
||||
pub tool_type: ToolType,
|
||||
pub function: FunctionDefinition,
|
||||
}
|
||||
|
||||
pub struct FunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub parameters: Value, // JSON Schema
|
||||
}
|
||||
|
||||
pub struct ToolCall {
|
||||
pub id: String,
|
||||
pub tool_type: ToolType,
|
||||
pub function: ToolFunctionCall,
|
||||
}
|
||||
|
||||
pub struct ToolFunctionCall {
|
||||
pub name: String,
|
||||
pub arguments: String, // JSON string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Tauri Desktop Backend
|
||||
|
||||
### Entry Point - `lib.rs`
|
||||
|
||||
Local: `tauri-app/src-tauri/src/lib.rs`
|
||||
|
||||
O backend Tauri gerencia:
|
||||
- Ciclo de vida do runtime (start/stop)
|
||||
- Gerenciamento de modelos locais
|
||||
- Downloads do HuggingFace
|
||||
- Detecção de GPU
|
||||
- Sistema ForgeAgent (multi-agente)
|
||||
|
||||
### Commands Tauri
|
||||
|
||||
```rust
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
query_gpu, // Detectar GPU NVIDIA
|
||||
scan_models, // Escanear arquivos .gguf
|
||||
pick_models_dir, // Selecionar diretório
|
||||
pick_model_files, // Selecionar arquivos
|
||||
import_model, // Importar modelo
|
||||
search_hugging_face_models_command, // Buscar no HF
|
||||
start_hugging_face_download_command, // Download do HF
|
||||
cancel_hugging_face_download_command,// Cancelar download
|
||||
swap_model_command, // Trocar modelo
|
||||
delete_model_file, // Deletar arquivo
|
||||
open_model_folder_command, // Abrir pasta
|
||||
list_model_library_entries_command, // Listar biblioteca
|
||||
get_model_library_state_command, // Estado da biblioteca
|
||||
bootstrap_runtime_command, // Bootstrap runtime
|
||||
get_runtime_snapshot_command, // Snapshot do runtime
|
||||
save_runtime_config_command_handler, // Salvar config
|
||||
start_runtime_command, // Iniciar runtime
|
||||
stop_runtime_command, // Parar runtime
|
||||
resolve_startup_model_command, // Resolver modelo inicial
|
||||
get_app_data_dir, // Diretório de dados
|
||||
find_server_binary, // Localizar binário
|
||||
start_forgeagent_session_command, // Iniciar ForgeAgent
|
||||
])
|
||||
```
|
||||
|
||||
### Runtime Types - `runtime/types.rs`
|
||||
|
||||
#### ConnectionMode
|
||||
|
||||
```rust
|
||||
pub enum ConnectionMode {
|
||||
Local, // Servidor local spawnado
|
||||
Remote, // Conectar a servidor externo
|
||||
}
|
||||
```
|
||||
|
||||
#### RuntimeStatus
|
||||
|
||||
```rust
|
||||
pub enum RuntimeStatus {
|
||||
Stopped,
|
||||
Starting,
|
||||
Loading,
|
||||
Ready,
|
||||
Degraded,
|
||||
Stopping,
|
||||
Error,
|
||||
}
|
||||
```
|
||||
|
||||
#### RuntimeConfig
|
||||
|
||||
```rust
|
||||
pub struct RuntimeConfig {
|
||||
pub connection_mode: ConnectionMode,
|
||||
pub start_policy: StartPolicy, // Manual, OnAppLaunch
|
||||
pub server_url: String, // Para Remote mode
|
||||
pub api_key: String,
|
||||
pub model_path: Option<String>,
|
||||
pub models_dir: Option<String>,
|
||||
pub addr: String, // "127.0.0.1:8080"
|
||||
pub gpu_layers: i32,
|
||||
pub ctx_size: u32, // default: 4096
|
||||
pub extra_model_paths: Vec<String>,
|
||||
pub selected_model_id: Option<String>,
|
||||
pub remote_presets: Vec<RemotePreset>,
|
||||
pub hugging_face_token: String,
|
||||
pub preferred_startup_model_alias: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
#### RuntimeSnapshot
|
||||
|
||||
```rust
|
||||
pub struct RuntimeSnapshot {
|
||||
pub status: RuntimeStatus,
|
||||
pub ready: bool,
|
||||
pub process_running: bool,
|
||||
pub resolved_url: String,
|
||||
pub binary_path: Option<String>,
|
||||
pub last_error: Option<RuntimeErrorInfo>,
|
||||
pub active_command: RuntimeLifecycleCommand,
|
||||
pub last_command_result: RuntimeLifecycleResult,
|
||||
pub config: RuntimeConfig,
|
||||
}
|
||||
```
|
||||
|
||||
### Model Library - `model_library/`
|
||||
|
||||
#### Tipos
|
||||
|
||||
```rust
|
||||
pub struct ModelLibraryEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub repo_id: String,
|
||||
pub description: String,
|
||||
pub tags: Vec<String>,
|
||||
pub size_gb: f64,
|
||||
pub default_file: String,
|
||||
pub installed: bool,
|
||||
pub installed_path: Option<String>,
|
||||
}
|
||||
|
||||
pub struct HuggingFaceModelResult {
|
||||
pub id: String,
|
||||
pub model_id: String,
|
||||
pub downloads: i64,
|
||||
pub likes: i32,
|
||||
pub tags: Vec<String>,
|
||||
pub gguf_files: Vec<GgufFileInfo>,
|
||||
}
|
||||
```
|
||||
|
||||
### Model Files - `model_files.rs`
|
||||
|
||||
```rust
|
||||
#[derive(Serialize)]
|
||||
pub struct ModelFileInfo {
|
||||
pub path: String,
|
||||
pub file_name: String,
|
||||
pub size_bytes: u64,
|
||||
pub modified: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Fluxo de Dados
|
||||
|
||||
### 1. Requisição Chat Completion
|
||||
|
||||
```
|
||||
┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ Client │───►│ Axum Router │───►│ compat.rs │───►│ routes.rs │
|
||||
└──────────┘ └─────────────┘ └──────────────┘ └──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ Client │◄───│ JSON │◄───│ Generation │◄───│ Engine │
|
||||
│ │ │ Response │ │ Session │ │ (Mutex) │
|
||||
└──────────┘ └─────────────┘ └──────────────┘ └──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ llama-sys │
|
||||
│ (llama.dll) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 2. Streaming vs Blocking
|
||||
|
||||
**Streaming Mode:**
|
||||
1. Cria stream SSE
|
||||
2. Gera tokens em `spawn_blocking`
|
||||
3. Envia chunks via channel
|
||||
4. Client recebe tokens em tempo real
|
||||
|
||||
**Blocking Mode:**
|
||||
1. Gera completion completa
|
||||
2. Retorna JSON único
|
||||
|
||||
### 3. Ciclo de Vida do Modelo
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Stopped │
|
||||
└──────┬──────┘
|
||||
│ start_runtime_command()
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Starting │
|
||||
└──────┬──────┘
|
||||
│ Processo spawnado
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Loading │
|
||||
└──────┬──────┘
|
||||
│ Modelo carregado
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Ready │◄──────────┐
|
||||
└──────┬──────┘ │
|
||||
│ stop_runtime() │ swap_model_command()
|
||||
▼ │
|
||||
┌─────────────┐ │
|
||||
│ Stopping │ │
|
||||
└─────────────┘ │
|
||||
│
|
||||
├─────────────────────────┤
|
||||
│ swap │
|
||||
│ ┌─────┐ ┌─────┐ │
|
||||
│ │Stop │──►│Start│ │
|
||||
│ └─────┘ └─────┘ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. ForgeAgent - Sistema Multi-Agente
|
||||
|
||||
### Especialistas
|
||||
|
||||
| ID | Nome | Emoji | Função |
|
||||
|----|------|-------|--------|
|
||||
| `analyst` | Analista | 🔍 | Análise técnica e decomposição |
|
||||
| `architect` | Arquiteto | 🏗️ | Arquitetura de software e padrões |
|
||||
| `developer` | Desenvolvedor | 💻 | Implementação pragmática |
|
||||
| `critic` | Crítico | ⚖️ | Revisão e identificação de riscos |
|
||||
|
||||
### Fluxo de Eventos
|
||||
|
||||
```
|
||||
start_forgeagent_session_command()
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ forgeagent:start │──► Envia plano estruturado
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ forgeagent:step-started │──► Início de cada passo
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ forgeagent:step-completed│──► Passo concluído
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────┐
|
||||
│ forgeagent:specialist-thought │──► Pensamento do especialista
|
||||
└───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ forgeagent:specialist-response │──► Resposta do especialista
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ forgeagent:plan-completed│──► Resumo final
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. API HTTP - Exemplos
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"model_loaded": true,
|
||||
"model_id": "qwen2.5-7b-instruct",
|
||||
"uptime_secs": 3600
|
||||
}
|
||||
```
|
||||
|
||||
### Listar Modelos
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/v1/models \
|
||||
-H "Authorization: Bearer sua-chave"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "local-model",
|
||||
"object": "model",
|
||||
"created": 1234567890,
|
||||
"owned_by": "local",
|
||||
"root": "qwen2.5-7b-instruct",
|
||||
"parent": "qwen2.5-7b-instruct",
|
||||
"permission": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Chat Completion (Non-streaming)
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer sua-chave" \
|
||||
-d '{
|
||||
"model": "local-model",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Olá!"}
|
||||
],
|
||||
"max_tokens": 1024,
|
||||
"temperature": 0.7
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1234567890,
|
||||
"model": "qwen2.5-7b-instruct",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Olá! Como posso ajudar?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 20,
|
||||
"total_tokens": 30,
|
||||
"tokens_per_second": 45.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Chat Completion (Streaming)
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer sua-chave" \
|
||||
-d '{
|
||||
"model": "local-model",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Conte uma piada"}
|
||||
],
|
||||
"stream": true
|
||||
}'
|
||||
```
|
||||
|
||||
**Response (SSE):**
|
||||
```
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}
|
||||
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Por"}}]}
|
||||
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":" que"}}]}
|
||||
...
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"finish_reason":"stop"}]}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
### Tool Calling
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer sua-chave" \
|
||||
-d '{
|
||||
"model": "local-model",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Liste os arquivos"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_files",
|
||||
"description": "Lista arquivos do projeto",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Configuração e Deploy
|
||||
|
||||
### Compilação
|
||||
|
||||
```bash
|
||||
# Servidor
|
||||
$env:LOCALLLM_LOG_FORMAT="json"
|
||||
cargo build --release -p inference-server
|
||||
|
||||
# CLI
|
||||
cargo build --release -p localllm-cli
|
||||
|
||||
# Tauri App
|
||||
cd tauri-app
|
||||
cargo tauri build
|
||||
```
|
||||
|
||||
### Execução
|
||||
|
||||
```bash
|
||||
# Com variáveis de ambiente
|
||||
$env:LOCALLLM_MODEL="D:\models\model.gguf"
|
||||
$env:LOCALLLM_PROFILE="balanced_9b"
|
||||
$env:LOCALLLM_API_KEY="minha-chave-secreta"
|
||||
$env:LOCALLLM_ADDR="0.0.0.0:8080"
|
||||
|
||||
.\target\release\inference-server.exe
|
||||
```
|
||||
|
||||
### Docker (se aplicável)
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 \
|
||||
-v /path/to/models:/models \
|
||||
-e LOCALLLM_MODEL=/models/model.gguf \
|
||||
-e LOCALLLM_API_KEY=secret \
|
||||
forgelm/inference-server:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Estrutura de Arquivos
|
||||
|
||||
### Diretórios Importantes
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%/ForgeLM/ # Windows
|
||||
~/.local/share/ForgeLM/ # Linux
|
||||
~/Library/Application Support/ForgeLM/ # macOS
|
||||
├── models/ # Modelos baixados
|
||||
├── runtime/ # Binários do runtime
|
||||
└── config.json # Configuração do runtime
|
||||
```
|
||||
|
||||
### Frontend (Tauri App)
|
||||
|
||||
```
|
||||
tauri-app/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── db.ts # Persistência local (sql.js)
|
||||
│ │ └── types.ts # Tipos TypeScript
|
||||
│ ├── main.tsx # Entry React
|
||||
│ └── App.tsx # App shell
|
||||
└── src-tauri/
|
||||
├── src/
|
||||
│ ├── lib.rs # Entry Tauri
|
||||
│ ├── runtime/ # Gerenciamento runtime
|
||||
│ ├── model_library/ # HuggingFace integration
|
||||
│ ├── model_files.rs # Arquivos locais
|
||||
│ ├── model_alias.rs # Resolução aliases
|
||||
│ └── gpu.rs # Detecção GPU
|
||||
└── taury.conf.json # Config Tauri
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Glossário
|
||||
|
||||
| Termo | Descrição |
|
||||
|-------|-----------|
|
||||
| **GGUF** | Format binary for LLMs (GPT-Generated Unified Format) |
|
||||
| **KV Cache** | Key-Value cache for attention mechanism |
|
||||
| **Prefill** | Initial prompt processing phase |
|
||||
| **Decode** | Token generation phase |
|
||||
| **Context Window** | Maximum tokens the model can process |
|
||||
| **Batch** | Number of tokens processed together |
|
||||
| **GPU Layers** | Number of model layers offloaded to GPU |
|
||||
| **Flash Attention** | Optimized attention implementation |
|
||||
| **Tool Calling** | Function calling capability for LLMs |
|
||||
| **SSE** | Server-Sent Events for streaming |
|
||||
|
||||
---
|
||||
|
||||
## 12. Referências Rápidas
|
||||
|
||||
### Arquivos Principais
|
||||
|
||||
| Função | Arquivo |
|
||||
|--------|---------|
|
||||
| Entry Server | `crates/inference-server/src/main.rs` |
|
||||
| Rotas HTTP | `crates/inference-server/src/routes.rs` |
|
||||
| Compat OpenAI | `crates/inference-server/src/compat.rs` |
|
||||
| Engine | `crates/inference-core/src/engine.rs` |
|
||||
| Session | `crates/inference-core/src/session.rs` |
|
||||
| Sampler | `crates/inference-core/src/sampler.rs` |
|
||||
| Schemas | `crates/api-contracts/src/chat.rs` |
|
||||
| Entry Tauri | `tauri-app/src-tauri/src/lib.rs` |
|
||||
| Runtime Types | `tauri-app/src-tauri/src/runtime/types.rs` |
|
||||
|
||||
### Env Vars Completas
|
||||
|
||||
```bash
|
||||
# Modelo e Performance
|
||||
LOCALLLM_MODEL="/path/to/model.gguf"
|
||||
LOCALLLM_PROFILE="turbo_4b" # turbo_4b, balanced_9b, cpu_fallback
|
||||
LOCALLLM_CTX=8192
|
||||
LOCALLLM_BATCH=4096
|
||||
LOCALLLM_GPU_LAYERS=-1
|
||||
LOCALLLM_THREADS=4
|
||||
LOCALLLM_THREADS_BATCH=4
|
||||
|
||||
# Servidor
|
||||
LOCALLLM_ADDR="0.0.0.0:8080"
|
||||
LOCALLLM_API_KEY="secret"
|
||||
LOCALLLM_AUTH_HEADER="authorization"
|
||||
LOCALLLM_AUTH_SCHEME="Bearer"
|
||||
LOCALLLM_CORS_ORIGINS="*,https://example.com"
|
||||
|
||||
# Features
|
||||
LOCALLLM_ENABLE_TOOL_CALLS="true"
|
||||
LOCALLLM_MODEL_ALIASES="alias1,alias2"
|
||||
LOCALLLM_SYSTEM_PROMPT="Você é um assistente..."
|
||||
|
||||
# Logging
|
||||
LOCALLLM_LOG_FORMAT="json" # ou pretty
|
||||
RUST_LOG="info"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusão
|
||||
|
||||
Este documento cobre a arquitetura completa do backend ForgeLM, incluindo:
|
||||
|
||||
1. **HTTP Server** - API compatível com OpenAI usando Axum
|
||||
2. **Inference Core** - Motor de inferência via llama.cpp
|
||||
3. **Tauri Backend** - Gerenciamento desktop do ciclo de vida
|
||||
4. **API Contracts** - Schemas de request/response
|
||||
5. **Fluxos de Dados** - Como as requisições fluem pelo sistema
|
||||
|
||||
Para mais detalhes, consulte os arquivos de código fonte mencionados nas referências.
|
||||
Loaded 100 of 246 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user