pull-request-split-advisor 3.1.2 → 3.2.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.
package/README.md CHANGED
@@ -27,6 +27,8 @@ La capa de IA ofrece tres puntos de enriquecimiento: **mejora de mensajes de com
27
27
 
28
28
  La v3.0.0 también mejora el **apply**: rama de respaldo con snapshot completo del working tree, re-prompt en nombres duplicados en lugar de rollback, y nomenclatura de ramas con mayúsculas en la descripción (p. ej. `feature/TEAM-123-PRUEBA-IA`). Si la IA no está configurada o falla en cualquier punto, la herramienta continúa en modo heurístico sin interrupciones.
29
29
 
30
+ A partir de la **v3.2.0**, se añade el subcomando **`score`** que calcula y muestra el score del estado actual de la rama sin dividir en PRs, exportando también un reporte HTML independiente (`pr-split-score.html`) con desglose de métricas, valores brutos y detalle de archivos.
31
+
30
32
  A partir de la **v3.1.0**, el proyecto incluye una **suite completa de tests** (Vitest, 199 tests, 12 suites) que cubre todos los módulos core, AI, git y configuración con mocks de dependencias externas (git, fs). A partir de la **v3.1.1**, el proveedor `copilot` solo puede activarse dentro de **Visual Studio Code** — fuera del IDE se emite un aviso y se desactiva automáticamente sin interrumpir el análisis.
31
33
 
32
34
  ---
@@ -38,6 +40,7 @@ A partir de la **v3.1.0**, el proyecto incluye una **suite completa de tests** (
38
40
  - Cálculo de score de calidad por rama (0–5) con factores ponderados
39
41
  - Generación de nombres de rama y mensajes de commit convencionales
40
42
  - Reporte HTML interactivo exportado automáticamente (8 secciones: hero ejecutivo, resumen de cambios, TOC con barras de score, detalle de archivos, dependencias, bloques, plan de commits con línea de tiempo y comandos git listos para ejecutar) — ver [formato completo](docs/FEATURES.md#reporte-html-interactivo)
43
+ - **`score`**: subcomando (v3.2.0+) que calcula el score del estado actual sin dividir y genera `pr-split-score.html` con desglose completo de métricas
41
44
  - Exportación del plan en formato JSON para integraciones externas
42
45
  - Historial de scores con seguimiento entre análisis
43
46
  - Validaciones de nomenclatura de ramas y estado del working tree
@@ -74,6 +77,10 @@ pr-split-advisor --apply
74
77
  # Especificar rama base de forma explícita
75
78
  pr-split-advisor --base develop
76
79
 
80
+ # Ver el score actual de la rama sin dividir en PRs
81
+ pr-split-advisor score
82
+ pr-split-advisor score --base develop
83
+
77
84
  # Consultar historial de análisis anteriores
78
85
  pr-split-advisor --stats
79
86
 
package/dist/cli.js CHANGED
@@ -30,9 +30,10 @@ import { APP_BIN, APP_NAME, APP_VERSION } from "./shared/constants.js";
30
30
  import { applyPlan } from "./git/executor.js";
31
31
  import { detectRemote, fetchQuiet, getCurrentBranch, localAheadCount, localBehindCount, remoteTrackingExists, requireCleanIndex, requireGitRepo } from "./git/git.js";
32
32
  import { buildBlocks, buildDependencyEdges, findBestPlan, gatherChangedFiles, getFileStats } from "./core/planner.js";
33
- import { exportJson, writeHtmlReport } from "./output/report.js";
33
+ import { exportJson, writeHtmlReport, writeScoreHtmlReport } from "./output/report.js";
34
34
  import { appendHistory, readHistory } from "./core/history.js";
35
35
  import { buildSuggestedMessage } from "./core/commit-planner.js";
36
+ import { scorePullRequest } from "./core/scoring.js";
36
37
  import { ui, closeReadlineInterface } from "./output/ui.js";
37
38
  import { changeTypeLabel } from "./shared/utils.js";
38
39
  import { aiEnrichPlans } from "./ai/enricher.js";
@@ -417,6 +418,83 @@ async function main() {
417
418
  }
418
419
  closeReadlineInterface();
419
420
  });
421
+ // ─── Subcomando: score ───────────────────────────────────────────────────
422
+ program
423
+ .command("score")
424
+ .description("Muestra el score del estado actual de la rama sin dividir")
425
+ .option("-b, --base <branch>", "rama base para calcular los cambios")
426
+ .action(async (opts) => {
427
+ const config = loadConfig();
428
+ try {
429
+ requireGitRepo();
430
+ }
431
+ catch (error) {
432
+ ui.error(error.message);
433
+ closeReadlineInterface();
434
+ process.exit(1);
435
+ }
436
+ const baseBranch = opts.base ?? config.baseBranch ?? "main";
437
+ const currentBranch = getCurrentBranch();
438
+ config.runtimeExcludedFiles = [
439
+ config.jsonOutputFile,
440
+ "pr-split-advisor.config.json",
441
+ "pr-split-report.html",
442
+ config.historyFile
443
+ ].filter(Boolean);
444
+ ui.spinner.start("Calculando score del estado actual...");
445
+ const changedFiles = gatherChangedFiles(config, baseBranch);
446
+ if (!changedFiles.length) {
447
+ ui.spinner.stop();
448
+ ui.warn("No hay cambios respecto a la rama base.");
449
+ closeReadlineInterface();
450
+ return;
451
+ }
452
+ const fileStats = getFileStats(changedFiles, baseBranch);
453
+ ui.spinner.stop("Calculo completado.");
454
+ const totalFiles = fileStats.length;
455
+ const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
456
+ const aheadCommits = localAheadCount(baseBranch);
457
+ const commitCount = Math.max(aheadCommits, 1);
458
+ const filesPerCommit = Math.round((totalFiles / commitCount) * 10) / 10;
459
+ const avgLinesPerCommit = Math.round(totalLines / commitCount);
460
+ const result = scorePullRequest({ commitCount, filesPerCommit, avgLinesPerCommit, totalLinesChanged: totalLines }, config);
461
+ const scoreColor = ui.scoreColor(result.complexity, config.targetScore);
462
+ const statusBadge = result.complexity >= config.targetScore
463
+ ? ui.badge("ÓPTIMO", "green")
464
+ : result.complexity >= config.targetScore - 1
465
+ ? ui.badge("ACEPTABLE", "yellow")
466
+ : ui.badge("RIESGO", "red");
467
+ const scoreHtmlFile = "pr-split-score.html";
468
+ writeScoreHtmlReport(scoreHtmlFile, {
469
+ currentBranch,
470
+ baseBranch,
471
+ config,
472
+ fileStats,
473
+ commitCount,
474
+ filesPerCommit,
475
+ avgLinesPerCommit,
476
+ result
477
+ });
478
+ ui.ok(`Reporte HTML generado: ${scoreHtmlFile}`);
479
+ ui.card(`Score actual de la rama: ${currentBranch}`, [
480
+ `${statusBadge}`,
481
+ "",
482
+ `Rama base: ${baseBranch}`,
483
+ `Archivos cambiados: ${totalFiles}`,
484
+ `Líneas totales: ${totalLines}`,
485
+ `Commits adelantados: ${commitCount}`,
486
+ `Archivos por commit: ${filesPerCommit}`,
487
+ `Líneas por commit (avg):${avgLinesPerCommit}`,
488
+ "",
489
+ `Score: ${ui.score(result.complexity, config.targetScore)}`,
490
+ `Score objetivo: ${config.targetScore}`,
491
+ "",
492
+ ...Object.values(result.metrics).map((m) => ` ${m.label}: valor=${m.rawValue} pts=${m.points} pond=${m.weightedScore}`),
493
+ "",
494
+ `Recomendación: ${result.recommendation}`
495
+ ], scoreColor);
496
+ closeReadlineInterface();
497
+ });
420
498
  // ─── Subcomando: config ──────────────────────────────────────────────────
421
499
  program
422
500
  .command("config")
@@ -706,6 +706,141 @@ function handleCopy(btn) {
706
706
  export function writeHtmlReport(filePath, input) {
707
707
  writeFileSync(filePath, renderHtmlReport(input), "utf8");
708
708
  }
709
+ /**
710
+ * Genera el reporte HTML del subcomando `score` como string.
711
+ * Muestra el score actual de la rama sin dividir, con desglose de métricas y detalle de archivos.
712
+ */
713
+ export function renderScoreHtmlReport(input) {
714
+ const { currentBranch, baseBranch, config, fileStats, commitCount, filesPerCommit, avgLinesPerCommit, result } = input;
715
+ const totalFiles = fileStats.length;
716
+ const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
717
+ const totalAdditions = fileStats.reduce((sum, f) => sum + f.additions, 0);
718
+ const totalDeletions = fileStats.reduce((sum, f) => sum + f.deletions, 0);
719
+ const maxLinesForBar = totalFiles > 0 ? Math.max(...fileStats.map((f) => f.lines)) : 1;
720
+ const label = scoreLabel(result.complexity, config.targetScore);
721
+ const warnThreshold = config.targetScore > 4 ? 4 : Math.max(0, config.targetScore - 1);
722
+ const scoreClass = result.complexity >= config.targetScore ? "green" : result.complexity >= warnThreshold ? "yellow" : "red";
723
+ const timestamp = new Date().toLocaleString("es-PE", {
724
+ year: "numeric", month: "long", day: "numeric",
725
+ hour: "2-digit", minute: "2-digit"
726
+ });
727
+ const metricRows = Object.values(result.metrics).map((m) => {
728
+ const ptsPct = Math.round((m.points / 5) * 100);
729
+ const barColor = m.points >= 4 ? "#16a34a" : m.points >= 3 ? "#d97706" : "#dc2626";
730
+ const ptsClass = m.points >= 4 ? "pts-good" : m.points >= 3 ? "pts-warn" : "pts-bad";
731
+ return `<tr>
732
+ <td><code class="metric-code">${esc(m.code)}</code></td>
733
+ <td class="metric-label">${esc(m.label)}</td>
734
+ <td class="metric-value">${m.rawValue}</td>
735
+ <td>
736
+ <div class="pts-bar-wrap"><div class="pts-bar" style="width:${ptsPct}%;background:${barColor}"></div></div>
737
+ <span class="${ptsClass}">${m.points} / 5</span>
738
+ </td>
739
+ <td class="metric-weight">${(m.weight * 100).toFixed(0)}%</td>
740
+ <td class="metric-contrib">${m.weightedScore.toFixed(3)}</td>
741
+ </tr>`;
742
+ }).join("");
743
+ return `<!doctype html>
744
+ <html lang="es">
745
+ <head>
746
+ <meta charset="utf-8" />
747
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
748
+ <title>Score actual – ${esc(currentBranch)}</title>
749
+ <style>${buildReportStyles()}</style>
750
+ </head>
751
+ <body>
752
+ <div class="container">
753
+
754
+ <header class="hero">
755
+ <div class="hero-top">
756
+ <div class="hero-title-area">
757
+ <h1>📊 Score actual de la rama</h1>
758
+ <p>Análisis del estado presente sin dividir en múltiples PRs.</p>
759
+ </div>
760
+ <div class="hero-timestamp">📅 ${timestamp}</div>
761
+ </div>
762
+ <div class="hero-grid">
763
+ <div class="hero-item"><div class="label">Rama analizada</div><div class="value">⎇ ${esc(currentBranch)}</div></div>
764
+ <div class="hero-item"><div class="label">Rama base</div><div class="value">⎇ ${esc(baseBranch)}</div></div>
765
+ <div class="hero-item"><div class="label">Score actual</div><div class="value">${result.complexity.toFixed(2)}</div></div>
766
+ <div class="hero-item"><div class="label">Score objetivo</div><div class="value">${config.targetScore} / 5.00</div></div>
767
+ <div class="hero-item"><div class="label">Archivos</div><div class="value">${totalFiles}</div></div>
768
+ <div class="hero-item"><div class="label">Líneas totales</div><div class="value">${totalLines}</div></div>
769
+ <div class="hero-item"><div class="label">Commits adelantados</div><div class="value">${commitCount}</div></div>
770
+ <div class="hero-item"><div class="label">Estado</div><div class="value score-label-${scoreClass}">${esc(label)}</div></div>
771
+ </div>
772
+ </header>
773
+
774
+ <section class="section">
775
+ <h2 class="section-heading">🎯 Score</h2>
776
+ <div class="card" style="display:flex;align-items:center;gap:32px;padding:24px">
777
+ ${buildScoreGaugeSvg(result.complexity, config.targetScore)}
778
+ <div>
779
+ <div style="font-size:2rem;font-weight:800;color:${result.complexity >= config.targetScore ? "#16a34a" : result.complexity >= warnThreshold ? "#d97706" : "#dc2626"}">${result.complexity.toFixed(2)} / 5.00</div>
780
+ <div class="badge ${scoreClass}" style="margin-top:8px">${esc(label)}</div>
781
+ <p style="margin-top:12px;color:#64748b">${esc(result.recommendation)}</p>
782
+ </div>
783
+ </div>
784
+ </section>
785
+
786
+ <section class="section">
787
+ <h2 class="section-heading">📊 Desglose de métricas</h2>
788
+ <table class="metrics-table">
789
+ <thead>
790
+ <tr><th>Cód.</th><th>Métrica</th><th>Valor medido</th><th>Puntos</th><th>Peso</th><th>Aporte</th></tr>
791
+ </thead>
792
+ <tbody>
793
+ ${metricRows}
794
+ <tr class="metrics-total-row">
795
+ <td colspan="5" style="text-align:right;font-weight:700;">Score total</td>
796
+ <td class="metric-total-value">${result.complexity.toFixed(3)}</td>
797
+ </tr>
798
+ </tbody>
799
+ </table>
800
+ </section>
801
+
802
+ <section class="section">
803
+ <h2 class="section-heading">📈 Valores brutos usados</h2>
804
+ <div class="summary-grid">
805
+ <div class="stat-card blue"><div class="label">Commits</div><div class="value">${commitCount}</div></div>
806
+ <div class="stat-card blue"><div class="label">Archivos / commit</div><div class="value">${filesPerCommit}</div></div>
807
+ <div class="stat-card blue"><div class="label">Líneas / commit (avg)</div><div class="value">${avgLinesPerCommit}</div></div>
808
+ <div class="stat-card blue"><div class="label">Total de líneas</div><div class="value">${totalLines}</div></div>
809
+ <div class="stat-card green"><div class="label">Líneas agregadas</div><div class="value">+${totalAdditions}</div></div>
810
+ <div class="stat-card red"><div class="label">Líneas eliminadas</div><div class="value">-${totalDeletions}</div></div>
811
+ </div>
812
+ </section>
813
+
814
+ <section class="section">
815
+ <h2 class="section-heading">📄 Detalle de archivos</h2>
816
+ <table>
817
+ <thead>
818
+ <tr><th>Tipo</th><th>Archivo</th><th>Líneas</th><th>Cambios (+/-)</th><th>Prioridad</th></tr>
819
+ </thead>
820
+ <tbody>
821
+ ${fileStats.map((f) => buildFilesBarsRow(f, maxLinesForBar)).join("")}
822
+ </tbody>
823
+ </table>
824
+ </section>
825
+
826
+ <footer class="footer">
827
+ <p>Generado por <strong>${APP_REPORT_TITLE}</strong> · ${timestamp}</p>
828
+ <p>Rama: <strong>${esc(currentBranch)}</strong> vs <strong>${esc(baseBranch)}</strong> · Score: <strong>${result.complexity.toFixed(2)}</strong> · Objetivo: <strong>${config.targetScore}</strong></p>
829
+ </footer>
830
+
831
+ </div>
832
+ </body>
833
+ </html>`;
834
+ }
835
+ /**
836
+ * Genera el reporte HTML de score puntual y lo escribe en disco.
837
+ *
838
+ * @param filePath - Ruta de destino del archivo HTML.
839
+ * @param input - Datos del análisis de score.
840
+ */
841
+ export function writeScoreHtmlReport(filePath, input) {
842
+ writeFileSync(filePath, renderScoreHtmlReport(input), "utf8");
843
+ }
709
844
  /**
710
845
  * Genera el JSON serializado del plan.
711
846
  * @param plans - Lista de planes de rama.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pull-request-split-advisor",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "description": "CLI that analyses your Git working tree and suggests how to split changes into smaller, reviewable pull requests and commits.",
5
5
  "keywords": [
6
6
  "git",
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "build:styles": "node scripts/compile-styles.cjs",
36
- "build": "npm run build:styles && tsc -p tsconfig.json",
36
+ "build": "npm run build:styles && tsc -p tsconfig.build.json",
37
37
  "prepare": "npm run build",
38
38
  "dev": "tsx src/cli.ts",
39
39
  "start": "node dist/cli.js",