@ronaldjdevfs/forge 1.2.0 → 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 (74) hide show
  1. package/README.md +36 -21
  2. package/package.json +7 -2
  3. package/skills/forge/SKILL.md +56 -122
  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 +86 -0
  45. package/skills/forge/scripts/forgeSentinel.mjs +184 -0
  46. package/skills/forge/scripts/forgeSmith-admin.mjs +104 -0
  47. package/skills/forge/scripts/forgeSmith.mjs +164 -0
  48. package/skills/forge/scripts/graph.mjs +65 -9
  49. package/skills/forge/scripts/hook.mjs +2 -2
  50. package/skills/forge/scripts/inspect.mjs +56 -48
  51. package/skills/forge/scripts/parse-imports.mjs +0 -2
  52. package/skills/forge/scripts/pin.mjs +10 -3
  53. package/skills/forge/scripts/posttool.mjs +2 -2
  54. package/skills/forge/scripts/recommendation-engine.mjs +125 -0
  55. package/skills/forge/scripts/rollback.mjs +5 -3
  56. package/skills/forge/templates/agents/SKILL.md.template +283 -0
  57. package/skills/forge/templates/agents/agents/hooks.json +18 -0
  58. package/skills/forge/templates/agents/claude/CLAUDE.md +17 -16
  59. package/skills/forge/templates/agents/claude/settings.local.json +18 -0
  60. package/skills/forge/templates/agents/codex/hooks.json +18 -0
  61. package/skills/forge/templates/agents/cursor/.cursorrules +22 -5
  62. package/skills/forge/templates/agents/cursor/hooks.json +11 -0
  63. package/skills/forge/templates/agents/gemini/SKILL.md +13 -0
  64. package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
  65. package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
  66. package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
  67. package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
  68. package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
  69. package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
  70. package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
  71. package/skills/forge/tests/core.test.mjs +284 -0
  72. package/src/agents.mjs +35 -2
  73. package/src/cli.js +112 -39
  74. package/src/wizard.mjs +142 -90
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const ROOT = process.cwd();
7
+ const FORGE_DIR = join(ROOT, ".forge");
8
+ const CONFIG_PATH = join(FORGE_DIR, "hooks-config.json");
9
+ const AUDIT_DIR = join(FORGE_DIR, "audit");
10
+
11
+ const DEFAULT_CONFIG = {
12
+ forgeSentinel: { enabled: true },
13
+ forgeSmith: { enabled: true },
14
+ };
15
+
16
+ function readConfig() {
17
+ try {
18
+ if (existsSync(CONFIG_PATH)) {
19
+ const data = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
20
+ return { ...DEFAULT_CONFIG, ...data };
21
+ }
22
+ } catch {}
23
+ return { ...DEFAULT_CONFIG };
24
+ }
25
+
26
+ function writeConfig(config) {
27
+ mkdirSync(FORGE_DIR, { recursive: true });
28
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
29
+ }
30
+
31
+ function cmdStatus() {
32
+ const config = readConfig();
33
+ const auditPath = join(AUDIT_DIR, "forgeSentinel.json");
34
+ let lastRun = null;
35
+ try {
36
+ if (existsSync(auditPath)) {
37
+ const log = JSON.parse(readFileSync(auditPath, "utf-8"));
38
+ if (Array.isArray(log) && log.length > 0) {
39
+ lastRun = log[log.length - 1];
40
+ }
41
+ }
42
+ } catch {}
43
+
44
+ console.log("── forgeSmith-admin ──");
45
+ console.log(` forgeSentinel (PostToolUse): ${config.forgeSentinel.enabled ? "✓ activo" : "✘ inactivo"}`);
46
+ console.log(` forgeSmith (preToolUse): ${config.forgeSmith.enabled ? "✓ activo" : "✘ inactivo"}`);
47
+ if (lastRun) {
48
+ console.log(` Último forgeSentinel: ${lastRun.ts} (${lastRun.filesChecked || 0} archivos, ${lastRun.total || 0} violaciones)`);
49
+ }
50
+ console.log(` Config: ${CONFIG_PATH}`);
51
+ }
52
+
53
+ function cmdOn(hookName) {
54
+ const config = readConfig();
55
+ if (hookName === "all" || hookName === "forgeSentinel") {
56
+ config.forgeSentinel.enabled = true;
57
+ }
58
+ if (hookName === "all" || hookName === "forgeSmith") {
59
+ config.forgeSmith.enabled = true;
60
+ }
61
+ writeConfig(config);
62
+ console.log(`forgeSmith-admin: hooks activados (${hookName})`);
63
+ }
64
+
65
+ function cmdOff(hookName) {
66
+ const config = readConfig();
67
+ if (hookName === "all" || hookName === "forgeSentinel") {
68
+ config.forgeSentinel.enabled = false;
69
+ }
70
+ if (hookName === "all" || hookName === "forgeSmith") {
71
+ config.forgeSmith.enabled = false;
72
+ }
73
+ writeConfig(config);
74
+ console.log(`forgeSmith-admin: hooks desactivados (${hookName})`);
75
+ }
76
+
77
+ function cmdReset() {
78
+ writeConfig(DEFAULT_CONFIG);
79
+ console.log("forgeSmith-admin: configuración restaurada a valores por defecto");
80
+ }
81
+
82
+ const [,, action, arg] = process.argv;
83
+
84
+ switch (action) {
85
+ case "status":
86
+ cmdStatus();
87
+ break;
88
+ case "on":
89
+ cmdOn(arg || "all");
90
+ break;
91
+ case "off":
92
+ cmdOff(arg || "all");
93
+ break;
94
+ case "reset":
95
+ cmdReset();
96
+ break;
97
+ default:
98
+ console.log("Uso: node forgeSmith-admin.mjs <status|on|off|reset> [hook-name]");
99
+ console.log("\n status Mostrar estado de hooks");
100
+ console.log(" on [forgeSentinel|forgeSmith|all] Activar hook(s)");
101
+ console.log(" off [forgeSentinel|forgeSmith|all] Desactivar hook(s)");
102
+ console.log(" reset Restaurar configuración por defecto");
103
+ process.exit(1);
104
+ }
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { getGraph } from "./graph.mjs";
6
+ import { buildContext } from "./context.mjs";
7
+ import { detectFeaturesOnSrc, allChecks } from "./detect.mjs";
8
+ import { evaluateRules } from "./registry/rules.mjs";
9
+ import {
10
+ CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM,
11
+ } from "./formatter.mjs";
12
+ import { writeAuditLog } from "./forgeSentinel-lib.mjs";
13
+
14
+ const ROOT = process.cwd();
15
+
16
+ const ALLOWED_EXTS = [".ts", ".mjs", ".js", ".tsx", ".jsx"];
17
+
18
+ /**
19
+ * Parse Cursor preToolUse event from stdin.
20
+ * Cursor sends: { toolName, args: { filePath, content } }
21
+ */
22
+ function parseCursorEvent(stdin) {
23
+ if (!stdin) return null;
24
+ try {
25
+ const event = JSON.parse(stdin);
26
+
27
+ // Cursor preToolUse format
28
+ if (event.toolName && event.args?.filePath) {
29
+ return { filePath: event.args.filePath, content: event.args.content || "" };
30
+ }
31
+
32
+ // Alternative: direct filePath + content
33
+ if (event.filePath && event.content !== undefined) {
34
+ return { filePath: event.filePath, content: event.content };
35
+ }
36
+
37
+ // Array of proposed edits
38
+ if (Array.isArray(event.edits)) {
39
+ return event.edits.map(e => ({
40
+ filePath: e.filePath || e.path || "",
41
+ content: e.content || e.newContent || "",
42
+ })).filter(e => e.filePath)[0] || null;
43
+ }
44
+
45
+ return null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function isSourceFile(filePath) {
52
+ return filePath.startsWith("src/") && ALLOWED_EXTS.some(ext => filePath.endsWith(ext));
53
+ }
54
+
55
+ function hasCriticalViolations(result) {
56
+ return result.violations.some(v => v.severity === "CRITICAL" || v.severity === "ERROR");
57
+ }
58
+
59
+ /**
60
+ * Quick check against proposed content.
61
+ * Returns violations that would be introduced by the proposed content.
62
+ */
63
+ async function checkProposedFile(filePath, content) {
64
+ if (!isSourceFile(filePath)) {
65
+ return { violations: [], total: 0, hasCritical: false, hasErrors: false };
66
+ }
67
+
68
+ const ctx = await buildContext();
69
+ const features = detectFeaturesOnSrc();
70
+ const graph = ctx.graph || getGraph();
71
+ const results = allChecks(features, graph, ctx);
72
+
73
+ const violations = [];
74
+ for (const [, cat] of Object.entries(results)) {
75
+ for (const check of cat.checks) {
76
+ if (check.pass) continue;
77
+
78
+ const touchesFile = (check.detail && check.detail.includes(filePath)) ||
79
+ (check.label && check.label.includes(filePath));
80
+
81
+ if (!touchesFile) continue;
82
+
83
+ violations.push(check);
84
+ }
85
+ }
86
+
87
+ const graphViolations = evaluateRules(graph, ctx).filter(v => {
88
+ return (v.file && v.file.includes(filePath)) ||
89
+ (v.from && v.from.includes(filePath)) ||
90
+ (v.to && v.to.includes(filePath));
91
+ });
92
+
93
+ const allViolations = [...violations, ...graphViolations];
94
+ const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
95
+ const hasErrors = allViolations.some(v => v.severity === "ERROR");
96
+
97
+ return { violations: allViolations, total: allViolations.length, hasCritical, hasErrors };
98
+ }
99
+
100
+ function buildDenyPayload(result) {
101
+ const criticalCount = result.violations.filter(v => v.severity === "CRITICAL").length;
102
+ const errorCount = result.violations.filter(v => v.severity === "ERROR").length;
103
+
104
+ return {
105
+ deny: true,
106
+ severity: criticalCount > 0 ? "CRITICAL" : "ERROR",
107
+ message: `[forgeSmith] ⚠ ${result.total} violación(es) arquitectónica(s) (${criticalCount} CRITICAL, ${errorCount} ERROR). Corregí antes de escribir.`,
108
+ violations: result.violations.slice(0, 10).map(v => ({
109
+ severity: v.severity,
110
+ label: v.label,
111
+ rule: v.rule,
112
+ fix: v.fix,
113
+ })),
114
+ };
115
+ }
116
+
117
+ function buildAllowPayload() {
118
+ return { deny: false };
119
+ }
120
+
121
+ async function main() {
122
+ const chunks = [];
123
+ if (!process.stdin.isTTY) {
124
+ for await (const chunk of process.stdin) chunks.push(chunk);
125
+ }
126
+ const stdin = Buffer.concat(chunks).toString("utf-8");
127
+
128
+ if (!stdin) {
129
+ process.stdout.write(JSON.stringify(buildAllowPayload()));
130
+ process.exit(0);
131
+ }
132
+
133
+ const edit = parseCursorEvent(stdin);
134
+ if (!edit || !edit.filePath) {
135
+ process.stdout.write(JSON.stringify(buildAllowPayload()));
136
+ process.exit(0);
137
+ }
138
+
139
+ const result = await checkProposedFile(edit.filePath, edit.content || "");
140
+
141
+ writeAuditLog(process.env, {
142
+ ts: new Date().toISOString(),
143
+ hook: "forgeSmith",
144
+ file: edit.filePath,
145
+ total: result.total,
146
+ hasCritical: result.hasCritical,
147
+ hasErrors: result.hasErrors,
148
+ }, ROOT);
149
+
150
+ if (result.total > 0 && hasCriticalViolations(result)) {
151
+ process.stdout.write(JSON.stringify(buildDenyPayload(result)));
152
+ } else {
153
+ process.stdout.write(JSON.stringify(buildAllowPayload()));
154
+ }
155
+
156
+ process.exit(0);
157
+ }
158
+
159
+ if (process.argv[1]?.endsWith("forgeSmith.mjs")) {
160
+ main().catch(() => {
161
+ process.stdout.write(JSON.stringify({ deny: false }));
162
+ process.exit(0);
163
+ });
164
+ }
@@ -3,6 +3,7 @@
3
3
  import { join, relative, dirname, basename } from "path";
4
4
  import { readFileSync, existsSync, readdirSync, statSync } from "fs";
5
5
  import { parseImportPaths } from "./parse-imports.mjs";
6
+ import { saveCache, loadCache } from "./forge-config.mjs";
6
7
 
7
8
  const ROOT = process.cwd();
8
9
  const SRC = join(ROOT, "src");
@@ -308,7 +309,7 @@ export function buildGraph(projectRoot = ROOT) {
308
309
  const gImports = parseImportPaths(content, filePath);
309
310
  const seenTargets = new Set();
310
311
 
311
- for (const imp of imports) {
312
+ for (const imp of gImports) {
312
313
  let targetId = resolveNodeId(imp, featureMap);
313
314
 
314
315
  if (!targetId) {
@@ -421,7 +422,7 @@ export function buildGraph(projectRoot = ROOT) {
421
422
  if (directedEdges[e.from]) directedEdges[e.from].push(e.to);
422
423
  }
423
424
 
424
- let hasCycle = false;
425
+ let hasCycles = false;
425
426
  function dfs(node, visited, stack) {
426
427
  if (stack.has(node)) return true;
427
428
  if (visited.has(node)) return false;
@@ -436,20 +437,28 @@ export function buildGraph(projectRoot = ROOT) {
436
437
 
437
438
  const visited = new Set();
438
439
  for (const n of allNodeIds) {
439
- if (hasCycle) break;
440
+ if (hasCycles) break;
440
441
  if (!visited.has(n)) {
441
442
  if (dfs(n, visited, new Set())) {
442
- hasCycle = true;
443
+ hasCycles = true;
443
444
  }
444
445
  }
445
446
  }
446
447
 
447
- if (hasCycle) {
448
+ if (hasCycles) {
448
449
  addViolation("(cycle)", "(cycle)", SEVERITY.ERROR, "R9",
449
450
  "Ciclo de dependencia detectado en el grafo global");
450
451
  }
451
452
 
452
- /* ── 8. Stats ── */
453
+ /* ── 7a. Summary output (compact) ── */
454
+ function buildSummary(graph) {
455
+ return {
456
+ stats: graph.stats,
457
+ violations: graph.violations.map(v => ({ rule: v.rule, severity: v.severity, from: v.from, to: v.to, description: v.description })),
458
+ };
459
+ }
460
+
461
+ /* ── 8. Stats ── */
453
462
  const bySeverity = { CRITICAL: 0, ERROR: 0, WARNING: 0 };
454
463
  for (const v of violations) {
455
464
  bySeverity[v.severity] = (bySeverity[v.severity] || 0) + 1;
@@ -488,6 +497,7 @@ export function buildGraph(projectRoot = ROOT) {
488
497
  criticalViolations: bySeverity.CRITICAL || 0,
489
498
  errorViolations: bySeverity.ERROR || 0,
490
499
  warningViolations: bySeverity.WARNING || 0,
500
+ hasCycles,
491
501
  riskScore,
492
502
  health,
493
503
  dependencyHealth,
@@ -504,6 +514,42 @@ export function validateGraph(graph) {
504
514
  };
505
515
  }
506
516
 
517
+ /**
518
+ * getGraph — Cache-aware graph builder.
519
+ * Uses cached graph if src/ hasn't changed, builds fresh otherwise.
520
+ * Call with { force: true } to always rebuild.
521
+ */
522
+ let _graphCache = null;
523
+
524
+ export function getGraph(projectRoot = ROOT, opts = {}) {
525
+ const { force = false } = opts;
526
+
527
+ // Return memoized in-memory graph (same process)
528
+ if (_graphCache && !force) return _graphCache;
529
+
530
+ // Try disk cache
531
+ if (!force) {
532
+ const cached = loadCache("graph", projectRoot);
533
+ if (cached.valid && cached.data) {
534
+ _graphCache = cached.data;
535
+ return _graphCache;
536
+ }
537
+ }
538
+
539
+ // Build fresh and cache
540
+ const graph = buildGraph(projectRoot);
541
+ saveCache("graph", graph, projectRoot);
542
+ _graphCache = graph;
543
+ return graph;
544
+ }
545
+
546
+ /**
547
+ * Reset in-memory cache (for testing or forced refresh).
548
+ */
549
+ export function resetGraphCache() {
550
+ _graphCache = null;
551
+ }
552
+
507
553
  export function exportGraph(graph) {
508
554
  let out = "## Architecture Graph\n\n";
509
555
  out += `**Nodes:** ${graph.stats.totalNodes} \n`;
@@ -565,11 +611,21 @@ export function exportGraph(graph) {
565
611
  async function main() {
566
612
  const args = process.argv.slice(2);
567
613
  const format = args.includes("--json") ? "json" : "text";
568
- const graph = buildGraph();
614
+ const summary = args.includes("--summary");
615
+ const force = args.includes("--force");
616
+ const graph = getGraph(ROOT, { force });
569
617
  if (format === "json") {
570
- console.log(JSON.stringify(graph, null, 2));
618
+ if (summary) {
619
+ console.log(JSON.stringify(buildSummary(graph), null, 2));
620
+ } else {
621
+ console.log(JSON.stringify(graph, null, 2));
622
+ }
571
623
  } else {
572
- console.log(exportGraph(graph));
624
+ if (summary) {
625
+ console.log(`Graph: ${graph.stats.totalNodes} nodes, ${graph.stats.totalEdges} edges, ${graph.stats.violations} violations, risk ${graph.stats.riskScore}/100, health: ${graph.stats.health}`);
626
+ } else {
627
+ console.log(exportGraph(graph));
628
+ }
573
629
  }
574
630
  }
575
631
 
@@ -150,11 +150,11 @@ async function cmdCheck() {
150
150
 
151
151
  // Run detect only over the staged source files
152
152
  const { allChecks, detectFeaturesOnSrc } = await import("./detect.mjs");
153
- const { buildGraph } = await import("./graph.mjs");
153
+ const { getGraph } = await import("./graph.mjs");
154
154
  const { buildContext } = await import("./context.mjs");
155
155
 
156
156
  const ctx = await buildContext();
157
- const graph = buildGraph(ROOT);
157
+ const graph = ctx.graph || getGraph();
158
158
  const features = detectFeaturesOnSrc();
159
159
  const results = allChecks(features, graph, ctx);
160
160
 
@@ -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
 
@@ -18,9 +18,16 @@ import { fileURLToPath } from 'node:url';
18
18
 
19
19
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
20
20
 
21
- const HARNESS_DIRS = ['.claude', '.cursor', '.gemini', '.codex', '.agents', '.opencode', '.kiro'];
22
-
23
- const VALID_COMMANDS = ['forge', 'cast', 'inspect', 'quench', 'temper', 'chain', 'graph', 'relocate', 'reforge', 'smelt', 'inscribe'];
21
+ const HARNESS_DIRS = [
22
+ '.claude', '.cursor', '.gemini', '.codex', '.agents',
23
+ '.opencode', '.kiro', '.rovodev', '.trae', '.trae-cn', '.pi',
24
+ ];
25
+
26
+ const VALID_COMMANDS = [
27
+ 'forge', 'cast', 'inspect', 'quench', 'temper', 'chain', 'graph',
28
+ 'relocate', 'reforge', 'smelt', 'inscribe', 'armorer',
29
+ 'assay', 'nail', 'unnail', 'hook', 'api', 'rollback', 'state',
30
+ ];
24
31
 
25
32
  const PIN_MARKER = '<!-- forge-pinned-skill -->';
26
33
 
@@ -25,7 +25,7 @@
25
25
  import { readFileSync, existsSync, statSync, readdirSync } from "fs";
26
26
  import { join, relative, resolve, extname } from "path";
27
27
  import { execFileSync } from "child_process";
28
- import { buildGraph } from "./graph.mjs";
28
+ import { getGraph } from "./graph.mjs";
29
29
  import { buildContext } from "./context.mjs";
30
30
  import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
31
31
  import { evaluateRules } from "./registry/rules.mjs";
@@ -82,7 +82,7 @@ export async function postToolCheck(files, opts = {}) {
82
82
  // Build context and run full audit
83
83
  const ctx = await buildContext();
84
84
  const features = detectFeaturesOnSrc();
85
- const graph = buildGraph(ROOT);
85
+ const graph = ctx.graph || getGraph();
86
86
  const results = allChecks(features, graph, ctx);
87
87
 
88
88
  // Load inline ignores