Files
ForgeLM/.agents/rules/architectural-pattern.md
T

194 lines
7.5 KiB
Markdown

---
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