Our philosophy: If it's hard to test, it's hard to use. Write code that's obvious to humans and AI.
Classes hide complexity. Pure functions expose it.
❌ BAD:
class StatsManager {
private mergeStats(current, incremental) { /* 100 lines */ }
}
// Can't test without instantiating class✅ GOOD:
// utils/stats-merger.ts
export function mergeStats(current: Stats, incremental: Stats): Stats {
return { files: current.files + incremental.files, ... };
}
// Direct test: expect(mergeStats(a, b)).toEqual(expected)When? If it's >20 lines, pure (no side effects), or reusable → extract it.
TypeScript types vanish at runtime. Validate external data.
❌ BAD (found in old codebase):
const request = message.payload as unknown as ExplorationRequest;
// Runtime bomb waiting to happen✅ GOOD (Real example from MCP adapters):
import { z } from 'zod';
import { validateArgs } from './validation.js';
// Define schema once
const ExploreArgsSchema = z.object({
action: z.enum(['pattern', 'similar', 'relationships']),
query: z.string().min(1),
limit: z.number().int().min(1).max(100).default(10),
threshold: z.number().min(0).max(1).default(0.7),
}).strict(); // Reject unknown properties
// Use in adapter
async execute(args: Record<string, unknown>): Promise<ToolResult> {
const validation = validateArgs(ExploreArgsSchema, args);
if (!validation.success) {
return validation.error; // Detailed error with field paths
}
// TypeScript knows exact types! Zero assertions needed.
const { action, query, limit, threshold } = validation.data;
// ^'pattern'|'similar'|'relationships' ^string ^number ^number
}Benefits:
- ✅ Zero type assertions (
as,!) - ✅ Automatic TypeScript type inference
- ✅ Runtime validation with detailed errors
- ✅ Schemas are testable (see
packages/mcp-server/src/schemas/__tests__) - ✅ ~63% less validation code vs. manual checks
Rule: Never use as, as unknown as, or ! without runtime checks.
Exceptions are invisible in type signatures. Result types are explicit.
❌ BAD:
async function fetchUser(id: string): Promise<User> {
if (!valid) throw new Error('Invalid');
if (!found) throw new Error('Not found');
return user;
}
// Forces try-catch, unclear what can fail✅ GOOD:
type Result<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> {
if (!valid) return { ok: false, error: { code: 'INVALID_ID' } };
if (!found) return { ok: false, error: { code: 'NOT_FOUND' } };
return { ok: true, value: user };
}
// Clean usage
const result = await fetchUser('123');
if (!result.ok) {
logger.error(result.error);
return;
}
const user = result.value; // Type-safeWhen to throw: Only for programmer errors (throw new Error('INVARIANT: ...'))
Hard-coded dependencies = untestable code.
❌ BAD:
class PlannerAgent {
async createPlan() {
const github = new GitHubClient(); // Can't mock
}
}✅ GOOD:
interface PlannerDeps {
github: GitHubClient;
indexer: RepositoryIndexer;
}
class PlannerAgent {
constructor(private deps: PlannerDeps) {}
}
// Testing is easy
new PlannerAgent({ github: mockGitHub, indexer: mockIndexer });- Modules: < 300 lines → Split by domain
- Classes: < 400 lines → Use Strategy pattern
- Functions: < 50 lines → Extract helpers
Current refactoring targets:
explore-adapter.ts(690 lines)github-adapter.ts(724 lines)coordinator.ts(480 lines)
src/
├── utils/ # Pure functions
│ ├── validation.ts (120 lines, 100% coverage)
│ ├── validation.test.ts
│ ├── formatting.ts (150 lines, 100% coverage)
│ ├── formatting.test.ts
│ └── index.ts (barrel export)
└── index.ts (integration)
Rules:
- Organize by domain (not "misc")
- Colocate tests with source
- Barrel exports (
index.ts) - Each module < 300 lines
| Type | Coverage | Why |
|---|---|---|
| Pure utilities | 100% | Easy to test, no excuses |
| Integration | 80%+ | Side effects, mocks needed |
| CLI/UI | 60%+ | Harder to test |
Test factories:
// __tests__/factories.ts
export function createMessage(overrides?: Partial<Message>): Message {
return { id: randomUUID(), type: 'request', ...overrides };
}
// Use in tests
const msg = createMessage({ type: 'response' });Standard error format:
interface AppError {
code: string; // 'NOT_FOUND', 'VALIDATION_ERROR'
message: string; // Human-readable
details?: unknown; // Context
recoverable: boolean; // Can retry?
suggestion?: string; // What to do
}Before committing:
- No
as,as unknown as, or! - External data validated (Zod)
- Result types for expected failures
- Dependencies injected
- Pure functions extracted
- 100% coverage on utilities
- Modules < 300 lines, classes < 400 lines
Before: 102-line mergeIncrementalStats() method with mutations
After: 6 pure functions in stats-merger.ts (225 lines, 17 tests, 100% coverage)
Impact:
- Tests run in <1ms (no setup)
- No side effects to track
- Reusable across packages
- AI can understand each function
See: docs/REFACTORING_SUMMARY.md
# Runtime validation
pnpm add zod
# Result type
pnpm add neverthrow # or implement your own
# Property-based testing
pnpm add -D fast-check- Zod Documentation
- Result Type Pattern
- FEATURE_TEMPLATE.md - Implementation guide
- WORKFLOW.md - Git workflow
Questions? If you're unsure whether to extract something: extract it.