siesa-agents 2.1.25-dev.1 → 2.1.26
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 +7 -10
- package/bin/install.js +107 -9
- package/bmad-core/agents/architect.md +2 -0
- package/bmad-core/agents/ux-expert.md +1 -0
- package/bmad-core/data/architecture-patterns.md +261 -0
- package/bmad-core/data/backend-standards.md +127 -195
- package/bmad-core/data/technology-stack.md +81 -0
- package/claude/settings.local.json +1 -37
- package/package.json +1 -1
- package/claude/hooks/file-restriction-hook.py +0 -51
- package/claude/hooks/track-agent.py +0 -67
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SIESA
|
|
1
|
+
# SIESA Agents
|
|
2
2
|
|
|
3
3
|
Paquete para instalar y configurar agentes SIESA en tu proyecto.
|
|
4
4
|
|
|
@@ -52,6 +52,7 @@ El paquete instala las siguientes carpetas en tu directorio actual:
|
|
|
52
52
|
- **`.bmad-core/`** - Archivos principales del sistema BMAD
|
|
53
53
|
- **`.vscode/`** - Configuración de Visual Studio Code
|
|
54
54
|
- **`.github/`** - Configuración de GitHub Actions y workflows
|
|
55
|
+
- **`.claude/`** - Configuración de Claude Code Commands y workflows
|
|
55
56
|
|
|
56
57
|
## Características
|
|
57
58
|
|
|
@@ -70,22 +71,17 @@ El paquete instala las siguientes carpetas en tu directorio actual:
|
|
|
70
71
|
|
|
71
72
|
Para actualizar una instalación existente, simplemente ejecuta el comando nuevamente:
|
|
72
73
|
|
|
73
|
-
**Versión estable (recomendado):**
|
|
74
74
|
```bash
|
|
75
75
|
npx siesa-agents
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
**Versión de desarrollo:**
|
|
79
|
-
```bash
|
|
80
|
-
npx siesa-agents@dev
|
|
81
|
-
```
|
|
82
|
-
|
|
83
78
|
El sistema detectará automáticamente que ya existe una instalación y la actualizará.
|
|
84
79
|
|
|
85
80
|
## Requisitos
|
|
86
81
|
|
|
87
82
|
- Node.js >= 14.0.0
|
|
88
83
|
- npm >= 6.0.0
|
|
84
|
+
- Python >= 3.7 (requerido para hooks de Claude Code)
|
|
89
85
|
|
|
90
86
|
## Estructura de archivos instalados
|
|
91
87
|
|
|
@@ -99,8 +95,6 @@ tu-proyecto/
|
|
|
99
95
|
└── [workflows y configuración de GitHub]
|
|
100
96
|
```
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
|
|
104
98
|
## Soporte
|
|
105
99
|
|
|
106
100
|
Si encuentras algún problema durante la instalación, por favor verifica:
|
|
@@ -115,4 +109,7 @@ MIT
|
|
|
115
109
|
|
|
116
110
|
## Autor
|
|
117
111
|
|
|
118
|
-
SIESA - Sistemas de Información Empresarial
|
|
112
|
+
SIESA - Sistemas de Información Empresarial
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
*Versión actualizada automáticamente por CI/CD*
|
package/bin/install.js
CHANGED
|
@@ -4,7 +4,6 @@ const fs = require('fs-extra');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
class SiesaBmadInstaller {
|
|
9
8
|
constructor() {
|
|
10
9
|
// Definir las carpetas primero (nombres en el paquete vs nombres finales)
|
|
@@ -16,10 +15,20 @@ class SiesaBmadInstaller {
|
|
|
16
15
|
{ source: 'kiro', target: '.kiro' },
|
|
17
16
|
{ source: 'resources', target: '.resources' }
|
|
18
17
|
];
|
|
18
|
+
|
|
19
|
+
// Lista de archivos que se preservan automáticamente (no se crean backups)
|
|
20
|
+
this.ignoredFiles = [
|
|
21
|
+
'data/technical-preferences.md'
|
|
22
|
+
];
|
|
23
|
+
|
|
19
24
|
this.targetDir = process.cwd();
|
|
20
25
|
// Intentar múltiples ubicaciones posibles para el paquete
|
|
21
26
|
this.packageDir = this.findPackageDir();
|
|
27
|
+
|
|
28
|
+
// Almacenamiento temporal para contenido de archivos ignorados
|
|
29
|
+
this.preservedContent = new Map();
|
|
22
30
|
}
|
|
31
|
+
|
|
23
32
|
|
|
24
33
|
showBanner() {
|
|
25
34
|
console.log('\n');
|
|
@@ -29,8 +38,7 @@ class SiesaBmadInstaller {
|
|
|
29
38
|
console.log('╚════██║██║██╔══╝ ╚════██║██╔══██║ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║');
|
|
30
39
|
console.log('███████║██║███████╗███████║██║ ██║ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████║');
|
|
31
40
|
console.log('╚══════╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝');
|
|
32
|
-
console.log('
|
|
33
|
-
console.log('\n');
|
|
41
|
+
console.log('');
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
findPackageDir() {
|
|
@@ -127,7 +135,8 @@ class SiesaBmadInstaller {
|
|
|
127
135
|
modifiedFiles.push({
|
|
128
136
|
folder: mapping.target,
|
|
129
137
|
file: relativePath,
|
|
130
|
-
fullPath: targetFile
|
|
138
|
+
fullPath: targetFile,
|
|
139
|
+
is_ignored: this.ignoredFiles.includes(relativePath)
|
|
131
140
|
});
|
|
132
141
|
}
|
|
133
142
|
} catch (error) {
|
|
@@ -139,7 +148,8 @@ class SiesaBmadInstaller {
|
|
|
139
148
|
modifiedFiles.push({
|
|
140
149
|
folder: mapping.target,
|
|
141
150
|
file: relativePath,
|
|
142
|
-
fullPath: targetFile
|
|
151
|
+
fullPath: targetFile,
|
|
152
|
+
is_ignored: this.ignoredFiles.includes(relativePath)
|
|
143
153
|
});
|
|
144
154
|
}
|
|
145
155
|
}
|
|
@@ -170,6 +180,10 @@ class SiesaBmadInstaller {
|
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
async promptUser(modifiedFiles) {
|
|
183
|
+
|
|
184
|
+
const hasNonIgnoredFiles = modifiedFiles.some(file => file.is_ignored == false)
|
|
185
|
+
if (!hasNonIgnoredFiles) return '2'
|
|
186
|
+
|
|
173
187
|
console.log('\n⚠️ Se detectaron archivos modificados:');
|
|
174
188
|
|
|
175
189
|
// Agrupar por carpeta
|
|
@@ -210,6 +224,12 @@ class SiesaBmadInstaller {
|
|
|
210
224
|
console.log('\n🔄 Creando backup de archivos modificados...');
|
|
211
225
|
|
|
212
226
|
for (const item of modifiedFiles) {
|
|
227
|
+
// No crear backup de archivos ignorados
|
|
228
|
+
if (item.is_ignored) {
|
|
229
|
+
console.log(`✓ Preservando: ${item.file} (sin backup)`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
213
233
|
const originalPath = item.fullPath;
|
|
214
234
|
const backupPath = this.getBackupPath(originalPath);
|
|
215
235
|
|
|
@@ -273,11 +293,20 @@ class SiesaBmadInstaller {
|
|
|
273
293
|
async copyWithBackupPreservation(sourcePath, targetPath) {
|
|
274
294
|
// Obtener todos los archivos backup existentes
|
|
275
295
|
const backupFiles = await this.findBackupFiles(targetPath);
|
|
276
|
-
|
|
277
|
-
// Copiar la carpeta
|
|
296
|
+
|
|
297
|
+
// Copiar la carpeta preservando technical-preferences.md
|
|
278
298
|
await fs.copy(sourcePath, targetPath, {
|
|
279
299
|
overwrite: true,
|
|
280
|
-
recursive: true
|
|
300
|
+
recursive: true,
|
|
301
|
+
filter: (src) => {
|
|
302
|
+
const relativePath = path.relative(sourcePath, src);
|
|
303
|
+
// No sobrescribir archivos ignorados si ya existen
|
|
304
|
+
if (this.ignoredFiles.includes(relativePath)) {
|
|
305
|
+
const targetFile = path.join(targetPath, relativePath);
|
|
306
|
+
return !fs.existsSync(targetFile);
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
281
310
|
});
|
|
282
311
|
|
|
283
312
|
// Restaurar los archivos backup
|
|
@@ -332,7 +361,16 @@ class SiesaBmadInstaller {
|
|
|
332
361
|
if (fs.existsSync(sourcePath)) {
|
|
333
362
|
await fs.copy(sourcePath, targetPath, {
|
|
334
363
|
overwrite: true,
|
|
335
|
-
recursive: true
|
|
364
|
+
recursive: true,
|
|
365
|
+
filter: (src) => {
|
|
366
|
+
const relativePath = path.relative(sourcePath, src);
|
|
367
|
+
// No sobrescribir archivos ignorados si ya existen
|
|
368
|
+
if (this.ignoredFiles.includes(relativePath)) {
|
|
369
|
+
const targetFile = path.join(targetPath, relativePath);
|
|
370
|
+
return !fs.existsSync(targetFile);
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
336
374
|
});
|
|
337
375
|
} else {
|
|
338
376
|
console.warn(`⚠️ Carpeta ${mapping.source} no encontrada en el paquete`);
|
|
@@ -368,6 +406,9 @@ class SiesaBmadInstaller {
|
|
|
368
406
|
await this.performUpdateWithBackups();
|
|
369
407
|
} else {
|
|
370
408
|
// Si no hay backups, hacer actualización normal (remover y copiar)
|
|
409
|
+
// Pero primero preservar archivos ignorados
|
|
410
|
+
await this.preserveIgnoredFiles();
|
|
411
|
+
|
|
371
412
|
for (const mapping of this.folderMappings) {
|
|
372
413
|
const targetPath = path.join(this.targetDir, mapping.target);
|
|
373
414
|
|
|
@@ -378,7 +419,64 @@ class SiesaBmadInstaller {
|
|
|
378
419
|
|
|
379
420
|
// Realizar instalación nueva
|
|
380
421
|
await this.performInstallation();
|
|
422
|
+
|
|
423
|
+
// Restaurar archivos ignorados
|
|
424
|
+
await this.restoreIgnoredFiles();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async preserveIgnoredFiles() {
|
|
429
|
+
console.log('🔒 Preservando archivos de configuración...');
|
|
430
|
+
|
|
431
|
+
for (const mapping of this.folderMappings) {
|
|
432
|
+
const targetFolderPath = path.join(this.targetDir, mapping.target);
|
|
433
|
+
|
|
434
|
+
if (!fs.existsSync(targetFolderPath)) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const ignoredFile of this.ignoredFiles) {
|
|
439
|
+
const filePath = path.join(targetFolderPath, ignoredFile);
|
|
440
|
+
|
|
441
|
+
if (fs.existsSync(filePath)) {
|
|
442
|
+
try {
|
|
443
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
444
|
+
const key = `${mapping.target}/${ignoredFile}`;
|
|
445
|
+
this.preservedContent.set(key, content);
|
|
446
|
+
console.log(`✓ Preservando: ${ignoredFile}`);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.warn(`⚠️ Error leyendo ${ignoredFile}: ${error.message}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async restoreIgnoredFiles() {
|
|
456
|
+
if (this.preservedContent.size === 0) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
console.log('🔄 Restaurando archivos de configuración...');
|
|
461
|
+
|
|
462
|
+
for (const [key, content] of this.preservedContent) {
|
|
463
|
+
const [targetFolder, ...filePathParts] = key.split('/');
|
|
464
|
+
const filePath = path.join(this.targetDir, targetFolder, ...filePathParts);
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
// Asegurar que el directorio existe
|
|
468
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
469
|
+
|
|
470
|
+
// Restaurar el contenido
|
|
471
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
472
|
+
console.log(`✓ Restaurado: ${filePathParts.join('/')}`);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.warn(`⚠️ Error restaurando ${filePathParts.join('/')}: ${error.message}`);
|
|
475
|
+
}
|
|
381
476
|
}
|
|
477
|
+
|
|
478
|
+
// Limpiar el mapa después de restaurar
|
|
479
|
+
this.preservedContent.clear();
|
|
382
480
|
}
|
|
383
481
|
|
|
384
482
|
showPostInstallMessage() {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Architecture Patterns & Design Decisions
|
|
2
|
+
|
|
3
|
+
## Frontend Architecture
|
|
4
|
+
|
|
5
|
+
### Architecture Style
|
|
6
|
+
- **Clean Architecture** + **Domain-Driven Design (DDD)**
|
|
7
|
+
|
|
8
|
+
### Folder Structure
|
|
9
|
+
|
|
10
|
+
Next.js 15 App Router Structure with Clean Architecture + DDD:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
├── app/ # Next.js App Router directory
|
|
14
|
+
│ ├── (dashboard)/ # Route groups for dashboard
|
|
15
|
+
│ ├── sales/ # Routes for sales module
|
|
16
|
+
│ │ ├── quotes/ # Quote management pages
|
|
17
|
+
│ │ └── invoices/ # Invoice pages
|
|
18
|
+
│ ├── inventory/ # Inventory routes
|
|
19
|
+
│ ├── globals.css # Global styles
|
|
20
|
+
│ ├── layout.tsx # Root layout component
|
|
21
|
+
│ ├── page.tsx # Home page
|
|
22
|
+
│ ├── loading.tsx # Global loading UI
|
|
23
|
+
│ └── not-found.tsx # 404 page
|
|
24
|
+
│
|
|
25
|
+
├── src/
|
|
26
|
+
│ ├── modules/ # Business modules following DDD
|
|
27
|
+
│ │ ├── sales/ # Sales module
|
|
28
|
+
│ │ │ ├── quotes/ # Quote domain
|
|
29
|
+
│ │ │ │ ├── cart/ # Shopping cart feature
|
|
30
|
+
│ │ │ │ │ ├── domain/
|
|
31
|
+
│ │ │ │ │ │ ├── entities/
|
|
32
|
+
│ │ │ │ │ │ ├── repositories/
|
|
33
|
+
│ │ │ │ │ │ ├── services/
|
|
34
|
+
│ │ │ │ │ │ └── types/
|
|
35
|
+
│ │ │ │ │ ├── application/
|
|
36
|
+
│ │ │ │ │ │ ├── use-cases/
|
|
37
|
+
│ │ │ │ │ │ ├── hooks/
|
|
38
|
+
│ │ │ │ │ │ └── store/
|
|
39
|
+
│ │ │ │ │ ├── infrastructure/
|
|
40
|
+
│ │ │ │ │ │ ├── repositories/
|
|
41
|
+
│ │ │ │ │ │ ├── api/
|
|
42
|
+
│ │ │ │ │ │ └── adapters/
|
|
43
|
+
│ │ │ │ │ └── presentation/
|
|
44
|
+
│ │ │ │ │ └── components/ # Only components, pages in app/
|
|
45
|
+
│ │ │ │ └── products/ # Products feature
|
|
46
|
+
│ │ │ └── billing/ # Billing domain
|
|
47
|
+
│ │ ├── inventory/ # Inventory module
|
|
48
|
+
│ │ └── users/ # User module
|
|
49
|
+
│ │
|
|
50
|
+
│ ├── shared/
|
|
51
|
+
│ │ ├── components/ # Reusable UI components
|
|
52
|
+
│ │ ├── hooks/ # Shared hooks
|
|
53
|
+
│ │ ├── utils/ # Utility functions
|
|
54
|
+
│ │ ├── types/ # Common TypeScript types
|
|
55
|
+
│ │ └── constants/ # App constants
|
|
56
|
+
│ │
|
|
57
|
+
│ ├── providers/ # React context providers
|
|
58
|
+
│ ├── store/ # Global Zustand stores
|
|
59
|
+
│ └── middleware.ts # Next.js middleware
|
|
60
|
+
│
|
|
61
|
+
├── lib/ # Next.js utilities and configurations
|
|
62
|
+
├── components/ # Global UI components (alternative to src/shared)
|
|
63
|
+
├── public/ # Static assets and PWA manifest
|
|
64
|
+
└── styles/ # Additional stylesheets
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Core Principles
|
|
68
|
+
|
|
69
|
+
#### Clean Architecture First
|
|
70
|
+
Strict separation of:
|
|
71
|
+
- **Domain layer** - Business entities, repositories interfaces, domain services, and types
|
|
72
|
+
- **Application layer** - Use cases, hooks, and state management (Zustand stores)
|
|
73
|
+
- **Infrastructure layer** - Repository implementations, API clients, and adapters
|
|
74
|
+
- **Presentation layer** - UI components (pages go in `app/`, components in `presentation/`)
|
|
75
|
+
|
|
76
|
+
#### Domain-Driven Design
|
|
77
|
+
Business logic drives architecture decisions. Organize by business modules and domains, not technical layers.
|
|
78
|
+
|
|
79
|
+
#### Component Composition
|
|
80
|
+
Build complex UIs from simple, reusable components.
|
|
81
|
+
|
|
82
|
+
#### Type Safety
|
|
83
|
+
Leverage TypeScript for compile-time safety and developer experience.
|
|
84
|
+
|
|
85
|
+
#### Performance by Design
|
|
86
|
+
- Lazy loading
|
|
87
|
+
- Memoization
|
|
88
|
+
- Bundle optimization
|
|
89
|
+
|
|
90
|
+
#### Accessibility as Standard
|
|
91
|
+
WCAG 2.1 AA compliance in all components.
|
|
92
|
+
|
|
93
|
+
#### Test-Driven Development
|
|
94
|
+
Unit tests for all use cases and components.
|
|
95
|
+
|
|
96
|
+
#### Progressive Web App
|
|
97
|
+
Offline-first approach with service workers.
|
|
98
|
+
|
|
99
|
+
#### Minimal and Functional
|
|
100
|
+
Only build what's explicitly requested, nothing more.
|
|
101
|
+
|
|
102
|
+
#### User-Centered Design
|
|
103
|
+
Start with user needs and work backward to implementation.
|
|
104
|
+
|
|
105
|
+
#### MCP Shadcn Available
|
|
106
|
+
Use MCP to install Shadcn components instead of creating manually.
|
|
107
|
+
|
|
108
|
+
### Framework Selection Rules
|
|
109
|
+
|
|
110
|
+
**Default**: Always use Next.js 15 with App Router unless explicitly told otherwise.
|
|
111
|
+
|
|
112
|
+
**Exceptions**: Only use pure React + Vite when user specifically mentions offline-first functionality or requests non-Next.js setup.
|
|
113
|
+
|
|
114
|
+
**Reasoning**: Next.js provides better developer experience, built-in optimization, and easier deployment while maintaining PWA capabilities.
|
|
115
|
+
|
|
116
|
+
## Backend Architecture
|
|
117
|
+
|
|
118
|
+
### Architecture Style
|
|
119
|
+
- **Hexagonal Architecture** (Ports & Adapters) + **Domain-Driven Design (DDD)**
|
|
120
|
+
|
|
121
|
+
### Folder Structure
|
|
122
|
+
|
|
123
|
+
MonoRepo Structure with Hexagonal Architecture + DDD:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
├── apps/ # Microservices applications
|
|
127
|
+
│ ├── sales-service/ # Sales domain microservice
|
|
128
|
+
│ │ ├── src/
|
|
129
|
+
│ │ │ ├── modules/
|
|
130
|
+
│ │ │ │ ├── quotes/ # Quote bounded context
|
|
131
|
+
│ │ │ │ │ ├── application/
|
|
132
|
+
│ │ │ │ │ │ ├── ports/ # Interfaces (secondary ports)
|
|
133
|
+
│ │ │ │ │ │ │ ├── repositories/
|
|
134
|
+
│ │ │ │ │ │ │ └── services/
|
|
135
|
+
│ │ │ │ │ │ ├── use-cases/ # Primary ports
|
|
136
|
+
│ │ │ │ │ │ ├── commands/
|
|
137
|
+
│ │ │ │ │ │ ├── queries/
|
|
138
|
+
│ │ │ │ │ │ └── dto/
|
|
139
|
+
│ │ │ │ │ ├── domain/
|
|
140
|
+
│ │ │ │ │ │ ├── entities/
|
|
141
|
+
│ │ │ │ │ │ ├── value-objects/
|
|
142
|
+
│ │ │ │ │ │ ├── aggregates/
|
|
143
|
+
│ │ │ │ │ │ ├── events/
|
|
144
|
+
│ │ │ │ │ │ └── services/ # Domain services
|
|
145
|
+
│ │ │ │ │ └── infrastructure/ # Adapters (secondary adapters)
|
|
146
|
+
│ │ │ │ │ ├── repositories/ # Prisma implementations
|
|
147
|
+
│ │ │ │ │ ├── services/ # External service adapters
|
|
148
|
+
│ │ │ │ │ └── events/
|
|
149
|
+
│ │ │ │ └── products/ # Product bounded context
|
|
150
|
+
│ │ │ ├── api/ # Primary adapters
|
|
151
|
+
│ │ │ │ ├── controllers/
|
|
152
|
+
│ │ │ │ ├── guards/
|
|
153
|
+
│ │ │ │ ├── middlewares/
|
|
154
|
+
│ │ │ │ └── filters/
|
|
155
|
+
│ │ │ ├── config/
|
|
156
|
+
│ │ │ ├── main.ts
|
|
157
|
+
│ │ │ └── app.module.ts
|
|
158
|
+
│ │ ├── test/
|
|
159
|
+
│ │ ├── prisma/
|
|
160
|
+
│ │ │ ├── schema.prisma
|
|
161
|
+
│ │ │ └── migrations/
|
|
162
|
+
│ │ └── package.json
|
|
163
|
+
│ │
|
|
164
|
+
│ ├── inventory-service/ # Inventory domain microservice
|
|
165
|
+
│ └── user-service/ # User domain microservice
|
|
166
|
+
│
|
|
167
|
+
├── libs/ # Shared libraries
|
|
168
|
+
│ ├── common/ # Common utilities
|
|
169
|
+
│ │ ├── src/
|
|
170
|
+
│ │ │ ├── decorators/
|
|
171
|
+
│ │ │ ├── filters/
|
|
172
|
+
│ │ │ ├── guards/
|
|
173
|
+
│ │ │ ├── interceptors/
|
|
174
|
+
│ │ │ ├── pipes/
|
|
175
|
+
│ │ │ ├── types/
|
|
176
|
+
│ │ │ └── utils/
|
|
177
|
+
│ │ └── package.json
|
|
178
|
+
│ │
|
|
179
|
+
│ ├── domain-core/ # Shared domain concepts
|
|
180
|
+
│ │ ├── src/
|
|
181
|
+
│ │ │ ├── base/
|
|
182
|
+
│ │ │ │ ├── aggregate-root.ts
|
|
183
|
+
│ │ │ │ ├── entity.ts
|
|
184
|
+
│ │ │ │ ├── value-object.ts
|
|
185
|
+
│ │ │ │ └── domain-event.ts
|
|
186
|
+
│ │ │ ├── interfaces/
|
|
187
|
+
│ │ │ └── exceptions/
|
|
188
|
+
│ │ └── package.json
|
|
189
|
+
│ │
|
|
190
|
+
│ └── database/ # Shared database utilities
|
|
191
|
+
│ ├── src/
|
|
192
|
+
│ │ ├── base-repository.ts
|
|
193
|
+
│ │ ├── transaction.decorator.ts
|
|
194
|
+
│ │ └── prisma.service.ts
|
|
195
|
+
│ └── package.json
|
|
196
|
+
│
|
|
197
|
+
├── tools/ # Development tools
|
|
198
|
+
├── nx.json # Nx workspace configuration
|
|
199
|
+
├── package.json # Root package.json
|
|
200
|
+
└── tsconfig.base.json # Base TypeScript config
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Core Principles
|
|
204
|
+
|
|
205
|
+
#### Hexagonal Architecture First
|
|
206
|
+
Strict separation of concerns following ports & adapters pattern:
|
|
207
|
+
- **Domain Layer**: Pure business logic with entities, value objects, aggregates, and domain services
|
|
208
|
+
- **Application Layer**: Use cases orchestrating domain logic, defining ports (interfaces)
|
|
209
|
+
- **Infrastructure Layer**: Adapters implementing ports (Prisma repos, HTTP clients, message publishers)
|
|
210
|
+
- **API Layer**: Primary adapters exposing application via REST/GraphQL (controllers, resolvers)
|
|
211
|
+
|
|
212
|
+
#### Domain-Driven Design
|
|
213
|
+
Business logic drives all architectural decisions:
|
|
214
|
+
- **Bounded Contexts**: Each module represents a bounded context (quotes, products, billing)
|
|
215
|
+
- **Ubiquitous Language**: Code reflects business terminology
|
|
216
|
+
- **Aggregates**: Consistency boundaries for domain entities
|
|
217
|
+
- **Domain Events**: Communicate changes across bounded contexts
|
|
218
|
+
- **Repository Pattern**: Abstract data access behind interfaces
|
|
219
|
+
|
|
220
|
+
#### Dependency Rules
|
|
221
|
+
- Domain layer has zero dependencies on frameworks or external libraries
|
|
222
|
+
- All dependencies point inward toward the domain core
|
|
223
|
+
- Use dependency inversion for all external concerns (databases, APIs, messaging)
|
|
224
|
+
- Interfaces defined in application layer, implementations in infrastructure layer
|
|
225
|
+
|
|
226
|
+
#### Microservices Independence
|
|
227
|
+
- Each microservice has its own database (no shared databases)
|
|
228
|
+
- Shared code through libraries only (common, domain-core, database)
|
|
229
|
+
- Independent deployment pipelines per service
|
|
230
|
+
- Service-to-service communication via events (async messaging)
|
|
231
|
+
- No direct database access between services
|
|
232
|
+
|
|
233
|
+
#### Test-Driven Development
|
|
234
|
+
- Unit tests for domain entities, value objects, and use cases
|
|
235
|
+
- Integration tests for repository implementations
|
|
236
|
+
- E2E tests for complete API workflows
|
|
237
|
+
- TDD approach: write tests before implementation
|
|
238
|
+
|
|
239
|
+
#### Type Safety & Validation
|
|
240
|
+
- Leverage TypeScript strict mode for compile-time safety
|
|
241
|
+
- Domain validation in value objects and entities
|
|
242
|
+
- DTO validation at API boundaries with class-validator
|
|
243
|
+
- No `any` types allowed
|
|
244
|
+
|
|
245
|
+
#### Security by Design
|
|
246
|
+
- Authentication and authorization at every layer
|
|
247
|
+
- Input validation on all endpoints
|
|
248
|
+
- OWASP Top 10 compliance
|
|
249
|
+
- Audit logging for critical operations
|
|
250
|
+
|
|
251
|
+
### Framework Selection Rules
|
|
252
|
+
|
|
253
|
+
**Default**: Always use NestJS 10+ with TypeScript for backend services.
|
|
254
|
+
|
|
255
|
+
**Database**: Prisma ORM only - no raw SQL queries allowed.
|
|
256
|
+
|
|
257
|
+
**Testing**: Jest + Supertest with TDD approach.
|
|
258
|
+
|
|
259
|
+
**Documentation**: Swagger/OpenAPI auto-generated from decorators.
|
|
260
|
+
|
|
261
|
+
**Reasoning**: NestJS provides excellent DI container, decorator-based development, and native support for microservices patterns while enforcing SOLID principles.
|
|
@@ -1,43 +1,32 @@
|
|
|
1
1
|
# Backend Development Standards
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
### Hexagonal Architecture Implementation
|
|
6
|
-
- **Application Core**: Domain entities, value objects, aggregates, and domain services
|
|
7
|
-
- **Primary Ports**: Use cases, commands, queries, and application services
|
|
8
|
-
- **Primary Adapters**: REST controllers, GraphQL resolvers, message handlers
|
|
9
|
-
- **Secondary Ports**: Repository interfaces, external service interfaces
|
|
10
|
-
- **Secondary Adapters**: Prisma repositories, HTTP clients, message publishers
|
|
11
|
-
|
|
12
|
-
### Dependency Rules
|
|
13
|
-
- Application core must not depend on external frameworks
|
|
14
|
-
- All dependencies point inward toward the domain
|
|
15
|
-
- Use dependency inversion for all external concerns
|
|
16
|
-
- Interfaces defined in application layer, implementations in infrastructure
|
|
3
|
+
> **Note**: For architecture patterns and principles (Hexagonal Architecture, DDD, folder structure), see [architecture-patterns.md](./architecture-patterns.md)
|
|
17
4
|
|
|
18
5
|
## Technology Stack Standards
|
|
19
6
|
|
|
20
7
|
### Core Technologies
|
|
21
8
|
- **NestJS**: 10+ with TypeScript and decorators
|
|
22
9
|
- **TypeScript**: Strict mode enabled, no `any` types
|
|
23
|
-
- **Prisma**: ORM for database operations (no raw queries)
|
|
24
|
-
- **Jest**: Unit and
|
|
25
|
-
- **Class-validator**:
|
|
10
|
+
- **Prisma**: ORM for database operations (no raw queries allowed)
|
|
11
|
+
- **Jest + Supertest**: Unit, integration, and E2E testing
|
|
12
|
+
- **Class-validator + Class-transformer**: DTO validation
|
|
26
13
|
|
|
27
|
-
### Framework
|
|
28
|
-
- **Default**:
|
|
29
|
-
- **Database**: Prisma ORM only - no raw SQL queries
|
|
30
|
-
- **Testing**: TDD approach with
|
|
31
|
-
- **Documentation**: Swagger/OpenAPI
|
|
14
|
+
### Framework Standards
|
|
15
|
+
- **Default Framework**: NestJS 10+ with TypeScript
|
|
16
|
+
- **Database**: Prisma ORM only - no raw SQL queries
|
|
17
|
+
- **Testing**: TDD approach with comprehensive test coverage
|
|
18
|
+
- **Documentation**: Swagger/OpenAPI auto-generated from decorators
|
|
19
|
+
- **Messaging**: NestJS Microservices (Redis, RabbitMQ, or gRPC)
|
|
32
20
|
|
|
33
21
|
### Development Tools
|
|
34
|
-
- **Nx**: MonoRepo management and build
|
|
22
|
+
- **Nx**: MonoRepo management and build orchestration
|
|
35
23
|
- **ESLint + Prettier**: Code quality and formatting
|
|
36
|
-
- **Husky**:
|
|
37
|
-
- **Winston**: Structured logging
|
|
38
|
-
- **Redis**: Caching and message transport
|
|
24
|
+
- **Husky**: Pre-commit hooks for quality gates
|
|
25
|
+
- **Winston**: Structured logging with log levels
|
|
26
|
+
- **Redis**: Caching, session storage, and message transport
|
|
27
|
+
- **Passport + JWT**: Authentication and authorization
|
|
39
28
|
|
|
40
|
-
## Domain-Driven Design
|
|
29
|
+
## Domain-Driven Design Implementation
|
|
41
30
|
|
|
42
31
|
### Entity Structure
|
|
43
32
|
```typescript
|
|
@@ -46,7 +35,6 @@ export class UserEntity extends AggregateRoot {
|
|
|
46
35
|
public readonly id: UserId,
|
|
47
36
|
private _email: EmailValueObject,
|
|
48
37
|
private _name: NameValueObject,
|
|
49
|
-
private _createdAt: Date,
|
|
50
38
|
) {
|
|
51
39
|
super();
|
|
52
40
|
}
|
|
@@ -56,7 +44,6 @@ export class UserEntity extends AggregateRoot {
|
|
|
56
44
|
UserId.generate(),
|
|
57
45
|
EmailValueObject.create(props.email),
|
|
58
46
|
NameValueObject.create(props.name),
|
|
59
|
-
new Date(),
|
|
60
47
|
);
|
|
61
48
|
user.addDomainEvent(new UserCreatedEvent(user.id));
|
|
62
49
|
return user;
|
|
@@ -91,11 +78,6 @@ export class EmailValueObject {
|
|
|
91
78
|
}
|
|
92
79
|
}
|
|
93
80
|
|
|
94
|
-
private isValidEmail(email: string): boolean {
|
|
95
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
96
|
-
return emailRegex.test(email);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
81
|
equals(other: EmailValueObject): boolean {
|
|
100
82
|
return this.value === other.value;
|
|
101
83
|
}
|
|
@@ -106,15 +88,45 @@ export class EmailValueObject {
|
|
|
106
88
|
}
|
|
107
89
|
```
|
|
108
90
|
|
|
109
|
-
### Repository
|
|
91
|
+
### Repository Pattern
|
|
110
92
|
```typescript
|
|
93
|
+
// Interface (in application/ports/repositories)
|
|
111
94
|
export interface UserRepositoryInterface {
|
|
112
95
|
save(user: UserEntity): Promise<UserEntity>;
|
|
113
96
|
findById(id: UserId): Promise<UserEntity | null>;
|
|
114
97
|
findByEmail(email: EmailValueObject): Promise<UserEntity | null>;
|
|
115
|
-
findAll(criteria: FindUsersCriteria): Promise<UserEntity[]>;
|
|
116
98
|
delete(id: UserId): Promise<void>;
|
|
117
99
|
}
|
|
100
|
+
|
|
101
|
+
// Implementation (in infrastructure/repositories)
|
|
102
|
+
@Injectable()
|
|
103
|
+
export class PrismaUserRepository implements UserRepositoryInterface {
|
|
104
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
105
|
+
|
|
106
|
+
async save(user: UserEntity): Promise<UserEntity> {
|
|
107
|
+
const data = {
|
|
108
|
+
id: user.id.toString(),
|
|
109
|
+
email: user.email.toString(),
|
|
110
|
+
name: user.name.toString(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const savedUser = await this.prisma.user.upsert({
|
|
114
|
+
where: { id: data.id },
|
|
115
|
+
update: data,
|
|
116
|
+
create: data,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return this.toDomain(savedUser);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private toDomain(prismaUser: User): UserEntity {
|
|
123
|
+
return UserEntity.reconstitute({
|
|
124
|
+
id: UserId.create(prismaUser.id),
|
|
125
|
+
email: EmailValueObject.create(prismaUser.email),
|
|
126
|
+
name: NameValueObject.create(prismaUser.name),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
118
130
|
```
|
|
119
131
|
|
|
120
132
|
## Use Case Standards
|
|
@@ -133,20 +145,19 @@ export class CreateUserUseCase {
|
|
|
133
145
|
async execute(command: CreateUserCommand): Promise<UserResponseDto> {
|
|
134
146
|
// 1. Validate business rules
|
|
135
147
|
await this.validateUserDoesNotExist(command.email);
|
|
136
|
-
|
|
148
|
+
|
|
137
149
|
// 2. Create domain entity
|
|
138
150
|
const user = UserEntity.create({
|
|
139
151
|
email: command.email,
|
|
140
152
|
name: command.name,
|
|
141
153
|
});
|
|
142
|
-
|
|
154
|
+
|
|
143
155
|
// 3. Persist entity
|
|
144
156
|
const savedUser = await this.userRepository.save(user);
|
|
145
|
-
|
|
157
|
+
|
|
146
158
|
// 4. Publish domain events
|
|
147
159
|
await this.eventBus.publishAll(savedUser.getUncommittedEvents());
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
|
|
150
161
|
// 5. Return response DTO
|
|
151
162
|
return UserResponseDto.fromEntity(savedUser);
|
|
152
163
|
}
|
|
@@ -172,20 +183,16 @@ export class CreateUserCommand {
|
|
|
172
183
|
@IsNotEmpty()
|
|
173
184
|
@Length(2, 50)
|
|
174
185
|
readonly name: string;
|
|
175
|
-
|
|
176
|
-
@IsOptional()
|
|
177
|
-
@IsString()
|
|
178
|
-
readonly organizationId?: string;
|
|
179
186
|
}
|
|
180
187
|
```
|
|
181
188
|
|
|
182
189
|
## Testing Standards
|
|
183
190
|
|
|
184
191
|
### Testing Strategy
|
|
185
|
-
- **Unit Tests**: Domain entities, value objects, use cases
|
|
186
|
-
- **Integration Tests**: Repository implementations,
|
|
187
|
-
- **E2E Tests**: Complete API workflows
|
|
188
|
-
- **
|
|
192
|
+
- **Unit Tests**: Domain entities, value objects, use cases (isolated logic)
|
|
193
|
+
- **Integration Tests**: Repository implementations, database operations
|
|
194
|
+
- **E2E Tests**: Complete API workflows with Supertest
|
|
195
|
+
- **TDD Approach**: Write tests before implementation
|
|
189
196
|
|
|
190
197
|
### Test Structure
|
|
191
198
|
```typescript
|
|
@@ -198,19 +205,8 @@ describe('CreateUserUseCase', () => {
|
|
|
198
205
|
const module = await Test.createTestingModule({
|
|
199
206
|
providers: [
|
|
200
207
|
CreateUserUseCase,
|
|
201
|
-
{
|
|
202
|
-
|
|
203
|
-
useValue: {
|
|
204
|
-
save: jest.fn(),
|
|
205
|
-
findByEmail: jest.fn(),
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
provide: EVENT_BUS,
|
|
210
|
-
useValue: {
|
|
211
|
-
publishAll: jest.fn(),
|
|
212
|
-
},
|
|
213
|
-
},
|
|
208
|
+
{ provide: USER_REPOSITORY, useValue: { save: jest.fn(), findByEmail: jest.fn() } },
|
|
209
|
+
{ provide: EVENT_BUS, useValue: { publishAll: jest.fn() } },
|
|
214
210
|
],
|
|
215
211
|
}).compile();
|
|
216
212
|
|
|
@@ -219,46 +215,33 @@ describe('CreateUserUseCase', () => {
|
|
|
219
215
|
eventBus = module.get(EVENT_BUS);
|
|
220
216
|
});
|
|
221
217
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
command.name = 'Test User';
|
|
228
|
-
|
|
229
|
-
const expectedUser = UserEntity.create({
|
|
230
|
-
email: command.email,
|
|
231
|
-
name: command.name,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
userRepository.findByEmail.mockResolvedValue(null);
|
|
235
|
-
userRepository.save.mockResolvedValue(expectedUser);
|
|
218
|
+
it('should create user successfully', async () => {
|
|
219
|
+
// Arrange
|
|
220
|
+
const command = new CreateUserCommand();
|
|
221
|
+
command.email = 'test@example.com';
|
|
222
|
+
command.name = 'Test User';
|
|
236
223
|
|
|
237
|
-
|
|
238
|
-
|
|
224
|
+
userRepository.findByEmail.mockResolvedValue(null);
|
|
225
|
+
userRepository.save.mockResolvedValue(UserEntity.create(command));
|
|
239
226
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
expect(userRepository.save).toHaveBeenCalledWith(expect.any(UserEntity));
|
|
243
|
-
expect(eventBus.publishAll).toHaveBeenCalled();
|
|
244
|
-
});
|
|
227
|
+
// Act
|
|
228
|
+
const result = await useCase.execute(command);
|
|
245
229
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
230
|
+
// Assert
|
|
231
|
+
expect(result.email).toBe(command.email);
|
|
232
|
+
expect(userRepository.save).toHaveBeenCalledWith(expect.any(UserEntity));
|
|
233
|
+
expect(eventBus.publishAll).toHaveBeenCalled();
|
|
234
|
+
});
|
|
251
235
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
236
|
+
it('should throw error when user already exists', async () => {
|
|
237
|
+
// Arrange
|
|
238
|
+
const command = new CreateUserCommand();
|
|
239
|
+
command.email = 'existing@example.com';
|
|
256
240
|
|
|
257
|
-
|
|
241
|
+
userRepository.findByEmail.mockResolvedValue(UserEntity.create(command));
|
|
258
242
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
243
|
+
// Act & Assert
|
|
244
|
+
await expect(useCase.execute(command)).rejects.toThrow(UserAlreadyExistsException);
|
|
262
245
|
});
|
|
263
246
|
});
|
|
264
247
|
```
|
|
@@ -277,23 +260,20 @@ export class UserController {
|
|
|
277
260
|
|
|
278
261
|
@Post()
|
|
279
262
|
@ApiOperation({ summary: 'Create a new user' })
|
|
280
|
-
@ApiResponse({ status: 201,
|
|
263
|
+
@ApiResponse({ status: 201, type: UserResponseDto })
|
|
281
264
|
@ApiResponse({ status: 400, description: 'Bad request' })
|
|
282
|
-
@
|
|
283
|
-
async createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
|
265
|
+
async createUser(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
|
|
284
266
|
const command = new CreateUserCommand();
|
|
285
|
-
Object.assign(command,
|
|
267
|
+
Object.assign(command, dto);
|
|
286
268
|
return this.createUserUseCase.execute(command);
|
|
287
269
|
}
|
|
288
270
|
|
|
289
271
|
@Get(':id')
|
|
290
272
|
@ApiOperation({ summary: 'Get user by ID' })
|
|
291
|
-
@
|
|
292
|
-
@ApiResponse({ status: 200, description: 'User found', type: UserResponseDto })
|
|
273
|
+
@ApiResponse({ status: 200, type: UserResponseDto })
|
|
293
274
|
@ApiResponse({ status: 404, description: 'User not found' })
|
|
294
275
|
async getUser(@Param('id', ParseUUIDPipe) id: string): Promise<UserResponseDto> {
|
|
295
|
-
|
|
296
|
-
return this.getUserUseCase.execute(query);
|
|
276
|
+
return this.getUserUseCase.execute(new GetUserQuery(id));
|
|
297
277
|
}
|
|
298
278
|
}
|
|
299
279
|
```
|
|
@@ -308,9 +288,7 @@ model User {
|
|
|
308
288
|
name String
|
|
309
289
|
createdAt DateTime @default(now())
|
|
310
290
|
updatedAt DateTime @updatedAt
|
|
311
|
-
|
|
312
|
-
// Relationships
|
|
313
|
-
orders Order[]
|
|
291
|
+
orders Order[]
|
|
314
292
|
|
|
315
293
|
@@map("users")
|
|
316
294
|
}
|
|
@@ -320,8 +298,6 @@ model Order {
|
|
|
320
298
|
total Decimal @db.Decimal(10, 2)
|
|
321
299
|
status OrderStatus
|
|
322
300
|
userId String
|
|
323
|
-
|
|
324
|
-
// Relationships
|
|
325
301
|
user User @relation(fields: [userId], references: [id])
|
|
326
302
|
items OrderItem[]
|
|
327
303
|
|
|
@@ -337,104 +313,60 @@ enum OrderStatus {
|
|
|
337
313
|
}
|
|
338
314
|
```
|
|
339
315
|
|
|
340
|
-
###
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const data = {
|
|
348
|
-
id: user.id.toString(),
|
|
349
|
-
email: user.email.toString(),
|
|
350
|
-
name: user.name.toString(),
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const savedUser = await this.prisma.user.upsert({
|
|
354
|
-
where: { id: data.id },
|
|
355
|
-
update: data,
|
|
356
|
-
create: data,
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
return this.toDomain(savedUser);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async findById(id: UserId): Promise<UserEntity | null> {
|
|
363
|
-
const user = await this.prisma.user.findUnique({
|
|
364
|
-
where: { id: id.toString() },
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
return user ? this.toDomain(user) : null;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
private toDomain(prismaUser: User): UserEntity {
|
|
371
|
-
return UserEntity.reconstitute({
|
|
372
|
-
id: UserId.create(prismaUser.id),
|
|
373
|
-
email: EmailValueObject.create(prismaUser.email),
|
|
374
|
-
name: NameValueObject.create(prismaUser.name),
|
|
375
|
-
createdAt: prismaUser.createdAt,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
```
|
|
316
|
+
### Prisma Best Practices
|
|
317
|
+
- Use enums for fixed value sets
|
|
318
|
+
- Always add indexes on foreign keys
|
|
319
|
+
- Use `@@map` for table naming (plural snake_case)
|
|
320
|
+
- Include `createdAt` and `updatedAt` timestamps
|
|
321
|
+
- Use `cuid()` for primary keys
|
|
322
|
+
- No raw SQL queries - use Prisma Client only
|
|
380
323
|
|
|
381
324
|
## Security Standards
|
|
382
325
|
|
|
383
326
|
### Authentication & Authorization
|
|
384
|
-
- JWT
|
|
385
|
-
- Role-based access control
|
|
386
|
-
- Input validation on all endpoints
|
|
387
|
-
- Rate
|
|
388
|
-
- HTTPS only in production
|
|
327
|
+
- **JWT Tokens**: Proper expiration and refresh token handling
|
|
328
|
+
- **RBAC**: Role-based access control with Guards
|
|
329
|
+
- **Validation**: Input validation on all endpoints (class-validator)
|
|
330
|
+
- **Rate Limiting**: Throttle public endpoints to prevent abuse
|
|
331
|
+
- **Environment**: HTTPS only in production, secrets in env variables
|
|
389
332
|
|
|
390
333
|
### Data Protection
|
|
391
|
-
- Encrypt sensitive data at rest
|
|
392
|
-
-
|
|
393
|
-
- Implement audit logging
|
|
394
|
-
-
|
|
395
|
-
-
|
|
334
|
+
- Encrypt sensitive data at rest and in transit
|
|
335
|
+
- Never commit secrets to repository
|
|
336
|
+
- Implement audit logging for critical operations
|
|
337
|
+
- OWASP Top 10 compliance
|
|
338
|
+
- Regular dependency security audits
|
|
396
339
|
|
|
397
340
|
## Performance Standards
|
|
398
341
|
|
|
399
342
|
### Database Optimization
|
|
400
|
-
- Proper indexing
|
|
401
|
-
- Connection pooling
|
|
402
|
-
-
|
|
403
|
-
-
|
|
404
|
-
-
|
|
343
|
+
- Proper indexing on frequently queried fields
|
|
344
|
+
- Connection pooling via Prisma
|
|
345
|
+
- Pagination for large datasets (cursor-based preferred)
|
|
346
|
+
- Avoid N+1 queries with Prisma `include`
|
|
347
|
+
- Query monitoring and slow query logging
|
|
405
348
|
|
|
406
349
|
### Caching Strategy
|
|
407
|
-
- Redis
|
|
408
|
-
- Application-
|
|
409
|
-
-
|
|
410
|
-
-
|
|
411
|
-
- Cache invalidation patterns
|
|
350
|
+
- **Redis**: Session data, rate limiting, and frequently accessed data
|
|
351
|
+
- **Application Cache**: In-memory caching for configuration
|
|
352
|
+
- **TTL Strategy**: Appropriate time-to-live for different data types
|
|
353
|
+
- **Invalidation**: Event-driven cache invalidation
|
|
412
354
|
|
|
413
|
-
##
|
|
355
|
+
## Error Handling
|
|
414
356
|
|
|
415
|
-
###
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
│ ├── filters/
|
|
421
|
-
│ ├── guards/
|
|
422
|
-
│ ├── interceptors/
|
|
423
|
-
│ ├── pipes/
|
|
424
|
-
│ └── utils/
|
|
425
|
-
├── domain-core/
|
|
426
|
-
│ ├── base/
|
|
427
|
-
│ ├── interfaces/
|
|
428
|
-
│ └── exceptions/
|
|
429
|
-
└── database/
|
|
430
|
-
├── base-repository.ts
|
|
431
|
-
├── transaction.decorator.ts
|
|
432
|
-
└── prisma.service.ts
|
|
433
|
-
```
|
|
357
|
+
### Exception Hierarchy
|
|
358
|
+
- Domain exceptions for business rule violations
|
|
359
|
+
- Application exceptions for use case errors
|
|
360
|
+
- Infrastructure exceptions for external service failures
|
|
361
|
+
- HTTP exception filters for API responses
|
|
434
362
|
|
|
435
|
-
###
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
363
|
+
### Error Response Format
|
|
364
|
+
```typescript
|
|
365
|
+
{
|
|
366
|
+
"statusCode": 400,
|
|
367
|
+
"message": "User with email already exists",
|
|
368
|
+
"error": "UserAlreadyExistsException",
|
|
369
|
+
"timestamp": "2024-01-15T10:30:00Z",
|
|
370
|
+
"path": "/api/users"
|
|
371
|
+
}
|
|
372
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Technology Stack
|
|
2
|
+
|
|
3
|
+
## Frontend Stack
|
|
4
|
+
|
|
5
|
+
### Framework
|
|
6
|
+
- **Next.js 15** with TypeScript (App Router)
|
|
7
|
+
- Default framework unless explicitly told otherwise
|
|
8
|
+
- Built-in Turbopack/Webpack for building
|
|
9
|
+
- File-based routing with App Router
|
|
10
|
+
- Exception: Use pure React + Vite only when user specifically mentions offline-first functionality or requests non-Next.js setup
|
|
11
|
+
|
|
12
|
+
### State Management
|
|
13
|
+
- **Zustand**
|
|
14
|
+
|
|
15
|
+
### UI Framework & Styling
|
|
16
|
+
- **Shadcn/ui** (component library)
|
|
17
|
+
- **Radix UI** (primitives)
|
|
18
|
+
- **TailwindCSS v4** (styling)
|
|
19
|
+
|
|
20
|
+
### Architecture
|
|
21
|
+
- **Clean Architecture** + **Domain-Driven Design (DDD)**
|
|
22
|
+
|
|
23
|
+
### Testing
|
|
24
|
+
- **Vitest** (test runner)
|
|
25
|
+
- **React Testing Library** (component testing)
|
|
26
|
+
- **MSW** (Mock Service Worker - API mocking)
|
|
27
|
+
|
|
28
|
+
### Forms & Validation
|
|
29
|
+
- **React Hook Form** (form management)
|
|
30
|
+
- **Zod** (schema validation)
|
|
31
|
+
|
|
32
|
+
### HTTP Client
|
|
33
|
+
- **Axios** with interceptors
|
|
34
|
+
|
|
35
|
+
### Progressive Web App (PWA)
|
|
36
|
+
- **Next.js PWA plugin**
|
|
37
|
+
- **Workbox** (service worker library)
|
|
38
|
+
|
|
39
|
+
### Routing
|
|
40
|
+
- **Next.js App Router** (file-based routing)
|
|
41
|
+
|
|
42
|
+
## Core Principles
|
|
43
|
+
|
|
44
|
+
### Clean Architecture First
|
|
45
|
+
Strict separation of:
|
|
46
|
+
- Domain layer
|
|
47
|
+
- Application layer
|
|
48
|
+
- Infrastructure layer
|
|
49
|
+
- Presentation layer
|
|
50
|
+
|
|
51
|
+
### Domain-Driven Design
|
|
52
|
+
Business logic drives architecture decisions
|
|
53
|
+
|
|
54
|
+
### Component Composition
|
|
55
|
+
Build complex UIs from simple, reusable components
|
|
56
|
+
|
|
57
|
+
### Type Safety
|
|
58
|
+
Leverage TypeScript for compile-time safety and developer experience
|
|
59
|
+
|
|
60
|
+
### Performance by Design
|
|
61
|
+
- Lazy loading
|
|
62
|
+
- Memoization
|
|
63
|
+
- Bundle optimization
|
|
64
|
+
|
|
65
|
+
### Accessibility as Standard
|
|
66
|
+
WCAG 2.1 AA compliance in all components
|
|
67
|
+
|
|
68
|
+
### Test-Driven Development
|
|
69
|
+
Unit tests for all use cases and components
|
|
70
|
+
|
|
71
|
+
### Progressive Web App
|
|
72
|
+
Offline-first approach with service workers
|
|
73
|
+
|
|
74
|
+
### Minimal and Functional
|
|
75
|
+
Only build what's explicitly requested, nothing more
|
|
76
|
+
|
|
77
|
+
### User-Centered Design
|
|
78
|
+
Start with user needs and work backward to implementation
|
|
79
|
+
|
|
80
|
+
### MCP Shadcn Available
|
|
81
|
+
Use MCP to install Shadcn components instead of creating manually
|
|
@@ -16,41 +16,5 @@
|
|
|
16
16
|
],
|
|
17
17
|
"deny": [],
|
|
18
18
|
"ask": []
|
|
19
|
-
}
|
|
20
|
-
"hooks": {
|
|
21
|
-
"UserPromptSubmit": [
|
|
22
|
-
{
|
|
23
|
-
"matcher": ".*",
|
|
24
|
-
"hooks": [
|
|
25
|
-
{
|
|
26
|
-
"type": "command",
|
|
27
|
-
"command": "python .claude/hooks/track-agent.py"
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"PreToolUse": [
|
|
33
|
-
{
|
|
34
|
-
"matcher": "Write|Edit",
|
|
35
|
-
"hooks": [
|
|
36
|
-
{
|
|
37
|
-
"type": "command",
|
|
38
|
-
"command": "python .claude/hooks/file-restriction-hook.py"
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
],
|
|
43
|
-
"SessionEnd": [
|
|
44
|
-
{
|
|
45
|
-
"matcher": ".*",
|
|
46
|
-
"hooks": [
|
|
47
|
-
{
|
|
48
|
-
"type": "command",
|
|
49
|
-
"command": "python .claude/hooks/cleanup-agent.py"
|
|
50
|
-
}
|
|
51
|
-
]
|
|
52
|
-
}
|
|
53
|
-
]
|
|
54
|
-
},
|
|
55
|
-
"disableAllHooks": false
|
|
19
|
+
}
|
|
56
20
|
}
|
package/package.json
CHANGED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
# Leer JSON desde stdin
|
|
7
|
-
data = json.load(sys.stdin)
|
|
8
|
-
|
|
9
|
-
# Obtener información del archivo y sesión
|
|
10
|
-
file_path = data.get('tool_input', {}).get('file_path', '')
|
|
11
|
-
extension = os.path.splitext(file_path)[1].lower() if file_path else ''
|
|
12
|
-
session_id = data.get('session_id', '')
|
|
13
|
-
cwd = data.get('cwd', '')
|
|
14
|
-
|
|
15
|
-
# Construir ruta relativa al log desde el cwd
|
|
16
|
-
log_file = os.path.join(cwd, '.claude', 'logs', 'active_agents.json')
|
|
17
|
-
|
|
18
|
-
# Agentes que solo pueden escribir markdown
|
|
19
|
-
MARKDOWN_ONLY_AGENTS = ['PO', 'SM', 'PM', 'ANALYST', 'ARCHITECT', 'UX-EXPERT']
|
|
20
|
-
|
|
21
|
-
# Verificar si la sesión actual tiene un agente activo
|
|
22
|
-
if session_id and os.path.exists(log_file):
|
|
23
|
-
try:
|
|
24
|
-
with open(log_file, 'r', encoding='utf-8') as f:
|
|
25
|
-
active_agents = json.load(f)
|
|
26
|
-
|
|
27
|
-
# Si la sesión actual tiene un agente activo
|
|
28
|
-
if session_id in active_agents:
|
|
29
|
-
agent_type = active_agents[session_id]['agent']
|
|
30
|
-
|
|
31
|
-
# Si el agente está en la lista de solo markdown
|
|
32
|
-
if agent_type in MARKDOWN_ONLY_AGENTS:
|
|
33
|
-
# Solo permitir archivos markdown
|
|
34
|
-
if extension != '.md':
|
|
35
|
-
result = {
|
|
36
|
-
"hookSpecificOutput": {
|
|
37
|
-
"hookEventName": "PreToolUse",
|
|
38
|
-
"permissionDecision": "deny",
|
|
39
|
-
"permissionDecisionReason": f"⛔ El agente de tipo {agent_type} solo puede redactar archivos markdown"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
print(json.dumps(result))
|
|
43
|
-
sys.exit(0)
|
|
44
|
-
except:
|
|
45
|
-
# Si hay error leyendo el log, permitir la operación
|
|
46
|
-
pass
|
|
47
|
-
|
|
48
|
-
# Si no está bloqueado, permitir la operación (no imprimir nada)
|
|
49
|
-
except Exception as e:
|
|
50
|
-
# En caso de error, permitir la operación
|
|
51
|
-
pass
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
|
|
6
|
-
try:
|
|
7
|
-
# Leer JSON desde stdin
|
|
8
|
-
data = json.load(sys.stdin)
|
|
9
|
-
|
|
10
|
-
session_id = data.get('session_id', '')
|
|
11
|
-
prompt = data.get('prompt', '').lower()
|
|
12
|
-
cwd = data.get('cwd', '')
|
|
13
|
-
|
|
14
|
-
# Construir ruta relativa al log desde el cwd
|
|
15
|
-
log_file = os.path.join(cwd, '.claude', 'logs', 'active_agents.json')
|
|
16
|
-
|
|
17
|
-
# Crear directorio si no existe
|
|
18
|
-
log_dir = os.path.dirname(log_file)
|
|
19
|
-
os.makedirs(log_dir, exist_ok=True)
|
|
20
|
-
|
|
21
|
-
# Lista completa de agentes disponibles
|
|
22
|
-
agent_identifiers = {
|
|
23
|
-
'agents:po': 'PO',
|
|
24
|
-
'agents:sm': 'SM',
|
|
25
|
-
'agents:pm': 'PM',
|
|
26
|
-
'agents:analyst': 'ANALYST',
|
|
27
|
-
'agents:architect': 'ARCHITECT',
|
|
28
|
-
'agents:dev': 'DEV',
|
|
29
|
-
'agents:backend': 'BACKEND',
|
|
30
|
-
'agents:frontend': 'FRONTEND',
|
|
31
|
-
'agents:qa': 'QA',
|
|
32
|
-
'agents:ux-expert': 'UX-EXPERT',
|
|
33
|
-
'agents:bmad-master': 'BMAD-MASTER',
|
|
34
|
-
'agents:bmad-orchestrator': 'BMAD-ORCHESTRATOR'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# Detectar si se está invocando un agente
|
|
38
|
-
agent_type = None
|
|
39
|
-
for identifier, agent_name in agent_identifiers.items():
|
|
40
|
-
if identifier in prompt or f'/bmad:{identifier}' in prompt:
|
|
41
|
-
agent_type = agent_name
|
|
42
|
-
break
|
|
43
|
-
|
|
44
|
-
if agent_type and session_id:
|
|
45
|
-
# Leer log existente
|
|
46
|
-
active_agents = {}
|
|
47
|
-
if os.path.exists(log_file):
|
|
48
|
-
try:
|
|
49
|
-
with open(log_file, 'r', encoding='utf-8') as f:
|
|
50
|
-
active_agents = json.load(f)
|
|
51
|
-
except:
|
|
52
|
-
active_agents = {}
|
|
53
|
-
|
|
54
|
-
# Actualizar o agregar la sesión con el agente actual
|
|
55
|
-
active_agents[session_id] = {
|
|
56
|
-
'agent': agent_type,
|
|
57
|
-
'timestamp': datetime.now().isoformat(),
|
|
58
|
-
'last_prompt': prompt[:100] # Guardar inicio del prompt para debug
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# Guardar log actualizado
|
|
62
|
-
with open(log_file, 'w', encoding='utf-8') as f:
|
|
63
|
-
json.dump(active_agents, f, indent=2, ensure_ascii=False)
|
|
64
|
-
|
|
65
|
-
except Exception as e:
|
|
66
|
-
# En caso de error, no bloquear la operación
|
|
67
|
-
pass
|