@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.
- package/README.md +36 -21
- package/package.json +7 -2
- package/skills/forge/SKILL.md +56 -122
- 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 +86 -0
- package/skills/forge/scripts/forgeSentinel.mjs +184 -0
- package/skills/forge/scripts/forgeSmith-admin.mjs +104 -0
- package/skills/forge/scripts/forgeSmith.mjs +164 -0
- 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/pin.mjs +10 -3
- package/skills/forge/scripts/posttool.mjs +2 -2
- package/skills/forge/scripts/recommendation-engine.mjs +125 -0
- package/skills/forge/scripts/rollback.mjs +5 -3
- package/skills/forge/templates/agents/SKILL.md.template +283 -0
- package/skills/forge/templates/agents/agents/hooks.json +18 -0
- package/skills/forge/templates/agents/claude/CLAUDE.md +17 -16
- package/skills/forge/templates/agents/claude/settings.local.json +18 -0
- package/skills/forge/templates/agents/codex/hooks.json +18 -0
- package/skills/forge/templates/agents/cursor/.cursorrules +22 -5
- package/skills/forge/templates/agents/cursor/hooks.json +11 -0
- package/skills/forge/templates/agents/gemini/SKILL.md +13 -0
- 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 +284 -0
- package/src/agents.mjs +35 -2
- package/src/cli.js +112 -39
- 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
|
|
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
|
|
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 (
|
|
440
|
+
if (hasCycles) break;
|
|
440
441
|
if (!visited.has(n)) {
|
|
441
442
|
if (dfs(n, visited, new Set())) {
|
|
442
|
-
|
|
443
|
+
hasCycles = true;
|
|
443
444
|
}
|
|
444
445
|
}
|
|
445
446
|
}
|
|
446
447
|
|
|
447
|
-
if (
|
|
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
|
-
/* ──
|
|
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
|
|
614
|
+
const summary = args.includes("--summary");
|
|
615
|
+
const force = args.includes("--force");
|
|
616
|
+
const graph = getGraph(ROOT, { force });
|
|
569
617
|
if (format === "json") {
|
|
570
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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:
|
|
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
|
|
|
@@ -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 = [
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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 =
|
|
85
|
+
const graph = ctx.graph || getGraph();
|
|
86
86
|
const results = allChecks(features, graph, ctx);
|
|
87
87
|
|
|
88
88
|
// Load inline ignores
|