- name
- adela-new-module
- description
- Scaffold y creación de nuevos módulos Adela siguiendo el ecosistema de piezas intercambiables
## Adela New Module
Skill para que un LLM cree un nuevo módulo Adela desde cero, siguiendo los estándares del ecosistema (TypeScript strict, zero-deps, TODO en castellano).
### Cuándo usarlo
- El usuario dice "crea un módulo Adela para X"
- Necesitas añadir una nueva pieza al ecosistema Adela
- El usuario pide "otro módulo como los que ya tenemos"
- El usuario da un roadmap multi-fase para mejorar Adela (seguridad, observabilidad, escalabilidad, API) — cargar `references/backend-roadmap.md` para entender las fases y categorías
### ⚠️ Regla CRÍTICA: Todos los repos Adela son PRIVADOS
**Cada módulo Adela se crea como repositorio PRIVADO en GitHub.** El usuario lo exige explícitamente. NO crear repos públicos.
### Categorías de módulos (nuevas desde v2.0)
| Categoría | Descripción | Ejemplos |
|-----------|-------------|----------|
| `infra` | Capa base — sin dependencias de otros Adela | time, env, http, cache, db |
| `core` | Capa funcional — depende de infra | auth, health, ai |
| `export` | Capa de exportación | export |
| `presentation` | Capa de interfaz | i18n, admin |
| `seguridad` | **NUEVA** — Seguridad y validación | security, rateLimit, validation |
| `observabilidad` | **NUEVA** — Logging y errores | logger, errors |
| `escalabilidad` | **NUEVA** — Escalado horizontal | db_pg, cache_redis |
| `api-layer` | **NUEVA** — Capa de API | pagination, router |
### Principio de explicación
El usuario quiere que **le expliques cómo funciona cada módulo**. Al crear un módulo nuevo:
- README.md debe explicar la arquitectura y el flujo
- Explicar por qué se eligió ese enfoque (no solo el qué, sino el porqué)
- Incluir diagramas de flujo ASCII cuando sea relevante
### Pasos
#### 1. Copiar template scaffold
```bash
cp -r /root/workspace/AdelaMasterMind/templates/adela-module-scaffold/ /root/workspace/Adela/Adela_<NOMBRE>/
```
#### 2. Rellenar package.json
Reemplazar placeholders `{{MODULE_NAME}}`, `{{MODULE_NAME_LOWERCASE}}`, `{{MODULE_DESCRIPTION}}`.
Reglas de dependencias:
- **Zero runtime deps** siempre que sea posible
- Si necesita base de datos → `sql.js`
- Si necesita auth → `bcryptjs`, `jsonwebtoken`
- Si necesita export → `csv-parse`, `csv-stringify`, `pdfkit`
- Si necesita fetch → usar `fetch()` nativo de Node (18+)
#### 3. Implementar src/
Estructura estándar:
```
src/
├── index.ts # Barrel export: exporta todo
├── <modulo>.ts # Implementación principal con createX() factory
└── types.ts # Interfaces públicas
```
**Patrón multi-archivo:** Cuando un módulo tiene responsabilidades separadas (ej: implementación + formato de salida), dividir en archivos:
```
src/
├── index.ts # Barrel export
├── metrics.ts # Lógica principal (createMetrics, counter, histogram, gauge, middleware)
├── prometheus.ts # Formato de exportación (toPrometheusFormat, toPrometheusJSON)
└── types.ts # Tipos públicos
```
Cada archivo debe ser autocontenido y testable por separado. `index.ts` solo re-exporta.
**🔴 PITFALL — Token GitHub enmascarado:**
El `GITHUB_TOKEN` en `/hermes-home/.env` se muestra como `***` (enmascaramiento visual) pero es un PAT real de 40 caracteres. Sin embargo, **puede expirar entre llamadas consecutivas**, causando "Bad credentials" intermitentes.
**Patrón seguro:** siempre sourcear `.env` y usar el token en el mismo bloque de comando:
```bash
source /hermes-home/.env 2>/dev/null
# Usar $GITHUB_TOKEN inmediatamente en el mismo bloque
curl -s -X POST -H "Authorization: token $GITHUB_TOKEN" ...
```
Si el token falla, verificar si se expiró y reemplazarlo en `.env` con un nuevo PAT desde GitHub Settings → Developer Settings → Personal access tokens.
**🔴 PITFALL CRÍTICO — JWT en ESM con jsonwebtoken:**
`import * as jwt from 'jsonwebtoken'` crea un **namespace object** donde `jwt.verify` funciona pero `jwt.sign` NO existe como método directo. Esto causa fallos silenciosos: el login genera token OK pero el middleware lo rechaza como "inválido".
**Solución:** usar `createRequire` en TODOS los archivos que usen jsonwebtoken (routes Y middleware):
```typescript
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const jwt = require('jsonwebtoken')
// Ahora jwt.sign Y jwt.verify funcionan
```
**NUNCA mezclar:** un archivo con `import * as jwt` y otro con `require('jsonwebtoken')` = tokens que no se verifican.
**🔴 PITFALL CRÍTICO — JWT_SECRET compartido entre módulos:**
Si cada archivo (routes/auth.ts, middleware/auth.ts) genera su propio `process.env.JWT_SECRET || crypto.randomUUID()`, cada uno crea un UUID diferente. Resultado: el token se firma con el UUID de auth.ts pero se verifica con el UUID de middleware/auth.ts → siempre falla como "Token inválido".
**Solución:** un solo `config.ts` exporta el JWT_SECRET, todos los archivos importan de ahí:
```typescript
// src/config.ts
export const JWT_SECRET = process.env.JWT_SECRET || crypto.randomUUID()
// routes/auth.ts
import { JWT_SECRET } from '../config.js'
// middleware/auth.ts
import { JWT_SECRET } from '../config.js'
```
**NUNCA** generar JWT_SECRET inline en cada archivo. Siempre un módulo compartido.
**🔴 PITFALL — better-sqlite3 vs sql.js:**
`better-sqlite3` requiere compilación nativa (node-gyp + make). Si no hay `make` en el sistema, el `npm install` falla. En entornos sin herramientas de compilación, usar **`sql.js`** (JS puro, sin compilación):
```bash
npm install sql.js
```
Importación: `import initSqlJs from 'sql.js'` (async init).
**🔴 PITFALL — sql.js es en memoria: persistencia en archivo:**
sql.js por defecto guarda todo en memoria. Si el contenedor se reinicia, se pierden TODOS los datos. Para persistencia, exportar a archivo tras cada operación de escritura:
```typescript
import fs from 'fs'
import path from 'path'
const DB_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'data', 'datos.db')
// Al iniciar: cargar desde archivo si existe
if (fs.existsSync(DB_PATH)) {
const buffer = fs.readFileSync(DB_PATH)
db = new SQL.Database(buffer)
} else {
db = new SQL.Database()
}
// Después de CADA db.run() (INSERT, UPDATE, DELETE):
function saveDatabase() {
const data = db.export()
const buffer = Buffer.from(data)
const dir = path.dirname(DB_PATH)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(DB_PATH, buffer)
}
// Wrapper que guarda automáticamente:
function run(sql: string, params: any[] = []) {
db.run(sql, params)
saveDatabase() // ← persistir tras cada escritura
}
```
**Dockerfile:** crear directorio de datos y dar permisos:
```dockerfile
RUN mkdir -p /app/data
RUN chown -R appuser:appgroup /app
```
**Env var:** `DB_PATH=/app/data/datos.db` para rutas personalizadas.
**🔴 PITFALL — sql.js INSERT con valores undefined:**
Al hacer INSERT con sql.js, pasar `undefined` como valor de parámetro causa fallos silenciosos o errores de tipo. Siempre usar fallbacks:
```typescript
// ❌ MAL — data.descripcion puede ser undefined
db.run('INSERT INTO table (a, b, c) VALUES (?, ?, ?)',
[id, data.nombre, data.descripcion])
// ✅ BIEN — fallbacks explícitos
db.run('INSERT INTO table (a, b, c) VALUES (?, ?, ?)',
[id, data.nombre, data.descripcion || ''])
```
Regla: `|| ''` para strings, `|| 0` para números, `|| null` para NULL explícito.
**🔴 PITFALL — sql.js INSERT column ordering:**
Verificar que las columnas del INSERT coinciden con los valores. Un error común es poner `rol` como entero `1` en vez de string `'admin'` en el INSERT del usuario admin.
**🔴 PITFALL — sql.js db.run() devuelve void/Promise, no el objeto creado:**
`db.run()` NO devuelve el registro insertado. Si una función como `crearLead()` llama `db.run()` y luego construye el objeto manualmente, la función DEBE ser `async` y hacer `await db.run(...)`. Si la función del route no hace `await`, Express serializa la Promise como `{}` → respuesta vacía.
Síntoma: `{"lead":{}}` en POST, `{"leads":{}}` en GET, `{"stats":{}}` en stats. El objeto se construye bien internamente pero Express lo serializa vacío porque la función devuelve una Promise sin await.
**✅ Fix:** TODAS las funciones de route que llaman a `db.*()` deben ser `async` + `await`. TODAS las funciones en db.ts que llaman `db.run()` o `db.exec()` deben ser `async` + `await`.
**🔴 PITFALL — Template literal SQL: backtick de cierre al añadir tablas con patch:**
Al añadir un nuevo bloque `db.run(\`...\`)` al final de un bloque SQL existente usando `patch()`, el `old_string` DEBE incluir el backtick de cierre `\`)` del bloque anterior. Si no, se deja un `db.run()` sin cerrar, causando TS1005 '`,` expected' en todas las líneas SQL siguientes.
```typescript
// ❌ MAL — old_string sin el backtick de cierre del bloque anterior
// old_string: "CREATE INDEX ... ;\n\n // Admin user"
// Resultado: el db.run() de las tablas base se queda abierto → TS1005 en todas las tablas nuevas
// ✅ BIEN — old_string incluye el cierre del bloque anterior
// old_string: "CREATE INDEX ... ;\n `)\n\n // Admin user"
// Resultado: el bloque anterior se cierra correctamente, el nuevo abre su propio db.run()
// new_string correcto:
// "CREATE INDEX ... ;\n `)\n\n // === Nuevo módulo ===\n db.run(`\n ...\n `)\n\n // Admin user"
```
**🔴 PITFALL — createRequire no va en types.ts:**
`createRequire` es para archivos runtime (db.ts, routes, middleware). Si se pone en `types.ts`, causa error TS1343: `import.meta` meta-property only allowed with module es2020/esnext/node16+. types.ts solo debe tener interfaces y tipos, nunca imports de runtime.
**🔴 PITFALL — Migración de datos al cambiar schema (admin pin_hash NULL):**
Cuando se actualiza el código para usar bcrypt en PINs, los usuarios existentes tienen `pin_hash = NULL` (creados con el código viejo). El login falla silenciosamente porque `bcrypt.compareSync(pin, null)` siempre devuelve false.
**Solución — migración automática en initDatabase():**
```typescript
// Después de CREATE TABLE, detectar y migrar:
const adminRow = db.exec("SELECT id, pin_hash FROM usuarios WHERE email = 'admin@adelacrm.local'")
if (adminRow[0]?.values[0][1] === null) {
const pinHash = bcrypt.hashSync(String(process.env.ADMIN_PIN || '1234'), 10)
db.run("UPDATE usuarios SET pin_hash = ? WHERE email = 'admin@adelacrm.local'", [pinHash])
saveDatabase()
}
```
**Regla:** Siempre que se cambie el formato de un campo (plain → hash, string → enum, etc.), añadir migración automática en initDatabase() que detecte el formato viejo y lo actualice. Nunca asumir que la BD está vacía.
**🔴 PITFALL — API devuelve campos sensibles (pin_hash, password):**
Nunca devolver `pin_hash`, `password`, ni otros campos sensibles en respuestas API. Crear un helper `sanitizeUser()` en la ruta:
```typescript
function sanitizeUser(u: any) {
const { pin_hash, ...rest } = u
return rest
}
// En GET /usuarios:
const usuarios = await obtenerUsuarios()
res.json({ usuarios: usuarios.map(sanitizeUser) })
// En POST /usuarios (respuesta):
res.status(201).json({ usuario: sanitizeUser(usuario) })
```
**🔴 PITFALL — Admin PIN hardcodeado:**
Nunca hardcodear PINs de admin en el código fuente. Usar variable de entorno:
```typescript
// En db.ts, al crear el admin inicial:
const adminPin = process.env.ADMIN_PIN || '1234'
const pinHash = bcrypt.hashSync(String(adminPin), 10)
db.run("INSERT INTO usuarios ... VALUES (?, ?, ?, ?, ?, ?, ?)",
['admin-001', 'Administrador', 'admin@adelacrm.local', pinHash, 'admin', 1, ahora])
```
Documentar en `.env.example`:
```
ADMIN_PIN=1234
JWT_SECRET=tu-secreto-super-seguro
DB_PATH=/app/data/datos.db
```
**🔴 PITFALL — PIN visible en HTML (login hint):**
Nunca mostrar el PIN real en el HTML del login. En vez de `Demo: admin@local / 1234`, usar:
```html
<p class="hint">Credenciales de demostración en el README</p>
```
**🔴 PITFALL — Healthcheck en Dockerfile apunta a endpoint inexistente:**
Verificar que el healthcheck del Dockerfile usa el endpoint REAL del servidor. Error común: Dockerfile dice `/healthz` pero el servidor expone `/health`.
```dockerfile
# ❌ MAL — endpoint no existe
CMD wget -qO- http://localhost:9000/healthz || exit 1
# ✅ BIEN — coincide con el servidor
CMD wget -qO- http://localhost:9000/health || exit 1
```
**🔴 PITFALL — Express req.params.id es string | string[], no string:**
Al leer parámetros de ruta en Express, `req.params.id` devuelve `string | string[]` (el tipo de Express), no `string` directamente. Si se pasa a una función que espera `string`, TypeScript lanza TS2345.
```typescript
// ❌ MAL
const item = await obtenerPorId(req.params.id) // TS2345: string | string[] no asignable a string
// ✅ BIEN — castear siempre
const id = req.params.id as string
const item = await obtenerPorId(id)
```
Regla: SIEMPRE hacer `const id = req.params.id as string` en handlers de rutas Express, tanto en GET/PUT/DELETE de una entidad como en rutas anidadas (`req.params.presupuestoId`, `req.params.lineaId`, etc.).
Las rutas estáticas (ej: `/stats`) deben definirse ANTES de las rutas con parámetros (ej: `/:id`). Si no, Express captura `/stats` como un ID. Esto es la causa #1 de "endpoint no encontrado" en Express.
**🔴 PITFALL — TypeScript module resolution:**
La combinación que funciona para proyectos ESM con `import.meta`, `createRequire`, y default imports depende del tipo de módulo:
**Para módulos backend (Express, sql.js, JWT):**
Ver no GitHub