@jpkme/diff-review 0.1.16 → 0.1.18
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 +22 -14
- package/bin/diff-review.mjs +220 -29
- package/lib/acp.mjs +86 -10
- package/lib/agent-login.mjs +848 -36
- package/lib/agent-login.test.mjs +538 -0
- package/lib/agents.mjs +0 -39
- package/lib/agents.test.mjs +17 -24
- package/lib/extract-cache.test.mjs +162 -162
- package/lib/git.mjs +25 -9
- package/lib/model-label.mjs +20 -4
- package/lib/model-label.test.mjs +15 -2
- package/lib/prompt.mjs +12 -10
- package/lib/prompt.test.mjs +12 -0
- package/lib/review-meta.mjs +11 -3
- package/lib/review-meta.test.mjs +19 -2
- package/lib/review-prep.mjs +63 -63
- package/lib/validate.mjs +1 -1
- package/lib/wait-output.test.mjs +1 -1
- package/package.json +2 -2
- package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css +1 -0
- package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css.br +0 -0
- package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{cYiFfRa3.js → -cMMFbSG.js} +1 -1
- package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.br +2 -0
- package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/0vBZPdKl.js +215 -0
- package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{DHO7NTh1.js → BMoYpzpA.js} +2 -2
- package/web/build/client/_app/immutable/chunks/BMoYpzpA.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BMoYpzpA.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{CXhIC34H.js → DA0YnpGR.js} +3 -3
- package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js +2 -0
- package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js +1 -0
- package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js +12 -0
- package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js +340 -0
- package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.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-BUdytb9b.js +11 -0
- package/web/build/server/chunks/{0-D7NSxn1-.js.map → 0-BUdytb9b.js.map} +1 -1
- package/web/build/server/chunks/1-DRokSBkY.js +7 -0
- package/web/build/server/chunks/{1-DboYz8Fw.js.map → 1-DRokSBkY.js.map} +1 -1
- package/web/build/server/chunks/2-BxyzaJTR.js +7 -0
- package/web/build/server/chunks/2-BxyzaJTR.js.map +1 -0
- package/web/build/server/chunks/3-D8o0VWkj.js +7 -0
- package/web/build/server/chunks/{3-DgJ_LmTL.js.map → 3-D8o0VWkj.js.map} +1 -1
- package/web/build/server/chunks/{_server.ts-CP5cRLhW.js → _server.ts-kax5Szn0.js} +5 -4
- package/web/build/server/chunks/_server.ts-kax5Szn0.js.map +1 -0
- package/web/build/server/chunks/_server.ts-vS3A9t3f.js +123 -0
- package/web/build/server/chunks/_server.ts-vS3A9t3f.js.map +1 -0
- package/web/build/server/chunks/{agent-sessions-2XGojSJ_.js → agent-sessions-BArYLTW6.js} +1023 -135
- package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
- package/web/build/server/chunks/{git-review-jZ_JJYfv.js → git-review-hZSqk_1P.js} +54 -21
- package/web/build/server/chunks/{git-review-jZ_JJYfv.js.map → git-review-hZSqk_1P.js.map} +1 -1
- package/web/build/server/chunks/{handler-B_8yUXji.js → handler-DbFqzg7l.js} +3 -3
- package/web/build/server/chunks/{handler-B_8yUXji.js.map → handler-DbFqzg7l.js.map} +1 -1
- package/web/build/server/chunks/{hooks.server-rF_eBCff.js → hooks.server-BEl7rVad.js} +4 -3
- package/web/build/server/chunks/{hooks.server-rF_eBCff.js.map → hooks.server-BEl7rVad.js.map} +1 -1
- package/web/build/server/chunks/{index.js-CGw8_Q7V.js → index.js-BB9DZBdR.js} +4 -4
- package/web/build/server/chunks/index.js-BB9DZBdR.js.map +1 -0
- package/web/build/server/chunks/manifest.js-Dk6YX1jw.js +65 -0
- package/web/build/server/chunks/manifest.js-Dk6YX1jw.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-BMpjk8UD.js → remote-1g4ayag-Bz1vj8X1.js} +6 -6
- package/web/build/server/chunks/remote-1g4ayag-Bz1vj8X1.js.map +1 -0
- package/web/build/server/chunks/{remote-wh6t6j-dYLCg8ks.js → remote-wh6t6j-DSwM3S9h.js} +44 -10
- package/web/build/server/chunks/remote-wh6t6j-DSwM3S9h.js.map +1 -0
- package/web/build/server/chunks/{validate-Da6e71V3.js → validate-CXjTRGnR.js} +34 -17
- package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css +0 -1
- 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.BxR6930R.css +0 -1
- package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js +0 -1
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.br +0 -1
- package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.gz +0 -0
- 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/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 +0 -215
- 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/cYiFfRa3.js.br +0 -1
- package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.CCv57mvp.js +0 -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 +0 -1
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js.br +0 -1
- package/web/build/client/_app/immutable/entry/start.B012TSbW.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.slrnBioC.js +0 -1
- 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 +0 -1
- 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 +0 -10
- 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 +0 -173
- 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/server/chunks/0-D7NSxn1-.js +0 -11
- package/web/build/server/chunks/1-DboYz8Fw.js +0 -7
- package/web/build/server/chunks/2-B7LX817Q.js +0 -7
- package/web/build/server/chunks/2-B7LX817Q.js.map +0 -1
- package/web/build/server/chunks/3-DgJ_LmTL.js +0 -7
- package/web/build/server/chunks/_server.ts-CP5cRLhW.js.map +0 -1
- package/web/build/server/chunks/agent-sessions-2XGojSJ_.js.map +0 -1
- package/web/build/server/chunks/index.js-CGw8_Q7V.js.map +0 -1
- package/web/build/server/chunks/manifest.js-CImZFS6G.js +0 -58
- package/web/build/server/chunks/manifest.js-CImZFS6G.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-BMpjk8UD.js.map +0 -1
- package/web/build/server/chunks/remote-wh6t6j-dYLCg8ks.js.map +0 -1
- package/web/build/server/chunks/validate-Da6e71V3.js.map +0 -1
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { afterEach, test } from "node:test";
|
|
6
|
-
import { extractBranchDiff, parseGitDiffFiles } from "./extract.mjs";
|
|
7
|
-
import { OUTPUT_DIR } from "./output-name.mjs";
|
|
8
|
-
import { companionPaths, ensureReviewDir, writeDiffText } from "./review-dir.mjs";
|
|
9
|
-
import { checkReportFile } from "./check-report.mjs";
|
|
10
|
-
|
|
11
|
-
const sample = [
|
|
12
|
-
"diff --git a/src/a.ts b/src/a.ts",
|
|
13
|
-
"index 111..222 100644",
|
|
14
|
-
"--- a/src/a.ts",
|
|
15
|
-
"+++ b/src/a.ts",
|
|
16
|
-
"@@ -1,2 +1,3 @@",
|
|
17
|
-
" keep",
|
|
18
|
-
"-old",
|
|
19
|
-
"+new",
|
|
20
|
-
"+extra",
|
|
21
|
-
"diff --git a/package-lock.json b/package-lock.json",
|
|
22
|
-
"index 1..2 100644",
|
|
23
|
-
"--- a/package-lock.json",
|
|
24
|
-
"+++ b/package-lock.json",
|
|
25
|
-
"@@ -1 +1 @@",
|
|
26
|
-
"-{}",
|
|
27
|
-
"+{ }",
|
|
28
|
-
"diff --git a/old.ts b/new.ts",
|
|
29
|
-
"similarity index 90%",
|
|
30
|
-
"rename from old.ts",
|
|
31
|
-
"rename to new.ts",
|
|
32
|
-
"index 1..2 100644",
|
|
33
|
-
"--- a/old.ts",
|
|
34
|
-
"+++ b/new.ts",
|
|
35
|
-
"@@ -1 +1 @@",
|
|
36
|
-
"-x",
|
|
37
|
-
"+y",
|
|
38
|
-
].join("\n");
|
|
39
|
-
|
|
40
|
-
const dirs = [];
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
for (const d of dirs.splice(0)) {
|
|
43
|
-
try {
|
|
44
|
-
rmSync(d, { recursive: true, force: true });
|
|
45
|
-
} catch {
|
|
46
|
-
/* ignore */
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("parseGitDiffFiles splits paths and change types", () => {
|
|
52
|
-
const files = parseGitDiffFiles(sample);
|
|
53
|
-
assert.equal(files.length, 3);
|
|
54
|
-
assert.equal(files[0].path, "src/a.ts");
|
|
55
|
-
assert.equal(files[0].changeType, "modified");
|
|
56
|
-
assert.match(files[0].hunkDiff, /^@@/);
|
|
57
|
-
assert.equal(files[1].path, "package-lock.json");
|
|
58
|
-
assert.equal(files[2].changeType, "renamed");
|
|
59
|
-
assert.equal(files[2].path, "new.ts");
|
|
60
|
-
assert.equal(files[2].oldPath, "old.ts");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("extractBranchDiff with cachedDiff applies DEFAULT_IGNORE", () => {
|
|
64
|
-
const repo = mkdtempSync(join(tmpdir(), "diff-review-cache-"));
|
|
65
|
-
dirs.push(repo);
|
|
66
|
-
mkdirSync(join(repo, "src"), { recursive: true });
|
|
67
|
-
// repo path only used for config/guidelines; no git needed when cachedDiff is set
|
|
68
|
-
const skeleton = extractBranchDiff({
|
|
69
|
-
repo,
|
|
70
|
-
branch: "feature",
|
|
71
|
-
base: "develop",
|
|
72
|
-
cachedDiff: sample,
|
|
73
|
-
});
|
|
74
|
-
assert.deepEqual(
|
|
75
|
-
skeleton.files.map((f) => f.path).sort(),
|
|
76
|
-
["new.ts", "src/a.ts"]
|
|
77
|
-
);
|
|
78
|
-
assert.ok(skeleton.context.ignored.includes("package-lock.json"));
|
|
79
|
-
assert.match(skeleton.files.find((f) => f.path === "src/a.ts").diff, /^@@/);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("checkReportFile reuses companion .diff paths without inventing coverage", () => {
|
|
83
|
-
const repo = mkdtempSync(join(tmpdir(), "diff-review-check-"));
|
|
84
|
-
dirs.push(repo);
|
|
85
|
-
ensureReviewDir(repo);
|
|
86
|
-
const filename = "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json";
|
|
87
|
-
writeDiffText(repo, filename, sample);
|
|
88
|
-
const payload = {
|
|
89
|
-
meta: {
|
|
90
|
-
agent: "Cursor",
|
|
91
|
-
model: "composer",
|
|
92
|
-
repo,
|
|
93
|
-
branch: "feature",
|
|
94
|
-
base: "develop",
|
|
95
|
-
},
|
|
96
|
-
intent: "Cambia src/a.ts y renombra old.ts para dejar el módulo listo en la rama.",
|
|
97
|
-
groups: [{ id: "g1", kind: "feat", title: "Ajuste", intent: "base" }],
|
|
98
|
-
blocks: [
|
|
99
|
-
{
|
|
100
|
-
id: "b1",
|
|
101
|
-
group: "g1",
|
|
102
|
-
file: "src/a.ts",
|
|
103
|
-
lines: "L1-3",
|
|
104
|
-
op: "mod",
|
|
105
|
-
what: "cambia la lógica",
|
|
106
|
-
why: "hace falta",
|
|
107
|
-
source: "code",
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: "b2",
|
|
111
|
-
group: "g1",
|
|
112
|
-
file: "new.ts",
|
|
113
|
-
lines: "L1",
|
|
114
|
-
op: "rename",
|
|
115
|
-
what: "renombra el módulo",
|
|
116
|
-
why: "nombre más claro",
|
|
117
|
-
source: "code",
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
findings: [],
|
|
121
|
-
skipped: [{ file: "package-lock.json", reason: "lockfile" }],
|
|
122
|
-
};
|
|
123
|
-
const jsonPath = companionPaths(repo, filename).json;
|
|
124
|
-
writeFileSync(jsonPath, JSON.stringify(payload));
|
|
125
|
-
// Sin git: loadBranchDiff lee el .diff y aplica ignore (lockfile fuera de paths).
|
|
126
|
-
const result = checkReportFile(jsonPath, { repo, branch: "feature", base: "develop" });
|
|
127
|
-
assert.equal(result.ok, true, result.errors.join("; "));
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("buildPrompt with precomputed context skips merge-base rediscovery", async () => {
|
|
131
|
-
const { buildPrompt } = await import("./prompt.mjs");
|
|
132
|
-
const text = buildPrompt(
|
|
133
|
-
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
134
|
-
{
|
|
135
|
-
outputFilename: "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json",
|
|
136
|
-
diffRelPath: ".diff-review/diff-rev_aaaaaaaaaa_bbbbbbbbbb.diff",
|
|
137
|
-
mergeBase: "abc123",
|
|
138
|
-
branchSha: "def456",
|
|
139
|
-
baseSha: "789abc",
|
|
140
|
-
files: [{ path: "src/a.ts", changeType: "modified" }],
|
|
141
|
-
ignored: ["package-lock.json"],
|
|
142
|
-
ignoreGlobs: ["**/package-lock.json"],
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
assert.match(text, /Precomputed git context/);
|
|
146
|
-
assert.match(text, /diff-rev_aaaaaaaaaa_bbbbbbbbbb\.diff/);
|
|
147
|
-
assert.match(text, /Merge-base SHA: `abc123`/);
|
|
148
|
-
assert.match(text, /modified: src\/a\.ts/);
|
|
149
|
-
assert.doesNotMatch(text, /Resolve the merge-base/);
|
|
150
|
-
assert.doesNotMatch(text, /Run exactly: `git diff/);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("buildPrompt acp omits validate CLI", async () => {
|
|
154
|
-
const { buildPrompt, validateCliCommand } = await import("./prompt.mjs");
|
|
155
|
-
const filename = "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json";
|
|
156
|
-
const text = buildPrompt(
|
|
157
|
-
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
158
|
-
{ outputFilename: filename, acp: true, diffRelPath: `.${OUTPUT_DIR}/${filename.replace(/\.json$/, ".diff")}` }
|
|
159
|
-
);
|
|
160
|
-
assert.doesNotMatch(text, new RegExp(validateCliCommand(filename).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
161
|
-
assert.match(text, /host validates/i);
|
|
162
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, test } from "node:test";
|
|
6
|
+
import { extractBranchDiff, parseGitDiffFiles } from "./extract.mjs";
|
|
7
|
+
import { OUTPUT_DIR } from "./output-name.mjs";
|
|
8
|
+
import { companionPaths, ensureReviewDir, writeDiffText } from "./review-dir.mjs";
|
|
9
|
+
import { checkReportFile } from "./check-report.mjs";
|
|
10
|
+
|
|
11
|
+
const sample = [
|
|
12
|
+
"diff --git a/src/a.ts b/src/a.ts",
|
|
13
|
+
"index 111..222 100644",
|
|
14
|
+
"--- a/src/a.ts",
|
|
15
|
+
"+++ b/src/a.ts",
|
|
16
|
+
"@@ -1,2 +1,3 @@",
|
|
17
|
+
" keep",
|
|
18
|
+
"-old",
|
|
19
|
+
"+new",
|
|
20
|
+
"+extra",
|
|
21
|
+
"diff --git a/package-lock.json b/package-lock.json",
|
|
22
|
+
"index 1..2 100644",
|
|
23
|
+
"--- a/package-lock.json",
|
|
24
|
+
"+++ b/package-lock.json",
|
|
25
|
+
"@@ -1 +1 @@",
|
|
26
|
+
"-{}",
|
|
27
|
+
"+{ }",
|
|
28
|
+
"diff --git a/old.ts b/new.ts",
|
|
29
|
+
"similarity index 90%",
|
|
30
|
+
"rename from old.ts",
|
|
31
|
+
"rename to new.ts",
|
|
32
|
+
"index 1..2 100644",
|
|
33
|
+
"--- a/old.ts",
|
|
34
|
+
"+++ b/new.ts",
|
|
35
|
+
"@@ -1 +1 @@",
|
|
36
|
+
"-x",
|
|
37
|
+
"+y",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
const dirs = [];
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
for (const d of dirs.splice(0)) {
|
|
43
|
+
try {
|
|
44
|
+
rmSync(d, { recursive: true, force: true });
|
|
45
|
+
} catch {
|
|
46
|
+
/* ignore */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("parseGitDiffFiles splits paths and change types", () => {
|
|
52
|
+
const files = parseGitDiffFiles(sample);
|
|
53
|
+
assert.equal(files.length, 3);
|
|
54
|
+
assert.equal(files[0].path, "src/a.ts");
|
|
55
|
+
assert.equal(files[0].changeType, "modified");
|
|
56
|
+
assert.match(files[0].hunkDiff, /^@@/);
|
|
57
|
+
assert.equal(files[1].path, "package-lock.json");
|
|
58
|
+
assert.equal(files[2].changeType, "renamed");
|
|
59
|
+
assert.equal(files[2].path, "new.ts");
|
|
60
|
+
assert.equal(files[2].oldPath, "old.ts");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("extractBranchDiff with cachedDiff applies DEFAULT_IGNORE", () => {
|
|
64
|
+
const repo = mkdtempSync(join(tmpdir(), "diff-review-cache-"));
|
|
65
|
+
dirs.push(repo);
|
|
66
|
+
mkdirSync(join(repo, "src"), { recursive: true });
|
|
67
|
+
// repo path only used for config/guidelines; no git needed when cachedDiff is set
|
|
68
|
+
const skeleton = extractBranchDiff({
|
|
69
|
+
repo,
|
|
70
|
+
branch: "feature",
|
|
71
|
+
base: "develop",
|
|
72
|
+
cachedDiff: sample,
|
|
73
|
+
});
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
skeleton.files.map((f) => f.path).sort(),
|
|
76
|
+
["new.ts", "src/a.ts"]
|
|
77
|
+
);
|
|
78
|
+
assert.ok(skeleton.context.ignored.includes("package-lock.json"));
|
|
79
|
+
assert.match(skeleton.files.find((f) => f.path === "src/a.ts").diff, /^@@/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("checkReportFile reuses companion .diff paths without inventing coverage", () => {
|
|
83
|
+
const repo = mkdtempSync(join(tmpdir(), "diff-review-check-"));
|
|
84
|
+
dirs.push(repo);
|
|
85
|
+
ensureReviewDir(repo);
|
|
86
|
+
const filename = "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json";
|
|
87
|
+
writeDiffText(repo, filename, sample);
|
|
88
|
+
const payload = {
|
|
89
|
+
meta: {
|
|
90
|
+
agent: "Cursor",
|
|
91
|
+
model: "composer",
|
|
92
|
+
repo,
|
|
93
|
+
branch: "feature",
|
|
94
|
+
base: "develop",
|
|
95
|
+
},
|
|
96
|
+
intent: "Cambia src/a.ts y renombra old.ts para dejar el módulo listo en la rama.",
|
|
97
|
+
groups: [{ id: "g1", kind: "feat", title: "Ajuste", intent: "base" }],
|
|
98
|
+
blocks: [
|
|
99
|
+
{
|
|
100
|
+
id: "b1",
|
|
101
|
+
group: "g1",
|
|
102
|
+
file: "src/a.ts",
|
|
103
|
+
lines: "L1-3",
|
|
104
|
+
op: "mod",
|
|
105
|
+
what: "cambia la lógica",
|
|
106
|
+
why: "hace falta",
|
|
107
|
+
source: "code",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "b2",
|
|
111
|
+
group: "g1",
|
|
112
|
+
file: "new.ts",
|
|
113
|
+
lines: "L1",
|
|
114
|
+
op: "rename",
|
|
115
|
+
what: "renombra el módulo",
|
|
116
|
+
why: "nombre más claro",
|
|
117
|
+
source: "code",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
findings: [],
|
|
121
|
+
skipped: [{ file: "package-lock.json", reason: "lockfile" }],
|
|
122
|
+
};
|
|
123
|
+
const jsonPath = companionPaths(repo, filename).json;
|
|
124
|
+
writeFileSync(jsonPath, JSON.stringify(payload));
|
|
125
|
+
// Sin git: loadBranchDiff lee el .diff y aplica ignore (lockfile fuera de paths).
|
|
126
|
+
const result = checkReportFile(jsonPath, { repo, branch: "feature", base: "develop" });
|
|
127
|
+
assert.equal(result.ok, true, result.errors.join("; "));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("buildPrompt with precomputed context skips merge-base rediscovery", async () => {
|
|
131
|
+
const { buildPrompt } = await import("./prompt.mjs");
|
|
132
|
+
const text = buildPrompt(
|
|
133
|
+
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
134
|
+
{
|
|
135
|
+
outputFilename: "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json",
|
|
136
|
+
diffRelPath: ".diff-review/diff-rev_aaaaaaaaaa_bbbbbbbbbb.diff",
|
|
137
|
+
mergeBase: "abc123",
|
|
138
|
+
branchSha: "def456",
|
|
139
|
+
baseSha: "789abc",
|
|
140
|
+
files: [{ path: "src/a.ts", changeType: "modified" }],
|
|
141
|
+
ignored: ["package-lock.json"],
|
|
142
|
+
ignoreGlobs: ["**/package-lock.json"],
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
assert.match(text, /Precomputed git context/);
|
|
146
|
+
assert.match(text, /diff-rev_aaaaaaaaaa_bbbbbbbbbb\.diff/);
|
|
147
|
+
assert.match(text, /Merge-base SHA: `abc123`/);
|
|
148
|
+
assert.match(text, /modified: src\/a\.ts/);
|
|
149
|
+
assert.doesNotMatch(text, /Resolve the merge-base/);
|
|
150
|
+
assert.doesNotMatch(text, /Run exactly: `git diff/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("buildPrompt acp omits validate CLI", async () => {
|
|
154
|
+
const { buildPrompt, validateCliCommand } = await import("./prompt.mjs");
|
|
155
|
+
const filename = "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json";
|
|
156
|
+
const text = buildPrompt(
|
|
157
|
+
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
158
|
+
{ outputFilename: filename, acp: true, diffRelPath: `.${OUTPUT_DIR}/${filename.replace(/\.json$/, ".diff")}` }
|
|
159
|
+
);
|
|
160
|
+
assert.doesNotMatch(text, new RegExp(validateCliCommand(filename).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
161
|
+
assert.match(text, /host validates/i);
|
|
162
|
+
});
|
package/lib/git.mjs
CHANGED
|
@@ -49,12 +49,14 @@ export function tryResolveGitRoot(cwd) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function git(repo, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
|
|
52
|
+
export function git(repo, args, { maxBuffer = 64 * 1024 * 1024, quiet = false } = {}) {
|
|
53
53
|
return execFileSync("git", args, {
|
|
54
54
|
cwd: repo,
|
|
55
55
|
encoding: "utf8",
|
|
56
56
|
maxBuffer,
|
|
57
57
|
windowsHide: true,
|
|
58
|
+
// Evita `fatal: …` en la terminal cuando el fallo es esperado (repo vacío, etc.).
|
|
59
|
+
...(quiet ? { stdio: ["ignore", "pipe", "ignore"] } : {}),
|
|
58
60
|
});
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -136,13 +138,22 @@ export function showFileLines(repo, rev, path) {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
export function listBranches(repo) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
let raw = "";
|
|
142
|
+
try {
|
|
143
|
+
raw = git(
|
|
144
|
+
repo,
|
|
145
|
+
[
|
|
146
|
+
"for-each-ref",
|
|
147
|
+
"--sort=-committerdate",
|
|
148
|
+
"--format=%(refname:short)",
|
|
149
|
+
"refs/heads",
|
|
150
|
+
"refs/remotes",
|
|
151
|
+
],
|
|
152
|
+
{ quiet: true },
|
|
153
|
+
);
|
|
154
|
+
} catch {
|
|
155
|
+
throw new Error(`No es un repositorio git usable: ${repo}`);
|
|
156
|
+
}
|
|
146
157
|
const names = raw
|
|
147
158
|
.split("\n")
|
|
148
159
|
.map((s) => s.trim())
|
|
@@ -153,7 +164,12 @@ export function listBranches(repo) {
|
|
|
153
164
|
// Dedup conservando el orden de for-each-ref (más reciente primero).
|
|
154
165
|
const unique = [...new Set(names)];
|
|
155
166
|
|
|
156
|
-
|
|
167
|
+
let current;
|
|
168
|
+
try {
|
|
169
|
+
current = git(repo, ["rev-parse", "--abbrev-ref", "HEAD"], { quiet: true }).trim();
|
|
170
|
+
} catch {
|
|
171
|
+
throw new Error("El repositorio no tiene commits (falta HEAD).");
|
|
172
|
+
}
|
|
157
173
|
const bases = unique.filter((n) => n === "develop" || n === "main" || n === "master");
|
|
158
174
|
const defaultBase = bases.includes("develop")
|
|
159
175
|
? "develop"
|
package/lib/model-label.mjs
CHANGED
|
@@ -209,8 +209,22 @@ export function sortModelSelectOptions(options) {
|
|
|
209
209
|
return [...defaults, ...autos, ...middle];
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/** ¿ACP trajo un catálogo real (no solo default/auto)? */
|
|
213
|
+
export function liveModelsHaveCatalog(models) {
|
|
214
|
+
return (Array.isArray(models) ? models : []).some((m) => {
|
|
215
|
+
const v = String(m?.value ?? "")
|
|
216
|
+
.trim()
|
|
217
|
+
.toLowerCase();
|
|
218
|
+
if (!v || v === "default" || v.startsWith("default[")) return false;
|
|
219
|
+
if (v === "auto" || v.startsWith("auto[")) return false;
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
/**
|
|
213
|
-
*
|
|
225
|
+
* Lista para el select: default + modelos ACP.
|
|
226
|
+
* Los aliases fijos (`opus`/`sonnet`/`haiku`) solo se usan si ACP no trajo catálogo;
|
|
227
|
+
* si no, se ven repetidos (p. ej. `opus` + Opus 4.6 / 4.7 / 5).
|
|
214
228
|
* Clack: `label` = título amigable, `hint` = `[ 300k, medium ]`.
|
|
215
229
|
* UI: también `title` + `badges` para chips.
|
|
216
230
|
*
|
|
@@ -220,9 +234,11 @@ export function sortModelSelectOptions(options) {
|
|
|
220
234
|
*/
|
|
221
235
|
export function buildAcpModelSelectOptions(live, fixed) {
|
|
222
236
|
const fixedOptions = fixed?.options ?? [];
|
|
223
|
-
const
|
|
237
|
+
const liveModels = Array.isArray(live?.models) ? live.models : [];
|
|
238
|
+
const useFixedAliases = !liveModelsHaveCatalog(liveModels);
|
|
239
|
+
const fallback = useFixedAliases ? fixedOptions.filter((m) => m.value !== "") : [];
|
|
224
240
|
const configuredLabel = fixedOptions.find((m) => m.value === "")?.label;
|
|
225
|
-
const current =
|
|
241
|
+
const current = liveModels.find((m) => m.value === live.currentModel);
|
|
226
242
|
const seen = new Set(["", "default"]);
|
|
227
243
|
/** @type {{ value: string, label: string, hint?: string, title?: string, badges?: string[] }[]} */
|
|
228
244
|
const options = [
|
|
@@ -246,7 +262,7 @@ export function buildAcpModelSelectOptions(live, fixed) {
|
|
|
246
262
|
});
|
|
247
263
|
}
|
|
248
264
|
|
|
249
|
-
for (const m of
|
|
265
|
+
for (const m of liveModels) {
|
|
250
266
|
if (seen.has(m.value)) continue;
|
|
251
267
|
seen.add(m.value);
|
|
252
268
|
const formatted = formatModelOption(m.name, m.value);
|
package/lib/model-label.test.mjs
CHANGED
|
@@ -42,7 +42,7 @@ test("formatModelOption compacto y amigable", () => {
|
|
|
42
42
|
assert.deepEqual(c.badges, []);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test("buildAcpModelSelectOptions
|
|
45
|
+
test("buildAcpModelSelectOptions usa catálogo ACP y omite aliases fijos", () => {
|
|
46
46
|
const { options } = buildAcpModelSelectOptions(
|
|
47
47
|
{
|
|
48
48
|
currentModel: "default",
|
|
@@ -62,13 +62,26 @@ test("buildAcpModelSelectOptions fusiona aliases y formatea ACP", () => {
|
|
|
62
62
|
}
|
|
63
63
|
);
|
|
64
64
|
assert.equal(options[0].label, "Por defecto (opus)");
|
|
65
|
-
assert.ok(options.some((o) => o.value === "opus"));
|
|
65
|
+
assert.ok(!options.some((o) => o.value === "opus"));
|
|
66
66
|
const opusAcp = options.find((o) => o.value.startsWith("claude-opus-5-5"));
|
|
67
67
|
assert.equal(opusAcp?.title, "Claude Opus 5.5");
|
|
68
68
|
assert.deepEqual(opusAcp?.badges, ["300k", "medium"]);
|
|
69
69
|
assert.equal(opusAcp?.hint, "[ 300k, medium ]");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
test("buildAcpModelSelectOptions cae a aliases si ACP no trae catálogo", () => {
|
|
73
|
+
const { options } = buildAcpModelSelectOptions(
|
|
74
|
+
{ currentModel: "default", models: [{ value: "default", name: "Default" }] },
|
|
75
|
+
{
|
|
76
|
+
options: [
|
|
77
|
+
{ value: "", label: "Por defecto del agente" },
|
|
78
|
+
{ value: "opus", label: "opus", hint: "el más capaz" },
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
assert.ok(options.some((o) => o.value === "opus"));
|
|
83
|
+
});
|
|
84
|
+
|
|
72
85
|
test("modelOptionLabel compat", () => {
|
|
73
86
|
assert.equal(
|
|
74
87
|
modelOptionLabel("grok-4.7", "grok-4.7[context=256k,fast=true]"),
|
package/lib/prompt.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
35
35
|
"id": "t1",
|
|
36
36
|
"kind": "feat|fix|refactor|perf|test|chore|docs|infra",
|
|
37
37
|
"title": "string, aim for ≤72 chars, imperative, no kind prefix",
|
|
38
|
-
"intent": "string"
|
|
38
|
+
"intent": "string: why this theme exists + what it changes for the reader (not a restatement of the title)"
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
41
|
"blocks": [
|
|
@@ -48,8 +48,8 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
48
48
|
"start": 40,
|
|
49
49
|
"end": 58,
|
|
50
50
|
"op": "add|mod|del|rename",
|
|
51
|
-
"what": "string
|
|
52
|
-
"why": "string
|
|
51
|
+
"what": "string: concrete change + effect (aim ≤120 chars; longer OK if needed for clarity)",
|
|
52
|
+
"why": "string: motivation / why this change belongs in the branch (aim ≤120 chars)",
|
|
53
53
|
"source": "code|commit|pr|issue|inferred"
|
|
54
54
|
}
|
|
55
55
|
],
|
|
@@ -63,8 +63,8 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
63
63
|
"file": "relative/path.ext",
|
|
64
64
|
"line": 87,
|
|
65
65
|
"block": "b1",
|
|
66
|
-
"what": "string
|
|
67
|
-
"fix": "string
|
|
66
|
+
"what": "string: cause + why it is a problem + observable consequence (aim ≤120 chars; prefer clarity over brevity)",
|
|
67
|
+
"fix": "string: suggested change + why that change resolves the problem / what it avoids (aim ≤120 chars)"
|
|
68
68
|
}
|
|
69
69
|
],
|
|
70
70
|
"skipped": [
|
|
@@ -79,13 +79,15 @@ const RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\
|
|
|
79
79
|
- Line anchors: "lines"/"start"/"end"/"line" MUST match the chosen "side" ("new" = post-image / right side of @@; "old" = pre-image, typically deletions). One block per coherent change span, not one per line. Point findings at that block; leave "block" empty only if cross-cutting.
|
|
80
80
|
- Do NOT report: pre-existing code outside the diff; linter/formatter noise; naming/style prefs with no real consequence; intentional branch behavior; silenced lint rules; "missing docs/coverage" without a concrete broken scenario.
|
|
81
81
|
- Quality findings must name a concrete cost, not a vague feeling.
|
|
82
|
-
-
|
|
83
|
-
-
|
|
82
|
+
- Theme "intent": explain why the theme exists and what it changes for the reader — not a paraphrase of "title". One short sentence is too little; aim for 1–3 sentences with cause and scope.
|
|
83
|
+
- Top-level "intent": 2–4 sentences on what the branch solves and why it matters.
|
|
84
|
+
- Finding "what": cover (1) the concrete cause in the diff, (2) why it is a problem (failure mode, risk, or cost), and (3) the observable consequence. Do not stop at a symptom label.
|
|
85
|
+
- Finding "fix": cover (1) the suggested change and (2) why that change resolves the problem / what the reader gains or avoids. Vague advice ("mejorar el manejo") is not enough.
|
|
84
86
|
- Signal over volume: prefer fewer high-signal findings. Order findings by severity (high, then nit). Use "nit" sparingly.
|
|
85
87
|
- "blocking": true ONLY if the branch should not merge as-is. Use only with class "risk".
|
|
86
|
-
-
|
|
88
|
+
- Parallelize: when several read-only lookups are independent (Read, Grep/search, Glob, etc.), issue them in the same turn if your tools allow parallel calls. Do not serialize independent work — finish faster by batching.
|
|
87
89
|
- 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.
|
|
90
|
+
- Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters when possible. Prefer clarity over shortening — thin one-liners that omit cause, impact, or why-the-fix-works are worse than a slightly longer explanation. Longer text is still valid. Do not rewrite the file only to shorten it.
|
|
89
91
|
- 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.`;
|
|
90
92
|
|
|
91
93
|
/** @param {{ hasDiff?: boolean }} [ctx] */
|
|
@@ -315,7 +317,7 @@ Write the result to \`${outputPath}\` (relative to the repo root; use forward sl
|
|
|
315
317
|
|
|
316
318
|
${outputSchemaBlock(meta)}
|
|
317
319
|
|
|
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,
|
|
320
|
+
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, Copilot, …) 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).
|
|
319
321
|
|
|
320
322
|
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.
|
|
321
323
|
|
package/lib/prompt.test.mjs
CHANGED
|
@@ -35,3 +35,15 @@ test("buildPrompt tells agent to preserve finding resolution on rewrite", () =>
|
|
|
35
35
|
assert.match(text, /resolution/);
|
|
36
36
|
assert.match(text, /copy through any per-finding `resolution`/i);
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
test("buildPrompt asks for richer theme/finding explanations and parallel work", () => {
|
|
40
|
+
const text = buildPrompt(
|
|
41
|
+
{ source: "local", repo: "/tmp/r", branch: "feat", base: "develop" },
|
|
42
|
+
{ outputFilename: "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json" }
|
|
43
|
+
);
|
|
44
|
+
assert.match(text, /Theme "intent"/i);
|
|
45
|
+
assert.match(text, /why it is a problem/i);
|
|
46
|
+
assert.match(text, /why that change resolves the problem/i);
|
|
47
|
+
assert.match(text, /Parallelize/i);
|
|
48
|
+
assert.match(text, /same turn/i);
|
|
49
|
+
});
|
package/lib/review-meta.mjs
CHANGED
|
@@ -7,12 +7,15 @@ import { decodePayload } from "./json-payload.mjs";
|
|
|
7
7
|
/**
|
|
8
8
|
* `agentSession` = la sesión ACP que generó la review, para seguir conversando con el agente desde la web.
|
|
9
9
|
* @param {string} filePath
|
|
10
|
-
*
|
|
10
|
+
* `modelLabel` = nombre legible que el agente da al modelo ("Haiku 5.5"): `model` puede ser un alias
|
|
11
|
+
* (`haiku`) que más adelante apunte a otra versión.
|
|
12
|
+
* @param {{ agent?: string, model?: string, modelLabel?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
|
|
11
13
|
* @returns {boolean}
|
|
12
14
|
*/
|
|
13
|
-
export function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
|
|
15
|
+
export function stampReviewProvenance(filePath, { agent, model, modelLabel, agentSession } = {}) {
|
|
14
16
|
const agentName = String(agent || "").trim();
|
|
15
17
|
const modelId = String(model || "").trim();
|
|
18
|
+
const label = String(modelLabel || "").trim();
|
|
16
19
|
if (!agentName && !modelId && !agentSession) return false;
|
|
17
20
|
|
|
18
21
|
let payload;
|
|
@@ -27,7 +30,12 @@ export function stampReviewProvenance(filePath, { agent, model, agentSession } =
|
|
|
27
30
|
const meta =
|
|
28
31
|
current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
|
|
29
32
|
if (agentName) meta.agent = agentName;
|
|
30
|
-
if (modelId)
|
|
33
|
+
if (modelId) {
|
|
34
|
+
meta.model = modelId;
|
|
35
|
+
// Un label viejo describiría otro modelo: se reemplaza o se borra junto con `model`.
|
|
36
|
+
if (label && label !== modelId) meta.modelLabel = label;
|
|
37
|
+
else delete meta.modelLabel;
|
|
38
|
+
}
|
|
31
39
|
if (agentSession) meta.agentSession = { ...agentSession };
|
|
32
40
|
payload.meta = meta;
|
|
33
41
|
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
package/lib/review-meta.test.mjs
CHANGED
|
@@ -22,10 +22,10 @@ test("stampReviewProvenance no pisa el modelo si no hay uno", () => {
|
|
|
22
22
|
const file = join(dir, "diff-rev_a_b.json");
|
|
23
23
|
writeFileSync(file, JSON.stringify({ meta: { model: "sonnet" }, intent: "x" }));
|
|
24
24
|
|
|
25
|
-
stampReviewProvenance(file, { agent: "
|
|
25
|
+
stampReviewProvenance(file, { agent: "Cursor" });
|
|
26
26
|
|
|
27
27
|
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
28
|
-
assert.equal(saved.meta.agent, "
|
|
28
|
+
assert.equal(saved.meta.agent, "Cursor");
|
|
29
29
|
assert.equal(saved.meta.model, "sonnet");
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -39,3 +39,20 @@ test("stampReviewProvenance guarda la sesión del agente", () => {
|
|
|
39
39
|
|
|
40
40
|
assert.deepEqual(JSON.parse(readFileSync(file, "utf8")).meta.agentSession, agentSession);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
test("stampReviewProvenance guarda el nombre con versión junto al alias", () => {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), "diff-review-meta-"));
|
|
45
|
+
const file = join(dir, "diff-rev_a_b.json");
|
|
46
|
+
writeFileSync(file, JSON.stringify({ meta: { model: "claude-haiku-5-5" }, intent: "x" }));
|
|
47
|
+
|
|
48
|
+
stampReviewProvenance(file, { agent: "Claude Code", model: "haiku", modelLabel: "Haiku 5.5" });
|
|
49
|
+
let meta = JSON.parse(readFileSync(file, "utf8")).meta;
|
|
50
|
+
assert.equal(meta.model, "haiku");
|
|
51
|
+
assert.equal(meta.modelLabel, "Haiku 5.5");
|
|
52
|
+
|
|
53
|
+
// Otro modelo sin nombre conocido: el label anterior ya no corresponde.
|
|
54
|
+
stampReviewProvenance(file, { model: "claude-opus-5" });
|
|
55
|
+
meta = JSON.parse(readFileSync(file, "utf8")).meta;
|
|
56
|
+
assert.equal(meta.model, "claude-opus-5");
|
|
57
|
+
assert.equal(meta.modelLabel, undefined);
|
|
58
|
+
});
|