pull-request-split-advisor 3.2.13 → 3.2.17

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/dist/cli.js CHANGED
@@ -43,6 +43,23 @@ import { runConfigWizard, runConfigWithKey } from "./ai/config-wizard.js";
43
43
  function normalizeStoryNumber(value) {
44
44
  return value.trim().replace(/[^0-9]/g, "");
45
45
  }
46
+ /**
47
+ * Convierte las reglas de scoring de una métrica en una cadena legible con los umbrales.
48
+ * Ejemplo: "≤2→5 ≤4→4 =5→3 ≤7→2 *→1"
49
+ */
50
+ function formatThresholds(rules) {
51
+ return rules.map((r) => {
52
+ const cond = r.default ? "*"
53
+ : r.eq !== undefined ? `=${r.eq}`
54
+ : r.lte !== undefined ? `≤${r.lte}`
55
+ : r.lt !== undefined ? `<${r.lt}`
56
+ : r.gte !== undefined && r.lt !== undefined ? `${r.gte}–${r.lt}`
57
+ : r.gte !== undefined ? `≥${r.gte}`
58
+ : r.gt !== undefined ? `>${r.gt}`
59
+ : "?";
60
+ return `${cond}→${r.points}pts`;
61
+ }).join(" ");
62
+ }
46
63
  /**
47
64
  * Pide al usuario el número de subtarea para cada commit del plan y rellena
48
65
  * `commit.ticketCode` y `commit.suggestedMessage` con el resultado.
@@ -585,8 +602,13 @@ async function main() {
585
602
  }
586
603
  const aheadCommits = localAheadCount(baseBranch);
587
604
  const commitCount = Math.max(aheadCommits, 1);
588
- const filesPerCommit = Number((scoredFiles / commitCount).toFixed(2));
589
- const avgLinesPerCommit = Math.round(scoredLines / commitCount);
605
+ // Filtrar commits chore/style/docs del denominador de M1.4 y M1.5,
606
+ // igual que el script bash original que usa filtered_commit_count.
607
+ const allAheadCommits = localAheadCommits(baseBranch);
608
+ const filteredCommits = allAheadCommits.filter((c) => !/^(chore|style|docs)(\(|!|:|\s)/i.test(c.subject));
609
+ const filteredCount = Math.max(filteredCommits.length, 1);
610
+ const filesPerCommit = Number((scoredFiles / filteredCount).toFixed(2));
611
+ const avgLinesPerCommit = Math.round(scoredLines / filteredCount);
590
612
  const result = scorePullRequest({ commitCount, filesPerCommit, avgLinesPerCommit, totalLinesChanged: scoredLines }, config);
591
613
  const warnThreshold = config.targetScore > 4 ? 4 : Math.max(0, config.targetScore - 1);
592
614
  const scoreColor = ui.scoreColor(result.complexity, config.targetScore);
@@ -629,10 +651,23 @@ async function main() {
629
651
  `Score: ${ui.score(result.complexity, config.targetScore)}`,
630
652
  `Score objetivo: ${config.targetScore}`,
631
653
  "",
632
- ...Object.values(result.metrics).map((m) => ` ${m.label}: valor=${m.rawValue} pts=${m.points} pond=${m.weightedScore}`),
633
- "",
634
654
  `Recomendación: ${result.recommendation}`
635
655
  ], scoreColor);
656
+ // ─── Tabla de métricas con umbrales ─────────────────────────────────
657
+ ui.section("DETALLE DE MÉTRICAS", "-");
658
+ ui.table(["Cód.", "Métrica", "Valor", "Pts", "Peso", "Aporte", "Umbrales"], Object.entries(result.metrics).map(([code, m]) => {
659
+ const def = config.metrics[code];
660
+ const thresholds = def ? formatThresholds(def.scoring) : "";
661
+ return [
662
+ code,
663
+ m.label,
664
+ String(m.rawValue),
665
+ `${m.points} / 5`,
666
+ `${(m.weight * 100).toFixed(0)}%`,
667
+ String(m.weightedScore),
668
+ thresholds
669
+ ];
670
+ }));
636
671
  closeReadlineInterface();
637
672
  });
638
673
  // ─── Subcomando: config ──────────────────────────────────────────────────
@@ -94,10 +94,10 @@ export const defaultConfig = {
94
94
  // ── Métricas de scoring ───────────────────────────────────────────────────
95
95
  //
96
96
  // Orden posicional REQUERIDO (no cambiar el orden de las claves):
97
- // posición 0 → commitCount (M1.3)
98
- // posición 1 → filesPerCommit (M1.4)
99
- // posición 2 → avgLinesPerCommit (M1.5)
100
- // posición 3 → totalLinesChanged (M3.2)
97
+ // posición 0 → commitCount (M1.3 — total de commits)
98
+ // posición 1 → filesPerCommit (M1.4 — archivos / commits filtrados sin chore/style/docs)
99
+ // posición 2 → avgLinesPerCommit (M1.5 — líneas / commits filtrados sin chore/style/docs)
100
+ // posición 3 → totalLinesChanged (M3.2 — total de líneas)
101
101
  //
102
102
  metrics: {
103
103
  /**
@@ -131,7 +131,7 @@ export const defaultConfig = {
131
131
  ]
132
132
  },
133
133
  /**
134
- * M1.5 — Promedio de líneas por commit (excluye chore/style/docs).
134
+ * M1.5 — Promedio de líneas por commit (excluye chore/style/docs del denominador).
135
135
  * Peso: 0.25 | Contribución máxima: 1.25 puntos
136
136
  */
137
137
  "M1.5": {
@@ -132,6 +132,63 @@ export function buildBlocks(fileStats, config, deps) {
132
132
  depScore: 0
133
133
  });
134
134
  }
135
+ // ── Post-proceso: fusionar bloques de solo-tests con su bloque fuente ──────
136
+ // Cubre el caso en que test y fuente están en directorios distintos
137
+ // (ej: `tests/` vs `src/`) y getGroupKey los asigna a bloques distintos.
138
+ // Regla: un test no puede ir a una rama sin su fuente, salvo que la fuente
139
+ // no tenga cambios y no esté en el conjunto analizado.
140
+ {
141
+ const linesByPath = new Map(fileStats.map((f) => [f.path, f.lines]));
142
+ // Índice: nombre-base del fuente → bloque que lo contiene.
143
+ const sourceBlockByBaseName = new Map();
144
+ for (const block of blocks) {
145
+ for (const f of block.files) {
146
+ if (!isTestFile(f)) {
147
+ const baseName = basename(f).replace(/\.[^.]+$/, "").replace(/\.?(test|spec)$/, "");
148
+ if (!sourceBlockByBaseName.has(baseName)) {
149
+ sourceBlockByBaseName.set(baseName, block);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ // Para cada bloque que contiene exclusivamente tests, intentar fusionarlo
155
+ // con el bloque de su archivo fuente si éste tiene cambios.
156
+ const blocksToDelete = new Set();
157
+ for (const block of blocks) {
158
+ if (blocksToDelete.has(block))
159
+ continue;
160
+ if (!block.files.every((f) => isTestFile(f)))
161
+ continue; // ya tiene fuente: OK
162
+ const moved = [];
163
+ for (const testFile of block.files) {
164
+ const testBase = basename(testFile)
165
+ .replace(/\.[^.]+$/, "")
166
+ .replace(/\.?(test|spec)$/, "")
167
+ .replace(/^test_/, "")
168
+ .replace(/_test$/, "");
169
+ const sourceBlock = sourceBlockByBaseName.get(testBase);
170
+ if (!sourceBlock || sourceBlock === block)
171
+ continue;
172
+ // Fusionar el test en el bloque del fuente.
173
+ sourceBlock.files.push(testFile);
174
+ sourceBlock.lines += linesByPath.get(testFile) ?? 0;
175
+ sourceBlock.divisible = false; // test + fuente → siempre indivisible
176
+ moved.push(testFile);
177
+ }
178
+ if (moved.length === block.files.length) {
179
+ // Todos los tests fueron reasignados: eliminar este bloque vacío.
180
+ blocksToDelete.add(block);
181
+ }
182
+ else if (moved.length > 0) {
183
+ // Quitar solo los archivos que se movieron.
184
+ block.files = block.files.filter((f) => !moved.includes(f));
185
+ block.lines = block.files.reduce((sum, f) => sum + (linesByPath.get(f) ?? 0), 0);
186
+ }
187
+ }
188
+ if (blocksToDelete.size > 0) {
189
+ blocks.splice(0, blocks.length, ...blocks.filter((b) => !blocksToDelete.has(b)));
190
+ }
191
+ }
135
192
  // Calcular depScore con un mapa de frecuencias: O(n+m) en lugar de O(n×m).
136
193
  const fileEdgeCount = new Map();
137
194
  for (const { from, to } of deps) {
@@ -15,9 +15,9 @@
15
15
  * El motor asigna valores brutos **posicionalmente** según el orden declarado
16
16
  * en `config.metrics`. Los 4 valores brutos, en orden obligatorio, son:
17
17
  *
18
- * 1. Cantidad de commits del PR
19
- * 2. Promedio de archivos por commit
20
- * 3. Promedio de líneas por commit
18
+ * 1. Cantidad de commits del PR (total, incluye chore/style/docs)
19
+ * 2. Promedio de archivos por commit (denominador: commits sin chore/style/docs)
20
+ * 3. Promedio de líneas por commit (denominador: commits sin chore/style/docs)
21
21
  * 4. Total de líneas cambiadas
22
22
  *
23
23
  * Los códigos de cada métrica son libres («sólo etiquetas») y cada equipo
@@ -130,8 +130,8 @@ export function scorePullRequest(raw, config) {
130
130
  // Mapeo posicional: el orden declarado en config.metrics determina qué valor
131
131
  // bruto recibe cada métrica. Convención invariante (ver MetricCode en types.ts):
132
132
  // posición 0 → commitCount
133
- // posición 1 → filesPerCommit
134
- // posición 2 → avgLinesPerCommit
133
+ // posición 1 → filesPerCommit (excl. chore/style/docs del denominador)
134
+ // posición 2 → avgLinesPerCommit (excl. chore/style/docs del denominador)
135
135
  // posición 3 → totalLinesChanged
136
136
  const rawValues = [
137
137
  raw.commitCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pull-request-split-advisor",
3
- "version": "3.2.13",
3
+ "version": "3.2.17",
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",