@jpkme/diff-review 0.1.4 → 0.1.6

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 (73) hide show
  1. package/README.md +4 -4
  2. package/bin/diff-review.mjs +240 -90
  3. package/lib/autocomplete-es.mjs +143 -0
  4. package/lib/check-report.mjs +213 -0
  5. package/lib/check-report.test.mjs +46 -0
  6. package/lib/output-name.mjs +18 -30
  7. package/lib/output-name.test.mjs +10 -27
  8. package/lib/prompt.mjs +29 -10
  9. package/package.json +3 -2
  10. package/schema.json +12 -0
  11. package/web/build/client/_app/immutable/chunks/{CAnYe5db.js → Ber_0nz7.js} +1 -1
  12. package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.br +2 -0
  13. package/web/build/client/_app/immutable/chunks/Ber_0nz7.js.gz +0 -0
  14. package/web/build/client/_app/immutable/chunks/{BaYDuTXu.js → DRga3k74.js} +28 -17
  15. package/web/build/client/_app/immutable/chunks/DRga3k74.js.br +0 -0
  16. package/web/build/client/_app/immutable/chunks/DRga3k74.js.gz +0 -0
  17. package/web/build/client/_app/immutable/chunks/{Gya4IU1I.js → zvcaSoGP.js} +2 -2
  18. package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.br +0 -0
  19. package/web/build/client/_app/immutable/chunks/zvcaSoGP.js.gz +0 -0
  20. package/web/build/client/_app/immutable/entry/{app.e96uPVtl.js → app.BP_R_kTF.js} +2 -2
  21. package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.br +0 -0
  22. package/web/build/client/_app/immutable/entry/app.BP_R_kTF.js.gz +0 -0
  23. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js +1 -0
  24. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.br +1 -0
  25. package/web/build/client/_app/immutable/entry/start.lXpYdwbt.js.gz +0 -0
  26. package/web/build/client/_app/immutable/nodes/{1.V-g5-sfL.js → 1.CGJk77oo.js} +1 -1
  27. package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.br +3 -0
  28. package/web/build/client/_app/immutable/nodes/1.CGJk77oo.js.gz +0 -0
  29. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js +66 -0
  30. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.br +0 -0
  31. package/web/build/client/_app/immutable/nodes/2.Dl2xIvoV.js.gz +0 -0
  32. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js +170 -0
  33. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.br +0 -0
  34. package/web/build/client/_app/immutable/nodes/3.Bn5l-P1e.js.gz +0 -0
  35. package/web/build/client/_app/version.json +1 -1
  36. package/web/build/client/_app/version.json.br +0 -0
  37. package/web/build/client/_app/version.json.gz +0 -0
  38. package/web/build/handler.js +3 -3
  39. package/web/build/index.js +3 -3
  40. package/web/build/server/chunks/1-CRg9pswO.js +7 -0
  41. package/web/build/server/chunks/{1-DCKScL3A.js.map → 1-CRg9pswO.js.map} +1 -1
  42. package/web/build/server/chunks/{2-BpBQQ02B.js → 2-BmYzresl.js} +2 -2
  43. package/web/build/server/chunks/{2-BpBQQ02B.js.map → 2-BmYzresl.js.map} +1 -1
  44. package/web/build/server/chunks/{3-rGkcNCVz.js → 3-DaeIJXI7.js} +2 -2
  45. package/web/build/server/chunks/{3-rGkcNCVz.js.map → 3-DaeIJXI7.js.map} +1 -1
  46. package/web/build/server/chunks/{handler-BaT-QavL.js → handler-2eew4UOl.js} +3 -3
  47. package/web/build/server/chunks/{handler-BaT-QavL.js.map → handler-2eew4UOl.js.map} +1 -1
  48. package/web/build/server/chunks/{index.js-C-K0YDBJ.js → index.js-DbiNsix3.js} +2 -2
  49. package/web/build/server/chunks/{index.js-C-K0YDBJ.js.map → index.js-DbiNsix3.js.map} +1 -1
  50. package/web/build/server/chunks/{manifest.js-CDz-Fem4.js → manifest.js-BYNXhn6E.js} +6 -6
  51. package/web/build/server/chunks/{manifest.js-CDz-Fem4.js.map → manifest.js-BYNXhn6E.js.map} +1 -1
  52. package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js → remote-1g4ayag-DESnyPRn.js} +27 -40
  53. package/web/build/server/chunks/{remote-1g4ayag-DXdFe1BG.js.map → remote-1g4ayag-DESnyPRn.js.map} +1 -1
  54. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.br +0 -0
  55. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.gz +0 -0
  56. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.br +0 -1
  57. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.gz +0 -0
  58. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.br +0 -0
  59. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.gz +0 -0
  60. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.br +0 -0
  61. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.gz +0 -0
  62. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js +0 -1
  63. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.br +0 -0
  64. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.gz +0 -0
  65. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.br +0 -0
  66. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.gz +0 -0
  67. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js +0 -66
  68. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.br +0 -0
  69. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.gz +0 -0
  70. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js +0 -170
  71. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.br +0 -0
  72. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.gz +0 -0
  73. package/web/build/server/chunks/1-DCKScL3A.js +0 -7
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Valida un JSON de review y arma un texto accionable para el agente.
3
+ */
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ import { globMatch } from "./config.mjs";
7
+ import { extractBranchDiff } from "./extract.mjs";
8
+ import { tryResolveGitRoot } from "./git.mjs";
9
+ import { decodePayload } from "./json-payload.mjs";
10
+ import { REVIEW_SCHEMA, reviewIssues, validateAgainst } from "./validate.mjs";
11
+
12
+ const MISSING_LIMIT = 40;
13
+
14
+ /**
15
+ * @param {unknown} payload
16
+ * @param {string[]} [paths] rutas del diff (sin ignorados)
17
+ * @returns {{ ok: boolean, errors: string[], notes: string[] }}
18
+ */
19
+ export function checkPayload(payload, paths = []) {
20
+ const errors = [...validateAgainst(REVIEW_SCHEMA, payload), ...reviewIssues(payload)];
21
+ const notes = [];
22
+ const known = (paths || []).map((p) => String(p || "").replaceAll("\\", "/")).filter(Boolean);
23
+
24
+ if (known.length) {
25
+ const blocks = Array.isArray(payload?.blocks) ? payload.blocks : [];
26
+ const findings = Array.isArray(payload?.findings) ? payload.findings : [];
27
+ const skipped = Array.isArray(payload?.skipped) ? payload.skipped : [];
28
+ const skippedPatterns = skipped.map((s) => String(s?.file || "")).filter(Boolean);
29
+
30
+ const covers = (file) => {
31
+ const norm = String(file || "").replaceAll("\\", "/");
32
+ if (blocks.some((b) => String(b?.file || "").replaceAll("\\", "/") === norm)) return true;
33
+ return skippedPatterns.some((pattern) => {
34
+ const p = pattern.replaceAll("\\", "/");
35
+ return p === norm || globMatch(norm, p);
36
+ });
37
+ };
38
+
39
+ for (const block of blocks) {
40
+ const file = String(block?.file || "").replaceAll("\\", "/");
41
+ if (file && !known.includes(file)) {
42
+ errors.push(`el bloque ${block.id || file} apunta a un archivo que no está en el diff: ${file}`);
43
+ }
44
+ }
45
+ for (const finding of findings) {
46
+ const file = String(finding?.file || "").replaceAll("\\", "/");
47
+ if (file && !known.includes(file)) {
48
+ errors.push(
49
+ `el hallazgo ${finding.id || finding.what || file} apunta a un archivo fuera del diff: ${file}`
50
+ );
51
+ }
52
+ }
53
+
54
+ const missing = known.filter((file) => !covers(file));
55
+ if (missing.length) {
56
+ const shown = missing.slice(0, MISSING_LIMIT);
57
+ const rest = missing.length - shown.length;
58
+ errors.push(
59
+ `quedaron ${missing.length} archivo(s) del diff sin bloque ni skipped: ${shown.join(", ")}${
60
+ rest > 0 ? ` … y ${rest} más` : ""
61
+ }`
62
+ );
63
+ }
64
+ } else {
65
+ notes.push("No contrasté cobertura contra git (sin lista de paths del diff).");
66
+ }
67
+
68
+ return { ok: errors.length === 0, errors, notes };
69
+ }
70
+
71
+ /**
72
+ * @param {string} filePath
73
+ * @param {{ repo?: string, branch?: string, base?: string, cwd?: string }} [opts]
74
+ */
75
+ export function checkReportFile(filePath, opts = {}) {
76
+ const cwd = opts.cwd || process.cwd();
77
+ const abs = isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
78
+ if (!existsSync(abs)) {
79
+ return {
80
+ ok: false,
81
+ file: abs,
82
+ errors: [`No existe el archivo: ${abs}`],
83
+ notes: [],
84
+ usage: true,
85
+ };
86
+ }
87
+
88
+ let payload;
89
+ try {
90
+ payload = decodePayload(readFileSync(abs, "utf8"));
91
+ } catch (err) {
92
+ return {
93
+ ok: false,
94
+ file: abs,
95
+ errors: [err instanceof Error ? err.message : String(err)],
96
+ notes: [],
97
+ };
98
+ }
99
+
100
+ const meta = payload && typeof payload === "object" ? payload.meta || {} : {};
101
+ const branch = String(opts.branch || meta.branch || "").trim();
102
+ const base = String(opts.base || meta.base || "develop").trim();
103
+ const repoHint = String(opts.repo || meta.repo || "").trim();
104
+ const repo =
105
+ (repoHint && !/^(https?:\/\/|git@|ssh:\/\/)/i.test(repoHint) && tryResolveGitRoot(repoHint)) ||
106
+ tryResolveGitRoot(cwd);
107
+
108
+ /** @type {string[]} */
109
+ let paths = [];
110
+ /** @type {string[]} */
111
+ const errors = [];
112
+
113
+ if (!branch) {
114
+ errors.push("Falta meta.branch (o --branch) para contrastar el diff.");
115
+ } else if (!repo) {
116
+ errors.push(
117
+ "No encontré un repo git local (meta.repo o el directorio actual). Sin eso no puedo verificar cobertura."
118
+ );
119
+ } else {
120
+ try {
121
+ const extracted = extractBranchDiff({ repo, branch, base });
122
+ paths = (extracted.files || []).map((f) => f.path);
123
+ } catch (err) {
124
+ errors.push(
125
+ `No pude listar el diff con git (${err instanceof Error ? err.message : String(err)}). Revisá meta.repo, meta.branch y meta.base.`
126
+ );
127
+ }
128
+ }
129
+
130
+ const result = checkPayload(payload, paths);
131
+ const allErrors = [...result.errors, ...errors];
132
+ return { ok: allErrors.length === 0, errors: allErrors, notes: result.notes, file: abs };
133
+ }
134
+
135
+ /**
136
+ * @param {string} file
137
+ * @param {string} command
138
+ * @param {{ ok: boolean, errors: string[], notes: string[] }} result
139
+ */
140
+ export function formatCheckReport(file, command, result) {
141
+ if (result.ok) {
142
+ const extra = result.notes?.length ? `\n${result.notes.map((n) => `Note: ${n}`).join("\n")}` : "";
143
+ return `OK: ${file}${extra}\nSchema and references passed. You can stop.`;
144
+ }
145
+
146
+ const problems = (result.errors || []).map((e) => `- ${e}`).join("\n");
147
+ const notes = (result.notes || []).map((n) => `Note: ${n}`).join("\n");
148
+ const lines = [
149
+ `INVALID: ${file}`,
150
+ "",
151
+ "The JSON is not ready. Edit this same file until the command prints OK. Do not create a new filename.",
152
+ "",
153
+ "Problems:",
154
+ problems || "- (unknown)",
155
+ ];
156
+ if (notes) lines.push("", notes);
157
+ lines.push(
158
+ "",
159
+ "How to continue:",
160
+ "- Fix every problem above in place. Keep existing meta/intent/groups/blocks/findings; only change what is wrong or missing.",
161
+ '- Every diff path must appear in "blocks" or "skipped" (a skipped glob counts).',
162
+ "- what/why/fix stay ≤120 chars; intent ≥40 chars; enums exactly as in the schema.",
163
+ "- Human-readable strings in Spanish.",
164
+ `- Re-run exactly: \`${command}\``,
165
+ "- Stop only when that command prints OK."
166
+ );
167
+ return lines.join("\n");
168
+ }
169
+
170
+ /**
171
+ * @param {string[]} argv args después de `validate`
172
+ * @param {{ stdout?: (s: string) => void, stderr?: (s: string) => void, cwd?: string }} [io]
173
+ * @returns {number} exit code
174
+ */
175
+ export function runValidateCli(argv, io = {}) {
176
+ const stdout = io.stdout || ((s) => console.log(s));
177
+ const stderr = io.stderr || ((s) => console.error(s));
178
+ const fileFlag = [];
179
+ /** @type {{ repo?: string, branch?: string, base?: string }} */
180
+ const opts = { cwd: io.cwd };
181
+
182
+ for (let i = 0; i < argv.length; i++) {
183
+ const a = argv[i];
184
+ if (a === "-h" || a === "--help") {
185
+ stdout(`Usage: npx --yes @jpkme/diff-review validate <file.json> [--repo <path>] [--branch <b>] [--base <b>]
186
+
187
+ Exit 0 and "OK" when the JSON matches the review contract (and diff coverage, when git can list paths).
188
+ Exit 1 with problems and how to continue otherwise.`);
189
+ return 0;
190
+ }
191
+ if (a === "--repo") opts.repo = argv[++i];
192
+ else if (a === "--branch") opts.branch = argv[++i];
193
+ else if (a === "--base") opts.base = argv[++i];
194
+ else if (a.startsWith("-")) {
195
+ stderr(`Opción desconocida: ${a}`);
196
+ return 2;
197
+ } else fileFlag.push(a);
198
+ }
199
+
200
+ const file = fileFlag[0];
201
+ if (!file) {
202
+ stderr("Falta el archivo JSON. Uso: diff-review validate <file.json>");
203
+ return 2;
204
+ }
205
+
206
+ const command = `npx --yes @jpkme/diff-review validate ${file}`;
207
+ const result = checkReportFile(file, opts);
208
+ const text = formatCheckReport(result.file || file, command, result);
209
+ if (result.ok) stdout(text);
210
+ else stderr(text);
211
+ if (result.usage) return 2;
212
+ return result.ok ? 0 : 1;
213
+ }
@@ -0,0 +1,46 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { checkPayload, formatCheckReport } from "./check-report.mjs";
4
+
5
+ const valid = () => ({
6
+ intent: "Agrega el ABM de depósitos locales end-to-end para dejar de administrarlos por SQL.",
7
+ groups: [{ id: "g1", kind: "feat", title: "CRUD de depósitos", intent: "base de todo" }],
8
+ blocks: [
9
+ {
10
+ id: "b1",
11
+ group: "g1",
12
+ file: "src/a.ts",
13
+ lines: "L1-20",
14
+ op: "add",
15
+ what: "controller nuevo",
16
+ why: "",
17
+ source: "code",
18
+ },
19
+ ],
20
+ findings: [],
21
+ skipped: [{ file: "**/*.lock", reason: "lockfile" }],
22
+ });
23
+
24
+ test("checkPayload acepta cobertura exacta y por glob", () => {
25
+ const result = checkPayload(valid(), ["src/a.ts", "bun.lock"]);
26
+ assert.equal(result.ok, true);
27
+ assert.deepEqual(result.errors, []);
28
+ });
29
+
30
+ test("checkPayload lista archivos del diff sin cubrir", () => {
31
+ const result = checkPayload(valid(), ["src/a.ts", "src/olvidado.ts"]);
32
+ assert.equal(result.ok, false);
33
+ assert.ok(result.errors.some((e) => e.includes("src/olvidado.ts")));
34
+ });
35
+
36
+ test("formatCheckReport explica cómo seguir", () => {
37
+ const text = formatCheckReport("diff-rev_a_b.json", "npx --yes @jpkme/diff-review validate diff-rev_a_b.json", {
38
+ ok: false,
39
+ errors: ["$.intent: texto demasiado corto"],
40
+ notes: [],
41
+ });
42
+ assert.match(text, /INVALID/);
43
+ assert.match(text, /How to continue/);
44
+ assert.match(text, /npx --yes @jpkme\/diff-review validate/);
45
+ assert.match(text, /same file/);
46
+ });
@@ -1,54 +1,42 @@
1
1
  /**
2
2
  * Nombre del JSON que escribe el agente.
3
- * Preferido: tips cortos + stamp. Legacy sin tips sigue siendo válido al importar.
3
+ * Preferido: tips cortos branch/base. Legacy (con stamp o sin tips) sigue válido al importar.
4
4
  */
5
5
  import { fingerprint } from "./git.mjs";
6
6
 
7
- /** Prefijo del formato actual (con tips). */
8
- export const OUTPUT_PREFIX = "diff-report";
7
+ /** Prefijo del formato actual. */
8
+ export const OUTPUT_PREFIX = "diff-rev";
9
9
 
10
- /** Prefijo legacy (sin tips). */
10
+ /** Prefijo legacy (sin tips / con stamp). */
11
11
  export const OUTPUT_PREFIX_LEGACY = "diff-review-output";
12
+ export const OUTPUT_PREFIX_REPORT = "diff-report";
12
13
 
13
14
  /** Nombre legacy fijo. */
14
15
  export const OUTPUT_FILENAME_LEGACY = `${OUTPUT_PREFIX_LEGACY}.json`;
15
16
 
16
17
  /**
17
18
  * Válidos:
18
- * - diff-review-output.json
19
- * - diff-review-output-YYYYMMDD-HHmmss.json
20
- * - diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json
19
+ * - diff-rev_{branchTip}_{baseTip}.json
20
+ * - diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json (legacy)
21
+ * - diff-review-output.json / diff-review-output-YYYYMMDD-HHmmss.json (legacy)
21
22
  */
22
23
  export const OUTPUT_FILENAME_RE =
23
- /^(?:diff-review-output(?:-\d{8}-\d{6})?|diff-report_[0-9a-f]{7,12}-[0-9a-f]{7,12}_\d{8}-\d{6})\.json$/i;
24
+ /^(?:diff-rev_[0-9a-f]{7,12}_[0-9a-f]{7,12}|diff-report_[0-9a-f]{7,12}-[0-9a-f]{7,12}_\d{8}-\d{6}|diff-review-output(?:-\d{8}-\d{6})?)\.json$/i;
24
25
 
25
26
  /**
26
- * @param {Date} [when]
27
- * @returns {string} ej. 20260915-143045
28
- */
29
- export function formatOutputStamp(when = new Date()) {
30
- const pad = (n) => String(n).padStart(2, "0");
31
- const y = when.getFullYear();
32
- const mo = pad(when.getMonth() + 1);
33
- const d = pad(when.getDate());
34
- const h = pad(when.getHours());
35
- const mi = pad(when.getMinutes());
36
- const s = pad(when.getSeconds());
37
- return `${y}${mo}${d}-${h}${mi}${s}`;
38
- }
39
-
40
- /**
41
- * @param {{ branchSha?: string, baseSha?: string, stamp?: string, when?: Date } | Date} [input]
27
+ * @param {{ branchSha?: string, baseSha?: string }} [input]
42
28
  * @returns {string}
43
- * con tips: diff-report_aaaaaaaaaa-bbbbbbbbbb_20260915-143045.json
44
- * sin tips: diff-review-output-20260915-143045.json
29
+ * con tips: diff-rev_aaaaaaaaaa_bbbbbbbbbb.json
30
+ * sin tips: diff-review-output.json
45
31
  */
46
32
  export function makeOutputFilename(input = {}) {
47
- const opts = input instanceof Date ? { when: input } : input || {};
48
- const stamp = opts.stamp || formatOutputStamp(opts.when ?? new Date());
33
+ const opts = input instanceof Date ? {} : input || {};
49
34
  const fp = fingerprint(opts.branchSha, opts.baseSha);
50
- if (fp) return `${OUTPUT_PREFIX}_${fp}_${stamp}.json`;
51
- return `${OUTPUT_PREFIX_LEGACY}-${stamp}.json`;
35
+ if (fp) {
36
+ const [branchTip, baseTip] = fp.split("-");
37
+ if (branchTip && baseTip) return `${OUTPUT_PREFIX}_${branchTip}_${baseTip}.json`;
38
+ }
39
+ return OUTPUT_FILENAME_LEGACY;
52
40
  }
53
41
 
54
42
  export function isOutputFilename(name) {
@@ -1,54 +1,37 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
  import {
4
- formatOutputStamp,
5
4
  isOutputFilename,
6
5
  makeOutputFilename,
7
6
  OUTPUT_FILENAME_LEGACY,
8
7
  } from "./output-name.mjs";
9
8
 
10
9
  describe("output-name", () => {
11
- const when = new Date(2026, 8, 15, 14, 30, 45);
12
- const stamp = "20260915-143045";
13
-
14
- it("formats stamp", () => {
15
- assert.equal(formatOutputStamp(when), stamp);
16
- });
17
-
18
10
  it("builds name with short tip hashes", () => {
19
11
  const name = makeOutputFilename({
20
12
  branchSha: "aaaaaaaaaa1111111111",
21
13
  baseSha: "bbbbbbbbbb2222222222",
22
- when,
23
14
  });
24
- assert.equal(name, "diff-report_aaaaaaaaaa-bbbbbbbbbb_20260915-143045.json");
15
+ assert.equal(name, "diff-rev_aaaaaaaaaa_bbbbbbbbbb.json");
25
16
  assert.equal(isOutputFilename(name), true);
26
17
  });
27
18
 
28
- it("reuses an explicit stamp", () => {
29
- const name = makeOutputFilename({
30
- branchSha: "abcdef0123456789",
31
- baseSha: "fedcba9876543210",
32
- stamp: "20260101-000000",
33
- });
34
- assert.equal(name, "diff-report_abcdef0123-fedcba9876_20260101-000000.json");
35
- });
36
-
37
- it("falls back to legacy stamp without tips", () => {
38
- const name = makeOutputFilename({ when });
39
- assert.equal(name, "diff-review-output-20260915-143045.json");
40
- assert.equal(isOutputFilename(name), true);
19
+ it("falls back to legacy without tips", () => {
20
+ assert.equal(makeOutputFilename({}), OUTPUT_FILENAME_LEGACY);
21
+ assert.equal(isOutputFilename(OUTPUT_FILENAME_LEGACY), true);
41
22
  });
42
23
 
43
- it("accepts legacy names", () => {
44
- assert.equal(OUTPUT_FILENAME_LEGACY, "diff-review-output.json");
45
- assert.equal(isOutputFilename(OUTPUT_FILENAME_LEGACY), true);
24
+ it("accepts legacy stamped and report names", () => {
46
25
  assert.equal(isOutputFilename("diff-review-output-20260915-143045.json"), true);
26
+ assert.equal(
27
+ isOutputFilename("diff-report_aaaaaaaaaa-bbbbbbbbbb_20260915-143045.json"),
28
+ true
29
+ );
47
30
  });
48
31
 
49
32
  it("rejects other names", () => {
50
33
  assert.equal(isOutputFilename("review.json"), false);
51
34
  assert.equal(isOutputFilename("diff-review-output-foo.json"), false);
52
- assert.equal(isOutputFilename("diff-report_short-tips_20260915-143045.json"), false);
35
+ assert.equal(isOutputFilename("diff-rev_short_tips.json"), false);
53
36
  });
54
37
  });
package/lib/prompt.mjs CHANGED
@@ -4,14 +4,12 @@
4
4
  */
5
5
  import {
6
6
  makeOutputFilename,
7
- formatOutputStamp,
8
7
  isOutputFilename,
9
8
  OUTPUT_FILENAME_LEGACY,
10
9
  } from "./output-name.mjs";
11
10
 
12
11
  export {
13
12
  makeOutputFilename,
14
- formatOutputStamp,
15
13
  isOutputFilename,
16
14
  OUTPUT_FILENAME_LEGACY,
17
15
  };
@@ -22,7 +20,10 @@ export const OUTPUT_SCHEMA_BLOCK = `{
22
20
  "repo": "<absolute path or git URL>",
23
21
  "branch": "<branch under review>",
24
22
  "base": "develop",
25
- "remoteUrl": "<origin URL; optional when source=url>"
23
+ "remoteUrl": "<origin URL; optional when source=url>",
24
+ "generatedAt": "<ISO-8601 datetime; optional, informational>",
25
+ "agent": "<tool name e.g. Claude Code / Cursor; optional, informational>",
26
+ "model": "<model id e.g. claude-opus-…; optional, informational>"
26
27
  },
27
28
  "intent": "string",
28
29
  "groups": [
@@ -82,7 +83,14 @@ function workflowLocal(base, branch) {
82
83
  1. Resolve the merge-base of \`${base}\` and \`${branch}\`.
83
84
  2. Run exactly: \`git diff ${base}...${branch}\` (three-dot / merge-base diff). Do not use two-dot unless three-dot is impossible.
84
85
  3. Read enough surrounding code (types, callers, tests) to judge behavior — not only the hunk lines.
85
- 4. Emit the JSON file. Do not modify the repo.`;
86
+ 4. Emit the JSON file. Do not modify the repo.
87
+ 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
+ }
89
+
90
+ /** Comando que el agente debe correr; npx por si el bin no está en PATH. */
91
+ export function validateCliCommand(filename) {
92
+ const file = String(filename || "diff-rev_<branchTip>_<baseTip>.json").trim();
93
+ return `npx --yes @jpkme/diff-review validate ${file}`;
86
94
  }
87
95
 
88
96
  function workflowUrl(base, branch) {
@@ -91,7 +99,8 @@ function workflowUrl(base, branch) {
91
99
  2. Resolve the merge-base of \`${base}\` and \`${branch}\`.
92
100
  3. Run exactly: \`git diff ${base}...${branch}\` (three-dot / merge-base diff). Do not use two-dot unless three-dot is impossible.
93
101
  4. Read enough surrounding code (types, callers, tests) to judge behavior — not only the hunk lines.
94
- 5. Emit the JSON file.`;
102
+ 5. Emit the JSON file.
103
+ 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.`;
95
104
  }
96
105
 
97
106
  /**
@@ -108,9 +117,14 @@ export function outputSchemaBlock(meta) {
108
117
  ` "source": ${JSON.stringify(source)},`,
109
118
  ` "repo": ${JSON.stringify(repo)},`,
110
119
  ` "branch": ${JSON.stringify(branch)},`,
111
- ` "base": ${JSON.stringify(base)}${remoteUrl ? "," : ""}`,
120
+ ` "base": ${JSON.stringify(base)},`,
112
121
  ];
113
- if (remoteUrl) metaLines.push(` "remoteUrl": ${JSON.stringify(remoteUrl)}`);
122
+ if (remoteUrl) metaLines.push(` "remoteUrl": ${JSON.stringify(remoteUrl)},`);
123
+ metaLines.push(
124
+ ` "generatedAt": "<ISO-8601 datetime>",`,
125
+ ` "agent": "<tool name, e.g. Claude Code / Cursor>",`,
126
+ ` "model": "<model id>"`
127
+ );
114
128
  const metaBlock = `"meta": {\n${metaLines.join("\n")}\n }`;
115
129
  return OUTPUT_SCHEMA_BLOCK.replace(/"meta": \{[\s\S]*?\n \}/, metaBlock);
116
130
  }
@@ -142,13 +156,18 @@ ${RULES}
142
156
  Write the result to \`${outputFilename}\` at the repo root.
143
157
  - One JSON object only: first character \`{\`, last character \`}\`.
144
158
  - No prose before/after, no markdown fences.
145
- - Shape (copy "meta" exactly as given):
159
+ - Shape (copy "meta" source/repo/branch/base/remoteUrl as given; fill generatedAt/agent/model):
146
160
 
147
161
  ${outputSchemaBlock(meta)}
148
162
 
149
- Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). That lets someone reopen the report elsewhere without re-picking the repo.
163
+ Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). Also set informational fields: "generatedAt" (ISO-8601 now), "agent" (tool name), "model" (model id). That lets someone reopen the report elsewhere without re-picking the repo.
150
164
 
151
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.
152
166
 
153
- When done, tell me that \`${outputFilename}\` was written.`;
167
+ After writing the file, validate it (npx so it works even if \`diff-review\` is not on PATH):
168
+
169
+ \`${validateCliCommand(outputFilename)}\`
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.`;
154
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpkme/diff-review",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local UI to prompt a coding agent for a branch review and browse the result.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,6 +49,7 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@bindrs/rfd": "^2.2.2"
52
+ "@bindrs/rfd": "^2.2.2",
53
+ "@clack/prompts": "^1.8.1"
53
54
  }
54
55
  }
package/schema.json CHANGED
@@ -21,6 +21,18 @@
21
21
  "remoteUrl": {
22
22
  "type": "string",
23
23
  "description": "URL del remote (origin). Fallback si la carpeta local no existe en otra máquina."
24
+ },
25
+ "generatedAt": {
26
+ "type": "string",
27
+ "description": "ISO-8601 de cuándo se escribió la review (informativo)."
28
+ },
29
+ "agent": {
30
+ "type": "string",
31
+ "description": "Nombre del tool/agente (informativo), ej. Claude Code / Cursor."
32
+ },
33
+ "model": {
34
+ "type": "string",
35
+ "description": "Id del modelo usado (informativo)."
24
36
  }
25
37
  }
26
38
  },
@@ -1 +1 @@
1
- import"./Dur-vdUf.js";import{c as e}from"./Gya4IU1I.js";var t={get data(){return e.data},get error(){return e.error},get form(){return e.form},get params(){return e.params},get route(){return e.route},get state(){return e.state},get status(){return e.status},get url(){return e.url}};export{t};
1
+ import"./Dur-vdUf.js";import{c as e}from"./zvcaSoGP.js";var t={get data(){return e.data},get error(){return e.error},get form(){return e.form},get params(){return e.params},get route(){return e.route},get state(){return e.state},get status(){return e.status},get url(){return e.url}};export{t};
@@ -0,0 +1,2 @@
1
+ %@��\��'��ǚU��)d�"*�ߎ�0�,�4 =�=�Ȧ�+3���_��澨#�
2
+ �Y ��M!^!�ƙ�9�8W"����:�|G����=�������<��`,�Z�Fxc��j��˾���K7�����an?