docs: enterprise refactor plan with ralph specs
This commit is contained in:
136
.ralph/specs/phase-02-shared-infrastructure.md
Normal file
136
.ralph/specs/phase-02-shared-infrastructure.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Phase 2: Shared Infrastructure
|
||||
|
||||
## Config.ts
|
||||
Usa Zod para validar TODAS las env vars al arranque. Si falla → crash inmediato con mensaje claro.
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const configSchema = z.object({
|
||||
port: z.coerce.number().default(3001),
|
||||
host: z.string().default('0.0.0.0'),
|
||||
nodeEnv: z.enum(['development', 'production', 'test']).default('development'),
|
||||
db: z.object({
|
||||
driver: z.enum(['sqlite', 'postgres']).default('sqlite'),
|
||||
path: z.string().default('./data/abe.db'),
|
||||
url: z.string().optional(),
|
||||
}),
|
||||
auth: z.object({
|
||||
secret: z.string().min(16).default('abe-dev-secret-change-in-prod'),
|
||||
sessionMaxAge: z.coerce.number().default(86400),
|
||||
}),
|
||||
storage: z.object({
|
||||
driver: z.enum(['local', 's3']).default('local'),
|
||||
path: z.string().default('./data/storage'),
|
||||
}),
|
||||
cors: z.object({ origin: z.string().default('http://localhost:5173') }),
|
||||
log: z.object({ level: z.enum(['debug','info','warn','error']).default('info') }),
|
||||
api: z.object({
|
||||
key: z.string().default('abe-dev-key-123'),
|
||||
rateLimitWindowMs: z.coerce.number().default(900000),
|
||||
rateLimitMax: z.coerce.number().default(100),
|
||||
}),
|
||||
ai: z.object({
|
||||
provider: z.enum(['claude','openai','ollama','none']).default('none'),
|
||||
apiKey: z.string().default(''),
|
||||
autoEnrich: z.coerce.boolean().default(false),
|
||||
minSeverity: z.enum(['low','medium','high','critical']).default('high'),
|
||||
}),
|
||||
jobs: z.object({
|
||||
maxConcurrentSessions: z.coerce.number().default(3),
|
||||
pollIntervalMs: z.coerce.number().default(1000),
|
||||
}),
|
||||
license: z.object({ key: z.string().default('') }),
|
||||
});
|
||||
|
||||
export type AppConfig = z.infer<typeof configSchema>;
|
||||
|
||||
export function loadConfig(): AppConfig {
|
||||
// Map env vars to schema shape, parse
|
||||
}
|
||||
```
|
||||
|
||||
## Logger.ts
|
||||
```typescript
|
||||
import pino from 'pino';
|
||||
|
||||
export function createLogger(config: { level: string; nodeEnv: string }): pino.Logger {
|
||||
return pino({
|
||||
level: config.level,
|
||||
transport: config.nodeEnv === 'development'
|
||||
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss' } }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
export type Logger = pino.Logger;
|
||||
```
|
||||
|
||||
## DatabaseConnection.ts
|
||||
```typescript
|
||||
import { Kysely, SqliteDialect } from 'kysely';
|
||||
import SQLite from 'better-sqlite3';
|
||||
|
||||
// Define Database interface con todas las tablas
|
||||
export interface Database {
|
||||
sessions: SessionTable;
|
||||
states: StateTable;
|
||||
actions: ActionTable;
|
||||
anomalies: AnomalyTable;
|
||||
// ... más tablas se añaden en fases posteriores
|
||||
}
|
||||
|
||||
export function createDatabase(config: { driver: string; path: string; url?: string }): Kysely<Database> {
|
||||
if (config.driver === 'postgres') {
|
||||
// Import dinámico de pg para no requerir en SQLite
|
||||
const { Pool } = require('pg');
|
||||
const { PostgresDialect } = require('kysely');
|
||||
return new Kysely<Database>({
|
||||
dialect: new PostgresDialect({ pool: new Pool({ connectionString: config.url }) }),
|
||||
});
|
||||
}
|
||||
|
||||
// Crear directorio data/ si no existe
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
fs.mkdirSync(path.dirname(config.path), { recursive: true });
|
||||
|
||||
return new Kysely<Database>({
|
||||
dialect: new SqliteDialect({ database: new SQLite(config.path) }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## InProcessEventBus.ts
|
||||
```typescript
|
||||
import { EventEmitter } from 'events';
|
||||
// Implements EventBus interface from shared/application
|
||||
// Logging de cada evento publicado
|
||||
// Catch errors en handlers (log pero no crash)
|
||||
// setMaxListeners(50)
|
||||
```
|
||||
|
||||
## StorageProvider.ts
|
||||
```typescript
|
||||
export interface IStorageProvider {
|
||||
save(relativePath: string, data: Buffer): Promise<string>;
|
||||
get(relativePath: string): Promise<Buffer | null>;
|
||||
delete(relativePath: string): Promise<void>;
|
||||
exists(relativePath: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
// LocalStorageProvider: usa fs.promises, base path = config.storage.path
|
||||
// Crea directorios automáticamente con mkdir recursive
|
||||
```
|
||||
|
||||
## Migración 001
|
||||
Crea las tablas que ya existen en el schema actual (sessions, states, actions, anomalies, notifications).
|
||||
Usar `CREATE TABLE IF NOT EXISTS` para idempotencia.
|
||||
Los tipos de columna deben coincidir con lo que ya tiene better-sqlite3.
|
||||
|
||||
## IMPORTANTE
|
||||
- Config DEBE fallar rápido si hay env vars inválidas
|
||||
- Logger NUNCA debe usar console.log
|
||||
- Database factory NUNCA importa pg a menos que driver sea postgres
|
||||
- EventBus handlers que fallan se loguean pero NO crashean el bus
|
||||
|
||||
Reference in New Issue
Block a user