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 +39 -4
- package/dist/config/default-config.js +5 -5
- package/dist/core/blocks.js +57 -0
- package/dist/core/scoring.js +5 -5
- package/package.json +1 -1
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
|
-
|
|
589
|
-
|
|
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
|
|
98
|
-
// posición 1 → filesPerCommit
|
|
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": {
|
package/dist/core/blocks.js
CHANGED
|
@@ -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) {
|
package/dist/core/scoring.js
CHANGED
|
@@ -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