@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.
Files changed (154) hide show
  1. package/README.md +22 -14
  2. package/bin/diff-review.mjs +220 -29
  3. package/lib/acp.mjs +86 -10
  4. package/lib/agent-login.mjs +848 -36
  5. package/lib/agent-login.test.mjs +538 -0
  6. package/lib/agents.mjs +0 -39
  7. package/lib/agents.test.mjs +17 -24
  8. package/lib/extract-cache.test.mjs +162 -162
  9. package/lib/git.mjs +25 -9
  10. package/lib/model-label.mjs +20 -4
  11. package/lib/model-label.test.mjs +15 -2
  12. package/lib/prompt.mjs +12 -10
  13. package/lib/prompt.test.mjs +12 -0
  14. package/lib/review-meta.mjs +11 -3
  15. package/lib/review-meta.test.mjs +19 -2
  16. package/lib/review-prep.mjs +63 -63
  17. package/lib/validate.mjs +1 -1
  18. package/lib/wait-output.test.mjs +1 -1
  19. package/package.json +2 -2
  20. package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css +1 -0
  21. package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css.br +0 -0
  22. package/web/build/client/_app/immutable/assets/0.DdOl5Uve.css.gz +0 -0
  23. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
  24. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
  25. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
  26. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
  27. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
  28. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
  29. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
  30. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
  31. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
  32. package/web/build/client/_app/immutable/chunks/{cYiFfRa3.js → -cMMFbSG.js} +1 -1
  33. package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.br +2 -0
  34. package/web/build/client/_app/immutable/chunks/-cMMFbSG.js.gz +0 -0
  35. package/web/build/client/_app/immutable/chunks/0vBZPdKl.js +215 -0
  36. package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.br +0 -0
  37. package/web/build/client/_app/immutable/chunks/0vBZPdKl.js.gz +0 -0
  38. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
  39. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
  40. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
  41. package/web/build/client/_app/immutable/chunks/{DHO7NTh1.js → BMoYpzpA.js} +2 -2
  42. package/web/build/client/_app/immutable/chunks/BMoYpzpA.js.br +0 -0
  43. package/web/build/client/_app/immutable/chunks/BMoYpzpA.js.gz +0 -0
  44. package/web/build/client/_app/immutable/chunks/{CXhIC34H.js → DA0YnpGR.js} +3 -3
  45. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
  46. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
  47. package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js +2 -0
  48. package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.br +0 -0
  49. package/web/build/client/_app/immutable/entry/app.CT6P0WMK.js.gz +0 -0
  50. package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js +1 -0
  51. package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.br +0 -0
  52. package/web/build/client/_app/immutable/entry/start.U0TDB-jm.js.gz +0 -0
  53. package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js +1 -0
  54. package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.br +0 -0
  55. package/web/build/client/_app/immutable/nodes/0.ii5weUjH.js.gz +0 -0
  56. package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js +1 -0
  57. package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.br +2 -0
  58. package/web/build/client/_app/immutable/nodes/1.CM10dYkJ.js.gz +0 -0
  59. package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js +12 -0
  60. package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.br +0 -0
  61. package/web/build/client/_app/immutable/nodes/2.B1LHk3LQ.js.gz +0 -0
  62. package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js +340 -0
  63. package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js.br +0 -0
  64. package/web/build/client/_app/immutable/nodes/3.BQOdjQBQ.js.gz +0 -0
  65. package/web/build/client/_app/version.json +1 -1
  66. package/web/build/client/_app/version.json.br +0 -0
  67. package/web/build/client/_app/version.json.gz +0 -0
  68. package/web/build/handler.js +3 -3
  69. package/web/build/index.js +3 -3
  70. package/web/build/server/chunks/0-BUdytb9b.js +11 -0
  71. package/web/build/server/chunks/{0-D7NSxn1-.js.map → 0-BUdytb9b.js.map} +1 -1
  72. package/web/build/server/chunks/1-DRokSBkY.js +7 -0
  73. package/web/build/server/chunks/{1-DboYz8Fw.js.map → 1-DRokSBkY.js.map} +1 -1
  74. package/web/build/server/chunks/2-BxyzaJTR.js +7 -0
  75. package/web/build/server/chunks/2-BxyzaJTR.js.map +1 -0
  76. package/web/build/server/chunks/3-D8o0VWkj.js +7 -0
  77. package/web/build/server/chunks/{3-DgJ_LmTL.js.map → 3-D8o0VWkj.js.map} +1 -1
  78. package/web/build/server/chunks/{_server.ts-CP5cRLhW.js → _server.ts-kax5Szn0.js} +5 -4
  79. package/web/build/server/chunks/_server.ts-kax5Szn0.js.map +1 -0
  80. package/web/build/server/chunks/_server.ts-vS3A9t3f.js +123 -0
  81. package/web/build/server/chunks/_server.ts-vS3A9t3f.js.map +1 -0
  82. package/web/build/server/chunks/{agent-sessions-2XGojSJ_.js → agent-sessions-BArYLTW6.js} +1023 -135
  83. package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
  84. package/web/build/server/chunks/{git-review-jZ_JJYfv.js → git-review-hZSqk_1P.js} +54 -21
  85. package/web/build/server/chunks/{git-review-jZ_JJYfv.js.map → git-review-hZSqk_1P.js.map} +1 -1
  86. package/web/build/server/chunks/{handler-B_8yUXji.js → handler-DbFqzg7l.js} +3 -3
  87. package/web/build/server/chunks/{handler-B_8yUXji.js.map → handler-DbFqzg7l.js.map} +1 -1
  88. package/web/build/server/chunks/{hooks.server-rF_eBCff.js → hooks.server-BEl7rVad.js} +4 -3
  89. package/web/build/server/chunks/{hooks.server-rF_eBCff.js.map → hooks.server-BEl7rVad.js.map} +1 -1
  90. package/web/build/server/chunks/{index.js-CGw8_Q7V.js → index.js-BB9DZBdR.js} +4 -4
  91. package/web/build/server/chunks/index.js-BB9DZBdR.js.map +1 -0
  92. package/web/build/server/chunks/manifest.js-Dk6YX1jw.js +65 -0
  93. package/web/build/server/chunks/manifest.js-Dk6YX1jw.js.map +1 -0
  94. package/web/build/server/chunks/{remote-1g4ayag-BMpjk8UD.js → remote-1g4ayag-Bz1vj8X1.js} +6 -6
  95. package/web/build/server/chunks/remote-1g4ayag-Bz1vj8X1.js.map +1 -0
  96. package/web/build/server/chunks/{remote-wh6t6j-dYLCg8ks.js → remote-wh6t6j-DSwM3S9h.js} +44 -10
  97. package/web/build/server/chunks/remote-wh6t6j-DSwM3S9h.js.map +1 -0
  98. package/web/build/server/chunks/{validate-Da6e71V3.js → validate-CXjTRGnR.js} +34 -17
  99. package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
  100. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
  101. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  102. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
  103. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
  104. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  105. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  106. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css +0 -1
  107. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.br +0 -0
  108. package/web/build/client/_app/immutable/assets/3.DB-bJJq2.css.gz +0 -0
  109. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css +0 -1
  110. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.br +0 -0
  111. package/web/build/client/_app/immutable/assets/storage.BxR6930R.css.gz +0 -0
  112. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js +0 -1
  113. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.br +0 -1
  114. package/web/build/client/_app/immutable/chunks/BLMWhW3J.js.gz +0 -0
  115. package/web/build/client/_app/immutable/chunks/CXhIC34H.js.br +0 -0
  116. package/web/build/client/_app/immutable/chunks/CXhIC34H.js.gz +0 -0
  117. package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.br +0 -0
  118. package/web/build/client/_app/immutable/chunks/DHO7NTh1.js.gz +0 -0
  119. package/web/build/client/_app/immutable/chunks/DpxY75GB.js +0 -215
  120. package/web/build/client/_app/immutable/chunks/DpxY75GB.js.br +0 -0
  121. package/web/build/client/_app/immutable/chunks/DpxY75GB.js.gz +0 -0
  122. package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.br +0 -1
  123. package/web/build/client/_app/immutable/chunks/cYiFfRa3.js.gz +0 -0
  124. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js +0 -2
  125. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.br +0 -0
  126. package/web/build/client/_app/immutable/entry/app.CCv57mvp.js.gz +0 -0
  127. package/web/build/client/_app/immutable/entry/start.B012TSbW.js +0 -1
  128. package/web/build/client/_app/immutable/entry/start.B012TSbW.js.br +0 -1
  129. package/web/build/client/_app/immutable/entry/start.B012TSbW.js.gz +0 -0
  130. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js +0 -1
  131. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.br +0 -0
  132. package/web/build/client/_app/immutable/nodes/0.slrnBioC.js.gz +0 -0
  133. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js +0 -1
  134. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.br +0 -0
  135. package/web/build/client/_app/immutable/nodes/1.Bs5JdO4l.js.gz +0 -0
  136. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js +0 -10
  137. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.br +0 -0
  138. package/web/build/client/_app/immutable/nodes/2.D5ULH3EX.js.gz +0 -0
  139. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js +0 -173
  140. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.br +0 -0
  141. package/web/build/client/_app/immutable/nodes/3.hjzQQgyj.js.gz +0 -0
  142. package/web/build/server/chunks/0-D7NSxn1-.js +0 -11
  143. package/web/build/server/chunks/1-DboYz8Fw.js +0 -7
  144. package/web/build/server/chunks/2-B7LX817Q.js +0 -7
  145. package/web/build/server/chunks/2-B7LX817Q.js.map +0 -1
  146. package/web/build/server/chunks/3-DgJ_LmTL.js +0 -7
  147. package/web/build/server/chunks/_server.ts-CP5cRLhW.js.map +0 -1
  148. package/web/build/server/chunks/agent-sessions-2XGojSJ_.js.map +0 -1
  149. package/web/build/server/chunks/index.js-CGw8_Q7V.js.map +0 -1
  150. package/web/build/server/chunks/manifest.js-CImZFS6G.js +0 -58
  151. package/web/build/server/chunks/manifest.js-CImZFS6G.js.map +0 -1
  152. package/web/build/server/chunks/remote-1g4ayag-BMpjk8UD.js.map +0 -1
  153. package/web/build/server/chunks/remote-wh6t6j-dYLCg8ks.js.map +0 -1
  154. 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
- const raw = git(repo, [
140
- "for-each-ref",
141
- "--sort=-committerdate",
142
- "--format=%(refname:short)",
143
- "refs/heads",
144
- "refs/remotes",
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
- const current = git(repo, ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
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"
@@ -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
- * Misma lista que la UI: default + aliases fijos + modelos ACP.
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 fallback = fixedOptions.filter((m) => m.value !== "");
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 = live.models.find((m) => m.value === live.currentModel);
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 live.models) {
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);
@@ -42,7 +42,7 @@ test("formatModelOption compacto y amigable", () => {
42
42
  assert.deepEqual(c.badges, []);
43
43
  });
44
44
 
45
- test("buildAcpModelSelectOptions fusiona aliases y formatea ACP", () => {
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, aim for ≤120 chars: concrete change + effect",
52
- "why": "string, aim for ≤120 chars",
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, aim for ≤120 chars: concrete cause + observable consequence (what fails / risk / cost)",
67
- "fix": "string, aim for ≤120 chars: suggested change + what the reader gains or avoids if applied"
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
- - Finding "what": state the concrete cause and the observable consequence (what fails, the risk, or the cost) — not only a symptom.
83
- - Finding "fix": state the suggested change and what the reader gains or avoids if they apply it.
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
- - When several read-only lookups are independent (files, searches), batch them in the same turn if your tools allow parallel calls.
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, 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).
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
 
@@ -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
+ });
@@ -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
- * @param {{ agent?: string, model?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
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) meta.model = 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");
@@ -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: "Codex" });
25
+ stampReviewProvenance(file, { agent: "Cursor" });
26
26
 
27
27
  const saved = JSON.parse(readFileSync(file, "utf8"));
28
- assert.equal(saved.meta.agent, "Codex");
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
+ });