@ronaldjdevfs/forge 1.3.3 → 1.3.5

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <img src="favicon.svg" alt="Forge Logo" width="100" height="100">
2
2
 
3
- > **v1.3.2** — Domain Subdirectory Structure & Port Support
3
+ > **v1.3.5** — R13 Platform Domain Guard & Legacy Cleanup
4
4
 
5
5
  ## Forge — Backend Architecture Operating System
6
6
 
@@ -29,10 +29,10 @@ Los proyectos backend degeneran en código acoplado porque la infraestructura t
29
29
  |-----------|---------|-------------|
30
30
  | **Proyecto nuevo** | `forge` | Inicializa platform/features/shared/infra, detecta stack, crea `ARCHITECTURE.md` |
31
31
  | **Crear un nuevo dominio** | `cast` | Genera un feature completo desde cero (verifica platform/shared/infra primero) |
32
- | **Auditar arquitectura** | `inspect` | Evaluación completa 110pts → 0-100 con ownership, platform y grafo |
32
+ | **Auditar arquitectura** | `inspect` | Evaluación completa 180pts → 0-100 con ownership, platform, grafo e import conventions |
33
33
  | **Migrar código legacy** | `relocate` | Traslada código a platform/, shared/, infra/ o features/ |
34
34
  | **Refactorizar** | `reforge` | Reestructura features o componentes multi-capa |
35
- | **Validar reglas** | `quench` | Verifica 9 reglas arquitectónicas (R1-R9) |
35
+ | **Validar reglas** | `quench` | Verifica 12 reglas arquitectónicas (R1-R12) |
36
36
  | **Endurecer DI** | `temper` | Aplica inyección por constructor, elimina service locators |
37
37
  | **Analizar dependencias** | `chain` | Grafo multi-capa (platform, features, shared, infra), orden topológico, ciclos |
38
38
  | **Documentar** | `inscribe` | Genera/actualiza `ARCHITECTURE.md` con métricas, ownership y violaciones |
@@ -72,16 +72,20 @@ src/features/<name>/
72
72
 
73
73
  ### `inspect` — Auditoría arquitectónica
74
74
 
75
- Evalúa 6 categorías contra un máximo de 110 puntos (normalizado a 0-100):
75
+ Evalúa 10 categorías contra un máximo de 180 puntos (normalizado a 0-100):
76
76
 
77
77
  | Categoría | Puntos | Qué mide |
78
78
  |-----------|--------|----------|
79
- | Structure | 20 | Organización de platform, features, shared, infra |
80
- | Layers | 20 | Aislamiento entre capas, imports prohibidos |
79
+ | Structure | 30 | Organización de platform, features, shared, infra |
80
+ | Layers | 25 | Aislamiento entre capas, imports prohibidos |
81
+ | Decorators | 20 | Decoradores @injectable()/@inject() en use cases, controllers, repos |
81
82
  | Ownership | 20 | Huérfanos, duplicados, mal ubicados |
82
83
  | Platform | 15 | Completitud del backbone técnico (config, server, logger, di, etc.) |
83
84
  | Dependencies | 15 | Dirección de dependencias, ciclos, edges inválidos |
84
85
  | Graph | 20 | Salud del grafo arquitectónico, risk score |
86
+ | Custom Rules | 5 | Reglas personalizadas desde `.forge/rules.json` |
87
+ | Naming | 10 | Convenciones de nomenclatura PascalCase/kebab |
88
+ | Import Conventions | 20 | R10-R12: bare specifiers, extensión .ts, bootstrap.di.js |
85
89
 
86
90
  **Resultado**: Score 0-100 con grado A-F y severidades por cada violación.
87
91
 
@@ -116,6 +120,10 @@ Ejecuta 9 reglas arquitectónicas (R1-R9) con severidad:
116
120
  | R7 | `infra → feature` (prohibido) | WARNING |
117
121
  | R8 | Cross-feature direct imports | ERROR |
118
122
  | R9 | Ciclos de dependencia | ERROR |
123
+ | R10 | Bare specifiers en imports locales | ERROR |
124
+ | R11 | Extensión `.ts` en imports (debe ser `.js`) | ERROR |
125
+ | R12 | Import a `bootstrap.di.js` (no existe) | CRITICAL |
126
+ | R12b | `registerSingleton` con model() de Mongoose | CRITICAL |
119
127
 
120
128
  ### `temper` — Endurecimiento de DI
121
129
 
@@ -311,8 +319,8 @@ Ver `reference/patterns.md` para el detalle completo.
311
319
  - **4 dominios arquitectónicos**: Platform (backbone), Features (negocio), Shared (código puro), Infra (implementaciones)
312
320
  - **Architecture graph como fuente de verdad**: 6 tipos de nodo (platform, feature, shared, infra, domain, adapter), 9 reglas (R1-R9), risk score y dependency health
313
321
  - **Ownership automático**: Detección de huérfanos, duplicados, componentes mal ubicados y sugerencias de reubicación
314
- - **Scoring arquitectónico**: 110 puntos en 6 categorías, normalizado a 0-100 con grado A-F
315
- - **5 perfiles tecnológicos predefinidos**: Express + MongoDB, Express + PostgreSQL, Express + Prisma, Fastify + Prisma, NestJS + Prisma
322
+ - **Scoring arquitectónico**: 180 puntos en 10 categorías, normalizado a 0-100 con grado A-F
323
+ - **10 perfiles tecnológicos predefinidos**: Express + MongoDB, Express + PostgreSQL, Express + Prisma, Express + Drizzle, Fastify + MongoDB, Fastify + PostgreSQL, Fastify + Prisma, NestJS + MongoDB, NestJS + PostgreSQL, NestJS + Prisma
316
324
  - **Boot sequence obligatoria**: 9 pasos que garantizan contexto completo antes de cualquier acción
317
325
  - **Documentación automática**: `ARCHITECTURE.md` vivo que se actualiza tras cada operación
318
326
  - **Sin dependencias runtime**: Solo Node ≥ 18, todo corre con scripts ESM propios
@@ -358,7 +366,7 @@ Donde vive toda la inteligencia arquitectónica:
358
366
  | `scripts/profile.mjs` | Matchea stack contra perfiles conocidos o sintetiza uno genérico |
359
367
  | `scripts/graph.mjs` | Grafo completo: 6 tipos de nodo, 4 capas, 9 reglas (R1-R9), risk score, dependency health |
360
368
  | `scripts/chain.mjs` | Grafo multi-capa (platform, features, shared, infra) con orden topológico |
361
- | `scripts/detect.mjs` | Validación de reglas R1-R9 con inline ignores y `--fix` |
369
+ | `scripts/detect.mjs` | Validación de reglas R1-R12 con inline ignores, `--fix` e import conventions |
362
370
  | `scripts/inspect.mjs` | Orquesta auditoría completa con reporte coloreado |
363
371
  | `scripts/architecture.mjs` | Genera/actualiza `ARCHITECTURE.md` vivo |
364
372
  | `scripts/bootstrap.mjs` | Inicializa platform/shared/infra según perfil (interno) |
@@ -385,7 +393,7 @@ Donde vive toda la inteligencia arquitectónica:
385
393
  | `reference/assay.md` | Documentación del comando assay |
386
394
  | `reference/hooks.md` | Documentación del sistema de hooks |
387
395
  | `profiles/` | 10 perfiles tecnológicos detallados |
388
- | `templates/feature/` | 17 templates TypeScript para features |
396
+ | `templates/feature/` | 19 templates TypeScript para features |
389
397
  | `templates/platform/` | 6 templates para componentes de platform |
390
398
  | `templates/shared/` | 4 templates para shared (errors, contracts, types, utils) |
391
399
  | `templates/infra/` | 4 templates para infra (prisma, mongodb, redis, mail) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronaldjdevfs/forge",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Forge — Architecture Operating System for backend systems. Arquitectura Hexagonal, DDD pragmático y vertical slices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -150,6 +150,17 @@ El boot usa caché de `.forge/cache/`. Si los archivos `src/` no cambiaron, los
150
150
  - Si `ARCHITECTURE.md` está desactualizado (fecha de auditoría > 7 días), sugerir `forge inscribe`.
151
151
  - Todos los resultados se muestran con severidades: `[CRITICAL]`, `[ERROR]`, `[WARNING]`, `[INFO]`, `[SUGGESTION]`.
152
152
 
153
+ ### ⚠️ Regla de Platform: Sin lógica de dominio
154
+
155
+ Cuando `reforge` o `relocate` operen sobre `src/platform/`, verificar que los archivos movidos/creados no contengan lógica de dominio:
156
+
157
+ - **No** mover entidades, value objects, casos de uso, mappers de dominio, schemas de entidades, repositorios de dominio a `platform/`
158
+ - **No** crear archivos con sufijos `.entity.ts`, `.uc.ts`, `.mapper.ts`, `.repository.ts` (domain), `.port.ts` dentro de `platform/`
159
+ - **No** importar desde `features/` dentro de `platform/` (viola R2)
160
+ - Si un componente tiene imports hacia `domain/` o `features/`, pertenece a un feature, no a platform
161
+
162
+ Platform solo acepta: config, database, http, server, logger, cache, security, events, scheduler, observability, di.
163
+
153
164
  ### Inline Ignores
154
165
 
155
166
  Forge soporta comentarios inline para excepcionar violaciones línea por línea:
@@ -143,6 +143,49 @@ Usar el perfil detectado para determinar:
143
143
  - Convenciones de imports (rutas relativas vs alias)
144
144
  - Componentes de platform a usar (config, logger, http, database)
145
145
 
146
+ ## ⚠️ Post-Cast: Entity Discovery
147
+
148
+ Antes de crear `<Name>.entity.ts`, verificar si la entidad ya existe como entidad compartida:
149
+
150
+ 1. **Buscar en `src/platform/domain/entities/<Name>.ts`** — si existe, NO crear entidad local
151
+ 2. **Si es compartida**: usar `@/domain/entities/<Name>.js` en vez de path relativo en todos los templates
152
+ 3. **Verificar también** `src/shared/contracts/` por interfaces/DTOs existentes
153
+
154
+ Regla: si la entidad vive fuera del feature, todos los imports deben usar path alias `@/domain/`, no `../../`.
155
+
156
+ ## ⚠️ Post-Cast: DI Wiring
157
+
158
+ Después de crear los archivos del feature, generar `di.ts` siguiendo el template `templates/feature/di.ts.md`:
159
+
160
+ 1. **Feature con DI propia**: crear `src/features/<name>/di.ts` usando el template
161
+ 2. **Feature SIN DI propia**: si ya existe `src/platform/setting/dependencies/<name>.di.js`, los controllers deben importar desde allí en vez de `bootstrap.di.js`
162
+ 3. **Controllers**: asegurar que el import en el controller apunte a `@/setting/dependencies/<name>.di.js` o `./di.js`, NUNCA a `bootstrap.di.js`
163
+ 4. **Mongoose model()**: si el schema exporta `export default model()` (objeto, no clase), el DI debe usar `container.register(..., { useValue: ... })`, NO `registerSingleton`
164
+
165
+ ## ⚠️ Post-Cast: Tests
166
+
167
+ Después del scaffold, generar tests unitarios para cada use case siguiendo `templates/feature/test.ts.md`:
168
+
169
+ 1. Crear `src/features/<name>/__tests__/Create<Name>.test.ts`
170
+ 2. Usar `node:test` (sin dependencias externas)
171
+ 3. Convenciones de test:
172
+ - Extension `.js` en imports (no `.ts`)
173
+ - `as const` para literales de union types: `status: "activo" as const`
174
+ - `result!` (non-null assertion) cuando execute() retorna `T | null`
175
+ - `(result as any)._id` si `_id` no existe en el tipo de dominio
176
+
177
+ ## ⚠️ Post-Cast: Import Validation Checklist
178
+
179
+ Antes de dar por terminado el feature, verificar CADA archivo generado:
180
+
181
+ - [ ] Todos los imports locales usan prefijo `./` o `../` — sin bare specifiers (`import X from "domain/..."` ❌)
182
+ - [ ] Todos los imports tienen extensión `.js` — sin extensión `.ts`
183
+ - [ ] Entidades compartidas usan `@/domain/` — sin paths relativos rotos
184
+ - [ ] Controllers importan desde `di.ts` o `@/setting/dependencies/` — no desde `bootstrap.di.js`
185
+ - [ ] Nombres de método del controller coinciden con los de la ruta (ej: `createHandler` en controller → `controller.createHandler` en routes)
186
+ - [ ] DI usa `register({ useValue })` para modelos Mongoose — no `registerSingleton`
187
+ - [ ] Tests: `.js` extension, `as const`, `!`, `as any` para _id
188
+
146
189
  ## Post-creación
147
190
 
148
191
  - `forge quench` — verificar que no hay violaciones
@@ -150,11 +150,70 @@ REST prefiere `/api/v1/resources`. GraphQL schema en `adapters/in/graphql/` dent
150
150
 
151
151
  ---
152
152
 
153
+ ## Import Conventions (OBLIGATORIO)
154
+
155
+ Todo import generado por Forge debe cumplir estas reglas. Violaciones = ERROR en forge quench:
156
+
157
+ ### 1. Prefijo relativo obligatorio
158
+ Los imports locales SIEMPRE deben usar prefijo `./` o `../`. Prohibido el bare specifier:
159
+ ```ts
160
+ // ✅ Correcto
161
+ import { X } from "./foo.js";
162
+ import { Y } from "../../bar.js";
163
+
164
+ // ❌ Incorrecto (bare specifier — se resuelve contra node_modules)
165
+ import { X } from "domain/entities/Task.js";
166
+ import { Y } from "domain/repositories/IRepo.js";
167
+ ```
168
+
169
+ ### 2. Extensión .js obligatoria
170
+ Con `moduleResolution: "nodenext"`, todos los imports locales requieren extensión `.js`:
171
+ ```ts
172
+ // ✅ Correcto
173
+ import { X } from "./foo.js";
174
+
175
+ // ❌ Incorrecto
176
+ import { X } from "./foo.ts";
177
+ import { X } from "./foo";
178
+ ```
179
+
180
+ ### 3. Path alias para cross-layer
181
+ Usar path alias `@/` para cruzar capas, no paths relativos largos:
182
+ | Capa | Alias | Ejemplo |
183
+ |------|-------|---------|
184
+ | Platform → cualquiera | `@/platform/` | `@/platform/config/App.config.js` |
185
+ | Shared → cualquiera | `@/shared/` | `@/shared/errors/NotFoundError.js` |
186
+ | Infra → cualquiera | `@/infra/` | `@/infra/mongodb/Mongo.config.js` |
187
+ | **Entities compartidas** | **`@/domain/`** | **`@/domain/entities/Task.js`** |
188
+
189
+ ```ts
190
+ // ✅ Correcto — path alias para entidad compartida
191
+ import { Task } from "@/domain/entities/Task.js";
192
+
193
+ // ❌ Incorrecto — path relativo largo que no resuelve
194
+ import { Task } from "../../../../domain/entities/Task.js";
195
+ ```
196
+
197
+ ### 4. Entity Discovery — compartida vs local
198
+ - Si la entidad vive en `src/features/<feature>/domain/entities/` → import relativo (`../../domain/entities/`)
199
+ - Si la entidad vive en `src/platform/domain/entities/` → path alias (`@/domain/entities/`)
200
+ - Verificar existencia ANTES de generar el import
201
+
202
+ ### 5. Controllers y DI
203
+ - Controllers importan desde `./di.js` (feature con DI propia) o `@/setting/dependencies/<name>.di.js` (feature sin DI propia)
204
+ - Prohibido importar desde `bootstrap.di.js` — ese archivo no existe en la arquitectura actual
205
+
206
+ ### 6. Tests
207
+ - Todos los imports con extensión `.js` (nunca `.ts`)
208
+ - Usar `as const` para literales de union types
209
+ - Usar `result!` para valores posiblemente null
210
+ - Usar `(result as any)._id` si `_id` no está en el tipo
211
+
153
212
  ## Export Conventions
154
213
 
155
214
  - Cada directorio expone un `index.ts` barrel con named exports
156
215
  - Usar `export * from "./<Name>.<artifact>.js"` en barrels
157
216
  - Preferir `export function` / `export class` sobre `export default`
158
217
  - Imports relativos dentro del mismo feature (`../../domain/`)
159
- - Path alias para cross-layer: `@/platform/`, `@/shared/`, `@/infra/`
218
+ - Path alias para cross-layer: `@/platform/`, `@/shared/`, `@/infra/`, `@/domain/`
160
219
  - Extension `.js` en imports (ESM compat): `import { X } from "./foo.js"`
@@ -28,6 +28,8 @@
28
28
 
29
29
  12. **Cuatro dominios arquitectónicos con ownership estricto** — Todo backend se modela en cuatro capas: Platform (backbone técnico), Features (negocio), Shared (código puro reutilizable) e Infrastructure (implementaciones concretas). Cada componente tiene un único propietario arquitectónico. Los huérfanos, duplicados y componentes mal ubicados se detectan automáticamente. Las reglas de dependencia entre capas son obligatorias: `feature → platform → infra`, `feature → shared`, `adapter → infra`. Cualquier violación es una degradación arquitectónica.
30
30
 
31
+ **⚠️ Platform es exclusivamente backbone técnico.** Nunca debe contener entidades de dominio, casos de uso, mappers, repositorios de dominio, schemas de entidades, DTOs de negocio ni ninguna lógica con reglas de negocio. Si un archivo en `platform/` tiene sufijos `.entity.ts`, `.uc.ts`, `.mapper.ts`, `.port.ts` o importa desde `features/`, está mal ubicado. Esa lógica pertenece a `src/features/<name>/`. Violar esto introduce acoplamiento `platform → feature` (R2) y contamina el backbone técnico con lógica de dominio (R13).
32
+
31
33
  13. **Errores tipados en el dominio** — Los errores de dominio son clases explícitas, no `throw Error()` genéricos. Viven en `shared/errors/` si son transversales o en `domain/` del feature si son específicos. Los adapters HTTP traducen errores de dominio a códigos HTTP. La capa de aplicación nunca sabe qué códigos HTTP existen. Ver `reference/errors.md`.
32
34
 
33
35
  14. **Tests como ciudadanos de primera clase** — El dominio y los casos de uso se testean con unit tests sin infraestructura (mocks en las interfaces). Los adapters se testean con integration tests contra infraestructura real. La pirámide de tests es 70% unit / 20% integration / 10% e2e. Sin coverage de use cases no hay aprobación arquitectónica. Ver `reference/testing-patterns.md`.
@@ -67,9 +67,15 @@ $ forge reforge src/features/users/domain/userEntity.ts
67
67
  - Naming violations (si no se corrigieron en paso 5)
68
68
  - Warnings
69
69
  7. Ejecutar cambios en el feature o componente
70
- 8. Verificar con `forge rollback verify` si el score empeora, restaurar
71
- 9. Ejecutar `forge quench` para verificar
72
- 10. Actualizar `ARCHITECTURE.md`
70
+ 8. **Eliminar estructura legacy** después de migrar/refactorizar, limpiar todo el código fuente legacy que ya no tenga referencias activas:
71
+ - Archivos originales en ubicaciones legacy (`src/domain/`, `src/application/`, `src/adapters/`, etc.)
72
+ - Barrel files (`index.ts`) que exportaban exclusivamente código legacy
73
+ - Directorios vacíos que quedaron huérfanos tras la migración
74
+ - No dejar imports rotos ni archivos sin dueño
75
+ 9. Verificar con `forge rollback verify` — si el score empeora, restaurar
76
+ 10. Ejecutar `forge quench` para verificar 0 violaciones
77
+ 11. Ejecutar `forge armorer` — confirmar ownership saludable (0 huérfanos, 0 duplicados)
78
+ 12. Actualizar `ARCHITECTURE.md`
73
79
 
74
80
  ### Flags
75
81
 
@@ -88,11 +94,27 @@ forge rollback list
88
94
  forge rollback restore <id>
89
95
  ```
90
96
 
97
+ ## ⚠️ Regla crítica: Platform solo acepta backbone técnico
98
+
99
+ **Platform NO debe contener lógica de dominio.** Al refactorizar hacia `src/platform/`, verificar que el componente sea exclusivamente técnico:
100
+
101
+ | ✅ Permitido en Platform | ❌ Prohibido en Platform |
102
+ |---|---|
103
+ | config, server, logger, DI | Entidades de dominio |
104
+ | http, middleware, router | Casos de uso |
105
+ | cache, security, events | Repositorios (domain) |
106
+ | database, scheduler, observability | Mappers de dominio |
107
+ | Conexiones, clientes de infra | Schemas de entidades |
108
+ | Tokens de DI, contenedores | DTOs de negocio |
109
+ | Health checks, métricas | Lógica de reglas de negocio |
110
+
111
+ Si durante la refactorización un componente tiene lógica de dominio (entidades, `if/switch` con reglas de negocio, casos de uso), **debe ir a `src/features/<name>/`**, no a `platform/`. Violar esto introduce acoplamiento `platform → feature` (R2).
112
+
91
113
  ## Refactorización multi-capa
92
114
 
93
115
  Reforge ahora considera las cuatro capas arquitectónicas:
94
116
 
95
- - **Platform**: Mover componentes técnicos sueltos a `src/platform/`
117
+ - **Platform**: Mover componentes técnicos sueltos a `src/platform/` (nunca lógica de dominio)
96
118
  - **Features**: Reestructurar features con violaciones
97
119
  - **Shared**: Extraer código duplicado a `src/shared/`
98
120
  - **Infra**: Organizar implementaciones concretas en `src/infra/`
@@ -25,10 +25,16 @@ También puede migrar componentes legacy a los layers Platform, Shared o Infrast
25
25
  3. Crear la estructura target según el layer
26
26
  4. Migrar archivos manteniendo la lógica intacta
27
27
  5. Actualizar imports
28
- 6. Eliminar estructura legacy
28
+ 6. **Eliminar estructura legacy por completo** — no dejar archivos huérfanos, barrel files (index.ts) vacíos, ni directorios legacy sin contenido. Verificar:
29
+ - `src/application/use-cases/<name>/` queda vacío y se elimina
30
+ - `src/adapters/in/http/controllers/` sin archivos del feature migrado
31
+ - `src/domain/entities/`, `src/domain/repositories/` sin duplicados
32
+ - `src/setting/dependencies/` sin `.di.ts` del feature migrado
33
+ - Barrel files (`index.ts`) que solo exportaban código legacy se eliminan
29
34
  7. Verificar con `forge rollback verify` — si el score empeora, restaurar con `forge rollback restore <backup-id>`
30
- 8. Ejecutar `forge quench` para verificar
31
- 9. Actualizar `ARCHITECTURE.md`
35
+ 8. Ejecutar `forge quench` para verificar 0 violaciones
36
+ 9. Ejecutar `forge armorer` — confirmar que no hay huérfanos ni duplicados del código legacy
37
+ 10. Actualizar `ARCHITECTURE.md`
32
38
 
33
39
  ## Rollback
34
40
 
@@ -41,6 +47,22 @@ forge rollback restore <id> # restaurar
41
47
 
42
48
  El backup se almacena en `.forge/backups/<target>--<timestamp>/` y preserva la estructura original completa.
43
49
 
50
+ ## ⚠️ Regla crítica: No insertar lógica de dominio en Platform
51
+
52
+ Al migrar componentes legacy, **nunca colocar lógica de dominio en `src/platform/`**. Platform es exclusivamente backbone técnico.
53
+
54
+ | Tipo de componente | Layer correcto |
55
+ |---|---|
56
+ | Configuración de framework, servidor, logger, DI | `src/platform/` |
57
+ | Middleware HTTP, routers, guards, interceptors | `src/platform/http/` |
58
+ | Conexiones a BD, clientes Redis, mail | `src/infra/` |
59
+ | Código utilitario puro (sin lógica de negocio) | `src/shared/` |
60
+ | **Entidades, value objects, reglas de dominio** | **`src/features/<name>/domain/`** |
61
+ | **Casos de uso, servicios de aplicación** | **`src/features/<name>/application/`** |
62
+ | **Controladores, repositorios, schemas** | **`src/features/<name>/adapters/`** |
63
+
64
+ Si un archivo contiene `class`, `interface` con reglas de negocio, `if/switch` con lógica de dominio, o importa desde `features/`, pertenece a un feature, no a platform.
65
+
44
66
  ## Estrategias por layer
45
67
 
46
68
  | Layer | Desde | Hacia |
@@ -246,6 +246,27 @@ export function detectMisplaced(projectRoot = ROOT) {
246
246
  suggestion: "Envolver en un adapter dentro de features/<name>/adapters/out/",
247
247
  });
248
248
  }
249
+
250
+ // R13: Platform con lógica de dominio
251
+ if (isPlatformFile) {
252
+ const basenamePath = basename(file);
253
+ const DOMAIN_PATTERNS = /\.(entity|uc|mapper|port|repository)\.(ts|js)$/i;
254
+ if (DOMAIN_PATTERNS.test(basenamePath)) {
255
+ misplaced.push({
256
+ file: relPath,
257
+ reason: "Platform contiene artefacto de dominio — violación R13",
258
+ suggestion: "Mover a src/features/<name>/domain/ o application/ según corresponda",
259
+ });
260
+ }
261
+ const featureImportMatch = content.match(featurePattern);
262
+ if (featureImportMatch) {
263
+ misplaced.push({
264
+ file: relPath,
265
+ reason: "Platform importa de features — violación R2",
266
+ suggestion: "Platform no debe conocer lógica de negocio. Extraer interfaz a shared/contracts/ o refactorizar.",
267
+ });
268
+ }
269
+ }
249
270
  }
250
271
 
251
272
  return misplaced;
@@ -848,6 +848,67 @@ export function checkPlatform(ctx) {
848
848
  return { score: Math.min(score, 15), checks };
849
849
  }
850
850
 
851
+ /* ── R13: Domain logic in platform ── */
852
+
853
+ export function checkPlatformForDomain(ctx) {
854
+ const checks = [];
855
+ let score = 10;
856
+
857
+ if (!ctx.platform || !ctx.platform.exists) {
858
+ return { score, checks };
859
+ }
860
+
861
+ const DOMAIN_PATTERNS = /\.(entity|uc|mapper|port|repository)\.(ts|js)$/i;
862
+ const DOMAIN_KEYWORDS = /\b(entity|useCase|use_case|valueObject|domainService|domain_event|dto)\b/i;
863
+ const LOGIC_KEYWORDS = /\b(if|for|while|switch|catch|throw|return)\s*\(/g;
864
+
865
+ const platformDir = join(ROOT, "src", "platform");
866
+ if (!isDir(platformDir)) return { score: score, checks };
867
+
868
+ const allPlatformFiles = findFiles(platformDir, ".ts", 6).concat(findFiles(platformDir, ".js", 6));
869
+ let violations = 0;
870
+
871
+ for (const f of allPlatformFiles) {
872
+ const relPath = relative(ROOT, f);
873
+ const basenamePath = basename(f);
874
+
875
+ // Check 1: File naming suggests domain artifact
876
+ if (DOMAIN_PATTERNS.test(basenamePath)) {
877
+ checks.push({
878
+ ...severity(`[R13] Platform contiene artefacto de dominio: ${basenamePath}`, SEVERITY.CRITICAL),
879
+ pass: false,
880
+ detail: relPath,
881
+ fix: "Mover a src/features/<name>/domain/ o application/. Platform no debe tener lógica de negocio.",
882
+ });
883
+ violations++;
884
+ score -= 3;
885
+ continue;
886
+ }
887
+
888
+ // Check 2: Content references domain concepts
889
+ const content = read(f);
890
+ if (!content) continue;
891
+
892
+ if (DOMAIN_KEYWORDS.test(content)) {
893
+ checks.push({
894
+ ...severity(`[R13] Platform contiene terminología de dominio en ${basenamePath}`, SEVERITY.WARNING),
895
+ pass: false,
896
+ detail: relPath,
897
+ fix: "Si contiene lógica de negocio, mover a features/. Si es naming accidental, renombrar.",
898
+ });
899
+ violations++;
900
+ score -= 2;
901
+ }
902
+ }
903
+
904
+ if (violations === 0 && allPlatformFiles.length > 0) {
905
+ checks.push({ ...severity("Platform sin lógica de dominio", SEVERITY.INFO), pass: true });
906
+ score = 10;
907
+ }
908
+
909
+ return { score: Math.max(score, 0), checks };
910
+ }
911
+
851
912
  export function checkDependencies(ctx) {
852
913
  const checks = [];
853
914
  let score = 0;
@@ -997,6 +1058,111 @@ export function checkNaming(projectRoot = ROOT) {
997
1058
  return { score: Math.max(score, 0), checks };
998
1059
  }
999
1060
 
1061
+ export function checkImportConventions(features) {
1062
+ const checks = [];
1063
+ let score = 20;
1064
+
1065
+ if (features.length === 0) return { score: 20, checks };
1066
+
1067
+ const allFeatureFiles = findFiles(FEATURES, ".ts", 6).concat(findFiles(FEATURES, ".js", 6));
1068
+ const allPlatformFiles = isDir(join(SRC, "platform")) ? findFiles(join(SRC, "platform"), ".ts", 6) : [];
1069
+ const files = [...allFeatureFiles, ...allPlatformFiles];
1070
+
1071
+ let r10Violations = 0; // Bare specifiers
1072
+ let r11Violations = 0; // Missing .js extension
1073
+ let r12Violations = 0; // bootstrap.di.js imports
1074
+
1075
+ for (const f of files) {
1076
+ const content = read(f);
1077
+ if (!content) continue;
1078
+ const imports = parseImportsWithLines(content, f);
1079
+
1080
+ for (const imp of imports) {
1081
+ const src = imp.source;
1082
+
1083
+ // R10: Bare specifier — import from "domain/..." (no ./ ../ @/ prefix)
1084
+ if (
1085
+ !src.startsWith("./") &&
1086
+ !src.startsWith("../") &&
1087
+ !src.startsWith("@/") &&
1088
+ !src.startsWith("/") &&
1089
+ src.includes("/") &&
1090
+ !src.startsWith("tsyringe") &&
1091
+ !src.startsWith("reflect-metadata") &&
1092
+ !src.startsWith("express") &&
1093
+ !src.startsWith("node:") &&
1094
+ !src.startsWith("mongoose") &&
1095
+ !src.startsWith("prisma") &&
1096
+ src !== "tsyringe" &&
1097
+ !src.includes("node_modules")
1098
+ ) {
1099
+ r10Violations++;
1100
+ checks.push({
1101
+ severity: SEVERITY.ERROR,
1102
+ label: `[R10] Bare specifier en import local — debe usar ./ o @/`,
1103
+ pass: false,
1104
+ detail: `${relative(ROOT, f)}:${imp.line} → "${src}"`,
1105
+ fix: `Reemplazar "${src}" por "./${src}.js" (relativo) o "@/shared/${src.split("/").pop()}" (alias)`,
1106
+ });
1107
+ score -= 2;
1108
+ }
1109
+
1110
+ // R11: Import con extensión .ts en vez de .js
1111
+ if (src.endsWith(".ts") && !src.endsWith(".d.ts")) {
1112
+ r11Violations++;
1113
+ checks.push({
1114
+ severity: SEVERITY.ERROR,
1115
+ label: `[R11] Import con extensión .ts — debe usar .js`,
1116
+ pass: false,
1117
+ detail: `${relative(ROOT, f)}:${imp.line} → "${src}"`,
1118
+ fix: src.replace(/\.ts$/, ".js"),
1119
+ });
1120
+ score -= 2;
1121
+ }
1122
+
1123
+ // R12: Import desde bootstrap.di.js
1124
+ if (src.includes("bootstrap.di")) {
1125
+ r12Violations++;
1126
+ checks.push({
1127
+ severity: SEVERITY.ERROR,
1128
+ label: `[R12] Import desde bootstrap.di.js — no existe en arquitectura actual`,
1129
+ pass: false,
1130
+ detail: `${relative(ROOT, f)}:${imp.line} → "${src}"`,
1131
+ fix: `Reemplazar por "./di.js" (feature con DI propia) o "@/setting/dependencies/<feature>.di.js" (feature sin DI propia)`,
1132
+ });
1133
+ score -= 3;
1134
+ }
1135
+ }
1136
+
1137
+ // R12b: registerSingleton con model() (Mongoose)
1138
+ if (content.includes("registerSingleton") && content.includes("model(")) {
1139
+ checks.push({
1140
+ severity: SEVERITY.WARNING,
1141
+ label: `[R12] registerSingleton usado con model() — debe usar register({ useValue })`,
1142
+ pass: false,
1143
+ detail: relative(ROOT, f),
1144
+ fix: 'Reemplazar container.registerSingleton(...) por container.register(..., { useValue: ... as any })',
1145
+ });
1146
+ score -= 2;
1147
+ }
1148
+ }
1149
+
1150
+ if (r10Violations === 0 && files.length > 0) {
1151
+ checks.push({ ...severity("[R10] Sin bare specifiers en imports", SEVERITY.INFO), pass: true });
1152
+ score += 2;
1153
+ }
1154
+ if (r11Violations === 0 && files.length > 0) {
1155
+ checks.push({ ...severity("[R11] Sin imports con extensión .ts", SEVERITY.INFO), pass: true });
1156
+ score += 2;
1157
+ }
1158
+ if (r12Violations === 0 && files.length > 0) {
1159
+ checks.push({ ...severity("[R12] Sin imports a bootstrap.di.js", SEVERITY.INFO), pass: true });
1160
+ score += 2;
1161
+ }
1162
+
1163
+ return { score: Math.max(score, 0), checks };
1164
+ }
1165
+
1000
1166
  export function allChecks(features, graph, ctx) {
1001
1167
  const g = graph || getGraph();
1002
1168
  const c = ctx || {};
@@ -1006,10 +1172,12 @@ export function allChecks(features, graph, ctx) {
1006
1172
  decorators: checkDecorators(features),
1007
1173
  ownership: checkOwnership(c),
1008
1174
  platform: checkPlatform(c),
1175
+ platformDomain: checkPlatformForDomain(c),
1009
1176
  dependencies: checkDependencies(c),
1010
1177
  graph: checkGraph(g),
1011
1178
  customRules: checkCustomRules(features),
1012
1179
  naming: checkNaming(),
1180
+ importConventions: checkImportConventions(features),
1013
1181
  };
1014
1182
  }
1015
1183
 
@@ -1103,6 +1271,26 @@ export function applyFixes(checks, projectRoot = ROOT) {
1103
1271
  }
1104
1272
  }
1105
1273
 
1274
+ // Fix R11: .ts → .js in import path
1275
+ if (check.label?.includes("[R11]") && check.fix && check.detail) {
1276
+ const filePath = join(projectRoot, check.detail.split(":")[0]);
1277
+ if (existsSync(filePath)) {
1278
+ const content = readFileSync(filePath, "utf-8");
1279
+ const newContent = content.replace(
1280
+ /(from\s+['"])([^'"]+)\.ts(['"])/g,
1281
+ (match, prefix, path, suffix) => {
1282
+ if (path.endsWith(".d")) return match;
1283
+ return `${prefix}${path}.js${suffix}`;
1284
+ }
1285
+ );
1286
+ if (newContent !== content) {
1287
+ writeFileSync(filePath, newContent);
1288
+ fixed++;
1289
+ details.push(` ${GREEN}✔${RESET} ${relative(projectRoot, filePath)}: extensión .ts → .js en imports`);
1290
+ }
1291
+ }
1292
+ }
1293
+
1106
1294
  // Fix missing reflect-metadata import
1107
1295
  if (check.fix?.includes('import "reflect-metadata"') && check.detail) {
1108
1296
  const filePath = join(projectRoot, check.detail);
@@ -60,11 +60,96 @@ function hasCriticalViolations(result) {
60
60
  * Quick check against proposed content.
61
61
  * Returns violations that would be introduced by the proposed content.
62
62
  */
63
+ /**
64
+ * Quickly check proposed content for common import violations
65
+ * before the file is written to disk (preToolUse guard).
66
+ */
67
+ function checkProposedContentViolations(filePath, content) {
68
+ const violations = [];
69
+ const lines = content.split("\n");
70
+
71
+ for (let i = 0; i < lines.length; i++) {
72
+ const line = lines[i];
73
+
74
+ // Match import/export from statements
75
+ const importMatch = line.match(
76
+ /(?:import|export)\s+(?:type\s+)?(?:\{[^}]*\}|[^;{]+?)\s+from\s+['"]([^'"]+)['"]/
77
+ );
78
+ if (!importMatch) continue;
79
+
80
+ const src = importMatch[1];
81
+ const lineNum = i + 1;
82
+
83
+ // R10: Bare specifier — import from "domain/..." (no ./ ../ @/ prefix)
84
+ if (
85
+ !src.startsWith("./") &&
86
+ !src.startsWith("../") &&
87
+ !src.startsWith("@/") &&
88
+ !src.startsWith("/") &&
89
+ src.includes("/") &&
90
+ !src.startsWith("tsyringe") &&
91
+ !src.startsWith("reflect-metadata") &&
92
+ !src.startsWith("express") &&
93
+ !src.startsWith("node:") &&
94
+ !src.startsWith("mongoose") &&
95
+ !src.startsWith("prisma") &&
96
+ src !== "tsyringe"
97
+ ) {
98
+ violations.push({
99
+ severity: "CRITICAL",
100
+ label: `[R10] Bare specifier — debe usar ./ o @/ prefix`,
101
+ detail: `${filePath}:${lineNum} → "${src}"`,
102
+ fix: `Agregar prefijo "./" o "@/" al import`,
103
+ });
104
+ }
105
+
106
+ // R11: Import con extensión .ts en vez de .js
107
+ if (src.endsWith(".ts") && !src.endsWith(".d.ts")) {
108
+ violations.push({
109
+ severity: "ERROR",
110
+ label: `[R11] Import con extensión .ts — debe usar .js`,
111
+ detail: `${filePath}:${lineNum} → "${src}"`,
112
+ fix: src.replace(/\.ts$/, ".js"),
113
+ });
114
+ }
115
+
116
+ // R12: import desde bootstrap.di.js
117
+ if (src.includes("bootstrap.di")) {
118
+ violations.push({
119
+ severity: "CRITICAL",
120
+ label: `[R12] Import a bootstrap.di.js — no existe en esta arquitectura`,
121
+ detail: `${filePath}:${lineNum} → "${src}"`,
122
+ fix: 'Usar "./di.js" o "@/setting/dependencies/<feature>.di.js"',
123
+ });
124
+ }
125
+ }
126
+
127
+ // R12b: registerSingleton con model() (Mongoose)
128
+ if (content.includes("registerSingleton") && content.includes("model(")) {
129
+ violations.push({
130
+ severity: "CRITICAL",
131
+ label: `[R12] registerSingleton con model() — usar register({ useValue })`,
132
+ detail: filePath,
133
+ fix: 'container.register("Token", { useValue: Model as any })',
134
+ });
135
+ }
136
+
137
+ return violations;
138
+ }
139
+
63
140
  async function checkProposedFile(filePath, content) {
64
141
  if (!isSourceFile(filePath)) {
65
142
  return { violations: [], total: 0, hasCritical: false, hasErrors: false };
66
143
  }
67
144
 
145
+ // Fast pre-check on proposed content (before it hits disk)
146
+ const contentViolations = checkProposedContentViolations(filePath, content);
147
+ if (contentViolations.length > 0) {
148
+ const hasCritical = contentViolations.some(v => v.severity === "CRITICAL");
149
+ const hasErrors = contentViolations.some(v => v.severity === "ERROR");
150
+ return { violations: contentViolations, total: contentViolations.length, hasCritical, hasErrors };
151
+ }
152
+
68
153
  const ctx = await buildContext();
69
154
  const features = detectFeaturesOnSrc();
70
155
  const graph = ctx.graph || getGraph();
@@ -18,8 +18,10 @@ const CAT_NAMES = {
18
18
  layers: "Capas",
19
19
  ownership: "Ownership",
20
20
  platform: "Platform",
21
+ platformDomain: "Platform Domain",
21
22
  dependencies: "Dependencias",
22
23
  graph: "Grafo",
24
+ importConventions: "Import Conventions",
23
25
  };
24
26
 
25
27
  const CAT_MAX = {
@@ -28,10 +30,12 @@ const CAT_MAX = {
28
30
  decorators: 20,
29
31
  ownership: 20,
30
32
  platform: 15,
33
+ platformDomain: 10,
31
34
  dependencies: 15,
32
35
  graph: 20,
33
36
  customRules: 5,
34
37
  naming: 10,
38
+ importConventions: 20,
35
39
  };
36
40
 
37
41
  function countBySeverity(checks) {
@@ -249,6 +249,31 @@ export const RULES = [
249
249
  return violations;
250
250
  },
251
251
  }),
252
+
253
+ defineRule({
254
+ id: "R13",
255
+ name: "Platform no contiene lógica de dominio",
256
+ severity: SEVERITY.CRITICAL,
257
+ category: CATEGORY.STRUCTURE,
258
+ description: "Platform es backbone técnico y no debe contener entidades, casos de uso, mappers de dominio, schemas de entidades, repositorios de dominio ni ninguna otra lógica de negocio.",
259
+ fix: "Mover el archivo con lógica de dominio a src/features/<name>/domain/, src/features/<name>/application/, o src/features/<name>/adapters/ según corresponda.",
260
+ example: "✘ platform/User.entity.ts, platform/payments/ creando lógica de dominio en platform/",
261
+ check: (graph, ctx) => {
262
+ const violations = [];
263
+ if (!ctx || !ctx.platform || !ctx.platform.exists) return violations;
264
+ for (const comp of (ctx.platform.components || [])) {
265
+ const lower = comp.toLowerCase();
266
+ if (lower.endsWith(".entity") || lower.endsWith(".uc") || lower.endsWith(".mapper") || lower.endsWith(".port")) {
267
+ violations.push({
268
+ rule: "R13", severity: SEVERITY.CRITICAL,
269
+ from: `platform:${comp}`, to: "(domain)",
270
+ description: `Platform contiene artefacto de dominio: "${comp}"`,
271
+ });
272
+ }
273
+ }
274
+ return violations;
275
+ },
276
+ }),
252
277
  ];
253
278
 
254
279
  /**
@@ -147,6 +147,15 @@ export function computeExpectedName(filePath) {
147
147
  // Platform layer
148
148
  if (parts[1] === "platform" && parts[2]) {
149
149
  const subdir = parts[2];
150
+ const DOMAIN_SUFFIXES = /\.(entity|uc|mapper|port|repository)\.(ts|js)$/i;
151
+ if (DOMAIN_SUFFIXES.test(filename)) {
152
+ return {
153
+ current: filePath,
154
+ expected: filePath,
155
+ relPath: relPath,
156
+ rule: `[R13] Platform no debe contener artefactos de dominio: "${filename}" pertenece a src/features/<name>/`,
157
+ };
158
+ }
150
159
  const rule = PLATFORM_RULES[subdir];
151
160
  if (!rule) return null;
152
161
 
@@ -17,7 +17,7 @@ const ROOT = process.cwd();
17
17
  const CACHE_PATH = join(ROOT, ".forge", "version-cache.json");
18
18
  const CACHE_TTL = 86400000; // 24h
19
19
  const VERSION_URL = "https://forge.dev/latest";
20
- const CURRENT_VERSION = "1.0.2";
20
+ const CURRENT_VERSION = "1.3.5";
21
21
 
22
22
  function readJson(path) {
23
23
  try {
@@ -1,12 +1,20 @@
1
1
  ```typescript
2
2
  // src/features/<domain>/adapters/in/http/<Domain>.controller.ts
3
+ //
4
+ // IMPORTANTE — Convenciones:
5
+ // 1. Nombres de métodos: usar createHandler (no "add", "store", etc.)
6
+ // 2. Si la entidad <Domain> es compartida desde platform/domain/,
7
+ // importar con path alias: import type { <Domain> } from "@/domain/entities/<Domain>.js";
8
+ // 3. Si el feature NO tiene di.ts propio, el controller se importa desde
9
+ // @/setting/dependencies/<domain>.di.js en vez de bootstrap.di.js
10
+
3
11
  import { injectable, inject } from "tsyringe";
4
12
  import type { Request, Response, NextFunction } from "express";
5
- import { Create<Domain> } from "../../../application/use-cases/Create<Domain>.uc.js";
6
- import { Get<Domain> } from "../../../application/use-cases/Get<Domain>.uc.js";
7
- import { List<Domain> } from "../../../application/use-cases/List<Domain>.uc.js";
8
- import { Update<Domain> } from "../../../application/use-cases/Update<Domain>.uc.js";
9
- import { Delete<Domain> } from "../../../application/use-cases/Delete<Domain>.uc.js";
13
+ import { Create<Domain> } from "../../application/use-cases/Create<Domain>.uc.js";
14
+ import { Get<Domain> } from "../../application/use-cases/Get<Domain>.uc.js";
15
+ import { List<Domain> } from "../../application/use-cases/List<Domain>.uc.js";
16
+ import { Update<Domain> } from "../../application/use-cases/Update<Domain>.uc.js";
17
+ import { Delete<Domain> } from "../../application/use-cases/Delete<Domain>.uc.js";
10
18
 
11
19
  @injectable()
12
20
  export class <Domain>Controller {
@@ -0,0 +1,33 @@
1
+ ```typescript
2
+ // src/features/<domain>/di.ts
3
+ // DI container wiring para el feature <Domain>.
4
+ //
5
+ // NOTA: Si el repositorio exporta un modelo Mongoose (model()) en lugar de una clase,
6
+ // usar container.register() con useValue en vez de registerSingleton.
7
+ //
8
+ // Para entidades compartidas desde platform/domain/, usar path alias @/domain/ en vez de relativo.
9
+
10
+ import { container } from "tsyringe";
11
+ import type { I<Domain>Repository } from "./domain/repositories/I<Domain>.repository.js";
12
+ import { <Domain>Repository } from "./adapters/out/persistence/<Domain>.repository.js";
13
+ import { Create<Domain> } from "./application/use-cases/Create<Domain>.uc.js";
14
+ import { Get<Domain> } from "./application/use-cases/Get<Domain>.uc.js";
15
+ import { List<Domain> } from "./application/use-cases/List<Domain>.uc.js";
16
+ import { Update<Domain> } from "./application/use-cases/Update<Domain>.uc.js";
17
+ import { Delete<Domain> } from "./application/use-cases/Delete<Domain>.uc.js";
18
+
19
+ // ── Repositorio ──
20
+ // Si <Domain>Repository es una clase (implementación estándar):
21
+ container.registerSingleton<I<Domain>Repository>("I<Domain>Repository", <Domain>Repository);
22
+
23
+ // Si <Domain>Repository es un modelo Mongoose (export default model()), usar:
24
+ // import <Domain>Model from "./adapters/out/persistence/<Domain>.schema.js";
25
+ // container.register<I<Domain>Repository>("I<Domain>Repository", { useValue: <Domain>Model as any });
26
+
27
+ // ── Use Cases ──
28
+ container.registerSingleton(Create<Domain>);
29
+ container.registerSingleton(Get<Domain>);
30
+ container.registerSingleton(List<Domain>);
31
+ container.registerSingleton(Update<Domain>);
32
+ container.registerSingleton(Delete<Domain>);
33
+ ```
@@ -1,5 +1,10 @@
1
1
  ```typescript
2
2
  // src/features/<domain>/application/mappers/<Domain>.mapper.ts
3
+ //
4
+ // IMPORTANTE: Si la entidad <Domain> está en platform/domain/entities/ (compartida),
5
+ // reemplazar el import relativo por path alias:
6
+ // import type { <Domain> } from "@/domain/entities/<Domain>.js";
7
+
3
8
  import type { <Domain> } from "../domain/entities/<Domain>.entity.js";
4
9
 
5
10
  export class <Domain>Mapper {
@@ -1,9 +1,14 @@
1
1
  ```typescript
2
2
  // src/features/<domain>/adapters/out/persistence/<Domain>.repository.ts
3
+ //
4
+ // IMPORTANTE: Si la entidad <Domain> está en platform/domain/entities/ (compartida),
5
+ // reemplazar el import relativo por path alias:
6
+ // import type { <Domain> } from "@/domain/entities/<Domain>.js";
7
+
3
8
  import { injectable } from "tsyringe";
4
9
  import type { <Domain> } from "../../../domain/entities/<Domain>.entity.js";
5
10
  import type { I<Domain>Repository } from "../../../domain/repositories/I<Domain>.repository.js";
6
- import { <Domain>Mapper } from "../../../application/mappers/<Domain>.mapper.js";
11
+ import { <Domain>Mapper } from "../../application/mappers/<Domain>.mapper.js";
7
12
  import <Domain>Model from "./<Domain>.schema.js";
8
13
  import { RepositoryError } from "@/shared/errors/RepositoryError.js";
9
14
 
@@ -1,5 +1,10 @@
1
1
  ```typescript
2
2
  // src/features/<domain>/adapters/in/http/<Domain>.routes.ts
3
+ //
4
+ // IMPORTANTE: Los nombres de método del controller deben coincidir
5
+ // con los invocados aquí. Si el controller usa "add" en vez de "createHandler",
6
+ // ajustar la ruta: router.post("/", controller.add);
7
+
3
8
  import { Router } from "express";
4
9
  import { container } from "tsyringe";
5
10
  import { <Domain>Controller } from "./<Domain>.controller.js";
@@ -12,5 +12,9 @@ const <Domain>Schema = new Schema<<Domain>>(
12
12
 
13
13
  <Domain>Schema.index({ /* índices */ });
14
14
 
15
+ // NOTA DI: model() exporta un objeto Model, NO una clase.
16
+ // En el contenedor DI usar container.register() con useValue, NO registerSingleton:
17
+ // import <Domain>Model from "./<Domain>.schema.js";
18
+ // container.register<I<Domain>Repository>("I<Domain>Repository", { useValue: <Domain>Model as any });
15
19
  export default model<<Domain>>("<Domain>", <Domain>Schema);
16
20
  ```
@@ -0,0 +1,68 @@
1
+ ```typescript
2
+ // src/features/<domain>/__tests__/Create<Domain>.test.ts
3
+ //
4
+ // Convenciones para tests:
5
+ // - Usar extension .js en imports (ESM + verbatimModuleSyntax)
6
+ // - Usar "as const" para literales que forman parte de union types
7
+ // - Usar "result!" (non-null assertion) cuando execute() retorna T | null
8
+ // - Usar "(result as any)._id" si _id no existe en el tipo de dominio
9
+
10
+ import { describe, it, before } from "node:test";
11
+ import assert from "node:assert";
12
+ import { Create<Domain> } from "../application/use-cases/Create<Domain>.uc.js";
13
+ import type { I<Domain>Repository } from "../domain/repositories/I<Domain>.repository.js";
14
+ import type { <Domain> } from "../domain/entities/<Domain>.entity.js";
15
+
16
+ class Mock<Domain>Repository implements I<Domain>Repository {
17
+ private store: <Domain>[] = [];
18
+
19
+ async create(data: Partial<<Domain>>): Promise<<Domain>> {
20
+ const entity = { id: "1", ...data } as <Domain>;
21
+ this.store.push(entity);
22
+ return entity;
23
+ }
24
+
25
+ async findById(id: string): Promise<<Domain> | null> {
26
+ return this.store.find(e => (e as any).id === id) || null;
27
+ }
28
+
29
+ async findAll(): Promise<{ data: <Domain>[]; total: number }> {
30
+ return { data: this.store, total: this.store.length };
31
+ }
32
+
33
+ async update(id: string, data: Partial<<Domain>>): Promise<<Domain> | null> {
34
+ const idx = this.store.findIndex(e => (e as any).id === id);
35
+ if (idx === -1) return null;
36
+ this.store[idx] = { ...this.store[idx], ...data };
37
+ return this.store[idx];
38
+ }
39
+
40
+ async delete(id: string): Promise<void> {
41
+ this.store = this.store.filter(e => (e as any).id !== id);
42
+ }
43
+ }
44
+
45
+ describe("Create<Domain>", () => {
46
+ let useCase: Create<Domain>;
47
+ let mockRepo: Mock<Domain>Repository;
48
+
49
+ before(() => {
50
+ mockRepo = new Mock<Domain>Repository();
51
+ useCase = new Create<Domain>(mockRepo);
52
+ });
53
+
54
+ it("debería crear una entidad <domain>", async () => {
55
+ const input = { name: "test" as const };
56
+ const result = await useCase.execute(input);
57
+ assert.ok(result!);
58
+ assert.equal((result as any)._id || result!.id, "1");
59
+ });
60
+
61
+ it("debería lanzar error si faltan datos requeridos", async () => {
62
+ await assert.rejects(
63
+ () => useCase.execute(null as any),
64
+ { message: /requeridos|requerid/i }
65
+ );
66
+ });
67
+ });
68
+ ```
@@ -1,5 +1,10 @@
1
1
  ```typescript
2
2
  // src/features/<domain>/application/use-cases/Create<Domain>.uc.ts
3
+ //
4
+ // IMPORTANTE: Si la entidad <Domain> está en platform/domain/entities/ (compartida),
5
+ // reemplazar el import relativo por path alias:
6
+ // import type { <Domain> } from "@/domain/entities/<Domain>.js";
7
+
3
8
  import { injectable, inject } from "tsyringe";
4
9
  import type { <Domain> } from "../../domain/entities/<Domain>.entity.js";
5
10
  import type { I<Domain>Repository } from "../../domain/repositories/I<Domain>.repository.js";
@@ -233,11 +233,12 @@ describe("formatter.mjs", () => {
233
233
  });
234
234
 
235
235
  describe("registry/rules.mjs", () => {
236
- it("has 9 built-in rules (R1-R9)", async () => {
236
+ it("has 10 built-in rules (R1-R9 + R13)", async () => {
237
237
  const { RULES, RULES_BY_ID } = await import("../scripts/registry/rules.mjs");
238
- assert.equal(RULES.length, 9);
238
+ assert.equal(RULES.length, 10);
239
239
  assert.ok(RULES_BY_ID.R1);
240
240
  assert.ok(RULES_BY_ID.R9);
241
+ assert.ok(RULES_BY_ID.R13);
241
242
  for (const r of RULES) {
242
243
  assert.ok(r.id);
243
244
  assert.ok(r.name);