@jpkme/diff-review 0.1.7 → 0.1.12
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 +38 -15
- package/bin/diff-review.mjs +156 -34
- package/lib/acp-policy.mjs +136 -0
- package/lib/acp-policy.test.mjs +83 -0
- package/lib/acp-shell.mjs +379 -0
- package/lib/acp-shell.test.mjs +127 -0
- package/lib/acp.mjs +368 -0
- package/lib/acp.test.mjs +58 -0
- package/lib/agent-login.mjs +105 -0
- package/lib/agents.mjs +102 -14
- package/lib/agents.test.mjs +4 -6
- package/lib/check-report.mjs +31 -12
- package/lib/check-report.test.mjs +1 -0
- package/lib/extract-cache.test.mjs +162 -0
- package/lib/extract.mjs +197 -1
- package/lib/model-label.mjs +263 -0
- package/lib/model-label.test.mjs +77 -0
- package/lib/models-cache.mjs +75 -0
- package/lib/models-cache.test.mjs +50 -0
- package/lib/output-name.mjs +8 -0
- package/lib/prompt.mjs +178 -31
- package/lib/review-dir.mjs +108 -0
- package/lib/review-meta.mjs +35 -0
- package/lib/review-meta.test.mjs +41 -0
- package/lib/review-prep.mjs +63 -0
- package/lib/review.test.mjs +1 -0
- package/lib/validate.mjs +28 -11
- package/lib/validate.test.mjs +25 -0
- package/lib/wait-output.mjs +46 -23
- package/lib/wait-output.test.mjs +25 -1
- package/package.json +3 -1
- package/schema.json +9 -10
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +1 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +1 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js +2 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js +1 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js +3 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +210 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +1 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js +2 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Cak4ehiE.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +171 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.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-SseDy__O.js → 0-BYok4D0n.js} +2 -2
- package/web/build/server/chunks/{0-SseDy__O.js.map → 0-BYok4D0n.js.map} +1 -1
- package/web/build/server/chunks/1-C7zcB0q0.js +7 -0
- package/web/build/server/chunks/1-C7zcB0q0.js.map +1 -0
- package/web/build/server/chunks/2-v6ejI_0-.js +7 -0
- package/web/build/server/chunks/2-v6ejI_0-.js.map +1 -0
- package/web/build/server/chunks/3-DAAmG6VX.js +7 -0
- package/web/build/server/chunks/3-DAAmG6VX.js.map +1 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js +62 -0
- package/web/build/server/chunks/_server.ts-qYVrMYPB.js.map +1 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js +2688 -0
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-B8KZPTkn.js → git-review-CpO_G8z8.js} +70 -704
- package/web/build/server/chunks/git-review-CpO_G8z8.js.map +1 -0
- package/web/build/server/chunks/{handler-cq3QoEzW.js → handler-Di1wJQuU.js} +3 -3
- package/web/build/server/chunks/{handler-cq3QoEzW.js.map → handler-Di1wJQuU.js.map} +1 -1
- package/web/build/server/chunks/hooks.server-BLOHobEK.js +42 -0
- package/web/build/server/chunks/hooks.server-BLOHobEK.js.map +1 -0
- package/web/build/server/chunks/{index.js-D4qkHe3r.js → index.js-yLYmrAWy.js} +3 -5
- package/web/build/server/chunks/index.js-yLYmrAWy.js.map +1 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js +58 -0
- package/web/build/server/chunks/manifest.js-CXj8IIo3.js.map +1 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js +144 -0
- package/web/build/server/chunks/remote-1g4ayag-BiJeU0Pk.js.map +1 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js +162 -0
- package/web/build/server/chunks/remote-wh6t6j-CHanXurm.js.map +1 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js +1073 -0
- package/web/build/server/chunks/validate-BsTKyhsq.js.map +1 -0
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css +0 -1
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.COsJT9ly.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.qhPDz1xQ.css +0 -1
- package/web/build/client/_app/immutable/assets/3.qhPDz1xQ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.qhPDz1xQ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css +0 -1
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.br +0 -0
- package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js +0 -2
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/C4--ziCr.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js +0 -174
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/D8sAxZVo.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js +0 -3
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js +0 -1
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js +0 -2
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dl9OTE-n.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js +0 -1
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js.br +0 -1
- package/web/build/client/_app/immutable/entry/start.CDz8Qs8Q.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.br +0 -1
- package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.B1c4AH1e.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Dx-oOpEf.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.js +0 -170
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.C-USexyX.js.gz +0 -0
- package/web/build/server/chunks/1-0X6Jp9nH.js +0 -7
- package/web/build/server/chunks/1-0X6Jp9nH.js.map +0 -1
- package/web/build/server/chunks/2-HlcVALg6.js +0 -7
- package/web/build/server/chunks/2-HlcVALg6.js.map +0 -1
- package/web/build/server/chunks/3-BN1eS7L2.js +0 -7
- package/web/build/server/chunks/3-BN1eS7L2.js.map +0 -1
- package/web/build/server/chunks/hooks.server-DERFlOe1.js +0 -14
- package/web/build/server/chunks/hooks.server-DERFlOe1.js.map +0 -1
- package/web/build/server/chunks/index.js-D4qkHe3r.js.map +0 -1
- package/web/build/server/chunks/manifest.js-DQHMKyJh.js +0 -50
- package/web/build/server/chunks/manifest.js-DQHMKyJh.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-B8KZPTkn.js.map +0 -1
package/lib/prompt.mjs
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
import {
|
|
6
6
|
makeOutputFilename,
|
|
7
7
|
isOutputFilename,
|
|
8
|
+
OUTPUT_DIR,
|
|
8
9
|
OUTPUT_FILENAME_LEGACY,
|
|
10
|
+
outputRelPath,
|
|
9
11
|
} from "./output-name.mjs";
|
|
10
12
|
|
|
11
13
|
export {
|
|
12
14
|
makeOutputFilename,
|
|
13
15
|
isOutputFilename,
|
|
16
|
+
OUTPUT_DIR,
|
|
14
17
|
OUTPUT_FILENAME_LEGACY,
|
|
18
|
+
outputRelPath,
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
export const OUTPUT_SCHEMA_BLOCK = `{
|
|
@@ -21,31 +25,31 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
21
25
|
"branch": "<branch under review>",
|
|
22
26
|
"base": "develop",
|
|
23
27
|
"remoteUrl": "<origin URL; optional when source=url>",
|
|
24
|
-
"generatedAt": "<ISO-8601 datetime; optional
|
|
25
|
-
"agent": "<
|
|
26
|
-
"model": "<model id
|
|
28
|
+
"generatedAt": "<ISO-8601 datetime; optional>",
|
|
29
|
+
"agent": "<required: product name you are, e.g. Claude Code>",
|
|
30
|
+
"model": "<required: exact model id you are running>"
|
|
27
31
|
},
|
|
28
32
|
"intent": "string",
|
|
29
33
|
"groups": [
|
|
30
34
|
{
|
|
31
|
-
"id": "
|
|
35
|
+
"id": "t1",
|
|
32
36
|
"kind": "feat|fix|refactor|perf|test|chore|docs|infra",
|
|
33
|
-
"title": "string ≤72 chars, imperative, no kind prefix",
|
|
37
|
+
"title": "string, aim for ≤72 chars, imperative, no kind prefix",
|
|
34
38
|
"intent": "string"
|
|
35
39
|
}
|
|
36
40
|
],
|
|
37
41
|
"blocks": [
|
|
38
42
|
{
|
|
39
43
|
"id": "b1",
|
|
40
|
-
"group": "
|
|
44
|
+
"group": "t1",
|
|
41
45
|
"file": "relative/path.ext",
|
|
42
46
|
"lines": "L40-58",
|
|
43
47
|
"side": "new|old",
|
|
44
48
|
"start": 40,
|
|
45
49
|
"end": 58,
|
|
46
50
|
"op": "add|mod|del|rename",
|
|
47
|
-
"what": "string",
|
|
48
|
-
"why": "string",
|
|
51
|
+
"what": "string, aim for ≤120 chars: concrete change + effect",
|
|
52
|
+
"why": "string, aim for ≤120 chars",
|
|
49
53
|
"source": "code|commit|pr|issue|inferred"
|
|
50
54
|
}
|
|
51
55
|
],
|
|
@@ -59,8 +63,8 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
59
63
|
"file": "relative/path.ext",
|
|
60
64
|
"line": 87,
|
|
61
65
|
"block": "b1",
|
|
62
|
-
"what": "string",
|
|
63
|
-
"fix": "string"
|
|
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"
|
|
64
68
|
}
|
|
65
69
|
],
|
|
66
70
|
"skipped": [
|
|
@@ -69,16 +73,29 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
69
73
|
"notes": ["optional short limitations"]
|
|
70
74
|
}`;
|
|
71
75
|
|
|
72
|
-
const RULES = `-
|
|
76
|
+
const RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\`, …; blocks use \`b1\`, \`b2\`, …. Reference themes from blocks via \`"group": "t1"\`.
|
|
77
|
+
- Coverage (hard): every path in the diff MUST appear in "blocks" and/or "skipped". Prefer a skipped glob when many files share one reason.
|
|
73
78
|
- Do NOT invent files, hunks, or line ranges that are not in the diff.
|
|
74
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.
|
|
75
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.
|
|
76
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.
|
|
77
84
|
- Signal over volume: prefer fewer high-signal findings. Order findings by severity (high, then nit). Use "nit" sparingly.
|
|
78
85
|
- "blocking": true ONLY if the branch should not merge as-is. Use only with class "risk".
|
|
79
|
-
-
|
|
86
|
+
- When several read-only lookups are independent (files, searches), batch them in the same turn if your tools allow parallel calls.
|
|
87
|
+
- Write human-readable strings (intent, title, what, why, fix, notes) in Spanish. Keep enums/ids/paths/JSON keys exactly as in the schema.
|
|
88
|
+
- Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Prefer clarity over shortening. Longer text is still valid. Do not rewrite the file only to shorten it.`;
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
/** @param {{ hasDiff?: boolean }} [ctx] */
|
|
91
|
+
function workflowLocal(base, branch, ctx = {}) {
|
|
92
|
+
if (ctx.hasDiff) {
|
|
93
|
+
return `Workflow:
|
|
94
|
+
1. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
|
|
95
|
+
2. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
96
|
+
3. Emit the JSON file. Do not modify the repo.
|
|
97
|
+
4. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
|
|
98
|
+
}
|
|
82
99
|
return `Workflow:
|
|
83
100
|
1. Resolve the merge-base of \`${base}\` and \`${branch}\`.
|
|
84
101
|
2. Run exactly: \`git diff ${base}...${branch}\` (three-dot / merge-base diff). Do not use two-dot unless three-dot is impossible.
|
|
@@ -87,13 +104,22 @@ function workflowLocal(base, branch) {
|
|
|
87
104
|
5. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
/** Comando que el agente debe correr; npx por si el bin no está en PATH. */
|
|
107
|
+
/** Comando que el agente debe correr (sobre `.diff-review/<nombre>`); npx por si el bin no está en PATH. */
|
|
91
108
|
export function validateCliCommand(filename) {
|
|
92
|
-
const file = String(filename || "diff-rev_<branchTip>_<baseTip>.json").trim();
|
|
109
|
+
const file = outputRelPath(String(filename || "diff-rev_<branchTip>_<baseTip>.json").trim());
|
|
93
110
|
return `npx --yes @jpkme/diff-review validate ${file}`;
|
|
94
111
|
}
|
|
95
112
|
|
|
96
|
-
|
|
113
|
+
/** @param {{ hasDiff?: boolean }} [ctx] */
|
|
114
|
+
function workflowUrl(base, branch, ctx = {}) {
|
|
115
|
+
if (ctx.hasDiff) {
|
|
116
|
+
return `Workflow:
|
|
117
|
+
1. If the repo is not local, clone it (or use whatever access you have). Do not modify it.
|
|
118
|
+
2. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
|
|
119
|
+
3. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
120
|
+
4. Emit the JSON file.
|
|
121
|
+
5. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
|
|
122
|
+
}
|
|
97
123
|
return `Workflow:
|
|
98
124
|
1. If the repo is not local, clone it (or use whatever access you have). Do not modify it.
|
|
99
125
|
2. Resolve the merge-base of \`${base}\` and \`${branch}\`.
|
|
@@ -103,6 +129,35 @@ function workflowUrl(base, branch) {
|
|
|
103
129
|
6. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
|
|
104
130
|
}
|
|
105
131
|
|
|
132
|
+
/** Workflow sin paso de validate por CLI (sesiones ACP: valida el server). */
|
|
133
|
+
function workflowLocalAcp(ctx = {}) {
|
|
134
|
+
if (ctx.hasDiff) {
|
|
135
|
+
return `Workflow:
|
|
136
|
+
1. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
|
|
137
|
+
2. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
138
|
+
3. Write the JSON with your file Write/Edit tool (never shell redirection). Do not modify other repo files. The host validates when your turn ends — do not run the validate CLI.`;
|
|
139
|
+
}
|
|
140
|
+
return `Workflow:
|
|
141
|
+
1. Resolve the merge-base of the base and branch named above, then run the three-dot \`git diff base...branch\`.
|
|
142
|
+
2. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
143
|
+
3. Write the JSON with your file Write/Edit tool (never shell redirection). Do not modify other repo files. The host validates when your turn ends — do not run the validate CLI.`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function workflowUrlAcp(ctx = {}) {
|
|
147
|
+
if (ctx.hasDiff) {
|
|
148
|
+
return `Workflow:
|
|
149
|
+
1. If the repo is not local, clone it (or use whatever access you have). Do not modify it.
|
|
150
|
+
2. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
|
|
151
|
+
3. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
152
|
+
4. Write the JSON with your file Write/Edit tool (never shell redirection). The host validates when your turn ends — do not run the validate CLI.`;
|
|
153
|
+
}
|
|
154
|
+
return `Workflow:
|
|
155
|
+
1. If the repo is not local, clone it (or use whatever access you have). Do not modify it.
|
|
156
|
+
2. Resolve the merge-base, then run the three-dot \`git diff base...branch\`.
|
|
157
|
+
3. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
158
|
+
4. Write the JSON with your file Write/Edit tool (never shell redirection). The host validates when your turn ends — do not run the validate CLI.`;
|
|
159
|
+
}
|
|
160
|
+
|
|
106
161
|
/**
|
|
107
162
|
* @param {{ source?: string, repo?: string, branch?: string, base?: string, remoteUrl?: string }} meta
|
|
108
163
|
*/
|
|
@@ -122,25 +177,120 @@ export function outputSchemaBlock(meta) {
|
|
|
122
177
|
if (remoteUrl) metaLines.push(` "remoteUrl": ${JSON.stringify(remoteUrl)},`);
|
|
123
178
|
metaLines.push(
|
|
124
179
|
` "generatedAt": "<ISO-8601 datetime>",`,
|
|
125
|
-
` "agent": "<
|
|
126
|
-
` "model": "<model id>"`
|
|
180
|
+
` "agent": "<required: product name you are, e.g. Claude Code>",`,
|
|
181
|
+
` "model": "<required: exact model id you are running>"`
|
|
127
182
|
);
|
|
128
183
|
const metaBlock = `"meta": {\n${metaLines.join("\n")}\n }`;
|
|
129
184
|
return OUTPUT_SCHEMA_BLOCK.replace(/"meta": \{[\s\S]*?\n \}/, metaBlock);
|
|
130
185
|
}
|
|
131
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Bloque de contexto git ya resuelto (diff en disco, tips, lista de archivos, ignores).
|
|
189
|
+
* @param {{
|
|
190
|
+
* diffRelPath?: string | null,
|
|
191
|
+
* mergeBase?: string,
|
|
192
|
+
* branchSha?: string,
|
|
193
|
+
* baseSha?: string,
|
|
194
|
+
* files?: Array<{ path: string, changeType?: string }>,
|
|
195
|
+
* ignored?: string[],
|
|
196
|
+
* ignoreGlobs?: string[],
|
|
197
|
+
* }} ctx
|
|
198
|
+
*/
|
|
199
|
+
function formatPrecomputedContext(ctx) {
|
|
200
|
+
const lines = ["Precomputed git context (do not rediscover):"];
|
|
201
|
+
if (ctx.mergeBase) lines.push(`- Merge-base SHA: \`${ctx.mergeBase}\``);
|
|
202
|
+
if (ctx.branchSha) lines.push(`- Branch tip SHA: \`${ctx.branchSha}\``);
|
|
203
|
+
if (ctx.baseSha) lines.push(`- Base tip SHA: \`${ctx.baseSha}\``);
|
|
204
|
+
if (ctx.diffRelPath) {
|
|
205
|
+
lines.push(
|
|
206
|
+
`- Diff file (relative to repo root): \`${ctx.diffRelPath}\` — read this file; do not re-run \`git diff\` unless it is missing.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const files = Array.isArray(ctx.files) ? ctx.files : [];
|
|
210
|
+
if (files.length) {
|
|
211
|
+
lines.push(`- Files in the diff (${files.length}):`);
|
|
212
|
+
const limit = 200;
|
|
213
|
+
for (const f of files.slice(0, limit)) {
|
|
214
|
+
const path = String(f?.path || "").trim();
|
|
215
|
+
if (!path) continue;
|
|
216
|
+
const kind = String(f?.changeType || "").trim();
|
|
217
|
+
lines.push(kind ? ` - ${kind}: ${path}` : ` - ${path}`);
|
|
218
|
+
}
|
|
219
|
+
if (files.length > limit) lines.push(` - … and ${files.length - limit} more`);
|
|
220
|
+
}
|
|
221
|
+
const ignored = Array.isArray(ctx.ignored) ? ctx.ignored.filter(Boolean) : [];
|
|
222
|
+
if (ignored.length) {
|
|
223
|
+
lines.push(
|
|
224
|
+
`- Ignored paths (excluded from the file list; if you mention them use "skipped" with reason "ignored"):`
|
|
225
|
+
);
|
|
226
|
+
for (const p of ignored.slice(0, 40)) lines.push(` - ${p}`);
|
|
227
|
+
if (ignored.length > 40) lines.push(` - … and ${ignored.length - 40} more`);
|
|
228
|
+
}
|
|
229
|
+
const globs = Array.isArray(ctx.ignoreGlobs) ? ctx.ignoreGlobs.filter(Boolean) : [];
|
|
230
|
+
if (globs.length) {
|
|
231
|
+
lines.push(`- Ignore globs (DEFAULT + config): ${globs.map((g) => `\`${g}\``).join(", ")}`);
|
|
232
|
+
}
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
132
236
|
/**
|
|
133
237
|
* @param {{ source?: string, repo?: string, branch?: string, base?: string, remoteUrl?: string }} meta
|
|
134
|
-
* @param {{
|
|
238
|
+
* @param {{
|
|
239
|
+
* outputFilename?: string,
|
|
240
|
+
* acp?: boolean,
|
|
241
|
+
* diffRelPath?: string | null,
|
|
242
|
+
* mergeBase?: string,
|
|
243
|
+
* branchSha?: string,
|
|
244
|
+
* baseSha?: string,
|
|
245
|
+
* files?: Array<{ path: string, changeType?: string }>,
|
|
246
|
+
* ignored?: string[],
|
|
247
|
+
* ignoreGlobs?: string[],
|
|
248
|
+
* }} [opts]
|
|
135
249
|
*/
|
|
136
250
|
export function buildPrompt(meta, opts = {}) {
|
|
137
251
|
const outputFilename = opts.outputFilename || makeOutputFilename();
|
|
252
|
+
const outputPath = outputRelPath(outputFilename);
|
|
138
253
|
const repo =
|
|
139
254
|
String(meta.repo || "").trim() ||
|
|
140
255
|
(meta.source === "url" ? "<git URL>" : "<absolute repo path>");
|
|
141
256
|
const branch = String(meta.branch || "").trim() || "<branch under review>";
|
|
142
257
|
const base = String(meta.base || "").trim() || "develop";
|
|
143
|
-
const
|
|
258
|
+
const acp = Boolean(opts.acp);
|
|
259
|
+
const hasDiff = Boolean(String(opts.diffRelPath || "").trim());
|
|
260
|
+
const workflow = acp
|
|
261
|
+
? meta.source === "url"
|
|
262
|
+
? workflowUrlAcp({ hasDiff })
|
|
263
|
+
: workflowLocalAcp({ hasDiff })
|
|
264
|
+
: meta.source === "url"
|
|
265
|
+
? workflowUrl(base, branch, { hasDiff })
|
|
266
|
+
: workflowLocal(base, branch, { hasDiff });
|
|
267
|
+
|
|
268
|
+
const precomputed = formatPrecomputedContext({
|
|
269
|
+
diffRelPath: opts.diffRelPath,
|
|
270
|
+
mergeBase: opts.mergeBase,
|
|
271
|
+
branchSha: opts.branchSha,
|
|
272
|
+
baseSha: opts.baseSha,
|
|
273
|
+
files: opts.files,
|
|
274
|
+
ignored: opts.ignored,
|
|
275
|
+
ignoreGlobs: opts.ignoreGlobs,
|
|
276
|
+
});
|
|
277
|
+
const hasPrecomputed =
|
|
278
|
+
hasDiff ||
|
|
279
|
+
opts.mergeBase ||
|
|
280
|
+
opts.branchSha ||
|
|
281
|
+
opts.baseSha ||
|
|
282
|
+
(opts.files && opts.files.length) ||
|
|
283
|
+
(opts.ignored && opts.ignored.length) ||
|
|
284
|
+
(opts.ignoreGlobs && opts.ignoreGlobs.length);
|
|
285
|
+
|
|
286
|
+
const validateSection = acp
|
|
287
|
+
? `When done writing \`${outputPath}\`, stop. The host validates the JSON automatically and will send you any problems — do not run \`npx … validate\` yourself.`
|
|
288
|
+
: `After writing the file, validate it (npx so it works even if \`diff-review\` is not on PATH):
|
|
289
|
+
|
|
290
|
+
\`${validateCliCommand(outputFilename)}\`
|
|
291
|
+
|
|
292
|
+
- Exit 0 and a line starting with \`OK\` means the JSON is acceptable. Then tell me that \`${outputPath}\` was written.
|
|
293
|
+
- Exit 1 prints \`INVALID\`, the problems, and how to continue. Fix the same file and re-run that exact command until it prints OK. Do not stop before OK.`;
|
|
144
294
|
|
|
145
295
|
return `You are a senior code reviewer: strict but fair.
|
|
146
296
|
|
|
@@ -149,25 +299,22 @@ Branch under review: ${branch}
|
|
|
149
299
|
Base (merge-base): ${base}
|
|
150
300
|
|
|
151
301
|
${workflow}
|
|
152
|
-
|
|
302
|
+
${hasPrecomputed ? `\n${precomputed}\n` : ""}
|
|
153
303
|
Rules:
|
|
154
304
|
${RULES}
|
|
155
305
|
|
|
156
|
-
Write the result to \`${
|
|
306
|
+
Write the result to \`${outputPath}\` (relative to the repo root; use forward slashes, never \`\\\`). The \`${OUTPUT_DIR}/\` folder is already created by the host — do not \`mkdir\` it.
|
|
307
|
+
- If that file already exists: **Read it first**, then Edit/overwrite (or Write after Read). Many agents reject Write with "File has not been read yet" unless you Read that exact path first.
|
|
308
|
+
- If it does not exist yet: Write is fine. Do NOT use shell redirection (\`>\`, \`>>\`, \`Out-File\`, \`tee\`, etc.) — those are rejected.
|
|
157
309
|
- One JSON object only: first character \`{\`, last character \`}\`.
|
|
158
310
|
- No prose before/after, no markdown fences.
|
|
159
|
-
- Shape (copy "meta" source/repo/branch/base/remoteUrl as given;
|
|
311
|
+
- Shape (copy "meta" source/repo/branch/base/remoteUrl as given; "agent" and "model" are required):
|
|
160
312
|
|
|
161
313
|
${outputSchemaBlock(meta)}
|
|
162
314
|
|
|
163
|
-
Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local).
|
|
164
|
-
|
|
165
|
-
Do NOT include diffs or a "files" array. The tool computes them with git when opening the report. Do not invent hunks or paste patches into the JSON.
|
|
315
|
+
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).
|
|
166
316
|
|
|
167
|
-
|
|
317
|
+
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.
|
|
168
318
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
- Exit 0 and a line starting with \`OK\` means the JSON is acceptable. Then tell me that \`${outputFilename}\` was written.
|
|
172
|
-
- Exit 1 prints \`INVALID\`, the problems, and how to continue. Fix the same file and re-run that exact command until it prints OK. Do not stop before OK.`;
|
|
319
|
+
${validateSection}`;
|
|
173
320
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carpeta `.diff-review/` en la raíz del repo revisado: el JSON del reporte, el diff con el que se hizo y la sesión
|
|
3
|
+
* del agente, todo con el mismo nombre base (`diff-rev_<branchTip>_<baseTip>`). Se ignora sola con su propio
|
|
4
|
+
* `.gitignore`, así no deja cambios en archivos trackeados del repo.
|
|
5
|
+
*
|
|
6
|
+
* Los JSON viejos en la raíz del repo se siguen encontrando (`locateReviewFile`).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { basename, join } from "node:path";
|
|
10
|
+
import { git } from "./git.mjs";
|
|
11
|
+
import { OUTPUT_DIR } from "./output-name.mjs";
|
|
12
|
+
|
|
13
|
+
export { OUTPUT_DIR };
|
|
14
|
+
|
|
15
|
+
/** @typedef {{ agentId: string, sessionId: string, model?: string, updatedAt?: string }} SessionRef */
|
|
16
|
+
|
|
17
|
+
export function reviewDir(repoRoot) {
|
|
18
|
+
return join(repoRoot, OUTPUT_DIR);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Crea `.diff-review/` (con su `.gitignore`) si no existe. */
|
|
22
|
+
export function ensureReviewDir(repoRoot) {
|
|
23
|
+
const dir = reviewDir(repoRoot);
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
const ignore = join(dir, ".gitignore");
|
|
26
|
+
if (!existsSync(ignore)) writeFileSync(ignore, "# Generado por diff-review: nada de esta carpeta va al repo.\n*\n");
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `diff-rev_a_b.json` → `{ json, diff, session }` dentro de `.diff-review/`. */
|
|
31
|
+
export function companionPaths(repoRoot, filename) {
|
|
32
|
+
const name = basename(String(filename || ""));
|
|
33
|
+
const stem = name.replace(/\.json$/i, "");
|
|
34
|
+
const dir = reviewDir(repoRoot);
|
|
35
|
+
return {
|
|
36
|
+
json: join(dir, name),
|
|
37
|
+
diff: join(dir, `${stem}.diff`),
|
|
38
|
+
session: join(dir, `${stem}.session.json`),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Dónde está el JSON de la review: `.diff-review/` primero y, si no, la raíz del repo (formato viejo). */
|
|
43
|
+
export function locateReviewFile(repoRoot, filename) {
|
|
44
|
+
const name = basename(String(filename || ""));
|
|
45
|
+
if (!name) return null;
|
|
46
|
+
for (const candidate of [companionPaths(repoRoot, name).json, join(repoRoot, name)]) {
|
|
47
|
+
try {
|
|
48
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
49
|
+
} catch {
|
|
50
|
+
/* siguiente */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Guarda `git diff base...branch` junto al JSON (una vez por par de commits: el nombre ya trae los tips).
|
|
58
|
+
* @returns {string | null} el path, o null si git falló
|
|
59
|
+
*/
|
|
60
|
+
export function writeDiffCache(repoRoot, filename, { branch, base }) {
|
|
61
|
+
const { diff } = companionPaths(repoRoot, filename);
|
|
62
|
+
if (existsSync(diff)) return diff;
|
|
63
|
+
try {
|
|
64
|
+
const text = git(repoRoot, ["diff", `${base}...${branch}`]);
|
|
65
|
+
ensureReviewDir(repoRoot);
|
|
66
|
+
writeFileSync(diff, text);
|
|
67
|
+
return diff;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Guarda un diff ya armado (p. ej. el que muestra la web), pisando el anterior. */
|
|
74
|
+
export function writeDiffText(repoRoot, filename, text) {
|
|
75
|
+
ensureReviewDir(repoRoot);
|
|
76
|
+
const { diff } = companionPaths(repoRoot, filename);
|
|
77
|
+
writeFileSync(diff, String(text ?? ""));
|
|
78
|
+
return diff;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @returns {string | null} texto del companion `.diff`, o null si no existe / no se pudo leer */
|
|
82
|
+
export function readDiffCache(repoRoot, filename) {
|
|
83
|
+
const { diff } = companionPaths(repoRoot, filename);
|
|
84
|
+
try {
|
|
85
|
+
if (!existsSync(diff)) return null;
|
|
86
|
+
return readFileSync(diff, "utf8");
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @returns {SessionRef | null} */
|
|
93
|
+
export function readSessionRef(repoRoot, filename) {
|
|
94
|
+
try {
|
|
95
|
+
const ref = JSON.parse(readFileSync(companionPaths(repoRoot, filename).session, "utf8"));
|
|
96
|
+
return ref && typeof ref.agentId === "string" && typeof ref.sessionId === "string" ? ref : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {SessionRef} ref */
|
|
103
|
+
export function writeSessionRef(repoRoot, filename, ref) {
|
|
104
|
+
ensureReviewDir(repoRoot);
|
|
105
|
+
const full = { ...ref, updatedAt: new Date().toISOString() };
|
|
106
|
+
writeFileSync(companionPaths(repoRoot, filename).session, `${JSON.stringify(full, null, 2)}\n`);
|
|
107
|
+
return full;
|
|
108
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estampa en el JSON quién generó la review. La CLI lo sabe mejor que el modelo.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { decodePayload } from "./json-payload.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `agentSession` = la sesión ACP que generó la review, para seguir conversando con el agente desde la web.
|
|
9
|
+
* @param {string} filePath
|
|
10
|
+
* @param {{ agent?: string, model?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
|
|
14
|
+
const agentName = String(agent || "").trim();
|
|
15
|
+
const modelId = String(model || "").trim();
|
|
16
|
+
if (!agentName && !modelId && !agentSession) return false;
|
|
17
|
+
|
|
18
|
+
let payload;
|
|
19
|
+
try {
|
|
20
|
+
payload = decodePayload(readFileSync(filePath, "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
25
|
+
|
|
26
|
+
const current = payload.meta;
|
|
27
|
+
const meta =
|
|
28
|
+
current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
|
|
29
|
+
if (agentName) meta.agent = agentName;
|
|
30
|
+
if (modelId) meta.model = modelId;
|
|
31
|
+
if (agentSession) meta.agentSession = { ...agentSession };
|
|
32
|
+
payload.meta = meta;
|
|
33
|
+
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { stampReviewProvenance } from "./review-meta.mjs";
|
|
7
|
+
|
|
8
|
+
test("stampReviewProvenance escribe agente y modelo sin perder el resto", () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "diff-review-meta-"));
|
|
10
|
+
const file = join(dir, "diff-rev_a_b.json");
|
|
11
|
+
writeFileSync(file, JSON.stringify({ intent: "algo concreto", meta: { branch: "feat" } }));
|
|
12
|
+
|
|
13
|
+
assert.equal(stampReviewProvenance(file, { agent: "Claude Code", model: "opus" }), true);
|
|
14
|
+
|
|
15
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
16
|
+
assert.equal(saved.intent, "algo concreto");
|
|
17
|
+
assert.deepEqual(saved.meta, { branch: "feat", agent: "Claude Code", model: "opus" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("stampReviewProvenance no pisa el modelo si no hay uno", () => {
|
|
21
|
+
const dir = mkdtempSync(join(tmpdir(), "diff-review-meta-"));
|
|
22
|
+
const file = join(dir, "diff-rev_a_b.json");
|
|
23
|
+
writeFileSync(file, JSON.stringify({ meta: { model: "sonnet" }, intent: "x" }));
|
|
24
|
+
|
|
25
|
+
stampReviewProvenance(file, { agent: "Codex" });
|
|
26
|
+
|
|
27
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
28
|
+
assert.equal(saved.meta.agent, "Codex");
|
|
29
|
+
assert.equal(saved.meta.model, "sonnet");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("stampReviewProvenance guarda la sesión del agente", () => {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), "diff-review-meta-"));
|
|
34
|
+
const file = join(dir, "diff-rev_a_b.json");
|
|
35
|
+
writeFileSync(file, JSON.stringify({ meta: { agent: "x" }, intent: "x" }));
|
|
36
|
+
|
|
37
|
+
const agentSession = { agentId: "claude", sessionId: "s-1", outputFilename: "diff-rev_a_b.json" };
|
|
38
|
+
stampReviewProvenance(file, { agent: "Claude Code", model: "opus", agentSession });
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(JSON.parse(readFileSync(file, "utf8")).meta.agentSession, agentSession);
|
|
41
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precomputa contexto git para el prompt de review (diff cache + lista de archivos).
|
|
3
|
+
*/
|
|
4
|
+
import { loadReviewConfig } from "./config.mjs";
|
|
5
|
+
import { extractBranchDiff } from "./extract.mjs";
|
|
6
|
+
import { outputRelPath } from "./output-name.mjs";
|
|
7
|
+
import { ensureReviewDir, readDiffCache, writeDiffCache } from "./review-dir.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cachea el three-dot diff y arma las opciones de `buildPrompt` (tips, files, ignores).
|
|
11
|
+
* @param {{
|
|
12
|
+
* repo: string,
|
|
13
|
+
* branch: string,
|
|
14
|
+
* base?: string,
|
|
15
|
+
* outputFilename: string,
|
|
16
|
+
* acp?: boolean,
|
|
17
|
+
* }} input
|
|
18
|
+
*/
|
|
19
|
+
export function prepareReviewPromptOpts(input) {
|
|
20
|
+
const repo = String(input.repo || "").trim();
|
|
21
|
+
const branch = String(input.branch || "").trim();
|
|
22
|
+
const base = String(input.base || "develop").trim() || "develop";
|
|
23
|
+
const outputFilename = String(input.outputFilename || "").trim();
|
|
24
|
+
if (!repo || !branch || !outputFilename) {
|
|
25
|
+
throw new Error("prepareReviewPromptOpts requiere repo, branch y outputFilename.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ensureReviewDir(repo);
|
|
29
|
+
writeDiffCache(repo, outputFilename, { branch, base });
|
|
30
|
+
const cached = readDiffCache(repo, outputFilename);
|
|
31
|
+
|
|
32
|
+
let extracted;
|
|
33
|
+
try {
|
|
34
|
+
extracted = extractBranchDiff({
|
|
35
|
+
repo,
|
|
36
|
+
branch,
|
|
37
|
+
base,
|
|
38
|
+
...(cached ? { cachedDiff: cached } : {}),
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
extracted = extractBranchDiff({ repo, branch, base });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = loadReviewConfig(repo);
|
|
45
|
+
const diffRelPath = cached
|
|
46
|
+
? outputRelPath(outputFilename).replace(/\.json$/i, ".diff")
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
outputFilename,
|
|
51
|
+
acp: Boolean(input.acp),
|
|
52
|
+
diffRelPath,
|
|
53
|
+
mergeBase: extracted.mergeBase || undefined,
|
|
54
|
+
branchSha: extracted.branchSha || undefined,
|
|
55
|
+
baseSha: extracted.baseSha || undefined,
|
|
56
|
+
files: (extracted.files || []).map((f) => ({
|
|
57
|
+
path: f.path,
|
|
58
|
+
changeType: f.changeType,
|
|
59
|
+
})),
|
|
60
|
+
ignored: extracted.context?.ignored || [],
|
|
61
|
+
ignoreGlobs: config.ignore,
|
|
62
|
+
};
|
|
63
|
+
}
|
package/lib/review.test.mjs
CHANGED
|
@@ -132,6 +132,7 @@ test("buildReview deduplica hallazgos repetidos y limpia referencias colgadas",
|
|
|
132
132
|
|
|
133
133
|
test("una respuesta JSON del modelo atraviesa decode, validación y normalización", () => {
|
|
134
134
|
const answer = JSON.stringify({
|
|
135
|
+
meta: { agent: "Claude Code", model: "claude-opus" },
|
|
135
136
|
intent: "Hace que las búsquedas con varias palabras matcheen los tokens en cualquier orden.",
|
|
136
137
|
groups: [
|
|
137
138
|
{ id: "g1", kind: "feat", title: "feat(search): matchear tokens", intent: "el cambio" },
|