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 +7 -0
- package/dist/cli.js +79 -1
- package/dist/output/report.js +135 -0
- package/package.json +2 -2
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")
|
package/dist/output/report.js
CHANGED
|
@@ -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.
|
|
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",
|