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 +8 -2
- package/dist/cli.js +78 -15
- package/dist/git/git.js +45 -0
- package/dist/output/report.js +57 -14
- 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";
|
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
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
|
-
|
|
348
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*
|
package/dist/output/report.js
CHANGED
|
@@ -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
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
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 ?
|
|
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
|
|
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
|
|
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 ?
|
|
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