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 +4 -1
- package/dist/cli.js +60 -14
- package/dist/core/file-stats.js +77 -0
- package/dist/core/planner.js +1 -1
- package/dist/output/report-styles.generated.js +1 -1
- package/dist/output/report.js +28 -3
- package/dist/output/ui.js +21 -2
- package/package.json +1 -1
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,
|
|
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
|
|
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((
|
|
461
|
-
const avgLinesPerCommit = Math.round(
|
|
462
|
-
const result = scorePullRequest({ commitCount, filesPerCommit, avgLinesPerCommit, totalLinesChanged:
|
|
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:
|
|
486
|
-
`Archivos
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
`Líneas
|
|
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}`,
|
package/dist/core/file-stats.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/planner.js
CHANGED
|
@@ -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}}`;
|
package/dist/output/report.js
CHANGED
|
@@ -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"
|
|
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