@ronaldjdevfs/forge 1.3.0-beta → 1.3.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/README.md +1 -1
- package/package.json +1 -1
- package/skills/forge/SKILL.md +57 -128
- package/skills/forge/command/forge.md +59 -14
- package/skills/forge/reference/adr.md +242 -0
- package/skills/forge/reference/anti-corruption-layer.md +340 -0
- package/skills/forge/reference/api-design.md +7 -0
- package/skills/forge/reference/api-versioning.md +354 -0
- package/skills/forge/reference/architectural-depth-checklist.md +311 -0
- package/skills/forge/reference/architecture-template.md +41 -0
- package/skills/forge/reference/assay.md +6 -0
- package/skills/forge/reference/bounded-contexts.md +311 -0
- package/skills/forge/reference/chain.md +6 -0
- package/skills/forge/reference/cohesion-checklist.md +256 -0
- package/skills/forge/reference/cqrs.md +286 -0
- package/skills/forge/reference/data-patterns.md +6 -0
- package/skills/forge/reference/di-strategies.md +6 -0
- package/skills/forge/reference/errors.md +5 -0
- package/skills/forge/reference/events.md +8 -0
- package/skills/forge/reference/evolutionary-architecture.md +300 -0
- package/skills/forge/reference/forge.md +7 -0
- package/skills/forge/reference/hooks.md +6 -0
- package/skills/forge/reference/idempotency.md +283 -0
- package/skills/forge/reference/inscribe.md +5 -0
- package/skills/forge/reference/inspect.md +6 -0
- package/skills/forge/reference/modular-monolith.md +252 -0
- package/skills/forge/reference/observability.md +5 -0
- package/skills/forge/reference/quench.md +5 -0
- package/skills/forge/reference/relocate.md +6 -0
- package/skills/forge/reference/sagas.md +359 -0
- package/skills/forge/reference/security-patterns.md +6 -0
- package/skills/forge/reference/smelt.md +6 -0
- package/skills/forge/reference/temper.md +6 -0
- package/skills/forge/reference/testing-patterns.md +6 -0
- package/skills/forge/reference/transactional-outbox.md +311 -0
- package/skills/forge/scripts/architecture.mjs +10 -5
- package/skills/forge/scripts/assay.mjs +2 -2
- package/skills/forge/scripts/chain.mjs +31 -5
- package/skills/forge/scripts/context.mjs +24 -4
- package/skills/forge/scripts/detect.mjs +39 -30
- package/skills/forge/scripts/forge-boot.mjs +108 -0
- package/skills/forge/scripts/forge-config.mjs +182 -3
- package/skills/forge/scripts/forge-state.mjs +1 -1
- package/skills/forge/scripts/forgeSentinel-lib.mjs +2 -2
- package/skills/forge/scripts/forgeSentinel.mjs +2 -2
- package/skills/forge/scripts/forgeSmith.mjs +2 -2
- package/skills/forge/scripts/graph.mjs +65 -9
- package/skills/forge/scripts/hook.mjs +2 -2
- package/skills/forge/scripts/inspect.mjs +56 -48
- package/skills/forge/scripts/parse-imports.mjs +0 -2
- package/skills/forge/scripts/posttool.mjs +211 -17
- package/skills/forge/scripts/recommendation-engine.mjs +125 -0
- package/skills/forge/scripts/rollback.mjs +5 -3
- package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
- package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
- package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
- package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
- package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
- package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
- package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
- package/skills/forge/tests/core.test.mjs +288 -4
- package/src/cli.js +26 -13
|
@@ -7,6 +7,7 @@ import { detectProfile, detectProfileExtended } from "./profile.mjs";
|
|
|
7
7
|
import { buildDependencyGraph } from "./chain.mjs";
|
|
8
8
|
import { allChecks, checkStructure, checkLayers, checkDecorators } from "./detect.mjs";
|
|
9
9
|
import { saveHistory, updateStateFromAudit } from "./forge-config.mjs";
|
|
10
|
+
import { buildPipeline, printPipeline } from "./recommendation-engine.mjs";
|
|
10
11
|
|
|
11
12
|
const ROOT = process.cwd();
|
|
12
13
|
|
|
@@ -22,12 +23,15 @@ const CAT_NAMES = {
|
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const CAT_MAX = {
|
|
25
|
-
structure:
|
|
26
|
-
layers:
|
|
26
|
+
structure: 30,
|
|
27
|
+
layers: 25,
|
|
28
|
+
decorators: 20,
|
|
27
29
|
ownership: 20,
|
|
28
30
|
platform: 15,
|
|
29
31
|
dependencies: 15,
|
|
30
32
|
graph: 20,
|
|
33
|
+
customRules: 5,
|
|
34
|
+
naming: 10,
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
function countBySeverity(checks) {
|
|
@@ -64,8 +68,8 @@ function printReport(report, ctx, profile, graph, archGraph, profileExtended) {
|
|
|
64
68
|
const barLen = 40;
|
|
65
69
|
const pct = report.max > 0 ? Math.round((report.total / report.max) * 100) : 0;
|
|
66
70
|
|
|
67
|
-
function
|
|
68
|
-
const p = max > 0 ? Math.round((score / max) * barLen) : 0;
|
|
71
|
+
function renderScoreBar(score, max) {
|
|
72
|
+
const p = Math.min(max > 0 ? Math.round((score / max) * barLen) : 0, barLen);
|
|
69
73
|
const filled = "█".repeat(p);
|
|
70
74
|
const empty = "░".repeat(barLen - p);
|
|
71
75
|
const color = score >= max * 0.8 ? GREEN : score >= max * 0.5 ? YELLOW : RED;
|
|
@@ -128,7 +132,7 @@ function printReport(report, ctx, profile, graph, archGraph, profileExtended) {
|
|
|
128
132
|
const name = CAT_NAMES[key] || key;
|
|
129
133
|
const cmax = CAT_MAX[key] || cat.max || 10;
|
|
130
134
|
console.log(` ${BOLD}${name} (${cat.score}/${cmax})${RESET}`);
|
|
131
|
-
console.log(` ${
|
|
135
|
+
console.log(` ${renderScoreBar(cat.score, cmax)}`);
|
|
132
136
|
|
|
133
137
|
for (const check of cat.checks) {
|
|
134
138
|
const icon = check.pass ? `${GREEN}✔${RESET}` : `${RED}✘${RESET}`;
|
|
@@ -142,41 +146,16 @@ function printReport(report, ctx, profile, graph, archGraph, profileExtended) {
|
|
|
142
146
|
console.log();
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/* Contextual suggestions */
|
|
153
|
-
const suggestions = [];
|
|
154
|
-
const v = report.severityCounts;
|
|
155
|
-
if ((v.CRITICAL || 0) > 0) suggestions.push("forge quench — revisar reglas CRITICAL en detalle");
|
|
156
|
-
if ((v.ERROR || 0) > 0) suggestions.push("forge quench — corregir violaciones ERROR antes de avanzar");
|
|
157
|
-
if (graph.hasCycles) suggestions.push("forge reforge — eliminar ciclos del grafo de dependencias");
|
|
158
|
-
if (ctx.platform.exists && ctx.platform.components.length < 3) suggestions.push("forge forge — bootstrap de componentes platform faltantes");
|
|
159
|
-
if (!ctx.platform.exists) suggestions.push("forge forge — iniciar bootstrap de platform");
|
|
160
|
-
if (ctx.features.legacy.length > 0) suggestions.push(`forge relocate — migrar ${ctx.features.legacy.length} feature(s) legacy a estructura hexagonal`);
|
|
161
|
-
if (archGraph && archGraph.stats.dependencyHealth < 70) suggestions.push("forge temper — endurecer inyección de dependencias");
|
|
162
|
-
if (archGraph && archGraph.stats.riskScore > 50) suggestions.push("forge reforge — reducir riesgo arquitectónico");
|
|
163
|
-
if (pct >= 80) suggestions.push("forge inspect — mantener auditoría periódica");
|
|
164
|
-
if (pct < 50) suggestions.push("forge inspect — auditoría focalizada tras correcciones");
|
|
165
|
-
|
|
166
|
-
// Profile-derived suggestions
|
|
167
|
-
if (profileExtended) {
|
|
168
|
-
for (const dep of profileExtended.depIssues || []) {
|
|
169
|
-
suggestions.push(`profiler: ${dep}`);
|
|
170
|
-
}
|
|
171
|
-
for (const s of profileExtended.suggestions || []) {
|
|
172
|
-
suggestions.push(`profiler: ${s}`);
|
|
149
|
+
/* Unified pipeline recommendation */
|
|
150
|
+
const allViolations = [];
|
|
151
|
+
for (const [, cat] of Object.entries(report.categories)) {
|
|
152
|
+
for (const c of cat.checks) {
|
|
153
|
+
if (!c.pass) allViolations.push(c);
|
|
173
154
|
}
|
|
174
155
|
}
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
suggestions.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
|
|
179
|
-
console.log();
|
|
156
|
+
const pipeline = buildPipeline(allViolations, graph, ctx.ownership, profileExtended, ctx);
|
|
157
|
+
if (pipeline.length > 0) {
|
|
158
|
+
printPipeline(pipeline);
|
|
180
159
|
}
|
|
181
160
|
|
|
182
161
|
console.log("═".repeat(58) + "\n");
|
|
@@ -234,16 +213,38 @@ function getChangedFeatures(changedFiles, allFeatures) {
|
|
|
234
213
|
async function main() {
|
|
235
214
|
const args = process.argv.slice(2);
|
|
236
215
|
const isJson = args.includes("--json");
|
|
237
|
-
const isDiff = args.includes("--diff");
|
|
216
|
+
const isDiff = args.includes("--diff") || !args.includes("--full");
|
|
217
|
+
const isSummary = args.includes("--summary");
|
|
218
|
+
const force = args.includes("--force");
|
|
238
219
|
const filterSeverity = args.includes("--severity") ? args[args.indexOf("--severity") + 1] : null;
|
|
239
220
|
|
|
240
|
-
const ctx = await buildContext();
|
|
221
|
+
const ctx = await buildContext(ROOT, null, { force });
|
|
241
222
|
const profileExtended = detectProfileExtended(ctx);
|
|
242
223
|
const profile = profileExtended.profile;
|
|
243
|
-
const chainGraph = buildDependencyGraph();
|
|
244
224
|
const archGraph = ctx.graph;
|
|
225
|
+
const chainGraph = buildDependencyGraph(process.cwd(), archGraph);
|
|
245
226
|
const features = ctx.features.migrated;
|
|
246
227
|
|
|
228
|
+
if (isSummary) {
|
|
229
|
+
const result = allChecks(features, archGraph, ctx);
|
|
230
|
+
const report = buildReport({ categories: result });
|
|
231
|
+
const pct = report.max > 0 ? Math.round((report.total / report.max) * 100) : 0;
|
|
232
|
+
if (isJson) {
|
|
233
|
+
console.log(JSON.stringify({
|
|
234
|
+
score: report.total,
|
|
235
|
+
max: report.max,
|
|
236
|
+
pct,
|
|
237
|
+
grade: pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 65 ? "C" : pct >= 50 ? "D" : "F",
|
|
238
|
+
severityCounts: report.severityCounts,
|
|
239
|
+
violations: report.violations.length,
|
|
240
|
+
health: pct >= 80 ? "healthy" : pct >= 50 ? "fair" : "poor",
|
|
241
|
+
}, null, 2));
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`Score: ${report.total}/${report.max} (${pct}%) | Violations: ${report.violations.length} | Health: ${pct >= 80 ? "healthy" : pct >= 50 ? "fair" : "poor"}`);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
247
248
|
if (isDiff) {
|
|
248
249
|
const changedFiles = getChangedFiles();
|
|
249
250
|
const changedFeatures = getChangedFeatures(changedFiles, features);
|
|
@@ -282,8 +283,12 @@ async function main() {
|
|
|
282
283
|
};
|
|
283
284
|
|
|
284
285
|
// Provide a simpler report
|
|
285
|
-
let totalScore =
|
|
286
|
-
|
|
286
|
+
let totalScore = 0;
|
|
287
|
+
let maxScore = 0;
|
|
288
|
+
for (const [key, cat] of Object.entries(result)) {
|
|
289
|
+
totalScore += cat.score;
|
|
290
|
+
maxScore += CAT_MAX[key] || 20;
|
|
291
|
+
}
|
|
287
292
|
const pct = Math.round((totalScore / maxScore) * 100);
|
|
288
293
|
|
|
289
294
|
if (!isJson) {
|
|
@@ -291,7 +296,8 @@ async function main() {
|
|
|
291
296
|
|
|
292
297
|
for (const [key, cat] of Object.entries(result)) {
|
|
293
298
|
const name = key.charAt(0).toUpperCase() + key.slice(1);
|
|
294
|
-
|
|
299
|
+
const cmax = CAT_MAX[key] || cat.max || 20;
|
|
300
|
+
console.log(` ${BOLD}${name} (${cat.score}/${cat.score === 0 && cat.checks.length > 0 ? "—" : cmax})${RESET}`);
|
|
295
301
|
for (const check of cat.checks) {
|
|
296
302
|
const icon = check.pass ? `${GREEN}✔${RESET}` : `${RED}✘${RESET}`;
|
|
297
303
|
const sev = check.pass ? "" : ` ${SEVERITY_COLORS[check.severity]}[${check.severity}]${RESET}`;
|
|
@@ -309,24 +315,26 @@ async function main() {
|
|
|
309
315
|
categories: result,
|
|
310
316
|
}, null, 2));
|
|
311
317
|
}
|
|
312
|
-
updateStateFromAudit({ total: totalScore, grade: `${pct}%`, violations: [], health: "diff", context: { features: { total: features.length, migrated: features, legacy: [] } } });
|
|
318
|
+
updateStateFromAudit({ total: totalScore, max: maxScore, grade: `${pct}%`, violations: [], health: "diff", context: { features: { total: features.length, migrated: features, legacy: [] } } });
|
|
313
319
|
saveHistory({ score: totalScore, grade: `${pct}%`, violationCount: 0, totalFeatures: features.length, migratedFeatures: features.length });
|
|
314
320
|
process.exit(0);
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
const result = allChecks(features, archGraph, ctx);
|
|
318
324
|
const report = buildReport({ categories: result });
|
|
325
|
+
const pct = report.max > 0 ? Math.round((report.total / report.max) * 100) : 0;
|
|
319
326
|
|
|
320
327
|
updateStateFromAudit({
|
|
321
328
|
total: report.total,
|
|
322
|
-
|
|
329
|
+
max: report.max,
|
|
330
|
+
grade: pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 65 ? "C" : pct >= 50 ? "D" : "F",
|
|
323
331
|
violations: report.violations || [],
|
|
324
|
-
health:
|
|
332
|
+
health: pct >= 80 ? "healthy" : pct >= 50 ? "fair" : "poor",
|
|
325
333
|
context: { features: { total: features.length, migrated: features, legacy: ctx.features?.legacy || [] } },
|
|
326
334
|
});
|
|
327
335
|
saveHistory({
|
|
328
336
|
score: report.total,
|
|
329
|
-
grade:
|
|
337
|
+
grade: pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 65 ? "C" : pct >= 50 ? "D" : "F",
|
|
330
338
|
violationCount: (report.violations || []).length,
|
|
331
339
|
totalFeatures: features.length,
|
|
332
340
|
migratedFeatures: features.length,
|
|
@@ -214,8 +214,6 @@ function parseWithRegexLines(content, filePath) {
|
|
|
214
214
|
export function parseImportPaths(content, filePath) {
|
|
215
215
|
const astResult = filePath ? parseWithAST(content, filePath) : null;
|
|
216
216
|
if (astResult) return astResult;
|
|
217
|
-
const regexResult = parseWithAST(content); // second try without filePath hint
|
|
218
|
-
if (regexResult) return regexResult;
|
|
219
217
|
return parseWithRegex(content);
|
|
220
218
|
}
|
|
221
219
|
|
|
@@ -1,30 +1,224 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* posttool.mjs —
|
|
4
|
+
* posttool.mjs — PostToolUse hook para Forge.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Se ejecuta después de que el agente escribe código. Analiza los archivos
|
|
7
|
+
* modificados y reporta violaciones arquitectónicas.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Uso (CLI):
|
|
10
|
+
* node posttool.mjs → analiza archivos changed en git
|
|
11
|
+
* node posttool.mjs <file1> [file2...] → analiza archivos específicos
|
|
12
|
+
* node posttool.mjs --diff → solo archivos changed vs default branch
|
|
13
|
+
* node posttool.mjs --json → salida JSON
|
|
14
|
+
* node posttool.mjs --strict → exit code 1 si hay CRITICAL/ERROR
|
|
15
|
+
* node posttool.mjs --reminder → solo recordatorio (no bloquea)
|
|
16
|
+
*
|
|
17
|
+
* Uso (API):
|
|
18
|
+
* import { postToolCheck } from "./posttool.mjs";
|
|
19
|
+
* const result = await postToolCheck([...files]);
|
|
20
|
+
*
|
|
21
|
+
* Integración en SKILL.md (Fase 7 del pipeline):
|
|
22
|
+
* node .opencode/skills/forge/scripts/posttool.mjs --reminder
|
|
11
23
|
*/
|
|
12
24
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
25
|
+
import { readFileSync, existsSync, statSync, readdirSync } from "fs";
|
|
26
|
+
import { join, relative, resolve, extname } from "path";
|
|
27
|
+
import { execFileSync } from "child_process";
|
|
28
|
+
import { getGraph } from "./graph.mjs";
|
|
29
|
+
import { buildContext } from "./context.mjs";
|
|
30
|
+
import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
|
|
31
|
+
import { evaluateRules } from "./registry/rules.mjs";
|
|
32
|
+
import {
|
|
33
|
+
CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM, GRAY,
|
|
34
|
+
formatViolation, formatCheck, formatJson,
|
|
35
|
+
} from "./formatter.mjs";
|
|
15
36
|
|
|
16
|
-
const
|
|
37
|
+
const ROOT = process.cwd();
|
|
17
38
|
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
function getChangedFiles() {
|
|
40
|
+
let files = [];
|
|
41
|
+
try {
|
|
42
|
+
const defaultBranch = execFileSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] })
|
|
43
|
+
.trim().replace("refs/remotes/origin/", "");
|
|
44
|
+
const raw = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", `${defaultBranch}...HEAD`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
45
|
+
files = raw.trim().split("\n").filter(Boolean);
|
|
46
|
+
} catch {}
|
|
20
47
|
|
|
21
|
-
if (
|
|
22
|
-
|
|
48
|
+
if (files.length === 0) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = execFileSync("git", ["status", "--porcelain"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
51
|
+
files = raw.split("\n").filter(Boolean).map(l => l.slice(3));
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return files.filter(f => f.startsWith("src/"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readFile(path) {
|
|
59
|
+
try { return readFileSync(path, "utf-8"); } catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check modified files for violations.
|
|
64
|
+
* Returns { violations, graphViolations, total, hasCritical, hasErrors, summary }
|
|
65
|
+
*/
|
|
66
|
+
export async function postToolCheck(files, opts = {}) {
|
|
67
|
+
const { strict = false, reminder = false } = opts;
|
|
68
|
+
|
|
69
|
+
if (!files || files.length === 0) {
|
|
70
|
+
files = getChangedFiles();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only check source files
|
|
74
|
+
files = files.filter(f =>
|
|
75
|
+
f.endsWith(".ts") || f.endsWith(".mjs") || f.endsWith(".js") || f.endsWith(".tsx")
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (files.length === 0) {
|
|
79
|
+
return { violations: [], graphViolations: [], total: 0, hasCritical: false, hasErrors: false, filesChecked: 0, summary: "Sin archivos fuente modificados" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build context and run full audit
|
|
83
|
+
const ctx = await buildContext();
|
|
84
|
+
const features = detectFeaturesOnSrc();
|
|
85
|
+
const graph = ctx.graph || getGraph();
|
|
86
|
+
const results = allChecks(features, graph, ctx);
|
|
87
|
+
|
|
88
|
+
// Load inline ignores
|
|
89
|
+
const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
|
|
90
|
+
|
|
91
|
+
// Collect violations that touch the modified files
|
|
92
|
+
const violations = [];
|
|
93
|
+
for (const [, cat] of Object.entries(results)) {
|
|
94
|
+
for (const check of cat.checks) {
|
|
95
|
+
if (check.pass) continue;
|
|
96
|
+
|
|
97
|
+
// Only include violations touching modified files
|
|
98
|
+
const touchesFile = files.some(f =>
|
|
99
|
+
(check.detail && check.detail.includes(f)) ||
|
|
100
|
+
(check.label && check.label.includes(f))
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!touchesFile && !reminder) continue;
|
|
104
|
+
|
|
105
|
+
// Check inline ignores
|
|
106
|
+
if (isIgnored(check, allIgnores)) continue;
|
|
107
|
+
|
|
108
|
+
violations.push(check);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Also evaluate graph rules for changed files
|
|
113
|
+
const graphViolations = evaluateRules(graph, ctx).filter(v => {
|
|
114
|
+
const touchesFile = files.some(f =>
|
|
115
|
+
(v.file && v.file.includes(f)) ||
|
|
116
|
+
(v.from && v.from.includes(f)) ||
|
|
117
|
+
(v.to && v.to.includes(f))
|
|
118
|
+
);
|
|
119
|
+
return touchesFile || reminder;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const allViolations = [...violations, ...graphViolations];
|
|
123
|
+
const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
|
|
124
|
+
const hasErrors = allViolations.some(v => v.severity === "ERROR");
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
violations: allViolations,
|
|
128
|
+
total: allViolations.length,
|
|
129
|
+
hasCritical,
|
|
130
|
+
hasErrors,
|
|
131
|
+
filesChecked: files.length,
|
|
132
|
+
summary: hasCritical
|
|
133
|
+
? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "CRITICAL").length} CRITICAL, ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
|
|
134
|
+
: hasErrors
|
|
135
|
+
? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
|
|
136
|
+
: `${allViolations.length} violación(es) menores (WARNING/INFO)`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function printResult(result, opts = {}) {
|
|
141
|
+
const { reminder = false } = opts;
|
|
142
|
+
|
|
143
|
+
console.log(`\n${BOLD}${CYAN}═══ Forge PostTool Hook ═══${RESET}`);
|
|
144
|
+
|
|
145
|
+
if (result.total === 0) {
|
|
146
|
+
console.log(` ${GREEN}✔${RESET} Sin violaciones arquitectónicas en archivos modificados`);
|
|
147
|
+
console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}\n`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}`);
|
|
152
|
+
console.log(` ${DIM}Violaciones: ${result.total}${RESET}\n`);
|
|
153
|
+
|
|
154
|
+
if (reminder) {
|
|
155
|
+
console.log(` ${YELLOW}Recordatorio:${RESET} Los archivos modificados tienen ${result.total} violación(es) arquitectónica(s).`);
|
|
156
|
+
console.log(` Ejecuta ${CYAN}forge quench${RESET} para ver el detalle completo.\n`);
|
|
157
|
+
|
|
158
|
+
if (result.hasCritical) {
|
|
159
|
+
const criticalCount = result.violations.filter(v => v.severity === "CRITICAL").length;
|
|
160
|
+
console.log(` ${RED}⚠ ${criticalCount} violación(es) CRITICAL detectadas${RESET}`);
|
|
161
|
+
}
|
|
162
|
+
if (result.hasErrors) {
|
|
163
|
+
const errorCount = result.violations.filter(v => v.severity === "ERROR").length;
|
|
164
|
+
console.log(` ${RED}⚠ ${errorCount} violación(es) ERROR detectadas${RESET}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Print first 5 violations as summary
|
|
168
|
+
const toShow = result.violations.slice(0, 5);
|
|
169
|
+
for (const v of toShow) {
|
|
170
|
+
console.log(formatCheck(v));
|
|
171
|
+
}
|
|
172
|
+
if (result.violations.length > 5) {
|
|
173
|
+
console.log(` ${DIM}... y ${result.violations.length - 5} más${RESET}`);
|
|
174
|
+
}
|
|
175
|
+
console.log();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Full output
|
|
180
|
+
for (const v of result.violations) {
|
|
181
|
+
console.log(formatCheck(v));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`\n ${BOLD}Resumen:${RESET} ${result.summary}`);
|
|
185
|
+
if (result.hasCritical) {
|
|
186
|
+
console.log(` ${RED}⚠ Se requiere acción correctiva antes de continuar${RESET}`);
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function main() {
|
|
23
192
|
const args = process.argv.slice(2);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
193
|
+
const isJson = args.includes("--json");
|
|
194
|
+
const strict = args.includes("--strict");
|
|
195
|
+
const reminder = args.includes("--reminder");
|
|
196
|
+
const isDiff = args.includes("--diff");
|
|
197
|
+
|
|
198
|
+
let files;
|
|
199
|
+
if (isDiff) {
|
|
200
|
+
files = getChangedFiles();
|
|
201
|
+
} else {
|
|
202
|
+
// Files passed as arguments
|
|
203
|
+
files = args.filter(a => !a.startsWith("--"));
|
|
204
|
+
if (files.length === 0) {
|
|
205
|
+
files = getChangedFiles();
|
|
206
|
+
}
|
|
29
207
|
}
|
|
208
|
+
|
|
209
|
+
const result = await postToolCheck(files, { strict, reminder });
|
|
210
|
+
|
|
211
|
+
if (isJson) {
|
|
212
|
+
console.log(formatJson(result));
|
|
213
|
+
} else {
|
|
214
|
+
printResult(result, { reminder });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (strict && (result.hasCritical || result.hasErrors)) {
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (process.argv[1]?.endsWith("posttool.mjs")) {
|
|
223
|
+
main().catch(console.error);
|
|
30
224
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM, GRAY, SEVERITY_COLORS } from "./formatter.mjs";
|
|
4
|
+
|
|
5
|
+
function countByRule(violations) {
|
|
6
|
+
const counts = {};
|
|
7
|
+
for (const v of violations) {
|
|
8
|
+
const rule = v.rule || "other";
|
|
9
|
+
if (!counts[rule]) counts[rule] = [];
|
|
10
|
+
counts[rule].push(v);
|
|
11
|
+
}
|
|
12
|
+
return counts;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildPipeline(auditChecks, graph, ownership, profile, ctx) {
|
|
16
|
+
const violations = auditChecks.filter(c => !c.pass);
|
|
17
|
+
const pipeline = [];
|
|
18
|
+
|
|
19
|
+
// --- CRITICAL: R2 (platform→feature) → forge temper ---
|
|
20
|
+
const r2 = violations.filter(v => v.rule === "R2" || (v.fix && v.fix.includes("Extraer interfaz") && v.severity === "CRITICAL"));
|
|
21
|
+
if (r2.length > 0) {
|
|
22
|
+
pipeline.push({ command: "temper", args: "", priority: "CRITICAL", reason: `Corrige ${r2.length} violación(es) R2 (platform→feature)`, count: r2.length });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- ERROR: R3 (shared→feature) → forge reforge ---
|
|
26
|
+
const r3 = violations.filter(v => v.rule === "R3" || (v.fix && v.fix.includes("Feature no accede")));
|
|
27
|
+
// --- ERROR: R4 (shared→infra) → forge reforge ---
|
|
28
|
+
const r4 = violations.filter(v => v.rule === "R4" || (v.fix && v.fix.includes("Feature no importa otro feature")));
|
|
29
|
+
// --- ERROR: R8 (cross-feature) → forge reforge ---
|
|
30
|
+
const r8 = violations.filter(v => v.rule === "R8");
|
|
31
|
+
const sharedIssues = [...r3, ...r4, ...r8];
|
|
32
|
+
if (sharedIssues.length > 0) {
|
|
33
|
+
pipeline.push({ command: "reforge", args: "", priority: "ERROR", reason: `Corrige ${sharedIssues.length} violación(es) de capas (shared mal acoplado)`, count: sharedIssues.length });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- ERROR: R9 (cycles) → forge reforge ---
|
|
37
|
+
const r9 = violations.filter(v => v.rule === "R9" || (v.label && v.label.includes("Ciclo")));
|
|
38
|
+
if (r9.length > 0 || (graph && graph.hasCycles)) {
|
|
39
|
+
pipeline.push({ command: "reforge", args: "--cycles", priority: "ERROR", reason: `Elimina ${r9.length || 1} ciclo(s) de dependencia (R9)`, count: r9.length || 1 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- R1 (feature→infra) → forge reforge ---
|
|
43
|
+
const r1 = violations.filter(v => v.rule === "R1");
|
|
44
|
+
if (r1.length > 0) {
|
|
45
|
+
pipeline.push({ command: "reforge", args: "", priority: "ERROR", reason: `Corrige ${r1.length} violación(es) R1 (feature→infra)`, count: r1.length });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- WARNING: orphan/misplaced → forge relocate ---
|
|
49
|
+
const orphans = ownership?.orphans || [];
|
|
50
|
+
const misplaced = ownership?.misplaced || [];
|
|
51
|
+
const duplicates = ownership?.duplicates || [];
|
|
52
|
+
const relocationCount = orphans.length + misplaced.length + duplicates.length;
|
|
53
|
+
if (relocationCount > 0) {
|
|
54
|
+
pipeline.push({ command: "relocate", args: "", priority: "WARNING", reason: `Migra ${relocationCount} componente(s) huérfanos/duplicados/mal ubicados`, count: relocationCount });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- WARNING: naming violations → forge reforge ---
|
|
58
|
+
const namingCount = violations.filter(v => v.severity === "SUGGESTION" && v.label && v.label.includes("Naming")).length;
|
|
59
|
+
if (namingCount > 0) {
|
|
60
|
+
pipeline.push({ command: "reforge", args: "<filename>", priority: "WARNING", reason: `Corrige ${namingCount} violación(es) de naming (ejecutar por feature)`, count: namingCount });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Legacy features → forge relocate ---
|
|
64
|
+
const legacyCount = ctx?.features?.legacy?.length || 0;
|
|
65
|
+
if (legacyCount > 0) {
|
|
66
|
+
pipeline.push({ command: "relocate", args: "", priority: "WARNING", reason: `Migra ${legacyCount} feature(s) legacy a estructura hexagonal`, count: legacyCount });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Missing platform → forge forge ---
|
|
70
|
+
if (ctx && !ctx.platform?.exists) {
|
|
71
|
+
pipeline.push({ command: "forge", args: "", priority: "INFO", reason: "Inicializa bootstrap de platform layer", count: 1 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Package dependencies → (not a forge command, just suggestion) ---
|
|
75
|
+
const profileSugs = profile?.suggestions || [];
|
|
76
|
+
if (profileSugs.length > 0) {
|
|
77
|
+
pipeline.push({ command: "profile", args: "--install", priority: "SUGGESTION", reason: `${profileSugs.length} dependencia(s) sugeridas por perfil`, count: profileSugs.length });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Verify & finalize ---
|
|
81
|
+
if (pipeline.length > 0) {
|
|
82
|
+
pipeline.push({ command: "quench", args: "", priority: "INFO", reason: "Verifica que todas las correcciones pasen", count: 0 });
|
|
83
|
+
pipeline.push({ command: "inscribe", args: "", priority: "INFO", reason: "Actualiza ARCHITECTURE.md con el nuevo estado", count: 0 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sort by priority
|
|
87
|
+
const order = { CRITICAL: 0, ERROR: 1, WARNING: 2, INFO: 3, SUGGESTION: 4 };
|
|
88
|
+
pipeline.sort((a, b) => (order[a.priority] ?? 99) - (order[b.priority] ?? 99));
|
|
89
|
+
|
|
90
|
+
return pipeline;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function printPipeline(pipeline) {
|
|
94
|
+
if (!pipeline || pipeline.length === 0) return;
|
|
95
|
+
console.log(`\n${BOLD}${CYAN}═══ Pipeline recomendado ═══${RESET}`);
|
|
96
|
+
console.log(`${DIM}Para resolver los problemas encontrados, ejecuta en orden:${RESET}\n`);
|
|
97
|
+
let step = 1;
|
|
98
|
+
for (const item of pipeline) {
|
|
99
|
+
const color = SEVERITY_COLORS[item.priority] || GRAY;
|
|
100
|
+
const cmd = item.command === "forge" ? "forge forge" : `forge ${item.command}`;
|
|
101
|
+
const args = item.args ? ` ${item.args}` : "";
|
|
102
|
+
const countStr = item.count > 0 ? ` ${DIM}(${item.count})${RESET}` : "";
|
|
103
|
+
console.log(` ${GREEN}${step}.${RESET} ${BOLD}${CYAN}${cmd}${args}${RESET} ${DIM}→${RESET} ${color}${item.reason}${countStr}${RESET}`);
|
|
104
|
+
step++;
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function pipelineToJson(pipeline) {
|
|
110
|
+
return JSON.stringify({ pipeline }, null, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── CLI ── */
|
|
114
|
+
if (process.argv[1] && (process.argv[1].endsWith("recommendation-engine.mjs") || process.argv[1].endsWith("recommendation-engine.js"))) {
|
|
115
|
+
const { buildContext } = await import("./context.mjs");
|
|
116
|
+
const ctx = await buildContext();
|
|
117
|
+
const features = ctx.features?.migrated || [];
|
|
118
|
+
const graph = ctx.graph;
|
|
119
|
+
const checks = [];
|
|
120
|
+
for (const [, cat] of Object.entries(ctx.checks || {})) {
|
|
121
|
+
if (cat.checks) checks.push(...cat.checks);
|
|
122
|
+
}
|
|
123
|
+
const pipeline = buildPipeline(checks, graph, ctx.ownership, ctx, ctx);
|
|
124
|
+
printPipeline(pipeline);
|
|
125
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, cpSync, rmSync, statSync } from "fs";
|
|
20
20
|
import { join, relative, resolve, basename } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
21
22
|
import { execFileSync } from "child_process";
|
|
22
23
|
|
|
23
24
|
const ROOT = process.cwd();
|
|
@@ -157,7 +158,8 @@ export function listBackups(target) {
|
|
|
157
158
|
|
|
158
159
|
export function verifyAfterChange() {
|
|
159
160
|
try {
|
|
160
|
-
const
|
|
161
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
162
|
+
const out = shell("node", [join(__dirname, "inspect.mjs"), "--diff", "--json"]);
|
|
161
163
|
if (!out) return { score: 0, improved: false, error: "inspect falló" };
|
|
162
164
|
|
|
163
165
|
const result = JSON.parse(out);
|
|
@@ -167,7 +169,7 @@ export function verifyAfterChange() {
|
|
|
167
169
|
let suggestedCommit = null;
|
|
168
170
|
if (improved && score > 0) {
|
|
169
171
|
const branch = shell("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
170
|
-
suggestedCommit = `git add -A && git commit -m "forge: relocate/reforge (score: ${score}
|
|
172
|
+
suggestedCommit = `git add -A && git commit -m "forge: relocate/reforge (score: ${score})"`;
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
return { score, improved, suggestedCommit };
|
|
@@ -205,7 +207,7 @@ if (action === "backup" && arg) {
|
|
|
205
207
|
console.error(`rollback: ${result.error}`);
|
|
206
208
|
process.exit(1);
|
|
207
209
|
}
|
|
208
|
-
console.log(`Score: ${result.score}
|
|
210
|
+
console.log(`Score: ${result.score} | ${result.improved ? "✓ Mejoró/igual" : "✘ Empeoró"}${result.suggestedCommit ? `\nSugerencia: ${result.suggestedCommit}` : ""}`);
|
|
209
211
|
process.exit(result.improved ? 0 : 1);
|
|
210
212
|
} else {
|
|
211
213
|
console.log("Uso: node rollback.mjs <backup|restore|list|verify> [target|id]");
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/adapters/out/legacy-<system>/<Domain>Gateway.ts
|
|
3
|
+
import { injectable } from "tsyringe";
|
|
4
|
+
import type { External<Domain>DTO } from "./External<Domain>DTO.js";
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class <Domain>Gateway {
|
|
8
|
+
private readonly baseUrl: string;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.baseUrl = process.env.LEGACY_<SYSTEM>_URL ?? "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async fetch(id: string): Promise<External<Domain>DTO | null> {
|
|
15
|
+
const response = await fetch(`${this.baseUrl}/api/${id}`, {
|
|
16
|
+
headers: { Authorization: `Bearer ${this.getApiKey()}` },
|
|
17
|
+
signal: AbortSignal.timeout(5000),
|
|
18
|
+
});
|
|
19
|
+
if (response.status === 404) return null;
|
|
20
|
+
if (!response.ok) throw new Error(`Legacy error: ${response.statusText}`);
|
|
21
|
+
return response.json();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async upsert(dto: External<Domain>DTO): Promise<External<Domain>DTO> {
|
|
25
|
+
const response = await fetch(`${this.baseUrl}/api`, {
|
|
26
|
+
method: "PUT",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
Authorization: `Bearer ${this.getApiKey()}`,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(dto),
|
|
32
|
+
signal: AbortSignal.timeout(5000),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) throw new Error(`Legacy error: ${response.statusText}`);
|
|
35
|
+
return response.json();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async remove(id: string): Promise<void> {
|
|
39
|
+
const response = await fetch(`${this.baseUrl}/api/${id}`, {
|
|
40
|
+
method: "DELETE",
|
|
41
|
+
headers: { Authorization: `Bearer ${this.getApiKey()}` },
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok && response.status !== 404) {
|
|
44
|
+
throw new Error(`Legacy error: ${response.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private getApiKey(): string {
|
|
49
|
+
return process.env.LEGACY_<SYSTEM>_API_KEY ?? "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|