@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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/skills/forge/SKILL.md +57 -128
  4. package/skills/forge/command/forge.md +59 -14
  5. package/skills/forge/reference/adr.md +242 -0
  6. package/skills/forge/reference/anti-corruption-layer.md +340 -0
  7. package/skills/forge/reference/api-design.md +7 -0
  8. package/skills/forge/reference/api-versioning.md +354 -0
  9. package/skills/forge/reference/architectural-depth-checklist.md +311 -0
  10. package/skills/forge/reference/architecture-template.md +41 -0
  11. package/skills/forge/reference/assay.md +6 -0
  12. package/skills/forge/reference/bounded-contexts.md +311 -0
  13. package/skills/forge/reference/chain.md +6 -0
  14. package/skills/forge/reference/cohesion-checklist.md +256 -0
  15. package/skills/forge/reference/cqrs.md +286 -0
  16. package/skills/forge/reference/data-patterns.md +6 -0
  17. package/skills/forge/reference/di-strategies.md +6 -0
  18. package/skills/forge/reference/errors.md +5 -0
  19. package/skills/forge/reference/events.md +8 -0
  20. package/skills/forge/reference/evolutionary-architecture.md +300 -0
  21. package/skills/forge/reference/forge.md +7 -0
  22. package/skills/forge/reference/hooks.md +6 -0
  23. package/skills/forge/reference/idempotency.md +283 -0
  24. package/skills/forge/reference/inscribe.md +5 -0
  25. package/skills/forge/reference/inspect.md +6 -0
  26. package/skills/forge/reference/modular-monolith.md +252 -0
  27. package/skills/forge/reference/observability.md +5 -0
  28. package/skills/forge/reference/quench.md +5 -0
  29. package/skills/forge/reference/relocate.md +6 -0
  30. package/skills/forge/reference/sagas.md +359 -0
  31. package/skills/forge/reference/security-patterns.md +6 -0
  32. package/skills/forge/reference/smelt.md +6 -0
  33. package/skills/forge/reference/temper.md +6 -0
  34. package/skills/forge/reference/testing-patterns.md +6 -0
  35. package/skills/forge/reference/transactional-outbox.md +311 -0
  36. package/skills/forge/scripts/architecture.mjs +10 -5
  37. package/skills/forge/scripts/assay.mjs +2 -2
  38. package/skills/forge/scripts/chain.mjs +31 -5
  39. package/skills/forge/scripts/context.mjs +24 -4
  40. package/skills/forge/scripts/detect.mjs +39 -30
  41. package/skills/forge/scripts/forge-boot.mjs +108 -0
  42. package/skills/forge/scripts/forge-config.mjs +182 -3
  43. package/skills/forge/scripts/forge-state.mjs +1 -1
  44. package/skills/forge/scripts/forgeSentinel-lib.mjs +2 -2
  45. package/skills/forge/scripts/forgeSentinel.mjs +2 -2
  46. package/skills/forge/scripts/forgeSmith.mjs +2 -2
  47. package/skills/forge/scripts/graph.mjs +65 -9
  48. package/skills/forge/scripts/hook.mjs +2 -2
  49. package/skills/forge/scripts/inspect.mjs +56 -48
  50. package/skills/forge/scripts/parse-imports.mjs +0 -2
  51. package/skills/forge/scripts/posttool.mjs +211 -17
  52. package/skills/forge/scripts/recommendation-engine.mjs +125 -0
  53. package/skills/forge/scripts/rollback.mjs +5 -3
  54. package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
  55. package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
  56. package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
  57. package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
  58. package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
  59. package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
  60. package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
  61. package/skills/forge/tests/core.test.mjs +288 -4
  62. 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: 20,
26
- layers: 20,
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 scoreBar(score, max) {
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(` ${scoreBar(cat.score, cmax)}`);
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
- if (report.recommendations.length > 0) {
146
- console.log(` ${BOLD}${YELLOW}Recomendaciones${RESET}`);
147
- const unique = [...new Set(report.recommendations)];
148
- unique.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
149
- console.log();
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 (suggestions.length > 0) {
177
- console.log(` ${BOLD}${CYAN}Siguientes pasos sugeridos${RESET}`);
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 = result.structure.score + result.layers.score + (result.decorators?.score || 0);
286
- const maxScore = 60; // structure 20 + layers 20 + decorators 20
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
- console.log(` ${BOLD}${name} (${cat.score}/${cat.score === 0 && cat.checks.length > 0 ? "—" : maxScore})${RESET}`);
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
- grade: report.grade,
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: report.total >= 80 ? "healthy" : report.total >= 50 ? "fair" : "poor",
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: report.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 — DEPRECATED: Backward-compat wrapper.
4
+ * posttool.mjs — PostToolUse hook para Forge.
5
5
  *
6
- * Use forgeSentinel.mjs en su lugar.
7
- * node forgeSentinel.mjs [options]
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
- * Este wrapper delega en forgeSentinel.mjs para mantener compatibilidad
10
- * con scripts y referencias existentes.
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 { join, dirname } from "path";
14
- import { fileURLToPath } from "url";
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 __dirname = dirname(fileURLToPath(import.meta.url));
37
+ const ROOT = process.cwd();
17
38
 
18
- // Re-export for backward compat
19
- export { runSentinelCheck as postToolCheck } from "./forgeSentinel-lib.mjs";
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 (process.argv[1]?.endsWith("posttool.mjs")) {
22
- const forgeSentinel = join(__dirname, "forgeSentinel.mjs");
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 { execFileSync } = await import("child_process");
25
- try {
26
- execFileSync(process.execPath, [forgeSentinel, ...args], { stdio: "inherit" });
27
- } catch {
28
- process.exit(0);
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 out = shell("node", [join(import.meta.url, "..", "inspect.mjs"), "--diff", "--json"]);
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}/110)"`;
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}/110 | ${result.improved ? "✓ Mejoró/igual" : "✘ Empeoró"}${result.suggestedCommit ? `\nSugerencia: ${result.suggestedCommit}` : ""}`);
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
+ ```