pull-request-split-advisor 3.2.3 → 3.2.4
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 +8 -2
- package/dist/cli.js +56 -6
- package/dist/git/git.js +45 -0
- package/dist/output/report.js +28 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,11 +27,17 @@ 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.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
|
+
|
|
30
32
|
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
33
|
|
|
32
|
-
A partir de la **v3.2.1**, cada archivo analizado recibe una **clasificación de origen** basada en su posición en el árbol git: `REMOTO` (cambios ya publicados en el remoto), `LOCAL` (commiteado solo en local), `WIP` (modificación sin commitear aún) o `NUEVO` (archivo sin rastrear). Esta información aparece como badge de color en la salida terminal y en la columna *Origen* de las tablas de archivos en ambos reportes HTML.
|
|
34
|
+
A partir de la **v3.2.1**, cada archivo analizado recibe una **clasificación de origen** basada en su posición en el árbol git: `REMOTO` (cambios ya publicados en el remoto), `LOCAL` (commiteado solo en local), `WIP` (modificación sin commitear aún) o `NUEVO` (archivo sin rastrear). Esta información aparece como badge de color en la salida terminal y en la columna *Origen* de las tablas de archivos en ambos reportes HTML.
|
|
33
35
|
|
|
34
|
-
A partir de la **v3.
|
|
36
|
+
A partir de la **v3.2.2**, el subcomando `score` evalúa **exclusivamente archivos WIP y sin rastrear**. Los archivos ya commiteados (LOCAL o REMOTO) se muestran en el reporte con su badge pero no influyen en las métricas. Si el working tree está limpio, el comando emite una advertencia y sale sin calcular un score incorrecto.
|
|
37
|
+
|
|
38
|
+
A partir de la **v3.2.3**, el reporte HTML principal (`pr-split-report.html`) y el resumen en terminal calculan todos sus agregados (hero, tarjetas de resumen, footer) **solo sobre archivos WIP**. Los archivos LOCAL y REMOTO siguen apareciendo en la tabla de detalle con sus badges, acompañados de un banner informativo que los identifica como referencias no incluidas en las métricas.
|
|
39
|
+
|
|
40
|
+
A partir de la **v3.2.4**, la herramienta detecta si la rama actual ya tiene commits adelantados respecto a la base antes de iniciar el análisis. En ese caso emite una **advertencia de integridad de cascada** con el comando exacto para crear la nueva rama correctamente: si existen ramas hermanas de la misma historia con commits adelantados, sugiere crear la siguiente rama desde la punta de la rama hermana más reciente (para continuar la cascada); si no, sugiere partir desde la base. El apply queda deshabilitado en esa ejecución para no romper el plan apilado. Esta versión también incluye tres correcciones internas: eliminación del doble sufijo `-wip` al sugerir nombres de rama, remplazo de `Math.max(...spread)` por `reduce` en la generación de reportes HTML (evita `RangeError` en repositorios con muchos archivos) y limpieza de un punto y coma duplicado en el cálculo de cobertura de tests.
|
|
35
41
|
|
|
36
42
|
---
|
|
37
43
|
|
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ import { parseAndValidateWorkingBranch } from "./git/branch-naming.js";
|
|
|
28
28
|
import { loadConfig } from "./config/config.js";
|
|
29
29
|
import { APP_BIN, APP_NAME, APP_VERSION } from "./shared/constants.js";
|
|
30
30
|
import { applyPlan } from "./git/executor.js";
|
|
31
|
-
import { detectRemote, fetchQuiet, getCurrentBranch, localAheadCount, localBehindCount, remoteTrackingExists, requireCleanIndex, requireGitRepo } from "./git/git.js";
|
|
31
|
+
import { detectRemote, fetchQuiet, findCascadeParent, getCurrentBranch, localAheadCount, localBehindCount, remoteTrackingExists, requireCleanIndex, requireGitRepo } from "./git/git.js";
|
|
32
32
|
import { buildBlocks, buildDependencyEdges, computeFileOrigins, findBestPlan, gatherChangedFiles, getFileStats } from "./core/planner.js";
|
|
33
33
|
import { resetTrackedFilesCache } from "./shared/utils.js";
|
|
34
34
|
import { exportJson, writeHtmlReport, writeScoreHtmlReport } from "./output/report.js";
|
|
@@ -108,7 +108,6 @@ function printSummary(fileStats, config, currentBranch, baseBranch) {
|
|
|
108
108
|
const renamedFiles = statsBase.filter((f) => f.changeType === "R").length;
|
|
109
109
|
const largeFiles = statsBase.filter((f) => f.lines > config.largeFileThreshold).length;
|
|
110
110
|
const testCoveragePercent = config.testCoveragePercent;
|
|
111
|
-
;
|
|
112
111
|
ui.header(APP_NAME.toUpperCase(), "Asesor de División de PRs", [
|
|
113
112
|
["Rama actual", currentBranch],
|
|
114
113
|
["Rama base del PR", baseBranch],
|
|
@@ -355,9 +354,53 @@ async function main() {
|
|
|
355
354
|
}
|
|
356
355
|
ui.ok(`Rama base '${baseBranch}' verificada en '${remote}' (última versión).`);
|
|
357
356
|
const aheadCount = localAheadCount(baseBranch);
|
|
357
|
+
let branchHasCommits = false;
|
|
358
|
+
// cascadeWarning se construye aquí (scope del action) para poder pasarlo
|
|
359
|
+
// al reporte HTML aunque el usuario haya elegido continuar el análisis.
|
|
360
|
+
let cascadeWarning;
|
|
358
361
|
if (aheadCount > 0) {
|
|
359
|
-
|
|
360
|
-
|
|
362
|
+
branchHasCommits = true;
|
|
363
|
+
// Buscar la última rama hermana de la cascada (mismo tipo/equipo-número)
|
|
364
|
+
// que tenga commits adelantados. Si existe, el usuario debe continuar
|
|
365
|
+
// la cascada desde esa rama; si no, debe partir desde la base.
|
|
366
|
+
const cascadeParent = findCascadeParent(currentBranch, baseBranch);
|
|
367
|
+
const branchFrom = cascadeParent ?? baseBranch;
|
|
368
|
+
const suggestedBranch = currentBranch.replace(/^(feature|fix|bugfix|hotfix|refactor|chore|docs|test|perf|ci)\/(.+?)(-wip|-backup-.+)?$/, (_, type, rest) => `${type}/${rest}-wip`);
|
|
369
|
+
cascadeWarning = { aheadCount, cascadeParent, branchFrom, suggestedBranch };
|
|
370
|
+
const cascadeLines = cascadeParent
|
|
371
|
+
? [
|
|
372
|
+
`Se detectó la rama '${cascadeParent}' como parte de la misma`,
|
|
373
|
+
`cascada (mismo tipo/equipo/historia). Para mantener el orden`,
|
|
374
|
+
`correcto de PRs apilados, la nueva rama debe crearse desde`,
|
|
375
|
+
`esa rama y no desde la base:`,
|
|
376
|
+
]
|
|
377
|
+
: [
|
|
378
|
+
`No se encontraron otras ramas de la misma cascada.`,
|
|
379
|
+
`Crea la nueva rama directamente desde la base:`,
|
|
380
|
+
];
|
|
381
|
+
ui.card("⚠ INTEGRIDAD DEL PLAN EN CASCADA COMPROMETIDA", [
|
|
382
|
+
`La rama '${currentBranch}' ya tiene ${aheadCount} commit${aheadCount !== 1 ? "s" : ""}`,
|
|
383
|
+
`adelantados respecto a '${baseBranch}'.`,
|
|
384
|
+
"",
|
|
385
|
+
"Para que el plan de PRs en cascada funcione correctamente, cada",
|
|
386
|
+
"rama del plan debe partir desde un estado limpio relativo a la",
|
|
387
|
+
"rama base. Si esta rama ya tiene commits previos, las ramas",
|
|
388
|
+
"derivadas los heredarán todos, rompiendo la separación de cambios",
|
|
389
|
+
"y haciendo los PRs imposibles de revisar de forma aislada.",
|
|
390
|
+
"",
|
|
391
|
+
ui.badge("RECOMENDACIÓN", "yellow") + " " + cascadeLines[0],
|
|
392
|
+
...cascadeLines.slice(1).map((l) => " " + l),
|
|
393
|
+
` git checkout ${branchFrom}`,
|
|
394
|
+
` git checkout -b ${suggestedBranch}`,
|
|
395
|
+
"",
|
|
396
|
+
"Puedes continuar para ver el análisis (sin aplicar el plan).",
|
|
397
|
+
"La opción --apply quedará deshabilitada en esta ejecución."
|
|
398
|
+
], "red");
|
|
399
|
+
const continueAnyway = await ui.confirm("¿Deseas continuar de todas formas (solo análisis, sin aplicar)?");
|
|
400
|
+
if (!continueAnyway) {
|
|
401
|
+
closeReadlineInterface();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
361
404
|
}
|
|
362
405
|
// ─── Pipeline de análisis ────────────────────────────────────────────────
|
|
363
406
|
ui.spinner.start("Analizando cambios del working tree...");
|
|
@@ -405,7 +448,7 @@ async function main() {
|
|
|
405
448
|
printPlans(plans, baseBranch, currentBranch, config);
|
|
406
449
|
// ─── Exportar reporte HTML ───────────────────────────────────────────────
|
|
407
450
|
const wipFileStats = fileStats.filter((f) => !f.origin || f.origin === "working-tree" || f.origin === "untracked");
|
|
408
|
-
const reportInput = { currentBranch, baseBranch, config, fileStats, wipFileStats, deps, blocks, plans };
|
|
451
|
+
const reportInput = { currentBranch, baseBranch, config, fileStats, wipFileStats, deps, blocks, plans, cascadeWarning };
|
|
409
452
|
const htmlFile = "pr-split-report.html";
|
|
410
453
|
writeHtmlReport(htmlFile, reportInput);
|
|
411
454
|
ui.ok(`Reporte HTML generado: ${htmlFile}`);
|
|
@@ -427,7 +470,9 @@ async function main() {
|
|
|
427
470
|
// ─── Decidir si aplicar el plan ───────────────────────────────────────────
|
|
428
471
|
// --apply omite la pregunta y ejecuta directamente (modo no interactivo/CI).
|
|
429
472
|
// Sin el flag, siempre se pregunta al usuario tras ver el reporte y el HTML.
|
|
430
|
-
|
|
473
|
+
// Si la rama ya tenía commits adelantados, el apply quedó deshabilitado
|
|
474
|
+
// para preservar la integridad del plan en cascada.
|
|
475
|
+
const shouldApply = !branchHasCommits && (!!options.apply || await ui.confirm("¿Deseas aplicar el plan ahora?"));
|
|
431
476
|
if (shouldApply) {
|
|
432
477
|
await enrichCommitTickets(plans, config);
|
|
433
478
|
try {
|
|
@@ -439,6 +484,11 @@ async function main() {
|
|
|
439
484
|
process.exit(1);
|
|
440
485
|
}
|
|
441
486
|
}
|
|
487
|
+
else if (branchHasCommits) {
|
|
488
|
+
ui.warn("Apply deshabilitado: la rama ya tenía commits adelantados respecto a la base.\n" +
|
|
489
|
+
"Crea una nueva rama desde la base para poder aplicar el plan sin romper\n" +
|
|
490
|
+
"la integridad de los PRs en cascada.");
|
|
491
|
+
}
|
|
442
492
|
else {
|
|
443
493
|
ui.info("Sin cambios en git.");
|
|
444
494
|
}
|
package/dist/git/git.js
CHANGED
|
@@ -226,6 +226,51 @@ export function localAheadCount(baseBranch) {
|
|
|
226
226
|
const count = shSafe(`git rev-list ${q(baseBranch)}..HEAD --count`);
|
|
227
227
|
return parseInt(count, 10) || 0;
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Busca la última rama local de la misma "familia" (mismo prefijo tipo/equipo-número)
|
|
231
|
+
* que tiene commits adelantados respecto a `baseBranch`, excluyendo `currentBranch`.
|
|
232
|
+
*
|
|
233
|
+
* Útil para sugerir al usuario que cree la siguiente rama de la cascada desde
|
|
234
|
+
* la punta de la rama hermana más reciente en lugar de desde la rama base.
|
|
235
|
+
*
|
|
236
|
+
* @param currentBranch - Rama actual (se excluye de la búsqueda).
|
|
237
|
+
* @param baseBranch - Rama base del análisis.
|
|
238
|
+
* @returns Nombre de la rama hermana más reciente con commits adelantados, o `null`.
|
|
239
|
+
*/
|
|
240
|
+
export function findCascadeParent(currentBranch, baseBranch) {
|
|
241
|
+
// Extraer el prefijo familia: tipo/EQUIPO-NUMERO
|
|
242
|
+
const familyMatch = currentBranch.match(/^(feature|fix|bugfix|hotfix|refactor|chore|docs|test|perf|ci)\/([A-Z]+-\d+)/i);
|
|
243
|
+
if (!familyMatch)
|
|
244
|
+
return null;
|
|
245
|
+
const familyPrefix = `${familyMatch[1]}/${familyMatch[2].toUpperCase()}`;
|
|
246
|
+
// Listar todas las ramas locales
|
|
247
|
+
const raw = shSafe("git branch --format=%(refname:short)");
|
|
248
|
+
if (!raw)
|
|
249
|
+
return null;
|
|
250
|
+
const candidates = raw
|
|
251
|
+
.split("\n")
|
|
252
|
+
.map((b) => b.trim())
|
|
253
|
+
.filter((b) => b &&
|
|
254
|
+
b !== currentBranch &&
|
|
255
|
+
b.toUpperCase().startsWith(familyPrefix.toUpperCase()));
|
|
256
|
+
if (candidates.length === 0)
|
|
257
|
+
return null;
|
|
258
|
+
// De los candidatos, quedarnos con los que tengan commits adelantados
|
|
259
|
+
// respecto a la base y ordenarlos por committerdate descendente
|
|
260
|
+
const withAhead = candidates.filter((b) => {
|
|
261
|
+
const count = shSafe(`git rev-list ${q(baseBranch)}..${q(b)} --count`);
|
|
262
|
+
return (parseInt(count, 10) || 0) > 0;
|
|
263
|
+
});
|
|
264
|
+
if (withAhead.length === 0)
|
|
265
|
+
return null;
|
|
266
|
+
// El más reciente es el que tiene el commit más nuevo
|
|
267
|
+
const sorted = withAhead.sort((a, b) => {
|
|
268
|
+
const dateA = shSafe(`git log -1 --format=%ct ${q(a)}`);
|
|
269
|
+
const dateB = shSafe(`git log -1 --format=%ct ${q(b)}`);
|
|
270
|
+
return (parseInt(dateB, 10) || 0) - (parseInt(dateA, 10) || 0);
|
|
271
|
+
});
|
|
272
|
+
return sorted[0] ?? null;
|
|
273
|
+
}
|
|
229
274
|
/**
|
|
230
275
|
* Hace push de una rama local al remoto indicado.
|
|
231
276
|
*
|
package/dist/output/report.js
CHANGED
|
@@ -436,7 +436,7 @@ export function renderHtmlReport(input) {
|
|
|
436
436
|
const optimalPlans = plans.filter((p) => p.score >= config.targetScore).length;
|
|
437
437
|
const acceptablePlans = plans.filter((p) => p.score >= warnThreshold && p.score < config.targetScore).length;
|
|
438
438
|
const riskPlans = plans.filter((p) => p.score < warnThreshold).length;
|
|
439
|
-
const maxLinesForBar = fileStats.length > 0 ?
|
|
439
|
+
const maxLinesForBar = fileStats.length > 0 ? fileStats.reduce((max, f) => f.lines > max ? f.lines : max, 0) : 1;
|
|
440
440
|
const timestamp = new Date().toLocaleString("es-PE", {
|
|
441
441
|
year: "numeric", month: "long", day: "numeric",
|
|
442
442
|
hour: "2-digit", minute: "2-digit"
|
|
@@ -498,6 +498,32 @@ export function renderHtmlReport(input) {
|
|
|
498
498
|
</div>
|
|
499
499
|
</div>
|
|
500
500
|
</header>
|
|
501
|
+
<!-- ══ AVISO DE INTEGRIDAD DE CASCADA (condicional) ════════════ -->
|
|
502
|
+
${input.cascadeWarning ? (() => {
|
|
503
|
+
const cw = input.cascadeWarning;
|
|
504
|
+
const parentNote = cw.cascadeParent
|
|
505
|
+
? `Se detectó la rama <code>${esc(cw.cascadeParent)}</code> como parte de la misma cascada.
|
|
506
|
+
Para mantener el orden correcto de PRs apilados, la nueva rama debe crearse desde esa rama.`
|
|
507
|
+
: `No se encontraron otras ramas de la misma cascada. Crea la nueva rama directamente desde la base.`;
|
|
508
|
+
return `<section class="disclaimer-banner" style="background:rgba(239,68,68,.12);border-color:#f87171" role="alert" aria-label="Aviso de integridad de cascada">
|
|
509
|
+
<div class="disclaimer-icon">⚠️</div>
|
|
510
|
+
<div class="disclaimer-body">
|
|
511
|
+
<strong>INTEGRIDAD DEL PLAN EN CASCADA COMPROMETIDA</strong>
|
|
512
|
+
<p>
|
|
513
|
+
La rama <strong>${esc(currentBranch)}</strong> ya tiene
|
|
514
|
+
<strong>${cw.aheadCount} commit${cw.aheadCount !== 1 ? "s" : ""}</strong>
|
|
515
|
+
adelantados respecto a <strong>${esc(baseBranch)}</strong>.
|
|
516
|
+
Las ramas derivadas heredarán esos commits, rompiendo la separación de cambios
|
|
517
|
+
y haciendo los PRs imposibles de revisar de forma aislada.
|
|
518
|
+
</p>
|
|
519
|
+
<p>${parentNote}</p>
|
|
520
|
+
<p><strong>Comandos recomendados:</strong></p>
|
|
521
|
+
<pre style="background:rgba(0,0,0,.25);padding:10px 14px;border-radius:6px;font-size:.85em;margin:8px 0 0"># Crear la siguiente rama de la cascada\ngit checkout ${esc(cw.branchFrom)}\ngit checkout -b ${esc(cw.suggestedBranch)}</pre>
|
|
522
|
+
<p class="disclaimer-footer" style="margin-top:8px">El apply estuvo deshabilitado en esta ejecución para preservar la integridad del plan.</p>
|
|
523
|
+
</div>
|
|
524
|
+
</section>`;
|
|
525
|
+
})() : ""}
|
|
526
|
+
|
|
501
527
|
<!-- ══ AVISO DE RESPONSABILIDAD ═══════════════════════════════ -->
|
|
502
528
|
<section class="disclaimer-banner" role="alert" aria-label="Aviso de responsabilidad">
|
|
503
529
|
<div class="disclaimer-icon">⚠️</div>
|
|
@@ -748,7 +774,7 @@ export function renderScoreHtmlReport(input) {
|
|
|
748
774
|
const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
|
|
749
775
|
const totalAdditions = fileStats.reduce((sum, f) => sum + f.additions, 0);
|
|
750
776
|
const totalDeletions = fileStats.reduce((sum, f) => sum + f.deletions, 0);
|
|
751
|
-
const maxLinesForBar = totalFiles > 0 ?
|
|
777
|
+
const maxLinesForBar = totalFiles > 0 ? fileStats.reduce((max, f) => f.lines > max ? f.lines : max, 0) : 1;
|
|
752
778
|
const label = scoreLabel(result.complexity, config.targetScore);
|
|
753
779
|
const warnThreshold = config.targetScore > 4 ? 4 : Math.max(0, config.targetScore - 1);
|
|
754
780
|
const scoreClass = result.complexity >= config.targetScore ? "green" : result.complexity >= warnThreshold ? "yellow" : "red";
|
package/package.json
CHANGED