@ronaldjdevfs/forge 1.3.0-beta → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/forge/SKILL.md +57 -128
- package/skills/forge/command/forge.md +59 -14
- package/skills/forge/reference/adr.md +242 -0
- package/skills/forge/reference/anti-corruption-layer.md +340 -0
- package/skills/forge/reference/api-design.md +7 -0
- package/skills/forge/reference/api-versioning.md +354 -0
- package/skills/forge/reference/architectural-depth-checklist.md +311 -0
- package/skills/forge/reference/architecture-template.md +41 -0
- package/skills/forge/reference/assay.md +6 -0
- package/skills/forge/reference/bounded-contexts.md +311 -0
- package/skills/forge/reference/chain.md +6 -0
- package/skills/forge/reference/cohesion-checklist.md +256 -0
- package/skills/forge/reference/cqrs.md +286 -0
- package/skills/forge/reference/data-patterns.md +6 -0
- package/skills/forge/reference/di-strategies.md +6 -0
- package/skills/forge/reference/errors.md +5 -0
- package/skills/forge/reference/events.md +8 -0
- package/skills/forge/reference/evolutionary-architecture.md +300 -0
- package/skills/forge/reference/forge.md +7 -0
- package/skills/forge/reference/hooks.md +6 -0
- package/skills/forge/reference/idempotency.md +283 -0
- package/skills/forge/reference/inscribe.md +5 -0
- package/skills/forge/reference/inspect.md +6 -0
- package/skills/forge/reference/modular-monolith.md +252 -0
- package/skills/forge/reference/observability.md +5 -0
- package/skills/forge/reference/quench.md +5 -0
- package/skills/forge/reference/relocate.md +6 -0
- package/skills/forge/reference/sagas.md +359 -0
- package/skills/forge/reference/security-patterns.md +6 -0
- package/skills/forge/reference/smelt.md +6 -0
- package/skills/forge/reference/temper.md +6 -0
- package/skills/forge/reference/testing-patterns.md +6 -0
- package/skills/forge/reference/transactional-outbox.md +311 -0
- package/skills/forge/scripts/architecture.mjs +10 -5
- package/skills/forge/scripts/assay.mjs +2 -2
- package/skills/forge/scripts/chain.mjs +31 -5
- package/skills/forge/scripts/context.mjs +24 -4
- package/skills/forge/scripts/detect.mjs +39 -30
- package/skills/forge/scripts/forge-boot.mjs +108 -0
- package/skills/forge/scripts/forge-config.mjs +182 -3
- package/skills/forge/scripts/forge-state.mjs +1 -1
- package/skills/forge/scripts/forgeSentinel-lib.mjs +2 -2
- package/skills/forge/scripts/forgeSentinel.mjs +2 -2
- package/skills/forge/scripts/forgeSmith.mjs +2 -2
- package/skills/forge/scripts/graph.mjs +65 -9
- package/skills/forge/scripts/hook.mjs +2 -2
- package/skills/forge/scripts/inspect.mjs +56 -48
- package/skills/forge/scripts/parse-imports.mjs +0 -2
- package/skills/forge/scripts/posttool.mjs +211 -17
- package/skills/forge/scripts/recommendation-engine.mjs +125 -0
- package/skills/forge/scripts/rollback.mjs +5 -3
- package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
- package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
- package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
- package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
- package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
- package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
- package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
- package/skills/forge/tests/core.test.mjs +288 -4
- package/src/cli.js +26 -13
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* forge-boot.mjs — Boot orchestrator with conditional depth.
|
|
5
|
+
*
|
|
6
|
+
* Uso:
|
|
7
|
+
* node forge-boot.mjs --depth minimal|standard|full [--command <name>] [--json] [--force]
|
|
8
|
+
*
|
|
9
|
+
* Profundidades:
|
|
10
|
+
* minimal → context + profile (cast, temper, smelt, relocate, reforge, inscribe)
|
|
11
|
+
* standard → minimal + graph + chain (chain, graph)
|
|
12
|
+
* full → standard + ownership + inspect (inspect, quench, default)
|
|
13
|
+
*
|
|
14
|
+
* Cache:
|
|
15
|
+
* Usa .forge/cache/ para reusar datos entre invocaciones.
|
|
16
|
+
* Pasa --force para ignorar caché y regenerar todo.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
|
|
21
|
+
const ROOT = process.cwd();
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const depth = args.includes("--depth")
|
|
26
|
+
? args[args.indexOf("--depth") + 1]
|
|
27
|
+
: "full";
|
|
28
|
+
const isJson = args.includes("--json");
|
|
29
|
+
const force = args.includes("--force");
|
|
30
|
+
|
|
31
|
+
const result = { depth, bootTime: Date.now(), context: null, profile: null, graph: null, chain: null, ownership: null, inspect: null };
|
|
32
|
+
|
|
33
|
+
// 1. Context
|
|
34
|
+
const { buildContext } = await import("./context.mjs");
|
|
35
|
+
const ctx = await buildContext(ROOT, null, { force });
|
|
36
|
+
result.context = ctx;
|
|
37
|
+
|
|
38
|
+
// 2. Profile
|
|
39
|
+
const { detectProfile, detectProfileExtended } = await import("./profile.mjs");
|
|
40
|
+
const profileExtended = detectProfileExtended(ctx);
|
|
41
|
+
result.profile = {
|
|
42
|
+
profile: profileExtended.profile,
|
|
43
|
+
hasPlatform: profileExtended.hasPlatform,
|
|
44
|
+
hasShared: profileExtended.hasShared,
|
|
45
|
+
hasInfra: profileExtended.hasInfra,
|
|
46
|
+
layers: profileExtended.layers,
|
|
47
|
+
complementary: profileExtended.complementary,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (depth === "minimal") {
|
|
51
|
+
if (isJson) return console.log(JSON.stringify(result, null, 2));
|
|
52
|
+
console.log(`Boot [${depth}]: ${result.profile.profile} | ${ctx.features.total} features | ${ctx.framework} | ${ctx.database}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Graph
|
|
57
|
+
result.graph = ctx.graph;
|
|
58
|
+
|
|
59
|
+
// 4. Chain
|
|
60
|
+
const { buildDependencyGraph } = await import("./chain.mjs");
|
|
61
|
+
const chain = buildDependencyGraph(ROOT, result.graph);
|
|
62
|
+
result.chain = {
|
|
63
|
+
features: chain.features.length,
|
|
64
|
+
hasCycles: chain.hasCycles,
|
|
65
|
+
globalCycles: chain.globalCycles,
|
|
66
|
+
illegalChains: chain.illegalChains.length,
|
|
67
|
+
isolated: chain.isolated.length,
|
|
68
|
+
topologicalOrder: chain.topologicalOrder,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (depth === "standard") {
|
|
72
|
+
if (isJson) return console.log(JSON.stringify(result, null, 2));
|
|
73
|
+
console.log(`Boot [${depth}]: ${result.profile.profile} | ${ctx.features.total} features | ${result.graph.stats.totalNodes} nodes | chains: ${result.chain.illegalChains}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 5. Ownership (from context)
|
|
78
|
+
result.ownership = {
|
|
79
|
+
health: ctx.ownership?.health || "unknown",
|
|
80
|
+
score: ctx.ownership?.score || 0,
|
|
81
|
+
orphans: ctx.ownership?.orphans?.length || 0,
|
|
82
|
+
duplicates: ctx.ownership?.duplicates?.length || 0,
|
|
83
|
+
misplaced: ctx.ownership?.misplaced?.length || 0,
|
|
84
|
+
hasPlatform: ctx.ownership?.hasPlatform || false,
|
|
85
|
+
hasFeatures: ctx.ownership?.hasFeatures || false,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// 6. Inspect (full audit)
|
|
89
|
+
const { allChecks } = await import("./detect.mjs");
|
|
90
|
+
const checks = allChecks(ctx.features.migrated, result.graph, ctx);
|
|
91
|
+
const violations = [];
|
|
92
|
+
for (const [catName, cat] of Object.entries(checks)) {
|
|
93
|
+
for (const check of cat.checks) {
|
|
94
|
+
if (!check.pass) violations.push({ severity: check.severity, label: check.label, category: catName });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
result.inspect = {
|
|
98
|
+
totalChecks: violations.length,
|
|
99
|
+
critical: violations.filter(v => v.severity === "CRITICAL").length,
|
|
100
|
+
errors: violations.filter(v => v.severity === "ERROR").length,
|
|
101
|
+
warnings: violations.filter(v => v.severity === "WARNING").length,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (isJson) return console.log(JSON.stringify(result, null, 2));
|
|
105
|
+
console.log(`Boot [${depth}]: ${result.profile.profile} | features: ${ctx.features.total} | graph: ${result.graph.stats.totalNodes}n/${result.graph.stats.totalEdges}e | violations: ${result.inspect.totalChecks} [CRIT:${result.inspect.critical} ERR:${result.inspect.errors} WARN:${result.inspect.warnings}]`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main().catch(console.error);
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
4
|
-
import { join, dirname } from "path";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
4
|
+
import { join, dirname, relative } from "path";
|
|
5
|
+
import { createHash } from "crypto";
|
|
5
6
|
|
|
6
7
|
const ROOT = process.cwd();
|
|
7
8
|
const CONFIG_DIR = join(ROOT, ".forge");
|
|
8
9
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
9
10
|
const STATE_PATH = join(CONFIG_DIR, "state.json");
|
|
10
11
|
const HISTORY_DIR = join(CONFIG_DIR, "history");
|
|
12
|
+
const CACHE_DIR = join(CONFIG_DIR, "cache");
|
|
13
|
+
|
|
14
|
+
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
11
15
|
|
|
12
16
|
const DEFAULT_CONFIG = {
|
|
13
17
|
profile: null,
|
|
@@ -23,6 +27,7 @@ const DEFAULT_CONFIG = {
|
|
|
23
27
|
const DEFAULT_STATE = {
|
|
24
28
|
lastAudit: null,
|
|
25
29
|
lastScore: null,
|
|
30
|
+
lastMax: null,
|
|
26
31
|
lastGrade: null,
|
|
27
32
|
violationCount: 0,
|
|
28
33
|
totalFeatures: 0,
|
|
@@ -78,6 +83,7 @@ export function updateStateFromAudit(auditResult) {
|
|
|
78
83
|
const state = loadState();
|
|
79
84
|
state.lastAudit = new Date().toISOString();
|
|
80
85
|
state.lastScore = auditResult.total || 0;
|
|
86
|
+
state.lastMax = auditResult.max || null;
|
|
81
87
|
state.lastGrade = auditResult.grade || "F";
|
|
82
88
|
state.violationCount = (auditResult.violations || []).length;
|
|
83
89
|
state.health = auditResult.health || "unknown";
|
|
@@ -116,7 +122,7 @@ export function displayTrend(entries) {
|
|
|
116
122
|
console.log("─".repeat(60));
|
|
117
123
|
for (const e of entries) {
|
|
118
124
|
const date = (e.timestamp || "").slice(0, 19).replace("T", " ");
|
|
119
|
-
const score = e.score !== undefined ? `${e.score}/
|
|
125
|
+
const score = e.score !== undefined ? `${e.score}${e.max ? "/".concat(e.max) : ""}` : "—";
|
|
120
126
|
const grade = e.grade || "—";
|
|
121
127
|
const violations = e.violationCount !== undefined ? String(e.violationCount) : "—";
|
|
122
128
|
const features = e.totalFeatures !== undefined ? `${e.migratedFeatures || 0}/${e.totalFeatures}` : "—";
|
|
@@ -173,6 +179,179 @@ export function configNeedsRefresh(config) {
|
|
|
173
179
|
return daysSinceUpdate > 7;
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
/* ── Cache Layer ── */
|
|
183
|
+
|
|
184
|
+
const CACHE_KEYS = ["context", "graph", "profile", "ownership", "chain"];
|
|
185
|
+
|
|
186
|
+
function cachePath(key) {
|
|
187
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
188
|
+
return join(CACHE_DIR, `${key}.json`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cacheMetaPath() {
|
|
192
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
193
|
+
return join(CACHE_DIR, "meta.json");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function loadCacheMeta() {
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(readFileSync(cacheMetaPath(), "utf-8"));
|
|
199
|
+
} catch {
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function saveCacheMeta(meta) {
|
|
205
|
+
const existing = loadCacheMeta();
|
|
206
|
+
writeFileSync(cacheMetaPath(), JSON.stringify({ ...existing, ...meta }, null, 2) + "\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Compute a hash of the src/ directory tree (file paths + mtimes).
|
|
211
|
+
* Used to detect if the project changed since last cache.
|
|
212
|
+
*/
|
|
213
|
+
export function hashSrcDir(projectRoot = ROOT) {
|
|
214
|
+
const src = join(projectRoot, "src");
|
|
215
|
+
if (!existsSync(src)) return null;
|
|
216
|
+
const hash = createHash("sha256");
|
|
217
|
+
const entries = [];
|
|
218
|
+
function walk(dir) {
|
|
219
|
+
let dirEntries;
|
|
220
|
+
try { dirEntries = readdirSync(dir); } catch { return; }
|
|
221
|
+
for (const entry of dirEntries.sort()) {
|
|
222
|
+
const full = join(dir, entry);
|
|
223
|
+
let st;
|
|
224
|
+
try { st = statSync(full); } catch { continue; }
|
|
225
|
+
if (st.isDirectory()) {
|
|
226
|
+
walk(full);
|
|
227
|
+
} else if (entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".tsx") || entry.endsWith(".json")) {
|
|
228
|
+
entries.push(relative(projectRoot, full) + ":" + st.mtimeMs);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
walk(src);
|
|
233
|
+
hash.update(entries.join("\n"));
|
|
234
|
+
return hash.digest("hex").slice(0, 16);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Save a value to the cache with a src hash for invalidation.
|
|
239
|
+
*/
|
|
240
|
+
export function saveCache(key, data, projectRoot = ROOT) {
|
|
241
|
+
if (!CACHE_KEYS.includes(key)) return false;
|
|
242
|
+
const srcHash = hashSrcDir(projectRoot);
|
|
243
|
+
const payload = {
|
|
244
|
+
key,
|
|
245
|
+
srcHash,
|
|
246
|
+
cachedAt: Date.now(),
|
|
247
|
+
data,
|
|
248
|
+
};
|
|
249
|
+
const ok = writeJson(cachePath(key), payload);
|
|
250
|
+
if (ok) {
|
|
251
|
+
saveCacheMeta({ [key]: { srcHash, cachedAt: Date.now() } });
|
|
252
|
+
}
|
|
253
|
+
return ok;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Load a value from cache if valid (same src hash and not expired).
|
|
258
|
+
* Returns { data, valid } where valid is false if cache is stale.
|
|
259
|
+
*/
|
|
260
|
+
export function loadCache(key, projectRoot = ROOT) {
|
|
261
|
+
if (!CACHE_KEYS.includes(key)) return { data: null, valid: false };
|
|
262
|
+
const path = cachePath(key);
|
|
263
|
+
const cached = readJson(path);
|
|
264
|
+
if (!cached) return { data: null, valid: false };
|
|
265
|
+
|
|
266
|
+
// Check TTL
|
|
267
|
+
if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {
|
|
268
|
+
return { data: null, valid: false };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check src hash match
|
|
272
|
+
const currentHash = hashSrcDir(projectRoot);
|
|
273
|
+
if (currentHash && currentHash !== cached.srcHash) {
|
|
274
|
+
return { data: null, valid: false };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { data: cached.data, valid: true };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Invalidate a specific cache key or all keys.
|
|
282
|
+
*/
|
|
283
|
+
export function invalidateCache(key = null) {
|
|
284
|
+
if (key) {
|
|
285
|
+
const path = cachePath(key);
|
|
286
|
+
try { writeFileSync(path, JSON.stringify({}), "utf-8"); } catch {}
|
|
287
|
+
} else {
|
|
288
|
+
for (const k of CACHE_KEYS) {
|
|
289
|
+
try { writeFileSync(cachePath(k), JSON.stringify({}), "utf-8"); } catch {}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Determine required boot depth based on command.
|
|
296
|
+
* Returns 'minimal' | 'standard' | 'full'
|
|
297
|
+
* - minimal: context + profile (cast, temper, smelt, relocate, reforge, inscribe)
|
|
298
|
+
* - standard: minimal + graph + chain (chain, graph)
|
|
299
|
+
* - full: standard + armorer + inspect + detect + architecture (inspect, quench, default)
|
|
300
|
+
*/
|
|
301
|
+
export function getBootDepth(command) {
|
|
302
|
+
const minimal = ["cast", "temper", "smelt", "relocate", "reforge", "inscribe", "nail", "unnail", "forge-api", "forge state", "forge rollback"];
|
|
303
|
+
const standard = ["chain", "graph", "forge hook"];
|
|
304
|
+
if (minimal.includes(command)) return "minimal";
|
|
305
|
+
if (standard.includes(command)) return "standard";
|
|
306
|
+
return "full";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Try to load cached boot data up to a given depth.
|
|
311
|
+
* Returns { context, profile, graph, chain, ownership } with valid fields populated.
|
|
312
|
+
*/
|
|
313
|
+
export function loadCachedBoot(depth = "full", projectRoot = ROOT) {
|
|
314
|
+
const result = { context: null, profile: null, graph: null, chain: null, ownership: null };
|
|
315
|
+
|
|
316
|
+
// Always try context
|
|
317
|
+
const ctxCache = loadCache("context", projectRoot);
|
|
318
|
+
if (ctxCache.valid) result.context = ctxCache.data;
|
|
319
|
+
|
|
320
|
+
// Always try profile
|
|
321
|
+
const profCache = loadCache("profile", projectRoot);
|
|
322
|
+
if (profCache.valid) result.profile = profCache.data;
|
|
323
|
+
|
|
324
|
+
if (depth === "minimal") return result;
|
|
325
|
+
|
|
326
|
+
// Standard: add graph + chain
|
|
327
|
+
const graphCache = loadCache("graph", projectRoot);
|
|
328
|
+
if (graphCache.valid) result.graph = graphCache.data;
|
|
329
|
+
|
|
330
|
+
const chainCache = loadCache("chain", projectRoot);
|
|
331
|
+
if (chainCache.valid) result.chain = chainCache.data;
|
|
332
|
+
|
|
333
|
+
if (depth === "standard") return result;
|
|
334
|
+
|
|
335
|
+
// Full: add ownership
|
|
336
|
+
const ownCache = loadCache("ownership", projectRoot);
|
|
337
|
+
if (ownCache.valid) result.ownership = ownCache.data;
|
|
338
|
+
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Check if cached boot is valid for a given depth.
|
|
344
|
+
*/
|
|
345
|
+
export function hasValidCache(depth = "full", projectRoot = ROOT) {
|
|
346
|
+
const cached = loadCachedBoot(depth, projectRoot);
|
|
347
|
+
if (!cached.context) return false;
|
|
348
|
+
if (depth === "minimal") return true;
|
|
349
|
+
if (!cached.graph) return false;
|
|
350
|
+
if (depth === "standard") return true;
|
|
351
|
+
if (!cached.ownership) return false;
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
176
355
|
async function main() {
|
|
177
356
|
const args = process.argv.slice(2);
|
|
178
357
|
const isJson = args.includes("--json");
|
|
@@ -85,7 +85,7 @@ async function main() {
|
|
|
85
85
|
if (isJson) {
|
|
86
86
|
console.log(JSON.stringify({ state, recentHistory: history }, null, 2));
|
|
87
87
|
} else {
|
|
88
|
-
const lastScore = state.lastScore !== null ? `${state.lastScore}
|
|
88
|
+
const lastScore = state.lastScore !== null ? `${state.lastScore}/${state.lastMax || "?"}` : "—";
|
|
89
89
|
const lastGrade = state.lastGrade || "—";
|
|
90
90
|
console.log(`Forge State | Score: ${lastScore} | Grade: ${lastGrade} | Violaciones: ${state.violationCount} | Features: ${state.migratedFeatures}/${state.totalFeatures}`);
|
|
91
91
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import {
|
|
5
|
+
import { getGraph } from "./graph.mjs";
|
|
6
6
|
import { buildContext } from "./context.mjs";
|
|
7
7
|
import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
|
|
8
8
|
import { evaluateRules } from "./registry/rules.mjs";
|
|
@@ -18,7 +18,7 @@ export async function runSentinelCheck(files, opts = {}) {
|
|
|
18
18
|
|
|
19
19
|
const ctx = await buildContext();
|
|
20
20
|
const features = detectFeaturesOnSrc();
|
|
21
|
-
const graph =
|
|
21
|
+
const graph = ctx.graph || getGraph();
|
|
22
22
|
const results = allChecks(features, graph, ctx);
|
|
23
23
|
|
|
24
24
|
const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { readFileSync, existsSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { execFileSync } from "child_process";
|
|
6
|
-
import {
|
|
6
|
+
import { getGraph } from "./graph.mjs";
|
|
7
7
|
import { buildContext } from "./context.mjs";
|
|
8
8
|
import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
|
|
9
9
|
import { evaluateRules } from "./registry/rules.mjs";
|
|
@@ -76,7 +76,7 @@ function printResult(result, opts = {}) {
|
|
|
76
76
|
if (reminder || hook) {
|
|
77
77
|
if (!hook) {
|
|
78
78
|
console.log(` ${YELLOW}Recordatorio:${RESET} ${result.total} violación(es) arquitectónica(s).`);
|
|
79
|
-
console.log(` Ejecuta ${CYAN}forge quench${RESET} para ver el detalle completo.\n`);
|
|
79
|
+
console.log(` Ejecuta ${CYAN}forge quench${RESET} para ver el detalle completo y pipeline de corrección.\n`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
if (result.hasCritical) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import {
|
|
5
|
+
import { getGraph } from "./graph.mjs";
|
|
6
6
|
import { buildContext } from "./context.mjs";
|
|
7
7
|
import { detectFeaturesOnSrc, allChecks } from "./detect.mjs";
|
|
8
8
|
import { evaluateRules } from "./registry/rules.mjs";
|
|
@@ -67,7 +67,7 @@ async function checkProposedFile(filePath, content) {
|
|
|
67
67
|
|
|
68
68
|
const ctx = await buildContext();
|
|
69
69
|
const features = detectFeaturesOnSrc();
|
|
70
|
-
const graph =
|
|
70
|
+
const graph = ctx.graph || getGraph();
|
|
71
71
|
const results = allChecks(features, graph, ctx);
|
|
72
72
|
|
|
73
73
|
const violations = [];
|
|
@@ -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
|
|