@jpkme/diff-review 0.1.10 → 0.1.13
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 +151 -34
- package/lib/acp-policy.mjs +223 -0
- package/lib/acp-policy.test.mjs +136 -0
- package/lib/acp-shell.mjs +409 -0
- package/lib/acp-shell.test.mjs +158 -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 +87 -14
- package/lib/agents.test.mjs +4 -6
- package/lib/check-report.mjs +30 -11
- 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 +168 -22
- package/lib/review-dir.mjs +108 -0
- package/lib/review-meta.mjs +5 -3
- package/lib/review-meta.test.mjs +11 -0
- package/lib/review-prep.mjs +63 -0
- package/lib/validate.mjs +2 -2
- package/lib/wait-output.mjs +46 -23
- package/lib/wait-output.test.mjs +24 -1
- package/package.json +3 -1
- package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
- package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
- package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css +1 -0
- package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/BXUWMim4.js +213 -0
- package/web/build/client/_app/immutable/chunks/BXUWMim4.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BXUWMim4.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js +1 -0
- package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.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/{BBWAo_yW.js → DPfXzQnm.js} +2 -2
- package/web/build/client/_app/immutable/chunks/DPfXzQnm.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/DPfXzQnm.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.CTgiuiU1.js +2 -0
- package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CFSl0kht.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js +1 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js +1 -0
- package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.br +2 -0
- package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.AciugOtL.js +171 -0
- package/web/build/client/_app/immutable/nodes/3.AciugOtL.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.AciugOtL.js.gz +0 -0
- package/web/build/client/_app/version.json +1 -1
- package/web/build/client/_app/version.json.br +0 -0
- package/web/build/client/_app/version.json.gz +0 -0
- package/web/build/handler.js +3 -3
- package/web/build/index.js +3 -3
- package/web/build/server/chunks/0-DUStDOsr.js +11 -0
- package/web/build/server/chunks/{0-SseDy__O.js.map → 0-DUStDOsr.js.map} +1 -1
- package/web/build/server/chunks/1-C2qlInYk.js +7 -0
- package/web/build/server/chunks/1-C2qlInYk.js.map +1 -0
- package/web/build/server/chunks/2-DyE177MK.js +7 -0
- package/web/build/server/chunks/2-DyE177MK.js.map +1 -0
- package/web/build/server/chunks/3-SO3atuWO.js +7 -0
- package/web/build/server/chunks/3-SO3atuWO.js.map +1 -0
- package/web/build/server/chunks/_server.ts-CRNhzL2a.js +62 -0
- package/web/build/server/chunks/_server.ts-CRNhzL2a.js.map +1 -0
- package/web/build/server/chunks/agent-sessions-kcUaLhK5.js +2819 -0
- package/web/build/server/chunks/agent-sessions-kcUaLhK5.js.map +1 -0
- package/web/build/server/chunks/{remote-1g4ayag-B67QMR_2.js → git-review-CXKcjYgZ.js} +70 -704
- package/web/build/server/chunks/git-review-CXKcjYgZ.js.map +1 -0
- package/web/build/server/chunks/{handler-B5P65tLk.js → handler-_E0NucKN.js} +3 -3
- package/web/build/server/chunks/{handler-B5P65tLk.js.map → handler-_E0NucKN.js.map} +1 -1
- package/web/build/server/chunks/hooks.server-D7yPODoc.js +42 -0
- package/web/build/server/chunks/hooks.server-D7yPODoc.js.map +1 -0
- package/web/build/server/chunks/{index.js-CL95e0Gl.js → index.js-lMtDM8ZQ.js} +3 -5
- package/web/build/server/chunks/index.js-lMtDM8ZQ.js.map +1 -0
- package/web/build/server/chunks/manifest.js-BIm6jot5.js +58 -0
- package/web/build/server/chunks/manifest.js-BIm6jot5.js.map +1 -0
- package/web/build/server/chunks/remote-1g4ayag-DmqxwUIz.js +144 -0
- package/web/build/server/chunks/remote-1g4ayag-DmqxwUIz.js.map +1 -0
- package/web/build/server/chunks/remote-wh6t6j-DZk0W9Pp.js +162 -0
- package/web/build/server/chunks/remote-wh6t6j-DZk0W9Pp.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/0.CLQiq3Ed.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css +0 -1
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css +0 -1
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.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/BBWAo_yW.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/BBWAo_yW.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/QdZF3nkf.js +0 -176
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/QdZF3nkf.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.Dce6uJU3.js +0 -2
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js +0 -1
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.DnBSC38l.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.5Iv4ssDT.js +0 -1
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js +0 -66
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js +0 -170
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.gz +0 -0
- package/web/build/server/chunks/0-SseDy__O.js +0 -11
- package/web/build/server/chunks/1-2OO2bb7H.js +0 -7
- package/web/build/server/chunks/1-2OO2bb7H.js.map +0 -1
- package/web/build/server/chunks/2-BJvuqKN6.js +0 -7
- package/web/build/server/chunks/2-BJvuqKN6.js.map +0 -1
- package/web/build/server/chunks/3-ZIl1rHef.js +0 -7
- package/web/build/server/chunks/3-ZIl1rHef.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-CL95e0Gl.js.map +0 -1
- package/web/build/server/chunks/manifest.js-uD_a0079.js +0 -50
- package/web/build/server/chunks/manifest.js-uD_a0079.js.map +0 -1
- package/web/build/server/chunks/remote-1g4ayag-B67QMR_2.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 = `{
|
|
@@ -28,7 +32,7 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
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
37
|
"title": "string, aim for ≤72 chars, imperative, no kind prefix",
|
|
34
38
|
"intent": "string"
|
|
@@ -37,14 +41,14 @@ export const OUTPUT_SCHEMA_BLOCK = `{
|
|
|
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, aim for ≤120 chars",
|
|
51
|
+
"what": "string, aim for ≤120 chars: concrete change + effect",
|
|
48
52
|
"why": "string, aim for ≤120 chars",
|
|
49
53
|
"source": "code|commit|pr|issue|inferred"
|
|
50
54
|
}
|
|
@@ -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, aim for ≤120 chars",
|
|
63
|
-
"fix": "string, aim for ≤120 chars"
|
|
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,17 +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".
|
|
86
|
+
- When several read-only lookups are independent (files, searches), batch them in the same turn if your tools allow parallel calls.
|
|
79
87
|
- Write human-readable strings (intent, title, what, why, fix, notes) in Spanish. Keep enums/ids/paths/JSON keys exactly as in the schema.
|
|
80
|
-
- Length is a target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Longer text is still valid. Do not rewrite the file only to shorten it.`;
|
|
88
|
+
- Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Prefer clarity over shortening. Longer text is still valid. Do not rewrite the file only to shorten it.`;
|
|
81
89
|
|
|
82
|
-
|
|
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
|
+
}
|
|
83
99
|
return `Workflow:
|
|
84
100
|
1. Resolve the merge-base of \`${base}\` and \`${branch}\`.
|
|
85
101
|
2. Run exactly: \`git diff ${base}...${branch}\` (three-dot / merge-base diff). Do not use two-dot unless three-dot is impossible.
|
|
@@ -88,13 +104,22 @@ function workflowLocal(base, branch) {
|
|
|
88
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.`;
|
|
89
105
|
}
|
|
90
106
|
|
|
91
|
-
/** 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. */
|
|
92
108
|
export function validateCliCommand(filename) {
|
|
93
|
-
const file = String(filename || "diff-rev_<branchTip>_<baseTip>.json").trim();
|
|
109
|
+
const file = outputRelPath(String(filename || "diff-rev_<branchTip>_<baseTip>.json").trim());
|
|
94
110
|
return `npx --yes @jpkme/diff-review validate ${file}`;
|
|
95
111
|
}
|
|
96
112
|
|
|
97
|
-
|
|
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
|
+
}
|
|
98
123
|
return `Workflow:
|
|
99
124
|
1. If the repo is not local, clone it (or use whatever access you have). Do not modify it.
|
|
100
125
|
2. Resolve the merge-base of \`${base}\` and \`${branch}\`.
|
|
@@ -104,6 +129,35 @@ function workflowUrl(base, branch) {
|
|
|
104
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.`;
|
|
105
130
|
}
|
|
106
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
|
+
|
|
107
161
|
/**
|
|
108
162
|
* @param {{ source?: string, repo?: string, branch?: string, base?: string, remoteUrl?: string }} meta
|
|
109
163
|
*/
|
|
@@ -130,18 +184,113 @@ export function outputSchemaBlock(meta) {
|
|
|
130
184
|
return OUTPUT_SCHEMA_BLOCK.replace(/"meta": \{[\s\S]*?\n \}/, metaBlock);
|
|
131
185
|
}
|
|
132
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
|
+
|
|
133
236
|
/**
|
|
134
237
|
* @param {{ source?: string, repo?: string, branch?: string, base?: string, remoteUrl?: string }} meta
|
|
135
|
-
* @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]
|
|
136
249
|
*/
|
|
137
250
|
export function buildPrompt(meta, opts = {}) {
|
|
138
251
|
const outputFilename = opts.outputFilename || makeOutputFilename();
|
|
252
|
+
const outputPath = outputRelPath(outputFilename);
|
|
139
253
|
const repo =
|
|
140
254
|
String(meta.repo || "").trim() ||
|
|
141
255
|
(meta.source === "url" ? "<git URL>" : "<absolute repo path>");
|
|
142
256
|
const branch = String(meta.branch || "").trim() || "<branch under review>";
|
|
143
257
|
const base = String(meta.base || "").trim() || "develop";
|
|
144
|
-
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.`;
|
|
145
294
|
|
|
146
295
|
return `You are a senior code reviewer: strict but fair.
|
|
147
296
|
|
|
@@ -150,11 +299,13 @@ Branch under review: ${branch}
|
|
|
150
299
|
Base (merge-base): ${base}
|
|
151
300
|
|
|
152
301
|
${workflow}
|
|
153
|
-
|
|
302
|
+
${hasPrecomputed ? `\n${precomputed}\n` : ""}
|
|
154
303
|
Rules:
|
|
155
304
|
${RULES}
|
|
156
305
|
|
|
157
|
-
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.
|
|
158
309
|
- One JSON object only: first character \`{\`, last character \`}\`.
|
|
159
310
|
- No prose before/after, no markdown fences.
|
|
160
311
|
- Shape (copy "meta" source/repo/branch/base/remoteUrl as given; "agent" and "model" are required):
|
|
@@ -163,12 +314,7 @@ ${outputSchemaBlock(meta)}
|
|
|
163
314
|
|
|
164
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).
|
|
165
316
|
|
|
166
|
-
Do NOT include diffs or a "files" array. The tool computes
|
|
167
|
-
|
|
168
|
-
After writing the file, validate it (npx so it works even if \`diff-review\` is not on PATH):
|
|
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.
|
|
169
318
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
- Exit 0 and a line starting with \`OK\` means the JSON is acceptable. Then tell me that \`${outputFilename}\` was written.
|
|
173
|
-
- 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}`;
|
|
174
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
|
+
}
|
package/lib/review-meta.mjs
CHANGED
|
@@ -5,14 +5,15 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { decodePayload } from "./json-payload.mjs";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
+
* `agentSession` = la sesión ACP que generó la review, para seguir conversando con el agente desde la web.
|
|
8
9
|
* @param {string} filePath
|
|
9
|
-
* @param {{ agent?: string, model?: string }} provenance
|
|
10
|
+
* @param {{ agent?: string, model?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
|
|
10
11
|
* @returns {boolean}
|
|
11
12
|
*/
|
|
12
|
-
export function stampReviewProvenance(filePath, { agent, model } = {}) {
|
|
13
|
+
export function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
|
|
13
14
|
const agentName = String(agent || "").trim();
|
|
14
15
|
const modelId = String(model || "").trim();
|
|
15
|
-
if (!agentName && !modelId) return false;
|
|
16
|
+
if (!agentName && !modelId && !agentSession) return false;
|
|
16
17
|
|
|
17
18
|
let payload;
|
|
18
19
|
try {
|
|
@@ -27,6 +28,7 @@ export function stampReviewProvenance(filePath, { agent, model } = {}) {
|
|
|
27
28
|
current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
|
|
28
29
|
if (agentName) meta.agent = agentName;
|
|
29
30
|
if (modelId) meta.model = modelId;
|
|
31
|
+
if (agentSession) meta.agentSession = { ...agentSession };
|
|
30
32
|
payload.meta = meta;
|
|
31
33
|
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
32
34
|
return true;
|
package/lib/review-meta.test.mjs
CHANGED
|
@@ -28,3 +28,14 @@ test("stampReviewProvenance no pisa el modelo si no hay uno", () => {
|
|
|
28
28
|
assert.equal(saved.meta.agent, "Codex");
|
|
29
29
|
assert.equal(saved.meta.model, "sonnet");
|
|
30
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/validate.mjs
CHANGED
|
@@ -218,9 +218,9 @@ TEXT LANGUAGE: Spanish (rioplatense, technical) for intent, groups.intent, block
|
|
|
218
218
|
|
|
219
219
|
{
|
|
220
220
|
"intent": "2-4 sentences on what the branch solves",
|
|
221
|
-
"groups": [{ "id": "
|
|
221
|
+
"groups": [{ "id": "t1", "kind": "feat", "title": "scope: summary (do not repeat kind)", "intent": "why this theme exists" }],
|
|
222
222
|
"meta": { "agent": "<required: product name, e.g. Claude Code>", "model": "<required: exact model id>" },
|
|
223
|
-
"blocks": [{ "id": "b1", "group": "
|
|
223
|
+
"blocks": [{ "id": "b1", "group": "t1", "file": "exact/path.ts", "lines": "L40-58", "op": "mod", "what": "what changed", "why": "why", "source": "code" }],
|
|
224
224
|
"findings": [{ "id": "f1", "class": "risk", "severity": "high", "blocking": true, "kind": "bug", "file": "path.ts", "line": 42, "block": "b1", "what": "what is wrong", "fix": "what to do" }],
|
|
225
225
|
"skipped": [{ "file": "bun.lock", "reason": "lockfile" }]
|
|
226
226
|
}
|
package/lib/wait-output.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Espera a que aparezca un JSON de review válido en el repo.
|
|
2
|
+
* Espera a que aparezca un JSON de review válido en el repo: en `.diff-review/` o, como antes, en la raíz.
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
import { decodePayload } from "./json-payload.mjs";
|
|
7
|
+
import { companionPaths, ensureReviewDir, locateReviewFile } from "./review-dir.mjs";
|
|
7
8
|
import { REVIEW_SCHEMA, validateAgainst } from "./validate.mjs";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -38,7 +39,8 @@ function readReview(target, since) {
|
|
|
38
39
|
* @returns {{ path: string, payload: unknown } | null}
|
|
39
40
|
*/
|
|
40
41
|
export function existingReviewOutput(repoRoot, filename) {
|
|
41
|
-
const target =
|
|
42
|
+
const target = locateReviewFile(repoRoot, filename);
|
|
43
|
+
if (!target) return null;
|
|
42
44
|
const read = readReview(target);
|
|
43
45
|
return read && !read.errors.length ? { path: target, payload: read.payload } : null;
|
|
44
46
|
}
|
|
@@ -51,13 +53,28 @@ export function existingReviewOutput(repoRoot, filename) {
|
|
|
51
53
|
* @returns {Promise<{ path: string, payload: unknown }>}
|
|
52
54
|
*/
|
|
53
55
|
export function waitForReviewOutput(repoRoot, filename, opts = {}) {
|
|
54
|
-
const target = join(repoRoot, filename);
|
|
55
56
|
const name = basename(filename);
|
|
57
|
+
// Primero `.diff-review/`; también la raíz del repo por compatibilidad con salidas viejas.
|
|
58
|
+
const targets = [companionPaths(repoRoot, name).json, join(repoRoot, name)];
|
|
56
59
|
const pollMs = opts.pollMs ?? 800;
|
|
57
60
|
let lastInvalidKey = "";
|
|
61
|
+
let reviewDir = "";
|
|
62
|
+
try {
|
|
63
|
+
reviewDir = ensureReviewDir(repoRoot);
|
|
64
|
+
} catch {
|
|
65
|
+
/* repo de solo lectura: se espera igual en la raíz */
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
const tryRead = () => {
|
|
60
|
-
|
|
69
|
+
let target = "";
|
|
70
|
+
let read = null;
|
|
71
|
+
for (const candidate of targets) {
|
|
72
|
+
read = readReview(candidate, opts.since);
|
|
73
|
+
if (read) {
|
|
74
|
+
target = candidate;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
61
78
|
if (!read) return null;
|
|
62
79
|
const { payload, errors } = read;
|
|
63
80
|
if (errors.length) {
|
|
@@ -73,8 +90,8 @@ export function waitForReviewOutput(repoRoot, filename, opts = {}) {
|
|
|
73
90
|
|
|
74
91
|
return new Promise((resolve, reject) => {
|
|
75
92
|
let settled = false;
|
|
76
|
-
/** @type {import('node:fs').FSWatcher
|
|
77
|
-
|
|
93
|
+
/** @type {import('node:fs').FSWatcher[]} */
|
|
94
|
+
const watchers = [];
|
|
78
95
|
/** @type {ReturnType<typeof setInterval> | null} */
|
|
79
96
|
let timer = null;
|
|
80
97
|
|
|
@@ -82,10 +99,12 @@ export function waitForReviewOutput(repoRoot, filename, opts = {}) {
|
|
|
82
99
|
if (settled) return;
|
|
83
100
|
settled = true;
|
|
84
101
|
if (timer) clearInterval(timer);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
for (const w of watchers) {
|
|
103
|
+
try {
|
|
104
|
+
w.close();
|
|
105
|
+
} catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
89
108
|
}
|
|
90
109
|
resolve(result);
|
|
91
110
|
};
|
|
@@ -94,10 +113,12 @@ export function waitForReviewOutput(repoRoot, filename, opts = {}) {
|
|
|
94
113
|
if (settled) return;
|
|
95
114
|
settled = true;
|
|
96
115
|
if (timer) clearInterval(timer);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
for (const w of watchers) {
|
|
117
|
+
try {
|
|
118
|
+
w.close();
|
|
119
|
+
} catch {
|
|
120
|
+
/* ignore */
|
|
121
|
+
}
|
|
101
122
|
}
|
|
102
123
|
reject(err);
|
|
103
124
|
};
|
|
@@ -114,15 +135,17 @@ export function waitForReviewOutput(repoRoot, filename, opts = {}) {
|
|
|
114
135
|
tick();
|
|
115
136
|
if (settled) return;
|
|
116
137
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
// Los watchers solo adelantan el aviso; el poll de abajo alcanza si fallan.
|
|
139
|
+
for (const dir of [reviewDir, repoRoot].filter(Boolean)) {
|
|
140
|
+
try {
|
|
141
|
+
const w = watch(dir, { persistent: true }, (_event, changed) => {
|
|
142
|
+
if (!changed || String(changed) === name || String(changed).endsWith(name)) tick();
|
|
143
|
+
});
|
|
144
|
+
w.on("error", () => {});
|
|
145
|
+
watchers.push(w);
|
|
146
|
+
} catch {
|
|
147
|
+
/* solo poll */
|
|
148
|
+
}
|
|
126
149
|
}
|
|
127
150
|
|
|
128
151
|
timer = setInterval(tick, pollMs);
|