@ronaldjdevfs/forge 1.0.2 → 1.1.0

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 (48) hide show
  1. package/README.md +130 -8
  2. package/package.json +2 -2
  3. package/skills/forge/SKILL.md +86 -15
  4. package/skills/forge/profiles/express-drizzle.md +107 -0
  5. package/skills/forge/profiles/fastify-mongodb.md +103 -0
  6. package/skills/forge/profiles/fastify-prisma.md +81 -0
  7. package/skills/forge/profiles/nestjs-mongodb.md +92 -0
  8. package/skills/forge/profiles/nestjs-postgres.md +98 -0
  9. package/skills/forge/reference/api-design.md +62 -0
  10. package/skills/forge/reference/assay.md +82 -0
  11. package/skills/forge/reference/cast.md +81 -7
  12. package/skills/forge/reference/data-patterns.md +86 -0
  13. package/skills/forge/reference/di-strategies.md +50 -0
  14. package/skills/forge/reference/errors.md +65 -0
  15. package/skills/forge/reference/events.md +95 -0
  16. package/skills/forge/reference/help.md +40 -0
  17. package/skills/forge/reference/hooks.md +62 -0
  18. package/skills/forge/reference/observability.md +66 -0
  19. package/skills/forge/reference/patterns.md +52 -0
  20. package/skills/forge/reference/principles.md +6 -0
  21. package/skills/forge/reference/reforge.md +69 -5
  22. package/skills/forge/reference/relocate.md +15 -2
  23. package/skills/forge/reference/security-patterns.md +87 -0
  24. package/skills/forge/reference/testing-patterns.md +69 -0
  25. package/skills/forge/scripts/assay.mjs +481 -0
  26. package/skills/forge/scripts/context.mjs +147 -43
  27. package/skills/forge/scripts/detect.mjs +371 -22
  28. package/skills/forge/scripts/forge-api.mjs +373 -0
  29. package/skills/forge/scripts/forge-config.mjs +268 -0
  30. package/skills/forge/scripts/forge-signals.mjs +131 -0
  31. package/skills/forge/scripts/forge-state.mjs +97 -0
  32. package/skills/forge/scripts/formatter.mjs +133 -0
  33. package/skills/forge/scripts/graph.mjs +5 -21
  34. package/skills/forge/scripts/hook.mjs +250 -0
  35. package/skills/forge/scripts/inspect.mjs +171 -22
  36. package/skills/forge/scripts/parse-imports.mjs +249 -0
  37. package/skills/forge/scripts/pin.mjs +151 -0
  38. package/skills/forge/scripts/posttool.mjs +224 -0
  39. package/skills/forge/scripts/profile.mjs +124 -20
  40. package/skills/forge/scripts/registry/rules.mjs +344 -0
  41. package/skills/forge/scripts/rename.mjs +669 -0
  42. package/skills/forge/scripts/rollback.mjs +213 -0
  43. package/skills/forge/scripts/update.mjs +114 -0
  44. package/skills/forge/templates/feature/domain-error.ts.md +9 -0
  45. package/skills/forge/templates/feature/domain-event.ts.md +9 -0
  46. package/skills/forge/templates/feature/event-handler.ts.md +10 -0
  47. package/skills/forge/templates/feature/use-case.ts.md +10 -2
  48. package/skills/forge/tests/core.test.mjs +403 -0
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * posttool.mjs — PostToolUse hook para Forge.
5
+ *
6
+ * Se ejecuta después de que el agente escribe código. Analiza los archivos
7
+ * modificados y reporta violaciones arquitectónicas.
8
+ *
9
+ * Uso (CLI):
10
+ * node posttool.mjs → analiza archivos changed en git
11
+ * node posttool.mjs <file1> [file2...] → analiza archivos específicos
12
+ * node posttool.mjs --diff → solo archivos changed vs default branch
13
+ * node posttool.mjs --json → salida JSON
14
+ * node posttool.mjs --strict → exit code 1 si hay CRITICAL/ERROR
15
+ * node posttool.mjs --reminder → solo recordatorio (no bloquea)
16
+ *
17
+ * Uso (API):
18
+ * import { postToolCheck } from "./posttool.mjs";
19
+ * const result = await postToolCheck([...files]);
20
+ *
21
+ * Integración en SKILL.md (Fase 7 del pipeline):
22
+ * node .opencode/skills/forge/scripts/posttool.mjs --reminder
23
+ */
24
+
25
+ import { readFileSync, existsSync, statSync, readdirSync } from "fs";
26
+ import { join, relative, resolve, extname } from "path";
27
+ import { execFileSync } from "child_process";
28
+ import { buildGraph } from "./graph.mjs";
29
+ import { buildContext } from "./context.mjs";
30
+ import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
31
+ import { evaluateRules } from "./registry/rules.mjs";
32
+ import {
33
+ CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM, GRAY,
34
+ formatViolation, formatCheck, formatJson,
35
+ } from "./formatter.mjs";
36
+
37
+ const ROOT = process.cwd();
38
+
39
+ function getChangedFiles() {
40
+ let files = [];
41
+ try {
42
+ const defaultBranch = execFileSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] })
43
+ .trim().replace("refs/remotes/origin/", "");
44
+ const raw = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", `${defaultBranch}...HEAD`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
45
+ files = raw.trim().split("\n").filter(Boolean);
46
+ } catch {}
47
+
48
+ if (files.length === 0) {
49
+ try {
50
+ const raw = execFileSync("git", ["status", "--porcelain"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
51
+ files = raw.split("\n").filter(Boolean).map(l => l.slice(3));
52
+ } catch {}
53
+ }
54
+
55
+ return files.filter(f => f.startsWith("src/"));
56
+ }
57
+
58
+ function readFile(path) {
59
+ try { return readFileSync(path, "utf-8"); } catch { return null; }
60
+ }
61
+
62
+ /**
63
+ * Check modified files for violations.
64
+ * Returns { violations, graphViolations, total, hasCritical, hasErrors, summary }
65
+ */
66
+ export async function postToolCheck(files, opts = {}) {
67
+ const { strict = false, reminder = false } = opts;
68
+
69
+ if (!files || files.length === 0) {
70
+ files = getChangedFiles();
71
+ }
72
+
73
+ // Only check source files
74
+ files = files.filter(f =>
75
+ f.endsWith(".ts") || f.endsWith(".mjs") || f.endsWith(".js") || f.endsWith(".tsx")
76
+ );
77
+
78
+ if (files.length === 0) {
79
+ return { violations: [], graphViolations: [], total: 0, hasCritical: false, hasErrors: false, filesChecked: 0, summary: "Sin archivos fuente modificados" };
80
+ }
81
+
82
+ // Build context and run full audit
83
+ const ctx = await buildContext();
84
+ const features = detectFeaturesOnSrc();
85
+ const graph = buildGraph(ROOT);
86
+ const results = allChecks(features, graph, ctx);
87
+
88
+ // Load inline ignores
89
+ const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
90
+
91
+ // Collect violations that touch the modified files
92
+ const violations = [];
93
+ for (const [, cat] of Object.entries(results)) {
94
+ for (const check of cat.checks) {
95
+ if (check.pass) continue;
96
+
97
+ // Only include violations touching modified files
98
+ const touchesFile = files.some(f =>
99
+ (check.detail && check.detail.includes(f)) ||
100
+ (check.label && check.label.includes(f))
101
+ );
102
+
103
+ if (!touchesFile && !reminder) continue;
104
+
105
+ // Check inline ignores
106
+ if (isIgnored(check, allIgnores)) continue;
107
+
108
+ violations.push(check);
109
+ }
110
+ }
111
+
112
+ // Also evaluate graph rules for changed files
113
+ const graphViolations = evaluateRules(graph, ctx).filter(v => {
114
+ const touchesFile = files.some(f =>
115
+ (v.file && v.file.includes(f)) ||
116
+ (v.from && v.from.includes(f)) ||
117
+ (v.to && v.to.includes(f))
118
+ );
119
+ return touchesFile || reminder;
120
+ });
121
+
122
+ const allViolations = [...violations, ...graphViolations];
123
+ const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
124
+ const hasErrors = allViolations.some(v => v.severity === "ERROR");
125
+
126
+ return {
127
+ violations: allViolations,
128
+ total: allViolations.length,
129
+ hasCritical,
130
+ hasErrors,
131
+ filesChecked: files.length,
132
+ summary: hasCritical
133
+ ? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "CRITICAL").length} CRITICAL, ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
134
+ : hasErrors
135
+ ? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
136
+ : `${allViolations.length} violación(es) menores (WARNING/INFO)`,
137
+ };
138
+ }
139
+
140
+ function printResult(result, opts = {}) {
141
+ const { reminder = false } = opts;
142
+
143
+ console.log(`\n${BOLD}${CYAN}═══ Forge PostTool Hook ═══${RESET}`);
144
+
145
+ if (result.total === 0) {
146
+ console.log(` ${GREEN}✔${RESET} Sin violaciones arquitectónicas en archivos modificados`);
147
+ console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}\n`);
148
+ return;
149
+ }
150
+
151
+ console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}`);
152
+ console.log(` ${DIM}Violaciones: ${result.total}${RESET}\n`);
153
+
154
+ if (reminder) {
155
+ console.log(` ${YELLOW}Recordatorio:${RESET} Los archivos modificados tienen ${result.total} violación(es) arquitectónica(s).`);
156
+ console.log(` Ejecuta ${CYAN}forge quench${RESET} para ver el detalle completo.\n`);
157
+
158
+ if (result.hasCritical) {
159
+ const criticalCount = result.violations.filter(v => v.severity === "CRITICAL").length;
160
+ console.log(` ${RED}⚠ ${criticalCount} violación(es) CRITICAL detectadas${RESET}`);
161
+ }
162
+ if (result.hasErrors) {
163
+ const errorCount = result.violations.filter(v => v.severity === "ERROR").length;
164
+ console.log(` ${RED}⚠ ${errorCount} violación(es) ERROR detectadas${RESET}`);
165
+ }
166
+
167
+ // Print first 5 violations as summary
168
+ const toShow = result.violations.slice(0, 5);
169
+ for (const v of toShow) {
170
+ console.log(formatCheck(v));
171
+ }
172
+ if (result.violations.length > 5) {
173
+ console.log(` ${DIM}... y ${result.violations.length - 5} más${RESET}`);
174
+ }
175
+ console.log();
176
+ return;
177
+ }
178
+
179
+ // Full output
180
+ for (const v of result.violations) {
181
+ console.log(formatCheck(v));
182
+ }
183
+
184
+ console.log(`\n ${BOLD}Resumen:${RESET} ${result.summary}`);
185
+ if (result.hasCritical) {
186
+ console.log(` ${RED}⚠ Se requiere acción correctiva antes de continuar${RESET}`);
187
+ }
188
+ console.log();
189
+ }
190
+
191
+ async function main() {
192
+ const args = process.argv.slice(2);
193
+ const isJson = args.includes("--json");
194
+ const strict = args.includes("--strict");
195
+ const reminder = args.includes("--reminder");
196
+ const isDiff = args.includes("--diff");
197
+
198
+ let files;
199
+ if (isDiff) {
200
+ files = getChangedFiles();
201
+ } else {
202
+ // Files passed as arguments
203
+ files = args.filter(a => !a.startsWith("--"));
204
+ if (files.length === 0) {
205
+ files = getChangedFiles();
206
+ }
207
+ }
208
+
209
+ const result = await postToolCheck(files, { strict, reminder });
210
+
211
+ if (isJson) {
212
+ console.log(formatJson(result));
213
+ } else {
214
+ printResult(result, { reminder });
215
+ }
216
+
217
+ if (strict && (result.hasCritical || result.hasErrors)) {
218
+ process.exit(1);
219
+ }
220
+ }
221
+
222
+ if (process.argv[1]?.endsWith("posttool.mjs")) {
223
+ main().catch(console.error);
224
+ }
@@ -1,64 +1,142 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { join } from "path";
3
5
  import { buildContext } from "./context.mjs";
4
6
 
7
+ const ROOT = process.cwd();
8
+
5
9
  const PROFILES = [
6
10
  {
7
11
  name: "express-mongodb",
8
- match: (ctx) =>
9
- ctx.framework === "Express" &&
10
- ctx.database === "MongoDB" &&
11
- ctx.orm === "Mongoose",
12
+ match: (ctx) => ctx.framework === "Express" && ctx.database === "MongoDB" && ctx.orm === "Mongoose",
13
+ deps: ["express", "mongoose"],
14
+ opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
12
15
  },
13
16
  {
14
17
  name: "express-prisma",
15
- match: (ctx) =>
16
- ctx.framework === "Express" &&
17
- ctx.orm === "Prisma",
18
+ match: (ctx) => ctx.framework === "Express" && ctx.orm === "Prisma",
19
+ deps: ["express", "@prisma/client"],
20
+ opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
18
21
  },
19
22
  {
20
23
  name: "express-postgres",
21
- match: (ctx) =>
22
- ctx.framework === "Express" &&
23
- ctx.database === "PostgreSQL" &&
24
- ctx.orm === "native",
24
+ match: (ctx) => ctx.framework === "Express" && ctx.database === "PostgreSQL" && ctx.orm === "native",
25
+ deps: ["express", "pg"],
26
+ opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
27
+ },
28
+ {
29
+ name: "express-drizzle",
30
+ match: (ctx) => ctx.framework === "Express" && ctx.orm === "Drizzle",
31
+ deps: ["express", "drizzle-orm"],
32
+ opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
25
33
  },
26
34
  {
27
35
  name: "fastify-postgres",
28
- match: (ctx) =>
29
- ctx.framework === "Fastify" &&
30
- ctx.database === "PostgreSQL" &&
31
- ctx.orm === "Prisma",
36
+ match: (ctx) => ctx.framework === "Fastify" && ctx.database === "PostgreSQL" && ctx.orm === "Prisma",
37
+ deps: ["fastify", "@prisma/client"],
38
+ opts: { test: "vitest|jest", validation: "zod|typebox", logger: "pino" },
39
+ },
40
+ {
41
+ name: "fastify-prisma",
42
+ match: (ctx) => ctx.framework === "Fastify" && ctx.orm === "Prisma",
43
+ deps: ["fastify", "@prisma/client"],
44
+ opts: { test: "vitest|jest", validation: "zod|typebox", logger: "pino" },
45
+ },
46
+ {
47
+ name: "fastify-mongodb",
48
+ match: (ctx) => ctx.framework === "Fastify" && ctx.database === "MongoDB" && ctx.orm === "Mongoose",
49
+ deps: ["fastify", "mongoose"],
50
+ opts: { test: "vitest|jest", validation: "zod|typebox", logger: "pino" },
32
51
  },
33
52
  {
34
53
  name: "nestjs-prisma",
35
- match: (ctx) =>
36
- ctx.framework === "NestJS" &&
37
- ctx.orm === "Prisma",
54
+ match: (ctx) => ctx.framework === "NestJS" && ctx.orm === "Prisma",
55
+ deps: ["@nestjs/core", "@prisma/client"],
56
+ opts: { test: "vitest|jest", validation: "class-validator|zod" },
57
+ },
58
+ {
59
+ name: "nestjs-postgres",
60
+ match: (ctx) => ctx.framework === "NestJS" && ctx.database === "PostgreSQL" && (ctx.orm === "native" || ctx.orm === "TypeORM"),
61
+ deps: ["@nestjs/core", "pg"],
62
+ opts: { test: "vitest|jest", validation: "class-validator|zod" },
63
+ },
64
+ {
65
+ name: "nestjs-mongodb",
66
+ match: (ctx) => ctx.framework === "NestJS" && ctx.database === "MongoDB" && ctx.orm === "Mongoose",
67
+ deps: ["@nestjs/core", "mongoose"],
68
+ opts: { test: "vitest|jest", validation: "class-validator|zod" },
38
69
  },
39
70
  ];
40
71
 
72
+ function readPkg() {
73
+ try {
74
+ const raw = readFileSync(join(ROOT, "package.json"), "utf-8");
75
+ return JSON.parse(raw);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ function findInstalled(depPattern, pkg) {
82
+ if (!pkg) return false;
83
+ const all = { ...pkg.dependencies, ...pkg.devDependencies };
84
+ const parts = depPattern.split("|");
85
+ return parts.some((p) => all[p]);
86
+ }
87
+
41
88
  export function detectProfile(ctx) {
42
89
  for (const profile of PROFILES) {
43
90
  if (profile.match(ctx)) return profile.name;
44
91
  }
45
-
46
92
  if (ctx.framework !== "unknown") {
47
93
  return `${ctx.framework.toLowerCase()}-${ctx.database.toLowerCase()}`;
48
94
  }
49
-
50
95
  return "unknown";
51
96
  }
52
97
 
53
98
  export function detectProfileExtended(ctx) {
54
99
  const profile = detectProfile(ctx);
100
+ const profileDef = PROFILES.find((p) => p.name === profile);
55
101
 
102
+ const pkg = readPkg();
56
103
  const diStrategy = ctx.diStrategy || "manual";
57
104
  const hasPlatform = ctx.platform?.exists || false;
58
105
  const hasShared = ctx.shared?.exists || false;
59
106
  const hasInfra = ctx.infra?.exists || false;
60
107
  const platformComponents = ctx.platform?.components || [];
61
108
 
109
+ // Detección de librerías complementarias
110
+ const hasTest = pkg ? findInstalled("vitest|jest|mocha|ava", pkg) : false;
111
+ const hasValidation = pkg ? findInstalled("zod|joi|class-validator|typebox|yup", pkg) : false;
112
+ const hasLogger = pkg ? findInstalled("pino|winston|log4js", pkg) : false;
113
+ const hasAuth = pkg ? findInstalled("bcrypt|argon2|jsonwebtoken|jose|passport", pkg) : false;
114
+ const hasHelmet = pkg ? findInstalled("helmet|cors", pkg) : false;
115
+ const hasRateLimit = pkg ? findInstalled("express-rate-limit|rate-limiter-flexible|@fastify/rate-limit", pkg) : false;
116
+ const hasSwagger = pkg ? findInstalled("swagger-ui-express|@nestjs/swagger|@fastify/swagger|openapi", pkg) : false;
117
+ const hasEvents = pkg ? findInstalled("eventemitter3|kafkajs|amqplib|ioredis|bull|bullmq|@nestjs/event-emitter", pkg) : false;
118
+
119
+ // Sugerencias de paquetes faltantes
120
+ const suggestions = [];
121
+ if (!hasTest) suggestions.push("Agregar framework de testing: vitest o jest");
122
+ if (!hasValidation) suggestions.push("Agregar validación: zod o class-validator");
123
+ if (!hasLogger) suggestions.push("Agregar logger: pino (recomendado) o winston");
124
+ if (!hasAuth) suggestions.push("Agregar autenticación: bcrypt + jsonwebtoken o jose");
125
+ if (!hasHelmet) suggestions.push("Agregar seguridad HTTP: helmet + cors");
126
+ if (!hasRateLimit) suggestions.push("Agregar rate limiting: express-rate-limit o rate-limiter-flexible");
127
+ if (!hasSwagger) suggestions.push("Agregar documentación API: OpenAPI con zod-to-openapi o @nestjs/swagger");
128
+ if (!hasEvents) suggestions.push("Agregar event bus: eventemitter3 (local) o kafkajs (distribuido)");
129
+
130
+ // Validación de dependencias del perfil detectado
131
+ const depIssues = [];
132
+ if (profileDef && pkg) {
133
+ for (const dep of profileDef.deps) {
134
+ if (!findInstalled(dep, pkg)) {
135
+ depIssues.push(`Falta dependencia requerida: ${dep}`);
136
+ }
137
+ }
138
+ }
139
+
62
140
  return {
63
141
  profile,
64
142
  hasPlatform,
@@ -72,6 +150,18 @@ export function detectProfileExtended(ctx) {
72
150
  shared: hasShared,
73
151
  infra: hasInfra,
74
152
  },
153
+ complementary: {
154
+ test: hasTest,
155
+ validation: hasValidation,
156
+ logger: hasLogger,
157
+ auth: hasAuth,
158
+ helmet: hasHelmet,
159
+ rateLimit: hasRateLimit,
160
+ swagger: hasSwagger,
161
+ events: hasEvents,
162
+ },
163
+ suggestions,
164
+ depIssues,
75
165
  };
76
166
  }
77
167
 
@@ -80,8 +170,22 @@ async function main() {
80
170
  const profile = detectProfile(ctx);
81
171
  const extended = detectProfileExtended(ctx);
82
172
  const args = process.argv.slice(2);
173
+
83
174
  if (args.includes("--extended")) {
84
175
  console.log(JSON.stringify(extended, null, 2));
176
+ } else if (args.includes("--suggest")) {
177
+ if (extended.suggestions.length === 0 && extended.depIssues.length === 0) {
178
+ console.log(`✓ Perfil "${profile}" — todas las dependencias y complementos detectados`);
179
+ } else {
180
+ if (extended.depIssues.length > 0) {
181
+ console.log(`⚠ Perfil "${profile}" — problemas de dependencias:`);
182
+ extended.depIssues.forEach((d) => console.log(` ✘ ${d}`));
183
+ }
184
+ if (extended.suggestions.length > 0) {
185
+ console.log(`\n💡 Sugerencias para complementar "${profile}":`);
186
+ extended.suggestions.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
187
+ }
188
+ }
85
189
  } else {
86
190
  console.log(profile);
87
191
  }