playbook/outfitter-agents/plugins/outfitter-stack/skills/stack-patterns/references/conversion.md

212 lines
6.1 KiB
Markdown

# Conversion Patterns
Patterns for converting existing code to Outfitter Stack conventions.
## Exceptions to Result
Convert throw-based error handling to Result types.
**Before:**
```typescript
async function getUser(id: string): Promise<User> {
const user = await db.users.findById(id);
if (!user) throw new Error(`Not found: ${id}`);
return user;
}
```
**After:**
```typescript
import { Result, NotFoundError, type Handler } from "@outfitter/contracts";
const getUser: Handler<{ id: string }, User, NotFoundError> = async (input, ctx) => {
const user = await db.users.findById(input.id);
if (!user) return Result.err(new NotFoundError("user", input.id));
return Result.ok(user);
};
```
## Console to Structured Logging
Replace console calls with structured logging via context.
**Before:**
```typescript
console.log("Processing", userId);
console.error("Failed to process", error);
console.warn("Deprecated API usage");
```
**After:**
```typescript
ctx.logger.info("Processing", { userId });
ctx.logger.error("Failed to process", { error: error.message });
ctx.logger.warn("Deprecated API usage", { api: "oldEndpoint" });
```
### Logging Level Mapping
| Console Method | Logger Method | When to Use |
|----------------|---------------|-------------|
| `console.log` | `ctx.logger.info` | Normal operations |
| `console.debug` | `ctx.logger.debug` | Development debugging |
| `console.warn` | `ctx.logger.warn` | Unexpected but handled |
| `console.error` | `ctx.logger.error` | Failures requiring attention |
## Hardcoded Paths to XDG
Replace hardcoded home directory paths with XDG-compliant paths.
**Before:**
```typescript
import os from "node:os";
import path from "node:path";
const configPath = path.join(os.homedir(), ".myapp", "config.json");
const cachePath = path.join(os.homedir(), ".cache", "myapp");
const dataPath = path.join(os.homedir(), ".local", "share", "myapp");
```
**After:**
```typescript
import { getConfigDir, getCacheDir, getDataDir } from "@outfitter/config";
import path from "node:path";
const configPath = path.join(getConfigDir("myapp"), "config.json");
const cachePath = getCacheDir("myapp");
const dataPath = getDataDir("myapp");
```
### XDG Path Functions
| Function | Default Path | Env Override |
|----------|--------------|--------------|
| `getConfigDir(app)` | `~/.config/{app}` | `XDG_CONFIG_HOME` |
| `getCacheDir(app)` | `~/.cache/{app}` | `XDG_CACHE_HOME` |
| `getDataDir(app)` | `~/.local/share/{app}` | `XDG_DATA_HOME` |
| `getStateDir(app)` | `~/.local/state/{app}` | `XDG_STATE_HOME` |
## Error Taxonomy Mapping
Map existing custom errors to the 10 taxonomy categories.
| Original Pattern | Outfitter Error | Category |
|------------------|-----------------|----------|
| `NotFoundError` | `NotFoundError` | `not_found` |
| `InvalidInputError` | `ValidationError` | `validation` |
| `DuplicateError` | `ConflictError` | `conflict` |
| `UnauthorizedError` | `AuthError` | `auth` |
| `ForbiddenError` | `PermissionError` | `permission` |
| `TimeoutError` | `TimeoutError` | `timeout` |
| `RateLimitError` | `RateLimitError` | `rate_limit` |
| `ConnectionError` | `NetworkError` | `network` |
| Generic `Error` | `InternalError` | `internal` |
| `AbortError` | `CancelledError` | `cancelled` |
### Mapping by Error Name Keywords
| Keyword in Error Name | Maps To |
|-----------------------|---------|
| `notfound`, `missing` | `NotFoundError` |
| `validation`, `invalid`, `input` | `ValidationError` |
| `conflict`, `duplicate`, `exists` | `ConflictError` |
| `permission`, `forbidden` | `PermissionError` |
| `timeout` | `TimeoutError` |
| `ratelimit`, `rate`, `throttle` | `RateLimitError` |
| `network`, `connection` | `NetworkError` |
| `auth`, `unauthorized`, `unauthenticated` | `AuthError` |
| `cancel`, `abort` | `CancelledError` |
## Compatibility Layer
Wrap legacy throwing code during transition with a Result-returning wrapper.
```typescript
import { Result, InternalError } from "@outfitter/contracts";
/**
* Wraps a synchronous function that may throw, returning a Result.
*/
function wrapSync<T>(fn: () => T): Result<T, InternalError> {
try {
return Result.ok(fn());
} catch (error) {
return Result.err(new InternalError(
error instanceof Error ? error.message : "Unknown error",
{ cause: error }
));
}
}
/**
* Wraps an async function that may throw, returning a Result.
*/
async function wrapAsync<T>(fn: () => Promise<T>): Promise<Result<T, InternalError>> {
try {
return Result.ok(await fn());
} catch (error) {
return Result.err(new InternalError(
error instanceof Error ? error.message : "Unknown error",
{ cause: error }
));
}
}
```
### Usage Example
```typescript
// Wrap a third-party library call
const result = await wrapAsync(() => thirdPartyApi.fetch(id));
if (result.isErr()) {
ctx.logger.error("Third-party API failed", { error: result.error });
return result;
}
const data = result.value;
```
## Try-Catch to Result
Convert try-catch blocks to Result chains.
**Before:**
```typescript
async function processOrder(orderId: string): Promise<Order> {
try {
const order = await fetchOrder(orderId);
const validated = validateOrder(order);
const processed = await processPayment(validated);
return processed;
} catch (error) {
console.error("Order processing failed", error);
throw error;
}
}
```
**After:**
```typescript
const processOrder: Handler<{ orderId: string }, Order, OrderError> = async (input, ctx) => {
const orderResult = await fetchOrder(input.orderId, ctx);
if (orderResult.isErr()) return orderResult;
const validatedResult = validateOrder(orderResult.value);
if (validatedResult.isErr()) return validatedResult;
const processedResult = await processPayment(validatedResult.value, ctx);
if (processedResult.isErr()) return processedResult;
return Result.ok(processedResult.value);
};
```
## Conversion Strategy
1. **New code first** - All new code uses stack patterns
2. **Leaf functions** - Start with functions that don't call others
3. **Bottom-up** - Convert dependencies before dependents
4. **Feature boundaries** - Complete one feature at a time
5. **Test coverage** - Add tests before converting