@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,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}/110` : "—";
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}/110` : "—";
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
+ }