pull-request-split-advisor 3.2.1 → 3.2.3

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
@@ -29,6 +29,8 @@ La v3.0.0 también mejora el **apply**: rama de respaldo con snapshot completo d
29
29
 
30
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
31
 
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.
33
+
32
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.
33
35
 
34
36
  ---
@@ -40,7 +42,8 @@ A partir de la **v3.1.0**, el proyecto incluye una **suite completa de tests** (
40
42
  - Cálculo de score de calidad por rama (0–5) con factores ponderados
41
43
  - Generación de nombres de rama y mensajes de commit convencionales
42
44
  - 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
45
+ - **`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 — evalúa **solo archivos WIP y sin rastrear**; si el working tree está limpio, informa al usuario y sale sin generar un score incorrecto
46
+ - **Clasificación de origen por archivo** (v3.2.1+): diferencia entre `REMOTO` (publicado), `LOCAL` (commit local no publicado), `WIP` (sin commitear) y `NUEVO` (sin rastrear) — badge de color en terminal y columna *Origen* en ambos reportes HTML
44
47
  - Exportación del plan en formato JSON para integraciones externas
45
48
  - Historial de scores con seguimiento entre análisis
46
49
  - Validaciones de nomenclatura de ramas y estado del working tree
package/dist/cli.js CHANGED
@@ -29,7 +29,8 @@ 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
31
  import { detectRemote, fetchQuiet, getCurrentBranch, localAheadCount, localBehindCount, remoteTrackingExists, requireCleanIndex, requireGitRepo } from "./git/git.js";
32
- import { buildBlocks, buildDependencyEdges, findBestPlan, gatherChangedFiles, getFileStats } from "./core/planner.js";
32
+ import { buildBlocks, buildDependencyEdges, computeFileOrigins, findBestPlan, gatherChangedFiles, getFileStats } from "./core/planner.js";
33
+ import { resetTrackedFilesCache } from "./shared/utils.js";
33
34
  import { exportJson, writeHtmlReport, writeScoreHtmlReport } from "./output/report.js";
34
35
  import { appendHistory, readHistory } from "./core/history.js";
35
36
  import { buildSuggestedMessage } from "./core/commit-planner.js";
@@ -91,16 +92,23 @@ function computeTestCoveragePercent(fileStats) {
91
92
  return Math.round((testFiles / fileStats.length) * 100);
92
93
  }
93
94
  function printSummary(fileStats, config, currentBranch, baseBranch) {
94
- const totalFiles = fileStats.length;
95
- const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
96
- const totalAdditions = fileStats.reduce((sum, f) => sum + f.additions, 0);
97
- const totalDeletions = fileStats.reduce((sum, f) => sum + f.deletions, 0);
98
- const createdFiles = fileStats.filter((f) => f.changeType === "A" || f.changeType === "U").length;
99
- const updatedFiles = fileStats.filter((f) => f.changeType === "M").length;
100
- const deletedFiles = fileStats.filter((f) => f.changeType === "D").length;
101
- const renamedFiles = fileStats.filter((f) => f.changeType === "R").length;
102
- 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;
103
110
  const testCoveragePercent = config.testCoveragePercent;
111
+ ;
104
112
  ui.header(APP_NAME.toUpperCase(), "Asesor de División de PRs", [
105
113
  ["Rama actual", currentBranch],
106
114
  ["Rama base del PR", baseBranch],
@@ -111,8 +119,13 @@ function printSummary(fileStats, config, currentBranch, baseBranch) {
111
119
  ["Número de historia", config.branchNamingContext?.storyNumber ?? "N/A"]
112
120
  ]);
113
121
  ui.section("RESUMEN DE CAMBIOS", "#");
122
+ if (hasInfoFiles) {
123
+ ui.info(`${infoFileCount} archivo${infoFileCount !== 1 ? "s" : ""} con origen LOCAL o REMOTO ` +
124
+ "se muestran en la tabla de archivos de forma informativa. " +
125
+ "Sus métricas no se incluyen en este resumen.");
126
+ }
114
127
  ui.dashboard([
115
- ["Total de archivos", totalFiles, "blue"],
128
+ ["Total de archivos WIP", totalFiles, "blue"],
116
129
  ["Lineas agregadas", totalAdditions, "green"],
117
130
  ["Lineas borradas", totalDeletions, "red"],
118
131
  ["Archivos creados", createdFiles, createdFiles > 0 ? "green" : "cyan"],
@@ -129,9 +142,10 @@ function printSummary(fileStats, config, currentBranch, baseBranch) {
129
142
  }
130
143
  function printFileStats(fileStats) {
131
144
  ui.section("DETALLE DE ARCHIVOS", "#");
132
- ui.table(["Marca", "Archivo", "Lineas", "+", "-", "Prioridad"], fileStats.map((f) => [
145
+ ui.table(["Origen", "Marca", "Archivo", "Lineas", "+", "-", "Prioridad"], fileStats.map((f) => [
146
+ ui.originBadge(f.origin),
133
147
  changeTypeLabel(f.changeType),
134
- ui.truncateMiddle(f.path, 55),
148
+ ui.truncateMiddle(f.path, 50),
135
149
  f.lines,
136
150
  f.additions,
137
151
  f.deletions,
@@ -350,7 +364,7 @@ async function main() {
350
364
  const changedFiles = gatherChangedFiles(config, baseBranch);
351
365
  if (!changedFiles.length) {
352
366
  ui.spinner.stop();
353
- ui.warn("No hay cambios sin commit en el working tree.");
367
+ ui.warn("No hay cambios respecto a la rama base.");
354
368
  closeReadlineInterface();
355
369
  return;
356
370
  }
@@ -365,6 +379,16 @@ async function main() {
365
379
  const plans = findBestPlan(blocks, currentBranch, config, deps);
366
380
  config.testCoveragePercent = computeTestCoveragePercent(fileStats);
367
381
  ui.spinner.stop("Analisis completado.");
382
+ // ─── Computar origen de cada archivo en el árbol git ─────────────────────
383
+ // Diferencia entre: ya publicado (REMOTO), commiteado local (LOCAL),
384
+ // cambio sin commitear (WIP) y archivo nuevo sin rastrear (NUEVO).
385
+ // Resetear la caché de tracked files para que computeFileOrigins use
386
+ // un Set fresco (evita stale data si el index cambió durante el análisis).
387
+ resetTrackedFilesCache();
388
+ const origins = computeFileOrigins(changedFiles, baseBranch, currentBranch, remote);
389
+ for (const stat of fileStats) {
390
+ stat.origin = origins.get(stat.path);
391
+ }
368
392
  // ─── Enriquecimiento con IA (opcional) ───────────────────────────────────
369
393
  // Si la IA no está habilitada o configurada, `aiEnrichPlans` retorna
370
394
  // de inmediato sin modificar los planes (modo heurístico puro).
@@ -380,7 +404,8 @@ async function main() {
380
404
  printBlocks(blocks);
381
405
  printPlans(plans, baseBranch, currentBranch, config);
382
406
  // ─── Exportar reporte HTML ───────────────────────────────────────────────
383
- const reportInput = { currentBranch, baseBranch, config, fileStats, deps, blocks, plans };
407
+ 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 };
384
409
  const htmlFile = "pr-split-report.html";
385
410
  writeHtmlReport(htmlFile, reportInput);
386
411
  ui.ok(`Reporte HTML generado: ${htmlFile}`);
@@ -453,13 +478,38 @@ async function main() {
453
478
  }
454
479
  const fileStats = getFileStats(changedFiles, baseBranch);
455
480
  ui.spinner.stop("Calculo completado.");
481
+ // ─── Computar origen de cada archivo en el árbol git ─────────────────────
482
+ // Resetear caché para asegurar datos frescos (el score subcommand no llama
483
+ // a requireCleanIndex, por lo que puede haber archivos staged/unstaged que
484
+ // cambien el estado del index entre la primera lectura y esta clasificación).
485
+ resetTrackedFilesCache();
486
+ const scoreRemote = detectRemote(currentBranch);
487
+ const scoreOrigins = computeFileOrigins(changedFiles, baseBranch, currentBranch, scoreRemote);
488
+ for (const stat of fileStats) {
489
+ stat.origin = scoreOrigins.get(stat.path);
490
+ }
456
491
  const totalFiles = fileStats.length;
457
492
  const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
493
+ // El score se calcula ÚNICAMENTE sobre archivos del working tree en ese momento.
494
+ // Los archivos ya commiteados (LOCAL o REMOTO) se muestran en el reporte
495
+ // con su badge de origen pero NO afectan las métricas del score.
496
+ const scoredStats = fileStats.filter((f) => f.origin === "working-tree" || f.origin === "untracked");
497
+ const scoredFiles = scoredStats.length;
498
+ const scoredLines = scoredStats.reduce((sum, f) => sum + f.lines, 0);
499
+ // Si no hay archivos WIP no hay nada que puntuar: todos los cambios ya
500
+ // están en commits (LOCAL o REMOTO) y el working tree está limpio.
501
+ if (scoredFiles === 0) {
502
+ ui.warn("No hay cambios en el working tree para analizar. " +
503
+ "Todos los archivos detectados ya están en commits (LOCAL o REMOTO). " +
504
+ "Realiza cambios sin commitear para obtener un score.");
505
+ closeReadlineInterface();
506
+ return;
507
+ }
458
508
  const aheadCommits = localAheadCount(baseBranch);
459
509
  const commitCount = Math.max(aheadCommits, 1);
460
- const filesPerCommit = Number((totalFiles / commitCount).toFixed(2));
461
- const avgLinesPerCommit = Math.round(totalLines / commitCount);
462
- const result = scorePullRequest({ commitCount, filesPerCommit, avgLinesPerCommit, totalLinesChanged: totalLines }, config);
510
+ const filesPerCommit = Number((scoredFiles / commitCount).toFixed(2));
511
+ const avgLinesPerCommit = Math.round(scoredLines / commitCount);
512
+ const result = scorePullRequest({ commitCount, filesPerCommit, avgLinesPerCommit, totalLinesChanged: scoredLines }, config);
463
513
  const warnThreshold = config.targetScore > 4 ? 4 : Math.max(0, config.targetScore - 1);
464
514
  const scoreColor = ui.scoreColor(result.complexity, config.targetScore);
465
515
  const statusBadge = result.complexity >= config.targetScore
@@ -476,18 +526,27 @@ async function main() {
476
526
  commitCount,
477
527
  filesPerCommit,
478
528
  avgLinesPerCommit,
479
- result
529
+ result,
530
+ scoredFileCount: scoredFiles !== totalFiles ? scoredFiles : undefined,
531
+ scoredLines: scoredFiles !== totalFiles ? scoredLines : undefined
480
532
  });
481
533
  ui.ok(`Reporte HTML generado: ${scoreHtmlFile}`);
534
+ const hasWipDistinction = scoredFiles !== totalFiles;
482
535
  ui.card(`Score actual de la rama: ${currentBranch}`, [
483
536
  `${statusBadge}`,
484
537
  "",
485
- `Rama base: ${baseBranch}`,
486
- `Archivos cambiados: ${totalFiles}`,
487
- `Líneas totales: ${totalLines}`,
488
- `Commits adelantados: ${commitCount}`,
489
- `Archivos por commit: ${filesPerCommit}`,
490
- `Líneas por commit (avg):${avgLinesPerCommit}`,
538
+ `Rama base: ${baseBranch}`,
539
+ `Archivos totales analizados: ${totalFiles}${hasWipDistinction ? ` (${totalFiles - scoredFiles} ya commiteados, excluidos del score)` : ""}`,
540
+ ...(hasWipDistinction
541
+ ? [`Archivos scored (WIP): ${scoredFiles} ← base del score`]
542
+ : []),
543
+ `Líneas totales analizadas: ${totalLines}`,
544
+ ...(hasWipDistinction
545
+ ? [`Líneas scored (WIP): ${scoredLines} ← base del score`]
546
+ : []),
547
+ `Commits adelantados: ${commitCount}`,
548
+ `Archivos / commit (scored): ${filesPerCommit}`,
549
+ `Líneas / commit (scored avg):${avgLinesPerCommit}`,
491
550
  "",
492
551
  `Score: ${ui.score(result.complexity, config.targetScore)}`,
493
552
  `Score objetivo: ${config.targetScore}`,
@@ -339,3 +339,80 @@ export function getFileStats(files, baseBranch) {
339
339
  };
340
340
  });
341
341
  }
342
+ // ---------------------------------------------------------------------------
343
+ // File origin classification
344
+ // ---------------------------------------------------------------------------
345
+ /**
346
+ * Clasifica cada archivo de la lista según su origen en el árbol git:
347
+ *
348
+ * - `"pushed"`: existe en commits ya publicados al remoto (base → remoto/rama).
349
+ * - `"local-commit"`: commiteado localmente pero aún no publicado (remoto/rama → HEAD,
350
+ * o base → HEAD cuando no hay ref de tracking remota).
351
+ * - `"working-tree"`: cambio staged o unstaged no commiteado en el working tree.
352
+ * - `"untracked"`: archivo nuevo sin seguimiento git (nunca añadido al índice).
353
+ *
354
+ * La asignación sigue un orden de prioridad:
355
+ * untracked > working-tree/staged > local-commit > pushed > fallback("working-tree")
356
+ *
357
+ * Cuando un archivo tiene cambios en el working tree además de estar en un commit
358
+ * ya publicado, se clasifica como `"working-tree"` porque tiene cambios pendientes
359
+ * por encima del commit remoto.
360
+ *
361
+ * @param files - Lista de rutas relativas a clasificar.
362
+ * @param baseBranch - Rama base del PR (ej. "main").
363
+ * @param currentBranch - Rama de trabajo actual.
364
+ * @param remote - Nombre del remoto (ej. "origin").
365
+ * @returns Mapa `ruta → FileOrigin` con una entrada por cada ruta de `files`.
366
+ */
367
+ export function computeFileOrigins(files, baseBranch, currentBranch, remote) {
368
+ const origins = new Map();
369
+ // Archivos con cambios unstaged en el working tree (tracked pero no staged)
370
+ const workingTreeSet = new Set(shSafe("git diff --name-only")
371
+ .split("\n").map((s) => s.trim()).filter(Boolean));
372
+ // Archivos staged (git add pero no commiteados) — también son "working-tree"
373
+ const stagedSet = new Set(shSafe("git diff --cached --name-only")
374
+ .split("\n").map((s) => s.trim()).filter(Boolean));
375
+ // Archivos untracked (nuevos, sin git add)
376
+ const untrackedSet = new Set(shSafe("git ls-files --others --exclude-standard")
377
+ .split("\n").map((s) => s.trim()).filter(Boolean));
378
+ // Verificar si existe una ref de tracking remota para la rama actual
379
+ const remoteRef = `${remote}/${currentBranch}`;
380
+ const remoteExists = shSafe(`git rev-parse --verify ${q(`refs/remotes/${remoteRef}`)}`).length > 0;
381
+ // Archivos en commits ya publicados (base → remoto/rama)
382
+ const pushedSet = new Set();
383
+ if (remoteExists) {
384
+ shSafe(`git diff ${q(baseBranch)}..${q(remoteRef)} --name-only`)
385
+ .split("\n").map((s) => s.trim()).filter(Boolean)
386
+ .forEach((f) => pushedSet.add(f));
387
+ }
388
+ // Archivos en commits locales aún no publicados
389
+ // Si existe tracking remota: commits entre remoto/rama y HEAD.
390
+ // Si no existe tracking: todos los commits adelantados respecto a base son locales.
391
+ const localCommitSet = new Set();
392
+ const localBaseRef = remoteExists ? remoteRef : baseBranch;
393
+ shSafe(`git diff ${q(localBaseRef)}..HEAD --name-only`)
394
+ .split("\n").map((s) => s.trim()).filter(Boolean)
395
+ .forEach((f) => localCommitSet.add(f));
396
+ // Asignar origen por prioridad: untracked > working-tree/staged > local-commit > pushed
397
+ for (const file of files) {
398
+ const norm = normalizePathValue(file);
399
+ if (untrackedSet.has(file) || untrackedSet.has(norm)) {
400
+ origins.set(file, "untracked");
401
+ }
402
+ else if (workingTreeSet.has(file) || workingTreeSet.has(norm) ||
403
+ stagedSet.has(file) || stagedSet.has(norm)) {
404
+ origins.set(file, "working-tree");
405
+ }
406
+ else if (localCommitSet.has(file) || localCommitSet.has(norm)) {
407
+ origins.set(file, "local-commit");
408
+ }
409
+ else if (pushedSet.has(file) || pushedSet.has(norm)) {
410
+ origins.set(file, "pushed");
411
+ }
412
+ else {
413
+ // Fallback: si el archivo no aparece en ningún diff conocido, asumir working-tree
414
+ origins.set(file, "working-tree");
415
+ }
416
+ }
417
+ return origins;
418
+ }
@@ -19,7 +19,7 @@
19
19
  * Importar desde este barrel en lugar de los módulos individuales reduce el
20
20
  * acoplamiento entre `cli.ts` y la estructura interna del directorio `core/`.
21
21
  */
22
- export { gatherChangedFiles, getFileStats } from "./file-stats.js";
22
+ export { gatherChangedFiles, getFileStats, computeFileOrigins } from "./file-stats.js";
23
23
  export { buildDependencyEdges } from "./dependency.js";
24
24
  export { buildBlocks } from "./blocks.js";
25
25
  export { findBestPlan } from "./strategy.js";
@@ -7,4 +7,4 @@
7
7
  * Para aplicar cambios editá `src/output/report.scss` y ejecutá:
8
8
  * npm run build:styles
9
9
  */
10
- export const reportStyles = `@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap";:root{--bg: #f0f4f8;--surface: #ffffff;--surface-soft: #f7f9fc;--border: #dde3ed;--text: #1e293b;--muted: #64748b;--green: #15803d;--green-soft: #dcfce7;--green-border: #86efac;--yellow: #a16207;--yellow-soft: #fef3c7;--yellow-border: #fde68a;--orange: #c2410c;--orange-soft: #ffedd5;--orange-border: #fdba74;--red: #b91c1c;--red-soft: #fee2e2;--red-border: #fca5a5;--blue: #1d4ed8;--blue-soft: #dbeafe;--blue-border: #93c5fd;--purple: #6d28d9;--purple-soft: #ede9fe;--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.07), 0 1px 2px rgba(15, 23, 42, 0.05);--shadow: 0 4px 16px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.05);--shadow-lg: 0 10px 40px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(15, 23, 42, 0.06);--radius: 14px;--radius-sm: 8px}*,*::before,*::after{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;padding:0;background:var(--bg);color:var(--text);font-family:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.6}.container{max-width:1320px;margin:0 auto;padding:32px 24px 80px}.hero{background:linear-gradient(135deg, #0f172a 0%, #1e3a6e 50%, #1d4ed8 100%);color:#fff;border-radius:24px;padding:36px 36px 28px;box-shadow:var(--shadow-lg);margin-bottom:28px;position:relative;overflow:hidden}.hero::before{content:"";position:absolute;inset:0;background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");pointer-events:none}.hero-top{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;flex-wrap:wrap}.hero-title-area h1{margin:0 0 6px;font-size:34px;font-weight:800;letter-spacing:-0.5px;line-height:1.1}.hero-title-area p{margin:0;color:hsla(0,0%,100%,.75);font-size:14px;max-width:500px}.hero-timestamp{background:hsla(0,0%,100%,.12);border:1px solid hsla(0,0%,100%,.2);border-radius:10px;padding:8px 14px;font-size:12px;color:hsla(0,0%,100%,.85);white-space:nowrap}.hero-grid{margin-top:24px;display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:12px}.hero-item{background:hsla(0,0%,100%,.11);border:1px solid hsla(0,0%,100%,.16);border-radius:14px;padding:14px 18px;backdrop-filter:blur(4px);transition:background .2s}.hero-item:hover{background:hsla(0,0%,100%,.17)}.hero-item .label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;opacity:.7;margin-bottom:5px}.hero-item .value{font-size:17px;font-weight:700;word-break:break-word}.disclaimer-banner{display:flex;align-items:flex-start;gap:16px;margin:24px 0 0;padding:20px 24px;background:#fefce8;border:2px solid #f59e0b;border-radius:12px;box-shadow:0 2px 8px rgba(245,158,11,.18)}.disclaimer-banner .disclaimer-icon{font-size:32px;line-height:1;flex-shrink:0;margin-top:2px}.disclaimer-banner .disclaimer-body{flex:1;color:#78350f}.disclaimer-banner .disclaimer-body strong:first-child{display:block;font-size:15px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:#92400e;margin-bottom:8px}.disclaimer-banner .disclaimer-body p{margin:0 0 8px;font-size:14px;line-height:1.6}.disclaimer-banner .disclaimer-body p:last-child{margin-bottom:0}.disclaimer-banner .disclaimer-body ul{margin:6px 0 10px 18px;padding:0;font-size:14px;line-height:1.7}.disclaimer-banner .disclaimer-body .disclaimer-footer{font-size:12px;font-style:italic;color:#a16207;margin-top:8px}.section{margin-top:36px}.section-heading{display:flex;align-items:center;gap:10px;margin:0 0 16px;font-size:20px;font-weight:700;color:#0f172a}.section-heading::after{content:"";flex:1;height:1px;background:var(--border)}.summary-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));gap:12px}.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;box-shadow:var(--shadow-sm);display:flex;flex-direction:column;gap:4px;transition:box-shadow .2s,transform .2s}.stat-card:hover{box-shadow:var(--shadow);transform:translateY(-1px)}.stat-card .label{font-size:12px;color:var(--muted);font-weight:500;text-transform:uppercase;letter-spacing:.04em}.stat-card .value{font-size:26px;font-weight:800;line-height:1.2}.stat-card .sub{font-size:12px;color:var(--muted)}.stat-card.blue .value{color:var(--blue)}.stat-card.green .value{color:var(--green)}.stat-card.yellow .value{color:var(--yellow)}.stat-card.red .value{color:var(--red)}.stat-card.purple .value{color:var(--purple)}.card{background:var(--surface);border:1px solid var(--border);border-radius:18px;padding:22px;box-shadow:var(--shadow);margin-bottom:20px}.badges{display:flex;flex-wrap:wrap;gap:8px}.badge{display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;border:1px solid rgba(0,0,0,0)}.badge.green{background:var(--green-soft);color:var(--green);border-color:var(--green-border)}.badge.yellow{background:var(--yellow-soft);color:var(--yellow);border-color:var(--yellow-border)}.badge.orange{background:var(--orange-soft);color:var(--orange);border-color:var(--orange-border)}.badge.red{background:var(--red-soft);color:var(--red);border-color:var(--red-border)}.badge.blue{background:var(--blue-soft);color:var(--blue);border-color:var(--blue-border)}.badge.purple{background:var(--purple-soft);color:var(--purple);border-color:#c4b5fd}table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;box-shadow:var(--shadow-sm);margin:10px 0 20px}table thead th{background:#f1f5fd;color:#374151;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:11px 14px;border-bottom:1px solid var(--border)}table tbody td{padding:10px 14px;border-bottom:1px solid #e9eff7;vertical-align:middle;font-size:13px}table tbody tr:last-child td{border-bottom:none}table tbody tr:nth-child(even){background:#fafbff}table tbody tr:hover{background:#f0f6ff}.num-cell{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}code{background:#eef2ff;color:#312e81;padding:2px 7px;border-radius:6px;font-size:12px;font-family:"SF Mono","Fira Code","Cascadia Code",monospace;word-break:break-word}.muted{color:var(--muted);font-size:13px}.toc-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;box-shadow:var(--shadow-sm);margin-bottom:20px}.toc-title{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px}.toc-list{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.toc-link{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;font-size:12px;font-weight:600;text-decoration:none;border:1px solid rgba(0,0,0,0);transition:opacity .15s}.toc-link:hover{opacity:.8;text-decoration:none}.toc-ok{background:var(--green-soft);color:var(--green);border-color:var(--green-border)}.toc-warn{background:var(--yellow-soft);color:var(--yellow);border-color:var(--yellow-border)}.toc-bad{background:var(--red-soft);color:var(--red);border-color:var(--red-border)}.toc-score{background:rgba(0,0,0,.08);border-radius:999px;padding:1px 7px;font-size:11px;font-weight:800}.plan-card{border-radius:20px;overflow:hidden;box-shadow:var(--shadow);border:1px solid var(--border);margin-bottom:28px}.plan-card-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;padding:22px 26px}.plan-card-body{background:var(--surface);padding:22px 26px}.plan-header-green{background:linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);border-bottom:1px solid #86efac}.plan-header-yellow{background:linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);border-bottom:1px solid #fde68a}.plan-header-red{background:linear-gradient(135deg, #fff1f2 0%, #fee2e2 100%);border-bottom:1px solid #fca5a5}.plan-header-left{display:flex;align-items:center;gap:14px}.plan-branch-icon{font-size:28px;width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.06);border-radius:14px;flex-shrink:0}.plan-branch-name{font-size:17px;font-weight:800;color:#0f172a;word-break:break-all;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.plan-branch-sub{font-size:13px;color:var(--muted);margin-top:2px}.existing-badge{display:inline-block;background:#dbeafe;color:#1e40af;border:1px solid #93c5fd;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;margin-right:4px}.plan-header-right{display:flex;flex-direction:column;align-items:center;gap:4px}.gauge-svg{display:block}.plan-score-label{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.score-label-green{color:var(--green)}.score-label-yellow{color:var(--yellow)}.score-label-red{color:var(--red)}.kpi-row{display:flex;flex-wrap:wrap;gap:12px;margin:16px 0 24px}.kpi{background:var(--surface-soft);border:1px solid var(--border);border-radius:12px;padding:10px 16px;min-width:90px;text-align:center}.kpi-v{font-size:22px;font-weight:800;color:#0f172a;line-height:1.1}.kpi-k{font-size:11px;color:var(--muted);margin-top:3px;text-transform:uppercase;letter-spacing:.04em}.recommendation-box{display:flex;align-items:flex-start;gap:10px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:12px;padding:12px 16px;margin-bottom:16px;font-size:14px;color:#0369a1}.exclusion-note{display:flex;align-items:flex-start;gap:8px;background:#fffbeb;border:1px solid #fde68a;border-radius:10px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#92400e}.rec-icon{font-size:16px;flex-shrink:0}.section-title{font-size:14px;font-weight:700;color:#0f172a;margin:28px 0 10px;display:flex;align-items:center;gap:6px}.metrics-table thead th{background:#f8faff}.metric-code{background:#e0e7ff;color:#3730a3;font-weight:700}.metric-label{font-size:13px}.metric-value{font-weight:600;color:#374151}.metric-weight{font-weight:600;color:var(--muted)}.metric-contrib{font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums}.pts-bar-wrap{height:6px;background:#e2e8f0;border-radius:999px;overflow:hidden;width:80px;display:inline-block;vertical-align:middle;margin-right:6px}.pts-bar{height:100%;border-radius:999px;transition:width .4s ease}.pts-good{color:var(--green);font-weight:700}.pts-warn{color:var(--yellow);font-weight:700}.pts-bad{color:var(--red);font-weight:700}.metrics-total-row td{background:#f8faff;font-size:13px}.metric-total-value{font-size:16px;font-weight:800;color:#0f172a}.ct-badge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.ct-added{background:#dcfce7;color:#15803d;border:1px solid #86efac}.ct-untracked{background:#ede9fe;color:#6d28d9;border:1px solid #c4b5fd}.ct-modified{background:#dbeafe;color:#1d4ed8;border:1px solid #93c5fd}.ct-deleted{background:#fee2e2;color:#b91c1c;border:1px solid #fca5a5}.ct-renamed{background:#fef3c7;color:#a16207;border:1px solid #fde68a}.ct-default{background:#f1f5f9;color:#475569}.ct-feat{background:#dbeafe;color:#1d4ed8}.ct-fix{background:#fee2e2;color:#b91c1c}.ct-chore{background:#f1f5f9;color:#475569}.ct-docs{background:#fef3c7;color:#a16207}.ct-test{background:#ede9fe;color:#6d28d9}.ct-refactor{background:#f0fdfa;color:#0f766e}.ct-style{background:#fdf4ff;color:#9333ea}.ct-perf{background:#fff7ed;color:#c2410c}.file-path code{max-width:380px;display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.diff-bar{display:inline-flex;height:8px;border-radius:4px;overflow:hidden;background:#e2e8f0;vertical-align:middle;min-width:4px}.diff-add{background:#4ade80;height:100%}.diff-del{background:#f87171;height:100%}.diff-labels{font-size:11px;margin-left:6px;font-variant-numeric:tabular-nums}.add-lbl{color:var(--green);font-weight:700}.del-lbl{color:var(--red);font-weight:700;margin-left:3px}.prio-dot{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:999px;font-size:12px;font-weight:800}.prio-1{background:#fee2e2;color:#b91c1c}.prio-2{background:#ffedd5;color:#c2410c}.prio-3{background:#fef3c7;color:#a16207}.prio-4{background:#dbeafe;color:#1d4ed8}.prio-5{background:#f1f5f9;color:#475569}.divisible-yes{color:var(--green);font-size:13px;font-weight:700}.divisible-no{color:var(--red);font-size:13px;font-weight:700}.block-id{background:#f1f5f9;color:#334155;font-size:11px}.block-files code{font-size:10px;margin:1px}.timeline{position:relative;padding-left:28px}.timeline::before{content:"";position:absolute;left:9px;top:16px;bottom:16px;width:2px;background:#e2e8f0;border-radius:999px}.timeline-item{position:relative;margin-bottom:20px}.timeline-dot{position:absolute;left:-24px;top:14px;width:12px;height:12px;background:#3b82f6;border:2px solid #fff;border-radius:999px;box-shadow:0 0 0 2px #bfdbfe}.timeline-body{background:var(--surface-soft);border:1px solid var(--border);border-radius:12px;padding:12px 16px}.timeline-header{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:6px}.commit-index{background:#e2e8f0;color:#475569;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:800}.commit-type-badge{padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.commit-msg{font-size:13px;color:#0f172a;background:#f8fafc;border:1px solid #e2e8f0;padding:2px 8px;border-radius:6px;word-break:break-word}.timeline-meta{display:flex;flex-wrap:wrap;gap:12px;font-size:12px;color:var(--muted);margin-bottom:8px}.timeline-meta span{display:flex;align-items:center;gap:3px}.commit-files-list{margin:0;padding-left:16px;list-style:disc}.commit-files-list li{margin:2px 0;font-size:12px}.commit-files-list code{font-size:11px;padding:1px 5px}.dep-arrow{color:var(--muted);font-size:16px;vertical-align:middle}.footer{margin-top:48px;border-top:1px solid var(--border);padding-top:20px;text-align:center;font-size:12px;color:var(--muted)}.copy-btn{background:none;border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:1px 6px;cursor:pointer;font-size:11px;color:#64748b;line-height:1.4;transition:background .15s,color .15s;flex-shrink:0;vertical-align:middle}.copy-btn:hover{background:#f1f5f9;color:#0f172a;border-color:#94a3b8}.commit-type-pills{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.commit-type-pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;opacity:.85}.toc-bar-wrap{width:50px;height:4px;background:rgba(0,0,0,.1);border-radius:999px;overflow:hidden;flex-shrink:0}.toc-bar{height:100%;border-radius:999px;transition:width .4s ease}.toc-bar-ok{background:var(--green)}.toc-bar-warn{background:var(--yellow)}.toc-bar-bad{background:var(--red)}.toc-commits{font-size:11px;opacity:.65;font-weight:500;white-space:nowrap}.back-to-top{text-align:right;padding-top:12px;border-top:1px solid var(--border);margin-top:20px}.back-link{font-size:12px;color:var(--muted);text-decoration:none;font-weight:600}.back-link:hover{color:#3b82f6;text-decoration:underline}.git-commands-block{background:#0f172a;border-radius:14px;padding:18px 20px;overflow-x:auto;margin-top:8px;box-shadow:inset 0 1px 4px rgba(0,0,0,.3)}.git-commands-block pre{margin:0;font-family:"JetBrains Mono","Fira Mono",ui-monospace,monospace;font-size:12px;line-height:1.8;color:#e2e8f0;white-space:pre}.git-commands-block .gc-comment{color:#4b6988;font-style:italic}.git-commands-block .gc-cmd{color:#7dd3fc;font-weight:700}.git-commands-block .gc-branch{color:#86efac}.git-commands-block .gc-file{color:#fca5a5}.git-commands-block .gc-msg{color:#fde68a}.ct-ci{background:#ecfdf5;color:#059669}.ct-revert{background:#fdf4ff;color:#c026d3}@media(max-width: 960px){.hero{padding:24px 20px}.hero-title-area h1{font-size:26px}.plan-card-header{padding:18px}.plan-card-body{padding:18px}.file-path code{max-width:200px}}@media(max-width: 640px){.container{padding:16px 12px 60px}.kpi-row{gap:8px}.kpi{padding:8px 12px;min-width:72px}.kpi-v{font-size:18px}}@media print{body{background:#fff;font-size:12px}.container{max-width:100%;padding:0}.hero{border-radius:0;box-shadow:none}.plan-card,.card,.stat-card,table{box-shadow:none;break-inside:avoid}.section{break-before:auto}a{color:inherit;text-decoration:none}}`;
10
+ export const reportStyles = `@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap";:root{--bg: #f0f4f8;--surface: #ffffff;--surface-soft: #f7f9fc;--border: #dde3ed;--text: #1e293b;--muted: #64748b;--green: #15803d;--green-soft: #dcfce7;--green-border: #86efac;--yellow: #a16207;--yellow-soft: #fef3c7;--yellow-border: #fde68a;--orange: #c2410c;--orange-soft: #ffedd5;--orange-border: #fdba74;--red: #b91c1c;--red-soft: #fee2e2;--red-border: #fca5a5;--blue: #1d4ed8;--blue-soft: #dbeafe;--blue-border: #93c5fd;--purple: #6d28d9;--purple-soft: #ede9fe;--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.07), 0 1px 2px rgba(15, 23, 42, 0.05);--shadow: 0 4px 16px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.05);--shadow-lg: 0 10px 40px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(15, 23, 42, 0.06);--radius: 14px;--radius-sm: 8px}*,*::before,*::after{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;padding:0;background:var(--bg);color:var(--text);font-family:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.6}.container{max-width:1320px;margin:0 auto;padding:32px 24px 80px}.hero{background:linear-gradient(135deg, #0f172a 0%, #1e3a6e 50%, #1d4ed8 100%);color:#fff;border-radius:24px;padding:36px 36px 28px;box-shadow:var(--shadow-lg);margin-bottom:28px;position:relative;overflow:hidden}.hero::before{content:"";position:absolute;inset:0;background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");pointer-events:none}.hero-top{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;flex-wrap:wrap}.hero-title-area h1{margin:0 0 6px;font-size:34px;font-weight:800;letter-spacing:-0.5px;line-height:1.1}.hero-title-area p{margin:0;color:hsla(0,0%,100%,.75);font-size:14px;max-width:500px}.hero-timestamp{background:hsla(0,0%,100%,.12);border:1px solid hsla(0,0%,100%,.2);border-radius:10px;padding:8px 14px;font-size:12px;color:hsla(0,0%,100%,.85);white-space:nowrap}.hero-grid{margin-top:24px;display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:12px}.hero-item{background:hsla(0,0%,100%,.11);border:1px solid hsla(0,0%,100%,.16);border-radius:14px;padding:14px 18px;backdrop-filter:blur(4px);transition:background .2s}.hero-item:hover{background:hsla(0,0%,100%,.17)}.hero-item .label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;opacity:.7;margin-bottom:5px}.hero-item .value{font-size:17px;font-weight:700;word-break:break-word}.disclaimer-banner{display:flex;align-items:flex-start;gap:16px;margin:24px 0 0;padding:20px 24px;background:#fefce8;border:2px solid #f59e0b;border-radius:12px;box-shadow:0 2px 8px rgba(245,158,11,.18)}.disclaimer-banner .disclaimer-icon{font-size:32px;line-height:1;flex-shrink:0;margin-top:2px}.disclaimer-banner .disclaimer-body{flex:1;color:#78350f}.disclaimer-banner .disclaimer-body strong:first-child{display:block;font-size:15px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:#92400e;margin-bottom:8px}.disclaimer-banner .disclaimer-body p{margin:0 0 8px;font-size:14px;line-height:1.6}.disclaimer-banner .disclaimer-body p:last-child{margin-bottom:0}.disclaimer-banner .disclaimer-body ul{margin:6px 0 10px 18px;padding:0;font-size:14px;line-height:1.7}.disclaimer-banner .disclaimer-body .disclaimer-footer{font-size:12px;font-style:italic;color:#a16207;margin-top:8px}.section{margin-top:36px}.section-heading{display:flex;align-items:center;gap:10px;margin:0 0 16px;font-size:20px;font-weight:700;color:#0f172a}.section-heading::after{content:"";flex:1;height:1px;background:var(--border)}.summary-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));gap:12px}.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;box-shadow:var(--shadow-sm);display:flex;flex-direction:column;gap:4px;transition:box-shadow .2s,transform .2s}.stat-card:hover{box-shadow:var(--shadow);transform:translateY(-1px)}.stat-card .label{font-size:12px;color:var(--muted);font-weight:500;text-transform:uppercase;letter-spacing:.04em}.stat-card .value{font-size:26px;font-weight:800;line-height:1.2}.stat-card .sub{font-size:12px;color:var(--muted)}.stat-card.blue .value{color:var(--blue)}.stat-card.green .value{color:var(--green)}.stat-card.yellow .value{color:var(--yellow)}.stat-card.red .value{color:var(--red)}.stat-card.purple .value{color:var(--purple)}.card{background:var(--surface);border:1px solid var(--border);border-radius:18px;padding:22px;box-shadow:var(--shadow);margin-bottom:20px}.badges{display:flex;flex-wrap:wrap;gap:8px}.badge{display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;border:1px solid rgba(0,0,0,0)}.badge.green{background:var(--green-soft);color:var(--green);border-color:var(--green-border)}.badge.yellow{background:var(--yellow-soft);color:var(--yellow);border-color:var(--yellow-border)}.badge.orange{background:var(--orange-soft);color:var(--orange);border-color:var(--orange-border)}.badge.red{background:var(--red-soft);color:var(--red);border-color:var(--red-border)}.badge.blue{background:var(--blue-soft);color:var(--blue);border-color:var(--blue-border)}.badge.purple{background:var(--purple-soft);color:var(--purple);border-color:#c4b5fd}table{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;box-shadow:var(--shadow-sm);margin:10px 0 20px}table thead th{background:#f1f5fd;color:#374151;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;text-align:left;padding:11px 14px;border-bottom:1px solid var(--border)}table tbody td{padding:10px 14px;border-bottom:1px solid #e9eff7;vertical-align:middle;font-size:13px}table tbody tr:last-child td{border-bottom:none}table tbody tr:nth-child(even){background:#fafbff}table tbody tr:hover{background:#f0f6ff}.num-cell{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}code{background:#eef2ff;color:#312e81;padding:2px 7px;border-radius:6px;font-size:12px;font-family:"SF Mono","Fira Code","Cascadia Code",monospace;word-break:break-word}.muted{color:var(--muted);font-size:13px}.toc-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;box-shadow:var(--shadow-sm);margin-bottom:20px}.toc-title{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px}.toc-list{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.toc-link{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;font-size:12px;font-weight:600;text-decoration:none;border:1px solid rgba(0,0,0,0);transition:opacity .15s}.toc-link:hover{opacity:.8;text-decoration:none}.toc-ok{background:var(--green-soft);color:var(--green);border-color:var(--green-border)}.toc-warn{background:var(--yellow-soft);color:var(--yellow);border-color:var(--yellow-border)}.toc-bad{background:var(--red-soft);color:var(--red);border-color:var(--red-border)}.toc-score{background:rgba(0,0,0,.08);border-radius:999px;padding:1px 7px;font-size:11px;font-weight:800}.plan-card{border-radius:20px;overflow:hidden;box-shadow:var(--shadow);border:1px solid var(--border);margin-bottom:28px}.plan-card-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;padding:22px 26px}.plan-card-body{background:var(--surface);padding:22px 26px}.plan-header-green{background:linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);border-bottom:1px solid #86efac}.plan-header-yellow{background:linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);border-bottom:1px solid #fde68a}.plan-header-red{background:linear-gradient(135deg, #fff1f2 0%, #fee2e2 100%);border-bottom:1px solid #fca5a5}.plan-header-left{display:flex;align-items:center;gap:14px}.plan-branch-icon{font-size:28px;width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.06);border-radius:14px;flex-shrink:0}.plan-branch-name{font-size:17px;font-weight:800;color:#0f172a;word-break:break-all;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.plan-branch-sub{font-size:13px;color:var(--muted);margin-top:2px}.existing-badge{display:inline-block;background:#dbeafe;color:#1e40af;border:1px solid #93c5fd;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;margin-right:4px}.plan-header-right{display:flex;flex-direction:column;align-items:center;gap:4px}.gauge-svg{display:block}.plan-score-label{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.score-label-green{color:var(--green)}.score-label-yellow{color:var(--yellow)}.score-label-red{color:var(--red)}.kpi-row{display:flex;flex-wrap:wrap;gap:12px;margin:16px 0 24px}.kpi{background:var(--surface-soft);border:1px solid var(--border);border-radius:12px;padding:10px 16px;min-width:90px;text-align:center}.kpi-v{font-size:22px;font-weight:800;color:#0f172a;line-height:1.1}.kpi-k{font-size:11px;color:var(--muted);margin-top:3px;text-transform:uppercase;letter-spacing:.04em}.recommendation-box{display:flex;align-items:flex-start;gap:10px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:12px;padding:12px 16px;margin-bottom:16px;font-size:14px;color:#0369a1}.exclusion-note{display:flex;align-items:flex-start;gap:8px;background:#fffbeb;border:1px solid #fde68a;border-radius:10px;padding:10px 14px;margin-bottom:16px;font-size:13px;color:#92400e}.rec-icon{font-size:16px;flex-shrink:0}.section-title{font-size:14px;font-weight:700;color:#0f172a;margin:28px 0 10px;display:flex;align-items:center;gap:6px}.metrics-table thead th{background:#f8faff}.metric-code{background:#e0e7ff;color:#3730a3;font-weight:700}.metric-label{font-size:13px}.metric-value{font-weight:600;color:#374151}.metric-weight{font-weight:600;color:var(--muted)}.metric-contrib{font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums}.pts-bar-wrap{height:6px;background:#e2e8f0;border-radius:999px;overflow:hidden;width:80px;display:inline-block;vertical-align:middle;margin-right:6px}.pts-bar{height:100%;border-radius:999px;transition:width .4s ease}.pts-good{color:var(--green);font-weight:700}.pts-warn{color:var(--yellow);font-weight:700}.pts-bad{color:var(--red);font-weight:700}.metrics-total-row td{background:#f8faff;font-size:13px}.metric-total-value{font-size:16px;font-weight:800;color:#0f172a}.ct-badge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.ct-added{background:#dcfce7;color:#15803d;border:1px solid #86efac}.ct-untracked{background:#ede9fe;color:#6d28d9;border:1px solid #c4b5fd}.ct-modified{background:#dbeafe;color:#1d4ed8;border:1px solid #93c5fd}.ct-deleted{background:#fee2e2;color:#b91c1c;border:1px solid #fca5a5}.ct-renamed{background:#fef3c7;color:#a16207;border:1px solid #fde68a}.ct-default{background:#f1f5f9;color:#475569}.ct-feat{background:#dbeafe;color:#1d4ed8}.ct-fix{background:#fee2e2;color:#b91c1c}.ct-chore{background:#f1f5f9;color:#475569}.ct-docs{background:#fef3c7;color:#a16207}.ct-test{background:#ede9fe;color:#6d28d9}.ct-refactor{background:#f0fdfa;color:#0f766e}.ct-style{background:#fdf4ff;color:#9333ea}.ct-perf{background:#fff7ed;color:#c2410c}.file-path code{max-width:380px;display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.diff-bar{display:inline-flex;height:8px;border-radius:4px;overflow:hidden;background:#e2e8f0;vertical-align:middle;min-width:4px}.diff-add{background:#4ade80;height:100%}.diff-del{background:#f87171;height:100%}.diff-labels{font-size:11px;margin-left:6px;font-variant-numeric:tabular-nums}.add-lbl{color:var(--green);font-weight:700}.del-lbl{color:var(--red);font-weight:700;margin-left:3px}.prio-dot{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:999px;font-size:12px;font-weight:800}.prio-1{background:#fee2e2;color:#b91c1c}.prio-2{background:#ffedd5;color:#c2410c}.prio-3{background:#fef3c7;color:#a16207}.prio-4{background:#dbeafe;color:#1d4ed8}.prio-5{background:#f1f5f9;color:#475569}.divisible-yes{color:var(--green);font-size:13px;font-weight:700}.divisible-no{color:var(--red);font-size:13px;font-weight:700}.block-id{background:#f1f5f9;color:#334155;font-size:11px}.block-files code{font-size:10px;margin:1px}.timeline{position:relative;padding-left:28px}.timeline::before{content:"";position:absolute;left:9px;top:16px;bottom:16px;width:2px;background:#e2e8f0;border-radius:999px}.timeline-item{position:relative;margin-bottom:20px}.timeline-dot{position:absolute;left:-24px;top:14px;width:12px;height:12px;background:#3b82f6;border:2px solid #fff;border-radius:999px;box-shadow:0 0 0 2px #bfdbfe}.timeline-body{background:var(--surface-soft);border:1px solid var(--border);border-radius:12px;padding:12px 16px}.timeline-header{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:6px}.commit-index{background:#e2e8f0;color:#475569;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:800}.commit-type-badge{padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.commit-msg{font-size:13px;color:#0f172a;background:#f8fafc;border:1px solid #e2e8f0;padding:2px 8px;border-radius:6px;word-break:break-word}.timeline-meta{display:flex;flex-wrap:wrap;gap:12px;font-size:12px;color:var(--muted);margin-bottom:8px}.timeline-meta span{display:flex;align-items:center;gap:3px}.commit-files-list{margin:0;padding-left:16px;list-style:disc}.commit-files-list li{margin:2px 0;font-size:12px}.commit-files-list code{font-size:11px;padding:1px 5px}.dep-arrow{color:var(--muted);font-size:16px;vertical-align:middle}.footer{margin-top:48px;border-top:1px solid var(--border);padding-top:20px;text-align:center;font-size:12px;color:var(--muted)}.copy-btn{background:none;border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:1px 6px;cursor:pointer;font-size:11px;color:#64748b;line-height:1.4;transition:background .15s,color .15s;flex-shrink:0;vertical-align:middle}.copy-btn:hover{background:#f1f5f9;color:#0f172a;border-color:#94a3b8}.commit-type-pills{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.commit-type-pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;opacity:.85}.toc-bar-wrap{width:50px;height:4px;background:rgba(0,0,0,.1);border-radius:999px;overflow:hidden;flex-shrink:0}.toc-bar{height:100%;border-radius:999px;transition:width .4s ease}.toc-bar-ok{background:var(--green)}.toc-bar-warn{background:var(--yellow)}.toc-bar-bad{background:var(--red)}.toc-commits{font-size:11px;opacity:.65;font-weight:500;white-space:nowrap}.back-to-top{text-align:right;padding-top:12px;border-top:1px solid var(--border);margin-top:20px}.back-link{font-size:12px;color:var(--muted);text-decoration:none;font-weight:600}.back-link:hover{color:#3b82f6;text-decoration:underline}.git-commands-block{background:#0f172a;border-radius:14px;padding:18px 20px;overflow-x:auto;margin-top:8px;box-shadow:inset 0 1px 4px rgba(0,0,0,.3)}.git-commands-block pre{margin:0;font-family:"JetBrains Mono","Fira Mono",ui-monospace,monospace;font-size:12px;line-height:1.8;color:#e2e8f0;white-space:pre}.git-commands-block .gc-comment{color:#4b6988;font-style:italic}.git-commands-block .gc-cmd{color:#7dd3fc;font-weight:700}.git-commands-block .gc-branch{color:#86efac}.git-commands-block .gc-file{color:#fca5a5}.git-commands-block .gc-msg{color:#fde68a}.ct-ci{background:#ecfdf5;color:#059669}.ct-revert{background:#fdf4ff;color:#c026d3}.origin-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:9999px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;white-space:nowrap;border:1px solid rgba(0,0,0,0)}.origin-pushed{background:#cffafe;color:#0e7490;border-color:#67e8f9}.origin-local-commit{background:#ede9fe;color:#6d28d9;border-color:#c4b5fd}.origin-working-tree{background:#fef3c7;color:#a16207;border-color:#fde68a}.origin-untracked{background:#f1f5f9;color:#475569;border-color:#cbd5e1}@media(max-width: 960px){.hero{padding:24px 20px}.hero-title-area h1{font-size:26px}.plan-card-header{padding:18px}.plan-card-body{padding:18px}.file-path code{max-width:200px}}@media(max-width: 640px){.container{padding:16px 12px 60px}.kpi-row{gap:8px}.kpi{padding:8px 12px;min-width:72px}.kpi-v{font-size:18px}}@media print{body{background:#fff;font-size:12px}.container{max-width:100%;padding:0}.hero{border-radius:0;box-shadow:none}.plan-card,.card,.stat-card,table{box-shadow:none;break-inside:avoid}.section{break-before:auto}a{color:inherit;text-decoration:none}}`;
@@ -153,6 +153,19 @@ function buildMetricsBreakdownTable(plan, config) {
153
153
  </tbody>
154
154
  </table>`;
155
155
  }
156
+ /**
157
+ * Devuelve el badge HTML para el origen de un archivo en el árbol git.
158
+ *
159
+ * @param origin - Valor de `FileOrigin` (o `undefined` → trata como `"working-tree"`).
160
+ */
161
+ function originHtmlBadge(origin) {
162
+ const cls = `origin-badge origin-${origin ?? "working-tree"}`;
163
+ const label = origin === "pushed" ? "REMOTO"
164
+ : origin === "local-commit" ? "LOCAL"
165
+ : origin === "untracked" ? "NUEVO"
166
+ : "WIP";
167
+ return `<span class="${esc(cls)}">${label}</span>`;
168
+ }
156
169
  /**
157
170
  * Genera una fila `<tr>` para la tabla de archivos con mini-barras visuales
158
171
  * proporcionales al máximo de líneas del conjunto.
@@ -173,6 +186,7 @@ function buildFilesBarsRow(f, maxLines) {
173
186
  const addPct = f.lines > 0 ? Math.round((f.additions / f.lines) * totalBar) : 0;
174
187
  const delPct = totalBar - addPct;
175
188
  return `<tr>
189
+ <td>${originHtmlBadge(f.origin)}</td>
176
190
  <td><span class="ct-badge ${ctClass}">${esc(changeTypeLabel(f.changeType))}</span></td>
177
191
  <td class="file-path"><code>${esc(f.path)}</code></td>
178
192
  <td class="num-cell">${f.lines}</td>
@@ -401,15 +415,20 @@ function buildReportStyles() {
401
415
  */
402
416
  export function renderHtmlReport(input) {
403
417
  const { currentBranch, baseBranch, config, fileStats, deps, blocks, plans } = input;
404
- const totalFiles = fileStats.length;
405
- const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
406
- const totalAdditions = fileStats.reduce((sum, f) => sum + f.additions, 0);
407
- const totalDeletions = fileStats.reduce((sum, f) => sum + f.deletions, 0);
408
- const createdFiles = fileStats.filter((f) => f.changeType === "A" || f.changeType === "U").length;
409
- const updatedFiles = fileStats.filter((f) => f.changeType === "M").length;
410
- const deletedFiles = fileStats.filter((f) => f.changeType === "D").length;
411
- const renamedFiles = fileStats.filter((f) => f.changeType === "R").length;
412
- 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;
413
432
  const avgScore = plans.length > 0
414
433
  ? plans.reduce((sum, p) => sum + p.score, 0) / plans.length
415
434
  : 0;
@@ -473,8 +492,9 @@ export function renderHtmlReport(input) {
473
492
  <div class="value" style="color:#f87171">${riskPlans}</div>
474
493
  </div>
475
494
  <div class="hero-item">
476
- <div class="label">Archivos modificados</div>
477
- <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>` : ""}
478
498
  </div>
479
499
  </div>
480
500
  </header>
@@ -572,9 +592,21 @@ export function renderHtmlReport(input) {
572
592
  <!-- ══ DETALLE DE ARCHIVOS ════════════════════════════════════════ -->
573
593
  <section class="section">
574
594
  <h2 class="section-heading">📄 Detalle de archivos</h2>
595
+ ${hasInfoFiles ? `<div class="disclaimer-banner" style="margin-bottom:16px;padding:12px 18px" role="note">
596
+ <div class="disclaimer-icon" style="font-size:1.1em">ℹ️</div>
597
+ <div class="disclaimer-body" style="margin:0">
598
+ <strong>Archivos informativos</strong>
599
+ — ${infoFileCount} archivo${infoFileCount !== 1 ? "s" : ""} con badge
600
+ <span class="origin-badge origin-pushed">REMOTO</span> o
601
+ <span class="origin-badge origin-local-commit">LOCAL</span>
602
+ se muestran a modo de referencia.
603
+ Sus métricas <strong>no</strong> se incluyen en el resumen de cambios ni en el score.
604
+ </div>
605
+ </div>` : ""}
575
606
  <table>
576
607
  <thead>
577
608
  <tr>
609
+ <th>Origen</th>
578
610
  <th>Tipo</th>
579
611
  <th>Archivo</th>
580
612
  <th>Líneas</th>
@@ -679,7 +711,7 @@ export function renderHtmlReport(input) {
679
711
  <!-- ══ FOOTER ════════════════════════════════════════════════════ -->
680
712
  <footer class="footer">
681
713
  <p>Generado por <strong>${APP_REPORT_TITLE}</strong> · ${timestamp}</p>
682
- <p>Score objetivo: <strong>${config.targetScore}</strong> · Ramas analizadas: <strong>${plans.length}</strong> · Archivos: <strong>${totalFiles}</strong></p>
714
+ <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>
683
715
  </footer>
684
716
 
685
717
  </div>
@@ -764,7 +796,10 @@ export function renderScoreHtmlReport(input) {
764
796
  <div class="hero-item"><div class="label">Rama base</div><div class="value">⎇ ${esc(baseBranch)}</div></div>
765
797
  <div class="hero-item"><div class="label">Score actual</div><div class="value">${result.complexity.toFixed(2)}</div></div>
766
798
  <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>
799
+ <div class="hero-item">
800
+ <div class="label">${input.scoredFileCount !== undefined && input.scoredFileCount !== totalFiles ? "Archivos scored / total" : "Archivos"}</div>
801
+ <div class="value">${input.scoredFileCount !== undefined && input.scoredFileCount !== totalFiles ? `${input.scoredFileCount} / ${totalFiles}` : totalFiles}</div>
802
+ </div>
768
803
  <div class="hero-item"><div class="label">Líneas totales</div><div class="value">${totalLines}</div></div>
769
804
  <div class="hero-item"><div class="label">Commits adelantados</div><div class="value">${commitCount}</div></div>
770
805
  <div class="hero-item"><div class="label">Estado</div><div class="value score-label-${scoreClass}">${esc(label)}</div></div>
@@ -773,6 +808,12 @@ export function renderScoreHtmlReport(input) {
773
808
 
774
809
  <section class="section">
775
810
  <h2 class="section-heading">🎯 Score</h2>
811
+ ${input.scoredFileCount !== undefined && input.scoredFileCount !== totalFiles
812
+ ? `<div class="recommendation-box" style="margin-bottom:16px">
813
+ <span class="rec-icon">ℹ️</span>
814
+ <span>Score calculado sobre <strong>${input.scoredFileCount} archivos del working tree</strong> (cambios no commiteados) de <strong>${totalFiles} archivos</strong> analizados. Los archivos <span class="origin-badge origin-pushed">REMOTO</span> y <span class="origin-badge origin-local-commit">LOCAL</span> se muestran en la tabla pero <strong>no afectan el score</strong>.</span>
815
+ </div>`
816
+ : ""}
776
817
  <div class="card" style="display:flex;align-items:center;gap:32px;padding:24px">
777
818
  ${buildScoreGaugeSvg(result.complexity, config.targetScore)}
778
819
  <div>
@@ -803,9 +844,10 @@ export function renderScoreHtmlReport(input) {
803
844
  <h2 class="section-heading">📈 Valores brutos usados</h2>
804
845
  <div class="summary-grid">
805
846
  <div class="stat-card blue"><div class="label">Commits</div><div class="value">${commitCount}</div></div>
847
+ <div class="stat-card blue"><div class="label">Archivos scored (WIP)</div><div class="value">${input.scoredFileCount ?? totalFiles}</div></div>
806
848
  <div class="stat-card blue"><div class="label">Archivos / commit</div><div class="value">${filesPerCommit}</div></div>
849
+ <div class="stat-card blue"><div class="label">Líneas scored (WIP)</div><div class="value">${input.scoredLines ?? totalLines}</div></div>
807
850
  <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
851
  <div class="stat-card green"><div class="label">Líneas agregadas</div><div class="value">+${totalAdditions}</div></div>
810
852
  <div class="stat-card red"><div class="label">Líneas eliminadas</div><div class="value">-${totalDeletions}</div></div>
811
853
  </div>
@@ -815,7 +857,7 @@ export function renderScoreHtmlReport(input) {
815
857
  <h2 class="section-heading">📄 Detalle de archivos</h2>
816
858
  <table>
817
859
  <thead>
818
- <tr><th>Tipo</th><th>Archivo</th><th>Líneas</th><th>Cambios (+/-)</th><th>Prioridad</th></tr>
860
+ <tr><th>Origen</th><th>Tipo</th><th>Archivo</th><th>Líneas</th><th>Cambios (+/-)</th><th>Prioridad</th></tr>
819
861
  </thead>
820
862
  <tbody>
821
863
  ${fileStats.map((f) => buildFilesBarsRow(f, maxLinesForBar)).join("")}
package/dist/output/ui.js CHANGED
@@ -338,8 +338,7 @@ function summaryCard(title, items) {
338
338
  });
339
339
  card(title, lines, "cyan");
340
340
  }
341
- /**
342
- * Genera la representación visual de un bloque para tablas y listas.
341
+ /** Genera la representación visual de un bloque para tablas y listas.
343
342
  * Distingue bloques grandes (`large::`) de bloques agrupados normales.
344
343
  */
345
344
  function blockLabel(id) {
@@ -352,6 +351,25 @@ function blockLabel(id) {
352
351
  }
353
352
  return `${badge("block", "blue")} ${chalk.whiteBright(id)}`;
354
353
  }
354
+ /**
355
+ * Genera una pastilla de terminal que indica el origen de un archivo en el árbol git.
356
+ *
357
+ * - REMOTO (cyan) : existe en commits ya publicados al remoto.
358
+ * - LOCAL (magenta) : commiteado localmente, aún no publicado.
359
+ * - WIP (yellow) : cambio en el working tree (staged o unstaged).
360
+ * - NUEVO (white) : archivo nuevo sin seguimiento git.
361
+ *
362
+ * @param origin - Valor de `FileOrigin` o `undefined` (trata como `"working-tree"`).
363
+ */
364
+ function originBadge(origin) {
365
+ switch (origin) {
366
+ case "pushed": return badge("REMOTO", "cyan");
367
+ case "local-commit": return badge("LOCAL", "magenta");
368
+ case "untracked": return badge("NUEVO", "white");
369
+ case "working-tree":
370
+ default: return badge("WIP", "yellow");
371
+ }
372
+ }
355
373
  /** Imprime una línea horizontal de 100 guiones en gris para separar entradas dentro de una sección. */
356
374
  function divider() {
357
375
  process.stdout.write(chalk.gray("─".repeat(100)) + "\n");
@@ -410,6 +428,7 @@ export const ui = {
410
428
  scoreColor,
411
429
  score,
412
430
  blockLabel,
431
+ originBadge,
413
432
  divider,
414
433
  confirm,
415
434
  prompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pull-request-split-advisor",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
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",