arckode-framework 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/analyze.ts CHANGED
@@ -42,12 +42,15 @@ export interface Violation {
42
42
  | 'IDOR_RISK' // findById sin assertOwnership posterior
43
43
  // ── Portabilidad ──
44
44
  | 'SERVICE_DEPENDS_ON_ORM' // service inyecta ORM en lugar de RepositoryAdapter
45
+ // ── Conectores ──
46
+ | 'DUPLICATE_CONNECTOR' // mismo nombre lógico con distinto casing (foo-bar.ts y fooBar.ts)
47
+ | 'UNREGISTERED_CONNECTOR' // archivo existe pero no está en composition-root.ts
45
48
  module: string
46
49
  message: string
47
50
  }
48
51
 
49
52
  export interface Warning {
50
- type: 'NO_CONNECTORS' | 'SINGLE_MODULE' | 'NO_COMPOSITION_ROOT' | 'INDEX_PENDING_CHANGES'
53
+ type: 'NO_CONNECTORS' | 'SINGLE_MODULE' | 'NO_COMPOSITION_ROOT' | 'INDEX_PENDING_CHANGES' | 'LEGACY_ACTIONS_FOLDER'
51
54
  message: string
52
55
  }
53
56
 
@@ -55,6 +58,8 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
55
58
  const violations: Violation[] = []
56
59
  const warnings: Warning[] = []
57
60
  const modules: ModuleReport[] = []
61
+ // Mapa de paths internos resueltos por módulo (nueva estructura o legacy)
62
+ const internalPaths = new Map<string, { service: string | null; controller: string | null }>()
58
63
 
59
64
  const srcPath = join(basePath, 'src')
60
65
  const modulesPath = join(srcPath, 'modules')
@@ -80,21 +85,28 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
80
85
 
81
86
  for (const name of moduleNames) {
82
87
  const modPath = join(modulesPath, name)
88
+ // Soporta estructura nueva (root) y legacy (actions/) — el path resuelto
89
+ // queda guardado en internalPaths para reusar en chequeos posteriores
90
+ const servicePath = await resolvePath(modPath, ['service.ts', 'actions/service.ts'])
91
+ const controllerPath = await resolvePath(modPath, ['controller.ts', 'actions/controller.ts'])
92
+
83
93
  const report: ModuleReport = {
84
94
  name,
85
95
  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')),
96
+ hasService: servicePath !== null,
97
+ hasController: controllerPath !== null,
88
98
  hasTypes: await fileExists(join(modPath, 'types.ts')),
89
99
  hasSockets: await fileExists(join(modPath, 'sockets.ts')),
90
100
  hasValidators: await fileExists(join(modPath, 'validators', 'schema.ts')),
91
101
  hasTests: await fileExists(join(modPath, 'tests', 'service.test.ts')),
92
102
  }
93
103
 
104
+ internalPaths.set(name, { service: servicePath, controller: controllerPath })
105
+
94
106
  if (!report.hasIndex) violations.push({ type: 'MISSING_INDEX', module: name, message: `Módulo "${name}" sin index.ts (puerta pública)` })
95
107
  if (!report.hasTypes) violations.push({ type: 'MISSING_TYPES', module: name, message: `Módulo "${name}" sin types.ts (DTOs)` })
96
108
  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` })
109
+ if (!report.hasService) violations.push({ type: 'MISSING_SERVICE', module: name, message: `Módulo "${name}" sin service.ts (al root del módulo)` })
98
110
  if (!report.hasValidators) violations.push({ type: 'MISSING_VALIDATORS', module: name, message: `Módulo "${name}" sin validators/schema.ts` })
99
111
  if (!report.hasTests) violations.push({ type: 'MISSING_TESTS', module: name, message: `Módulo "${name}" sin tests/` })
100
112
 
@@ -115,6 +127,41 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
115
127
  if (connectors.length === 0 && modules.length > 1) {
116
128
  warnings.push({ type: 'NO_CONNECTORS', message: `Hay ${modules.length} módulos pero 0 conectores. Los módulos no están conectados.` })
117
129
  }
130
+
131
+ // Detectar conectores duplicados (ej: trading-wallets.ts + tradingWallets.ts)
132
+ const canonicalMap = new Map<string, string[]>()
133
+ for (const file of connectors) {
134
+ const canonical = canonicalizeConnectorName(file)
135
+ const list = canonicalMap.get(canonical) ?? []
136
+ list.push(file)
137
+ canonicalMap.set(canonical, list)
138
+ }
139
+ for (const [canonical, files] of canonicalMap) {
140
+ if (files.length > 1) {
141
+ violations.push({
142
+ type: 'DUPLICATE_CONNECTOR',
143
+ module: 'connectors',
144
+ message: `Conectores duplicados con mismo nombre lógico "${canonical}": ${files.join(', ')}. Eliminar los stubs muertos del CLI.`,
145
+ })
146
+ }
147
+ }
148
+
149
+ // Detectar conectores no registrados en composition-root.ts
150
+ try {
151
+ const crContent = await readFile(crPath, 'utf-8')
152
+ for (const file of connectors) {
153
+ const basename = file.replace(/\.ts$/, '')
154
+ // El conector debe aparecer como import o como string literal en addConnector()
155
+ const importPattern = new RegExp(`from\\s+['"][^'"]*connectors\\/${basename}['"]`)
156
+ if (!importPattern.test(crContent)) {
157
+ violations.push({
158
+ type: 'UNREGISTERED_CONNECTOR',
159
+ module: 'connectors',
160
+ message: `Conector "${file}" existe pero no se importa en composition-root.ts. Dead code o falta registrar con system.addConnector().`,
161
+ })
162
+ }
163
+ }
164
+ } catch { /* sin composition-root, ya warned arriba */ }
118
165
  } catch {
119
166
  // No hay carpeta de conectores, puede ser válido si hay 1 módulo
120
167
  }
@@ -227,36 +274,52 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
227
274
  }
228
275
  } catch { /* no test file — ya detectado por MISSING_TESTS */ }
229
276
 
277
+ // Paths resueltos (nueva estructura o legacy)
278
+ const paths = internalPaths.get(module.name) ?? { service: null, controller: null }
279
+ const controllerPath = paths.controller
280
+ const servicePath = paths.service
281
+
230
282
  // 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 */ }
283
+ if (controllerPath) {
284
+ try {
285
+ const ctrlContent = await readFile(controllerPath, 'utf-8')
286
+ const hasMutatingRoute = /router\.(post|put|patch)\s*\(/.test(ctrlContent)
287
+ const hasValidation = /validateSchema\s*\(/.test(ctrlContent)
288
+ if (hasMutatingRoute && !hasValidation) {
289
+ violations.push({
290
+ type: 'CONTROLLER_MISSING_VALIDATION',
291
+ module: module.name,
292
+ message: `"${module.name}/controller.ts" tiene rutas POST/PUT/PATCH sin validateSchema(). Datos no validados llegan al service.`,
293
+ })
294
+ }
295
+ } catch { /* no controller */ }
244
296
 
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 */ }
297
+ // 4. LÓGICA EN CONTROLLER — ORM directo en controller (debería estar en service)
298
+ try {
299
+ const ctrlContent = await readFile(controllerPath, 'utf-8')
300
+ const ormInController = /\borm\.(findMany|findById|create|update|delete|paginate|count)\s*\(/.test(ctrlContent)
301
+ if (ormInController) {
302
+ violations.push({
303
+ type: 'BUSINESS_LOGIC_IN_CONTROLLER',
304
+ module: module.name,
305
+ message: `"${module.name}/controller.ts" llama al ORM directamente. La lógica de datos va en service.ts.`,
306
+ })
307
+ }
308
+ } catch { /* no controller */ }
309
+ }
310
+
311
+ // Warning: legacy folder layout
312
+ if ((servicePath && servicePath.includes('/actions/')) || (controllerPath && controllerPath.includes('/actions/'))) {
313
+ warnings.push({
314
+ type: 'LEGACY_ACTIONS_FOLDER',
315
+ message: `"${module.name}" usa la estructura legacy actions/. Mover service.ts y controller.ts al root del módulo.`,
316
+ })
317
+ }
257
318
 
258
319
  // 5. GOD SERVICE — service con más de 200 líneas indica demasiadas responsabilidades
259
- const servicePath = join(modPath, 'actions', 'service.ts')
320
+ if (!servicePath) continue // sin service, skip los siguientes checks
321
+ // El service principal puede tener orquestador < 200 líneas + sub-services en usecases/
322
+ // Aquí solo medimos el archivo service.ts principal
260
323
  try {
261
324
  const serviceContent = await readFile(servicePath, 'utf-8')
262
325
  const lineCount = serviceContent.split('\n').length
@@ -264,7 +327,7 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
264
327
  violations.push({
265
328
  type: 'GOD_SERVICE',
266
329
  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.`,
330
+ message: `"${module.name}/service.ts" tiene ${lineCount} líneas. Un service > 200 líneas es un God Object — extraer a usecases/.`,
268
331
  })
269
332
  }
270
333
  } catch { /* no service */ }
@@ -311,24 +374,55 @@ export async function analyzeProject(basePath: string): Promise<AnalysisResult>
311
374
  } catch { /* no service */ }
312
375
 
313
376
  // 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
377
+ // Analiza también sub-services en usecases/ (la nueva estructura) o actions/ (legacy)
378
+ const filesToScan: string[] = [servicePath]
379
+ for (const subdir of ['usecases', 'actions']) {
380
+ const subdirPath = join(modPath, subdir)
381
+ try {
382
+ const subEntries = await readdir(subdirPath, { withFileTypes: true })
383
+ for (const e of subEntries) {
384
+ if (e.isFile() && e.name.endsWith('.ts') && e.name !== 'service.ts' && e.name !== 'controller.ts') {
385
+ filesToScan.push(join(subdirPath, e.name))
328
386
  }
329
387
  }
330
- }
331
- } catch { /* no service */ }
388
+ } catch { /* sin subdir */ }
389
+ }
390
+
391
+ for (const filePath of filesToScan) {
392
+ // Skip archivos de adapter/repo/interfaz — declaran findById pero no es business logic
393
+ if (/-orm\.ts$|-repo\.ts$|-repository\.ts$|repository\.ts$/.test(filePath)) continue
394
+ try {
395
+ const serviceContent = await readFile(filePath, 'utf-8')
396
+ const lines = serviceContent.split('\n')
397
+ let insideInterface = 0
398
+ for (let i = 0; i < lines.length; i++) {
399
+ const line = lines[i] ?? ''
400
+ // Trackear si estamos dentro de un bloque de interface
401
+ if (/^\s*(export\s+)?interface\s+\w+/.test(line)) insideInterface++
402
+ const opens = (line.match(/\{/g) ?? []).length
403
+ const closes = (line.match(/\}/g) ?? []).length
404
+ if (insideInterface > 0) insideInterface = Math.max(0, insideInterface + opens - closes)
405
+
406
+ // Skip findById en interfaces TS — no es business logic
407
+ if (insideInterface > 0) continue
408
+ // Skip declaraciones de tipo: "findById(...): Promise<...>" sin await ni asignación
409
+ if (/^\s*findById\s*\([^)]*\)\s*:/.test(line)) continue
410
+
411
+ if (/\bfindById\s*\(/.test(line)) {
412
+ const window = lines.slice(i + 1, i + 6).join('\n')
413
+ if (!window.includes('assertOwnership') && !window.includes('userId') && !window.includes('ownerId')) {
414
+ const relFile = relative(modulesPath, filePath)
415
+ violations.push({
416
+ type: 'IDOR_RISK',
417
+ module: module.name,
418
+ message: `"${relFile}" línea ${i + 1}: findById sin verificación de ownership. Usar auth.assertOwnership() para prevenir IDOR.`,
419
+ })
420
+ break
421
+ }
422
+ }
423
+ }
424
+ } catch { /* file unreadable */ }
425
+ }
332
426
 
333
427
  // 9. SERVICE DEPENDS ON ORM — service inyecta ORM en lugar de RepositoryAdapter (Regla #18)
334
428
  try {
@@ -357,6 +451,25 @@ async function fileExists(path: string): Promise<boolean> {
357
451
  try { await access(path); return true } catch { return false }
358
452
  }
359
453
 
454
+ // Resuelve el primer path existente de una lista de candidatos relativos.
455
+ // Permite soportar la estructura nueva (root del módulo) y la legacy (actions/).
456
+ async function resolvePath(modPath: string, candidates: string[]): Promise<string | null> {
457
+ for (const candidate of candidates) {
458
+ const full = join(modPath, candidate)
459
+ if (await fileExists(full)) return full
460
+ }
461
+ return null
462
+ }
463
+
464
+ // Normaliza nombre de conector a forma canónica para detectar duplicados
465
+ // "trading-wallets.ts" y "tradingWallets.ts" → "tradingwallets"
466
+ function canonicalizeConnectorName(filename: string): string {
467
+ return filename
468
+ .replace(/\.ts$/, '')
469
+ .replace(/[-_]/g, '')
470
+ .toLowerCase()
471
+ }
472
+
360
473
  async function getAllTsFiles(dir: string): Promise<string[]> {
361
474
  const files: string[] = []
362
475
  try {
@@ -424,6 +537,8 @@ export function printAnalysis(result: AnalysisResult): void {
424
537
  byCategory['Performance']!.push(v)
425
538
  } else if (['SERVICE_DEPENDS_ON_ORM'].includes(v.type)) {
426
539
  byCategory['Portabilidad']!.push(v)
540
+ } else if (['DUPLICATE_CONNECTOR', 'UNREGISTERED_CONNECTOR'].includes(v.type)) {
541
+ byCategory['Acoplamiento']!.push(v)
427
542
  } else {
428
543
  byCategory['Calidad']!.push(v)
429
544
  }
package/cli/generate.ts CHANGED
@@ -5,7 +5,7 @@ import { mkdir, writeFile } from 'node:fs/promises'
5
5
  import { join } from 'node:path'
6
6
  import type { ModuleStubParams } from './stubs/module-stub'
7
7
  import {
8
- indexStub, typesStub, socketsStub,
8
+ indexStub, modelStub, typesStub, socketsStub,
9
9
  serviceStub, controllerStub, validatorStub,
10
10
  testStub, migrationStub, seedStub,
11
11
  } from './stubs/module-stub'
@@ -52,15 +52,20 @@ export async function generateModule(params: {
52
52
  }
53
53
 
54
54
  const modulePath = join(basePath, 'modules', kebab)
55
- await mkdir(join(modulePath, 'actions'), { recursive: true })
56
55
  await mkdir(join(modulePath, 'validators'), { recursive: true })
57
56
  await mkdir(join(modulePath, 'tests'), { recursive: true })
58
57
 
58
+ // Nueva estructura:
59
+ // - service.ts y controller.ts al ROOT del módulo
60
+ // - model.ts separado de types.ts (schema DB vs DTOs)
61
+ // - repository.ts NO se genera por default (es opcional)
62
+ // - usecases/ se crea solo si el service supera 200 líneas (extracción manual)
59
63
  await writeFile(join(modulePath, 'index.ts'), indexStub(stubParams), 'utf-8')
64
+ await writeFile(join(modulePath, 'model.ts'), modelStub(stubParams), 'utf-8')
60
65
  await writeFile(join(modulePath, 'types.ts'), typesStub(stubParams), 'utf-8')
61
66
  await writeFile(join(modulePath, 'sockets.ts'), socketsStub(stubParams), 'utf-8')
62
- await writeFile(join(modulePath, 'actions/service.ts'), serviceStub(stubParams), 'utf-8')
63
- await writeFile(join(modulePath, 'actions/controller.ts'), controllerStub(stubParams), 'utf-8')
67
+ await writeFile(join(modulePath, 'service.ts'), serviceStub(stubParams), 'utf-8')
68
+ await writeFile(join(modulePath, 'controller.ts'), controllerStub(stubParams), 'utf-8')
64
69
  await writeFile(join(modulePath, 'validators/schema.ts'), validatorStub(stubParams), 'utf-8')
65
70
  await writeFile(join(modulePath, 'tests/service.test.ts'), testStub(stubParams), 'utf-8')
66
71
 
@@ -75,20 +80,20 @@ export async function generateModule(params: {
75
80
  await writeFile(join(seedsPath, `${kebab}.ts`), seedStub(stubParams), 'utf-8')
76
81
 
77
82
  console.log(`✅ Módulo "${pascal}" creado en modules/${kebab}`)
78
- console.log(` Archivos: index, types, sockets, service, controller, validators, tests`)
83
+ console.log(` Archivos: index, model, types, sockets, service, controller, validators, tests`)
84
+ console.log(` Opcional: crear repository.ts si necesitás queries del dominio`)
79
85
  console.log(` Migración: migrations/..._create_${kebab}.ts`)
80
86
  console.log(` Seed: seeds/${kebab}.ts`)
81
87
  console.log('')
82
88
  console.log(` Registralo en composition-root.ts:`)
83
89
  console.log(` import { ${pascal}Module } from './modules/${kebab}'`)
84
- console.log(` import { OrmRepository } from 'arckode-framework'`)
85
- console.log(` import type { ${pascal}DTO } from './modules/${kebab}/types'`)
86
- console.log(` orm.define('${pascal}', { table: '${kebab}', fields: { nombre: { type: 'string' } }, timestamps: true })`)
87
- console.log(` const ${kebab}Repo = new OrmRepository<${pascal}DTO>(orm, '${pascal}')`)
88
- console.log(` system.addModule(${pascal}Module) // pasar ${kebab}Repo al módulo`)
90
+ console.log(` system.addModule(${pascal}Module())`)
91
+ console.log(` // El módulo registra su modelo internamente via register${pascal}Models(orm)`)
89
92
  }
90
93
 
91
94
  // ─── Generador de conector ───
95
+ // Archivo: kebab-case (consistente con composition-root.ts)
96
+ // Función exportada: connect + PascalCase
92
97
  export async function generateConnector(params: {
93
98
  name: string
94
99
  basePath: string
@@ -96,34 +101,36 @@ export async function generateConnector(params: {
96
101
  description?: string
97
102
  }): Promise<void> {
98
103
  const { name, basePath, modules, description = '' } = params
99
- const camel = toCase(name, 'camel')
104
+ const kebab = toCase(name, 'kebab')
105
+ const pascal = toCase(name, 'pascal')
106
+ const fnName = `connect${pascal}`
100
107
 
101
108
  const connectorPath = join(basePath, 'connectors')
102
109
  await mkdir(connectorPath, { recursive: true })
103
110
 
104
- await writeFile(join(connectorPath, `${camel}.ts`), [
105
- `// connectors/${camel}.ts — Conector entre módulos`,
106
- `// Responsabilidad ÚNICA: conectar módulos SIN modificarlos`,
107
- `// SOLID: cada conector conecta módulos, no contiene lógica de negocio`,
111
+ await writeFile(join(connectorPath, `${kebab}.ts`), [
112
+ `// connectors/${kebab}.ts — Conector entre módulos`,
113
+ `// Responsabilidad ÚNICA: wirear módulos vía setSockets()`,
114
+ `// REGLA #3: cero lógica de negocio. Solo DELEGAR a acciones públicas.`,
108
115
  ``,
109
116
  `// Conecta: ${modules.join(' → ')}`,
110
117
  description ? `// Descripción: ${description}` : '',
111
118
  ``,
112
119
  `import type { ConnectorContext } from 'arckode-framework'`,
120
+ modules.map(m => `import type { ${toCase(m, 'pascal')}Service } from '../modules/${toCase(m, 'kebab')}'`).join('\n'),
113
121
  ``,
114
- `export function ${camel}(ctx: ConnectorContext): void {`,
115
- modules.map(m => ` const ${toCase(m, 'camel')} = ctx.resolveModule<any>('${m}')`).join('\n'),
122
+ `export function ${fnName}(ctx: ConnectorContext): void {`,
123
+ modules.map(m => ` const ${toCase(m, 'camel')} = ctx.resolveModule<${toCase(m, 'pascal')}Service>('${toCase(m, 'kebab')}')`).join('\n'),
116
124
  ``,
117
- modules.slice(1).map(m => {
118
- const prev = toCase(modules[0] ?? '', 'camel')
119
- const curr = toCase(m, 'camel')
120
- return ` // ${prev} → ${curr}: conectar eventos`
121
- }).join('\n'),
125
+ ` // Ejemplo: ${toCase(modules[0] ?? '', 'camel')}.setSockets({ onEvent: async (payload) => { await ${toCase(modules[1] ?? '', 'camel')}.action(payload) } })`,
122
126
  `}`,
123
127
  ``,
124
128
  ].join('\n'))
125
129
 
126
- console.log(`✅ Conector "${camel}" creado en ${connectorPath} (conecta: ${modules.join(', ')})`)
130
+ console.log(`✅ Conector "${kebab}.ts" creado (función: ${fnName})`)
131
+ console.log(` Registralo en composition-root.ts:`)
132
+ console.log(` import { ${fnName} } from './connectors/${kebab}'`)
133
+ console.log(` system.addConnector('${kebab}', ${fnName})`)
127
134
  }
128
135
 
129
136
  function mapFieldType(type: string): string {
package/cli/index.ts CHANGED
@@ -6,6 +6,7 @@ import { mkdir, writeFile } from 'node:fs/promises'
6
6
  import { join } from 'node:path'
7
7
  import { generateModule, generateConnector } from './generate'
8
8
  import { analyzeProject, printAnalysis, buildManifest } from './analyze'
9
+ import { claudeMdStub } from './stubs/claude-md-stub'
9
10
 
10
11
  async function main() {
11
12
  const [, , cmd, ...args] = process.argv
@@ -18,7 +19,8 @@ async function main() {
18
19
 
19
20
  // Detectar DB adapter: --db=mysql | --db=postgres | default: sqlite
20
21
  const dbFlag = args.find(a => a.startsWith('--db='))?.split('=')[1] ?? 'sqlite'
21
- const db = ['sqlite', 'mysql', 'postgres'].includes(dbFlag) ? dbFlag : 'sqlite'
22
+ const db: 'sqlite' | 'mysql' | 'postgres' =
23
+ dbFlag === 'mysql' || dbFlag === 'postgres' ? dbFlag : 'sqlite'
22
24
 
23
25
  const base = join(process.cwd(), name)
24
26
 
@@ -27,79 +29,9 @@ async function main() {
27
29
  await mkdir(join(base, 'src', 'connectors'), { recursive: true })
28
30
 
29
31
  // ─── CLAUDE.md ─────────────────────────────────────────────────────
30
- await writeFile(join(base, 'CLAUDE.md'), [
31
- `# CLAUDE.md ${name}`,
32
- `## Lee esto ANTES de escribir cualquier línea de código.`,
33
- ``,
34
- `### PASO 0 — Cargar contexto del sistema`,
35
- `1. Leer \`arckode.json\` → saber qué módulos, rutas y modelos ya existen.`,
36
- `2. Si no existe: \`bun run analyze --json\` para generarlo.`,
37
- `3. Leer \`src/composition-root.ts\` → es la única fuente de verdad del sistema.`,
38
- ``,
39
- `### Reglas INMUTABLES (no se negocian, no se omiten)`,
40
- ``,
41
- `| # | Regla | Detectada por |`,
42
- `|---|-------|--------------|`,
43
- `| 1 | Módulos NO importan de otros módulos → usar conectores | \`arckode analyze\` |`,
44
- `| 2 | index.ts es APPEND-ONLY → no eliminar exports | manual |`,
45
- `| 3 | Conectores NO tienen lógica de negocio → solo delegan | \`arckode analyze\` |`,
46
- `| 4 | Cada tabla pertenece a UN módulo | manual |`,
47
- `| 5 | Si no está en composition-root.ts, no existe | \`arckode analyze\` |`,
48
- `| 6 | Todo POST/PUT/PATCH requiere validateSchema() | \`arckode analyze\` |`,
49
- `| 7 | Controller NO llama al ORM → llama al service | \`arckode analyze\` |`,
50
- `| 8 | Service > 200 líneas → dividir | \`arckode analyze\` |`,
51
- `| 9 | findById → verificar ownership inmediatamente (IDOR) | \`arckode analyze\` |`,
52
- `| 10 | ORM dentro de un loop → N+1 garantizado, prohibido | \`arckode analyze\` |`,
53
- `| 11 | Service recibe RepositoryAdapter<T>, no ORM directo | \`arckode analyze\` |`,
54
- ``,
55
- `### Protocolo de la IA (4 pasos obligatorios)`,
56
- ``,
57
- `\`\`\``,
58
- `PASO 1 — IDENTIFICAR ALCANCE`,
59
- ` ¿Toca 1 módulo? → Seguir`,
60
- ` ¿Toca 1 conector? → Seguir`,
61
- ` ¿Toca 2+ módulos? → DETENERSE. Dividir la tarea.`,
62
- ``,
63
- `PASO 2 — VERIFICAR REGLAS`,
64
- ` ¿Importo de otro módulo? → NO`,
65
- ` ¿Modifico index.ts? → Solo append`,
66
- ` ¿Pongo lógica en el conector? → NO`,
67
- ` ¿Comparto una tabla? → NO`,
68
- ``,
69
- `PASO 3 — GENERAR`,
70
- ` Estructura obligatoria: index.ts, types.ts, sockets.ts,`,
71
- ` actions/service.ts, actions/controller.ts,`,
72
- ` validators/schema.ts, tests/service.test.ts`,
73
- ``,
74
- `PASO 4 — VERIFICAR`,
75
- ` bun run analyze --json → violations: 0`,
76
- ` bun test → 0 fallos`,
77
- `\`\`\``,
78
- ``,
79
- `### DB Adapter activo: \`${db}\``,
80
- ``,
81
- `### Estructura del proyecto`,
82
- `\`\`\``,
83
- `src/`,
84
- ` composition-root.ts ← TODO el sistema`,
85
- ` modules/ ← módulos independientes (NO se importan entre sí)`,
86
- ` connectors/ ← puentes entre módulos (sin lógica de negocio)`,
87
- ` shared/helpers/ ← funciones puras sin estado`,
88
- ` shared/services/ ← servicios con estado compartidos`,
89
- `seeds/ ← datos de prueba`,
90
- `migrations/ ← migraciones de BD`,
91
- `\`\`\``,
92
- ``,
93
- `### Comandos útiles`,
94
- `\`\`\`bash`,
95
- `bun run dev # desarrollo con hot reload`,
96
- `bun run analyze # verificar arquitectura (consola)`,
97
- `bun run analyze --json # generar/actualizar arckode.json`,
98
- `bun arckode make:module Nombre # generar módulo completo`,
99
- `bun arckode make:connector nombre mod1 mod2 # conectar dos módulos`,
100
- `bun test # correr todos los tests`,
101
- `\`\`\``,
102
- ].join('\n'))
32
+ // Contenido del stub vive en cli/stubs/claude-md-stub.ts
33
+ // (sincronizado con CLAUDE.md maestro del framework)
34
+ await writeFile(join(base, 'CLAUDE.md'), claudeMdStub({ projectName: name, db }))
103
35
 
104
36
  // composition-root.ts
105
37
  await writeFile(join(base, 'src', 'composition-root.ts'), [
@@ -165,14 +97,7 @@ async function main() {
165
97
  `const cache = new MemoryCache()`,
166
98
  `const auth = new Auth(jwtTokenAdapter, config.get('JWT_SECRET'), logger)`,
167
99
  ``,
168
- `// ─── 3. MODELOS (cada módulo es dueño de su ModelDefinition) ─`,
169
- `// import { MiModuloModel } from './modules/mi-modulo/types'`,
170
- `// orm.define('MiModulo', MiModuloModel)`,
171
- ``,
172
- `// ─── 4. MIGRAR ───────────────────────────────`,
173
- `await orm.migrate()`,
174
- ``,
175
- `// ─── 5. REGISTRAR ────────────────────────────`,
100
+ `// ─── 3. CONTAINER ────────────────────────────`,
176
101
  `container.register('config', () => config)`,
177
102
  `container.register('logger', () => logger)`,
178
103
  `container.register('db', () => dbAdapter, () => dbAdapter.close())`,
@@ -180,32 +105,44 @@ async function main() {
180
105
  `container.register('cache', () => cache)`,
181
106
  `container.init()`,
182
107
  ``,
183
- `// ─── 6. SISTEMA ───────────────────────────────`,
108
+ `// ─── 4. SISTEMA ───────────────────────────────`,
184
109
  `const system = new System({`,
185
110
  ` config, container, logger, orm, router, http, cache, auth,`,
186
111
  `})`,
187
112
  ``,
188
- `// Registrar módulos usar factory functions generadas por arckode make:module:`,
113
+ `// ─── 5. MÓDULOS ───────────────────────────────`,
114
+ `// Cada módulo se autorregistra: su index.ts llama registerXxxModels(orm)`,
115
+ `// en el create(). Acá SOLO addModule(). No tocar orm.define() manualmente.`,
116
+ ``,
189
117
  `// import { MiModuloModule } from './modules/mi-modulo'`,
190
118
  `// system.addModule(MiModuloModule())`,
191
119
  ``,
192
- `// Conectar módulos:`,
193
- `// system.addConnector('nombre', (ctx) => {`,
194
- `// const modA = ctx.resolveModule('mod-a')`,
195
- `// ctx.resolveModule('mod-b', { onEvento: async (data) => modA.accion(data) })`,
196
- `// })`,
120
+ `// ─── 6. CONECTORES ────────────────────────────`,
121
+ `// Generar con: arckode make:connector nombre-x mod-a mod-b`,
122
+ `// import { connectModAModB } from './connectors/mod-a-mod-b'`,
123
+ `// system.addConnector('mod-a-mod-b', connectModAModB)`,
124
+ ``,
125
+ `// Para atomicidad multi-tabla (financieras, ledgers, transferencias):`,
126
+ `// import { OrmTransactor } from 'arckode-framework'`,
127
+ `// const transactor = new OrmTransactor(orm)`,
128
+ `// → inyectarlo al service que lo necesite (Regla #19)`,
197
129
  ``,
198
- `// ─── 7. RUTAS ─────────────────────────────────`,
130
+ `// ─── 7. RUTAS BASE ────────────────────────────`,
199
131
  `router.get('/health', async () => ({`,
200
132
  ` status: 200, body: { status: 'ok', uptime: process.uptime() }`,
201
133
  `}))`,
202
134
  ``,
203
- `// ─── 8. SEEDS (solo en desarrollo) ────────────`,
135
+ `// ─── 8. INIT + MIGRATE + START ────────────────`,
136
+ `// Orden CRÍTICO: init() ejecuta los create() de los módulos (registra modelos),`,
137
+ `// luego migrate() crea las tablas, luego start() levanta el HTTP.`,
138
+ `system.init()`,
139
+ `await orm.migrate()`,
140
+ ``,
141
+ `// ─── 9. SEEDS (solo en desarrollo) ────────────`,
204
142
  `if (env.RUN_SEEDS === 'true') {`,
205
143
  ` // await seeds.runAll()`,
206
144
  `}`,
207
145
  ``,
208
- `// ─── 9. INICIAR ───────────────────────────────`,
209
146
  `await system.start()`,
210
147
  ``,
211
148
  `process.on('SIGINT', async () => { await system.stop(); process.exit(0) })`,