arckode-framework 1.0.8 → 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 +162 -47
- package/cli/generate.ts +30 -23
- package/cli/index.ts +29 -92
- package/cli/stubs/claude-md-stub.ts +174 -0
- package/cli/stubs/module-stub.ts +90 -21
- package/kernel/framework.ts +55 -1
- package/package.json +3 -3
- package/skills/auth/SKILL.md +1 -1
- package/skills/cli/SKILL.md +21 -12
- package/skills/connectors/SKILL.md +4 -4
- package/skills/helpers/SKILL.md +1 -1
- package/skills/orm/SKILL.md +154 -18
- package/skills/realtime/SKILL.md +1 -1
- package/skills/services/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
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:
|
|
87
|
-
hasController:
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
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
|
-
}
|
|
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, '
|
|
63
|
-
await writeFile(join(modulePath, '
|
|
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(`
|
|
85
|
-
console.log(`
|
|
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
|
|
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, `${
|
|
105
|
-
`// connectors/${
|
|
106
|
-
`// Responsabilidad ÚNICA:
|
|
107
|
-
`//
|
|
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 ${
|
|
115
|
-
modules.map(m => ` const ${toCase(m, 'camel')} = ctx.resolveModule
|
|
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.
|
|
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 "${
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
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
|
-
`// ───
|
|
108
|
+
`// ─── 4. SISTEMA ───────────────────────────────`,
|
|
184
109
|
`const system = new System({`,
|
|
185
110
|
` config, container, logger, orm, router, http, cache, auth,`,
|
|
186
111
|
`})`,
|
|
187
112
|
``,
|
|
188
|
-
`//
|
|
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
|
-
`//
|
|
193
|
-
`//
|
|
194
|
-
`//
|
|
195
|
-
`//
|
|
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.
|
|
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) })`,
|