@ronaldjdevfs/forge 1.0.2 → 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/README.md +130 -8
- package/package.json +2 -2
- package/skills/forge/SKILL.md +86 -15
- package/skills/forge/profiles/express-drizzle.md +107 -0
- package/skills/forge/profiles/fastify-mongodb.md +103 -0
- package/skills/forge/profiles/fastify-prisma.md +81 -0
- package/skills/forge/profiles/nestjs-mongodb.md +92 -0
- package/skills/forge/profiles/nestjs-postgres.md +98 -0
- package/skills/forge/reference/api-design.md +62 -0
- package/skills/forge/reference/assay.md +82 -0
- package/skills/forge/reference/cast.md +81 -7
- package/skills/forge/reference/data-patterns.md +86 -0
- package/skills/forge/reference/di-strategies.md +50 -0
- package/skills/forge/reference/errors.md +65 -0
- package/skills/forge/reference/events.md +95 -0
- package/skills/forge/reference/help.md +40 -0
- package/skills/forge/reference/hooks.md +62 -0
- package/skills/forge/reference/observability.md +66 -0
- package/skills/forge/reference/patterns.md +52 -0
- package/skills/forge/reference/principles.md +6 -0
- package/skills/forge/reference/reforge.md +69 -5
- package/skills/forge/reference/relocate.md +15 -2
- package/skills/forge/reference/security-patterns.md +87 -0
- package/skills/forge/reference/testing-patterns.md +69 -0
- package/skills/forge/scripts/assay.mjs +481 -0
- package/skills/forge/scripts/context.mjs +147 -43
- package/skills/forge/scripts/detect.mjs +371 -22
- package/skills/forge/scripts/forge-api.mjs +373 -0
- package/skills/forge/scripts/forge-config.mjs +268 -0
- package/skills/forge/scripts/forge-signals.mjs +131 -0
- package/skills/forge/scripts/forge-state.mjs +97 -0
- package/skills/forge/scripts/formatter.mjs +133 -0
- package/skills/forge/scripts/graph.mjs +5 -21
- package/skills/forge/scripts/hook.mjs +250 -0
- package/skills/forge/scripts/inspect.mjs +171 -22
- package/skills/forge/scripts/parse-imports.mjs +249 -0
- package/skills/forge/scripts/pin.mjs +151 -0
- package/skills/forge/scripts/posttool.mjs +224 -0
- package/skills/forge/scripts/profile.mjs +124 -20
- package/skills/forge/scripts/registry/rules.mjs +344 -0
- package/skills/forge/scripts/rename.mjs +669 -0
- package/skills/forge/scripts/rollback.mjs +213 -0
- package/skills/forge/scripts/update.mjs +114 -0
- package/skills/forge/templates/feature/domain-error.ts.md +9 -0
- package/skills/forge/templates/feature/domain-event.ts.md +9 -0
- package/skills/forge/templates/feature/event-handler.ts.md +10 -0
- package/skills/forge/templates/feature/use-case.ts.md +10 -2
- package/skills/forge/tests/core.test.mjs +403 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* rules.mjs — Anti-pattern rule registry (R1-R9 + custom).
|
|
5
|
+
*
|
|
6
|
+
* Centraliza todas las reglas arquitectónicas. Cada regla tiene:
|
|
7
|
+
* id, name, severity, category, description, check(deps), fix
|
|
8
|
+
*
|
|
9
|
+
* Uso:
|
|
10
|
+
* import { RULES, loadCustomRules, evaluateRules } from "./registry/rules.mjs";
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
|
|
16
|
+
const ROOT = process.cwd();
|
|
17
|
+
|
|
18
|
+
export const SEVERITY = {
|
|
19
|
+
CRITICAL: "CRITICAL",
|
|
20
|
+
ERROR: "ERROR",
|
|
21
|
+
WARNING: "WARNING",
|
|
22
|
+
INFO: "INFO",
|
|
23
|
+
SUGGESTION: "SUGGESTION",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const CATEGORY = {
|
|
27
|
+
DEPENDENCY: "dependency",
|
|
28
|
+
STRUCTURE: "structure",
|
|
29
|
+
NAMING: "naming",
|
|
30
|
+
DECORATOR: "decorator",
|
|
31
|
+
OWNERSHIP: "ownership",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Core rule definition template.
|
|
36
|
+
* Each rule has a `check` function receiving (graph, ctx) and returning violations[].
|
|
37
|
+
*/
|
|
38
|
+
export function defineRule({ id, name, severity, category, description, check, fix, example }) {
|
|
39
|
+
return { id, name, severity, category, description, check, fix, example };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* R1-R9 built-in rules.
|
|
44
|
+
* check(graph, ctx) → Array<{ rule, severity, from, to, file, line?, description }>
|
|
45
|
+
*/
|
|
46
|
+
export const RULES = [
|
|
47
|
+
defineRule({
|
|
48
|
+
id: "R1",
|
|
49
|
+
name: "Feature no importa infraestructura",
|
|
50
|
+
severity: SEVERITY.CRITICAL,
|
|
51
|
+
category: CATEGORY.DEPENDENCY,
|
|
52
|
+
description: "Un feature no debe importar directamente de infra/",
|
|
53
|
+
fix: "Crear un adapter en el feature y delegar en la implementación de infra vía interfaz inyectada.",
|
|
54
|
+
example: "✘ features/auth/adapters/out/persistence/PrismaAuthRepo.ts importa de infra/prisma/",
|
|
55
|
+
check: (graph) => {
|
|
56
|
+
const violations = [];
|
|
57
|
+
for (const edge of graph.edges || []) {
|
|
58
|
+
if (edge.fromLayer === "feature" && edge.toLayer === "infra") {
|
|
59
|
+
violations.push({
|
|
60
|
+
rule: "R1", severity: SEVERITY.CRITICAL,
|
|
61
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
62
|
+
description: `Feature "${edge.from}" importa infraestructura "${edge.to}"`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return violations;
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
defineRule({
|
|
71
|
+
id: "R2",
|
|
72
|
+
name: "Platform no importa features",
|
|
73
|
+
severity: SEVERITY.CRITICAL,
|
|
74
|
+
category: CATEGORY.DEPENDENCY,
|
|
75
|
+
description: "Platform no puede depender de features de negocio.",
|
|
76
|
+
fix: "Extraer la lógica necesaria a shared/ o inyectar la dependencia.",
|
|
77
|
+
example: "✘ platform/logger.ts importa de features/users/",
|
|
78
|
+
check: (graph) => {
|
|
79
|
+
const violations = [];
|
|
80
|
+
for (const edge of graph.edges || []) {
|
|
81
|
+
if (edge.fromLayer === "platform" && edge.toLayer === "feature") {
|
|
82
|
+
violations.push({
|
|
83
|
+
rule: "R2", severity: SEVERITY.CRITICAL,
|
|
84
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
85
|
+
description: `Platform "${edge.from}" importa feature "${edge.to}"`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return violations;
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
defineRule({
|
|
94
|
+
id: "R3",
|
|
95
|
+
name: "Shared no importa features",
|
|
96
|
+
severity: SEVERITY.CRITICAL,
|
|
97
|
+
category: CATEGORY.DEPENDENCY,
|
|
98
|
+
description: "Shared es código puro sin dependencias de negocio.",
|
|
99
|
+
fix: "Mover la lógica al feature que la necesita en lugar de a shared/.",
|
|
100
|
+
example: "✘ shared/utils/helper.ts importa de features/auth/",
|
|
101
|
+
check: (graph) => {
|
|
102
|
+
const violations = [];
|
|
103
|
+
for (const edge of graph.edges || []) {
|
|
104
|
+
if (edge.fromLayer === "shared" && edge.toLayer === "feature") {
|
|
105
|
+
violations.push({
|
|
106
|
+
rule: "R3", severity: SEVERITY.CRITICAL,
|
|
107
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
108
|
+
description: `Shared "${edge.from}" importa feature "${edge.to}"`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return violations;
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
defineRule({
|
|
117
|
+
id: "R4",
|
|
118
|
+
name: "Shared no importa infraestructura",
|
|
119
|
+
severity: SEVERITY.CRITICAL,
|
|
120
|
+
category: CATEGORY.DEPENDENCY,
|
|
121
|
+
description: "Shared no debe acoplarse a implementaciones concretas.",
|
|
122
|
+
fix: "Mover la implementación a infra/ y la interfaz a shared/contracts/.",
|
|
123
|
+
example: "✘ shared/errors/AppError.ts importa de infra/prisma/",
|
|
124
|
+
check: (graph) => {
|
|
125
|
+
const violations = [];
|
|
126
|
+
for (const edge of graph.edges || []) {
|
|
127
|
+
if (edge.fromLayer === "shared" && edge.toLayer === "infra") {
|
|
128
|
+
violations.push({
|
|
129
|
+
rule: "R4", severity: SEVERITY.CRITICAL,
|
|
130
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
131
|
+
description: `Shared "${edge.from}" importa infra "${edge.to}"`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return violations;
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
defineRule({
|
|
140
|
+
id: "R5",
|
|
141
|
+
name: "Domain no importa infraestructura",
|
|
142
|
+
severity: SEVERITY.CRITICAL,
|
|
143
|
+
category: CATEGORY.DEPENDENCY,
|
|
144
|
+
description: "El dominio debe estar completamente aislado de la infraestructura.",
|
|
145
|
+
fix: "Invertir la dependencia: interfaz en domain, implementación en infra/adapter.",
|
|
146
|
+
example: "✘ features/users/domain/UserEntity.ts importa de infra/prisma/",
|
|
147
|
+
check: (graph) => {
|
|
148
|
+
const violations = [];
|
|
149
|
+
for (const edge of graph.edges || []) {
|
|
150
|
+
if (edge.fromLayer === "domain" && edge.toLayer === "infra") {
|
|
151
|
+
violations.push({
|
|
152
|
+
rule: "R5", severity: SEVERITY.CRITICAL,
|
|
153
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
154
|
+
description: `Domain "${edge.from}" importa infra "${edge.to}"`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return violations;
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
|
|
162
|
+
defineRule({
|
|
163
|
+
id: "R6",
|
|
164
|
+
name: "Domain no importa platform",
|
|
165
|
+
severity: SEVERITY.ERROR,
|
|
166
|
+
category: CATEGORY.DEPENDENCY,
|
|
167
|
+
description: "El dominio no debe acoplarse a la plataforma técnica.",
|
|
168
|
+
fix: "Inyectar la dependencia de platform como interfaz en lugar de import directo.",
|
|
169
|
+
example: "✘ features/users/domain/UserEntity.ts importa de platform/config/",
|
|
170
|
+
check: (graph) => {
|
|
171
|
+
const violations = [];
|
|
172
|
+
for (const edge of graph.edges || []) {
|
|
173
|
+
if (edge.fromLayer === "domain" && edge.toLayer === "platform") {
|
|
174
|
+
violations.push({
|
|
175
|
+
rule: "R6", severity: SEVERITY.ERROR,
|
|
176
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
177
|
+
description: `Domain "${edge.from}" importa platform "${edge.to}"`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return violations;
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
|
|
185
|
+
defineRule({
|
|
186
|
+
id: "R7",
|
|
187
|
+
name: "Infra no importa features",
|
|
188
|
+
severity: SEVERITY.ERROR,
|
|
189
|
+
category: CATEGORY.DEPENDENCY,
|
|
190
|
+
description: "Infraestructura no debe conocer features de negocio.",
|
|
191
|
+
fix: "Si infra necesita datos del feature, crear un adapter o evento.",
|
|
192
|
+
example: "✘ infra/prisma/client.ts importa de features/users/",
|
|
193
|
+
check: (graph) => {
|
|
194
|
+
const violations = [];
|
|
195
|
+
for (const edge of graph.edges || []) {
|
|
196
|
+
if (edge.fromLayer === "infra" && edge.toLayer === "feature") {
|
|
197
|
+
violations.push({
|
|
198
|
+
rule: "R7", severity: SEVERITY.ERROR,
|
|
199
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
200
|
+
description: `Infra "${edge.from}" importa feature "${edge.to}"`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return violations;
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
|
|
208
|
+
defineRule({
|
|
209
|
+
id: "R8",
|
|
210
|
+
name: "Sin imports directos entre features",
|
|
211
|
+
severity: SEVERITY.ERROR,
|
|
212
|
+
category: CATEGORY.DEPENDENCY,
|
|
213
|
+
description: "Un feature no debe importar directamente otro feature.",
|
|
214
|
+
fix: "Extraer interfaz compartida a shared/contracts/ o inyectar vía DI.",
|
|
215
|
+
example: "✘ features/products/ usa clases de features/users/ directamente",
|
|
216
|
+
check: (graph) => {
|
|
217
|
+
const violations = [];
|
|
218
|
+
for (const edge of graph.edges || []) {
|
|
219
|
+
if (edge.fromLayer === "feature" && edge.toLayer === "feature") {
|
|
220
|
+
violations.push({
|
|
221
|
+
rule: "R8", severity: SEVERITY.ERROR,
|
|
222
|
+
from: edge.from, to: edge.to, file: edge.file,
|
|
223
|
+
description: `Feature "${edge.from}" importa otro feature "${edge.to}" directamente`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return violations;
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
|
|
231
|
+
defineRule({
|
|
232
|
+
id: "R9",
|
|
233
|
+
name: "Sin ciclos de dependencias",
|
|
234
|
+
severity: SEVERITY.ERROR,
|
|
235
|
+
category: CATEGORY.DEPENDENCY,
|
|
236
|
+
description: "No deben existir ciclos en el grafo de dependencias.",
|
|
237
|
+
fix: "Extraer interfaz común a shared/ y romper la dependencia circular.",
|
|
238
|
+
example: "features/users → features/products → features/users",
|
|
239
|
+
check: (graph) => {
|
|
240
|
+
const violations = [];
|
|
241
|
+
if (graph?.stats?.hasCycles) {
|
|
242
|
+
violations.push({
|
|
243
|
+
rule: "R9", severity: SEVERITY.ERROR,
|
|
244
|
+
from: "(multiple)", to: "(cycle)",
|
|
245
|
+
description: "Ciclo de dependencias detectado en el grafo arquitectónico",
|
|
246
|
+
fix: "Extraer interfaz compartida a shared/ y romper el ciclo",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return violations;
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Map of rule id → rule for quick lookup.
|
|
256
|
+
*/
|
|
257
|
+
export const RULES_BY_ID = Object.fromEntries(RULES.map(r => [r.id, r]));
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Load custom rules from .forge/rules.json.
|
|
261
|
+
*/
|
|
262
|
+
export function loadCustomRules() {
|
|
263
|
+
const path = join(ROOT, ".forge", "rules.json");
|
|
264
|
+
try {
|
|
265
|
+
const raw = readFileSync(path, "utf-8");
|
|
266
|
+
return JSON.parse(raw);
|
|
267
|
+
} catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Evaluate all built-in + custom rules against a graph.
|
|
274
|
+
* Returns violations[] compatible with detect.mjs / inspect.mjs.
|
|
275
|
+
*/
|
|
276
|
+
export function evaluateRules(graph, ctx = {}) {
|
|
277
|
+
const violations = [];
|
|
278
|
+
|
|
279
|
+
for (const rule of RULES) {
|
|
280
|
+
try {
|
|
281
|
+
const hits = rule.check(graph, ctx);
|
|
282
|
+
for (const h of hits) {
|
|
283
|
+
violations.push({
|
|
284
|
+
...h,
|
|
285
|
+
rule: h.rule || rule.id,
|
|
286
|
+
severity: h.severity || rule.severity,
|
|
287
|
+
name: rule.name,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
// skip rules that error
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const customRules = loadCustomRules();
|
|
296
|
+
for (const rule of customRules) {
|
|
297
|
+
if (!rule.id || !rule.pattern) continue;
|
|
298
|
+
try {
|
|
299
|
+
const sev = rule.severity || SEVERITY.WARNING;
|
|
300
|
+
const re = new RegExp(rule.pattern);
|
|
301
|
+
const fileRe = rule.files ? new RegExp(rule.files) : null;
|
|
302
|
+
for (const node of graph.nodes || []) {
|
|
303
|
+
if (fileRe && !fileRe.test(node.id)) continue;
|
|
304
|
+
if (re.test(node.id)) {
|
|
305
|
+
violations.push({
|
|
306
|
+
rule: rule.id,
|
|
307
|
+
severity: sev,
|
|
308
|
+
from: node.id,
|
|
309
|
+
to: "",
|
|
310
|
+
description: rule.message || `Violación de regla custom: ${rule.id}`,
|
|
311
|
+
fix: rule.fix || null,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
// skip invalid custom rules
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return violations;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (process.argv[1]?.endsWith("rules.mjs")) {
|
|
324
|
+
const args = process.argv.slice(2);
|
|
325
|
+
if (args.includes("--list")) {
|
|
326
|
+
console.log("── Built-in Rules ──");
|
|
327
|
+
for (const r of RULES) {
|
|
328
|
+
console.log(` ${r.id} [${r.severity}] ${r.name}`);
|
|
329
|
+
console.log(` ${r.description}`);
|
|
330
|
+
console.log(` Fix: ${r.fix}`);
|
|
331
|
+
if (r.example) console.log(` Ej: ${r.example}`);
|
|
332
|
+
console.log();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (args.includes("--test")) {
|
|
336
|
+
const mockGraph = {
|
|
337
|
+
nodes: [{ id: "features/auth", layer: "feature" }, { id: "infra/prisma", layer: "infra" }],
|
|
338
|
+
edges: [{ from: "features/auth", fromLayer: "feature", to: "infra/prisma", toLayer: "infra", file: "src/features/auth/repo.ts" }],
|
|
339
|
+
stats: { hasCycles: false },
|
|
340
|
+
};
|
|
341
|
+
const violations = evaluateRules(mockGraph);
|
|
342
|
+
console.log(JSON.stringify(violations, null, 2));
|
|
343
|
+
}
|
|
344
|
+
}
|