@ronaldjdevfs/forge 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -8
- package/package.json +3 -3
- package/skills/forge/SKILL.md +86 -15
- package/skills/forge/profiles/express-drizzle.md +107 -0
- package/skills/forge/profiles/fastify-mongodb.md +103 -0
- package/skills/forge/profiles/fastify-prisma.md +81 -0
- package/skills/forge/profiles/nestjs-mongodb.md +92 -0
- package/skills/forge/profiles/nestjs-postgres.md +98 -0
- package/skills/forge/reference/api-design.md +62 -0
- package/skills/forge/reference/assay.md +82 -0
- package/skills/forge/reference/cast.md +81 -7
- package/skills/forge/reference/data-patterns.md +86 -0
- package/skills/forge/reference/di-strategies.md +50 -0
- package/skills/forge/reference/errors.md +65 -0
- package/skills/forge/reference/events.md +95 -0
- package/skills/forge/reference/help.md +40 -0
- package/skills/forge/reference/hooks.md +62 -0
- package/skills/forge/reference/observability.md +66 -0
- package/skills/forge/reference/patterns.md +52 -0
- package/skills/forge/reference/principles.md +6 -0
- package/skills/forge/reference/reforge.md +69 -5
- package/skills/forge/reference/relocate.md +15 -2
- package/skills/forge/reference/security-patterns.md +87 -0
- package/skills/forge/reference/testing-patterns.md +69 -0
- package/skills/forge/scripts/assay.mjs +481 -0
- package/skills/forge/scripts/context.mjs +147 -43
- package/skills/forge/scripts/detect.mjs +371 -22
- package/skills/forge/scripts/forge-api.mjs +373 -0
- package/skills/forge/scripts/forge-config.mjs +268 -0
- package/skills/forge/scripts/forge-signals.mjs +131 -0
- package/skills/forge/scripts/forge-state.mjs +97 -0
- package/skills/forge/scripts/formatter.mjs +133 -0
- package/skills/forge/scripts/graph.mjs +5 -21
- package/skills/forge/scripts/hook.mjs +250 -0
- package/skills/forge/scripts/inspect.mjs +171 -22
- package/skills/forge/scripts/parse-imports.mjs +249 -0
- package/skills/forge/scripts/pin.mjs +151 -0
- package/skills/forge/scripts/posttool.mjs +224 -0
- package/skills/forge/scripts/profile.mjs +124 -20
- package/skills/forge/scripts/registry/rules.mjs +344 -0
- package/skills/forge/scripts/rename.mjs +669 -0
- package/skills/forge/scripts/rollback.mjs +213 -0
- package/skills/forge/scripts/update.mjs +114 -0
- package/skills/forge/templates/feature/domain-error.ts.md +9 -0
- package/skills/forge/templates/feature/domain-event.ts.md +9 -0
- package/skills/forge/templates/feature/event-handler.ts.md +10 -0
- package/skills/forge/templates/feature/use-case.ts.md +10 -2
- package/skills/forge/tests/core.test.mjs +403 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Profile: NestJS + MongoDB + Mongoose
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
framework: NestJS
|
|
7
|
+
runtime: Node 20+
|
|
8
|
+
database: MongoDB
|
|
9
|
+
orm: Mongoose
|
|
10
|
+
di_strategy: framework (NestJS DI)
|
|
11
|
+
architecture: hexagonal-feature
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## DI Strategy
|
|
15
|
+
|
|
16
|
+
NestJS DI nativo con `@Injectable()` y módulos. Conexión Mongoose via `MongooseModule.forRoot()`.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// app.module.ts
|
|
20
|
+
@Module({
|
|
21
|
+
imports: [
|
|
22
|
+
MongooseModule.forRoot(process.env.MONGO_URI),
|
|
23
|
+
CreditModule,
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
export class AppModule {}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Project Structure
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
├── src/
|
|
33
|
+
│ ├── features/
|
|
34
|
+
│ │ └── credit/
|
|
35
|
+
│ │ ├── domain/
|
|
36
|
+
│ │ │ ├── Credit.entity.ts # Plain domain object
|
|
37
|
+
│ │ │ └── ICredit.repository.ts
|
|
38
|
+
│ │ ├── application/
|
|
39
|
+
│ │ │ └── use-cases/
|
|
40
|
+
│ │ │ └── AddCredit.uc.ts
|
|
41
|
+
│ │ └── adapters/
|
|
42
|
+
│ │ ├── in/http/
|
|
43
|
+
│ │ │ ├── Credit.controller.ts
|
|
44
|
+
│ │ │ └── Credit.module.ts
|
|
45
|
+
│ │ └── out/persistence/
|
|
46
|
+
│ │ ├── Credit.schema.ts # Mongoose schema
|
|
47
|
+
│ │ └── Credit.repository.ts
|
|
48
|
+
│ ├── shared/
|
|
49
|
+
│ ├── app.module.ts
|
|
50
|
+
│ └── main.ts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Persistence (Mongoose)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// adapters/out/persistence/Credit.schema.ts
|
|
57
|
+
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
|
58
|
+
|
|
59
|
+
@Schema({ timestamps: true })
|
|
60
|
+
export class CreditDocument {
|
|
61
|
+
@Prop({ required: true }) amount: number;
|
|
62
|
+
@Prop({ required: true }) userId: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const CreditSchema = SchemaFactory.createForClass(CreditDocument);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// adapters/out/persistence/credit.module.ts
|
|
70
|
+
@Module({
|
|
71
|
+
imports: [MongooseModule.forFeature([{ name: "Credit", schema: CreditSchema }])],
|
|
72
|
+
providers: [{ provide: ICreditRepository, useClass: CreditRepository }],
|
|
73
|
+
exports: [ICreditRepository],
|
|
74
|
+
})
|
|
75
|
+
export class CreditPersistenceModule {}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Testing Strategy
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// Test con MongoMemoryServer
|
|
82
|
+
const module = await Test.createTestingModule({
|
|
83
|
+
imports: [
|
|
84
|
+
MongooseModule.forRoot(MongoMemoryServer.create().then(m => m.getUri())),
|
|
85
|
+
CreditModule,
|
|
86
|
+
],
|
|
87
|
+
}).compile();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Naming Conventions
|
|
91
|
+
|
|
92
|
+
Schemas en PascalCase, colecciones en snake_case (Mongoose usa plural automático).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Profile: NestJS + PostgreSQL
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
framework: NestJS
|
|
7
|
+
runtime: Node 20+
|
|
8
|
+
database: PostgreSQL
|
|
9
|
+
orm: TypeORM / native
|
|
10
|
+
di_strategy: framework (NestJS DI)
|
|
11
|
+
architecture: hexagonal-feature
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## DI Strategy
|
|
15
|
+
|
|
16
|
+
NestJS DI nativo. Módulos con `@Module()`, `@Injectable()` para servicios y repositorios. Providers registrados en cada módulo de feature.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// features/credit/credit.module.ts
|
|
20
|
+
@Module({
|
|
21
|
+
imports: [TypeOrmModule.forFeature([CreditEntity])],
|
|
22
|
+
controllers: [CreditController],
|
|
23
|
+
providers: [
|
|
24
|
+
{ provide: ICreditRepository, useClass: CreditRepository },
|
|
25
|
+
AddCredit,
|
|
26
|
+
],
|
|
27
|
+
})
|
|
28
|
+
export class CreditModule {}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Project Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
├── src/
|
|
35
|
+
│ ├── features/
|
|
36
|
+
│ │ └── credit/
|
|
37
|
+
│ │ ├── domain/
|
|
38
|
+
│ │ │ ├── Credit.entity.ts # TypeORM entity (dominio anémico aceptable)
|
|
39
|
+
│ │ │ └── ICredit.repository.ts
|
|
40
|
+
│ │ ├── application/
|
|
41
|
+
│ │ │ └── use-cases/
|
|
42
|
+
│ │ │ └── AddCredit.uc.ts
|
|
43
|
+
│ │ └── adapters/
|
|
44
|
+
│ │ ├── in/http/
|
|
45
|
+
│ │ │ ├── Credit.controller.ts
|
|
46
|
+
│ │ │ └── Credit.module.ts
|
|
47
|
+
│ │ └── out/persistence/
|
|
48
|
+
│ │ └── Credit.repository.ts
|
|
49
|
+
│ ├── shared/
|
|
50
|
+
│ ├── app.module.ts
|
|
51
|
+
│ └── main.ts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Persistence (TypeORM)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// domain/Credit.entity.ts
|
|
58
|
+
@Entity()
|
|
59
|
+
export class Credit {
|
|
60
|
+
@PrimaryGeneratedColumn("uuid") id: string;
|
|
61
|
+
@Column() amount: number;
|
|
62
|
+
@Column() userId: string;
|
|
63
|
+
@CreateDateColumn() createdAt: Date;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// adapters/out/persistence/Credit.repository.ts
|
|
69
|
+
@Injectable()
|
|
70
|
+
export class CreditRepository implements ICreditRepository {
|
|
71
|
+
constructor(@InjectRepository(Credit) private readonly repo: Repository<Credit>) {}
|
|
72
|
+
|
|
73
|
+
async create(data: CreditInput): Promise<Credit> {
|
|
74
|
+
const entity = this.repo.create(data);
|
|
75
|
+
return this.repo.save(entity);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Testing Strategy
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// Test unitario sin NestJS
|
|
84
|
+
const mockRepo = { create: jest.fn() };
|
|
85
|
+
const uc = new AddCredit(mockRepo);
|
|
86
|
+
|
|
87
|
+
// Test de integración con Test.createTestingModule
|
|
88
|
+
const module = await Test.createTestingModule({
|
|
89
|
+
providers: [
|
|
90
|
+
AddCredit,
|
|
91
|
+
{ provide: ICreditRepository, useClass: MockCreditRepo },
|
|
92
|
+
],
|
|
93
|
+
}).compile();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Naming Conventions
|
|
97
|
+
|
|
98
|
+
Mismas que `nestjs-prisma`. Entidades TypeORM en PascalCase, tablas en snake_case con `@Entity("credit")`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# API Design — Diseño de APIs REST/GraphQL
|
|
2
|
+
|
|
3
|
+
## Principios arquitectónicos
|
|
4
|
+
|
|
5
|
+
- Las rutas/endpoints pertenecen a `adapters/in/http/`, nunca al dominio.
|
|
6
|
+
- El controller es la capa más delgada posible: parsea, delega, responde.
|
|
7
|
+
- La validación de entrada ocurre en el controller o middleware, no en domain/.
|
|
8
|
+
- El contrato API (OpenAPI/Swagger) se genera desde tipos compartidos.
|
|
9
|
+
|
|
10
|
+
## REST
|
|
11
|
+
|
|
12
|
+
### Convenciones
|
|
13
|
+
|
|
14
|
+
| Verbo | Ruta | Controller responde | Use case retorna |
|
|
15
|
+
|-------|------|-------------------|------------------|
|
|
16
|
+
| GET | `/resources` | 200 + array | `Resource[]` |
|
|
17
|
+
| GET | `/resources/:id` | 200 + object / 404 | `Resource \| null` |
|
|
18
|
+
| POST | `/resources` | 201 + object | `Resource` |
|
|
19
|
+
| PUT | `/resources/:id` | 200 + object | `Resource` |
|
|
20
|
+
| PATCH | `/resources/:id` | 200 + object | `Resource` |
|
|
21
|
+
| DELETE | `/resources/:id` | 204 / 404 | `void` |
|
|
22
|
+
|
|
23
|
+
### Paginación
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// shared/contracts/IPaginatedResponse.ts
|
|
27
|
+
export interface IPaginatedResponse<T> {
|
|
28
|
+
data: T[];
|
|
29
|
+
meta: {
|
|
30
|
+
total: number;
|
|
31
|
+
page: number;
|
|
32
|
+
limit: number;
|
|
33
|
+
totalPages: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// Formato query: GET /users?page=1&limit=20&sort=name:asc&filter=status:active
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Versionado
|
|
43
|
+
|
|
44
|
+
- Preferir versionado por encabezado (`Accept: application/vnd.api+json;version=2`)
|
|
45
|
+
- Si es por URL: `/api/v1/resources` (carpeta física `adapters/in/http/v1/`)
|
|
46
|
+
- Deprecar con encabezado `Sunset: Sat, 01 Jan 2027 00:00:00 GMT`
|
|
47
|
+
|
|
48
|
+
## GraphQL
|
|
49
|
+
|
|
50
|
+
- Schema en `adapters/in/graphql/` dentro del feature
|
|
51
|
+
- Resolvers = use cases wrapped; nunca contienen lógica de negocio
|
|
52
|
+
- DataLoader para N+1 en resolvers de listas
|
|
53
|
+
- Scalar types custom para IDs, fechas, emails
|
|
54
|
+
|
|
55
|
+
## Buenas prácticas
|
|
56
|
+
|
|
57
|
+
- OpenAPI 3.1 como fuente de verdad para REST
|
|
58
|
+
- Validación con zod (compartir schemas entre controller y validación)
|
|
59
|
+
- Rate limiting por ruta/rol en middleware de platform/http/
|
|
60
|
+
- Errores normalizados: `{ error: { code, message, details? } }`
|
|
61
|
+
- Idempotencia en POST con `Idempotency-Key` header
|
|
62
|
+
- HATEOAS opcional, solo para APIs hipermedia
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Assay
|
|
2
|
+
|
|
3
|
+
Genera un **ensayo arquitectónico multi-persona** que evalúa el estado actual de la arquitectura desde 5 perspectivas expertas.
|
|
4
|
+
|
|
5
|
+
## Cuándo usarlo
|
|
6
|
+
|
|
7
|
+
- Después de `forge inspect` para obtener interpretación cualitativa de los resultados
|
|
8
|
+
- Para entender el impacto de las violaciones desde ángulos complementarios
|
|
9
|
+
- Para priorizar acciones de refactor con criterio multi-disciplinario
|
|
10
|
+
- Como herramienta de discusión en revisiones arquitectónicas de equipo
|
|
11
|
+
|
|
12
|
+
## Las 5 Personas
|
|
13
|
+
|
|
14
|
+
| Persona | Rol | Enfoque |
|
|
15
|
+
|---------|-----|---------|
|
|
16
|
+
| **Jeff Bezos** | Arquitecto de Escalabilidad y Autonomía | Acoplamiento entre features, contratos de API, autonomía de equipos, ciclos de dependencias |
|
|
17
|
+
| **Martin Fowler** | Refinador de Patrones y Deuda Técnica | Dirección de dependencias, code smells, naming, refactoring evolutivo |
|
|
18
|
+
| **El Hacker** | Pragmático y Simplificador | Over-engineering, complejidad innecesaria, abstracciones sin valor, costo real de mantenimiento |
|
|
19
|
+
| **Alex** | Product Manager Técnico | Velocidad de entrega, ROI de la deuda técnica, impacto en roadmap, time-to-market |
|
|
20
|
+
| **Dra. Carter** | Arquitecta Senior — Deuda Técnica y Gobernanza | Sostenibilidad a 3-5 años, consistencia entre equipos, gobernanza y ownership |
|
|
21
|
+
|
|
22
|
+
## Uso
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Ensayo completo con todas las personas
|
|
26
|
+
node .opencode/skills/forge/scripts/assay.mjs
|
|
27
|
+
|
|
28
|
+
# Solo la opinión de una persona
|
|
29
|
+
node .opencode/skills/forge/scripts/assay.mjs --persona=bezos
|
|
30
|
+
node .opencode/skills/forge/scripts/assay.mjs --persona=fowler
|
|
31
|
+
node .opencode/skills/forge/scripts/assay.mjs --persona=hacker
|
|
32
|
+
node .opencode/skills/forge/scripts/assay.mjs --persona=pm
|
|
33
|
+
node .opencode/skills/forge/scripts/assay.mjs --persona=senior
|
|
34
|
+
|
|
35
|
+
# Salida JSON (para consumo por herramientas)
|
|
36
|
+
node .opencode/skills/forge/scripts/assay.mjs --json
|
|
37
|
+
|
|
38
|
+
# Persistir ensayo en .forge/assay/
|
|
39
|
+
node .opencode/skills/forge/scripts/assay.mjs --save
|
|
40
|
+
|
|
41
|
+
# Ver historial de ensayos
|
|
42
|
+
node .opencode/skills/forge/scripts/assay.mjs history
|
|
43
|
+
|
|
44
|
+
# Leer un ensayo previo
|
|
45
|
+
node .opencode/skills/forge/scripts/assay.mjs read assay-2026-06-25T00-00-00.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Formato de salida
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
═══ Forge Assay — Ensayo Arquitectónico ═══
|
|
52
|
+
Score: 72/140 (51%) — D
|
|
53
|
+
Violaciones: 8
|
|
54
|
+
Personas: Jeff Bezos, Martin Fowler, El Hacker, Alex, Dra. Carter
|
|
55
|
+
Fecha: 2026-06-25
|
|
56
|
+
|
|
57
|
+
──── Jeff Bezos — Arquitecto de Escalabilidad y Autonomía ────
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
──── Martin Fowler — Refinador de Patrones y Deuda Técnica ────
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
──── El Hacker — Pragmático y Simplificador ────
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
──── Alex — Product Manager Técnico ────
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
──── Dra. Carter — Arquitecta Senior ────
|
|
70
|
+
...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Integración con inspect
|
|
74
|
+
|
|
75
|
+
El ensayo se basa en los datos del último `forge inspect`. Si no hay auditoría previa, el ensayo se genera igual pero con menos datos contextuales.
|
|
76
|
+
|
|
77
|
+
Se recomienda:
|
|
78
|
+
1. `forge inspect` → obtener score, violaciones, grafo
|
|
79
|
+
2. `forge assay` → interpretar los resultados desde las 5 perspectivas
|
|
80
|
+
3. Priorizar acciones basado en las recomendaciones
|
|
81
|
+
4. Refactorizar con `reforge`, `temper`, o `smelt`
|
|
82
|
+
5. Repetir el ciclo
|
|
@@ -17,11 +17,84 @@ Antes de crear un feature, verificar la existencia de los layers arquitectónico
|
|
|
17
17
|
|
|
18
18
|
Si alguno no existe, ejecutar `bootstrapPlatform()` automáticamente para crearlos.
|
|
19
19
|
|
|
20
|
+
## ⚠️ Pre-Cast Discovery — 3 Gates Obligatorios
|
|
21
|
+
|
|
22
|
+
**NO escribir código hasta pasar los 3 gates de aprobación.** Cast solía crear scaffolding directamente del nombre del feature. Esto producía features genéricos que requerían refactor posterior. Ahora todo `cast` requiere descubrimiento direccional multi-ronda.
|
|
23
|
+
|
|
24
|
+
Usar las **señales de `forge-signals.mjs`** para contextualizar el descubrimiento:
|
|
25
|
+
- Si hay features existentes, revisar sus entidades y casos de uso para mantener coherencia.
|
|
26
|
+
- Si el perfil está detectado, usarlo para preguntas específicas (ej: "Prisma detectado → schema primero").
|
|
27
|
+
- Si hay archivos modificados en git, considerarlos como contexto del nuevo feature.
|
|
28
|
+
|
|
29
|
+
### Gate 1: Brief del Feature
|
|
30
|
+
|
|
31
|
+
Hacer **al menos 1 ronda de preguntas** usando la herramienta `question`. No inferir todas las respuestas del prompt inicial. Preguntar:
|
|
32
|
+
|
|
33
|
+
**Ronda 1 (obligatoria — 2-3 preguntas):**
|
|
34
|
+
- ¿Cuál es la entidad principal del dominio? (ej: "Factura", "Suscripción", "Producto")
|
|
35
|
+
- ¿Qué operaciones CRUD necesita? ¿Todas o solo un subconjunto?
|
|
36
|
+
- ¿Este feature se relaciona con algún feature existente? ¿Cuál y cómo?
|
|
37
|
+
|
|
38
|
+
**Ronda 2 (si aplica — preguntar solo si la Ronda 1 dejó dudas):**
|
|
39
|
+
- ¿Necesita eventos de dominio o integración con scheduler?
|
|
40
|
+
- ¿Expone API REST? ¿También GraphQL o gRPC?
|
|
41
|
+
- ¿Requiere cache? ¿Lectura > escritura o viceversa?
|
|
42
|
+
|
|
43
|
+
**No preguntes sobre detalles de implementación (nombre de columnas, puertos, etc.) en esta fase.** Eso se resuelve en el scaffold. El brief es sobre qué hace el feature, no cómo se implementa.
|
|
44
|
+
|
|
45
|
+
**Salida:** Brief del feature confirmado por el usuario.
|
|
46
|
+
|
|
47
|
+
### Gate 2: Confirmar Estructura
|
|
48
|
+
|
|
49
|
+
Tras el brief, presentar la estructura propuesta al usuario para confirmación:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
src/features/<name>/
|
|
53
|
+
├── domain/
|
|
54
|
+
│ ├── <Name>.entity.ts
|
|
55
|
+
│ └── I<Name>.repository.ts
|
|
56
|
+
├── application/
|
|
57
|
+
│ ├── use-cases/
|
|
58
|
+
│ │ ├── Create<Name>.uc.ts
|
|
59
|
+
│ │ ├── Get<Name>.uc.ts
|
|
60
|
+
│ │ └── List<Name>.uc.ts (los que apliquen según brief)
|
|
61
|
+
│ └── mappers/
|
|
62
|
+
│ └── <Name>.mapper.ts
|
|
63
|
+
└── adapters/
|
|
64
|
+
├── in/http/
|
|
65
|
+
│ ├── <Name>.controller.ts
|
|
66
|
+
│ └── <name>.routes.ts
|
|
67
|
+
└── out/persistence/
|
|
68
|
+
├── <Name>.schema.ts
|
|
69
|
+
└── <Name>.repository.ts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Preguntar: "¿Esta estructura cubre el dominio? ¿Falta algo, sobra algo?"
|
|
73
|
+
|
|
74
|
+
**No avanzar sin confirmación explícita del usuario.**
|
|
75
|
+
|
|
76
|
+
### Gate 3: Confirmar Wiring
|
|
77
|
+
|
|
78
|
+
Antes de escribir código, confirmar las decisiones de integración:
|
|
79
|
+
|
|
80
|
+
- **Repository**: "Inyecto `I<Name>Repository` vía interfaz. Implementación concreta en `adapters/out/persistence/`. ¿OK?"
|
|
81
|
+
- **Controller**: "El controller parsea, llama al use case, responde. Sin lógica de negocio. ¿OK?"
|
|
82
|
+
- **DI**: Según perfil: "Uso `@injectable()` + `@inject(Token)` con tsyringe" o "DI manual en bootstrap". ¿OK?
|
|
83
|
+
- **Routing**: "Registro las rutas en el enrutador principal de HTTP. ¿OK?"
|
|
84
|
+
|
|
85
|
+
Preguntar: "¿Confirmas este wiring antes de generar el feature?"
|
|
86
|
+
|
|
87
|
+
**Sin Gate 3 confirmado, no se escribe ni un archivo.**
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
20
91
|
## Flujo
|
|
21
92
|
|
|
22
93
|
1. Verificar que `src/platform/`, `src/shared/`, `src/infra/` existan (crearlos si no)
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
94
|
+
2. **Ejecutar Pre-Cast Discovery** (3 gates obligatorios)
|
|
95
|
+
3. Determinar el nombre del feature (formato: kebab-case) — ya debería estar claro del brief
|
|
96
|
+
4. Opcional: persistir brief en `.forge/features/<name>/brief.md` para futuras referencias
|
|
97
|
+
5. Crear estructura de directorios:
|
|
25
98
|
```
|
|
26
99
|
src/features/<name>/
|
|
27
100
|
├── domain/
|
|
@@ -32,18 +105,18 @@ Si alguno no existe, ejecutar `bootstrapPlatform()` automáticamente para crearl
|
|
|
32
105
|
├── in/http/
|
|
33
106
|
└── out/persistence/
|
|
34
107
|
```
|
|
35
|
-
|
|
108
|
+
6. Crear archivos del feature en este orden (ver `templates/feature/`):
|
|
36
109
|
- `<Name>.entity.ts` — interfaz de dominio
|
|
37
110
|
- `I<Name>Repository.ts` — puerto de repositorio
|
|
38
111
|
- `<Name>.mapper.ts` — mapper dominio ↔ persistencia
|
|
39
112
|
- `<Name>Schema.ts` — schema de BD (según perfil)
|
|
40
113
|
- `<Name>Repository.ts` — implementación del repositorio
|
|
41
|
-
- Use cases (`Create.ts`, `Get.ts`, `List.ts`, `Update.ts`, `Delete.ts`)
|
|
114
|
+
- Use cases (`Create.ts`, `Get.ts`, `List.ts`, `Update.ts`, `Delete.ts` — según brief)
|
|
42
115
|
- `<Name>Controller.ts` — controlador HTTP
|
|
43
116
|
- `<name>.routes.ts` — rutas HTTP
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
117
|
+
7. Registrar rutas en el enrutador principal
|
|
118
|
+
8. Ejecutar `forge quench` para verificar el feature
|
|
119
|
+
9. Actualizar `ARCHITECTURE.md` + estado persistente
|
|
47
120
|
|
|
48
121
|
## Convenciones
|
|
49
122
|
|
|
@@ -75,3 +148,4 @@ Usar el perfil detectado para determinar:
|
|
|
75
148
|
- `forge quench` — verificar que no hay violaciones
|
|
76
149
|
- `forge inspect` — confirmar puntuación
|
|
77
150
|
- `ARCHITECTURE.md` actualizado automáticamente
|
|
151
|
+
- `.forge/state.json` actualizado automáticamente
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Data Patterns — Repository, Unit of Work, CQRS, Event Sourcing
|
|
2
|
+
|
|
3
|
+
## Repository Pattern
|
|
4
|
+
|
|
5
|
+
### Reglas
|
|
6
|
+
|
|
7
|
+
- La interfaz del repositorio está en `domain/` del feature.
|
|
8
|
+
- La implementación está en `adapters/out/persistence/`.
|
|
9
|
+
- El use case recibe la interfaz por constructor y nunca sabe qué BD hay detrás.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// domain/IUser.repository.ts
|
|
13
|
+
export interface IUserRepository {
|
|
14
|
+
findById(id: string): Promise<UserEntity | null>;
|
|
15
|
+
findByEmail(email: string): Promise<UserEntity | null>;
|
|
16
|
+
save(user: UserEntity): Promise<UserEntity>;
|
|
17
|
+
delete(id: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// adapters/out/persistence/PostgresUser.repository.ts
|
|
23
|
+
export class PostgresUserRepository implements IUserRepository {
|
|
24
|
+
constructor(private readonly db: PrismaClient) {}
|
|
25
|
+
async findById(id: string) { … }
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Unit of Work
|
|
30
|
+
|
|
31
|
+
Para operaciones que requieren transacción entre múltiples repositorios.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// domain/IUnitOfWork.ts
|
|
35
|
+
export interface IUnitOfWork {
|
|
36
|
+
withTransaction<T>(fn: (uow: { userRepo: IUserRepository; accountRepo: IAccountRepository }) => Promise<T>): Promise<T>;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CQRS
|
|
41
|
+
|
|
42
|
+
Separar commands (escritura) de queries (lectura) solo cuando el modelo de lectura difiere significativamente del de escritura.
|
|
43
|
+
|
|
44
|
+
| Aspecto | Commands | Queries |
|
|
45
|
+
|---------|----------|---------|
|
|
46
|
+
| Feature dir | `application/use-cases/` | `application/queries/` |
|
|
47
|
+
| Retorno | `void` o ID | DTOs planos |
|
|
48
|
+
| Cache | No | Sí |
|
|
49
|
+
| Repositorio | Domain interface | Query interface separada |
|
|
50
|
+
|
|
51
|
+
## Event Sourcing
|
|
52
|
+
|
|
53
|
+
Para features que requieren auditoría completa o reconstrucción de estado histórico.
|
|
54
|
+
|
|
55
|
+
- Eventos en `domain/events/` dentro del feature
|
|
56
|
+
- Aggregate root: solo persiste eventos, nunca estado actual
|
|
57
|
+
- Proyecciones: tablas/materialized views que reflejan el estado actual
|
|
58
|
+
- Snapshotting cada N eventos para performance
|
|
59
|
+
|
|
60
|
+
### Estructura
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
features/<name>/
|
|
64
|
+
domain/
|
|
65
|
+
events/
|
|
66
|
+
UserCreated.event.ts
|
|
67
|
+
UserEmailChanged.event.ts
|
|
68
|
+
UserDeleted.event.ts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Cuándo usar cada patrón
|
|
72
|
+
|
|
73
|
+
| Patrón | Cuándo usarlo |
|
|
74
|
+
|--------|---------------|
|
|
75
|
+
| Repository | Siempre. Es el estándar para acceso a datos. |
|
|
76
|
+
| Unit of Work | Cuando una operación modifica 2+ agregados en una transacción. |
|
|
77
|
+
| CQRS | Cuando las queries de lectura son significativamente distintas a los comandos de escritura. |
|
|
78
|
+
| Event Sourcing | Solo cuando necesitas auditoría forense o reconstrucción de estado histórico. |
|
|
79
|
+
|
|
80
|
+
## Buenas prácticas
|
|
81
|
+
|
|
82
|
+
- Repositorios devuelven entidades de dominio, no schemas de BD
|
|
83
|
+
- Siempre hay un mapper entre schema de BD y entidad de dominio
|
|
84
|
+
- Transacciones en el Unit of Work, no en el use case ni en el repositorio individual
|
|
85
|
+
- CQRS no significa event sourcing; son patrones independientes
|
|
86
|
+
- Event sourcing sin snapshotting es inviable a escala
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# DI Strategies — Inyección de dependencias
|
|
2
|
+
|
|
3
|
+
## Principios
|
|
4
|
+
|
|
5
|
+
- Constructor injection sobre service locators o decoradores mágicos.
|
|
6
|
+
- El dominio nunca importa el contenedor. Solo application y adapters.
|
|
7
|
+
- Interfaces en domain/; implementaciones en adapters/out/ o platform/.
|
|
8
|
+
|
|
9
|
+
## Estrategias según tamaño del proyecto
|
|
10
|
+
|
|
11
|
+
| Tamaño | Estrategia | Cuándo usarla |
|
|
12
|
+
|--------|-----------|---------------|
|
|
13
|
+
| Pequeño (< 10 features) | DI Manual | Wires explícitos en composición root (src/app.ts) |
|
|
14
|
+
| Mediano (10-30 features) | Contenedor ligero | awilix, tsyringe, o el DI del framework |
|
|
15
|
+
| Grande (> 30 features) | Contenedor con módulos | NestJS modular, inversify con módulos lazy |
|
|
16
|
+
|
|
17
|
+
## DI Manual
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// src/app.ts — Composition Root
|
|
21
|
+
import { PostgresUserRepository } from "./features/users/adapters/out/persistence/PostgresUser.repository.js";
|
|
22
|
+
import { CreateUserUseCase } from "./features/users/application/use-cases/CreateUser.uc.js";
|
|
23
|
+
import { UserController } from "./features/users/adapters/in/http/User.controller.js";
|
|
24
|
+
|
|
25
|
+
const userRepo = new PostgresUserRepository(db);
|
|
26
|
+
const createUserUc = new CreateUserUseCase(userRepo);
|
|
27
|
+
const userController = new UserController(createUserUc);
|
|
28
|
+
|
|
29
|
+
router.post("/users", (req, res) => userController.create(req, res));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Contenedor (tsyringe)
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// di/Container.ts
|
|
36
|
+
import { container } from "tsyringe";
|
|
37
|
+
import { IUserRepository } from "@/features/users/domain/IUser.repository.js";
|
|
38
|
+
import { PostgresUserRepository } from "@/features/users/adapters/out/persistence/PostgresUser.repository.js";
|
|
39
|
+
|
|
40
|
+
container.registerSingleton<IUserRepository>("IUserRepository", PostgresUserRepository);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Buenas prácticas
|
|
44
|
+
|
|
45
|
+
- Composition Root único y lo más cercano al entrypoint posible
|
|
46
|
+
- Solo el Composition Root conoce todas las implementaciones concretas
|
|
47
|
+
- Preferir interfaces sobre clases abstractas en domain/
|
|
48
|
+
- Evitar `container.resolve()` fuera del Composition Root
|
|
49
|
+
- Usar tokens (strings o símbolos) para identificar dependencias
|
|
50
|
+
- Testear use cases con mocks manuales sin necesidad del contenedor
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Errors — Manejo de errores en arquitectura hexagonal
|
|
2
|
+
|
|
3
|
+
## Principios
|
|
4
|
+
|
|
5
|
+
- Los errores de dominio pertenecen al **dominio**, no al framework ni a HTTP.
|
|
6
|
+
- Los casos de uso lanzan errores de dominio; los adapters HTTP los traducen a respuestas HTTP.
|
|
7
|
+
- Nunca lanzar `throw new Error("...")` desde domain/application. Usar errores tipados.
|
|
8
|
+
|
|
9
|
+
## Estructura
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/shared/errors/
|
|
13
|
+
├── AppError.ts → Clase base abstracta (opcional)
|
|
14
|
+
├── NotFoundError.ts → 404
|
|
15
|
+
├── ValidationError.ts → 400 / 422
|
|
16
|
+
├── UnauthorizedError.ts → 401
|
|
17
|
+
├── ForbiddenError.ts → 403
|
|
18
|
+
├── ConflictError.ts → 409
|
|
19
|
+
└── DomainError.ts → 500 genérico de dominio
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Ejemplo
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// shared/errors/NotFoundError.ts
|
|
26
|
+
export class NotFoundError extends Error {
|
|
27
|
+
constructor(public readonly entity: string, public readonly id: string) {
|
|
28
|
+
super(`${entity} with id ${id} not found`);
|
|
29
|
+
this.name = "NotFoundError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// features/users/domain/IUser.repository.ts
|
|
36
|
+
import { NotFoundError } from "@/shared/errors/NotFoundError.js";
|
|
37
|
+
|
|
38
|
+
export interface IUserRepository {
|
|
39
|
+
findById(id: string): Promise<UserEntity | null>;
|
|
40
|
+
// el use case decide si lanza NotFoundError
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// features/users/adapters/in/http/User.controller.ts
|
|
46
|
+
import { NotFoundError } from "@/shared/errors/NotFoundError.js";
|
|
47
|
+
|
|
48
|
+
async getUser(req, res, next) {
|
|
49
|
+
try {
|
|
50
|
+
const user = await this.getUserUc.execute(req.params.id);
|
|
51
|
+
res.json(user);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err instanceof NotFoundError) return res.status(404).json({ error: err.message });
|
|
54
|
+
next(err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Buenas prácticas
|
|
60
|
+
|
|
61
|
+
- Errores de dominio en `domain/` (o shared/errors si son transversales)
|
|
62
|
+
- `Result<T, E>` pattern para casos donde el error es parte del flujo esperado
|
|
63
|
+
- Error handler centralizado en platform/http/ para errores no capturados
|
|
64
|
+
- Loggear errores en el adapter, no en el dominio
|
|
65
|
+
- Códigos de error consistentes: `DOMAIN_ENTITY_NOT_FOUND`, `VALIDATION_INVALID_EMAIL`
|