pull-request-split-advisor 3.2.2 → 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 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. El subcomando `score` utiliza exclusivamente los archivos en estado `WIP` o `NUEVO` para las métricas; si el working tree está limpio (todos los cambios ya están en commits), el comando informa y sale sin calcular un score basado en datos erróneos.
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.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.
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";
@@ -92,15 +92,21 @@ function computeTestCoveragePercent(fileStats) {
92
92
  return Math.round((testFiles / fileStats.length) * 100);
93
93
  }
94
94
  function printSummary(fileStats, config, currentBranch, baseBranch) {
95
- const totalFiles = fileStats.length;
96
- const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
97
- const totalAdditions = fileStats.reduce((sum, f) => sum + f.additions, 0);
98
- const totalDeletions = fileStats.reduce((sum, f) => sum + f.deletions, 0);
99
- const createdFiles = fileStats.filter((f) => f.changeType === "A" || f.changeType === "U").length;
100
- const updatedFiles = fileStats.filter((f) => f.changeType === "M").length;
101
- const deletedFiles = fileStats.filter((f) => f.changeType === "D").length;
102
- const renamedFiles = fileStats.filter((f) => f.changeType === "R").length;
103
- const largeFiles = fileStats.filter((f) => f.lines > config.largeFileThreshold).length;
95
+ // Los stats del resumen se calculan solo sobre archivos WIP/sin-rastrear.
96
+ // Los archivos LOCAL y REMOTO aparecen en la tabla detalle pero no en estas métricas.
97
+ const wipStats = fileStats.filter((f) => !f.origin || f.origin === "working-tree" || f.origin === "untracked");
98
+ const statsBase = wipStats.length > 0 ? wipStats : fileStats;
99
+ const hasInfoFiles = statsBase.length < fileStats.length;
100
+ const infoFileCount = fileStats.length - statsBase.length;
101
+ const totalFiles = statsBase.length;
102
+ const totalLines = statsBase.reduce((sum, f) => sum + f.lines, 0);
103
+ const totalAdditions = statsBase.reduce((sum, f) => sum + f.additions, 0);
104
+ const totalDeletions = statsBase.reduce((sum, f) => sum + f.deletions, 0);
105
+ const createdFiles = statsBase.filter((f) => f.changeType === "A" || f.changeType === "U").length;
106
+ const updatedFiles = statsBase.filter((f) => f.changeType === "M").length;
107
+ const deletedFiles = statsBase.filter((f) => f.changeType === "D").length;
108
+ const renamedFiles = statsBase.filter((f) => f.changeType === "R").length;
109
+ const largeFiles = statsBase.filter((f) => f.lines > config.largeFileThreshold).length;
104
110
  const testCoveragePercent = config.testCoveragePercent;
105
111
  ui.header(APP_NAME.toUpperCase(), "Asesor de División de PRs", [
106
112
  ["Rama actual", currentBranch],
@@ -112,8 +118,13 @@ function printSummary(fileStats, config, currentBranch, baseBranch) {
112
118
  ["Número de historia", config.branchNamingContext?.storyNumber ?? "N/A"]
113
119
  ]);
114
120
  ui.section("RESUMEN DE CAMBIOS", "#");
121
+ if (hasInfoFiles) {
122
+ ui.info(`${infoFileCount} archivo${infoFileCount !== 1 ? "s" : ""} con origen LOCAL o REMOTO ` +
123
+ "se muestran en la tabla de archivos de forma informativa. " +
124
+ "Sus métricas no se incluyen en este resumen.");
125
+ }
115
126
  ui.dashboard([
116
- ["Total de archivos", totalFiles, "blue"],
127
+ ["Total de archivos WIP", totalFiles, "blue"],
117
128
  ["Lineas agregadas", totalAdditions, "green"],
118
129
  ["Lineas borradas", totalDeletions, "red"],
119
130
  ["Archivos creados", createdFiles, createdFiles > 0 ? "green" : "cyan"],
@@ -343,9 +354,53 @@ async function main() {
343
354
  }
344
355
  ui.ok(`Rama base '${baseBranch}' verificada en '${remote}' (última versión).`);
345
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;
346
361
  if (aheadCount > 0) {
347
- ui.warn(`La rama '${currentBranch}' ya tiene ${aheadCount} commit(s) adelantados respecto a '${baseBranch}'.\n` +
348
- `Los archivos ya commiteados aparecerán en el análisis pero no podrán re-commitearse al aplicar el plan.`);
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
+ }
349
404
  }
350
405
  // ─── Pipeline de análisis ────────────────────────────────────────────────
351
406
  ui.spinner.start("Analizando cambios del working tree...");
@@ -392,7 +447,8 @@ async function main() {
392
447
  printBlocks(blocks);
393
448
  printPlans(plans, baseBranch, currentBranch, config);
394
449
  // ─── Exportar reporte HTML ───────────────────────────────────────────────
395
- const reportInput = { currentBranch, baseBranch, config, fileStats, deps, blocks, plans };
450
+ const wipFileStats = fileStats.filter((f) => !f.origin || f.origin === "working-tree" || f.origin === "untracked");
451
+ const reportInput = { currentBranch, baseBranch, config, fileStats, wipFileStats, deps, blocks, plans, cascadeWarning };
396
452
  const htmlFile = "pr-split-report.html";
397
453
  writeHtmlReport(htmlFile, reportInput);
398
454
  ui.ok(`Reporte HTML generado: ${htmlFile}`);
@@ -414,7 +470,9 @@ async function main() {
414
470
  // ─── Decidir si aplicar el plan ───────────────────────────────────────────
415
471
  // --apply omite la pregunta y ejecuta directamente (modo no interactivo/CI).
416
472
  // Sin el flag, siempre se pregunta al usuario tras ver el reporte y el HTML.
417
- const shouldApply = !!options.apply || await ui.confirm("¿Deseas aplicar el plan ahora?");
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?"));
418
476
  if (shouldApply) {
419
477
  await enrichCommitTickets(plans, config);
420
478
  try {
@@ -426,6 +484,11 @@ async function main() {
426
484
  process.exit(1);
427
485
  }
428
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
+ }
429
492
  else {
430
493
  ui.info("Sin cambios en git.");
431
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
  *
@@ -415,15 +415,20 @@ function buildReportStyles() {
415
415
  */
416
416
  export function renderHtmlReport(input) {
417
417
  const { currentBranch, baseBranch, config, fileStats, deps, blocks, plans } = input;
418
- const totalFiles = fileStats.length;
419
- const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
420
- const totalAdditions = fileStats.reduce((sum, f) => sum + f.additions, 0);
421
- const totalDeletions = fileStats.reduce((sum, f) => sum + f.deletions, 0);
422
- const createdFiles = fileStats.filter((f) => f.changeType === "A" || f.changeType === "U").length;
423
- const updatedFiles = fileStats.filter((f) => f.changeType === "M").length;
424
- const deletedFiles = fileStats.filter((f) => f.changeType === "D").length;
425
- const renamedFiles = fileStats.filter((f) => f.changeType === "R").length;
426
- const largeFiles = fileStats.filter((f) => f.lines > config.largeFileThreshold).length;
418
+ // Los stats del hero y del resumen se calculan solo sobre archivos WIP/sin-rastrear.
419
+ // fileStats completo (WIP + LOCAL + REMOTO) se usa únicamente para la tabla de detalle.
420
+ const wipStats = input.wipFileStats ?? fileStats;
421
+ const hasInfoFiles = input.wipFileStats != null && input.wipFileStats.length < fileStats.length;
422
+ const infoFileCount = fileStats.length - wipStats.length;
423
+ const totalFiles = wipStats.length;
424
+ const totalLines = wipStats.reduce((sum, f) => sum + f.lines, 0);
425
+ const totalAdditions = wipStats.reduce((sum, f) => sum + f.additions, 0);
426
+ const totalDeletions = wipStats.reduce((sum, f) => sum + f.deletions, 0);
427
+ const createdFiles = wipStats.filter((f) => f.changeType === "A" || f.changeType === "U").length;
428
+ const updatedFiles = wipStats.filter((f) => f.changeType === "M").length;
429
+ const deletedFiles = wipStats.filter((f) => f.changeType === "D").length;
430
+ const renamedFiles = wipStats.filter((f) => f.changeType === "R").length;
431
+ const largeFiles = wipStats.filter((f) => f.lines > config.largeFileThreshold).length;
427
432
  const avgScore = plans.length > 0
428
433
  ? plans.reduce((sum, p) => sum + p.score, 0) / plans.length
429
434
  : 0;
@@ -431,7 +436,7 @@ export function renderHtmlReport(input) {
431
436
  const optimalPlans = plans.filter((p) => p.score >= config.targetScore).length;
432
437
  const acceptablePlans = plans.filter((p) => p.score >= warnThreshold && p.score < config.targetScore).length;
433
438
  const riskPlans = plans.filter((p) => p.score < warnThreshold).length;
434
- const maxLinesForBar = fileStats.length > 0 ? Math.max(...fileStats.map((f) => f.lines)) : 1;
439
+ const maxLinesForBar = fileStats.length > 0 ? fileStats.reduce((max, f) => f.lines > max ? f.lines : max, 0) : 1;
435
440
  const timestamp = new Date().toLocaleString("es-PE", {
436
441
  year: "numeric", month: "long", day: "numeric",
437
442
  hour: "2-digit", minute: "2-digit"
@@ -487,11 +492,38 @@ export function renderHtmlReport(input) {
487
492
  <div class="value" style="color:#f87171">${riskPlans}</div>
488
493
  </div>
489
494
  <div class="hero-item">
490
- <div class="label">Archivos modificados</div>
491
- <div class="value">${totalFiles}</div>
495
+ <div class="label">Archivos WIP${hasInfoFiles ? ` / total` : ""}</div>
496
+ <div class="value">${totalFiles}${hasInfoFiles ? ` <span style="font-size:.8em;opacity:.7">/ ${fileStats.length}</span>` : ""}</div>
497
+ ${hasInfoFiles ? `<div class="sub" style="font-size:.75em;opacity:.65">${infoFileCount} commit${infoFileCount !== 1 ? "s" : ""} local/remoto</div>` : ""}
492
498
  </div>
493
499
  </div>
494
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
+
495
527
  <!-- ══ AVISO DE RESPONSABILIDAD ═══════════════════════════════ -->
496
528
  <section class="disclaimer-banner" role="alert" aria-label="Aviso de responsabilidad">
497
529
  <div class="disclaimer-icon">⚠️</div>
@@ -586,6 +618,17 @@ export function renderHtmlReport(input) {
586
618
  <!-- ══ DETALLE DE ARCHIVOS ════════════════════════════════════════ -->
587
619
  <section class="section">
588
620
  <h2 class="section-heading">📄 Detalle de archivos</h2>
621
+ ${hasInfoFiles ? `<div class="disclaimer-banner" style="margin-bottom:16px;padding:12px 18px" role="note">
622
+ <div class="disclaimer-icon" style="font-size:1.1em">ℹ️</div>
623
+ <div class="disclaimer-body" style="margin:0">
624
+ <strong>Archivos informativos</strong>
625
+ — ${infoFileCount} archivo${infoFileCount !== 1 ? "s" : ""} con badge
626
+ <span class="origin-badge origin-pushed">REMOTO</span> o
627
+ <span class="origin-badge origin-local-commit">LOCAL</span>
628
+ se muestran a modo de referencia.
629
+ Sus métricas <strong>no</strong> se incluyen en el resumen de cambios ni en el score.
630
+ </div>
631
+ </div>` : ""}
589
632
  <table>
590
633
  <thead>
591
634
  <tr>
@@ -694,7 +737,7 @@ export function renderHtmlReport(input) {
694
737
  <!-- ══ FOOTER ════════════════════════════════════════════════════ -->
695
738
  <footer class="footer">
696
739
  <p>Generado por <strong>${APP_REPORT_TITLE}</strong> · ${timestamp}</p>
697
- <p>Score objetivo: <strong>${config.targetScore}</strong> · Ramas analizadas: <strong>${plans.length}</strong> · Archivos: <strong>${totalFiles}</strong></p>
740
+ <p>Score objetivo: <strong>${config.targetScore}</strong> · Ramas analizadas: <strong>${plans.length}</strong> · Archivos WIP: <strong>${totalFiles}</strong>${hasInfoFiles ? ` (+ ${infoFileCount} informativo${infoFileCount !== 1 ? "s" : ""})` : ""}</p>
698
741
  </footer>
699
742
 
700
743
  </div>
@@ -731,7 +774,7 @@ export function renderScoreHtmlReport(input) {
731
774
  const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
732
775
  const totalAdditions = fileStats.reduce((sum, f) => sum + f.additions, 0);
733
776
  const totalDeletions = fileStats.reduce((sum, f) => sum + f.deletions, 0);
734
- const maxLinesForBar = totalFiles > 0 ? Math.max(...fileStats.map((f) => f.lines)) : 1;
777
+ const maxLinesForBar = totalFiles > 0 ? fileStats.reduce((max, f) => f.lines > max ? f.lines : max, 0) : 1;
735
778
  const label = scoreLabel(result.complexity, config.targetScore);
736
779
  const warnThreshold = config.targetScore > 4 ? 4 : Math.max(0, config.targetScore - 1);
737
780
  const scoreClass = result.complexity >= config.targetScore ? "green" : result.complexity >= warnThreshold ? "yellow" : "red";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pull-request-split-advisor",
3
- "version": "3.2.2",
3
+ "version": "3.2.4",
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",