@bi-analytics/arness-back 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +156 -2
  3. package/bin/arness.mjs +710 -0
  4. package/harness/AGENTS.md +234 -0
  5. package/harness/agents/implementador.md +42 -0
  6. package/harness/agents/lider.md +71 -0
  7. package/harness/agents/revisor.md +62 -0
  8. package/harness/decisiones/README.md +17 -0
  9. package/harness/decisiones/_plantilla.md +24 -0
  10. package/harness/init.sh +696 -0
  11. package/harness/playbooks/add-database.md +16 -0
  12. package/harness/playbooks/new-endpoint.md +88 -0
  13. package/harness/playbooks/new-module.md +21 -0
  14. package/harness/playbooks/new-monitor-widget.md +31 -0
  15. package/harness/playbooks/review.md +40 -0
  16. package/harness/reference/configuracion.md +104 -0
  17. package/harness/reference/contrato-http.md +138 -0
  18. package/harness/reference/novedades.json +107 -0
  19. package/harness/reference/progress-plantilla.md +50 -0
  20. package/harness/skills/commit-hygiene/SKILL.md +137 -0
  21. package/harness/skills/nest-base/SKILL.md +167 -0
  22. package/harness/skills/nest-base/templates/config/configuration.spec.ts +84 -0
  23. package/harness/skills/nest-base/templates/config/configuration.ts +143 -0
  24. package/harness/skills/nest-base/templates/cors/cors.spec.ts +53 -0
  25. package/harness/skills/nest-base/templates/cors/cors.ts +75 -0
  26. package/harness/skills/nest-base/templates/docs/EVOLUTIVO.md +22 -0
  27. package/harness/skills/nest-base/templates/docs/PENDIENTES.md +20 -0
  28. package/harness/skills/nest-base/templates/docs/TECNICO.md +43 -0
  29. package/harness/skills/nest-base/templates/e2e/cors.e2e-spec.ts +60 -0
  30. package/harness/skills/nest-base/templates/e2e/monitor.e2e-spec.ts +134 -0
  31. package/harness/skills/nest-base/templates/editorconfig +12 -0
  32. package/harness/skills/nest-base/templates/eslint.boundaries.mjs +65 -0
  33. package/harness/skills/nest-base/templates/eslint.config.mjs +82 -0
  34. package/harness/skills/nest-base/templates/logging/logger-options.spec.ts +57 -0
  35. package/harness/skills/nest-base/templates/logging/logger-options.ts +85 -0
  36. package/harness/skills/nest-base/templates/monitor/monitor-login-redirect.filter.ts +16 -0
  37. package/harness/skills/nest-base/templates/monitor/monitor-login.dto.ts +8 -0
  38. package/harness/skills/nest-base/templates/monitor/monitor-page.spec.ts +41 -0
  39. package/harness/skills/nest-base/templates/monitor/monitor-page.ts +190 -0
  40. package/harness/skills/nest-base/templates/monitor/monitor-session.guard.spec.ts +46 -0
  41. package/harness/skills/nest-base/templates/monitor/monitor-session.guard.ts +36 -0
  42. package/harness/skills/nest-base/templates/monitor/monitor-session.service.spec.ts +57 -0
  43. package/harness/skills/nest-base/templates/monitor/monitor-session.service.ts +77 -0
  44. package/harness/skills/nest-base/templates/monitor/monitor-widget.interface.ts +18 -0
  45. package/harness/skills/nest-base/templates/monitor/monitor.constants.ts +25 -0
  46. package/harness/skills/nest-base/templates/monitor/monitor.controller.ts +63 -0
  47. package/harness/skills/nest-base/templates/monitor/monitor.decorators.ts +11 -0
  48. package/harness/skills/nest-base/templates/monitor/monitor.module.ts +30 -0
  49. package/harness/skills/nest-base/templates/monitor/monitor.service.spec.ts +55 -0
  50. package/harness/skills/nest-base/templates/monitor/monitor.service.ts +49 -0
  51. package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.spec.ts +51 -0
  52. package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.ts +53 -0
  53. package/harness/skills/nest-base/templates/prettierignore +6 -0
  54. package/harness/skills/nest-base/templates/prettierrc.json +8 -0
  55. package/package.json +44 -3
@@ -0,0 +1,16 @@
1
+ # Playbook: agregar una conexión de BD
2
+
3
+ Referencia: `AGENTS.md` §6 y §13.
4
+
5
+ 1. **Confirmar con el humano:** el nombre lógico de la conexión (UPPER_SNAKE, ej. `REPORTS`) y qué módulos la usarán. Nunca pidas ni escribas credenciales reales en el código ni en el chat.
6
+ 2. Agrega el nombre a `DATABASES` y el bloque de variables `DB_<NOMBRE>_*` en:
7
+ - `.env.example`, con valores de ejemplo;
8
+ - `.env.local`, con valores locales de desarrollo.
9
+ - **No toques `.env`.**
10
+ 3. Verifica que la validación de `src/config/` exige las variables de cada nombre listado en `DATABASES`, y que `parseDatabases` de `src/config/configuration.ts` arma su bloque en `AppConfig.databases`. Normalmente no requiere cambios de código, porque ambas son dinámicas. Nunca leas `DB_<NOMBRE>_*` con `process.env` fuera de `src/config/`.
11
+ 4. En los repositories que la usen, inyecta el pool con `@Inject(getPoolToken('<NOMBRE>'))`.
12
+ 5. Confirma que `/health` reporta la nueva conexión.
13
+ 6. Documenta la conexión en el `README.md` de cada módulo que la use y en la tabla de conexiones de `docs/TECNICO.md` (solo nombre lógico y origen, nunca credenciales), y agrega una entrada en `docs/EVOLUTIVO.md`.
14
+ 7. Corre `./harness/init.sh`.
15
+
16
+ Recuerda que el usuario de BD debe ser de solo lectura. El pool fuerza `default_transaction_read_only = on` como segunda barrera, no como única.
@@ -0,0 +1,88 @@
1
+ # Playbook: crear un endpoint (reporte)
2
+
3
+ Referencia de reglas: `AGENTS.md` §5 (patrón), §6 (BD), §7 (caché), §9 (contrato), §12 (documentación).
4
+
5
+ ## 0. Confirmar el contrato (antes de escribir código)
6
+
7
+ Presenta esto al humano y espera su confirmación:
8
+
9
+ ```
10
+ Endpoint: GET /api/v1/<modulo>/<segmento>
11
+ Ubicación: src/modules/<cliente>/<modulo>/<endpoint>/
12
+ Conexión BD: <NOMBRE> Origen: <schema.tabla | vista>
13
+ Columnas: <col1: tipo>, <col2: tipo>, ...
14
+ Filtros: <nombre: tipo, obligatorio|opcional, default>
15
+ Paginación: sí|no (pageSize default N, máx M)
16
+ Orden: <columnas permitidas>, default <col DESC>
17
+ Caché TTL: <segundos>
18
+ ```
19
+
20
+ Si falta información, pregunta. No inventes columnas, tablas ni reglas.
21
+
22
+ ## 1. Archivos, en este orden
23
+
24
+ 1. `<endpoint>.columns.ts`: `export const X_COLUMNS = Object.freeze([...] as const)`.
25
+ 2. `<endpoint>.entity.ts`: el tipo de la fila, derivado de las columnas.
26
+ 3. `<endpoint>-query.dto.ts`: los params HTTP con `class-validator` (`@IsOptional`, `@IsDateString`, `@IsInt`, `@Min`, `@Max`, `@IsIn`).
27
+ 4. `<endpoint>-filters.dto.ts`: los filtros normalizados (fechas como `Date`, defaults aplicados, `page`/`pageSize` resueltos).
28
+ 5. `<endpoint>.sql.ts`: funciones puras `buildXQuery(filters): { text: string; values: unknown[] }` y, si pagina, `buildXCountQuery(filters)`. Todo valor va como `$n`, y el ORDER BY sale de una allowlist de `columns.ts`.
29
+ 6. `<endpoint>.repository.ts`: el token `X_REPOSITORY`, la interfaz `XRepository` y la clase `PgXRepository`, que inyecta el pool de la conexión nombrada.
30
+ 7. `<endpoint>.service.ts`: Cache-Aside con `CacheService`, y retorna `{ data, meta? }` junto al estado de caché.
31
+ 8. `<endpoint>.controller.ts`: `@Get('<segmento>')`, recibe el DTO, lo convierte en filtros, llama al service y fija `X-Cache`.
32
+ 9. Registrar controller, service y provider del repository (`{ provide: X_REPOSITORY, useClass: PgXRepository }`) en `<modulo>.module.ts`.
33
+
34
+ Aplica la regla de agrupación (más de 2 archivos del mismo tipo en una carpeta → carpeta por tipo). `init.sh` verifica que el endpoint tenga todos estos archivos y los specs de la sección 2.
35
+
36
+ ## 2. Tests
37
+
38
+ - `*.sql.spec.ts`: valida `text` y `values` para cada combinación de filtros, y que ningún valor aparezca interpolado en `text`.
39
+ - `*.service.spec.ts`: HIT, MISS, BYPASS (caché lanza error o está deshabilitada), con y sin paginación.
40
+ - `*.controller.spec.ts`: delega al service y fija `X-Cache`.
41
+ - `*.repository.spec.ts`: con pool mockeado, verifica que ejecuta el SQL del builder y mapea las filas.
42
+ - `*-query.dto.spec.ts`: casos válidos e inválidos.
43
+ - E2E en `test/`: 401 sin token, 200 con token y envelope correcto, 400 con filtro inválido.
44
+
45
+ ## 3. Documentación
46
+
47
+ ### Postman
48
+
49
+ Agrega la request en `postman/arness-back.postman_collection.json`, dentro de la carpeta `<cliente>/<modulo>`:
50
+ - `GET {{baseUrl}}/api/v1/<modulo>/<segmento>?param=ejemplo`
51
+ - Auth: heredada de la colección (`Bearer {{accessToken}}`)
52
+ - Sin descripciones largas.
53
+
54
+ ### FRONT.md
55
+
56
+ Agrega una sección con esta plantilla:
57
+
58
+ ````markdown
59
+ ### <Nombre legible del reporte>
60
+
61
+ `GET /api/v1/<modulo>/<segmento>` · requiere `Authorization: Bearer <accessToken>`
62
+
63
+ Qué devuelve: <una frase>.
64
+
65
+ | Parámetro | Tipo | Obligatorio | Ejemplo |
66
+ |---|---|---|---|
67
+ | desde | fecha ISO | sí | 2026-01-01 |
68
+ | page | número | no (default 1) | 1 |
69
+
70
+ ```json
71
+ { "data": [{ "col1": "…", "col2": 0 }], "meta": { "pagination": { "page": 1, "pageSize": 50, "total": 120, "totalPages": 3 } } }
72
+ ```
73
+
74
+ **Consumo recomendado:** <paginar con pageSize ≤ N | traer completo>; <filtros a enviar siempre>; ante `401 TOKEN_EXPIRED`, llamar a `/auth/refresh` y reintentar una vez; datos cacheados en servidor por <TTL>, así que no hace falta hacer polling más frecuente.
75
+ ````
76
+
77
+ ### README del módulo
78
+
79
+ Agrega el endpoint, su conexión y el origen de datos.
80
+
81
+ ### docs/
82
+
83
+ - `docs/EVOLUTIVO.md`: agrega al final una entrada `## <fecha> · <cliente>/<modulo>` con lo entregado en lenguaje de negocio (qué reporte quedó disponible), el endpoint y la referencia. Solo avance.
84
+ - `docs/PENDIENTES.md`: lo que quedó abierto (por ejemplo, una vista que aún no existe en la BD o un filtro aplazado).
85
+
86
+ ## 4. Cierre
87
+
88
+ Corre `./harness/init.sh` y revisa el Definition of Done de `AGENTS.md` §15.
@@ -0,0 +1,21 @@
1
+ # Playbook: crear un módulo o cliente
2
+
3
+ Referencia: `AGENTS.md` §4 y §5.
4
+
5
+ 1. **Confirmar con el humano:** nombre del cliente (carpeta de primer nivel), nombre del módulo, segmento base de la URL y conexión de BD por defecto.
6
+ 2. **Cliente nuevo:** crea `src/modules/<cliente>/`. Si se quiere agrupar sus módulos, agrega `<cliente>.module.ts` que los importe, y regístralo en `app.module.ts`.
7
+ 3. **Módulo nuevo:** crea `src/modules/<cliente>/<modulo>/` con:
8
+ - `<modulo>.module.ts`: `@Module({ controllers: [], providers: [] })`, sin imports de otros módulos de negocio. El código del módulo solo importa de su propia carpeta, de `src/common/` y de `src/config/` (ESLint lo verifica).
9
+ - `README.md` con esta plantilla:
10
+ ```markdown
11
+ # <Módulo>
12
+ Propósito: <una frase>.
13
+ Conexión BD: <NOMBRE>.
14
+ | Endpoint | Origen de datos | Caché TTL |
15
+ |---|---|---|
16
+ ```
17
+ 4. Registra el módulo en su padre (el módulo del cliente o `app.module.ts`).
18
+ 5. Crea la carpeta `<cliente>/<modulo>` en la colección de Postman y un encabezado `## <Módulo>` en `FRONT.md`.
19
+ 6. Agrega endpoints con `new-endpoint.md`.
20
+ 7. Agrega el módulo a la tabla de módulos de `docs/TECNICO.md` y una entrada en `docs/EVOLUTIVO.md` (solo avance; lo abierto va a `docs/PENDIENTES.md`).
21
+ 8. Corre `./harness/init.sh`.
@@ -0,0 +1,31 @@
1
+ # Playbook: agregar un widget al monitor
2
+
3
+ Referencia: `AGENTS.md` §10 y §13, y `harness/reference/contrato-http.md` (Monitor). El monitor ya existe en `src/common/monitor/` (skill `nest-base`).
4
+
5
+ ## 0. Confirmar con el humano
6
+
7
+ ```
8
+ Widget: <id-kebab> · Título: <texto corto> · Orden: <número>
9
+ Muestra: <qué cifra o estado técnico> · Desglose: <sí/no, por qué>
10
+ Origen: <de dónde sale el dato: Nest, pool, caché, process>
11
+ Costo: <consultas o llamadas por carga de página>
12
+ ```
13
+
14
+ **Qué puede mostrar:** conteos, estados (`up`/`down`), versiones, uptime, tamaños de pool y latencias agregadas.
15
+ **Qué nunca muestra:** secretos o variables de entorno, SQL, mensajes de error de PostgreSQL o Redis, hosts o usuarios de BD, y datos de negocio (filas, montos, nombres de clientes). Si el humano pide algo de esta lista, detente: requiere una decisión en `harness/decisiones/`.
16
+
17
+ ## 1. Implementar
18
+
19
+ 1. Crea `src/common/monitor/widgets/<id>.widget.ts`: una clase `@Injectable()` que implementa `MonitorWidget` (`id`, `title`, `order`, `collect(): Promise<MonitorValue>`).
20
+ 2. `collect()` debe ser barato y de solo lectura: sin escrituras, sin SQL de negocio y sin depender de Redis. Si necesita la BD, usa una consulta técnica en un `*.sql.ts` y el repository de `common/`; si falla, deja que lance: el monitor lo muestra como "No disponible" y registra solo el tipo de error.
21
+ 3. Regístrala en `src/common/monitor/monitor.module.ts`, agregándola al arreglo `WIDGETS`. El multi-provider `MONITOR_WIDGETS` la recoge sola.
22
+ 4. `src/common/` no importa de `src/modules/`. Si el dato es de un módulo de negocio, no va al monitor base: regístralo como decisión.
23
+
24
+ ## 2. Tests
25
+
26
+ - `<id>.widget.spec.ts`: el valor y el desglose esperados, y que un error del origen se propaga (no se traga ni se formatea).
27
+ - Si cambia el HTML, ajusta `test/monitor.e2e-spec.ts` para que siga comprobando que no hay `<script>` y que un widget con error no rompe la página.
28
+
29
+ ## 3. Cierre
30
+
31
+ Agrega una entrada en `docs/EVOLUTIVO.md` (qué muestra ahora el monitor). Corre `./harness/init.sh` y revisa el checklist de `harness/playbooks/review.md` (un widget con datos sensibles es bloqueante).
@@ -0,0 +1,40 @@
1
+ # Playbook: revisar un cambio
2
+
3
+ Revisa el diff contra `AGENTS.md` y reporta cada hallazgo con archivo y línea, del más grave al menos grave.
4
+
5
+ ## Bloqueantes
6
+
7
+ - [ ] SQL fuera de `*.sql.ts`, o valores concatenados o interpolados en SQL
8
+ - [ ] `ORDER BY` o nombres de columna que no salen de una allowlist
9
+ - [ ] Cualquier escritura, DDL o migración
10
+ - [ ] Un endpoint nuevo sin guard JWT, o un `@Public()` fuera de `login`, `refresh` o `health`
11
+ - [ ] CORS abierto: `enableCors()` sin opciones, `origin: true`, `'*'`, reflejar el `Origin`, `credentials: true`, o `CORS_ORIGINS` con comodín
12
+ - [ ] El monitor sin `MonitorSessionGuard`, marcado `@Public()`, dentro de `/api/v1`, con `<script>`, o un widget que muestra secretos, SQL, errores crudos o datos de negocio
13
+ - [ ] Tokens, contraseñas o secretos en logs, respuestas, código o commits
14
+ - [ ] `pino-pretty` en `dependencies` (va en devDependencies) o `LOG_FORMAT=pretty` para producción
15
+ - [ ] Cambios en `.env`, o `.env.local` incluido en el commit
16
+ - [ ] `process.env` fuera de `src/config/` en código nuevo (salvo tests), o un secreto con valor por defecto en `configuration.ts`
17
+ - [ ] Una dependencia nueva sin aprobación
18
+ - [ ] La API falla si Redis está caído o deshabilitado
19
+ - [ ] Se editó la base del harness (`AGENTS.md`, puentes o playbooks) en lugar de registrar la decisión en `harness/decisiones/`
20
+ - [ ] El cambio contradice una decisión `Aceptada` de `harness/decisiones/`, o toma una decisión nueva sin registrarla
21
+ - [ ] `./harness/init.sh` termina en 1 (crítico, `--strict` o `required`), o el cambio agrega advertencias nuevas respecto a la línea base
22
+ - [ ] Se silenció un chequeo en `harness/arness.config.json` sin aprobación humana o sin motivo
23
+
24
+ ## Contrato y arquitectura
25
+
26
+ - [ ] Estructura del endpoint según §5, y regla de agrupación (más de 2 archivos del mismo tipo en una carpeta)
27
+ - [ ] Fronteras: `common/` no importa de `modules/` y ningún módulo importa de otro módulo o cliente
28
+ - [ ] El service depende de la interfaz del repository, no de `pg`
29
+ - [ ] Envelope `{ data, meta? }` y errores `{ error: {...} }` con el status correcto
30
+ - [ ] Si cambió `.columns.ts` o los filtros, `FRONT.md` está actualizado y la versión de la clave de caché subió
31
+
32
+ ## Completitud
33
+
34
+ - [ ] Un spec por archivo, incluidos los casos HIT/MISS/BYPASS
35
+ - [ ] Postman, `FRONT.md` y el README del módulo actualizados
36
+ - [ ] Entrada nueva al final de `docs/EVOLUTIVO.md` (sin reescribir las anteriores) y lo abierto en `docs/PENDIENTES.md`
37
+ - [ ] `docs/EVOLUTIVO.md` no contiene pendientes, bloqueos, stoppers ni riesgos
38
+ - [ ] Variables nuevas en `.env.example`, `.env.local`, la validación y `src/config/configuration.ts` (`AppConfig`)
39
+ - [ ] `./harness/init.sh` termina en 0 sin advertencias nuevas; las preexistentes se reportan, no se corrigen fuera de alcance
40
+ - [ ] Código en inglés, docs en español, commit con formato Conventional Commits
@@ -0,0 +1,104 @@
1
+ # Referencia: variables de entorno
2
+
3
+ Parte de la base del harness (AGENTS.md §6 a §10). Todas las variables:
4
+ - se declaran en `.env.example` con valores de ejemplo y en `.env.local` con valores locales;
5
+ - se validan en `src/config/` al arrancar.
6
+
7
+ `.env` nunca lo toca un agente. Si falta una variable requerida, la app falla al arrancar con un mensaje claro.
8
+
9
+ ## Dónde se leen las variables
10
+
11
+ `process.env` se lee **solo** en `src/config/configuration.ts` (plantilla `harness/skills/nest-base/templates/config/`). Ese archivo agrupa las variables por tema y convierte sus tipos; el resto del código recibe la configuración ya tipada.
12
+
13
+ ```typescript
14
+ // src/config/configuration.ts (extracto)
15
+ export function buildConfiguration(env: Env): AppConfig {
16
+ return {
17
+ app: { nodeEnv: text(env, 'NODE_ENV', 'development'), port: int(env, 'PORT', 3000) },
18
+ databases: parseDatabases(env), // DATABASES=MAIN,REPORTS → { MAIN: { host, port, ... }, REPORTS: {...} }
19
+ auth: { accessToken: required(env, 'ACCESS_TOKEN'), jwt: { accessSecret: required(env, 'JWT_ACCESS_SECRET'), ... } },
20
+ // cache, cors, monitor...
21
+ };
22
+ }
23
+ export default (): AppConfig => buildConfiguration(process.env);
24
+ ```
25
+
26
+ - **Registro:** `ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env.local', '.env'], validate, load: [configuration] })`. La validación (class-validator) revisa las variables crudas; `configuration.ts` solo agrupa y convierte.
27
+ - **Consumo:** `ConfigService<AppConfig, true>` con rutas tipadas, por ejemplo `config.get('cache.enabled', { infer: true })`. También en `main.ts` (`app.get(ConfigService)`): ni siquiera el puerto se lee de `process.env`.
28
+ - **Secretos sin valor por defecto** (`ACCESS_TOKEN`, `JWT_*_SECRET`, `DB_*_PASSWORD`): si faltan, la app no arranca. El error nombra la variable, nunca su valor. Los defaults son solo para valores no sensibles.
29
+ - **Tests:** los specs y `test/` pueden preparar `process.env`; `buildConfiguration(env)` es una función pura que se prueba con un objeto propio.
30
+ - **Verificación:** ESLint (`configBoundaries` de `eslint.boundaries.mjs`, regla `no-restricted-properties`) e `init.sh` (`env.process-env`, `env.configuration-file`, `env.eslint`).
31
+
32
+ **Agregar una variable** es, en el mismo cambio: `.env.example` + `.env.local` + validación + `configuration.ts` (y su tipo `AppConfig`) + esta referencia.
33
+
34
+ ## App
35
+
36
+ ```dotenv
37
+ NODE_ENV=development # development | test | production (default development)
38
+ PORT=3000
39
+ LOG_LEVEL=info # en desarrollo se recomienda debug
40
+ LOG_FORMAT=json # json (default, producción) | pretty (solo desarrollo local)
41
+ ```
42
+
43
+ - `LOG_FORMAT=pretty` requiere `pino-pretty` en devDependencies y no se acepta con `NODE_ENV=production`. `.env.local` usa `pretty`; `.env.example` documenta `json`.
44
+
45
+ ## Bases de datos (conexiones nombradas)
46
+
47
+ ```dotenv
48
+ DATABASES=MAIN,REPORTS # una sola BD: DATABASES=MAIN
49
+
50
+ DB_MAIN_HOST=localhost
51
+ DB_MAIN_PORT=5432
52
+ DB_MAIN_NAME=analytics
53
+ DB_MAIN_USER=readonly_user
54
+ DB_MAIN_PASSWORD=change-me
55
+ DB_MAIN_SSL=false
56
+ DB_MAIN_POOL_MAX=10
57
+ DB_MAIN_STATEMENT_TIMEOUT_MS=30000
58
+
59
+ DB_REPORTS_HOST=... # mismo bloque por cada nombre en DATABASES
60
+ ```
61
+
62
+ - `common/database` crea un `Pool` por nombre y lo expone con `getPoolToken('<NOMBRE>')`.
63
+ - Requeridas por conexión: `HOST`, `PORT`, `NAME`, `USER` y `PASSWORD`. `init.sh` las verifica en `.env.example`.
64
+
65
+ ## Caché
66
+
67
+ ```dotenv
68
+ CACHE_ENABLED=true
69
+ REDIS_URL=redis://localhost:6379
70
+ CACHE_DEFAULT_TTL_SECONDS=300
71
+ CACHE_KEY_PREFIX=arness
72
+ CACHE_OP_TIMEOUT_MS=200
73
+ ```
74
+
75
+ ## Autenticación
76
+
77
+ ```dotenv
78
+ ACCESS_TOKEN=example123 # contraseña compartida para /auth/login
79
+ JWT_ACCESS_SECRET=change-me
80
+ JWT_ACCESS_EXPIRES_IN=15m
81
+ JWT_REFRESH_SECRET=change-me-too # distinto de JWT_ACCESS_SECRET
82
+ JWT_REFRESH_EXPIRES_IN=7d
83
+ ```
84
+
85
+ ## CORS
86
+
87
+ ```dotenv
88
+ CORS_ORIGINS=http://localhost:4200,https://reportes.example.com # obligatoria; orígenes exactos separados por comas
89
+ CORS_MAX_AGE_SECONDS=600 # opcional; caché del preflight (0–86400)
90
+ ```
91
+
92
+ - `CORS_ORIGINS` es **obligatoria**: sin ella la app no arranca. No admite `*` ni comodines, solo orígenes (`esquema://host[:puerto]`, sin ruta), y exige `https://` salvo `localhost`, `127.0.0.1` y `[::1]`.
93
+ - Cada entorno define su lista (desarrollo, QA, producción). `init.sh` verifica que `.env.example` la define y que no lleva `*`.
94
+
95
+ ## Monitor
96
+
97
+ ```dotenv
98
+ MONITOR_ENABLED=false # true para habilitar GET /monitor; con false responde 404
99
+ MONITOR_SESSION_EXPIRES_IN=15m # duración de la cookie de sesión del monitor
100
+ ```
101
+
102
+ - Por defecto `false`. Habilítalo solo donde el equipo lo necesite; en producción requiere aprobación.
103
+ - El login usa `ACCESS_TOKEN` y la sesión se firma con `JWT_ACCESS_SECRET`: no hay secretos nuevos.
104
+ - Si existe `src/common/monitor/`, `init.sh` verifica que `.env.example` define ambas variables.
@@ -0,0 +1,138 @@
1
+ # Referencia: contrato HTTP
2
+
3
+ Parte de la base del harness (AGENTS.md §8, §9 y §10).
4
+
5
+ ## Respuesta exitosa
6
+
7
+ `meta` aparece **solo** cuando el endpoint pagina o filtra.
8
+
9
+ ```jsonc
10
+ // Sin paginación ni filtros
11
+ { "data": [ /* filas */ ] }
12
+
13
+ // Con paginación y/o filtros
14
+ {
15
+ "data": [ /* filas */ ],
16
+ "meta": {
17
+ "pagination": { "page": 1, "pageSize": 50, "total": 1234, "totalPages": 25 },
18
+ "filters": { "desde": "2026-01-01", "regional": "ANT" }
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Respuesta de error
24
+
25
+ Siempre con el status HTTP correcto. `details` es opcional.
26
+
27
+ ```json
28
+ {
29
+ "error": {
30
+ "code": "VALIDATION_ERROR",
31
+ "message": "El parámetro 'desde' debe ser una fecha ISO",
32
+ "details": [{ "field": "desde", "issue": "isDateString" }],
33
+ "requestId": "b1f2c3...",
34
+ "path": "/api/v1/cartera/resumen",
35
+ "timestamp": "2026-09-25T14:00:00.000Z"
36
+ }
37
+ }
38
+ ```
39
+
40
+ | Status | `code` | Origen típico |
41
+ |---|---|---|
42
+ | 400 | `VALIDATION_ERROR` | DTO inválido |
43
+ | 401 | `UNAUTHORIZED` / `TOKEN_EXPIRED` / `INVALID_CREDENTIALS` | guard o login |
44
+ | 403 | `FORBIDDEN` | — |
45
+ | 404 | `NOT_FOUND` | ruta o recurso |
46
+ | 503 | `DATABASE_UNAVAILABLE` | pg: conexión rechazada, `08*`, `57P01` |
47
+ | 504 | `QUERY_TIMEOUT` | pg: `57014` (statement_timeout) |
48
+ | 500 | `INTERNAL_ERROR` | cualquier otro; sin stack ni SQL |
49
+
50
+ ## Autenticación
51
+
52
+ | Endpoint | Body | Respuesta |
53
+ |---|---|---|
54
+ | `POST /api/v1/auth/login` | `{ "password": "..." }` | `{ data: { accessToken, refreshToken, expiresIn } }` |
55
+ | `POST /api/v1/auth/refresh` | `{ "refreshToken": "..." }` | `{ data: { accessToken, refreshToken, expiresIn } }` |
56
+
57
+ - `password` se compara con `ACCESS_TOKEN` en tiempo constante: `crypto.timingSafeEqual` sobre el sha256 de ambos valores.
58
+ - Los tokens usan secretos distintos; el claim `type` (`access` o `refresh`) se valida.
59
+ - Los clientes envían `Authorization: Bearer <accessToken>`. Ante `401 TOKEN_EXPIRED`, llaman a `/auth/refresh` y reintentan una vez.
60
+
61
+ ## CORS
62
+
63
+ Allowlist por entorno: solo los orígenes de `CORS_ORIGINS` reciben `Access-Control-Allow-Origin`. Implementación de referencia: `harness/skills/nest-base/templates/cors/cors.ts` → `src/config/cors.ts`.
64
+
65
+ | Opción de `app.enableCors()` | Valor |
66
+ |---|---|
67
+ | `origin` | función que acepta solo los orígenes exactos de `CORS_ORIGINS` (sin `Origin` → sin cabeceras CORS) |
68
+ | `methods` | `GET, POST, OPTIONS` |
69
+ | `allowedHeaders` | `Authorization, Content-Type, X-Request-Id` |
70
+ | `exposedHeaders` | `X-Cache, X-Request-Id` |
71
+ | `credentials` | `false` (la API usa `Bearer`, no cookies) |
72
+ | `maxAge` | `CORS_MAX_AGE_SECONDS` (default 600) |
73
+ | `optionsSuccessStatus` | `204` |
74
+
75
+ - **Validación al arrancar** (`src/config/`): `CORS_ORIGINS` es obligatoria; se rechazan `*` y comodines, valores que no son un origen (`esquema://host[:puerto]`, sin ruta) y `http://` fuera de `localhost`, `127.0.0.1` o `[::1]`.
76
+ - **Prohibido:** `enableCors()` sin opciones, `origin: true`, `origin: '*'`, reflejar el `Origin` recibido o `credentials: true`.
77
+ - **Tests:** `src/config/cors.spec.ts` (parseo y validación) y `test/cors.e2e-spec.ts`: preflight permitido → `204` con `Access-Control-Allow-Origin`, métodos, cabeceras y `max-age`; preflight de un origen no permitido → sin `Access-Control-Allow-Origin`.
78
+ - `init.sh` advierte si `.env.example` no define `CORS_ORIGINS` o lo define con `*`, y si `src/main.ts` no llama a `enableCors` o lo abre a cualquier origen.
79
+
80
+ ## Caché
81
+
82
+ - Header `X-Cache: HIT | MISS | BYPASS` en toda respuesta cacheable.
83
+ - Clave: `<prefix>:<cliente>:<modulo>:<endpoint>:v<version>:<hash-estable-de-filtros>`. El hash se calcula con sha256 sobre el JSON de los filtros con las claves ordenadas.
84
+
85
+ ## Logging
86
+
87
+ - JSON estructurado (`nestjs-pino`). `requestId` desde `X-Request-Id` (si es seguro: `[A-Za-z0-9._-]`, máximo 128) o UUID generado, devuelto en la respuesta.
88
+ - Por request: método, ruta, status, duración, resultado de caché y conexión de BD.
89
+ - Redact: `authorization`, cookies (`cookie` y `set-cookie`), `password`, `accessToken`, `refreshToken`, `*_SECRET` y `*_PASSWORD`.
90
+ - Plantilla: `harness/skills/nest-base/templates/logging/logger-options.ts` (`buildLoggerParams`, `parseLogFormat`, `REDACT_PATHS`).
91
+
92
+ ### Formato por entorno (`LOG_FORMAT`)
93
+
94
+ | Entorno | `LOG_FORMAT` | Ejemplo |
95
+ |---|---|---|
96
+ | Producción, QA, CI (y por defecto) | `json` | `{"level":30,"requestId":"a1b2c3","req":{"method":"GET","url":"/api/v1/cartera/resumen"},"res":{"statusCode":200},"responseTime":42,"msg":"GET /api/v1/cartera/resumen 200 42ms"}` |
97
+ | Desarrollo local | `pretty` | `10:21:03 INFO: [a1b2c3] GET /api/v1/cartera/resumen 200 42ms {"cache":"HIT"}` |
98
+
99
+ - Ambos formatos llevan los mismos campos y la **misma redacción**: el formato legible nunca muestra secretos.
100
+ - `pretty` usa `pino-pretty`, que es **devDependency**: la imagen de producción no lo incluye. Si se pide `pretty` y no está instalado, el logger vuelve a JSON.
101
+ - La validación rechaza `LOG_FORMAT=pretty` con `NODE_ENV=production`, y cualquier valor distinto de `json` o `pretty`.
102
+ - Los logs no incluyen el SQL ejecutado ni sus parámetros en ningún formato.
103
+
104
+ ## Health
105
+
106
+ `GET /health` es público y usa terminus. Indicadores:
107
+ - `db_<nombre en minúsculas>`, uno por conexión;
108
+ - `cache`, con estado `up`, `down` o `disabled`.
109
+
110
+ | Situación | Respuesta |
111
+ |---|---|
112
+ | Todas las BDs arriba | `200` |
113
+ | Alguna BD caída | `503` |
114
+ | Redis caído o deshabilitado | sigue en `200`; `cache: down / disabled` |
115
+
116
+ `init.sh` consume este formato para informar qué base está caída.
117
+
118
+ ## Monitor
119
+
120
+ Tablero de estado para el equipo, **fuera de `/api/v1`** y apagado por defecto. Implementación de referencia: `harness/skills/nest-base/templates/monitor/` → `src/common/monitor/`.
121
+
122
+ | Ruta | Sesión | Respuesta |
123
+ |---|---|---|
124
+ | `GET /monitor` | cookie | HTML con los widgets; sin sesión → `303` a `/monitor/login` |
125
+ | `GET /monitor/login` | — | formulario HTML (`password`) |
126
+ | `POST /monitor/login` | — | correcta → `303` a `/monitor` y `Set-Cookie`; incorrecta → `401` con el formulario y sin cookie |
127
+ | `POST /monitor/logout` | — | borra la cookie (`Max-Age=0`) y `303` a `/monitor/login` |
128
+
129
+ - **Activación:** `MONITOR_ENABLED=true`. Con cualquier otro valor, todas las rutas responden `404`.
130
+ - **Render:** `react-dom/server` (`renderToStaticMarkup`) con `createElement`, sin JSX, sin hidratación, sin bundler y sin `<script>`. Estilos inline.
131
+ - **Login:** la contraseña se compara con `ACCESS_TOKEN` en tiempo constante, igual que `/auth/login`. Al acertar se emite un JWT con `type: "monitor"` (firmado con `JWT_ACCESS_SECRET`, dura `MONITOR_SESSION_EXPIRES_IN`) en la cookie `arness_monitor`: `HttpOnly; Secure; SameSite=Strict; Path=/monitor; Max-Age=<duración>`. Un access token (`type: "access"`) no sirve como cookie ni la cookie sirve como `Bearer`.
132
+ - **Guards:** el controller lleva `@MonitorRoute()`, que `JwtAuthGuard` omite, y `MonitorSessionGuard`, que exige la cookie salvo en los handlers `@MonitorLogin()` (formulario, login y logout). No es `@Public()`.
133
+ - **Rate limiting:** cuando el harness incorpore límite de intentos para `/auth/login`, el login del monitor lo comparte.
134
+ - **Cabeceras en toda respuesta:** `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`, `Cache-Control: no-store`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff` y `Referrer-Policy: no-referrer`.
135
+ - **Widgets:** `MonitorWidget { id; title; order; collect(): Promise<MonitorValue> }`, registrados en el multi-provider `MONITOR_WIDGETS` de `MonitorModule`. Un widget que lanza o tarda más de 2 s se muestra como "No disponible"; el log registra solo el tipo de error, nunca su mensaje.
136
+ - **Widget base `endpoints-total`:** cuenta las rutas HTTP con `DiscoveryService` y `MetadataScanner`, excluye `monitor`, `health` y `auth`, y desglosa por el primer segmento de la ruta (el módulo).
137
+ - **Nunca se muestran:** secretos, variables de entorno, SQL, errores de PostgreSQL ni datos de negocio (filas, montos, nombres de clientes). Solo conteos y estados técnicos.
138
+ - **Tests:** unit de servicio, página, sesión, guard y widget; e2e `test/monitor.e2e-spec.ts` (redirección sin cookie, `401` sin cookie con contraseña incorrecta, login y conteo correcto con controllers de prueba, widget con error que no rompe la página, sin `<script>`, `404` con `MONITOR_ENABLED=false`).
@@ -0,0 +1,107 @@
1
+ {
2
+ "schema": 1,
3
+ "description": "Novedades adoptables por versión del harness. Las lee `arness doctor` (qué falta adoptar) y `arness sync` (qué trae la versión). Tipos: seguridad | recomendada | opcional. Una novedad está adoptada cuando existen sus `paths` y init.sh no reporta ninguno de sus `checks`.",
4
+ "novedades": [
5
+ {
6
+ "id": "cors-allowlist",
7
+ "since": "0.2.0",
8
+ "type": "seguridad",
9
+ "title": "CORS con allowlist por entorno",
10
+ "why": "Sin allowlist, cualquier sitio web puede llamar la API desde el navegador de un usuario autenticado.",
11
+ "checks": ["security.cors-env", "security.cors", "security.cors-open"],
12
+ "paths": [],
13
+ "approval": null,
14
+ "steps": [
15
+ "Copia harness/skills/nest-base/templates/cors/ (cors.ts y su spec) a src/config/ y valida CORS_ORIGINS al arrancar.",
16
+ "En src/main.ts usa app.enableCors(buildCorsOptions(...)) con los valores validados.",
17
+ "Agrega CORS_ORIGINS y CORS_MAX_AGE_SECONDS a .env.example y .env.local, y define los orígenes de cada entorno antes de desplegar."
18
+ ],
19
+ "adr": "0006",
20
+ "reference": "harness/reference/contrato-http.md (CORS)"
21
+ },
22
+ {
23
+ "id": "eslint-boundaries",
24
+ "since": "0.2.0",
25
+ "type": "recomendada",
26
+ "title": "Fronteras de arquitectura verificadas por ESLint",
27
+ "why": "Evita que common/ dependa de los módulos y que un módulo o cliente importe de otro.",
28
+ "checks": ["architecture.eslint-boundaries"],
29
+ "paths": ["eslint.boundaries.mjs"],
30
+ "approval": null,
31
+ "steps": [
32
+ "Copia harness/skills/nest-base/templates/eslint.boundaries.mjs a la raíz.",
33
+ "Impórtala en eslint.config.mjs y agrega ...architectureBoundaries.",
34
+ "Si aparecen imports entre módulos, mueve lo compartido a src/common/ cuando toques ese módulo."
35
+ ],
36
+ "adr": "0007",
37
+ "reference": "harness/AGENTS.md §4"
38
+ },
39
+ {
40
+ "id": "monitor",
41
+ "since": "0.2.0",
42
+ "type": "opcional",
43
+ "title": "Tablero monitor (GET /monitor)",
44
+ "why": "Muestra el estado técnico del backend (endpoints, salud) sin exponer datos de negocio.",
45
+ "checks": ["security.monitor-env", "security.monitor-guard"],
46
+ "paths": ["src/common/monitor"],
47
+ "approval": "react y react-dom (aprobadas en AGENTS.md §2 solo para el monitor); registra la adopción en harness/decisiones/ si el proyecto aún no las usa.",
48
+ "steps": [
49
+ "Copia harness/skills/nest-base/templates/monitor/ a src/common/monitor/ y registra MonitorModule.",
50
+ "Excluye monitor del prefijo global y haz que JwtAuthGuard omita IS_MONITOR_ROUTE_KEY.",
51
+ "Agrega MONITOR_ENABLED=false y MONITOR_SESSION_EXPIRES_IN=15m a .env.example y .env.local."
52
+ ],
53
+ "adr": "0008",
54
+ "reference": "harness/playbooks/new-monitor-widget.md"
55
+ },
56
+ {
57
+ "id": "env-config",
58
+ "since": "0.2.0",
59
+ "type": "recomendada",
60
+ "title": "Configuración centralizada en src/config/configuration.ts",
61
+ "why": "Toda la configuración tipada en un solo lugar, sin process.env disperso ni secretos con valor por defecto.",
62
+ "checks": ["env.configuration-file", "env.process-env", "env.eslint"],
63
+ "paths": [],
64
+ "approval": null,
65
+ "steps": [
66
+ "Copia harness/skills/nest-base/templates/config/ (configuration.ts y su spec) a src/config/ y regístrala con ConfigModule.forRoot({ load: [configuration] }).",
67
+ "Cambia, módulo por módulo, cada process.env.X por ConfigService<AppConfig, true>.",
68
+ "Actualiza eslint.boundaries.mjs desde la plantilla y agrega ...configBoundaries en eslint.config.mjs."
69
+ ],
70
+ "adr": "0009",
71
+ "reference": "harness/reference/configuracion.md (Dónde se leen las variables)"
72
+ },
73
+ {
74
+ "id": "log-format",
75
+ "since": "0.2.0",
76
+ "type": "opcional",
77
+ "title": "Logs legibles en desarrollo (LOG_FORMAT=pretty)",
78
+ "why": "Una línea legible por petición en la terminal local; producción sigue en JSON.",
79
+ "checks": ["logging.format-env"],
80
+ "paths": [],
81
+ "approval": "pino-pretty, solo como devDependency (aprobada en AGENTS.md §2).",
82
+ "steps": [
83
+ "pnpm add -D pino-pretty",
84
+ "Copia harness/skills/nest-base/templates/logging/ a src/common/logging/ y usa buildLoggerParams en LoggerModule.forRootAsync.",
85
+ "Agrega LOG_FORMAT=json a .env.example y LOG_FORMAT=pretty a .env.local."
86
+ ],
87
+ "adr": "0010",
88
+ "reference": "harness/reference/contrato-http.md (Logging)"
89
+ },
90
+ {
91
+ "id": "project-docs",
92
+ "since": "0.2.0",
93
+ "type": "recomendada",
94
+ "title": "Documentación del proyecto en docs/ (técnico, evolutivo y pendientes)",
95
+ "why": "Negocio ve lo entregado en EVOLUTIVO.md y lo abierto queda en PENDIENTES.md, sin mezclarse.",
96
+ "checks": ["docs.files"],
97
+ "paths": [],
98
+ "approval": null,
99
+ "steps": [
100
+ "Copia harness/skills/nest-base/templates/docs/ a docs/.",
101
+ "Completa docs/TECNICO.md con lo que ya existe y, si quieres, resume lo entregado en docs/EVOLUTIVO.md."
102
+ ],
103
+ "adr": "0011",
104
+ "reference": "harness/AGENTS.md §12"
105
+ }
106
+ ]
107
+ }
@@ -0,0 +1,50 @@
1
+ # Progress
2
+
3
+ > Estado compartido del trabajo en curso. **Solo lo escribe el `lider`**; el `implementador` y el `revisor` lo leen.
4
+ > Aquí van resúmenes, no diffs ni logs completos. Mantenlo por debajo de ~150 líneas: las tareas cerradas se reducen a una línea en la tabla.
5
+
6
+ ## Objetivo
7
+
8
+ <Qué pidió el humano, en una o dos frases.>
9
+
10
+ ## Estado
11
+
12
+ - **Estado:** EN CURSO
13
+ - **Actualizado:** AAAA-MM-DD HH:MM
14
+ - **init.sh (última ejecución):** <fecha> · <exit 0 + N advertencias | exit 1: resumen de fallos>
15
+ - **Línea base de advertencias (inicio de la sesión):** <ids de init.sh --json, o "ninguna">
16
+
17
+ ## Contrato confirmado
18
+
19
+ <Lo confirmado con el humano (AGENTS.md §15.1): cliente/módulo/segmento, conexión y origen, columnas, filtros, paginación, TTL. Escribe "—" si no aplica.>
20
+
21
+ ## Decisiones de esta sesión
22
+
23
+ - <Enlaces a harness/decisiones/NNNN-*.md aceptadas o propuestas, y acuerdos del humano que aún no son decisión formal.>
24
+
25
+ ## Tareas
26
+
27
+ | ID | Tarea | Responsable | Estado | Ronda | Veredicto |
28
+ |---|---|---|---|---|---|
29
+ | T-001 | <título> | implementador | PENDIENTE | 0 | — |
30
+
31
+ Estados: `PENDIENTE` · `EN CURSO` · `EN REVISIÓN` · `CAMBIOS REQUERIDOS` · `APROBADA` · `BLOQUEADA`.
32
+
33
+ ## Detalle por tarea
34
+
35
+ ### T-001 · <título>
36
+
37
+ - **Encargo:** objetivo, playbook, archivos y criterios de aceptación.
38
+ - **Fuera de alcance:** <...>
39
+ - **Última entrega:** <resumen del reporte del implementador>
40
+ - **Último veredicto:** <resumen de los hallazgos del revisor, con archivo:línea>
41
+
42
+ ## Bloqueos y preguntas abiertas
43
+
44
+ - <Pregunta o bloqueo · a quién le toca responder>
45
+
46
+ ## Bitácora
47
+
48
+ <Append-only: una línea por evento.>
49
+
50
+ - AAAA-MM-DD HH:MM · lider · <evento>