@ronaldjdevfs/forge 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +338 -0
- package/logo.png +0 -0
- package/package.json +47 -0
- package/skills/forge/SKILL.md +207 -0
- package/skills/forge/command/forge.md +110 -0
- package/skills/forge/profiles/express-mongodb.md +289 -0
- package/skills/forge/profiles/express-postgres.md +164 -0
- package/skills/forge/profiles/express-prisma.md +116 -0
- package/skills/forge/profiles/fastify-postgres.md +110 -0
- package/skills/forge/profiles/nestjs-prisma.md +160 -0
- package/skills/forge/reference/cast.md +77 -0
- package/skills/forge/reference/chain.md +73 -0
- package/skills/forge/reference/forge.md +44 -0
- package/skills/forge/reference/inscribe.md +129 -0
- package/skills/forge/reference/inspect.md +58 -0
- package/skills/forge/reference/patterns.md +108 -0
- package/skills/forge/reference/principles.md +29 -0
- package/skills/forge/reference/quench.md +74 -0
- package/skills/forge/reference/reforge.md +40 -0
- package/skills/forge/reference/relocate.md +38 -0
- package/skills/forge/reference/smelt.md +31 -0
- package/skills/forge/reference/temper.md +49 -0
- package/skills/forge/scripts/architecture.mjs +176 -0
- package/skills/forge/scripts/armorer.mjs +421 -0
- package/skills/forge/scripts/bootstrap.mjs +187 -0
- package/skills/forge/scripts/chain.mjs +258 -0
- package/skills/forge/scripts/context.mjs +237 -0
- package/skills/forge/scripts/detect.mjs +843 -0
- package/skills/forge/scripts/graph.mjs +594 -0
- package/skills/forge/scripts/inspect.mjs +193 -0
- package/skills/forge/scripts/profile.mjs +92 -0
- package/skills/forge/templates/feature/controller.ts.md +66 -0
- package/skills/forge/templates/feature/entity.ts.md +11 -0
- package/skills/forge/templates/feature/mapper.ts.md +18 -0
- package/skills/forge/templates/feature/repository-impl.ts.md +59 -0
- package/skills/forge/templates/feature/repository-interface.ts.md +12 -0
- package/skills/forge/templates/feature/routes.ts.md +17 -0
- package/skills/forge/templates/feature/schema.ts.md +16 -0
- package/skills/forge/templates/feature/use-case.ts.md +19 -0
- package/skills/forge/templates/infra/mail.ts.md +31 -0
- package/skills/forge/templates/infra/mongodb.ts.md +12 -0
- package/skills/forge/templates/infra/prisma.ts.md +21 -0
- package/skills/forge/templates/infra/redis.ts.md +30 -0
- package/skills/forge/templates/platform/config.ts.md +37 -0
- package/skills/forge/templates/platform/database.ts.md +40 -0
- package/skills/forge/templates/platform/di.ts.md +18 -0
- package/skills/forge/templates/platform/http.ts.md +29 -0
- package/skills/forge/templates/platform/logger.ts.md +38 -0
- package/skills/forge/templates/platform/server.ts.md +25 -0
- package/skills/forge/templates/shared/contract.ts.md +20 -0
- package/skills/forge/templates/shared/error.ts.md +27 -0
- package/skills/forge/templates/shared/type.ts.md +18 -0
- package/skills/forge/templates/shared/util.ts.md +23 -0
- package/src/cli.js +179 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { join, relative, basename } from "path";
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { buildGraph } from "./graph.mjs";
|
|
6
|
+
import { buildOwnershipReport } from "./armorer.mjs";
|
|
7
|
+
|
|
8
|
+
const ROOT = process.cwd();
|
|
9
|
+
const SRC = join(ROOT, "src");
|
|
10
|
+
const FEATURES = join(SRC, "features");
|
|
11
|
+
|
|
12
|
+
const LEGACY_DIRS = [
|
|
13
|
+
["src/domain/entities", "Entidades legacy"],
|
|
14
|
+
["src/domain/repositories", "Repository interfaces legacy"],
|
|
15
|
+
["src/application/use-cases", "Casos de uso legacy (subcarpetas)"],
|
|
16
|
+
["src/adapters/in/http/controllers", "Controllers legacy"],
|
|
17
|
+
["src/adapters/out/database/repositories", "Repository impls legacy"],
|
|
18
|
+
["src/adapters/out/database/schemas", "Schemas legacy"],
|
|
19
|
+
["src/adapters/out/database/mappers", "Mappers legacy"],
|
|
20
|
+
["src/setting/dependencies", "DI wiring legacy (.di.ts)"],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const IMPORT_RE = /import\s+(?:type\s+)?(?:\{[^}]*\}|[^;{]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
24
|
+
const INJECTABLE_RE = /@injectable\s*\(\)/g;
|
|
25
|
+
const INJECT_RE = /@inject\s*\([^)]+\)/g;
|
|
26
|
+
const BD_MODEL_RE = /\b\w+Model\s*\.\s*(find|findOne|findById|create|insertMany|updateOne|updateMany|deleteOne|deleteMany|aggregate|save|lean)\s*\(/g;
|
|
27
|
+
const LOGIC_KEYWORDS_RE = /\b(if|for|while|switch|catch|throw|return)\s*\(/g;
|
|
28
|
+
|
|
29
|
+
const SEVERITY = {
|
|
30
|
+
CRITICAL: "CRITICAL",
|
|
31
|
+
ERROR: "ERROR",
|
|
32
|
+
WARNING: "WARNING",
|
|
33
|
+
INFO: "INFO",
|
|
34
|
+
SUGGESTION: "SUGGESTION",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function read(path) {
|
|
38
|
+
try {
|
|
39
|
+
return readFileSync(path, "utf-8");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function exists(path) {
|
|
46
|
+
return existsSync(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isDir(path) {
|
|
50
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listDir(path) {
|
|
54
|
+
try {
|
|
55
|
+
return readdirSync(path);
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findFiles(dir, ext, maxDepth = 5) {
|
|
62
|
+
const results = [];
|
|
63
|
+
function walk(d, depth) {
|
|
64
|
+
if (depth > maxDepth) return;
|
|
65
|
+
try {
|
|
66
|
+
for (const entry of readdirSync(d)) {
|
|
67
|
+
const full = join(d, entry);
|
|
68
|
+
if (statSync(full).isDirectory()) {
|
|
69
|
+
walk(full, depth + 1);
|
|
70
|
+
} else if (entry.endsWith(ext)) {
|
|
71
|
+
results.push(full);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch { /* skip */ }
|
|
75
|
+
}
|
|
76
|
+
if (existsSync(dir)) walk(dir, 0);
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function detectFeaturesOnSrc() {
|
|
81
|
+
if (!isDir(FEATURES)) return [];
|
|
82
|
+
return listDir(FEATURES).filter((f) => isDir(join(FEATURES, f)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectLegacyFeatures() {
|
|
86
|
+
const legacy = [];
|
|
87
|
+
const ucBase = join(SRC, "application", "use-cases");
|
|
88
|
+
const ctrlDir = join(SRC, "adapters", "in", "http", "controllers");
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
for (const sub of listDir(ucBase)) {
|
|
91
|
+
if (isDir(join(ucBase, sub))) { legacy.push(sub); seen.add(sub); }
|
|
92
|
+
}
|
|
93
|
+
for (const file of listDir(ctrlDir)) {
|
|
94
|
+
const m = file.match(/^(.+)\.controller\.(ts|js)$/);
|
|
95
|
+
if (m && !seen.has(m[1])) { legacy.push(m[1]); seen.add(m[1]); }
|
|
96
|
+
}
|
|
97
|
+
return legacy;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseImports(content, filePath) {
|
|
101
|
+
const imports = [];
|
|
102
|
+
const lines = content.split("\n");
|
|
103
|
+
for (let i = 0; i < lines.length; i++) {
|
|
104
|
+
const m = lines[i].match(IMPORT_RE);
|
|
105
|
+
if (m) {
|
|
106
|
+
for (const full of m) {
|
|
107
|
+
const src = full.match(/from\s+['"]([^'"]+)['"]/);
|
|
108
|
+
if (src) imports.push({ file: filePath, source: src[1], line: i + 1 });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return imports;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasDecorator(content, decoratorRe) {
|
|
116
|
+
decoratorRe.lastIndex = 0;
|
|
117
|
+
return decoratorRe.test(content);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hasFile(dir, pattern) {
|
|
121
|
+
return listDir(dir).some((f) => f.match(pattern));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hasSubdir(dir, sub) {
|
|
125
|
+
return isDir(join(dir, sub));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function severity(label, sev) {
|
|
129
|
+
return { severity: sev, label };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Checks ── */
|
|
133
|
+
|
|
134
|
+
export function checkStructure(features) {
|
|
135
|
+
const checks = [];
|
|
136
|
+
let score = 0;
|
|
137
|
+
|
|
138
|
+
if (isDir(FEATURES)) {
|
|
139
|
+
checks.push({ ...severity("src/features/ existe", SEVERITY.INFO), pass: true });
|
|
140
|
+
score += 2;
|
|
141
|
+
} else {
|
|
142
|
+
checks.push({ ...severity("src/features/ no existe", SEVERITY.ERROR), pass: false, fix: "Crear src/features/ directorio" });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const feat of features) {
|
|
146
|
+
const fDir = join(FEATURES, feat);
|
|
147
|
+
let featScore = 0;
|
|
148
|
+
|
|
149
|
+
if (hasSubdir(fDir, "domain")) {
|
|
150
|
+
const dDir = join(fDir, "domain");
|
|
151
|
+
if (hasFile(dDir, /\.entity\.ts$/)) {
|
|
152
|
+
checks.push({ ...severity(`${feat}: domain/<Name>.entity.ts`, SEVERITY.INFO), pass: true });
|
|
153
|
+
featScore += 3;
|
|
154
|
+
} else {
|
|
155
|
+
checks.push({ ...severity(`${feat}: falta entity en domain/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}.entity.ts en ${feat}/domain/` });
|
|
156
|
+
}
|
|
157
|
+
if (hasFile(dDir, /^I[A-Z]/)) {
|
|
158
|
+
checks.push({ ...severity(`${feat}: domain/I<Name>Repository.ts`, SEVERITY.INFO), pass: true });
|
|
159
|
+
featScore += 2;
|
|
160
|
+
} else {
|
|
161
|
+
checks.push({ ...severity(`${feat}: falta repository interface en domain/`, SEVERITY.WARNING), pass: false, fix: `Crear I${feat.charAt(0).toUpperCase() + feat.slice(1)}Repository.ts` });
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
checks.push({ ...severity(`${feat}: falta domain/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}/domain/` });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (hasSubdir(fDir, "application/use-cases")) {
|
|
168
|
+
if (hasFile(join(fDir, "application/use-cases"), /\.ts$/)) {
|
|
169
|
+
checks.push({ ...severity(`${feat}: application/use-cases/`, SEVERITY.INFO), pass: true });
|
|
170
|
+
featScore += 3;
|
|
171
|
+
} else {
|
|
172
|
+
checks.push({ ...severity(`${feat}: application/use-cases/ vacío`, SEVERITY.WARNING), pass: false, fix: `Migrar casos de uso a ${feat}/application/use-cases/` });
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
checks.push({ ...severity(`${feat}: falta application/use-cases/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}/application/use-cases/` });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (hasSubdir(fDir, "application/mappers")) {
|
|
179
|
+
if (hasFile(join(fDir, "application/mappers"), /\.mapper\.ts$/)) {
|
|
180
|
+
checks.push({ ...severity(`${feat}: application/mappers/`, SEVERITY.INFO), pass: true });
|
|
181
|
+
featScore += 2;
|
|
182
|
+
} else {
|
|
183
|
+
checks.push({ ...severity(`${feat}: application/mappers/ sin mappers`, SEVERITY.SUGGESTION), pass: false, fix: `Migrar mapper a ${feat}/application/mappers/` });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (hasSubdir(fDir, "adapters/in/http")) {
|
|
188
|
+
const httpDir = join(fDir, "adapters/in/http");
|
|
189
|
+
if (hasFile(httpDir, /Controller\.ts$/)) {
|
|
190
|
+
checks.push({ ...severity(`${feat}: adapters/in/http/<Name>Controller.ts`, SEVERITY.INFO), pass: true });
|
|
191
|
+
featScore += 3;
|
|
192
|
+
} else {
|
|
193
|
+
checks.push({ ...severity(`${feat}: falta controller en adapters/in/http/`, SEVERITY.ERROR), pass: false, fix: `Crear controller en ${feat}/adapters/in/http/` });
|
|
194
|
+
}
|
|
195
|
+
if (hasFile(httpDir, /\.routes\.ts$/)) {
|
|
196
|
+
checks.push({ ...severity(`${feat}: adapters/in/http/<name>.routes.ts`, SEVERITY.INFO), pass: true });
|
|
197
|
+
featScore += 2;
|
|
198
|
+
} else {
|
|
199
|
+
checks.push({ ...severity(`${feat}: falta routes en adapters/in/http/`, SEVERITY.WARNING), pass: false, fix: `Crear routes en ${feat}/adapters/in/http/` });
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
checks.push({ ...severity(`${feat}: falta adapters/in/http/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}/adapters/in/http/` });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (hasSubdir(fDir, "adapters/out/persistence")) {
|
|
206
|
+
const pDir = join(fDir, "adapters/out/persistence");
|
|
207
|
+
if (hasFile(pDir, /Repository\.ts$/)) {
|
|
208
|
+
checks.push({ ...severity(`${feat}: adapters/out/persistence/<Name>Repository.ts`, SEVERITY.INFO), pass: true });
|
|
209
|
+
featScore += 3;
|
|
210
|
+
} else {
|
|
211
|
+
checks.push({ ...severity(`${feat}: falta repository impl en persistence/`, SEVERITY.ERROR), pass: false, fix: `Migrar repository a ${feat}/adapters/out/persistence/` });
|
|
212
|
+
}
|
|
213
|
+
if (hasFile(pDir, /Schema\.ts$/)) {
|
|
214
|
+
checks.push({ ...severity(`${feat}: adapters/out/persistence/<Name>Schema.ts`, SEVERITY.INFO), pass: true });
|
|
215
|
+
featScore += 2;
|
|
216
|
+
} else {
|
|
217
|
+
checks.push({ ...severity(`${feat}: falta schema en persistence/`, SEVERITY.WARNING), pass: false, fix: `Migrar schema a ${feat}/adapters/out/persistence/` });
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
checks.push({ ...severity(`${feat}: falta adapters/out/persistence/`, SEVERITY.ERROR), pass: false, fix: `Crear ${feat}/adapters/out/persistence/` });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
score += Math.min(featScore, 20);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (features.length === 0) {
|
|
227
|
+
const legacy = detectLegacyFeatures();
|
|
228
|
+
if (legacy.length > 0) {
|
|
229
|
+
checks.push({ ...severity("Ningún feature migrado a src/features/", SEVERITY.ERROR), pass: false, fix: `Migrar: ${legacy.join(", ")}` });
|
|
230
|
+
} else {
|
|
231
|
+
checks.push({ ...severity("No se encontraron features", SEVERITY.INFO), pass: false, fix: "Crear primer feature en src/features/" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { score: Math.min(score, 30), checks };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function checkLayers(features) {
|
|
239
|
+
const checks = [];
|
|
240
|
+
let score = 0;
|
|
241
|
+
|
|
242
|
+
if (features.length === 0) {
|
|
243
|
+
checks.push({ ...severity("Sin features para analizar capas", SEVERITY.WARNING), pass: false, fix: "Migrar al menos un feature primero" });
|
|
244
|
+
return { score: 0, checks };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const feat of features) {
|
|
248
|
+
const fDir = join(FEATURES, feat);
|
|
249
|
+
|
|
250
|
+
/* Domain layer */
|
|
251
|
+
const domainFiles = findFiles(join(fDir, "domain"), ".ts", 3);
|
|
252
|
+
let domainOk = true;
|
|
253
|
+
for (const f of domainFiles) {
|
|
254
|
+
const content = read(f);
|
|
255
|
+
if (!content) continue;
|
|
256
|
+
const imports = parseImports(content, f);
|
|
257
|
+
for (const imp of imports) {
|
|
258
|
+
if (imp.source.startsWith("@/adapters") || imp.source.startsWith("../adapters") || imp.source.includes("adapters")) {
|
|
259
|
+
checks.push({
|
|
260
|
+
...severity(`${feat}: domain importa de adapters`, SEVERITY.CRITICAL),
|
|
261
|
+
pass: false,
|
|
262
|
+
detail: `${relative(ROOT, f)}:${imp.line}`,
|
|
263
|
+
fix: "Eliminar import de adapters en archivos de dominio",
|
|
264
|
+
});
|
|
265
|
+
domainOk = false;
|
|
266
|
+
score -= 3;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (domainOk && domainFiles.length > 0) {
|
|
271
|
+
checks.push({ ...severity(`${feat}: domain/ sin imports prohibidos`, SEVERITY.INFO), pass: true });
|
|
272
|
+
score += 3;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Application layer */
|
|
276
|
+
const appFiles = findFiles(join(fDir, "application"), ".ts", 5);
|
|
277
|
+
let appOk = true;
|
|
278
|
+
for (const f of appFiles) {
|
|
279
|
+
const content = read(f);
|
|
280
|
+
if (!content) continue;
|
|
281
|
+
const imports = parseImports(content, f);
|
|
282
|
+
for (const imp of imports) {
|
|
283
|
+
if (imp.source.startsWith("@/adapters") || imp.source.includes("/adapters/") || imp.source.startsWith("@/setting")) {
|
|
284
|
+
checks.push({
|
|
285
|
+
...severity(`${feat}: application importa de adapters/setting`, SEVERITY.ERROR),
|
|
286
|
+
pass: false,
|
|
287
|
+
detail: `${relative(ROOT, f)}:${imp.line}`,
|
|
288
|
+
fix: "Reemplazar import directo por inyección de interfaz en el constructor",
|
|
289
|
+
});
|
|
290
|
+
appOk = false;
|
|
291
|
+
score -= 3;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (content.includes("tsyringe") && content.includes("container.")) {
|
|
295
|
+
checks.push({
|
|
296
|
+
...severity(`${feat}: use case usa container.resolve()`, SEVERITY.ERROR),
|
|
297
|
+
pass: false,
|
|
298
|
+
detail: relative(ROOT, f),
|
|
299
|
+
fix: "Eliminar container.resolve() de casos de uso. Inyectar dependencias por constructor.",
|
|
300
|
+
});
|
|
301
|
+
appOk = false;
|
|
302
|
+
score -= 2;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (appOk && appFiles.length > 0) {
|
|
306
|
+
checks.push({ ...severity(`${feat}: application/ sin imports prohibidos`, SEVERITY.INFO), pass: true });
|
|
307
|
+
score += 3;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Controller business logic */
|
|
311
|
+
const ctrlFiles = findFiles(join(fDir, "adapters/in/http"), ".ts", 3);
|
|
312
|
+
let ctrlLogicOk = ctrlFiles.filter((f) => !f.endsWith(".routes.ts")).length > 0;
|
|
313
|
+
for (const f of ctrlFiles) {
|
|
314
|
+
if (f.endsWith(".routes.ts")) continue;
|
|
315
|
+
const content = read(f);
|
|
316
|
+
if (!content) continue;
|
|
317
|
+
const logicMatches = content.match(LOGIC_KEYWORDS_RE);
|
|
318
|
+
if (logicMatches && logicMatches.length > 3) {
|
|
319
|
+
checks.push({
|
|
320
|
+
...severity(`${feat}: controller con lógica de negocio`, SEVERITY.ERROR),
|
|
321
|
+
pass: false,
|
|
322
|
+
detail: `${basename(f)} (${logicMatches.length} keywords)`,
|
|
323
|
+
fix: "Extraer la lógica a un caso de uso e inyectarlo en el controller",
|
|
324
|
+
});
|
|
325
|
+
ctrlLogicOk = false;
|
|
326
|
+
score -= 3;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (ctrlLogicOk && ctrlFiles.length > 0) {
|
|
330
|
+
checks.push({ ...severity(`${feat}: controllers sin lógica de negocio`, SEVERITY.INFO), pass: true });
|
|
331
|
+
score += 2;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Cross-feature imports */
|
|
336
|
+
if (features.length > 1) {
|
|
337
|
+
let crossOk = true;
|
|
338
|
+
for (const f of findFiles(FEATURES, ".ts", 6)) {
|
|
339
|
+
const content = read(f);
|
|
340
|
+
if (!content) continue;
|
|
341
|
+
const imports = parseImports(content, f);
|
|
342
|
+
for (const imp of imports) {
|
|
343
|
+
for (const feat of features) {
|
|
344
|
+
const other = features.filter((x) => x !== feat);
|
|
345
|
+
for (const o of other) {
|
|
346
|
+
if (imp.source.includes(`features/${o}/`) || imp.source.includes(`${o}/domain/`) || imp.source.includes(`${o}/application/`)) {
|
|
347
|
+
checks.push({
|
|
348
|
+
...severity(`Import directo entre features`, SEVERITY.WARNING),
|
|
349
|
+
pass: false,
|
|
350
|
+
detail: `${relative(ROOT, f)} → ${imp.source}`,
|
|
351
|
+
fix: `Inyectar interfaz en lugar de import directo`,
|
|
352
|
+
});
|
|
353
|
+
crossOk = false;
|
|
354
|
+
score -= 2;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (crossOk) {
|
|
361
|
+
checks.push({ ...severity("Sin imports directos entre features", SEVERITY.INFO), pass: true });
|
|
362
|
+
score += 2;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Direct BD access */
|
|
367
|
+
if (features.length > 0) {
|
|
368
|
+
const allAppFiles = findFiles(FEATURES, ".ts", 6);
|
|
369
|
+
let directBdOk = true;
|
|
370
|
+
for (const f of allAppFiles) {
|
|
371
|
+
const rel = relative(ROOT, f);
|
|
372
|
+
if (rel.includes("out/persistence") || rel.includes("Schema")) continue;
|
|
373
|
+
const content = read(f);
|
|
374
|
+
if (!content) continue;
|
|
375
|
+
BD_MODEL_RE.lastIndex = 0;
|
|
376
|
+
const bdMatch = content.match(BD_MODEL_RE);
|
|
377
|
+
if (bdMatch) {
|
|
378
|
+
checks.push({
|
|
379
|
+
...severity(`Acceso directo a BD fuera de repository`, SEVERITY.ERROR),
|
|
380
|
+
pass: false,
|
|
381
|
+
detail: `${rel}: ${bdMatch.slice(0, 3).join(", ")}${bdMatch.length > 3 ? `... (+${bdMatch.length - 3})` : ""}`,
|
|
382
|
+
fix: "Mover la operación de BD al repository correspondiente e inyectarlo",
|
|
383
|
+
});
|
|
384
|
+
directBdOk = false;
|
|
385
|
+
score -= 3;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (directBdOk) {
|
|
389
|
+
checks.push({ ...severity("Sin acceso directo a BD fuera de repositories", SEVERITY.INFO), pass: true });
|
|
390
|
+
score += 2;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { score: Math.min(Math.max(score, 0), 25), checks };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function checkDecorators(features) {
|
|
398
|
+
const checks = [];
|
|
399
|
+
let score = 0;
|
|
400
|
+
|
|
401
|
+
if (features.length === 0) {
|
|
402
|
+
checks.push({ ...severity("Sin features para analizar decoradores", SEVERITY.WARNING), pass: false });
|
|
403
|
+
return { score: 0, checks };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const feat of features) {
|
|
407
|
+
const fDir = join(FEATURES, feat);
|
|
408
|
+
|
|
409
|
+
const ucFiles = findFiles(join(fDir, "application/use-cases"), ".ts", 3);
|
|
410
|
+
let ucOk = true;
|
|
411
|
+
for (const f of ucFiles) {
|
|
412
|
+
const content = read(f);
|
|
413
|
+
if (!content) continue;
|
|
414
|
+
if (!hasDecorator(content, INJECTABLE_RE) && (content.includes("class ") && content.includes("constructor("))) {
|
|
415
|
+
checks.push({ ...severity(`${feat}: falta @injectable() en ${basename(f)}`, SEVERITY.WARNING), pass: false, fix: `Agregar @injectable() a ${basename(f, ".ts")}` });
|
|
416
|
+
ucOk = false;
|
|
417
|
+
score -= 2;
|
|
418
|
+
}
|
|
419
|
+
if (content.includes("constructor(") && content.includes("@inject") === false && content.includes("private readonly")) {
|
|
420
|
+
checks.push({ ...severity(`${feat}: falta @inject() en constructor de ${basename(f)}`, SEVERITY.WARNING), pass: false, fix: "Reemplazar parámetros manuales por @inject(Token)" });
|
|
421
|
+
ucOk = false;
|
|
422
|
+
score -= 1;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (ucOk && ucFiles.length > 0) {
|
|
426
|
+
checks.push({ ...severity(`${feat}: use cases con @injectable()`, SEVERITY.INFO), pass: true });
|
|
427
|
+
score += 4;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const ctrlFiles = findFiles(join(fDir, "adapters/in/http"), ".ts", 3);
|
|
431
|
+
let ctrlOk = true;
|
|
432
|
+
for (const f of ctrlFiles) {
|
|
433
|
+
if (f.endsWith(".routes.ts")) continue;
|
|
434
|
+
const content = read(f);
|
|
435
|
+
if (!content) continue;
|
|
436
|
+
if (!hasDecorator(content, INJECTABLE_RE) && content.includes("class ")) {
|
|
437
|
+
checks.push({ ...severity(`${feat}: falta @injectable() en ${basename(f)}`, SEVERITY.WARNING), pass: false, fix: `Agregar @injectable() al controller ${basename(f, ".ts")}` });
|
|
438
|
+
ctrlOk = false;
|
|
439
|
+
score -= 3;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (ctrlOk && ctrlFiles.length > 0) {
|
|
443
|
+
checks.push({ ...severity(`${feat}: controllers con @injectable()`, SEVERITY.INFO), pass: true });
|
|
444
|
+
score += 4;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const repoFiles = findFiles(join(fDir, "adapters/out/persistence"), ".ts", 3);
|
|
448
|
+
let repoOk = true;
|
|
449
|
+
for (const f of repoFiles) {
|
|
450
|
+
if (f.endsWith("Schema.ts")) continue;
|
|
451
|
+
const content = read(f);
|
|
452
|
+
if (!content) continue;
|
|
453
|
+
if (!hasDecorator(content, INJECTABLE_RE) && content.includes("class ") && content.includes("implements ")) {
|
|
454
|
+
checks.push({ ...severity(`${feat}: falta @injectable() en ${basename(f)}`, SEVERITY.WARNING), pass: false, fix: `Agregar @injectable() al repository ${basename(f, ".ts")}` });
|
|
455
|
+
repoOk = false;
|
|
456
|
+
score -= 2;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (repoOk && repoFiles.length > 0) {
|
|
460
|
+
checks.push({ ...severity(`${feat}: repositories con @injectable()`, SEVERITY.INFO), pass: true });
|
|
461
|
+
score += 3;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const allFeatureFiles = findFiles(FEATURES, ".ts", 6);
|
|
466
|
+
let hasTsyringe = false;
|
|
467
|
+
for (const f of allFeatureFiles) {
|
|
468
|
+
const content = read(f);
|
|
469
|
+
if (content && content.includes('from "tsyringe"')) { hasTsyringe = true; break; }
|
|
470
|
+
}
|
|
471
|
+
if (hasTsyringe) {
|
|
472
|
+
checks.push({ ...severity("tsyringe importado en features", SEVERITY.INFO), pass: true });
|
|
473
|
+
score += 2;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let injectUsageOk = true;
|
|
477
|
+
for (const f of allFeatureFiles) {
|
|
478
|
+
const content = read(f);
|
|
479
|
+
if (!content) continue;
|
|
480
|
+
const injects = content.match(INJECT_RE);
|
|
481
|
+
if (injects) {
|
|
482
|
+
for (const inj of injects) {
|
|
483
|
+
if (inj.includes("'") || inj.includes('"')) {
|
|
484
|
+
checks.push({ ...severity(`@inject con string token`, SEVERITY.SUGGESTION), pass: false, detail: `${relative(ROOT, f)}: ${inj.trim()}`, fix: "Usar tokens de clase en lugar de strings" });
|
|
485
|
+
injectUsageOk = false;
|
|
486
|
+
score -= 1;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (injectUsageOk) {
|
|
492
|
+
checks.push({ ...severity("@inject() usa tokens de clase (no strings)", SEVERITY.INFO), pass: true });
|
|
493
|
+
score += 3;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return { score: Math.min(Math.max(score, 0), 20), checks };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function checkLegacy() {
|
|
500
|
+
const checks = [];
|
|
501
|
+
let score = 0;
|
|
502
|
+
|
|
503
|
+
for (const [dirPath, label] of LEGACY_DIRS) {
|
|
504
|
+
const fullPath = join(ROOT, dirPath);
|
|
505
|
+
if (!isDir(fullPath)) {
|
|
506
|
+
checks.push({ ...severity(`${label}: no existe (ok)`, SEVERITY.INFO), pass: true });
|
|
507
|
+
score += 1.5;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const files = listDir(fullPath).filter((f) => f.endsWith(".ts") && !f.endsWith(".d.ts"));
|
|
511
|
+
const subdirs = listDir(fullPath).filter((f) => isDir(join(fullPath, f)));
|
|
512
|
+
if (files.length === 0 && subdirs.length === 0) {
|
|
513
|
+
checks.push({ ...severity(`${label}: vacío (ok)`, SEVERITY.INFO), pass: true });
|
|
514
|
+
score += 2;
|
|
515
|
+
} else {
|
|
516
|
+
const items = [...files, ...subdirs.map((d) => `${d}/`)];
|
|
517
|
+
checks.push({
|
|
518
|
+
...severity(`${label}: ${items.length} archivo(s) legacy`, SEVERITY.WARNING),
|
|
519
|
+
pass: false,
|
|
520
|
+
detail: items.slice(0, 5).join(", ") + (items.length > 5 ? `... (+${items.length - 5})` : ""),
|
|
521
|
+
fix: `Migrar contenido a src/features/ y eliminar ${dirPath}`,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return { score: Math.round(score), checks };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function checkConfig() {
|
|
530
|
+
const checks = [];
|
|
531
|
+
let score = 0;
|
|
532
|
+
|
|
533
|
+
function stripJsonComments(raw) {
|
|
534
|
+
const lines = raw.split("\n");
|
|
535
|
+
const out = [];
|
|
536
|
+
for (let line of lines) {
|
|
537
|
+
const trimmed = line.trim();
|
|
538
|
+
if (trimmed.startsWith("/*") && trimmed.endsWith("*/")) continue;
|
|
539
|
+
const slashSlash = trimmed.indexOf("//");
|
|
540
|
+
if (slashSlash === 0) continue;
|
|
541
|
+
const ci = line.indexOf("/*");
|
|
542
|
+
if (ci > 0 && line.slice(ci).trim().endsWith("*/")) { out.push(line.slice(0, ci)); continue; }
|
|
543
|
+
out.push(line);
|
|
544
|
+
}
|
|
545
|
+
return out.join("\n").replace(/,\s*([}\]])/g, "$1");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function readJson(path) {
|
|
549
|
+
const raw = read(path);
|
|
550
|
+
if (!raw) return null;
|
|
551
|
+
try { return JSON.parse(stripJsonComments(raw)); } catch { return null; }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const tsconfig = readJson(join(ROOT, "tsconfig.json"));
|
|
555
|
+
if (tsconfig) {
|
|
556
|
+
const opts = tsconfig.compilerOptions || {};
|
|
557
|
+
if (opts.experimentalDecorators === true) {
|
|
558
|
+
checks.push({ ...severity("tsconfig: experimentalDecorators: true", SEVERITY.INFO), pass: true });
|
|
559
|
+
score += 3;
|
|
560
|
+
} else {
|
|
561
|
+
checks.push({ ...severity("tsconfig: falta experimentalDecorators: true", SEVERITY.ERROR), pass: false, fix: 'Agregar "experimentalDecorators": true en compilerOptions' });
|
|
562
|
+
}
|
|
563
|
+
if (opts.emitDecoratorMetadata === true) {
|
|
564
|
+
checks.push({ ...severity("tsconfig: emitDecoratorMetadata: true", SEVERITY.INFO), pass: true });
|
|
565
|
+
score += 2;
|
|
566
|
+
} else {
|
|
567
|
+
checks.push({ ...severity("tsconfig: falta emitDecoratorMetadata: true", SEVERITY.ERROR), pass: false, fix: 'Agregar "emitDecoratorMetadata": true en compilerOptions' });
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
checks.push({ ...severity("No se pudo leer tsconfig.json", SEVERITY.ERROR), pass: false });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const pkg = readJson(join(ROOT, "package.json"));
|
|
574
|
+
if (pkg) {
|
|
575
|
+
const deps = pkg.dependencies || {};
|
|
576
|
+
const devDeps = pkg.devDependencies || {};
|
|
577
|
+
if (deps.tsyringe) {
|
|
578
|
+
checks.push({ ...severity("tsyringe instalado", SEVERITY.INFO), pass: true });
|
|
579
|
+
score += 2;
|
|
580
|
+
} else {
|
|
581
|
+
checks.push({ ...severity("tsyringe no instalado", SEVERITY.WARNING), pass: false, fix: "pnpm add tsyringe" });
|
|
582
|
+
}
|
|
583
|
+
if (deps["reflect-metadata"]) {
|
|
584
|
+
checks.push({ ...severity("reflect-metadata instalado", SEVERITY.INFO), pass: true });
|
|
585
|
+
score += 1;
|
|
586
|
+
} else {
|
|
587
|
+
checks.push({ ...severity("reflect-metadata no instalado", SEVERITY.WARNING), pass: false, fix: "pnpm add reflect-metadata" });
|
|
588
|
+
}
|
|
589
|
+
if (devDeps["@types/tsyringe"]) {
|
|
590
|
+
checks.push({ ...severity("@types/tsyringe instalado", SEVERITY.INFO), pass: true });
|
|
591
|
+
score += 1;
|
|
592
|
+
} else {
|
|
593
|
+
checks.push({ ...severity("@types/tsyringe no instalado (dev)", SEVERITY.SUGGESTION), pass: false, fix: "pnpm add -D @types/tsyringe" });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const serverPath = join(SRC, "server.ts");
|
|
598
|
+
const serverContent = read(serverPath);
|
|
599
|
+
if (serverContent && serverContent.includes("reflect-metadata")) {
|
|
600
|
+
checks.push({ ...severity("reflect-metadata importado en server.ts", SEVERITY.INFO), pass: true });
|
|
601
|
+
score += 1;
|
|
602
|
+
} else {
|
|
603
|
+
const idxPath = join(SRC, "index.ts");
|
|
604
|
+
const idxContent = read(idxPath);
|
|
605
|
+
if (idxContent && idxContent.includes("reflect-metadata")) {
|
|
606
|
+
checks.push({ ...severity("reflect-metadata importado en index.ts", SEVERITY.INFO), pass: true });
|
|
607
|
+
score += 1;
|
|
608
|
+
} else {
|
|
609
|
+
checks.push({ ...severity("reflect-metadata no importado en entry point", SEVERITY.ERROR), pass: false, fix: 'Agregar import "reflect-metadata" al inicio de server.ts' });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return { score, checks };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function checkGraph(graph) {
|
|
617
|
+
const checks = [];
|
|
618
|
+
let score = 20; /* base */
|
|
619
|
+
|
|
620
|
+
if (!graph || graph.nodes.length === 0) {
|
|
621
|
+
checks.push({
|
|
622
|
+
...severity("Grafo arquitectónico vacío — no hay nodos detectados", SEVERITY.WARNING),
|
|
623
|
+
pass: false,
|
|
624
|
+
fix: "Crear src/features/ con al menos un feature",
|
|
625
|
+
});
|
|
626
|
+
return { score: 0, checks };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
checks.push({
|
|
630
|
+
...severity(`Grafo arquitectónico: ${graph.stats.totalNodes} nodos, ${graph.stats.totalEdges} edges`, SEVERITY.INFO),
|
|
631
|
+
pass: true,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
for (const v of graph.violations) {
|
|
635
|
+
const sev = v.severity === "CRITICAL" ? SEVERITY.CRITICAL
|
|
636
|
+
: v.severity === "ERROR" ? SEVERITY.ERROR
|
|
637
|
+
: SEVERITY.WARNING;
|
|
638
|
+
|
|
639
|
+
const penalty = v.severity === "CRITICAL" ? -5
|
|
640
|
+
: v.severity === "ERROR" ? -3
|
|
641
|
+
: -1;
|
|
642
|
+
|
|
643
|
+
checks.push({
|
|
644
|
+
severity: sev,
|
|
645
|
+
label: `[${v.rule}] ${v.description}`,
|
|
646
|
+
pass: false,
|
|
647
|
+
detail: `${v.from} → ${v.to}${v.file ? ` (${v.file})` : ""}`,
|
|
648
|
+
fix: v.rule === "R1" ? "Core no debe importar de features. Mover la lógica a shared."
|
|
649
|
+
: v.rule === "R2" ? "Domain no puede importar infraestructura. Extraer interfaz y usar adapter."
|
|
650
|
+
: v.rule === "R3" ? "Feature no accede infraestructura directamente. Crear adapter en el feature."
|
|
651
|
+
: v.rule === "R4" ? "Feature no importa otro feature directamente. Inyectar interfaz."
|
|
652
|
+
: v.rule === "R5" ? "Romper el ciclo de dependencias extrayendo interfaz común a shared/."
|
|
653
|
+
: "Revisar la dirección de la dependencia",
|
|
654
|
+
});
|
|
655
|
+
score += penalty;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (graph.stats.hasCycles) {
|
|
659
|
+
checks.push({
|
|
660
|
+
...severity("Ciclo de dependencias detectado entre features", SEVERITY.ERROR),
|
|
661
|
+
pass: false,
|
|
662
|
+
fix: "Extraer interfaz compartida a shared/ y romper el ciclo",
|
|
663
|
+
});
|
|
664
|
+
score -= 3;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (graph.stats.health === "healthy") {
|
|
668
|
+
checks.push({
|
|
669
|
+
...severity("Grafo arquitectónico saludable — sin violaciones", SEVERITY.INFO),
|
|
670
|
+
pass: true,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return { score: Math.max(score, 0), checks };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function checkOwnership(ctx) {
|
|
678
|
+
const checks = [];
|
|
679
|
+
let score = 0;
|
|
680
|
+
const report = ctx.ownership || buildOwnershipReport();
|
|
681
|
+
|
|
682
|
+
if (report.health === "healthy") {
|
|
683
|
+
checks.push({ ...severity("Ownership saludable — sin huérfanos ni duplicados", SEVERITY.INFO), pass: true });
|
|
684
|
+
score += 8;
|
|
685
|
+
} else if (report.health === "degraded") {
|
|
686
|
+
checks.push({ ...severity("Ownership degradado — hay componentes huérfanos o duplicados", SEVERITY.WARNING), pass: false, fix: "Revisar el reporte de ownership y mover componentes a su capa correcta" });
|
|
687
|
+
score += 4;
|
|
688
|
+
} else {
|
|
689
|
+
checks.push({ ...severity("Ownership crítico — múltiples problemas de organización", SEVERITY.ERROR), pass: false, fix: "Ejecutar node .opencode/skills/forge/scripts/armorer.mjs para diagnóstico completo" });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (report.orphans.length === 0) {
|
|
693
|
+
checks.push({ ...severity("Sin componentes huérfanos", SEVERITY.INFO), pass: true });
|
|
694
|
+
score += 3;
|
|
695
|
+
} else {
|
|
696
|
+
checks.push({ ...severity(`${report.orphans.length} componente(s) huérfano(s)`, SEVERITY.WARNING), pass: false, detail: report.orphans.map(o => o.path).join(", "), fix: "Mover cada componente a platform/, shared/, infra/ o features/" });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (report.duplicates.length === 0) {
|
|
700
|
+
checks.push({ ...severity("Sin componentes duplicados entre capas", SEVERITY.INFO), pass: true });
|
|
701
|
+
score += 3;
|
|
702
|
+
} else {
|
|
703
|
+
checks.push({ ...severity(`${report.duplicates.length} componente(s) duplicado(s)`, SEVERITY.WARNING), pass: false, detail: report.duplicates.map(d => `${d.name} en [${d.layers.join(", ")}]`).join("; "), fix: "Unificar el componente en una sola capa" });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (report.misplaced.length === 0) {
|
|
707
|
+
checks.push({ ...severity("Sin componentes mal ubicados", SEVERITY.INFO), pass: true });
|
|
708
|
+
score += 3;
|
|
709
|
+
} else {
|
|
710
|
+
checks.push({ ...severity(`${report.misplaced.length} componente(s) mal ubicado(s)`, SEVERITY.WARNING), pass: false, detail: report.misplaced.slice(0, 3).map(m => m.file).join(", "), fix: "Revisar sugerencias de reubicación en armorer" });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (report.hasPlatform) {
|
|
714
|
+
checks.push({ ...severity("Platform layer presente", SEVERITY.INFO), pass: true });
|
|
715
|
+
score += 3;
|
|
716
|
+
} else {
|
|
717
|
+
checks.push({ ...severity("Platform layer ausente", SEVERITY.SUGGESTION), pass: false, fix: "Considerar crear src/platform/ con los componentes técnicos globales" });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return { score: Math.min(score, 20), checks };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function checkPlatform(ctx) {
|
|
724
|
+
const checks = [];
|
|
725
|
+
let score = 0;
|
|
726
|
+
|
|
727
|
+
if (!ctx.platform || !ctx.platform.exists) {
|
|
728
|
+
checks.push({ ...severity("Platform layer no existe", SEVERITY.WARNING), pass: false, fix: "Crear src/platform/ con los componentes base del sistema" });
|
|
729
|
+
return { score: 0, checks };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
checks.push({ ...severity("Platform layer existe", SEVERITY.INFO), pass: true });
|
|
733
|
+
score += 2;
|
|
734
|
+
|
|
735
|
+
const expected = ["config", "database", "http", "server", "logger", "di"];
|
|
736
|
+
const found = new Set((ctx.platform.components || []).map(c => c.replace(/\.(ts|js)$/, "")));
|
|
737
|
+
|
|
738
|
+
for (const comp of expected) {
|
|
739
|
+
if (found.has(comp)) {
|
|
740
|
+
checks.push({ ...severity(`platform/${comp}/ existe`, SEVERITY.INFO), pass: true });
|
|
741
|
+
score += 2;
|
|
742
|
+
} else {
|
|
743
|
+
const isCritical = comp === "config" || comp === "server";
|
|
744
|
+
checks.push({
|
|
745
|
+
...severity(`platform/${comp}/ no existe`, isCritical ? SEVERITY.WARNING : SEVERITY.SUGGESTION),
|
|
746
|
+
pass: false,
|
|
747
|
+
fix: `Crear src/platform/${comp}/`,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return { score: Math.min(score, 15), checks };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function checkDependencies(ctx) {
|
|
756
|
+
const checks = [];
|
|
757
|
+
let score = 0;
|
|
758
|
+
const graph = ctx.graph || buildGraph();
|
|
759
|
+
|
|
760
|
+
if (!graph || graph.nodes.length === 0) {
|
|
761
|
+
checks.push({ ...severity("No hay nodos en el grafo para analizar dependencias", SEVERITY.WARNING), pass: false });
|
|
762
|
+
return { score: 0, checks };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const allowedCount = graph.edges.filter(e => e.type !== "violates").length;
|
|
766
|
+
const totalCount = graph.edges.length;
|
|
767
|
+
const health = totalCount > 0 ? Math.round((allowedCount / totalCount) * 100) : 100;
|
|
768
|
+
|
|
769
|
+
checks.push({ ...severity(`Dependency Health: ${health}% (${allowedCount}/${totalCount} edges válidos)`, SEVERITY.INFO), pass: true });
|
|
770
|
+
score += 4;
|
|
771
|
+
|
|
772
|
+
if (graph.stats.criticalViolations === 0) {
|
|
773
|
+
checks.push({ ...severity("Sin violaciones CRITICAL en dependencias", SEVERITY.INFO), pass: true });
|
|
774
|
+
score += 4;
|
|
775
|
+
} else {
|
|
776
|
+
checks.push({ ...severity(`${graph.stats.criticalViolations} violación(es) CRITICAL`, SEVERITY.CRITICAL), pass: false, fix: "Corregir violaciones de reglas R1, R2, R5, R6" });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (graph.stats.errorViolations === 0) {
|
|
780
|
+
checks.push({ ...severity("Sin violaciones ERROR en dependencias", SEVERITY.INFO), pass: true });
|
|
781
|
+
score += 4;
|
|
782
|
+
} else {
|
|
783
|
+
checks.push({ ...severity(`${graph.stats.errorViolations} violación(es) ERROR`, SEVERITY.ERROR), pass: false, fix: "Corregir violaciones de reglas R3, R4, R8, R9" });
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (graph.stats.riskScore === 0) {
|
|
787
|
+
checks.push({ ...severity("Risk Score 0 — sin riesgo estructural", SEVERITY.INFO), pass: true });
|
|
788
|
+
score += 3;
|
|
789
|
+
} else {
|
|
790
|
+
checks.push({ ...severity(`Risk Score: ${graph.stats.riskScore}/100`, graph.stats.riskScore > 30 ? SEVERITY.ERROR : SEVERITY.WARNING), pass: false, fix: "Reducir violaciones arquitectónicas para bajar el risk score" });
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return { score: Math.min(score, 15), checks };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function allChecks(features, graph, ctx) {
|
|
797
|
+
const g = graph || buildGraph(process.cwd());
|
|
798
|
+
const c = ctx || {};
|
|
799
|
+
return {
|
|
800
|
+
structure: checkStructure(features),
|
|
801
|
+
layers: checkLayers(features),
|
|
802
|
+
ownership: checkOwnership(c),
|
|
803
|
+
platform: checkPlatform(c),
|
|
804
|
+
dependencies: checkDependencies(c),
|
|
805
|
+
graph: checkGraph(g),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/* ── CLI ── */
|
|
810
|
+
async function main() {
|
|
811
|
+
const args = process.argv.slice(2);
|
|
812
|
+
const filterType = args.includes("--type") ? args[args.indexOf("--type") + 1] : null;
|
|
813
|
+
const filterSeverity = args.includes("--severity") ? args[args.indexOf("--severity") + 1] : null;
|
|
814
|
+
const format = args.includes("--json") ? "json" : "text";
|
|
815
|
+
|
|
816
|
+
const { buildContext } = await import("./context.mjs");
|
|
817
|
+
const ctx = await buildContext();
|
|
818
|
+
const features = detectFeaturesOnSrc();
|
|
819
|
+
const graph = buildGraph(ROOT);
|
|
820
|
+
const results = allChecks(features, graph, ctx);
|
|
821
|
+
|
|
822
|
+
let all = [];
|
|
823
|
+
for (const [, cat] of Object.entries(results)) {
|
|
824
|
+
all = all.concat(cat.checks);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (filterSeverity) all = all.filter((c) => c.severity === filterSeverity);
|
|
828
|
+
|
|
829
|
+
if (format === "json") {
|
|
830
|
+
console.log(JSON.stringify({ checks: all, total: all.length }, null, 2));
|
|
831
|
+
} else {
|
|
832
|
+
for (const c of all) {
|
|
833
|
+
const icon = c.pass ? "✔" : "✘";
|
|
834
|
+
console.log(`[${c.severity}] ${icon} ${c.label}${c.detail ? ` — ${c.detail}` : ""}`);
|
|
835
|
+
if (!c.pass && c.fix) console.log(` → Fix: ${c.fix}`);
|
|
836
|
+
}
|
|
837
|
+
console.log(`\nTotal: ${all.length} checks`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (process.argv[1] && (process.argv[1].endsWith("detect.mjs") || process.argv[1].endsWith("detect.js"))) {
|
|
842
|
+
main().catch(console.error);
|
|
843
|
+
}
|