@ronaldjdevfs/forge 1.0.1 → 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 +3 -3
  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,481 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * assay.mjs — Ensayo arquitectónico multi-persona.
5
+ *
6
+ * Lee el último reporte de inspect (score, violaciones, grafo, contexto)
7
+ * y genera un ensayo estructurado desde la perspectiva de 5 personas
8
+ * expertas en arquitectura backend.
9
+ *
10
+ * Uso:
11
+ * node assay.mjs → ensayo completo (modo texto)
12
+ * node assay.mjs --json → salida JSON
13
+ * node assay.mjs --persona=bezos → solo una persona
14
+ * node assay.mjs --last → re-generar desde último estado
15
+ * node assay.mjs --save → persistir en .forge/assay/latest.md
16
+ * node assay.mjs history → mostrar historial de ensayos
17
+ */
18
+
19
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from "fs";
20
+ import { join, basename } from "path";
21
+ import { loadState, loadHistory } from "./forge-config.mjs";
22
+ import { buildGraph } from "./graph.mjs";
23
+ import {
24
+ CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM, GRAY,
25
+ formatJson, formatViolation,
26
+ } from "./formatter.mjs";
27
+
28
+ const ROOT = process.cwd();
29
+ const ASSAY_DIR = join(ROOT, ".forge", "assay");
30
+
31
+ // ── Personas ──
32
+
33
+ const PERSONAS = [
34
+ {
35
+ id: "bezos",
36
+ name: "Jeff Bezos",
37
+ role: "Arquitecto de Escalabilidad y Autonomía",
38
+ focus: [
39
+ "Acoplamiento entre features (R8) y entre capas (R1-R7)",
40
+ "Autonomía de equipos para evolucionar features independientemente",
41
+ "Contratos de API explícitos vs imports directos",
42
+ "Ciclos de dependencias (R9) que impiden despliegue independiente",
43
+ ],
44
+ style: "Directo, obsesionado con desacoplamiento, citando el API Mandate.",
45
+ getOpinion: (report, graph) => {
46
+ const parts = [];
47
+ const violations = report.violations || [];
48
+ const r1 = violations.filter(v => v.rule === "R1" || v.label?.includes("R1"));
49
+ const r8 = violations.filter(v => v.rule === "R8" || v.label?.includes("R8"));
50
+ const r9 = violations.filter(v => v.rule === "R9" || v.label?.includes("R9"));
51
+ const cycles = graph?.stats?.hasCycles;
52
+
53
+ if (cycles || r9.length > 0) {
54
+ parts.push("Hay ciclos de dependencias. Esto es inaceptable. Un equipo no puede desplegar su feature sin coordinar con otros. Rompan el ciclo ahora.");
55
+ }
56
+ if (r8.length > 0) {
57
+ parts.push(`${r8.length} feature(s) importan directamente a otros features. Cada equipo debería poder desarrollar y desplegar su feature sin conocer los internos de los demás. Si necesitan comunicarse, que sea por contratos (interfaces) o eventos, no por imports directos. Es como si un equipo de Amazon tuviera que leer el código fuente de otro equipo para llamar a su servicio.`);
58
+ }
59
+ if (r1.length > 0) {
60
+ parts.push(`${r1.length} feature(s) importan infraestructura directamente. Esto los ata a implementaciones concretas. Un cambio de base de datos no debería requiring cambios en múltiples features.`);
61
+ }
62
+
63
+ if (parts.length === 0) {
64
+ if (report.total > 0) {
65
+ parts.push(`La arquitectura tiene un score de ${report.total}/${report.max}. No hay violaciones graves de acoplamiento, pero siempre se puede mejorar la autonomía de los equipos.`);
66
+ } else {
67
+ parts.push("No hay datos de auditoría para evaluar. Ejecuten forge inspect primero.");
68
+ }
69
+ }
70
+
71
+ return parts.join("\n\n");
72
+ },
73
+ },
74
+
75
+ {
76
+ id: "fowler",
77
+ name: "Martin Fowler",
78
+ role: "Refinador de Patrones y Deuda Técnica",
79
+ focus: [
80
+ "Health del grafo arquitectónico y dirección de dependencias",
81
+ "Code smells en estructura de capas (domain importa infra, etc.)",
82
+ "Coherencia en naming y convenciones",
83
+ "Oportunidades de refactoring evolutivo",
84
+ ],
85
+ style: "Analítico, cita patrones, sugiere refactorings incrementales sin reescrituras.",
86
+ getOpinion: (report, graph) => {
87
+ const parts = [];
88
+ const violations = report.violations || [];
89
+ const naming = violations.filter(v => v.category === "naming" || v.label?.includes("Naming"));
90
+ const layers = violations.filter(v => v.category === "layers");
91
+ const platform = violations.filter(v => v.category === "platform");
92
+ const deps = violations.filter(v => v.category === "dependencies");
93
+
94
+ if (naming.length > 0) {
95
+ parts.push(`Encontré ${naming.length} violaciones de naming. El naming consistente es la forma más barata de reducir la fricción cognitiva en un código base. Sugiero corregirlas de a una por feature, no todas a la vez.`);
96
+ }
97
+ if (layers.length > 0) {
98
+ const layerIssues = layers.filter(l => !l.pass);
99
+ parts.push(`${layerIssues.length} problema(s) en la estructura de capas. Recuerden: la dirección de las dependencias es el esqueleto de la arquitectura. Si domain importa infra, han perdido la batalla antes de empezar.`);
100
+ }
101
+ if (platform.length > 0 && !platform[0]?.pass) {
102
+ parts.push("La capa platform está ausente o incompleta. Sin una base técnica compartida, cada feature terminará reinventando la misma infraestructura. No necesitan todos los componentes de golpe — empiecen con config y logger, y evolucionen desde ahí.");
103
+ }
104
+ if (deps.length > 0) {
105
+ const depIssues = deps.filter(d => !d.pass);
106
+ parts.push(`La salud de dependencias tiene ${depIssues.length} issue(s). Un dependency health score bajo es una señal temprana de que el sistema se está petrificando. Abordenlo temprano, el costo de corregir dependencias mal dirigidas crece exponencialmente.`);
107
+ }
108
+
109
+ if (parts.length === 0) {
110
+ parts.push("La estructura de capas se ve razonable. Sigan prestando atención a la dirección de las dependencias en cada nuevo feature.");
111
+ }
112
+
113
+ return parts.join("\n\n");
114
+ },
115
+ },
116
+
117
+ {
118
+ id: "hacker",
119
+ name: "El Hacker",
120
+ role: "Pragmático y Simplificador",
121
+ focus: [
122
+ "Over-engineering y complejidad innecesaria",
123
+ "Abstracciones que no agregan valor",
124
+ "Costo de mantenimiento vs beneficio",
125
+ "Lo que realmente importa para que el sistema funcione",
126
+ ],
127
+ style: "Directo, irreverente, sin paciencia para capas vacías o arquitectura por la arquitectura misma.",
128
+ getOpinion: (report, graph) => {
129
+ const parts = [];
130
+ const violations = report.violations || [];
131
+ const structure = violations.filter(v => v.category === "structure");
132
+ const decorators = violations.filter(v => v.category === "decorators");
133
+
134
+ // Check for empty features / over-engineered structure
135
+ if (structure.some(s => !s.pass && s.label?.includes("domain/"))) {
136
+ parts.push("Veo features con estructura pero sin implementación real. Tener las carpetitas ordenadas no es arquitectura. Si un feature tiene domain/ pero ninguna entidad con lógica de negocio, es humo. Prefiero un monolito bien escrito que 20 microservicios vacíos.");
137
+ }
138
+
139
+ if (decorators.some(d => !d.pass && d.label?.includes("@injectable"))) {
140
+ parts.push(`Faltan decoradores @injectable() en algunos archivos. Si van a usar DI, úsenla bien. Si no, tal vez pregúntense si realmente necesitan tsyringe para este proyecto. Para proyectos pequeños, un simple contenedor manual basta.`);
141
+ }
142
+
143
+ const r9 = violations.filter(v => v.rule === "R9" || v.label?.includes("R9"));
144
+ if (r9.length > 0) {
145
+ parts.push("Ciclos de dependencias. Esto es una señal de que están forcejeando contra la arquitectura en lugar de dejarla fluir. A veces la solución más simple es fusionar los dos features en uno en lugar de crear interfaces rebuscadas para romper el ciclo.");
146
+ }
147
+
148
+ if (parts.length === 0) {
149
+ parts.push("No veo sobre-ingeniería evidente. El sistema parece proporcionado. Pero recuerden: la mejor arquitectura es la que pueden cambiar sin miedo.");
150
+ }
151
+
152
+ return parts.join("\n\n");
153
+ },
154
+ },
155
+
156
+ {
157
+ id: "pm",
158
+ name: "Alex",
159
+ role: "Product Manager Técnico",
160
+ focus: [
161
+ "Velocidad de entrega de features",
162
+ "Costo de oportunidad de la deuda técnica",
163
+ "Impacto en el roadmap",
164
+ "Riesgo de no entregar a tiempo por rigidez arquitectónica",
165
+ ],
166
+ style: "Orientado a resultados, traduce violaciones técnicas a riesgos de negocio. Corta cuando algo no suma valor al usuario.",
167
+ getOpinion: (report, graph) => {
168
+ const parts = [];
169
+ const violations = report.violations || [];
170
+ const critical = violations.filter(v => v.severity === "CRITICAL");
171
+ const errors = violations.filter(v => v.severity === "ERROR");
172
+
173
+ if (critical.length > 0) {
174
+ parts.push(`Hay ${critical.length} violación(es) CRITICAL. Esto es un bloqueador de productividad. Cada día que estas violaciones persisten, los equipos pierden tiempo en workarounds en lugar de construir valor para el usuario. Mi recomendación: dedicar un sprint a resolverlas. El ROI es inmediato.`);
175
+ } else if (errors.length > 0) {
176
+ parts.push(`${errors.length} violación(es) ERROR. No bloquean el desarrollo hoy, pero van a ralentizar al equipo en 3-6 meses. La deuda técnica es como la deuda financiera: los intereses se acumulan. Sugiero asignar 1 día por sprint para reducción de deuda técnica.`);
177
+ }
178
+
179
+ const score = report.total || 0;
180
+ const maxScore = report.max || 140;
181
+ const pct = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
182
+
183
+ if (pct < 50) {
184
+ parts.push(`Score actual: ${pct}% (${report.grade || "F"}). Honestamente, esto no es sostenible. Si la arquitectura sigue así, el time-to-market de los próximos features se va a duplicar. Mi voto es que prioricen refactor ahora, antes de que el próximo gran feature dependa de esta base.`);
185
+ } else if (pct >= 80) {
186
+ parts.push(`Score: ${pct}% (${report.grade || "A/B"}). La arquitectura está en buena forma. El equipo puede enfocarse en entregar valor sin preocuparse por la base técnica. Solo mantengan las revisiones periódicas.`);
187
+ } else {
188
+ parts.push(`Score: ${pct}% (${report.grade || "C/D"}). Zona de atención. No es crítica, pero si ignoran esto, en 2-3 sprints van a empezar a sentir la fricción. Mi consejo: incluyan 1 o 2 de las recomendaciones en el próximo sprint.`);
189
+ }
190
+
191
+ const featCount = report.context?.features?.total || 0;
192
+ if (featCount === 0) {
193
+ parts.push("El proyecto no tiene features migrados. ¿Deberíamos preguntarnos si la arquitectura actual nos está frenando más de lo que nos ayuda? A veces la deuda técnica de una migración no justifica el beneficio.");
194
+ }
195
+
196
+ return parts.join("\n\n");
197
+ },
198
+ },
199
+
200
+ {
201
+ id: "senior",
202
+ name: "Dra. Carter",
203
+ role: "Arquitecta Senior — Deuda Técnica y Gobernanza",
204
+ focus: [
205
+ "Sostenibilidad arquitectónica a largo plazo (3-5 años)",
206
+ "Consistencia entre equipos y features",
207
+ "Evolución del grafo arquitectónico en el tiempo",
208
+ "Gobernanza: reglas, ownership, y cumplimiento",
209
+ ],
210
+ style: "Sistemática, piensa en horizontes de 3-5 años, lee el grafo como un mapa de salud organizacional.",
211
+ getOpinion: (report, graph) => {
212
+ const parts = [];
213
+ const violations = report.violations || [];
214
+ const ownership = violations.filter(v => v.category === "ownership");
215
+ const rules = violations.filter(v => v.category === "customRules");
216
+
217
+ // Analyze graph health
218
+ if (graph) {
219
+ const dh = graph.stats?.dependencyHealth;
220
+ if (dh !== undefined && dh < 70) {
221
+ parts.push(`El Dependency Health está en ${dh}%. En una organización con múltiples equipos, esto implica que la arquitectura no escala. Cuando dos equipos no pueden trabajar simultáneamente sin pisarse, el problema no es de gestión — es de arquitectura.`);
222
+ }
223
+ }
224
+
225
+ if (ownership.some(o => !o.pass)) {
226
+ parts.push("Hay componentes huérfanos o mal ubicados. Sin ownership explícito, estos componentes se convierten en tierra de nadie. Nadie los mejora, nadie los elimina, y con el tiempo se vuelven legacy. Mi recomendación: asignar ownership a cada componente y documentarlo.");
227
+ }
228
+
229
+ const totalViolations = violations.filter(v => !v.pass).length;
230
+ if (totalViolations > 5) {
231
+ parts.push(`${totalViolations} violaciones activas. Con este volumen, no se van a resolver solas. Propongo establecer una governance board semanal de 30 minutos para revisar la evolución del grafo y decidir qué violaciones abordar.`);
232
+ }
233
+
234
+ if (rules.length === 0 || rules.some(r => r.pass)) {
235
+ parts.push("Tienen reglas custom activas, lo cual es una buena señal de madurez. El siguiente paso es hacer cumplir estas reglas en CI, no solo en commands ad-hoc.");
236
+ } else {
237
+ parts.push("No veo reglas custom definidas. Sugiero crear un archivo .forge/rules.json con al menos 2-3 reglas específicas de su dominio para capturar conocimiento arquitectónico que las reglas genéricas R1-R9 no cubren.");
238
+ }
239
+
240
+ if (parts.length === 0) {
241
+ parts.push("La arquitectura tiene una base sólida. Mi preocupación principal es que mantengan esta salud a lo largo del tiempo. Establezcan auditorías periódicas (cada 2-3 meses) y documenten las decisiones arquitectónicas en ARCHITECTURE.md.");
242
+ }
243
+
244
+ return parts.join("\n\n");
245
+ },
246
+ },
247
+ ];
248
+
249
+ // ── Ensayo engine ──
250
+
251
+ function formatPersonaOutput(persona, opinion) {
252
+ const lines = [];
253
+ const line = "─".repeat(60);
254
+ lines.push(`\n${BOLD}${CYAN}──── ${persona.name}${RESET}${DIM} — ${persona.role}${RESET}`);
255
+ lines.push(`${DIM}${line}${RESET}`);
256
+ lines.push(`\n${opinion}\n`);
257
+ return lines.join("\n");
258
+ }
259
+
260
+ export function generateAssay(report, graph) {
261
+ const opinions = [];
262
+ for (const persona of PERSONAS) {
263
+ try {
264
+ const text = persona.getOpinion(report, graph);
265
+ opinions.push({
266
+ persona: { id: persona.id, name: persona.name, role: persona.role },
267
+ opinion: text,
268
+ });
269
+ } catch (e) {
270
+ opinions.push({
271
+ persona: { id: persona.id, name: persona.name, role: persona.role },
272
+ opinion: `(Error al generar opinión: ${e.message})`,
273
+ });
274
+ }
275
+ }
276
+ return opinions;
277
+ }
278
+
279
+ function printAssay(report, opinions, graph) {
280
+ const score = report.total || 0;
281
+ const maxScore = report.max || 140;
282
+ const pct = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
283
+ const grade = report.grade || (pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 65 ? "C" : pct >= 50 ? "D" : "F");
284
+ const vCount = report.violations?.filter(v => !v.pass).length || 0;
285
+
286
+ console.log(`\n${BOLD}${CYAN}═══ Forge Assay — Ensayo Arquitectónico ═══${RESET}`);
287
+ console.log(`${DIM}Score: ${score}/${maxScore} (${pct}%) — ${grade}${RESET}`);
288
+ console.log(`${DIM}Violaciones: ${vCount}${RESET}`);
289
+ console.log(`${DIM}Personas: ${PERSONAS.map(p => p.name).join(", ")}${RESET}`);
290
+ console.log(`${DIM}Fecha: ${new Date().toISOString().slice(0, 10)}${RESET}\n`);
291
+
292
+ for (const op of opinions) {
293
+ console.log(formatPersonaOutput(op.persona, op.opinion));
294
+ }
295
+
296
+ console.log(`${DIM}─`.repeat(60) + `${RESET}`);
297
+ console.log(`${DIM}forge assay --save para persistir este ensayo${RESET}\n`);
298
+ }
299
+
300
+ function getState() {
301
+ try {
302
+ const state = loadState();
303
+ return state || {};
304
+ } catch {
305
+ return {};
306
+ }
307
+ }
308
+
309
+ function getLastInspectReport() {
310
+ const state = getState();
311
+ // Try to load the latest inspect result from forge-config state
312
+ return {
313
+ total: state.lastScore ?? 0,
314
+ max: 140,
315
+ grade: state.lastGrade ?? "—",
316
+ violations: [],
317
+ context: {
318
+ features: {
319
+ total: state.totalFeatures ?? 0,
320
+ migrated: [],
321
+ legacy: [],
322
+ },
323
+ },
324
+ severityCounts: {},
325
+ };
326
+ }
327
+
328
+ function rebuildGraph() {
329
+ try {
330
+ return buildGraph(ROOT);
331
+ } catch {
332
+ return { nodes: [], edges: [], stats: { hasCycles: false, dependencyHealth: 100, totalNodes: 0, totalEdges: 0, violations: 0, riskScore: 0, health: "unknown" } };
333
+ }
334
+ }
335
+
336
+ // ── Persistence ──
337
+
338
+ function saveAssay(opinions, report) {
339
+ mkdirSync(ASSAY_DIR, { recursive: true });
340
+ const score = report.total || 0;
341
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
342
+ const filename = `assay-${ts}.md`;
343
+ const path = join(ASSAY_DIR, filename);
344
+
345
+ const lines = [];
346
+ lines.push(`# Forge Assay — ${new Date().toISOString().slice(0, 10)}`);
347
+ lines.push(`\nScore: ${report.total}/${report.max}`);
348
+ lines.push(`Violaciones: ${report.violations?.filter(v => !v.pass).length || 0}`);
349
+ lines.push(`\n---\n`);
350
+
351
+ for (const op of opinions) {
352
+ lines.push(`## ${op.persona.name} — ${op.persona.role}`);
353
+ lines.push(`\n${op.opinion}\n`);
354
+ }
355
+
356
+ writeFileSync(path, lines.join("\n"));
357
+ return path;
358
+ }
359
+
360
+ function listAssays() {
361
+ if (!existsSync(ASSAY_DIR)) return [];
362
+ return readdirSync(ASSAY_DIR)
363
+ .filter(f => f.startsWith("assay-") && f.endsWith(".md"))
364
+ .sort()
365
+ .reverse()
366
+ .map(f => ({
367
+ file: f,
368
+ path: join(ASSAY_DIR, f),
369
+ date: f.replace("assay-", "").replace(".md", "").replace(/-/g, ":").slice(0, 19),
370
+ }));
371
+ }
372
+
373
+ // ── CLI ──
374
+
375
+ async function main() {
376
+ const args = process.argv.slice(2);
377
+
378
+ if (args.includes("--help") || args.includes("-h")) {
379
+ console.log(`Uso: node assay.mjs [flags]`);
380
+ console.log(` (sin flags) → ensayo completo en texto`);
381
+ console.log(` --json → salida JSON`);
382
+ console.log(` --persona=<id> → solo una persona (bezos, fowler, hacker, pm, senior)`);
383
+ console.log(` --save → persistir ensayo en .forge/assay/`);
384
+ console.log(` history → listar ensayos anteriores`);
385
+ console.log(` read <file> → leer ensayo persistido`);
386
+ console.log(` --help → esta ayuda`);
387
+ return;
388
+ }
389
+
390
+ if (args.includes("history")) {
391
+ const assays = listAssays();
392
+ if (assays.length === 0) {
393
+ console.log("No hay ensayos previos en .forge/assay/");
394
+ } else {
395
+ console.log("── Ensayos anteriores ──");
396
+ for (const a of assays.slice(0, 10)) {
397
+ console.log(` ${a.date} — ${a.file}`);
398
+ }
399
+ }
400
+ return;
401
+ }
402
+
403
+ const readIdx = args.indexOf("read");
404
+ if (readIdx !== -1 && args[readIdx + 1]) {
405
+ const path = join(ASSAY_DIR, args[readIdx + 1]);
406
+ if (existsSync(path)) {
407
+ console.log(readFileSync(path, "utf-8"));
408
+ } else {
409
+ console.error(`No existe: ${path}`);
410
+ process.exit(1);
411
+ }
412
+ return;
413
+ }
414
+
415
+ const personaFilter = args.find(a => a.startsWith("--persona="));
416
+ const personaId = personaFilter?.split("=")[1];
417
+ const isJson = args.includes("--json");
418
+ const doSave = args.includes("--save");
419
+
420
+ // Load report from last inspect
421
+ const report = getLastInspectReport();
422
+ const graph = rebuildGraph();
423
+
424
+ // Run violations from detect
425
+ try {
426
+ const { detectFeaturesOnSrc, allChecks } = await import("./detect.mjs");
427
+ const features = detectFeaturesOnSrc();
428
+ const results = allChecks(features, graph, {});
429
+
430
+ let violations = [];
431
+ for (const [, cat] of Object.entries(results)) {
432
+ violations = violations.concat(cat.checks);
433
+ }
434
+ report.violations = violations;
435
+ report.severityCounts = {};
436
+ for (const v of violations) {
437
+ if (!v.pass) {
438
+ report.severityCounts[v.severity] = (report.severityCounts[v.severity] || 0) + 1;
439
+ }
440
+ }
441
+ } catch { /* use empty violations */ }
442
+
443
+ // Generate opinions
444
+ let personas = PERSONAS;
445
+ if (personaId) {
446
+ const found = PERSONAS.find(p => p.id === personaId);
447
+ if (!found) {
448
+ console.error(`Persona no encontrada: ${personaId}. Opciones: ${PERSONAS.map(p => p.id).join(", ")}`);
449
+ process.exit(1);
450
+ }
451
+ personas = [found];
452
+ }
453
+
454
+ const opinions = [];
455
+ for (const persona of personas) {
456
+ try {
457
+ const text = persona.getOpinion(report, graph);
458
+ opinions.push({ persona: { id: persona.id, name: persona.name, role: persona.role }, opinion: text });
459
+ } catch (e) {
460
+ opinions.push({ persona: { id: persona.id, name: persona.name, role: persona.role }, opinion: `(Error: ${e.message})` });
461
+ }
462
+ }
463
+
464
+ if (isJson) {
465
+ console.log(formatJson({ report: { total: report.total, max: report.max, grade: report.grade, violationCount: report.violations?.length || 0 }, graph: graph?.stats || {}, opinions }));
466
+ return;
467
+ }
468
+
469
+ if (doSave) {
470
+ const saved = saveAssay(opinions, report);
471
+ console.log(`Ensayo guardado: ${saved}`);
472
+ }
473
+
474
+ printAssay(report, opinions, graph);
475
+ }
476
+
477
+ if (process.argv[1]?.endsWith("assay.mjs")) {
478
+ main().catch(console.error);
479
+ }
480
+
481
+ export { PERSONAS };