@jpkme/diff-review 0.1.12 → 0.1.14

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 (126) hide show
  1. package/README.md +1 -1
  2. package/bin/diff-review.mjs +72 -53
  3. package/lib/acp-policy.mjs +99 -12
  4. package/lib/acp-policy.test.mjs +56 -3
  5. package/lib/acp-shell.mjs +38 -8
  6. package/lib/acp-shell.test.mjs +32 -1
  7. package/lib/agent-login.mjs +105 -105
  8. package/lib/check-report.mjs +1 -0
  9. package/lib/check-report.test.mjs +1 -0
  10. package/lib/extract-cache.test.mjs +162 -162
  11. package/lib/model-label.mjs +263 -263
  12. package/lib/model-label.test.mjs +77 -77
  13. package/lib/models-cache.mjs +75 -75
  14. package/lib/models-cache.test.mjs +50 -50
  15. package/lib/prompt.mjs +8 -6
  16. package/lib/review-prep.mjs +63 -63
  17. package/package.json +1 -1
  18. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
  19. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  20. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
  21. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +1 -0
  22. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  23. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  24. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
  25. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
  26. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
  27. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css +1 -0
  28. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.br +0 -0
  29. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.gz +0 -0
  30. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js +2 -0
  31. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.br +0 -0
  32. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.gz +0 -0
  33. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js +210 -0
  34. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.br +0 -0
  35. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.gz +0 -0
  36. package/web/build/client/_app/immutable/chunks/{CPyWH418.js → FOc6rgIW.js} +1 -1
  37. package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.br +2 -0
  38. package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.gz +0 -0
  39. package/web/build/client/_app/immutable/entry/{app.BiBohpyO.js → app.DcAtSJ3n.js} +2 -2
  40. package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.br +0 -0
  41. package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.gz +0 -0
  42. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js +1 -0
  43. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.br +0 -0
  44. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.gz +0 -0
  45. package/web/build/client/_app/immutable/nodes/{1.BzPXaJJj.js → 1.E-Hs7ura.js} +1 -1
  46. package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.br +4 -0
  47. package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.gz +0 -0
  48. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js +10 -0
  49. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.br +0 -0
  50. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.gz +0 -0
  51. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js +171 -0
  52. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.br +0 -0
  53. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.gz +0 -0
  54. package/web/build/client/_app/version.json +1 -1
  55. package/web/build/client/_app/version.json.br +0 -0
  56. package/web/build/client/_app/version.json.gz +0 -0
  57. package/web/build/handler.js +3 -3
  58. package/web/build/index.js +3 -3
  59. package/web/build/server/chunks/{0-BYok4D0n.js → 0-DUStDOsr.js} +3 -3
  60. package/web/build/server/chunks/{0-BYok4D0n.js.map → 0-DUStDOsr.js.map} +1 -1
  61. package/web/build/server/chunks/1-BFhhV5JB.js +7 -0
  62. package/web/build/server/chunks/{1-C7zcB0q0.js.map → 1-BFhhV5JB.js.map} +1 -1
  63. package/web/build/server/chunks/2-BZ_MVqj8.js +7 -0
  64. package/web/build/server/chunks/2-BZ_MVqj8.js.map +1 -0
  65. package/web/build/server/chunks/3-BV-TIUp1.js +7 -0
  66. package/web/build/server/chunks/{3-DAAmG6VX.js.map → 3-BV-TIUp1.js.map} +1 -1
  67. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js → _server.ts-CdFoXw36.js} +4 -4
  68. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js.map → _server.ts-CdFoXw36.js.map} +1 -1
  69. package/web/build/server/chunks/{agent-sessions-DSqGavSn.js → agent-sessions-D0WwtWgR.js} +223 -51
  70. package/web/build/server/chunks/agent-sessions-D0WwtWgR.js.map +1 -0
  71. package/web/build/server/chunks/{git-review-CpO_G8z8.js → git-review-CgcBIbvM.js} +7 -7
  72. package/web/build/server/chunks/{git-review-CpO_G8z8.js.map → git-review-CgcBIbvM.js.map} +1 -1
  73. package/web/build/server/chunks/{handler-Di1wJQuU.js → handler-DbxjtK9M.js} +3 -3
  74. package/web/build/server/chunks/{handler-Di1wJQuU.js.map → handler-DbxjtK9M.js.map} +1 -1
  75. package/web/build/server/chunks/{hooks.server-BLOHobEK.js → hooks.server-Dg6gYUqs.js} +3 -3
  76. package/web/build/server/chunks/{hooks.server-BLOHobEK.js.map → hooks.server-Dg6gYUqs.js.map} +1 -1
  77. package/web/build/server/chunks/{index.js-yLYmrAWy.js → index.js-NNne-njN.js} +876 -879
  78. package/web/build/server/chunks/index.js-NNne-njN.js.map +1 -0
  79. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js → manifest.js-V8ViI5bl.js} +9 -9
  80. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js.map → manifest.js-V8ViI5bl.js.map} +1 -1
  81. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js → remote-1g4ayag-CCsG2QXw.js} +4 -4
  82. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js.map → remote-1g4ayag-CCsG2QXw.js.map} +1 -1
  83. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js → remote-wh6t6j-BCvmX37z.js} +5 -5
  84. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js.map → remote-wh6t6j-BCvmX37z.js.map} +1 -1
  85. package/web/build/server/chunks/{validate-BsTKyhsq.js → validate-CAQ31C7k.js} +2 -2
  86. package/web/build/server/chunks/{validate-BsTKyhsq.js.map → validate-CAQ31C7k.js.map} +1 -1
  87. package/web/build/client/_app/immutable/assets/0.CLQiq3Ed.css.br +0 -0
  88. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +0 -1
  89. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
  90. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
  91. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +0 -1
  92. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
  93. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
  94. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +0 -1
  95. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
  96. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
  97. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js +0 -2
  98. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
  99. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
  100. package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
  101. package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
  102. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +0 -210
  103. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
  104. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
  105. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
  106. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
  107. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +0 -1
  108. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
  109. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
  110. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +0 -2
  111. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
  112. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +0 -10
  113. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
  114. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
  115. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +0 -171
  116. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
  117. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.gz +0 -0
  118. package/web/build/server/chunks/1-C7zcB0q0.js +0 -7
  119. package/web/build/server/chunks/2-v6ejI_0-.js +0 -7
  120. package/web/build/server/chunks/2-v6ejI_0-.js.map +0 -1
  121. package/web/build/server/chunks/3-DAAmG6VX.js +0 -7
  122. package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +0 -1
  123. package/web/build/server/chunks/index.js-yLYmrAWy.js.map +0 -1
  124. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js → 0.Dj97JW46.js} +0 -0
  125. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.br → 0.Dj97JW46.js.br} +0 -0
  126. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.gz → 0.Dj97JW46.js.gz} +0 -0
@@ -1,105 +1,105 @@
1
- /**
2
- * Login de agentes ACP: mensajes claros + abrir una terminal interactiva cuando se pueda.
3
- */
4
- import { spawn } from "node:child_process";
5
-
6
- /**
7
- * @typedef {{ bin: string, args: string[], display: string }} LoginCommand
8
- */
9
-
10
- /**
11
- * Comando de login interactivo por agente (null = no hay CLI de login, p. ej. Cursor app).
12
- * @param {string} agentId
13
- * @returns {LoginCommand | null}
14
- */
15
- export function loginCommand(agentId) {
16
- switch (agentId) {
17
- case "claude":
18
- return { bin: "claude", args: ["/login"], display: "claude /login" };
19
- case "copilot":
20
- return { bin: "gh", args: ["auth", "login"], display: "gh auth login" };
21
- case "codex":
22
- return { bin: "codex", args: ["login"], display: "codex login" };
23
- case "gemini":
24
- return { bin: "gemini", args: [], display: "gemini" }; // el CLI guía el auth al arrancar
25
- default:
26
- return null;
27
- }
28
- }
29
-
30
- /** @param {string} text */
31
- export function looksLikeAuthFailure(text) {
32
- const s = String(text || "").toLowerCase();
33
- return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run/.test(
34
- s
35
- );
36
- }
37
-
38
- /**
39
- * Mensaje en español si el agente murió / falló por falta de login.
40
- * @param {string} agentId
41
- * @param {string} agentLabel
42
- * @param {string} detail
43
- * @returns {string | null}
44
- */
45
- export function authFailureMessage(agentId, agentLabel, detail) {
46
- if (!looksLikeAuthFailure(detail)) return null;
47
- const cmd = loginCommand(agentId);
48
- if (agentId === "cursor") {
49
- return `${agentLabel} no está autenticado. Iniciá sesión en la app de Cursor y reintentá.`;
50
- }
51
- if (cmd) {
52
- return `${agentLabel} no está autenticado. Corré \`${cmd.display}\` en una terminal y reintentá.`;
53
- }
54
- return `${agentLabel} no está autenticado. Revisá el login del agente y reintentá.`;
55
- }
56
-
57
- /**
58
- * Abre una terminal nueva con el login del agente (Windows / macOS / Linux).
59
- * @param {string} agentId
60
- * @returns {{ display: string }}
61
- */
62
- export function openLoginTerminal(agentId) {
63
- const cmd = loginCommand(agentId);
64
- if (!cmd) {
65
- throw new Error(
66
- agentId === "cursor"
67
- ? "Cursor se autentica desde la app de escritorio, no desde una terminal."
68
- : "Este agente no tiene un comando de login conocido."
69
- );
70
- }
71
-
72
- const platform = process.platform;
73
- if (platform === "win32") {
74
- // `start "title" cmd /k …` — el primer quoted es el título de la ventana.
75
- spawn("cmd.exe", ["/c", "start", `${cmd.bin} login`, "cmd.exe", "/k", cmd.bin, ...cmd.args], {
76
- detached: true,
77
- stdio: "ignore",
78
- windowsHide: false,
79
- }).unref();
80
- } else if (platform === "darwin") {
81
- const script = `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`;
82
- spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
83
- } else {
84
- const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
85
- const tried = [
86
- ["x-terminal-emulator", ["-e", "bash", "-lc", `${line}; exec bash`]],
87
- ["gnome-terminal", ["--", "bash", "-lc", `${line}; exec bash`]],
88
- ["konsole", ["-e", "bash", "-lc", `${line}; exec bash`]],
89
- ["xterm", ["-e", "bash", "-lc", `${line}; exec bash`]],
90
- ];
91
- let ok = false;
92
- for (const [bin, args] of tried) {
93
- try {
94
- spawn(bin, args, { detached: true, stdio: "ignore" }).unref();
95
- ok = true;
96
- break;
97
- } catch {
98
- /* siguiente */
99
- }
100
- }
101
- if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
102
- }
103
-
104
- return { display: cmd.display };
105
- }
1
+ /**
2
+ * Login de agentes ACP: mensajes claros + abrir una terminal interactiva cuando se pueda.
3
+ */
4
+ import { spawn } from "node:child_process";
5
+
6
+ /**
7
+ * @typedef {{ bin: string, args: string[], display: string }} LoginCommand
8
+ */
9
+
10
+ /**
11
+ * Comando de login interactivo por agente (null = no hay CLI de login, p. ej. Cursor app).
12
+ * @param {string} agentId
13
+ * @returns {LoginCommand | null}
14
+ */
15
+ export function loginCommand(agentId) {
16
+ switch (agentId) {
17
+ case "claude":
18
+ return { bin: "claude", args: ["/login"], display: "claude /login" };
19
+ case "copilot":
20
+ return { bin: "gh", args: ["auth", "login"], display: "gh auth login" };
21
+ case "codex":
22
+ return { bin: "codex", args: ["login"], display: "codex login" };
23
+ case "gemini":
24
+ return { bin: "gemini", args: [], display: "gemini" }; // el CLI guía el auth al arrancar
25
+ default:
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /** @param {string} text */
31
+ export function looksLikeAuthFailure(text) {
32
+ const s = String(text || "").toLowerCase();
33
+ return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run/.test(
34
+ s
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Mensaje en español si el agente murió / falló por falta de login.
40
+ * @param {string} agentId
41
+ * @param {string} agentLabel
42
+ * @param {string} detail
43
+ * @returns {string | null}
44
+ */
45
+ export function authFailureMessage(agentId, agentLabel, detail) {
46
+ if (!looksLikeAuthFailure(detail)) return null;
47
+ const cmd = loginCommand(agentId);
48
+ if (agentId === "cursor") {
49
+ return `${agentLabel} no está autenticado. Iniciá sesión en la app de Cursor y reintentá.`;
50
+ }
51
+ if (cmd) {
52
+ return `${agentLabel} no está autenticado. Corré \`${cmd.display}\` en una terminal y reintentá.`;
53
+ }
54
+ return `${agentLabel} no está autenticado. Revisá el login del agente y reintentá.`;
55
+ }
56
+
57
+ /**
58
+ * Abre una terminal nueva con el login del agente (Windows / macOS / Linux).
59
+ * @param {string} agentId
60
+ * @returns {{ display: string }}
61
+ */
62
+ export function openLoginTerminal(agentId) {
63
+ const cmd = loginCommand(agentId);
64
+ if (!cmd) {
65
+ throw new Error(
66
+ agentId === "cursor"
67
+ ? "Cursor se autentica desde la app de escritorio, no desde una terminal."
68
+ : "Este agente no tiene un comando de login conocido."
69
+ );
70
+ }
71
+
72
+ const platform = process.platform;
73
+ if (platform === "win32") {
74
+ // `start "title" cmd /k …` — el primer quoted es el título de la ventana.
75
+ spawn("cmd.exe", ["/c", "start", `${cmd.bin} login`, "cmd.exe", "/k", cmd.bin, ...cmd.args], {
76
+ detached: true,
77
+ stdio: "ignore",
78
+ windowsHide: false,
79
+ }).unref();
80
+ } else if (platform === "darwin") {
81
+ const script = `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`;
82
+ spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
83
+ } else {
84
+ const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
85
+ const tried = [
86
+ ["x-terminal-emulator", ["-e", "bash", "-lc", `${line}; exec bash`]],
87
+ ["gnome-terminal", ["--", "bash", "-lc", `${line}; exec bash`]],
88
+ ["konsole", ["-e", "bash", "-lc", `${line}; exec bash`]],
89
+ ["xterm", ["-e", "bash", "-lc", `${line}; exec bash`]],
90
+ ];
91
+ let ok = false;
92
+ for (const [bin, args] of tried) {
93
+ try {
94
+ spawn(bin, args, { detached: true, stdio: "ignore" }).unref();
95
+ ok = true;
96
+ break;
97
+ } catch {
98
+ /* siguiente */
99
+ }
100
+ }
101
+ if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
102
+ }
103
+
104
+ return { display: cmd.display };
105
+ }
@@ -177,6 +177,7 @@ export function formatCheckReport(file, command, result) {
177
177
  "How to continue:",
178
178
  "- Fix every problem above in place. Keep existing meta/intent/groups/blocks/findings; only change what is wrong or missing.",
179
179
  '- Every diff path must appear in "blocks" or "skipped" (a skipped glob counts).',
180
+ "- If a problem lists missing paths: ADD a block or a skipped entry for each exact path. Do not only bump meta.generatedAt, notes, or claim the file is already complete.",
180
181
  "- meta.agent and meta.model are required (product name and the exact model id you are running).",
181
182
  "- intent ≥40 chars; enums exactly as in the schema.",
182
183
  "- Human-readable strings in Spanish.",
@@ -44,4 +44,5 @@ test("formatCheckReport explica cómo seguir", () => {
44
44
  assert.match(text, /How to continue/);
45
45
  assert.match(text, /npx --yes @jpkme\/diff-review validate/);
46
46
  assert.match(text, /same file/);
47
+ assert.match(text, /generatedAt/);
47
48
  });
@@ -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
+ });