pull-request-split-advisor 3.2.1 → 3.2.2

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";
@@ -129,9 +130,10 @@ function printSummary(fileStats, config, currentBranch, baseBranch) {
129
130
  }
130
131
  function printFileStats(fileStats) {
131
132
  ui.section("DETALLE DE ARCHIVOS", "#");
132
- ui.table(["Marca", "Archivo", "Lineas", "+", "-", "Prioridad"], fileStats.map((f) => [
133
+ ui.table(["Origen", "Marca", "Archivo", "Lineas", "+", "-", "Prioridad"], fileStats.map((f) => [
134
+ ui.originBadge(f.origin),
133
135
  changeTypeLabel(f.changeType),
134
- ui.truncateMiddle(f.path, 55),
136
+ ui.truncateMiddle(f.path, 50),
135
137
  f.lines,
136
138
  f.additions,
137
139
  f.deletions,
@@ -350,7 +352,7 @@ async function main() {
350
352
  const changedFiles = gatherChangedFiles(config, baseBranch);
351
353
  if (!changedFiles.length) {
352
354
  ui.spinner.stop();
353
- ui.warn("No hay cambios sin commit en el working tree.");
355
+ ui.warn("No hay cambios respecto a la rama base.");
354
356
  closeReadlineInterface();
355
357
  return;
356
358
  }
@@ -365,6 +367,16 @@ async function main() {
365
367
  const plans = findBestPlan(blocks, currentBranch, config, deps);
366
368
  config.testCoveragePercent = computeTestCoveragePercent(fileStats);
367
369
  ui.spinner.stop("Analisis completado.");
370
+ // ─── Computar origen de cada archivo en el árbol git ─────────────────────
371
+ // Diferencia entre: ya publicado (REMOTO), commiteado local (LOCAL),
372
+ // cambio sin commitear (WIP) y archivo nuevo sin rastrear (NUEVO).
373
+ // Resetear la caché de tracked files para que computeFileOrigins use
374
+ // un Set fresco (evita stale data si el index cambió durante el análisis).
375
+ resetTrackedFilesCache();
376
+ const origins = computeFileOrigins(changedFiles, baseBranch, currentBranch, remote);
377
+ for (const stat of fileStats) {
378
+ stat.origin = origins.get(stat.path);
379
+ }
368
380
  // ─── Enriquecimiento con IA (opcional) ───────────────────────────────────
369
381
  // Si la IA no está habilitada o configurada, `aiEnrichPlans` retorna
370
382
  // de inmediato sin modificar los planes (modo heurístico puro).
@@ -453,13 +465,38 @@ async function main() {
453
465
  }
454
466
  const fileStats = getFileStats(changedFiles, baseBranch);
455
467
  ui.spinner.stop("Calculo completado.");
468
+ // ─── Computar origen de cada archivo en el árbol git ─────────────────────
469
+ // Resetear caché para asegurar datos frescos (el score subcommand no llama
470
+ // a requireCleanIndex, por lo que puede haber archivos staged/unstaged que
471
+ // cambien el estado del index entre la primera lectura y esta clasificación).
472
+ resetTrackedFilesCache();
473
+ const scoreRemote = detectRemote(currentBranch);
474
+ const scoreOrigins = computeFileOrigins(changedFiles, baseBranch, currentBranch, scoreRemote);
475
+ for (const stat of fileStats) {
476
+ stat.origin = scoreOrigins.get(stat.path);
477
+ }
456
478
  const totalFiles = fileStats.length;
457
479
  const totalLines = fileStats.reduce((sum, f) => sum + f.lines, 0);
480
+ // El score se calcula ÚNICAMENTE sobre archivos del working tree en ese momento.
481
+ // Los archivos ya commiteados (LOCAL o REMOTO) se muestran en el reporte
482
+ // con su badge de origen pero NO afectan las métricas del score.
483
+ const scoredStats = fileStats.filter((f) => f.origin === "working-tree" || f.origin === "untracked");
484
+ const scoredFiles = scoredStats.length;
485
+ const scoredLines = scoredStats.reduce((sum, f) => sum + f.lines, 0);
486
+ // Si no hay archivos WIP no hay nada que puntuar: todos los cambios ya
487
+ // están en commits (LOCAL o REMOTO) y el working tree está limpio.
488
+ if (scoredFiles === 0) {
489
+ ui.warn("No hay cambios en el working tree para analizar. " +
490
+ "Todos los archivos detectados ya están en commits (LOCAL o REMOTO). " +
491
+ "Realiza cambios sin commitear para obtener un score.");
492
+ closeReadlineInterface();
493
+ return;
494
+ }
458
495
  const aheadCommits = localAheadCount(baseBranch);
459
496
  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);
497
+ const filesPerCommit = Number((scoredFiles / commitCount).toFixed(2));
498
+ const avgLinesPerCommit = Math.round(scoredLines / commitCount);
499
+ const result = scorePullRequest({ commitCount, filesPerCommit, avgLinesPerCommit, totalLinesChanged: scoredLines }, config);
463
500
  const warnThreshold = config.targetScore > 4 ? 4 : Math.max(0, config.targetScore - 1);
464
501
  const scoreColor = ui.scoreColor(result.complexity, config.targetScore);
465
502
  const statusBadge = result.complexity >= config.targetScore
@@ -476,18 +513,27 @@ async function main() {
476
513
  commitCount,
477
514
  filesPerCommit,
478
515
  avgLinesPerCommit,
479
- result
516
+ result,
517
+ scoredFileCount: scoredFiles !== totalFiles ? scoredFiles : undefined,
518
+ scoredLines: scoredFiles !== totalFiles ? scoredLines : undefined
480
519
  });
481
520
  ui.ok(`Reporte HTML generado: ${scoreHtmlFile}`);
521
+ const hasWipDistinction = scoredFiles !== totalFiles;
482
522
  ui.card(`Score actual de la rama: ${currentBranch}`, [
483
523
  `${statusBadge}`,
484
524
  "",
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}`,
525
+ `Rama base: ${baseBranch}`,
526
+ `Archivos totales analizados: ${totalFiles}${hasWipDistinction ? ` (${totalFiles - scoredFiles} ya commiteados, excluidos del score)` : ""}`,
527
+ ...(hasWipDistinction
528
+ ? [`Archivos scored (WIP): ${scoredFiles} ← base del score`]
529
+ : []),
530
+ `Líneas totales analizadas: ${totalLines}`,
531
+ ...(hasWipDistinction
532
+ ? [`Líneas scored (WIP): ${scoredLines} ← base del score`]
533
+ : []),
534
+ `Commits adelantados: ${commitCount}`,
535
+ `Archivos / commit (scored): ${filesPerCommit}`,
536
+ `Líneas / commit (scored avg):${avgLinesPerCommit}`,
491
537
  "",
492
538
  `Score: ${ui.score(result.complexity, config.targetScore)}`,
493
539
  `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>
@@ -575,6 +589,7 @@ export function renderHtmlReport(input) {
575
589
  <table>
576
590
  <thead>
577
591
  <tr>
592
+ <th>Origen</th>
578
593
  <th>Tipo</th>
579
594
  <th>Archivo</th>
580
595
  <th>Líneas</th>
@@ -764,7 +779,10 @@ export function renderScoreHtmlReport(input) {
764
779
  <div class="hero-item"><div class="label">Rama base</div><div class="value">⎇ ${esc(baseBranch)}</div></div>
765
780
  <div class="hero-item"><div class="label">Score actual</div><div class="value">${result.complexity.toFixed(2)}</div></div>
766
781
  <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>
782
+ <div class="hero-item">
783
+ <div class="label">${input.scoredFileCount !== undefined && input.scoredFileCount !== totalFiles ? "Archivos scored / total" : "Archivos"}</div>
784
+ <div class="value">${input.scoredFileCount !== undefined && input.scoredFileCount !== totalFiles ? `${input.scoredFileCount} / ${totalFiles}` : totalFiles}</div>
785
+ </div>
768
786
  <div class="hero-item"><div class="label">Líneas totales</div><div class="value">${totalLines}</div></div>
769
787
  <div class="hero-item"><div class="label">Commits adelantados</div><div class="value">${commitCount}</div></div>
770
788
  <div class="hero-item"><div class="label">Estado</div><div class="value score-label-${scoreClass}">${esc(label)}</div></div>
@@ -773,6 +791,12 @@ export function renderScoreHtmlReport(input) {
773
791
 
774
792
  <section class="section">
775
793
  <h2 class="section-heading">🎯 Score</h2>
794
+ ${input.scoredFileCount !== undefined && input.scoredFileCount !== totalFiles
795
+ ? `<div class="recommendation-box" style="margin-bottom:16px">
796
+ <span class="rec-icon">ℹ️</span>
797
+ <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>
798
+ </div>`
799
+ : ""}
776
800
  <div class="card" style="display:flex;align-items:center;gap:32px;padding:24px">
777
801
  ${buildScoreGaugeSvg(result.complexity, config.targetScore)}
778
802
  <div>
@@ -803,9 +827,10 @@ export function renderScoreHtmlReport(input) {
803
827
  <h2 class="section-heading">📈 Valores brutos usados</h2>
804
828
  <div class="summary-grid">
805
829
  <div class="stat-card blue"><div class="label">Commits</div><div class="value">${commitCount}</div></div>
830
+ <div class="stat-card blue"><div class="label">Archivos scored (WIP)</div><div class="value">${input.scoredFileCount ?? totalFiles}</div></div>
806
831
  <div class="stat-card blue"><div class="label">Archivos / commit</div><div class="value">${filesPerCommit}</div></div>
832
+ <div class="stat-card blue"><div class="label">Líneas scored (WIP)</div><div class="value">${input.scoredLines ?? totalLines}</div></div>
807
833
  <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
834
  <div class="stat-card green"><div class="label">Líneas agregadas</div><div class="value">+${totalAdditions}</div></div>
810
835
  <div class="stat-card red"><div class="label">Líneas eliminadas</div><div class="value">-${totalDeletions}</div></div>
811
836
  </div>
@@ -815,7 +840,7 @@ export function renderScoreHtmlReport(input) {
815
840
  <h2 class="section-heading">📄 Detalle de archivos</h2>
816
841
  <table>
817
842
  <thead>
818
- <tr><th>Tipo</th><th>Archivo</th><th>Líneas</th><th>Cambios (+/-)</th><th>Prioridad</th></tr>
843
+ <tr><th>Origen</th><th>Tipo</th><th>Archivo</th><th>Líneas</th><th>Cambios (+/-)</th><th>Prioridad</th></tr>
819
844
  </thead>
820
845
  <tbody>
821
846
  ${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.2",
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",