@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,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
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { getGraph } from "./graph.mjs";
|
|
6
|
+
import { buildContext } from "./context.mjs";
|
|
7
|
+
import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
|
|
8
|
+
import { evaluateRules } from "./registry/rules.mjs";
|
|
9
|
+
|
|
10
|
+
const ROOT = process.cwd();
|
|
11
|
+
|
|
12
|
+
export async function runSentinelCheck(files, opts = {}) {
|
|
13
|
+
const { strict = false, reminder = false } = opts;
|
|
14
|
+
|
|
15
|
+
if (!files || files.length === 0) {
|
|
16
|
+
return { violations: [], graphViolations: [], total: 0, hasCritical: false, hasErrors: false, filesChecked: 0, summary: "Sin archivos fuente modificados" };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ctx = await buildContext();
|
|
20
|
+
const features = detectFeaturesOnSrc();
|
|
21
|
+
const graph = ctx.graph || getGraph();
|
|
22
|
+
const results = allChecks(features, graph, ctx);
|
|
23
|
+
|
|
24
|
+
const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
|
|
25
|
+
|
|
26
|
+
const violations = [];
|
|
27
|
+
for (const [, cat] of Object.entries(results)) {
|
|
28
|
+
for (const check of cat.checks) {
|
|
29
|
+
if (check.pass) continue;
|
|
30
|
+
|
|
31
|
+
const touchesFile = files.some(f =>
|
|
32
|
+
(check.detail && check.detail.includes(f)) ||
|
|
33
|
+
(check.label && check.label.includes(f))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!touchesFile && !reminder) continue;
|
|
37
|
+
|
|
38
|
+
if (isIgnored(check, allIgnores)) continue;
|
|
39
|
+
|
|
40
|
+
violations.push(check);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const graphViolations = evaluateRules(graph, ctx).filter(v => {
|
|
45
|
+
const touchesFile = files.some(f =>
|
|
46
|
+
(v.file && v.file.includes(f)) ||
|
|
47
|
+
(v.from && v.from.includes(f)) ||
|
|
48
|
+
(v.to && v.to.includes(f))
|
|
49
|
+
);
|
|
50
|
+
return touchesFile || reminder;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const allViolations = [...violations, ...graphViolations];
|
|
54
|
+
const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
|
|
55
|
+
const hasErrors = allViolations.some(v => v.severity === "ERROR");
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
violations: allViolations,
|
|
59
|
+
total: allViolations.length,
|
|
60
|
+
hasCritical,
|
|
61
|
+
hasErrors,
|
|
62
|
+
filesChecked: files.length,
|
|
63
|
+
summary: hasCritical
|
|
64
|
+
? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "CRITICAL").length} CRITICAL, ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
|
|
65
|
+
: hasErrors
|
|
66
|
+
? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
|
|
67
|
+
: `${allViolations.length} violación(es) menores (WARNING/INFO)`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeAuditLog(env, data, cwd) {
|
|
72
|
+
try {
|
|
73
|
+
const dir = join(cwd, ".forge", "audit");
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
const path = join(dir, "forgeSentinel.json");
|
|
76
|
+
const log = [];
|
|
77
|
+
if (existsSync(path)) {
|
|
78
|
+
try {
|
|
79
|
+
const existing = JSON.parse(readFileSync(path, "utf-8"));
|
|
80
|
+
if (Array.isArray(existing)) log.push(...existing);
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
log.push({ ...data, ts: data.ts || new Date().toISOString() });
|
|
84
|
+
writeFileSync(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
|
+
import { getGraph } from "./graph.mjs";
|
|
7
|
+
import { buildContext } from "./context.mjs";
|
|
8
|
+
import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
|
|
9
|
+
import { evaluateRules } from "./registry/rules.mjs";
|
|
10
|
+
import {
|
|
11
|
+
CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM,
|
|
12
|
+
formatCheck, formatJson,
|
|
13
|
+
} from "./formatter.mjs";
|
|
14
|
+
import { runSentinelCheck, writeAuditLog } from "./forgeSentinel-lib.mjs";
|
|
15
|
+
|
|
16
|
+
const ROOT = process.cwd();
|
|
17
|
+
|
|
18
|
+
function getChangedFiles() {
|
|
19
|
+
let files = [];
|
|
20
|
+
try {
|
|
21
|
+
const defaultBranch = execFileSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] })
|
|
22
|
+
.trim().replace("refs/remotes/origin/", "");
|
|
23
|
+
const raw = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", `${defaultBranch}...HEAD`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
24
|
+
files = raw.trim().split("\n").filter(Boolean);
|
|
25
|
+
} catch {}
|
|
26
|
+
|
|
27
|
+
if (files.length === 0) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = execFileSync("git", ["status", "--porcelain"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
30
|
+
files = raw.split("\n").filter(Boolean).map(l => l.slice(3));
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return files.filter(f => f.startsWith("src/"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseHookEvent(stdin) {
|
|
38
|
+
if (!stdin) return null;
|
|
39
|
+
try {
|
|
40
|
+
const event = JSON.parse(stdin);
|
|
41
|
+
let files = [];
|
|
42
|
+
|
|
43
|
+
// Claude Code format: { toolUse: { name, input: { filePath, content } } }
|
|
44
|
+
if (event.toolUse?.input?.filePath) {
|
|
45
|
+
files = [event.toolUse.input.filePath];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Cursor/Codex format: may have files array or filePath property
|
|
49
|
+
if (Array.isArray(event.files)) {
|
|
50
|
+
files = event.files.map(f => typeof f === "string" ? f : f.filePath).filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
if (event.filePath) {
|
|
53
|
+
files.push(event.filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return files.length > 0 ? [...new Set(files)] : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function printResult(result, opts = {}) {
|
|
63
|
+
const { reminder = false, hook = false } = opts;
|
|
64
|
+
|
|
65
|
+
const prefix = hook ? "" : `\n${BOLD}${CYAN}═══ forgeSentinel ═══${RESET}`;
|
|
66
|
+
if (!hook) console.log(prefix);
|
|
67
|
+
|
|
68
|
+
if (result.total === 0) {
|
|
69
|
+
if (!hook) {
|
|
70
|
+
console.log(` ${GREEN}✔${RESET} Sin violaciones arquitectónicas en archivos modificados`);
|
|
71
|
+
console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}\n`);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (reminder || hook) {
|
|
77
|
+
if (!hook) {
|
|
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 y pipeline de corrección.\n`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (result.hasCritical) {
|
|
83
|
+
const c = result.violations.filter(v => v.severity === "CRITICAL").length;
|
|
84
|
+
if (hook) {
|
|
85
|
+
const lines = [`[forgeSentinel] ⚠ ${c} CRITICAL, ${result.violations.filter(v => v.severity === "ERROR").length} ERROR`];
|
|
86
|
+
const toShow = result.violations.slice(0, 5);
|
|
87
|
+
for (const v of toShow) {
|
|
88
|
+
lines.push(` [${v.severity}] ${v.label}${v.file ? ` (${v.file})` : ""}`);
|
|
89
|
+
}
|
|
90
|
+
if (result.violations.length > 5) {
|
|
91
|
+
lines.push(` ${DIM}... y ${result.violations.length - 5} más. Ejecutá 'forge quench' para el detalle completo.${RESET}`);
|
|
92
|
+
}
|
|
93
|
+
console.log(lines.join("\n"));
|
|
94
|
+
} else {
|
|
95
|
+
console.log(` ${RED}⚠ ${c} violación(es) CRITICAL detectadas${RESET}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (result.hasErrors) {
|
|
99
|
+
const e = result.violations.filter(v => v.severity === "ERROR").length;
|
|
100
|
+
console.log(` ${RED}⚠ ${e} violación(es) ERROR detectadas${RESET}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!hook) {
|
|
104
|
+
const toShow = result.violations.slice(0, 5);
|
|
105
|
+
for (const v of toShow) {
|
|
106
|
+
console.log(formatCheck(v));
|
|
107
|
+
}
|
|
108
|
+
if (result.violations.length > 5) {
|
|
109
|
+
console.log(` ${DIM}... y ${result.violations.length - 5} más${RESET}`);
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const v of result.violations) {
|
|
117
|
+
console.log(formatCheck(v));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`\n ${BOLD}Resumen:${RESET} ${result.summary}`);
|
|
121
|
+
if (result.hasCritical) {
|
|
122
|
+
console.log(` ${RED}⚠ Se requiere acción correctiva antes de continuar${RESET}`);
|
|
123
|
+
}
|
|
124
|
+
console.log();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function main() {
|
|
128
|
+
const args = process.argv.slice(2);
|
|
129
|
+
const isJson = args.includes("--json");
|
|
130
|
+
const isHook = args.includes("--hook");
|
|
131
|
+
const reminder = args.includes("--reminder");
|
|
132
|
+
const isDiff = args.includes("--diff");
|
|
133
|
+
|
|
134
|
+
let files;
|
|
135
|
+
if (isHook) {
|
|
136
|
+
const chunks = [];
|
|
137
|
+
if (!process.stdin.isTTY) {
|
|
138
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
139
|
+
}
|
|
140
|
+
const stdin = Buffer.concat(chunks).toString("utf-8");
|
|
141
|
+
const hookFiles = parseHookEvent(stdin);
|
|
142
|
+
if (hookFiles) {
|
|
143
|
+
files = hookFiles;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!files || files.length === 0) {
|
|
148
|
+
if (isDiff) {
|
|
149
|
+
files = getChangedFiles();
|
|
150
|
+
} else {
|
|
151
|
+
files = args.filter(a => !a.startsWith("--"));
|
|
152
|
+
if (files.length === 0) {
|
|
153
|
+
files = getChangedFiles();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
files = (files || []).filter(f =>
|
|
159
|
+
f.endsWith(".ts") || f.endsWith(".mjs") || f.endsWith(".js") || f.endsWith(".tsx")
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const result = await runSentinelCheck(files, { strict: false, reminder: reminder || isHook });
|
|
163
|
+
|
|
164
|
+
writeAuditLog(process.env, {
|
|
165
|
+
ts: new Date().toISOString(),
|
|
166
|
+
hook: isHook ? "forgeSentinel" : "cli",
|
|
167
|
+
total: result.total,
|
|
168
|
+
hasCritical: result.hasCritical,
|
|
169
|
+
hasErrors: result.hasErrors,
|
|
170
|
+
filesChecked: result.filesChecked,
|
|
171
|
+
}, ROOT);
|
|
172
|
+
|
|
173
|
+
if (isJson) {
|
|
174
|
+
console.log(formatJson(result));
|
|
175
|
+
} else {
|
|
176
|
+
printResult(result, { reminder: reminder || isHook, hook: isHook });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (process.argv[1]?.endsWith("forgeSentinel.mjs")) {
|
|
183
|
+
main().catch(() => process.exit(0));
|
|
184
|
+
}
|