arckode-framework 1.3.2 → 1.4.1
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/adapters/jwt.ts +6 -4
- package/adapters/mysql.ts +7 -2
- package/adapters/postgres.ts +37 -0
- package/adapters/sqlite.ts +7 -1
- package/adapters/vendor.d.ts +48 -0
- package/cli/analyze/checks.ts +333 -0
- package/cli/analyze/index.ts +44 -0
- package/cli/analyze/report.ts +107 -0
- package/cli/analyze/types.ts +46 -0
- package/cli/analyze/utils.ts +36 -0
- package/cli/analyze.ts +2 -647
- package/cli/commands/db-migrate.ts +213 -89
- package/cli/commands/db-seed.ts +97 -32
- package/cli/commands/db-utils.ts +192 -0
- package/cli/commands/new.ts +175 -0
- package/cli/commands/routes.ts +94 -0
- package/cli/index.ts +57 -404
- package/cli/stubs/module/core.ts +162 -0
- package/cli/stubs/module/data.ts +171 -0
- package/cli/stubs/module/index.ts +5 -0
- package/cli/stubs/module/service.ts +198 -0
- package/cli/stubs/module/types.ts +12 -0
- package/cli/stubs/module-stub.ts +2 -552
- package/kernel/auth.ts +114 -0
- package/kernel/cache.ts +37 -0
- package/kernel/config.ts +129 -0
- package/kernel/container.ts +64 -0
- package/kernel/db/orm-migrate.ts +136 -0
- package/kernel/db/orm-repository.ts +45 -0
- package/kernel/db/orm-utils.ts +93 -0
- package/kernel/db/orm.ts +254 -0
- package/kernel/db/transactor.ts +17 -0
- package/kernel/db/types.ts +72 -0
- package/kernel/errors.ts +102 -0
- package/kernel/framework.default.ts +41 -0
- package/kernel/framework.ts +8 -2144
- package/kernel/http/router.ts +131 -0
- package/kernel/http/server.ts +303 -0
- package/kernel/http/types.ts +56 -0
- package/kernel/index.ts +25 -0
- package/kernel/logger.ts +50 -0
- package/kernel/middlewares.ts +19 -7
- package/kernel/modules/create-module.ts +5 -0
- package/kernel/modules/system.ts +149 -0
- package/kernel/modules/types.ts +46 -0
- package/kernel/seeds.ts +48 -0
- package/kernel/static.ts +11 -2
- package/kernel/testing.ts +8 -3
- package/kernel/validator.ts +116 -0
- package/modules/events/index.ts +19 -3
- package/modules/mail/index.ts +14 -2
- package/modules/storage/local-adapter.ts +19 -5
- package/modules/ws/index.ts +123 -18
- package/package.json +8 -11
- package/skills/auth/SKILL.md +36 -220
- package/skills/cli/SKILL.md +32 -251
- package/skills/config/SKILL.md +30 -239
- package/skills/connectors/SKILL.md +32 -295
- package/skills/helpers/SKILL.md +26 -195
- package/skills/middlewares/SKILL.md +30 -280
- package/skills/orm/SKILL.md +42 -349
- package/skills/realtime/SKILL.md +22 -297
- package/skills/services/SKILL.md +40 -183
- package/skills/testing/SKILL.md +34 -266
package/cli/analyze.ts
CHANGED
|
@@ -1,647 +1,2 @@
|
|
|
1
|
-
// cli/analyze.ts —
|
|
2
|
-
|
|
3
|
-
// SOLID: cada chequeo es una función con una responsabilidad
|
|
4
|
-
|
|
5
|
-
import { readdir, readFile, access } from 'node:fs/promises'
|
|
6
|
-
import { join, relative } from 'node:path'
|
|
7
|
-
|
|
8
|
-
export interface AnalysisResult {
|
|
9
|
-
valid: boolean
|
|
10
|
-
modules: ModuleReport[]
|
|
11
|
-
connectors: string[]
|
|
12
|
-
violations: Violation[]
|
|
13
|
-
warnings: Warning[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ModuleReport {
|
|
17
|
-
name: string
|
|
18
|
-
hasIndex: boolean
|
|
19
|
-
hasService: boolean
|
|
20
|
-
hasController: boolean
|
|
21
|
-
hasTypes: boolean
|
|
22
|
-
hasSockets: boolean
|
|
23
|
-
hasValidators: boolean
|
|
24
|
-
hasTests: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface Violation {
|
|
28
|
-
type:
|
|
29
|
-
| 'DIRECT_MODULE_IMPORT'
|
|
30
|
-
| 'MISSING_INDEX' | 'MISSING_CONTRACT' | 'MISSING_SERVICE' | 'MISSING_TYPES'
|
|
31
|
-
| 'MISSING_SOCKETS' | 'MISSING_VALIDATORS' | 'MISSING_TESTS'
|
|
32
|
-
| 'MISSING_CONNECTORS' | 'CONNECTOR_BUSINESS_LOGIC' | 'INDEX_PENDING_CHANGES'
|
|
33
|
-
// ── Los 6 que las reglas base no cubrían ──
|
|
34
|
-
| 'CONTROLLER_MISSING_VALIDATION' // ruta POST/PUT/PATCH sin validateSchema
|
|
35
|
-
| 'BUSINESS_LOGIC_IN_CONTROLLER' // ORM directo en controller
|
|
36
|
-
| 'EMPTY_MODULE_DESCRIPTION' // descripción vacía en módulo
|
|
37
|
-
| 'TESTS_WITHOUT_CASES' // test file sin test()
|
|
38
|
-
| 'SERVICE_IMPORTS_OTHER_MODULE' // service cruza módulo
|
|
39
|
-
| 'GOD_SERVICE' // service > 200 líneas
|
|
40
|
-
// ── Seguridad y performance ──
|
|
41
|
-
| 'N_PLUS_ONE_RISK' // ORM llamado dentro de loop en service
|
|
42
|
-
| 'IDOR_RISK' // findById sin assertOwnership posterior
|
|
43
|
-
// ── Portabilidad ──
|
|
44
|
-
| 'SERVICE_DEPENDS_ON_ORM' // service inyecta ORM en lugar de RepositoryAdapter
|
|
45
|
-
// ── Estructura de archivos (Regla #22) ──
|
|
46
|
-
| 'MODEL_IN_TYPES_FILE' // ModelDefinition en types.ts en vez de model.ts
|
|
47
|
-
| 'MISSING_MODEL_TS' // módulo sin model.ts
|
|
48
|
-
// ── Conectores ──
|
|
49
|
-
| 'DUPLICATE_CONNECTOR' // mismo nombre lógico con distinto casing (foo-bar.ts y fooBar.ts)
|
|
50
|
-
| 'UNREGISTERED_CONNECTOR' // archivo existe pero no está en composition-root.ts
|
|
51
|
-
module: string
|
|
52
|
-
message: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface Warning {
|
|
56
|
-
type: 'NO_CONNECTORS' | 'SINGLE_MODULE' | 'NO_COMPOSITION_ROOT' | 'INDEX_PENDING_CHANGES' | 'LEGACY_ACTIONS_FOLDER'
|
|
57
|
-
message: string
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function analyzeProject(basePath: string): Promise<AnalysisResult> {
|
|
61
|
-
const violations: Violation[] = []
|
|
62
|
-
const warnings: Warning[] = []
|
|
63
|
-
const modules: ModuleReport[] = []
|
|
64
|
-
// Mapa de paths internos resueltos por módulo (nueva estructura o legacy)
|
|
65
|
-
const internalPaths = new Map<string, { service: string | null; controller: string | null }>()
|
|
66
|
-
|
|
67
|
-
const srcPath = join(basePath, 'src')
|
|
68
|
-
const modulesPath = join(srcPath, 'modules')
|
|
69
|
-
const connectorsPath = join(srcPath, 'connectors')
|
|
70
|
-
const crPath = join(srcPath, 'composition-root.ts')
|
|
71
|
-
|
|
72
|
-
// Verificar composition root
|
|
73
|
-
try {
|
|
74
|
-
await access(crPath)
|
|
75
|
-
} catch {
|
|
76
|
-
warnings.push({ type: 'NO_COMPOSITION_ROOT', message: 'No hay composition-root.ts en src/' })
|
|
77
|
-
return { valid: false, modules: [], connectors: [], violations, warnings }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Leer módulos
|
|
81
|
-
try {
|
|
82
|
-
const moduleDirs = await readdir(modulesPath, { withFileTypes: true })
|
|
83
|
-
const moduleNames = moduleDirs.filter(d => d.isDirectory()).map(d => d.name)
|
|
84
|
-
|
|
85
|
-
if (moduleNames.length === 0) {
|
|
86
|
-
warnings.push({ type: 'SINGLE_MODULE', message: 'No hay módulos en src/modules/' })
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
for (const name of moduleNames) {
|
|
90
|
-
const modPath = join(modulesPath, name)
|
|
91
|
-
// Soporta estructura nueva (root) y legacy (actions/) — el path resuelto
|
|
92
|
-
// queda guardado en internalPaths para reusar en chequeos posteriores
|
|
93
|
-
const servicePath = await resolvePath(modPath, ['service.ts', 'actions/service.ts'])
|
|
94
|
-
const controllerPath = await resolvePath(modPath, ['controller.ts', 'actions/controller.ts'])
|
|
95
|
-
|
|
96
|
-
const report: ModuleReport = {
|
|
97
|
-
name,
|
|
98
|
-
hasIndex: await fileExists(join(modPath, 'index.ts')),
|
|
99
|
-
hasService: servicePath !== null,
|
|
100
|
-
hasController: controllerPath !== null,
|
|
101
|
-
hasTypes: await fileExists(join(modPath, 'types.ts')),
|
|
102
|
-
hasSockets: await fileExists(join(modPath, 'sockets.ts')),
|
|
103
|
-
hasValidators: await fileExists(join(modPath, 'validators', 'schema.ts')),
|
|
104
|
-
hasTests: await fileExists(join(modPath, 'tests', 'service.test.ts')),
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
internalPaths.set(name, { service: servicePath, controller: controllerPath })
|
|
108
|
-
|
|
109
|
-
if (!report.hasIndex) violations.push({ type: 'MISSING_INDEX', module: name, message: `Módulo "${name}" sin index.ts (puerta pública)` })
|
|
110
|
-
if (!report.hasTypes) violations.push({ type: 'MISSING_TYPES', module: name, message: `Módulo "${name}" sin types.ts (DTOs)` })
|
|
111
|
-
if (!report.hasSockets) violations.push({ type: 'MISSING_SOCKETS', module: name, message: `Módulo "${name}" sin sockets.ts (hooks opcionales)` })
|
|
112
|
-
if (!report.hasService) violations.push({ type: 'MISSING_SERVICE', module: name, message: `Módulo "${name}" sin service.ts (al root del módulo)` })
|
|
113
|
-
if (!report.hasValidators) violations.push({ type: 'MISSING_VALIDATORS', module: name, message: `Módulo "${name}" sin validators/schema.ts` })
|
|
114
|
-
if (!report.hasTests) violations.push({ type: 'MISSING_TESTS', module: name, message: `Módulo "${name}" sin tests/` })
|
|
115
|
-
|
|
116
|
-
modules.push(report)
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
violations.push({ type: 'MISSING_INDEX', module: 'modules', message: 'No existe src/modules/' })
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Verificar conectores
|
|
123
|
-
const connectors: string[] = []
|
|
124
|
-
try {
|
|
125
|
-
const connectorFiles = await readdir(connectorsPath)
|
|
126
|
-
for (const file of connectorFiles) {
|
|
127
|
-
if (file.endsWith('.ts')) connectors.push(file)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (connectors.length === 0 && modules.length > 1) {
|
|
131
|
-
warnings.push({ type: 'NO_CONNECTORS', message: `Hay ${modules.length} módulos pero 0 conectores. Los módulos no están conectados.` })
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Detectar conectores duplicados (ej: trading-wallets.ts + tradingWallets.ts)
|
|
135
|
-
const canonicalMap = new Map<string, string[]>()
|
|
136
|
-
for (const file of connectors) {
|
|
137
|
-
const canonical = canonicalizeConnectorName(file)
|
|
138
|
-
const list = canonicalMap.get(canonical) ?? []
|
|
139
|
-
list.push(file)
|
|
140
|
-
canonicalMap.set(canonical, list)
|
|
141
|
-
}
|
|
142
|
-
for (const [canonical, files] of canonicalMap) {
|
|
143
|
-
if (files.length > 1) {
|
|
144
|
-
violations.push({
|
|
145
|
-
type: 'DUPLICATE_CONNECTOR',
|
|
146
|
-
module: 'connectors',
|
|
147
|
-
message: `Conectores duplicados con mismo nombre lógico "${canonical}": ${files.join(', ')}. Eliminar los stubs muertos del CLI.`,
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Detectar conectores no registrados en composition-root.ts
|
|
153
|
-
try {
|
|
154
|
-
const crContent = await readFile(crPath, 'utf-8')
|
|
155
|
-
for (const file of connectors) {
|
|
156
|
-
const basename = file.replace(/\.ts$/, '')
|
|
157
|
-
// El conector debe aparecer como import o como string literal en addConnector()
|
|
158
|
-
const importPattern = new RegExp(`from\\s+['"][^'"]*connectors\\/${basename}['"]`)
|
|
159
|
-
if (!importPattern.test(crContent)) {
|
|
160
|
-
violations.push({
|
|
161
|
-
type: 'UNREGISTERED_CONNECTOR',
|
|
162
|
-
module: 'connectors',
|
|
163
|
-
message: `Conector "${file}" existe pero no se importa en composition-root.ts. Dead code o falta registrar con system.addConnector().`,
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
} catch { /* sin composition-root, ya warned arriba */ }
|
|
168
|
-
} catch {
|
|
169
|
-
// No hay carpeta de conectores, puede ser válido si hay 1 módulo
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Verificar imports directos entre módulos (REGLAS CLAUDE #1)
|
|
173
|
-
for (const module of modules) {
|
|
174
|
-
try {
|
|
175
|
-
const allFiles = await getAllTsFiles(join(modulesPath, module.name))
|
|
176
|
-
for (const file of allFiles) {
|
|
177
|
-
const content = await readFile(file, 'utf-8')
|
|
178
|
-
const relPath = file.replace(modulesPath, '')
|
|
179
|
-
for (const other of modules) {
|
|
180
|
-
if (other.name === module.name) continue
|
|
181
|
-
const importPattern = new RegExp(`from ['"][^'"]*\\/modules\\/${other.name}([/'"]|\$)`)
|
|
182
|
-
if (importPattern.test(content)) {
|
|
183
|
-
violations.push({
|
|
184
|
-
type: 'DIRECT_MODULE_IMPORT',
|
|
185
|
-
module: module.name,
|
|
186
|
-
message: `"${relPath}" importa directo de "${other.name}". Usar conector. (CLAUDE #1)`,
|
|
187
|
-
})
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
} catch { /* no actions dir */ }
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Verificar lógica de negocio en conectores (REGLA CLAUDE #3)
|
|
195
|
-
for (const connFile of connectors) {
|
|
196
|
-
try {
|
|
197
|
-
const content = await readFile(join(connectorsPath, connFile), 'utf-8')
|
|
198
|
-
// Detectar patrones de lógica de negocio en conectores
|
|
199
|
-
const businessPatterns = [
|
|
200
|
-
/\bif\s*\(/g, /\bswitch\s*\(/g, /\bfor\s*\(/g, /\bwhile\s*\(/g,
|
|
201
|
-
/\btry\s*\{/g, /\bcatch\s*\(/g, /\bnew\s+\w+Action/g,
|
|
202
|
-
/\.create\(/g, /\.update\(/g, /\.delete\(/g,
|
|
203
|
-
]
|
|
204
|
-
|
|
205
|
-
// Contar ocurrencias — 1 o 2 pueden ser normales, más indica lógica
|
|
206
|
-
let businessHits = 0
|
|
207
|
-
for (const pattern of businessPatterns) {
|
|
208
|
-
const matches = content.match(pattern)
|
|
209
|
-
if (matches) businessHits += matches.length
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (businessHits > 3) {
|
|
213
|
-
violations.push({
|
|
214
|
-
type: 'CONNECTOR_BUSINESS_LOGIC',
|
|
215
|
-
module: connFile,
|
|
216
|
-
message: `"${connFile}" tiene ${businessHits} patrones de lógica de negocio. Los conectores solo deben wirear. (CLAUDE #3)`,
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
} catch { /* ignore */ }
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Verificar regla APPEND-ONLY (REGLA CLAUDE #2) — detecta exports removidos
|
|
223
|
-
// Busca comentarios de deprecación o conflicto en index.ts
|
|
224
|
-
for (const module of modules) {
|
|
225
|
-
const indexPath = join(modulesPath, module.name, 'index.ts')
|
|
226
|
-
try {
|
|
227
|
-
const content = await readFile(indexPath, 'utf-8')
|
|
228
|
-
// Si index.ts tiene comentarios TODO o FIXME sobre exports rotos
|
|
229
|
-
if (content.includes('TODO:') || content.includes('FIXME:')) {
|
|
230
|
-
warnings.push({
|
|
231
|
-
type: 'INDEX_PENDING_CHANGES',
|
|
232
|
-
message: `"${module.name}/index.ts" tiene TODO/FIXME. Revisar antes de continuar.`,
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
} catch { /* no index */ }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Verificar que los conectores existen si hay más de 1 módulo (REGLA CLAUDE #5)
|
|
239
|
-
if (modules.length > 1 && connectors.length === 0) {
|
|
240
|
-
violations.push({
|
|
241
|
-
type: 'MISSING_CONNECTORS',
|
|
242
|
-
module: 'system',
|
|
243
|
-
message: `Hay ${modules.length} módulos pero 0 conectores. Los módulos no están conectados. (CLAUDE #5)`,
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ── Los 6 que las reglas base no prevenían ──────────────────────
|
|
248
|
-
|
|
249
|
-
for (const module of modules) {
|
|
250
|
-
const modPath = join(modulesPath, module.name)
|
|
251
|
-
|
|
252
|
-
// 1. DOCUMENTACIÓN FALTANTE — descripción vacía en el módulo
|
|
253
|
-
const indexPath = join(modPath, 'index.ts')
|
|
254
|
-
try {
|
|
255
|
-
const indexContent = await readFile(indexPath, 'utf-8')
|
|
256
|
-
// Busca description: '' o description: " " en el createModule/export
|
|
257
|
-
if (/description:\s*['"`]\s*['"`]/.test(indexContent)) {
|
|
258
|
-
violations.push({
|
|
259
|
-
type: 'EMPTY_MODULE_DESCRIPTION',
|
|
260
|
-
module: module.name,
|
|
261
|
-
message: `"${module.name}" tiene descripción vacía. Todo módulo DEBE documentar su propósito.`,
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
} catch { /* no index — ya detectado */ }
|
|
265
|
-
|
|
266
|
-
// 2. TEST COVERAGE — archivo de test existe pero sin casos
|
|
267
|
-
const testPath = join(modPath, 'tests', 'service.test.ts')
|
|
268
|
-
try {
|
|
269
|
-
const testContent = await readFile(testPath, 'utf-8')
|
|
270
|
-
const hasTests = /\btest\s*\(|\bit\s*\(|\bdescribe\s*\(/.test(testContent)
|
|
271
|
-
if (!hasTests) {
|
|
272
|
-
violations.push({
|
|
273
|
-
type: 'TESTS_WITHOUT_CASES',
|
|
274
|
-
module: module.name,
|
|
275
|
-
message: `"${module.name}/tests/service.test.ts" existe pero no tiene ningún test(). Coverage = 0.`,
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
|
-
} catch { /* no test file — ya detectado por MISSING_TESTS */ }
|
|
279
|
-
|
|
280
|
-
// Paths resueltos (nueva estructura o legacy)
|
|
281
|
-
const paths = internalPaths.get(module.name) ?? { service: null, controller: null }
|
|
282
|
-
const controllerPath = paths.controller
|
|
283
|
-
const servicePath = paths.service
|
|
284
|
-
|
|
285
|
-
// 3. VALIDACIONES INCOMPLETAS — controller con POST/PUT/PATCH sin validateSchema
|
|
286
|
-
if (controllerPath) {
|
|
287
|
-
try {
|
|
288
|
-
const ctrlContent = await readFile(controllerPath, 'utf-8')
|
|
289
|
-
const hasMutatingRoute = /router\.(post|put|patch)\s*\(/.test(ctrlContent)
|
|
290
|
-
const hasValidation = /validateSchema\s*\(/.test(ctrlContent)
|
|
291
|
-
if (hasMutatingRoute && !hasValidation) {
|
|
292
|
-
violations.push({
|
|
293
|
-
type: 'CONTROLLER_MISSING_VALIDATION',
|
|
294
|
-
module: module.name,
|
|
295
|
-
message: `"${module.name}/controller.ts" tiene rutas POST/PUT/PATCH sin validateSchema(). Datos no validados llegan al service.`,
|
|
296
|
-
})
|
|
297
|
-
}
|
|
298
|
-
} catch { /* no controller */ }
|
|
299
|
-
|
|
300
|
-
// 4. LÓGICA EN CONTROLLER — ORM directo en controller (debería estar en service)
|
|
301
|
-
try {
|
|
302
|
-
const ctrlContent = await readFile(controllerPath, 'utf-8')
|
|
303
|
-
const ormInController = /\borm\.(findMany|findById|create|update|delete|paginate|count)\s*\(/.test(ctrlContent)
|
|
304
|
-
if (ormInController) {
|
|
305
|
-
violations.push({
|
|
306
|
-
type: 'BUSINESS_LOGIC_IN_CONTROLLER',
|
|
307
|
-
module: module.name,
|
|
308
|
-
message: `"${module.name}/controller.ts" llama al ORM directamente. La lógica de datos va en service.ts.`,
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
} catch { /* no controller */ }
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Warning: legacy folder layout
|
|
315
|
-
if ((servicePath && servicePath.includes('/actions/')) || (controllerPath && controllerPath.includes('/actions/'))) {
|
|
316
|
-
warnings.push({
|
|
317
|
-
type: 'LEGACY_ACTIONS_FOLDER',
|
|
318
|
-
message: `"${module.name}" usa la estructura legacy actions/. Mover service.ts y controller.ts al root del módulo.`,
|
|
319
|
-
})
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// 5. GOD SERVICE — service con más de 200 líneas indica demasiadas responsabilidades
|
|
323
|
-
if (!servicePath) continue // sin service, skip los siguientes checks
|
|
324
|
-
// El service principal puede tener orquestador < 200 líneas + sub-services en usecases/
|
|
325
|
-
// Aquí solo medimos el archivo service.ts principal
|
|
326
|
-
try {
|
|
327
|
-
const serviceContent = await readFile(servicePath, 'utf-8')
|
|
328
|
-
const lineCount = serviceContent.split('\n').length
|
|
329
|
-
if (lineCount > 200) {
|
|
330
|
-
violations.push({
|
|
331
|
-
type: 'GOD_SERVICE',
|
|
332
|
-
module: module.name,
|
|
333
|
-
message: `"${module.name}/service.ts" tiene ${lineCount} líneas. Un service > 200 líneas es un God Object — extraer a usecases/.`,
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
} catch { /* no service */ }
|
|
337
|
-
|
|
338
|
-
// 6. SERVICE CRUZA MÓDULO — service importa de otro módulo directamente
|
|
339
|
-
try {
|
|
340
|
-
const serviceContent = await readFile(servicePath, 'utf-8')
|
|
341
|
-
for (const other of modules) {
|
|
342
|
-
if (other.name === module.name) continue
|
|
343
|
-
const importPattern = new RegExp(`from ['"][^'"]*\\/modules\\/${other.name}([/'"]|\$)`)
|
|
344
|
-
if (importPattern.test(serviceContent)) {
|
|
345
|
-
violations.push({
|
|
346
|
-
type: 'SERVICE_IMPORTS_OTHER_MODULE',
|
|
347
|
-
module: module.name,
|
|
348
|
-
message: `"${module.name}/service.ts" importa de "${other.name}". Los services no pueden cruzar módulos — usar conector. (CLAUDE #1)`,
|
|
349
|
-
})
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
} catch { /* no service */ }
|
|
353
|
-
|
|
354
|
-
// 7. N+1 RISK — ORM llamado dentro de un loop en el service
|
|
355
|
-
try {
|
|
356
|
-
const serviceContent = await readFile(servicePath, 'utf-8')
|
|
357
|
-
const lines = serviceContent.split('\n')
|
|
358
|
-
let insideLoop = 0
|
|
359
|
-
for (let i = 0; i < lines.length; i++) {
|
|
360
|
-
const line = lines[i] ?? ''
|
|
361
|
-
// Detectar apertura de loop
|
|
362
|
-
if (/\b(for\s*\(|forEach\s*\(|\.map\s*\(\s*async|\.filter\s*\(\s*async|while\s*\()/.test(line)) insideLoop++
|
|
363
|
-
// Detectar cierre de bloque (aproximación por llaves)
|
|
364
|
-
const opens = (line.match(/\{/g) ?? []).length
|
|
365
|
-
const closes = (line.match(/\}/g) ?? []).length
|
|
366
|
-
if (insideLoop > 0 && closes > opens) insideLoop = Math.max(0, insideLoop - 1)
|
|
367
|
-
// Detectar ORM dentro del loop
|
|
368
|
-
if (insideLoop > 0 && /await\s+\w*orm\w*\.(findMany|findById|create|update|delete|count|paginate)\s*\(/.test(line)) {
|
|
369
|
-
violations.push({
|
|
370
|
-
type: 'N_PLUS_ONE_RISK',
|
|
371
|
-
module: module.name,
|
|
372
|
-
message: `"${module.name}/service.ts" línea ${i + 1}: ORM llamado dentro de un loop — riesgo de N+1. Usar findMany/createMany/updateMany fuera del loop.`,
|
|
373
|
-
})
|
|
374
|
-
break // un warning por archivo es suficiente
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
} catch { /* no service */ }
|
|
378
|
-
|
|
379
|
-
// 8. IDOR RISK — findById sin assertOwnership inmediatamente después
|
|
380
|
-
// Analiza también sub-services en usecases/ (la nueva estructura) o actions/ (legacy)
|
|
381
|
-
const filesToScan: string[] = [servicePath]
|
|
382
|
-
for (const subdir of ['usecases', 'actions']) {
|
|
383
|
-
const subdirPath = join(modPath, subdir)
|
|
384
|
-
try {
|
|
385
|
-
const subEntries = await readdir(subdirPath, { withFileTypes: true })
|
|
386
|
-
for (const e of subEntries) {
|
|
387
|
-
if (e.isFile() && e.name.endsWith('.ts') && e.name !== 'service.ts' && e.name !== 'controller.ts') {
|
|
388
|
-
filesToScan.push(join(subdirPath, e.name))
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
} catch { /* sin subdir */ }
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
for (const filePath of filesToScan) {
|
|
395
|
-
// Skip archivos de adapter/repo/interfaz — declaran findById pero no es business logic
|
|
396
|
-
if (/-orm\.ts$|-repo\.ts$|-repository\.ts$|repository\.ts$/.test(filePath)) continue
|
|
397
|
-
try {
|
|
398
|
-
const serviceContent = await readFile(filePath, 'utf-8')
|
|
399
|
-
const lines = serviceContent.split('\n')
|
|
400
|
-
let insideInterface = 0
|
|
401
|
-
for (let i = 0; i < lines.length; i++) {
|
|
402
|
-
const line = lines[i] ?? ''
|
|
403
|
-
// Trackear si estamos dentro de un bloque de interface
|
|
404
|
-
if (/^\s*(export\s+)?interface\s+\w+/.test(line)) insideInterface++
|
|
405
|
-
const opens = (line.match(/\{/g) ?? []).length
|
|
406
|
-
const closes = (line.match(/\}/g) ?? []).length
|
|
407
|
-
if (insideInterface > 0) insideInterface = Math.max(0, insideInterface + opens - closes)
|
|
408
|
-
|
|
409
|
-
// Skip findById en interfaces TS — no es business logic
|
|
410
|
-
if (insideInterface > 0) continue
|
|
411
|
-
// Skip declaraciones de tipo: "findById(...): Promise<...>" sin await ni asignación
|
|
412
|
-
if (/^\s*findById\s*\([^)]*\)\s*:/.test(line)) continue
|
|
413
|
-
|
|
414
|
-
if (/\bfindById\s*\(/.test(line)) {
|
|
415
|
-
const window = lines.slice(i + 1, i + 6).join('\n')
|
|
416
|
-
if (!window.includes('assertOwnership') && !window.includes('userId') && !window.includes('ownerId')) {
|
|
417
|
-
const relFile = relative(modulesPath, filePath)
|
|
418
|
-
violations.push({
|
|
419
|
-
type: 'IDOR_RISK',
|
|
420
|
-
module: module.name,
|
|
421
|
-
message: `"${relFile}" línea ${i + 1}: findById sin verificación de ownership. Usar auth.assertOwnership() para prevenir IDOR.`,
|
|
422
|
-
})
|
|
423
|
-
break
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
} catch { /* file unreadable */ }
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// 9. SERVICE DEPENDS ON ORM — service inyecta ORM en lugar de RepositoryAdapter (Regla #18)
|
|
431
|
-
try {
|
|
432
|
-
const serviceContent = await readFile(servicePath, 'utf-8')
|
|
433
|
-
if (/private\s+(readonly\s+)?\w+\s*:\s*ORM\b/.test(serviceContent)) {
|
|
434
|
-
violations.push({
|
|
435
|
-
type: 'SERVICE_DEPENDS_ON_ORM',
|
|
436
|
-
module: module.name,
|
|
437
|
-
message: `"${module.name}/service.ts" inyecta ORM directamente. Usar RepositoryAdapter<T> para poder swapear SQL → MongoDB → Prisma sin tocar el service. (CLAUDE #18)`,
|
|
438
|
-
})
|
|
439
|
-
}
|
|
440
|
-
} catch { /* no service */ }
|
|
441
|
-
|
|
442
|
-
// 10. MODEL IN TYPES — ModelDefinition en types.ts en vez de model.ts (Regla #22)
|
|
443
|
-
const typesFilePath = join(modPath, 'types.ts')
|
|
444
|
-
try {
|
|
445
|
-
const typesContent = await readFile(typesFilePath, 'utf-8')
|
|
446
|
-
if (/\bModelDefinition\b/.test(typesContent)) {
|
|
447
|
-
violations.push({
|
|
448
|
-
type: 'MODEL_IN_TYPES_FILE',
|
|
449
|
-
module: module.name,
|
|
450
|
-
message: `"${module.name}/types.ts" contiene ModelDefinition o schema de DB. Moverlo a model.ts — son conceptos distintos. (CLAUDE #22)`,
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
} catch { /* no types.ts */ }
|
|
454
|
-
|
|
455
|
-
// 11. MISSING MODEL.TS — módulo sin model.ts (Regla #22)
|
|
456
|
-
if (!await fileExists(join(modPath, 'model.ts'))) {
|
|
457
|
-
violations.push({
|
|
458
|
-
type: 'MISSING_MODEL_TS',
|
|
459
|
-
module: module.name,
|
|
460
|
-
message: `"${module.name}" no tiene model.ts. El schema de DB debe vivir separado de types.ts. (CLAUDE #22)`,
|
|
461
|
-
})
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
return {
|
|
466
|
-
valid: violations.length === 0,
|
|
467
|
-
modules,
|
|
468
|
-
connectors,
|
|
469
|
-
violations,
|
|
470
|
-
warnings,
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async function fileExists(path: string): Promise<boolean> {
|
|
475
|
-
try { await access(path); return true } catch { return false }
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Resuelve el primer path existente de una lista de candidatos relativos.
|
|
479
|
-
// Permite soportar la estructura nueva (root del módulo) y la legacy (actions/).
|
|
480
|
-
async function resolvePath(modPath: string, candidates: string[]): Promise<string | null> {
|
|
481
|
-
for (const candidate of candidates) {
|
|
482
|
-
const full = join(modPath, candidate)
|
|
483
|
-
if (await fileExists(full)) return full
|
|
484
|
-
}
|
|
485
|
-
return null
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Normaliza nombre de conector a forma canónica para detectar duplicados
|
|
489
|
-
// "trading-wallets.ts" y "tradingWallets.ts" → "tradingwallets"
|
|
490
|
-
function canonicalizeConnectorName(filename: string): string {
|
|
491
|
-
return filename
|
|
492
|
-
.replace(/\.ts$/, '')
|
|
493
|
-
.replace(/[-_]/g, '')
|
|
494
|
-
.toLowerCase()
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
async function getAllTsFiles(dir: string): Promise<string[]> {
|
|
498
|
-
const files: string[] = []
|
|
499
|
-
try {
|
|
500
|
-
const entries = await readdir(dir, { withFileTypes: true })
|
|
501
|
-
for (const entry of entries) {
|
|
502
|
-
const full = join(dir, entry.name)
|
|
503
|
-
if (entry.isDirectory()) {
|
|
504
|
-
if (entry.name !== 'node_modules' && entry.name !== 'tests' && entry.name !== '__tests__') {
|
|
505
|
-
files.push(...await getAllTsFiles(full))
|
|
506
|
-
}
|
|
507
|
-
} else if (entry.name.endsWith('.ts')) {
|
|
508
|
-
files.push(full)
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
} catch { /* ignore */ }
|
|
512
|
-
return files
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
export function printAnalysis(result: AnalysisResult): void {
|
|
516
|
-
console.log('\n══════════════════════════════════════')
|
|
517
|
-
console.log(' Análisis de Arquitectura')
|
|
518
|
-
console.log('══════════════════════════════════════\n')
|
|
519
|
-
|
|
520
|
-
console.log(`📦 Módulos (${result.modules.length}):`)
|
|
521
|
-
for (const m of result.modules) {
|
|
522
|
-
const icon = m.hasIndex ? '✓' : '✗'
|
|
523
|
-
const parts = [
|
|
524
|
-
m.hasService ? 'service' : null,
|
|
525
|
-
m.hasController ? 'controller' : null,
|
|
526
|
-
m.hasTypes ? 'types' : null,
|
|
527
|
-
m.hasSockets ? 'sockets' : null,
|
|
528
|
-
m.hasValidators ? 'validators' : null,
|
|
529
|
-
m.hasTests ? 'tests' : null,
|
|
530
|
-
].filter(Boolean)
|
|
531
|
-
console.log(` ${icon} ${m.name} [${parts.join(', ')}]`)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
console.log(`\n🔌 Conectores (${result.connectors.length}):`)
|
|
535
|
-
for (const c of result.connectors) {
|
|
536
|
-
console.log(` ${c}`)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (result.violations.length > 0) {
|
|
540
|
-
// Agrupar por categoría para que la IA sepa exactamente qué arreglar
|
|
541
|
-
const byCategory: Record<string, Violation[]> = {
|
|
542
|
-
'Estructura': [],
|
|
543
|
-
'Acoplamiento': [],
|
|
544
|
-
'Diseño': [],
|
|
545
|
-
'Calidad': [],
|
|
546
|
-
'Seguridad': [],
|
|
547
|
-
'Performance': [],
|
|
548
|
-
'Portabilidad': [],
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
for (const v of result.violations) {
|
|
552
|
-
if (['MISSING_INDEX', 'MISSING_SERVICE', 'MISSING_TYPES', 'MISSING_SOCKETS', 'MISSING_VALIDATORS', 'MISSING_TESTS', 'MISSING_CONNECTORS', 'EMPTY_MODULE_DESCRIPTION', 'MODEL_IN_TYPES_FILE', 'MISSING_MODEL_TS'].includes(v.type)) {
|
|
553
|
-
byCategory['Estructura']!.push(v)
|
|
554
|
-
} else if (['DIRECT_MODULE_IMPORT', 'SERVICE_IMPORTS_OTHER_MODULE', 'CONNECTOR_BUSINESS_LOGIC'].includes(v.type)) {
|
|
555
|
-
byCategory['Acoplamiento']!.push(v)
|
|
556
|
-
} else if (['BUSINESS_LOGIC_IN_CONTROLLER', 'GOD_SERVICE', 'INDEX_PENDING_CHANGES'].includes(v.type)) {
|
|
557
|
-
byCategory['Diseño']!.push(v)
|
|
558
|
-
} else if (['IDOR_RISK'].includes(v.type)) {
|
|
559
|
-
byCategory['Seguridad']!.push(v)
|
|
560
|
-
} else if (['N_PLUS_ONE_RISK'].includes(v.type)) {
|
|
561
|
-
byCategory['Performance']!.push(v)
|
|
562
|
-
} else if (['SERVICE_DEPENDS_ON_ORM'].includes(v.type)) {
|
|
563
|
-
byCategory['Portabilidad']!.push(v)
|
|
564
|
-
} else if (['DUPLICATE_CONNECTOR', 'UNREGISTERED_CONNECTOR'].includes(v.type)) {
|
|
565
|
-
byCategory['Acoplamiento']!.push(v)
|
|
566
|
-
} else {
|
|
567
|
-
byCategory['Calidad']!.push(v)
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
console.log(`\n❌ Violaciones (${result.violations.length}):`)
|
|
572
|
-
for (const [cat, vs] of Object.entries(byCategory)) {
|
|
573
|
-
if (vs.length === 0) continue
|
|
574
|
-
console.log(`\n [${cat}]`)
|
|
575
|
-
for (const v of vs) console.log(` • ${v.message}`)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (result.warnings.length > 0) {
|
|
580
|
-
console.log(`\n⚠️ Advertencias (${result.warnings.length}):`)
|
|
581
|
-
for (const w of result.warnings) {
|
|
582
|
-
console.log(` ${w.message}`)
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
console.log(`\n📊 Resultado: ${result.valid ? '✅ VÁLIDO' : '❌ VIOLACIONES ENCONTRADAS'}`)
|
|
587
|
-
console.log('══════════════════════════════════════\n')
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Genera el objeto arckode.json — snapshot del sistema legible por la IA.
|
|
592
|
-
* La IA lee este archivo PRIMERO para conocer el estado del proyecto
|
|
593
|
-
* sin tener que parsear 1800+ líneas del kernel ni todos los módulos.
|
|
594
|
-
*/
|
|
595
|
-
export function buildManifest(result: AnalysisResult): Record<string, unknown> {
|
|
596
|
-
return {
|
|
597
|
-
_arckode: '1.0.0',
|
|
598
|
-
_generated: new Date().toISOString(),
|
|
599
|
-
_instructions: 'Lee este archivo ANTES de escribir código. Contiene el estado completo del sistema: módulos, conectores, violaciones. Si está desactualizado, corré: bun run analyze --json',
|
|
600
|
-
|
|
601
|
-
system: {
|
|
602
|
-
healthy: result.valid,
|
|
603
|
-
violations: result.violations.length,
|
|
604
|
-
warnings: result.warnings.length,
|
|
605
|
-
},
|
|
606
|
-
|
|
607
|
-
modules: result.modules.map(m => ({
|
|
608
|
-
name: m.name,
|
|
609
|
-
structure: {
|
|
610
|
-
index: m.hasIndex,
|
|
611
|
-
types: m.hasTypes,
|
|
612
|
-
sockets: m.hasSockets,
|
|
613
|
-
service: m.hasService,
|
|
614
|
-
controller: m.hasController,
|
|
615
|
-
validators: m.hasValidators,
|
|
616
|
-
tests: m.hasTests,
|
|
617
|
-
},
|
|
618
|
-
complete: m.hasIndex && m.hasTypes && m.hasService && m.hasController && m.hasValidators && m.hasTests,
|
|
619
|
-
})),
|
|
620
|
-
|
|
621
|
-
connectors: result.connectors,
|
|
622
|
-
|
|
623
|
-
violations: result.violations.map(v => ({
|
|
624
|
-
module: v.module,
|
|
625
|
-
type: v.type,
|
|
626
|
-
message: v.message,
|
|
627
|
-
})),
|
|
628
|
-
|
|
629
|
-
warnings: result.warnings.map(w => ({
|
|
630
|
-
type: w.type,
|
|
631
|
-
message: w.message,
|
|
632
|
-
})),
|
|
633
|
-
|
|
634
|
-
rules_summary: [
|
|
635
|
-
'REGLA 1: Módulos NO importan de otros módulos. Usar conectores.',
|
|
636
|
-
'REGLA 2: index.ts es APPEND-ONLY. No eliminar exports.',
|
|
637
|
-
'REGLA 3: Conectores NO tienen lógica de negocio. Solo delegan.',
|
|
638
|
-
'REGLA 4: Cada tabla pertenece a UN módulo.',
|
|
639
|
-
'REGLA 5: Si no está en composition-root.ts, no existe.',
|
|
640
|
-
'REGLA 6: Todo POST/PUT/PATCH requiere validateSchema().',
|
|
641
|
-
'REGLA 7: Controller NO llama al ORM. Llama al service.',
|
|
642
|
-
'REGLA 18: Service recibe RepositoryAdapter<T>, no ORM directamente.',
|
|
643
|
-
'REGLA 22: model.ts separado de types.ts. Schema DB ≠ contrato TS.',
|
|
644
|
-
'REGLA 23: Un DEFAULT por caso. Escalar SOLO cuando la condición lo justifica. Nivel 1: OrmRepository / service.ts / Conector. Nivel 2+ solo si aplica.',
|
|
645
|
-
],
|
|
646
|
-
}
|
|
647
|
-
}
|
|
1
|
+
// cli/analyze.ts — Re-export shim (módulos reales en cli/analyze/)
|
|
2
|
+
export * from './analyze/index'
|