arckode-framework 1.3.2 → 1.4.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.
Files changed (64) hide show
  1. package/adapters/jwt.ts +6 -4
  2. package/adapters/mysql.ts +7 -2
  3. package/adapters/postgres.ts +37 -0
  4. package/adapters/sqlite.ts +7 -1
  5. package/adapters/vendor.d.ts +48 -0
  6. package/cli/analyze/checks.ts +333 -0
  7. package/cli/analyze/index.ts +44 -0
  8. package/cli/analyze/report.ts +107 -0
  9. package/cli/analyze/types.ts +46 -0
  10. package/cli/analyze/utils.ts +36 -0
  11. package/cli/analyze.ts +2 -647
  12. package/cli/commands/db-migrate.ts +213 -89
  13. package/cli/commands/db-seed.ts +97 -32
  14. package/cli/commands/db-utils.ts +192 -0
  15. package/cli/commands/new.ts +175 -0
  16. package/cli/commands/routes.ts +94 -0
  17. package/cli/index.ts +57 -404
  18. package/cli/stubs/module/core.ts +162 -0
  19. package/cli/stubs/module/data.ts +171 -0
  20. package/cli/stubs/module/index.ts +5 -0
  21. package/cli/stubs/module/service.ts +198 -0
  22. package/cli/stubs/module/types.ts +12 -0
  23. package/cli/stubs/module-stub.ts +2 -552
  24. package/kernel/auth.ts +114 -0
  25. package/kernel/cache.ts +37 -0
  26. package/kernel/config.ts +129 -0
  27. package/kernel/container.ts +64 -0
  28. package/kernel/db/orm-migrate.ts +136 -0
  29. package/kernel/db/orm-repository.ts +45 -0
  30. package/kernel/db/orm-utils.ts +93 -0
  31. package/kernel/db/orm.ts +254 -0
  32. package/kernel/db/transactor.ts +17 -0
  33. package/kernel/db/types.ts +72 -0
  34. package/kernel/errors.ts +102 -0
  35. package/kernel/framework.default.ts +41 -0
  36. package/kernel/framework.ts +8 -2144
  37. package/kernel/http/router.ts +131 -0
  38. package/kernel/http/server.ts +303 -0
  39. package/kernel/http/types.ts +56 -0
  40. package/kernel/index.ts +25 -0
  41. package/kernel/logger.ts +50 -0
  42. package/kernel/middlewares.ts +19 -7
  43. package/kernel/modules/create-module.ts +5 -0
  44. package/kernel/modules/system.ts +149 -0
  45. package/kernel/modules/types.ts +46 -0
  46. package/kernel/seeds.ts +48 -0
  47. package/kernel/static.ts +11 -2
  48. package/kernel/testing.ts +8 -3
  49. package/kernel/validator.ts +116 -0
  50. package/modules/events/index.ts +19 -3
  51. package/modules/mail/index.ts +14 -2
  52. package/modules/storage/local-adapter.ts +19 -5
  53. package/modules/ws/index.ts +123 -18
  54. package/package.json +8 -11
  55. package/skills/auth/SKILL.md +36 -220
  56. package/skills/cli/SKILL.md +32 -251
  57. package/skills/config/SKILL.md +30 -239
  58. package/skills/connectors/SKILL.md +32 -295
  59. package/skills/helpers/SKILL.md +26 -195
  60. package/skills/middlewares/SKILL.md +30 -280
  61. package/skills/orm/SKILL.md +42 -349
  62. package/skills/realtime/SKILL.md +22 -297
  63. package/skills/services/SKILL.md +40 -183
  64. package/skills/testing/SKILL.md +34 -266
package/cli/analyze.ts CHANGED
@@ -1,647 +1,2 @@
1
- // cli/analyze.ts — Analizador estático de arquitectura
2
- // Lee el proyecto como grafo y detecta violaciones
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'