@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 +18 -10
- package/package.json +1 -1
- package/skills/forge/SKILL.md +11 -0
- package/skills/forge/reference/cast.md +43 -0
- package/skills/forge/reference/patterns.md +60 -1
- package/skills/forge/reference/principles.md +2 -0
- package/skills/forge/reference/reforge.md +26 -4
- package/skills/forge/reference/relocate.md +25 -3
- package/skills/forge/scripts/armorer.mjs +21 -0
- package/skills/forge/scripts/detect.mjs +188 -0
- package/skills/forge/scripts/forgeSmith.mjs +85 -0
- package/skills/forge/scripts/inspect.mjs +4 -0
- package/skills/forge/scripts/registry/rules.mjs +25 -0
- package/skills/forge/scripts/rename.mjs +9 -0
- package/skills/forge/scripts/update.mjs +1 -1
- package/skills/forge/templates/feature/controller.ts.md +13 -5
- package/skills/forge/templates/feature/di.ts.md +33 -0
- package/skills/forge/templates/feature/mapper.ts.md +5 -0
- package/skills/forge/templates/feature/repository-impl.ts.md +6 -1
- package/skills/forge/templates/feature/routes.ts.md +5 -0
- package/skills/forge/templates/feature/schema.ts.md +4 -0
- package/skills/forge/templates/feature/test.ts.md +68 -0
- package/skills/forge/templates/feature/use-case.ts.md +5 -0
- package/skills/forge/tests/core.test.mjs +3 -2
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.
|
|
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
|
|
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
|
|
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
|
|
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 |
|
|
80
|
-
| Layers |
|
|
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**:
|
|
315
|
-
- **
|
|
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-
|
|
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/` |
|
|
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
package/skills/forge/SKILL.md
CHANGED
|
@@ -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.
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
|
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.
|
|
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 "
|
|
6
|
-
import { Get<Domain> } from "
|
|
7
|
-
import { List<Domain> } from "
|
|
8
|
-
import { Update<Domain> } from "
|
|
9
|
-
import { Delete<Domain> } from "
|
|
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 "
|
|
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
|
|
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,
|
|
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);
|