arckode-framework 1.0.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 (52) hide show
  1. package/README.md +546 -0
  2. package/adapters/__tests__/mysql.test.ts +283 -0
  3. package/adapters/jwt.ts +18 -0
  4. package/adapters/mysql.ts +98 -0
  5. package/adapters/postgres.ts +52 -0
  6. package/adapters/redis-cache.ts +64 -0
  7. package/adapters/sqlite.ts +73 -0
  8. package/adapters/vendor.d.ts +48 -0
  9. package/bin/arckode.js +7 -0
  10. package/cli/analyze.ts +506 -0
  11. package/cli/commands/db-migrate.ts +121 -0
  12. package/cli/commands/db-seed.ts +54 -0
  13. package/cli/commands/generate-api-client.ts +106 -0
  14. package/cli/commands/make-adapter.ts +132 -0
  15. package/cli/commands/make-auth.ts +297 -0
  16. package/cli/commands/make-frontend-module.ts +271 -0
  17. package/cli/commands/make-helper.ts +65 -0
  18. package/cli/commands/make-migration.ts +30 -0
  19. package/cli/commands/make-seed.ts +29 -0
  20. package/cli/generate.ts +132 -0
  21. package/cli/index.ts +604 -0
  22. package/cli/stubs/frontend-stub.ts +294 -0
  23. package/cli/stubs/fullstack-stub.ts +46 -0
  24. package/cli/stubs/module-stub.ts +469 -0
  25. package/kernel/__tests__/adapters.test.ts +101 -0
  26. package/kernel/__tests__/analyzer.test.ts +282 -0
  27. package/kernel/__tests__/framework.test.ts +617 -0
  28. package/kernel/__tests__/middlewares.test.ts +174 -0
  29. package/kernel/__tests__/static.test.ts +94 -0
  30. package/kernel/framework.ts +1851 -0
  31. package/kernel/middlewares.ts +179 -0
  32. package/kernel/static.ts +76 -0
  33. package/kernel/testing.ts +237 -0
  34. package/modules/events/index.ts +99 -0
  35. package/modules/mail/index.ts +51 -0
  36. package/modules/mail/smtp-adapter.ts +42 -0
  37. package/modules/queue/index.ts +78 -0
  38. package/modules/storage/index.ts +40 -0
  39. package/modules/storage/local-adapter.ts +41 -0
  40. package/modules/ws/__tests__/ws.test.ts +114 -0
  41. package/modules/ws/index.ts +136 -0
  42. package/package.json +99 -0
  43. package/skills/auth/SKILL.md +243 -0
  44. package/skills/cli/SKILL.md +258 -0
  45. package/skills/config/SKILL.md +253 -0
  46. package/skills/connectors/SKILL.md +259 -0
  47. package/skills/helpers/SKILL.md +206 -0
  48. package/skills/middlewares/SKILL.md +282 -0
  49. package/skills/orm/SKILL.md +260 -0
  50. package/skills/realtime/SKILL.md +307 -0
  51. package/skills/services/SKILL.md +206 -0
  52. package/skills/testing/SKILL.md +257 -0
package/cli/analyze.ts ADDED
@@ -0,0 +1,506 @@
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
+ module: string
46
+ message: string
47
+ }
48
+
49
+ export interface Warning {
50
+ type: 'NO_CONNECTORS' | 'SINGLE_MODULE' | 'NO_COMPOSITION_ROOT' | 'INDEX_PENDING_CHANGES'
51
+ message: string
52
+ }
53
+
54
+ export async function analyzeProject(basePath: string): Promise<AnalysisResult> {
55
+ const violations: Violation[] = []
56
+ const warnings: Warning[] = []
57
+ const modules: ModuleReport[] = []
58
+
59
+ const srcPath = join(basePath, 'src')
60
+ const modulesPath = join(srcPath, 'modules')
61
+ const connectorsPath = join(srcPath, 'connectors')
62
+ const crPath = join(srcPath, 'composition-root.ts')
63
+
64
+ // Verificar composition root
65
+ try {
66
+ await access(crPath)
67
+ } catch {
68
+ warnings.push({ type: 'NO_COMPOSITION_ROOT', message: 'No hay composition-root.ts en src/' })
69
+ return { valid: false, modules: [], connectors: [], violations, warnings }
70
+ }
71
+
72
+ // Leer módulos
73
+ try {
74
+ const moduleDirs = await readdir(modulesPath, { withFileTypes: true })
75
+ const moduleNames = moduleDirs.filter(d => d.isDirectory()).map(d => d.name)
76
+
77
+ if (moduleNames.length === 0) {
78
+ warnings.push({ type: 'SINGLE_MODULE', message: 'No hay módulos en src/modules/' })
79
+ }
80
+
81
+ for (const name of moduleNames) {
82
+ const modPath = join(modulesPath, name)
83
+ const report: ModuleReport = {
84
+ name,
85
+ hasIndex: await fileExists(join(modPath, 'index.ts')),
86
+ hasService: await fileExists(join(modPath, 'actions', 'service.ts')),
87
+ hasController: await fileExists(join(modPath, 'actions', 'controller.ts')),
88
+ hasTypes: await fileExists(join(modPath, 'types.ts')),
89
+ hasSockets: await fileExists(join(modPath, 'sockets.ts')),
90
+ hasValidators: await fileExists(join(modPath, 'validators', 'schema.ts')),
91
+ hasTests: await fileExists(join(modPath, 'tests', 'service.test.ts')),
92
+ }
93
+
94
+ if (!report.hasIndex) violations.push({ type: 'MISSING_INDEX', module: name, message: `Módulo "${name}" sin index.ts (puerta pública)` })
95
+ if (!report.hasTypes) violations.push({ type: 'MISSING_TYPES', module: name, message: `Módulo "${name}" sin types.ts (DTOs)` })
96
+ if (!report.hasSockets) violations.push({ type: 'MISSING_SOCKETS', module: name, message: `Módulo "${name}" sin sockets.ts (hooks opcionales)` })
97
+ if (!report.hasService) violations.push({ type: 'MISSING_SERVICE', module: name, message: `Módulo "${name}" sin actions/service.ts` })
98
+ if (!report.hasValidators) violations.push({ type: 'MISSING_VALIDATORS', module: name, message: `Módulo "${name}" sin validators/schema.ts` })
99
+ if (!report.hasTests) violations.push({ type: 'MISSING_TESTS', module: name, message: `Módulo "${name}" sin tests/` })
100
+
101
+ modules.push(report)
102
+ }
103
+ } catch {
104
+ violations.push({ type: 'MISSING_INDEX', module: 'modules', message: 'No existe src/modules/' })
105
+ }
106
+
107
+ // Verificar conectores
108
+ const connectors: string[] = []
109
+ try {
110
+ const connectorFiles = await readdir(connectorsPath)
111
+ for (const file of connectorFiles) {
112
+ if (file.endsWith('.ts')) connectors.push(file)
113
+ }
114
+
115
+ if (connectors.length === 0 && modules.length > 1) {
116
+ warnings.push({ type: 'NO_CONNECTORS', message: `Hay ${modules.length} módulos pero 0 conectores. Los módulos no están conectados.` })
117
+ }
118
+ } catch {
119
+ // No hay carpeta de conectores, puede ser válido si hay 1 módulo
120
+ }
121
+
122
+ // Verificar imports directos entre módulos (REGLAS CLAUDE #1)
123
+ for (const module of modules) {
124
+ try {
125
+ const allFiles = await getAllTsFiles(join(modulesPath, module.name))
126
+ for (const file of allFiles) {
127
+ const content = await readFile(file, 'utf-8')
128
+ const relPath = file.replace(modulesPath, '')
129
+ for (const other of modules) {
130
+ if (other.name === module.name) continue
131
+ const importPattern = new RegExp(`from ['"][^'"]*\\/modules\\/${other.name}([/'"]|\$)`)
132
+ if (importPattern.test(content)) {
133
+ violations.push({
134
+ type: 'DIRECT_MODULE_IMPORT',
135
+ module: module.name,
136
+ message: `"${relPath}" importa directo de "${other.name}". Usar conector. (CLAUDE #1)`,
137
+ })
138
+ }
139
+ }
140
+ }
141
+ } catch { /* no actions dir */ }
142
+ }
143
+
144
+ // Verificar lógica de negocio en conectores (REGLA CLAUDE #3)
145
+ for (const connFile of connectors) {
146
+ try {
147
+ const content = await readFile(join(connectorsPath, connFile), 'utf-8')
148
+ // Detectar patrones de lógica de negocio en conectores
149
+ const businessPatterns = [
150
+ /\bif\s*\(/g, /\bswitch\s*\(/g, /\bfor\s*\(/g, /\bwhile\s*\(/g,
151
+ /\btry\s*\{/g, /\bcatch\s*\(/g, /\bnew\s+\w+Action/g,
152
+ /\.create\(/g, /\.update\(/g, /\.delete\(/g,
153
+ ]
154
+
155
+ // Contar ocurrencias — 1 o 2 pueden ser normales, más indica lógica
156
+ let businessHits = 0
157
+ for (const pattern of businessPatterns) {
158
+ const matches = content.match(pattern)
159
+ if (matches) businessHits += matches.length
160
+ }
161
+
162
+ if (businessHits > 3) {
163
+ violations.push({
164
+ type: 'CONNECTOR_BUSINESS_LOGIC',
165
+ module: connFile,
166
+ message: `"${connFile}" tiene ${businessHits} patrones de lógica de negocio. Los conectores solo deben wirear. (CLAUDE #3)`,
167
+ })
168
+ }
169
+ } catch { /* ignore */ }
170
+ }
171
+
172
+ // Verificar regla APPEND-ONLY (REGLA CLAUDE #2) — detecta exports removidos
173
+ // Busca comentarios de deprecación o conflicto en index.ts
174
+ for (const module of modules) {
175
+ const indexPath = join(modulesPath, module.name, 'index.ts')
176
+ try {
177
+ const content = await readFile(indexPath, 'utf-8')
178
+ // Si index.ts tiene comentarios TODO o FIXME sobre exports rotos
179
+ if (content.includes('TODO:') || content.includes('FIXME:')) {
180
+ warnings.push({
181
+ type: 'INDEX_PENDING_CHANGES',
182
+ message: `"${module.name}/index.ts" tiene TODO/FIXME. Revisar antes de continuar.`,
183
+ })
184
+ }
185
+ } catch { /* no index */ }
186
+ }
187
+
188
+ // Verificar que los conectores existen si hay más de 1 módulo (REGLA CLAUDE #5)
189
+ if (modules.length > 1 && connectors.length === 0) {
190
+ violations.push({
191
+ type: 'MISSING_CONNECTORS',
192
+ module: 'system',
193
+ message: `Hay ${modules.length} módulos pero 0 conectores. Los módulos no están conectados. (CLAUDE #5)`,
194
+ })
195
+ }
196
+
197
+ // ── Los 6 que las reglas base no prevenían ──────────────────────
198
+
199
+ for (const module of modules) {
200
+ const modPath = join(modulesPath, module.name)
201
+
202
+ // 1. DOCUMENTACIÓN FALTANTE — descripción vacía en el módulo
203
+ const indexPath = join(modPath, 'index.ts')
204
+ try {
205
+ const indexContent = await readFile(indexPath, 'utf-8')
206
+ // Busca description: '' o description: " " en el createModule/export
207
+ if (/description:\s*['"`]\s*['"`]/.test(indexContent)) {
208
+ violations.push({
209
+ type: 'EMPTY_MODULE_DESCRIPTION',
210
+ module: module.name,
211
+ message: `"${module.name}" tiene descripción vacía. Todo módulo DEBE documentar su propósito.`,
212
+ })
213
+ }
214
+ } catch { /* no index — ya detectado */ }
215
+
216
+ // 2. TEST COVERAGE — archivo de test existe pero sin casos
217
+ const testPath = join(modPath, 'tests', 'service.test.ts')
218
+ try {
219
+ const testContent = await readFile(testPath, 'utf-8')
220
+ const hasTests = /\btest\s*\(|\bit\s*\(|\bdescribe\s*\(/.test(testContent)
221
+ if (!hasTests) {
222
+ violations.push({
223
+ type: 'TESTS_WITHOUT_CASES',
224
+ module: module.name,
225
+ message: `"${module.name}/tests/service.test.ts" existe pero no tiene ningún test(). Coverage = 0.`,
226
+ })
227
+ }
228
+ } catch { /* no test file — ya detectado por MISSING_TESTS */ }
229
+
230
+ // 3. VALIDACIONES INCOMPLETAS — controller con POST/PUT/PATCH sin validateSchema
231
+ const controllerPath = join(modPath, 'actions', 'controller.ts')
232
+ try {
233
+ const ctrlContent = await readFile(controllerPath, 'utf-8')
234
+ const hasMutatingRoute = /router\.(post|put|patch)\s*\(/.test(ctrlContent)
235
+ const hasValidation = /validateSchema\s*\(/.test(ctrlContent)
236
+ if (hasMutatingRoute && !hasValidation) {
237
+ violations.push({
238
+ type: 'CONTROLLER_MISSING_VALIDATION',
239
+ module: module.name,
240
+ message: `"${module.name}/controller.ts" tiene rutas POST/PUT/PATCH sin validateSchema(). Datos no validados llegan al service.`,
241
+ })
242
+ }
243
+ } catch { /* no controller */ }
244
+
245
+ // 4. LÓGICA EN CONTROLLER — ORM directo en controller (debería estar en service)
246
+ try {
247
+ const ctrlContent = await readFile(controllerPath, 'utf-8')
248
+ const ormInController = /\borm\.(findMany|findById|create|update|delete|paginate|count)\s*\(/.test(ctrlContent)
249
+ if (ormInController) {
250
+ violations.push({
251
+ type: 'BUSINESS_LOGIC_IN_CONTROLLER',
252
+ module: module.name,
253
+ message: `"${module.name}/controller.ts" llama al ORM directamente. La lógica de datos va en service.ts.`,
254
+ })
255
+ }
256
+ } catch { /* no controller */ }
257
+
258
+ // 5. GOD SERVICE — service con más de 200 líneas indica demasiadas responsabilidades
259
+ const servicePath = join(modPath, 'actions', 'service.ts')
260
+ try {
261
+ const serviceContent = await readFile(servicePath, 'utf-8')
262
+ const lineCount = serviceContent.split('\n').length
263
+ if (lineCount > 200) {
264
+ violations.push({
265
+ type: 'GOD_SERVICE',
266
+ module: module.name,
267
+ message: `"${module.name}/service.ts" tiene ${lineCount} líneas. Un service > 200 líneas es un God Object — dividir en servicios especializados.`,
268
+ })
269
+ }
270
+ } catch { /* no service */ }
271
+
272
+ // 6. SERVICE CRUZA MÓDULO — service importa de otro módulo directamente
273
+ try {
274
+ const serviceContent = await readFile(servicePath, 'utf-8')
275
+ for (const other of modules) {
276
+ if (other.name === module.name) continue
277
+ const importPattern = new RegExp(`from ['"][^'"]*\\/modules\\/${other.name}([/'"]|\$)`)
278
+ if (importPattern.test(serviceContent)) {
279
+ violations.push({
280
+ type: 'SERVICE_IMPORTS_OTHER_MODULE',
281
+ module: module.name,
282
+ message: `"${module.name}/service.ts" importa de "${other.name}". Los services no pueden cruzar módulos — usar conector. (CLAUDE #1)`,
283
+ })
284
+ }
285
+ }
286
+ } catch { /* no service */ }
287
+
288
+ // 7. N+1 RISK — ORM llamado dentro de un loop en el service
289
+ try {
290
+ const serviceContent = await readFile(servicePath, 'utf-8')
291
+ const lines = serviceContent.split('\n')
292
+ let insideLoop = 0
293
+ for (let i = 0; i < lines.length; i++) {
294
+ const line = lines[i] ?? ''
295
+ // Detectar apertura de loop
296
+ if (/\b(for\s*\(|forEach\s*\(|\.map\s*\(\s*async|\.filter\s*\(\s*async|while\s*\()/.test(line)) insideLoop++
297
+ // Detectar cierre de bloque (aproximación por llaves)
298
+ const opens = (line.match(/\{/g) ?? []).length
299
+ const closes = (line.match(/\}/g) ?? []).length
300
+ if (insideLoop > 0 && closes > opens) insideLoop = Math.max(0, insideLoop - 1)
301
+ // Detectar ORM dentro del loop
302
+ if (insideLoop > 0 && /await\s+\w*orm\w*\.(findMany|findById|create|update|delete|count|paginate)\s*\(/.test(line)) {
303
+ violations.push({
304
+ type: 'N_PLUS_ONE_RISK',
305
+ module: module.name,
306
+ 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.`,
307
+ })
308
+ break // un warning por archivo es suficiente
309
+ }
310
+ }
311
+ } catch { /* no service */ }
312
+
313
+ // 8. IDOR RISK — findById sin assertOwnership inmediatamente después
314
+ try {
315
+ const serviceContent = await readFile(servicePath, 'utf-8')
316
+ const lines = serviceContent.split('\n')
317
+ for (let i = 0; i < lines.length; i++) {
318
+ if (/\bfindById\s*\(/.test(lines[i] ?? '')) {
319
+ // Buscar assertOwnership en las siguientes 5 líneas
320
+ const window = lines.slice(i + 1, i + 6).join('\n')
321
+ if (!window.includes('assertOwnership') && !window.includes('userId') && !window.includes('ownerId')) {
322
+ violations.push({
323
+ type: 'IDOR_RISK',
324
+ module: module.name,
325
+ message: `"${module.name}/service.ts" línea ${i + 1}: findById sin verificación de ownership. Usar auth.assertOwnership() para prevenir IDOR.`,
326
+ })
327
+ break
328
+ }
329
+ }
330
+ }
331
+ } catch { /* no service */ }
332
+
333
+ // 9. SERVICE DEPENDS ON ORM — service inyecta ORM en lugar de RepositoryAdapter (Regla #18)
334
+ try {
335
+ const serviceContent = await readFile(servicePath, 'utf-8')
336
+ // Detectar: private orm: ORM o private readonly orm: ORM en el constructor
337
+ if (/private\s+(readonly\s+)?\w+\s*:\s*ORM\b/.test(serviceContent)) {
338
+ violations.push({
339
+ type: 'SERVICE_DEPENDS_ON_ORM',
340
+ module: module.name,
341
+ message: `"${module.name}/service.ts" inyecta ORM directamente. Usar RepositoryAdapter<T> para poder swapear SQL → MongoDB → Prisma sin tocar el service. (CLAUDE #18)`,
342
+ })
343
+ }
344
+ } catch { /* no service */ }
345
+ }
346
+
347
+ return {
348
+ valid: violations.length === 0,
349
+ modules,
350
+ connectors,
351
+ violations,
352
+ warnings,
353
+ }
354
+ }
355
+
356
+ async function fileExists(path: string): Promise<boolean> {
357
+ try { await access(path); return true } catch { return false }
358
+ }
359
+
360
+ async function getAllTsFiles(dir: string): Promise<string[]> {
361
+ const files: string[] = []
362
+ try {
363
+ const entries = await readdir(dir, { withFileTypes: true })
364
+ for (const entry of entries) {
365
+ const full = join(dir, entry.name)
366
+ if (entry.isDirectory()) {
367
+ if (entry.name !== 'node_modules' && entry.name !== 'tests' && entry.name !== '__tests__') {
368
+ files.push(...await getAllTsFiles(full))
369
+ }
370
+ } else if (entry.name.endsWith('.ts')) {
371
+ files.push(full)
372
+ }
373
+ }
374
+ } catch { /* ignore */ }
375
+ return files
376
+ }
377
+
378
+ export function printAnalysis(result: AnalysisResult): void {
379
+ console.log('\n══════════════════════════════════════')
380
+ console.log(' Análisis de Arquitectura')
381
+ console.log('══════════════════════════════════════\n')
382
+
383
+ console.log(`📦 Módulos (${result.modules.length}):`)
384
+ for (const m of result.modules) {
385
+ const icon = m.hasIndex ? '✓' : '✗'
386
+ const parts = [
387
+ m.hasService ? 'service' : null,
388
+ m.hasController ? 'controller' : null,
389
+ m.hasTypes ? 'types' : null,
390
+ m.hasSockets ? 'sockets' : null,
391
+ m.hasValidators ? 'validators' : null,
392
+ m.hasTests ? 'tests' : null,
393
+ ].filter(Boolean)
394
+ console.log(` ${icon} ${m.name} [${parts.join(', ')}]`)
395
+ }
396
+
397
+ console.log(`\n🔌 Conectores (${result.connectors.length}):`)
398
+ for (const c of result.connectors) {
399
+ console.log(` ${c}`)
400
+ }
401
+
402
+ if (result.violations.length > 0) {
403
+ // Agrupar por categoría para que la IA sepa exactamente qué arreglar
404
+ const byCategory: Record<string, Violation[]> = {
405
+ 'Estructura': [],
406
+ 'Acoplamiento': [],
407
+ 'Diseño': [],
408
+ 'Calidad': [],
409
+ 'Seguridad': [],
410
+ 'Performance': [],
411
+ 'Portabilidad': [],
412
+ }
413
+
414
+ for (const v of result.violations) {
415
+ if (['MISSING_INDEX', 'MISSING_SERVICE', 'MISSING_TYPES', 'MISSING_SOCKETS', 'MISSING_VALIDATORS', 'MISSING_TESTS', 'MISSING_CONNECTORS', 'EMPTY_MODULE_DESCRIPTION'].includes(v.type)) {
416
+ byCategory['Estructura']!.push(v)
417
+ } else if (['DIRECT_MODULE_IMPORT', 'SERVICE_IMPORTS_OTHER_MODULE', 'CONNECTOR_BUSINESS_LOGIC'].includes(v.type)) {
418
+ byCategory['Acoplamiento']!.push(v)
419
+ } else if (['BUSINESS_LOGIC_IN_CONTROLLER', 'GOD_SERVICE', 'INDEX_PENDING_CHANGES'].includes(v.type)) {
420
+ byCategory['Diseño']!.push(v)
421
+ } else if (['IDOR_RISK'].includes(v.type)) {
422
+ byCategory['Seguridad']!.push(v)
423
+ } else if (['N_PLUS_ONE_RISK'].includes(v.type)) {
424
+ byCategory['Performance']!.push(v)
425
+ } else if (['SERVICE_DEPENDS_ON_ORM'].includes(v.type)) {
426
+ byCategory['Portabilidad']!.push(v)
427
+ } else {
428
+ byCategory['Calidad']!.push(v)
429
+ }
430
+ }
431
+
432
+ console.log(`\n❌ Violaciones (${result.violations.length}):`)
433
+ for (const [cat, vs] of Object.entries(byCategory)) {
434
+ if (vs.length === 0) continue
435
+ console.log(`\n [${cat}]`)
436
+ for (const v of vs) console.log(` • ${v.message}`)
437
+ }
438
+ }
439
+
440
+ if (result.warnings.length > 0) {
441
+ console.log(`\n⚠️ Advertencias (${result.warnings.length}):`)
442
+ for (const w of result.warnings) {
443
+ console.log(` ${w.message}`)
444
+ }
445
+ }
446
+
447
+ console.log(`\n📊 Resultado: ${result.valid ? '✅ VÁLIDO' : '❌ VIOLACIONES ENCONTRADAS'}`)
448
+ console.log('══════════════════════════════════════\n')
449
+ }
450
+
451
+ /**
452
+ * Genera el objeto arckode.json — snapshot del sistema legible por la IA.
453
+ * La IA lee este archivo PRIMERO para conocer el estado del proyecto
454
+ * sin tener que parsear 1800+ líneas del kernel ni todos los módulos.
455
+ */
456
+ export function buildManifest(result: AnalysisResult): Record<string, unknown> {
457
+ return {
458
+ _arckode: '1.0.0',
459
+ _generated: new Date().toISOString(),
460
+ _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',
461
+
462
+ system: {
463
+ healthy: result.valid,
464
+ violations: result.violations.length,
465
+ warnings: result.warnings.length,
466
+ },
467
+
468
+ modules: result.modules.map(m => ({
469
+ name: m.name,
470
+ structure: {
471
+ index: m.hasIndex,
472
+ types: m.hasTypes,
473
+ sockets: m.hasSockets,
474
+ service: m.hasService,
475
+ controller: m.hasController,
476
+ validators: m.hasValidators,
477
+ tests: m.hasTests,
478
+ },
479
+ complete: m.hasIndex && m.hasTypes && m.hasService && m.hasController && m.hasValidators && m.hasTests,
480
+ })),
481
+
482
+ connectors: result.connectors,
483
+
484
+ violations: result.violations.map(v => ({
485
+ module: v.module,
486
+ type: v.type,
487
+ message: v.message,
488
+ })),
489
+
490
+ warnings: result.warnings.map(w => ({
491
+ type: w.type,
492
+ message: w.message,
493
+ })),
494
+
495
+ rules_summary: [
496
+ 'REGLA 1: Módulos NO importan de otros módulos. Usar conectores.',
497
+ 'REGLA 2: index.ts es APPEND-ONLY. No eliminar exports.',
498
+ 'REGLA 3: Conectores NO tienen lógica de negocio. Solo delegan.',
499
+ 'REGLA 4: Cada tabla pertenece a UN módulo.',
500
+ 'REGLA 5: Si no está en composition-root.ts, no existe.',
501
+ 'REGLA 6: Todo POST/PUT/PATCH requiere validateSchema().',
502
+ 'REGLA 7: Controller NO llama al ORM. Llama al service.',
503
+ 'REGLA 18: Service recibe RepositoryAdapter<T>, no ORM directamente.',
504
+ ],
505
+ }
506
+ }
@@ -0,0 +1,121 @@
1
+ // cli/commands/db-migrate.ts — Ejecuta migraciones SQL versionadas
2
+ // Corre los archivos de src/migrations/*.ts que tienen up() / down()
3
+ // Usa _arckode_migrations para rastrear cuáles ya se aplicaron
4
+
5
+ import Database from 'better-sqlite3'
6
+ import { readdir, readFile } from 'node:fs/promises'
7
+ import { join, resolve } from 'node:path'
8
+ import { existsSync } from 'node:fs'
9
+
10
+ async function readDbPath(basePath: string): Promise<string> {
11
+ const envFile = join(basePath, '.env')
12
+ if (existsSync(envFile)) {
13
+ const content = await readFile(envFile, 'utf-8')
14
+ const match = content.match(/^DB_PATH=(.+)$/m)
15
+ if (match?.[1]) return match[1].trim()
16
+ }
17
+ return './data/db.sqlite'
18
+ }
19
+
20
+ interface DbRunner {
21
+ run(sql: string, params?: unknown[]): void
22
+ exec(sql: string): void
23
+ }
24
+
25
+ export async function dbMigrate(direction: 'up' | 'down' = 'up') {
26
+ const basePath = process.cwd()
27
+ const migrationsDir = join(basePath, 'src', 'migrations')
28
+
29
+ if (!existsSync(migrationsDir)) {
30
+ console.error('❌ No se encuentra src/migrations/')
31
+ console.error(' Creá una con: arckode make:migration <nombre>')
32
+ return
33
+ }
34
+
35
+ const dbRelPath = await readDbPath(basePath)
36
+ const dbAbsPath = resolve(basePath, dbRelPath)
37
+
38
+ const db = new Database(dbAbsPath)
39
+
40
+ db.exec(`
41
+ CREATE TABLE IF NOT EXISTS _arckode_migrations (
42
+ name TEXT PRIMARY KEY,
43
+ direction TEXT NOT NULL,
44
+ runAt TEXT NOT NULL
45
+ )
46
+ `)
47
+
48
+ const files = (await readdir(migrationsDir))
49
+ .filter(f => f.endsWith('.ts') || f.endsWith('.js'))
50
+ .sort()
51
+
52
+ if (files.length === 0) {
53
+ console.log('📭 No hay archivos en src/migrations/')
54
+ db.close()
55
+ return
56
+ }
57
+
58
+ const applied = new Set<string>(
59
+ (db.prepare('SELECT name FROM _arckode_migrations WHERE direction = ?').all('up') as { name: string }[])
60
+ .map(r => r.name)
61
+ )
62
+
63
+ const runner: DbRunner = {
64
+ run(sql, params = []) { db.prepare(sql).run(...params) },
65
+ exec(sql) { db.exec(sql) },
66
+ }
67
+
68
+ if (direction === 'up') {
69
+ const pending = files.filter(f => !applied.has(f))
70
+
71
+ if (pending.length === 0) {
72
+ console.log('✅ No hay migraciones pendientes')
73
+ db.close()
74
+ return
75
+ }
76
+
77
+ for (const file of pending) {
78
+ const filePath = join(migrationsDir, file)
79
+ const migration = await import(filePath)
80
+
81
+ if (typeof migration.up !== 'function') {
82
+ console.warn(`⚠️ ${file} no exporta up() — saltando`)
83
+ continue
84
+ }
85
+
86
+ console.log(`▶ Aplicando: ${file}`)
87
+ await migration.up(runner)
88
+ db.prepare('INSERT INTO _arckode_migrations (name, direction, runAt) VALUES (?, ?, ?)')
89
+ .run(file, 'up', new Date().toISOString())
90
+ console.log(`✅ ${file}`)
91
+ }
92
+
93
+ console.log(`\n${pending.length} migración(es) aplicada(s)`)
94
+ } else {
95
+ // down: revertir la última aplicada
96
+ const last = db.prepare('SELECT name FROM _arckode_migrations WHERE direction = ? ORDER BY runAt DESC LIMIT 1')
97
+ .get('up') as { name: string } | undefined
98
+
99
+ if (!last) {
100
+ console.log('📭 No hay migraciones para revertir')
101
+ db.close()
102
+ return
103
+ }
104
+
105
+ const filePath = join(migrationsDir, last.name)
106
+ const migration = await import(filePath)
107
+
108
+ if (typeof migration.down !== 'function') {
109
+ console.error(`❌ ${last.name} no exporta down()`)
110
+ db.close()
111
+ return
112
+ }
113
+
114
+ console.log(`▶ Revirtiendo: ${last.name}`)
115
+ await migration.down(runner)
116
+ db.prepare('DELETE FROM _arckode_migrations WHERE name = ?').run(last.name)
117
+ console.log(`✅ Revertida: ${last.name}`)
118
+ }
119
+
120
+ db.close()
121
+ }
@@ -0,0 +1,54 @@
1
+ // cli/commands/db-seed.ts — Comando arckode db:seed
2
+ // Ejecuta los seeds definidos en el composition root
3
+
4
+ import { readFile } from 'node:fs/promises'
5
+ import { join } from 'node:path'
6
+ import { existsSync } from 'node:fs'
7
+
8
+ export async function dbSeed(seedName?: string) {
9
+ const basePath = process.cwd()
10
+ const crPath = join(basePath, 'src', 'composition-root.ts')
11
+
12
+ if (!existsSync(crPath)) {
13
+ console.error('❌ No se encuentra src/composition-root.ts')
14
+ console.error(' Ejecutá este comando desde la raíz del proyecto.')
15
+ return
16
+ }
17
+
18
+ // Leer seeds/ y detectar archivos disponibles
19
+ const seedsPath = join(basePath, 'src', 'seeds')
20
+ if (!existsSync(seedsPath)) {
21
+ console.error('❌ No hay directorio src/seeds/')
22
+ console.error(' Creá seeds con: arckode make:seed <Nombre>')
23
+ return
24
+ }
25
+
26
+ const { readdir } = await import('node:fs/promises')
27
+ const seedFiles = (await readdir(seedsPath)).filter(f => f.endsWith('.ts'))
28
+
29
+ if (seedFiles.length === 0) {
30
+ console.log('📭 No hay seeds para ejecutar.')
31
+ return
32
+ }
33
+
34
+ if (seedName) {
35
+ const file = seedFiles.find(f => f.startsWith(seedName))
36
+ if (!file) {
37
+ console.error(`❌ Seed "${seedName}" no encontrado.`)
38
+ console.log(` Seeds disponibles: ${seedFiles.join(', ')}`)
39
+ return
40
+ }
41
+ console.log(`🌱 Ejecutando seed: ${file}`)
42
+ // La IA o el usuario debe agregar la lógica en composition-root según el seed
43
+ console.log(` Para ejecutarlo, corré: RUN_SEEDS=true bun run src/composition-root.ts`)
44
+ return
45
+ }
46
+
47
+ console.log('🌱 Seeds disponibles:')
48
+ for (const file of seedFiles) {
49
+ console.log(` - ${file.replace('.ts', '')}`)
50
+ }
51
+ console.log('')
52
+ console.log(' Para ejecutar todos: RUN_SEEDS=true bun run src/composition-root.ts')
53
+ console.log(' Para ejecutar uno: bun arckode db:seed <nombre>')
54
+ }