@jpkme/diff-review 0.1.14 → 0.1.16
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/bin/diff-review.mjs +141 -77
- package/lib/acp-policy.mjs +10 -2
- package/lib/acp-policy.test.mjs +27 -0
- package/lib/acp-shell.mjs +23 -1
- package/lib/acp-shell.test.mjs +7 -0
- package/lib/output-name.mjs +7 -0
- package/lib/prompt.mjs +3 -2
- package/lib/prompt.test.mjs +37 -0
- package/lib/review-dir.mjs +14 -1
- package/lib/review.mjs +36 -4
- package/lib/review.test.mjs +35 -0
- package/lib/validate.mjs +10 -0
- package/lib/validate.test.mjs +47 -0
- package/package.json +1 -1
- package/schema.json +16 -1
- package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css +1 -0
- package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/{storage.C8XGESwn.css → storage.BxR6930R.css} +1 -1
- package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.br +0 -0
- package/web/build/client/_app/immutable/assets/{storage.C8XGESwn.css.gz → storage.BxR6930R.css.gz} +0 -0
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js +1 -0
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.br +1 -0
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{D8WOD_eS.js → CXhIC34H.js} +1 -1
- package/web/build/client/_app/immutable/chunks/CXhIC34H.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CXhIC34H.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{C0hrdiUt.js → DHO7NTh1.js} +2 -2
- package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/DpxY75GB.js +215 -0
- package/web/build/client/_app/immutable/chunks/DpxY75GB.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DpxY75GB.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{FOc6rgIW.js → cYiFfRa3.js} +1 -1
- package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.br +1 -0
- package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/{app.DcAtSJ3n.js → app.CCv57mvp.js} +2 -2
- package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js +1 -0
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js.br +1 -0
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.slrnBioC.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js +173 -0
- package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.gz +0 -0
- package/web/build/client/_app/version.json +1 -1
- package/web/build/client/_app/version.json.br +0 -0
- package/web/build/client/_app/version.json.gz +0 -0
- package/web/build/handler.js +3 -3
- package/web/build/index.js +3 -3
- package/web/build/server/chunks/{0-DUStDOsr.js → 0-D7NSxn1-.js} +2 -2
- package/web/build/server/chunks/{0-DUStDOsr.js.map → 0-D7NSxn1-.js.map} +1 -1
- package/web/build/server/chunks/1-DboYz8Fw.js +7 -0
- package/web/build/server/chunks/{1-BFhhV5JB.js.map → 1-DboYz8Fw.js.map} +1 -1
- package/web/build/server/chunks/2-B7LX817Q.js +7 -0
- package/web/build/server/chunks/2-B7LX817Q.js.map +1 -0
- package/web/build/server/chunks/3-DgJ_LmTL.js +7 -0
- package/web/build/server/chunks/{3-BV-TIUp1.js.map → 3-DgJ_LmTL.js.map} +1 -1
- package/web/build/server/chunks/{_server.ts-CdFoXw36.js → _server.ts-CP5cRLhW.js} +4 -4
- package/web/build/server/chunks/{_server.ts-CdFoXw36.js.map → _server.ts-CP5cRLhW.js.map} +1 -1
- package/web/build/server/chunks/{agent-sessions-D0WwtWgR.js → agent-sessions-2XGojSJ_.js} +105 -16
- package/web/build/server/chunks/agent-sessions-2XGojSJ_.js.map +1 -0
- package/web/build/server/chunks/{git-review-CgcBIbvM.js → git-review-jZ_JJYfv.js} +36 -8
- package/web/build/server/chunks/git-review-jZ_JJYfv.js.map +1 -0
- package/web/build/server/chunks/{handler-DbxjtK9M.js → handler-B_8yUXji.js} +3 -3
- package/web/build/server/chunks/{handler-DbxjtK9M.js.map → handler-B_8yUXji.js.map} +1 -1
- package/web/build/server/chunks/{hooks.server-Dg6gYUqs.js → hooks.server-rF_eBCff.js} +3 -3
- package/web/build/server/chunks/{hooks.server-Dg6gYUqs.js.map → hooks.server-rF_eBCff.js.map} +1 -1
- package/web/build/server/chunks/{index.js-NNne-njN.js → index.js-CGw8_Q7V.js} +884 -881
- package/web/build/server/chunks/index.js-CGw8_Q7V.js.map +1 -0
- package/web/build/server/chunks/{manifest.js-V8ViI5bl.js → manifest.js-CImZFS6G.js} +9 -9
- package/web/build/server/chunks/{manifest.js-V8ViI5bl.js.map → manifest.js-CImZFS6G.js.map} +1 -1
- package/web/build/server/chunks/{remote-1g4ayag-CCsG2QXw.js → remote-1g4ayag-BMpjk8UD.js} +4 -4
- package/web/build/server/chunks/{remote-1g4ayag-CCsG2QXw.js.map → remote-1g4ayag-BMpjk8UD.js.map} +1 -1
- package/web/build/server/chunks/{remote-wh6t6j-BCvmX37z.js → remote-wh6t6j-dYLCg8ks.js} +19 -7
- package/web/build/server/chunks/remote-wh6t6j-dYLCg8ks.js.map +1 -0
- package/web/build/server/chunks/{validate-CAQ31C7k.js → validate-Da6e71V3.js} +34 -5
- package/web/build/server/chunks/validate-Da6e71V3.js.map +1 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +0 -1
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.br +0 -0
- package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CpeIO6nz.js +0 -210
- package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.br +0 -2
- package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +0 -1
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js +0 -1
- package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.br +0 -4
- package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js +0 -10
- package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js +0 -171
- package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.gz +0 -0
- package/web/build/server/chunks/1-BFhhV5JB.js +0 -7
- package/web/build/server/chunks/2-BZ_MVqj8.js +0 -7
- package/web/build/server/chunks/2-BZ_MVqj8.js.map +0 -1
- package/web/build/server/chunks/3-BV-TIUp1.js +0 -7
- package/web/build/server/chunks/agent-sessions-D0WwtWgR.js.map +0 -1
- package/web/build/server/chunks/git-review-CgcBIbvM.js.map +0 -1
- package/web/build/server/chunks/index.js-NNne-njN.js.map +0 -1
- package/web/build/server/chunks/remote-wh6t6j-BCvmX37z.js.map +0 -1
- package/web/build/server/chunks/validate-CAQ31C7k.js.map +0 -1
package/bin/diff-review.mjs
CHANGED
|
@@ -233,8 +233,9 @@ function ensureValue(value, label) {
|
|
|
233
233
|
* Selects interactivos salvo que --branch y --base vengan juntos.
|
|
234
234
|
* @param {{ listed: { branches: string[], current: string, defaultBase: string }, repo: string }} launch
|
|
235
235
|
* @param {{ branch: string, base: string }} opts
|
|
236
|
+
* @param {{ skipIntro?: boolean }} [pickOpts]
|
|
236
237
|
*/
|
|
237
|
-
async function resolveBranchBase(launch, opts) {
|
|
238
|
+
async function resolveBranchBase(launch, opts, pickOpts = {}) {
|
|
238
239
|
const flagBranch = String(opts.branch || '').trim();
|
|
239
240
|
const flagBase = String(opts.base || '').trim();
|
|
240
241
|
const bothFlags = Boolean(flagBranch && flagBase);
|
|
@@ -255,61 +256,86 @@ async function resolveBranchBase(launch, opts) {
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
const options = branchOptions(launch.listed);
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
if (!pickOpts.skipIntro) {
|
|
260
|
+
p.intro('diff-review');
|
|
261
|
+
p.log.info(launch.repo);
|
|
262
|
+
}
|
|
260
263
|
|
|
264
|
+
/** @type {string} */
|
|
261
265
|
let branch = flagBranch;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
message: 'Branch a revisar',
|
|
266
|
-
options,
|
|
267
|
-
initialValue: defaultBranch(launch.listed) || undefined,
|
|
268
|
-
maxItems: 12
|
|
269
|
-
}),
|
|
270
|
-
'branch'
|
|
271
|
-
);
|
|
272
|
-
} else {
|
|
273
|
-
p.log.step(`Branch: ${branch}`);
|
|
274
|
-
}
|
|
266
|
+
/** @type {string} */
|
|
267
|
+
let base = flagBase;
|
|
268
|
+
let first = true;
|
|
275
269
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
270
|
+
for (;;) {
|
|
271
|
+
if (!first) {
|
|
272
|
+
// Diff vacío / tips iguales: volver a preguntar branch y base (no solo la base).
|
|
273
|
+
branch = '';
|
|
274
|
+
base = '';
|
|
275
|
+
}
|
|
276
|
+
first = false;
|
|
277
|
+
|
|
278
|
+
if (!branch) {
|
|
279
|
+
branch = ensureValue(
|
|
280
|
+
await autocompleteEs({
|
|
281
|
+
message: 'Branch a revisar',
|
|
282
|
+
options,
|
|
283
|
+
initialValue: defaultBranch(launch.listed) || undefined,
|
|
284
|
+
maxItems: 12
|
|
285
|
+
}),
|
|
286
|
+
'branch'
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
p.log.step(`Branch: ${branch}`);
|
|
290
|
+
}
|
|
281
291
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
options: baseOptions,
|
|
288
|
-
initialValue: defaultBase(launch.listed, branch) || undefined,
|
|
289
|
-
maxItems: 12
|
|
290
|
-
}),
|
|
291
|
-
'base'
|
|
292
|
-
);
|
|
293
|
-
} else {
|
|
294
|
-
p.log.step(`Base: ${base}`);
|
|
295
|
-
}
|
|
292
|
+
const baseOptions = branchOptions(launch.listed, branch);
|
|
293
|
+
if (!baseOptions.length) {
|
|
294
|
+
p.cancel(`No hay otra base disponible distinta de \`${branch}\`.`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
296
297
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
298
|
+
if (!base) {
|
|
299
|
+
base = ensureValue(
|
|
300
|
+
await autocompleteEs({
|
|
301
|
+
message: 'Base (merge-base)',
|
|
302
|
+
options: baseOptions,
|
|
303
|
+
initialValue: defaultBase(launch.listed, branch) || undefined,
|
|
304
|
+
maxItems: 12
|
|
305
|
+
}),
|
|
306
|
+
'base'
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
p.log.step(`Base: ${base}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (branch === base || sameCommitTips(launch.repo, branch, base)) {
|
|
313
|
+
p.log.warn(emptyDiffPickMessage(branch, base));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
308
317
|
}
|
|
309
318
|
|
|
310
319
|
return { branch, base, prompted: true };
|
|
311
320
|
}
|
|
312
321
|
|
|
322
|
+
/** ¿Branch y base resuelven al mismo SHA? (diff committed vacío aunque los nombres difieran). */
|
|
323
|
+
function sameCommitTips(repo, branch, base) {
|
|
324
|
+
try {
|
|
325
|
+
const { branchSha, baseSha } = branchTips(repo, branch, base);
|
|
326
|
+
return Boolean(branchSha && baseSha && branchSha === baseSha);
|
|
327
|
+
} catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function emptyDiffPickMessage(branch, base) {
|
|
333
|
+
if (branch === base) {
|
|
334
|
+
return `Branch y base son iguales (${branch}): no hay diff que revisar.`;
|
|
335
|
+
}
|
|
336
|
+
return `\`${branch}\` y \`${base}\` apuntan al mismo commit: no hay diff que revisar.`;
|
|
337
|
+
}
|
|
338
|
+
|
|
313
339
|
function buildOutputFilename(repo, branch, base) {
|
|
314
340
|
try {
|
|
315
341
|
const tips = branchTips(repo, branch, base);
|
|
@@ -327,10 +353,10 @@ function buildOutputFilename(repo, branch, base) {
|
|
|
327
353
|
}
|
|
328
354
|
|
|
329
355
|
/**
|
|
330
|
-
* Precomputa el diff en CLI (spinner si hay Clack).
|
|
331
|
-
* @returns {ReturnType<typeof prepareReviewPromptOpts>}
|
|
356
|
+
* Precomputa el diff en CLI (spinner si hay Clack).
|
|
357
|
+
* @returns {{ empty: true, message: string } | { empty: false, promptOpts: ReturnType<typeof prepareReviewPromptOpts> }}
|
|
332
358
|
*/
|
|
333
|
-
function
|
|
359
|
+
function prepareContext(launch, branch, base, outputFilename, { useClack }) {
|
|
334
360
|
const spin = useClack ? p.spinner() : null;
|
|
335
361
|
if (!useClack) console.log('Preparando el contexto…');
|
|
336
362
|
spin?.start('Preparando el contexto…');
|
|
@@ -351,22 +377,22 @@ function prepareContextOrFail(launch, branch, base, outputFilename, { useClack }
|
|
|
351
377
|
else console.warn('No pude precomputar el diff:', msg);
|
|
352
378
|
ensureReviewDir(launch.repo);
|
|
353
379
|
writeDiffCache(launch.repo, outputFilename, { branch, base });
|
|
354
|
-
return promptOpts;
|
|
380
|
+
return { empty: false, promptOpts };
|
|
355
381
|
}
|
|
356
382
|
|
|
357
383
|
const n = Array.isArray(promptOpts.files) ? promptOpts.files.length : 0;
|
|
358
384
|
if (n === 0) {
|
|
359
385
|
spin?.stop('Diff vacío');
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
386
|
+
return {
|
|
387
|
+
empty: true,
|
|
388
|
+
message: `El diff entre \`${branch}\` y \`${base}\` está vacío: no hay nada que revisar.`
|
|
389
|
+
};
|
|
364
390
|
}
|
|
365
391
|
|
|
366
392
|
const done = `Contexto listo · ${n} archivo${n === 1 ? '' : 's'}`;
|
|
367
393
|
spin?.stop(done);
|
|
368
394
|
if (!useClack) console.log(done);
|
|
369
|
-
return promptOpts;
|
|
395
|
+
return { empty: false, promptOpts };
|
|
370
396
|
}
|
|
371
397
|
|
|
372
398
|
function copyPromptOrWarn(prompt) {
|
|
@@ -674,9 +700,15 @@ async function runUiMode(opts, launch, launchCwd) {
|
|
|
674
700
|
if (launch) {
|
|
675
701
|
console.log(`Repo: ${launch.repo}`);
|
|
676
702
|
console.log(`Branch: ${branch} (base: ${base})`);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
703
|
+
if (branch === base || sameCommitTips(launch.repo, branch, base)) {
|
|
704
|
+
console.error(emptyDiffPickMessage(branch, base));
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
const prep = prepareContext(launch, branch, base, outputFilename, { useClack: false });
|
|
708
|
+
if (prep.empty) {
|
|
709
|
+
console.error(prep.message);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
680
712
|
const prompt = buildPrompt(
|
|
681
713
|
{
|
|
682
714
|
source: 'local',
|
|
@@ -685,7 +717,7 @@ async function runUiMode(opts, launch, launchCwd) {
|
|
|
685
717
|
base,
|
|
686
718
|
remoteUrl: launch.remoteUrl
|
|
687
719
|
},
|
|
688
|
-
promptOpts
|
|
720
|
+
prep.promptOpts
|
|
689
721
|
);
|
|
690
722
|
if (copyTextToClipboard(prompt)) {
|
|
691
723
|
console.log('Prompt copiado al portapapeles.');
|
|
@@ -734,27 +766,59 @@ async function main() {
|
|
|
734
766
|
process.exit(1);
|
|
735
767
|
}
|
|
736
768
|
|
|
737
|
-
|
|
769
|
+
/** @type {{ branch: string, base: string }} */
|
|
770
|
+
let pick = { branch: opts.branch, base: opts.base };
|
|
771
|
+
let skipIntro = false;
|
|
772
|
+
/** @type {string} */
|
|
773
|
+
let branch;
|
|
774
|
+
/** @type {string} */
|
|
775
|
+
let base;
|
|
776
|
+
/** @type {boolean} */
|
|
777
|
+
let prompted;
|
|
778
|
+
/** @type {string} */
|
|
779
|
+
let outputFilename;
|
|
780
|
+
/** @type {ReturnType<typeof prepareReviewPromptOpts>} */
|
|
781
|
+
let promptOpts;
|
|
738
782
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (prompted)
|
|
742
|
-
else console.error(msg);
|
|
743
|
-
process.exit(1);
|
|
744
|
-
}
|
|
783
|
+
for (;;) {
|
|
784
|
+
({ branch, base, prompted } = await resolveBranchBase(launch, pick, { skipIntro }));
|
|
785
|
+
if (prompted) skipIntro = true;
|
|
745
786
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
787
|
+
if (branch === base || sameCommitTips(launch.repo, branch, base)) {
|
|
788
|
+
const msg = emptyDiffPickMessage(branch, base);
|
|
789
|
+
if (prompted || isInteractive()) {
|
|
790
|
+
p.log.warn(msg);
|
|
791
|
+
pick = { branch: '', base: '' };
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
console.error(msg);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
753
797
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
798
|
+
outputFilename = buildOutputFilename(launch.repo, branch, base);
|
|
799
|
+
if (!prompted) {
|
|
800
|
+
console.log(`Repo: ${launch.repo}`);
|
|
801
|
+
console.log(`Branch: ${branch} (base: ${base})`);
|
|
802
|
+
} else {
|
|
803
|
+
p.note(`${branch} → ${base}\n${outputRelPath(outputFilename)}`, 'Review');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// El agente escribe en .diff-review/ (que se ignora sola) y lee de ahí el diff ya calculado.
|
|
807
|
+
const prep = prepareContext(launch, branch, base, outputFilename, {
|
|
808
|
+
useClack: prompted || isInteractive()
|
|
809
|
+
});
|
|
810
|
+
if (prep.empty) {
|
|
811
|
+
if (prompted || isInteractive()) {
|
|
812
|
+
p.log.warn(prep.message);
|
|
813
|
+
pick = { branch: '', base: '' };
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
console.error(prep.message);
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
promptOpts = prep.promptOpts;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
758
822
|
|
|
759
823
|
const prompt = buildPrompt(
|
|
760
824
|
{
|
package/lib/acp-policy.mjs
CHANGED
|
@@ -13,8 +13,9 @@ import { checkShell } from "./acp-shell.mjs";
|
|
|
13
13
|
import { outputRelPath } from "./output-name.mjs";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* @typedef {"review" | "ask" | "edit"} PolicyMode
|
|
17
|
-
* review = generar el JSON; ask = preguntas (no puede escribir nada); edit = modificar el JSON
|
|
16
|
+
* @typedef {"review" | "ask" | "edit" | "fix"} PolicyMode
|
|
17
|
+
* review = generar el JSON; ask = preguntas (no puede escribir nada); edit = modificar el JSON;
|
|
18
|
+
* fix = editar código dentro del repo (no solo el JSON).
|
|
18
19
|
* @typedef {{ repo: string, outputFilename: string, mode: PolicyMode }} PolicyContext
|
|
19
20
|
* @typedef {{
|
|
20
21
|
* kind?: string | null,
|
|
@@ -190,6 +191,13 @@ export function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
|
190
191
|
if (kind === "edit" || kind === "delete") {
|
|
191
192
|
if (mode === "ask") return { allow: false, reason: "modo pregunta: no se edita nada" };
|
|
192
193
|
if (!paths.length) return { allow: false, reason: `${kind === "delete" ? "borrado" : "edición"} sin path` };
|
|
194
|
+
// fix mode: puede editar cualquier archivo dentro del repo (no solo el JSON de salida como edit mode).
|
|
195
|
+
if (mode === "fix") {
|
|
196
|
+
const outside = paths.find((p) => !isInside(p, repo));
|
|
197
|
+
return outside
|
|
198
|
+
? { allow: false, reason: `path fuera del repo: ${outside}` }
|
|
199
|
+
: { allow: true, reason: "escritura en el repo" };
|
|
200
|
+
}
|
|
193
201
|
const target = resolve(repo, outputRelPath(outputFilename));
|
|
194
202
|
const outside = paths.find((p) => !samePath(canonicalPath(p), canonicalPath(target)));
|
|
195
203
|
return outside
|
package/lib/acp-policy.test.mjs
CHANGED
|
@@ -87,6 +87,33 @@ test("decidePermission: escritura solo del JSON objetivo, nunca en modo pregunta
|
|
|
87
87
|
);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("decidePermission: modo fix permite edit/delete dentro del repo, deniega fuera", () => {
|
|
91
|
+
assert.equal(decidePermission(edit(join(repo, "lib", "a.mjs")), ctx("fix")).allow, true);
|
|
92
|
+
assert.equal(decidePermission(edit("lib/a.mjs"), ctx("fix")).allow, true);
|
|
93
|
+
assert.equal(decidePermission(edit(outPath), ctx("fix")).allow, true);
|
|
94
|
+
const home = process.platform === "win32" ? "C:\\Users\\me\\.ssh\\id_rsa" : "/home/me/.ssh/id_rsa";
|
|
95
|
+
assert.equal(decidePermission(edit(home), ctx("fix")).allow, false);
|
|
96
|
+
assert.equal(decidePermission(edit(join(repo, "..", "otro", "x")), ctx("fix")).allow, false);
|
|
97
|
+
assert.equal(decidePermission({ kind: "edit", title: "Edit", rawInput: {} }, ctx("fix")).allow, false);
|
|
98
|
+
assert.equal(
|
|
99
|
+
decidePermission(
|
|
100
|
+
{ kind: "delete", title: "Delete", rawInput: { path: join(repo, "lib", "a.mjs") }, locations: [{ path: join(repo, "lib", "a.mjs") }] },
|
|
101
|
+
ctx("fix")
|
|
102
|
+
).allow,
|
|
103
|
+
true
|
|
104
|
+
);
|
|
105
|
+
assert.equal(
|
|
106
|
+
decidePermission(
|
|
107
|
+
{ kind: "delete", title: "Delete", rawInput: { path: home }, locations: [{ path: home }] },
|
|
108
|
+
ctx("fix")
|
|
109
|
+
).allow,
|
|
110
|
+
false
|
|
111
|
+
);
|
|
112
|
+
// ask/edit JSON sin regresiones frente a paths de código.
|
|
113
|
+
assert.equal(decidePermission(edit(join(repo, "lib", "a.mjs")), ctx("ask")).allow, false);
|
|
114
|
+
assert.equal(decidePermission(edit(join(repo, "lib", "a.mjs")), ctx("edit")).allow, false);
|
|
115
|
+
});
|
|
116
|
+
|
|
90
117
|
test("decidePermission: comandos según isAllowedShell y el resto rechazado", () => {
|
|
91
118
|
assert.equal(decidePermission(bash("git diff a...b"), ctx("ask")).allow, true);
|
|
92
119
|
assert.equal(decidePermission(bash("rm -rf ."), ctx()).allow, false);
|
package/lib/acp-shell.mjs
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* El comando se tokeniza respetando comillas (un `grep "a\|b"` no es un pipe). Se aceptan `|`, `&&`, `||`, `;` y
|
|
7
7
|
* descartar salida a /dev/null; cualquier otra redirección, `$(…)`, backticks o `&` lo rechazan.
|
|
8
|
+
*
|
|
9
|
+
* En PowerShell, `(Get-Content f).Count` llega como `(Get-Content` + `f).Count`: se quitan paréntesis de
|
|
10
|
+
* agrupación y el `.Miembro` colgado al path/flag antes de evaluar la allowlist.
|
|
8
11
|
*/
|
|
9
12
|
import { realpathSync } from "node:fs";
|
|
10
13
|
import { basename, dirname, isAbsolute, normalize, resolve } from "node:path";
|
|
@@ -376,6 +379,23 @@ function checkReviewDirMkdir(argv, ctx) {
|
|
|
376
379
|
return null;
|
|
377
380
|
}
|
|
378
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Quita agrupación PowerShell pegada al token: `(Get-Content` → `Get-Content`,
|
|
384
|
+
* `file.diff).Count` → `file.diff`, `-Line).Lines` → `-Line`.
|
|
385
|
+
* @param {string} token
|
|
386
|
+
*/
|
|
387
|
+
function stripPsGrouping(token) {
|
|
388
|
+
let t = String(token ?? "");
|
|
389
|
+
t = t.replace(/^\(+/, "");
|
|
390
|
+
t = t.replace(/\)+(?:\.[A-Za-z_][\w]*(?:\(\)?)?)*$/, "");
|
|
391
|
+
return t;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @param {string[]} argv */
|
|
395
|
+
function normalizePsArgv(argv) {
|
|
396
|
+
return argv.map(stripPsGrouping).filter((w) => w.length > 0);
|
|
397
|
+
}
|
|
398
|
+
|
|
379
399
|
/**
|
|
380
400
|
* @param {string} command
|
|
381
401
|
* @param {{ repo?: string, platform?: NodeJS.Platform }} [opts]
|
|
@@ -387,7 +407,9 @@ export function checkShell(command, { repo, platform = process.platform } = {})
|
|
|
387
407
|
if (!parsed.pipelines.length) return { ok: false, reason: "comando vacío" };
|
|
388
408
|
const ctx = { repo: repo || process.cwd(), platform };
|
|
389
409
|
for (const pipeline of parsed.pipelines) {
|
|
390
|
-
for (const
|
|
410
|
+
for (const rawArgv of pipeline) {
|
|
411
|
+
const argv = normalizePsArgv(rawArgv);
|
|
412
|
+
if (!argv.length) continue;
|
|
391
413
|
const name = argv[0].toLowerCase();
|
|
392
414
|
if (argv[0].includes("=")) return { ok: false, reason: `variables de entorno en el comando: ${argv[0]}` };
|
|
393
415
|
/** @type {ShellCheck} */
|
package/lib/acp-shell.test.mjs
CHANGED
|
@@ -64,6 +64,10 @@ test("checkShell deja git de lectura, el validate y comandos que solo leen", ()
|
|
|
64
64
|
"git diff a...b -- lib 2>/dev/null || true",
|
|
65
65
|
"Get-Content lib/agents.mjs | Select-Object -First 40",
|
|
66
66
|
"Select-String -Path lib/*.mjs -Pattern 'export'",
|
|
67
|
+
// Agrupación PowerShell: `(cmd).Property` (Copilot/Opus suele contar líneas así).
|
|
68
|
+
"(Get-Content lib/agents.mjs).Count",
|
|
69
|
+
"(Get-Content .diff-review/diff-rev_x.diff).Count",
|
|
70
|
+
"(Get-Content lib/a.mjs | Measure-Object -Line).Lines",
|
|
67
71
|
]) {
|
|
68
72
|
assert.equal(ok(cmd), true, cmd);
|
|
69
73
|
}
|
|
@@ -73,6 +77,9 @@ test("checkShell rechaza escrituras, ejecución y paths fuera del repo", () => {
|
|
|
73
77
|
const home = win ? "C:\\Users\\me\\.ssh\\id_rsa" : "/home/me/.ssh/id_rsa";
|
|
74
78
|
for (const cmd of [
|
|
75
79
|
"",
|
|
80
|
+
// Agrupar no habilita comandos fuera de la allowlist.
|
|
81
|
+
"(Remove-Item lib/a.mjs)",
|
|
82
|
+
"(Invoke-WebRequest https://example.com).Content",
|
|
76
83
|
"git checkout main",
|
|
77
84
|
"git branch -D feat",
|
|
78
85
|
"git push",
|
package/lib/output-name.mjs
CHANGED
|
@@ -19,6 +19,13 @@ export function outputRelPath(filename) {
|
|
|
19
19
|
return `${OUTPUT_DIR}/${String(filename || "").replace(/^.*[\\/]/, "")}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Companion de handoff: mismo stem que el JSON → `.diff-review/<stem>.chat.md`. */
|
|
23
|
+
export function chatRelPath(filename) {
|
|
24
|
+
const name = String(filename || "").replace(/^.*[\\/]/, "");
|
|
25
|
+
const stem = name.replace(/\.json$/i, "") || "diff-rev";
|
|
26
|
+
return `${OUTPUT_DIR}/${stem}.chat.md`;
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
/** Nombre legacy fijo. */
|
|
23
30
|
export const OUTPUT_FILENAME_LEGACY = `${OUTPUT_PREFIX_LEGACY}.json`;
|
|
24
31
|
|
package/lib/prompt.mjs
CHANGED
|
@@ -85,7 +85,8 @@ const RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\
|
|
|
85
85
|
- "blocking": true ONLY if the branch should not merge as-is. Use only with class "risk".
|
|
86
86
|
- When several read-only lookups are independent (files, searches), batch them in the same turn if your tools allow parallel calls.
|
|
87
87
|
- Write human-readable strings (intent, title, what, why, fix, notes) in Spanish. Keep enums/ids/paths/JSON keys exactly as in the schema.
|
|
88
|
-
- Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Prefer clarity over shortening. Longer text is still valid. Do not rewrite the file only to shorten it
|
|
88
|
+
- Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Prefer clarity over shortening. Longer text is still valid. Do not rewrite the file only to shorten it.
|
|
89
|
+
- If the output JSON already exists and findings have \`resolution\` ({ kind: ignored|fix|corrected, detail?: string }), preserve those fields on rewrite (coverage / edit / regenerate). Keep existing finding ids when possible; do not drop or reshuffle resolutions.`;
|
|
89
90
|
|
|
90
91
|
/** @param {{ hasDiff?: boolean }} [ctx] */
|
|
91
92
|
function workflowLocal(base, branch, ctx = {}) {
|
|
@@ -316,7 +317,7 @@ ${outputSchemaBlock(meta)}
|
|
|
316
317
|
|
|
317
318
|
Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). "agent" and "model" are required: set "agent" to the product you are (Claude Code, Cursor, Codex, Copilot, Gemini, …) and "model" to the exact model id you are running. Do not leave placeholders or omit them; validation fails without both. "generatedAt" is optional (ISO-8601 now).
|
|
318
319
|
|
|
319
|
-
Do NOT include diffs or a "files" array. The tool loads the companion \`.diff\` (or computes with git if missing) when opening the report. Do not invent hunks or paste patches into the JSON.
|
|
320
|
+
Do NOT include diffs or a "files" array. The tool loads the companion \`.diff\` (or computes with git if missing) when opening the report. Do not invent hunks or paste patches into the JSON. If you are rewriting an existing review JSON, copy through any per-finding \`resolution\` already present.
|
|
320
321
|
|
|
321
322
|
${validateSection}`;
|
|
322
323
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { after, test } from "node:test";
|
|
6
|
+
import { buildPrompt } from "./prompt.mjs";
|
|
7
|
+
import { chatRelPath } from "./output-name.mjs";
|
|
8
|
+
import { companionPaths, writeHandoffChat } from "./review-dir.mjs";
|
|
9
|
+
|
|
10
|
+
const tmp = mkdtempSync(join(tmpdir(), "diff-review-prompt-"));
|
|
11
|
+
after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
12
|
+
|
|
13
|
+
test("chatRelPath uses same stem as JSON", () => {
|
|
14
|
+
assert.equal(
|
|
15
|
+
chatRelPath("diff-rev_aaaaaaaaaa_bbbbbbbbbb.json"),
|
|
16
|
+
".diff-review/diff-rev_aaaaaaaaaa_bbbbbbbbbb.chat.md"
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("writeHandoffChat overwrites companion .chat.md", () => {
|
|
21
|
+
const filename = "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json";
|
|
22
|
+
writeHandoffChat(tmp, filename, "# first\n");
|
|
23
|
+
writeHandoffChat(tmp, filename, "# second\n");
|
|
24
|
+
const { chat } = companionPaths(tmp, filename);
|
|
25
|
+
assert.equal(readFileSync(chat, "utf8"), "# second\n");
|
|
26
|
+
assert.equal(chatRelPath(filename), `.diff-review/${filename.replace(/\.json$/i, "")}.chat.md`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("buildPrompt tells agent to preserve finding resolution on rewrite", () => {
|
|
30
|
+
const text = buildPrompt(
|
|
31
|
+
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
32
|
+
{ outputFilename: "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json" }
|
|
33
|
+
);
|
|
34
|
+
assert.match(text, /preserve those fields on rewrite/i);
|
|
35
|
+
assert.match(text, /resolution/);
|
|
36
|
+
assert.match(text, /copy through any per-finding `resolution`/i);
|
|
37
|
+
});
|
package/lib/review-dir.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export function ensureReviewDir(repoRoot) {
|
|
|
27
27
|
return dir;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** `diff-rev_a_b.json` → `{ json, diff, session }` dentro de `.diff-review/`. */
|
|
30
|
+
/** `diff-rev_a_b.json` → `{ json, diff, session, chat }` dentro de `.diff-review/`. */
|
|
31
31
|
export function companionPaths(repoRoot, filename) {
|
|
32
32
|
const name = basename(String(filename || ""));
|
|
33
33
|
const stem = name.replace(/\.json$/i, "");
|
|
@@ -36,6 +36,8 @@ export function companionPaths(repoRoot, filename) {
|
|
|
36
36
|
json: join(dir, name),
|
|
37
37
|
diff: join(dir, `${stem}.diff`),
|
|
38
38
|
session: join(dir, `${stem}.session.json`),
|
|
39
|
+
/** Transcript legible al cambiar de agente (se pisa en cada switch). */
|
|
40
|
+
chat: join(dir, `${stem}.chat.md`),
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -106,3 +108,14 @@ export function writeSessionRef(repoRoot, filename, ref) {
|
|
|
106
108
|
writeFileSync(companionPaths(repoRoot, filename).session, `${JSON.stringify(full, null, 2)}\n`);
|
|
107
109
|
return full;
|
|
108
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Guarda (pisa) el transcript legible de handoff junto al JSON.
|
|
114
|
+
* @returns {string} path absoluto del `.chat.md`
|
|
115
|
+
*/
|
|
116
|
+
export function writeHandoffChat(repoRoot, filename, text) {
|
|
117
|
+
ensureReviewDir(repoRoot);
|
|
118
|
+
const { chat } = companionPaths(repoRoot, filename);
|
|
119
|
+
writeFileSync(chat, String(text ?? ""));
|
|
120
|
+
return chat;
|
|
121
|
+
}
|
package/lib/review.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
BLOCK_OPS,
|
|
3
3
|
FINDING_CLASSES,
|
|
4
4
|
FINDING_KINDS,
|
|
5
|
+
FINDING_RESOLUTION_KINDS,
|
|
5
6
|
FINDING_SEVERITIES,
|
|
6
7
|
GROUP_KINDS,
|
|
7
8
|
SKIP_REASONS,
|
|
@@ -111,6 +112,37 @@ function findingKey(finding) {
|
|
|
111
112
|
return [finding.file, finding.line ?? "", finding.kind, finding.what].join("|");
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/** Triage UI (`ignored` | `fix` | `corrected`); se preserva al rehidratar desde disco. */
|
|
116
|
+
function normalizeResolution(raw) {
|
|
117
|
+
const kind = String(raw?.kind ?? "")
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
if (!FINDING_RESOLUTION_KINDS.includes(kind)) return undefined;
|
|
121
|
+
const detail = String(raw?.detail ?? "").trim();
|
|
122
|
+
return detail ? { kind, detail } : { kind };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assignFindingIds(findings) {
|
|
126
|
+
const used = new Set();
|
|
127
|
+
for (const finding of findings) {
|
|
128
|
+
const id = String(finding.id || "").trim();
|
|
129
|
+
if (id && !used.has(id)) {
|
|
130
|
+
finding.id = id;
|
|
131
|
+
used.add(id);
|
|
132
|
+
} else {
|
|
133
|
+
finding.id = "";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let n = 1;
|
|
137
|
+
for (const finding of findings) {
|
|
138
|
+
if (finding.id) continue;
|
|
139
|
+
while (used.has(`f${n}`)) n += 1;
|
|
140
|
+
finding.id = `f${n}`;
|
|
141
|
+
used.add(finding.id);
|
|
142
|
+
n += 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
function normalizeFindings(raw, { blockIds, knownPaths }) {
|
|
115
147
|
const seen = new Set();
|
|
116
148
|
const findings = [];
|
|
@@ -123,8 +155,9 @@ function normalizeFindings(raw, { blockIds, knownPaths }) {
|
|
|
123
155
|
const severity = oneOf(item.severity, FINDING_SEVERITIES, "med");
|
|
124
156
|
const block = String(item?.block || "").trim();
|
|
125
157
|
const line = Number.isFinite(Number(item?.line)) ? Number(item.line) : null;
|
|
158
|
+
const resolution = normalizeResolution(item?.resolution);
|
|
126
159
|
const finding = {
|
|
127
|
-
id: "",
|
|
160
|
+
id: String(item?.id || "").trim(),
|
|
128
161
|
class: cls,
|
|
129
162
|
severity,
|
|
130
163
|
// Una mejora de calidad nunca frena un merge, diga lo que diga el modelo;
|
|
@@ -136,6 +169,7 @@ function normalizeFindings(raw, { blockIds, knownPaths }) {
|
|
|
136
169
|
block: blockIds.has(block) ? block : "",
|
|
137
170
|
what,
|
|
138
171
|
fix: text(item.fix || item.detail, MAX.finding),
|
|
172
|
+
...(resolution ? { resolution } : {}),
|
|
139
173
|
};
|
|
140
174
|
const key = findingKey(finding);
|
|
141
175
|
if (seen.has(key)) continue;
|
|
@@ -150,9 +184,7 @@ function normalizeFindings(raw, { blockIds, knownPaths }) {
|
|
|
150
184
|
rank[a.severity] - rank[b.severity] ||
|
|
151
185
|
a.file.localeCompare(b.file),
|
|
152
186
|
);
|
|
153
|
-
findings
|
|
154
|
-
finding.id = `f${i + 1}`;
|
|
155
|
-
});
|
|
187
|
+
assignFindingIds(findings);
|
|
156
188
|
return findings;
|
|
157
189
|
}
|
|
158
190
|
|
package/lib/review.test.mjs
CHANGED
|
@@ -130,6 +130,41 @@ test("buildReview deduplica hallazgos repetidos y limpia referencias colgadas",
|
|
|
130
130
|
assert.equal(doc.findings[0].block, "");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
test("buildReview preserva resolution e ids al rehidratar triage desde disco", () => {
|
|
134
|
+
const withFindings = payload();
|
|
135
|
+
withFindings.findings = [
|
|
136
|
+
{
|
|
137
|
+
id: "f2",
|
|
138
|
+
class: "quality",
|
|
139
|
+
severity: "low",
|
|
140
|
+
kind: "docs",
|
|
141
|
+
file: "src/a.ts",
|
|
142
|
+
line: 3,
|
|
143
|
+
what: "comentario viejo",
|
|
144
|
+
fix: "actualizar",
|
|
145
|
+
resolution: { kind: "ignored" },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "f1",
|
|
149
|
+
class: "risk",
|
|
150
|
+
severity: "med",
|
|
151
|
+
kind: "bug",
|
|
152
|
+
file: "src/a.ts",
|
|
153
|
+
line: 22,
|
|
154
|
+
block: "b1",
|
|
155
|
+
what: "null sin chequear",
|
|
156
|
+
fix: "guardar",
|
|
157
|
+
resolution: { kind: "corrected", detail: "null-check en parse" },
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
const doc = buildReview({ skeleton: skeleton(), payload: withFindings });
|
|
161
|
+
const byId = Object.fromEntries(doc.findings.map((f) => [f.id, f]));
|
|
162
|
+
assert.equal(doc.findings.length, 2);
|
|
163
|
+
assert.deepEqual(byId.f1.resolution, { kind: "corrected", detail: "null-check en parse" });
|
|
164
|
+
assert.deepEqual(byId.f2.resolution, { kind: "ignored" });
|
|
165
|
+
assert.equal(byId.f1.what, "null sin chequear");
|
|
166
|
+
});
|
|
167
|
+
|
|
133
168
|
test("una respuesta JSON del modelo atraviesa decode, validación y normalización", () => {
|
|
134
169
|
const answer = JSON.stringify({
|
|
135
170
|
meta: { agent: "Claude Code", model: "claude-opus" },
|
package/lib/validate.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export const FINDING_KINDS = [
|
|
|
8
8
|
"missing-test", "perf", "maintainability", "docs", "other",
|
|
9
9
|
];
|
|
10
10
|
export const SKIP_REASONS = ["generated", "lockfile", "format", "vendored", "binary", "trivial", "ignored"];
|
|
11
|
+
export const FINDING_RESOLUTION_KINDS = ["ignored", "fix", "corrected"];
|
|
11
12
|
|
|
12
13
|
/** Lo que tiene que devolver el modelo en un turno. El resto lo agrega el pipeline. */
|
|
13
14
|
export const REVIEW_SCHEMA = {
|
|
@@ -77,6 +78,15 @@ export const REVIEW_SCHEMA = {
|
|
|
77
78
|
block: { type: "string" },
|
|
78
79
|
what: { type: "string", minLength: 1 },
|
|
79
80
|
fix: { type: "string" },
|
|
81
|
+
resolution: {
|
|
82
|
+
type: "object",
|
|
83
|
+
required: ["kind"],
|
|
84
|
+
additionalProperties: true,
|
|
85
|
+
properties: {
|
|
86
|
+
kind: { type: "string", enum: FINDING_RESOLUTION_KINDS },
|
|
87
|
+
detail: { type: "string" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
80
90
|
},
|
|
81
91
|
},
|
|
82
92
|
},
|