@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.
Files changed (159) hide show
  1. package/README.md +38 -15
  2. package/bin/diff-review.mjs +151 -34
  3. package/lib/acp-policy.mjs +223 -0
  4. package/lib/acp-policy.test.mjs +136 -0
  5. package/lib/acp-shell.mjs +409 -0
  6. package/lib/acp-shell.test.mjs +158 -0
  7. package/lib/acp.mjs +368 -0
  8. package/lib/acp.test.mjs +58 -0
  9. package/lib/agent-login.mjs +105 -0
  10. package/lib/agents.mjs +87 -14
  11. package/lib/agents.test.mjs +4 -6
  12. package/lib/check-report.mjs +30 -11
  13. package/lib/check-report.test.mjs +1 -0
  14. package/lib/extract-cache.test.mjs +162 -0
  15. package/lib/extract.mjs +197 -1
  16. package/lib/model-label.mjs +263 -0
  17. package/lib/model-label.test.mjs +77 -0
  18. package/lib/models-cache.mjs +75 -0
  19. package/lib/models-cache.test.mjs +50 -0
  20. package/lib/output-name.mjs +8 -0
  21. package/lib/prompt.mjs +168 -22
  22. package/lib/review-dir.mjs +108 -0
  23. package/lib/review-meta.mjs +5 -3
  24. package/lib/review-meta.test.mjs +11 -0
  25. package/lib/review-prep.mjs +63 -0
  26. package/lib/validate.mjs +2 -2
  27. package/lib/wait-output.mjs +46 -23
  28. package/lib/wait-output.test.mjs +24 -1
  29. package/package.json +3 -1
  30. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
  31. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  32. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
  33. package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css +1 -0
  34. package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.br +0 -0
  35. package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.gz +0 -0
  36. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
  37. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
  38. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
  39. package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css +1 -0
  40. package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.br +0 -0
  41. package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.gz +0 -0
  42. package/web/build/client/_app/immutable/chunks/BXUWMim4.js +213 -0
  43. package/web/build/client/_app/immutable/chunks/BXUWMim4.js.br +0 -0
  44. package/web/build/client/_app/immutable/chunks/BXUWMim4.js.gz +0 -0
  45. package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js +1 -0
  46. package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js.br +0 -0
  47. package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js.gz +0 -0
  48. package/web/build/client/_app/immutable/chunks/D8WOD_eS.js +3 -0
  49. package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
  50. package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
  51. package/web/build/client/_app/immutable/chunks/{BBWAo_yW.js → DPfXzQnm.js} +2 -2
  52. package/web/build/client/_app/immutable/chunks/DPfXzQnm.js.br +0 -0
  53. package/web/build/client/_app/immutable/chunks/DPfXzQnm.js.gz +0 -0
  54. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +1 -0
  55. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
  56. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
  57. package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js +2 -0
  58. package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.br +0 -0
  59. package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.gz +0 -0
  60. package/web/build/client/_app/immutable/entry/start.CFSl0kht.js +1 -0
  61. package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.br +0 -0
  62. package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.gz +0 -0
  63. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js +1 -0
  64. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.br +0 -0
  65. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.gz +0 -0
  66. package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js +1 -0
  67. package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.br +2 -0
  68. package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.gz +0 -0
  69. package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js +10 -0
  70. package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.br +0 -0
  71. package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.gz +0 -0
  72. package/web/build/client/_app/immutable/nodes/3.AciugOtL.js +171 -0
  73. package/web/build/client/_app/immutable/nodes/3.AciugOtL.js.br +0 -0
  74. package/web/build/client/_app/immutable/nodes/3.AciugOtL.js.gz +0 -0
  75. package/web/build/client/_app/version.json +1 -1
  76. package/web/build/client/_app/version.json.br +0 -0
  77. package/web/build/client/_app/version.json.gz +0 -0
  78. package/web/build/handler.js +3 -3
  79. package/web/build/index.js +3 -3
  80. package/web/build/server/chunks/0-DUStDOsr.js +11 -0
  81. package/web/build/server/chunks/{0-SseDy__O.js.map → 0-DUStDOsr.js.map} +1 -1
  82. package/web/build/server/chunks/1-C2qlInYk.js +7 -0
  83. package/web/build/server/chunks/1-C2qlInYk.js.map +1 -0
  84. package/web/build/server/chunks/2-DyE177MK.js +7 -0
  85. package/web/build/server/chunks/2-DyE177MK.js.map +1 -0
  86. package/web/build/server/chunks/3-SO3atuWO.js +7 -0
  87. package/web/build/server/chunks/3-SO3atuWO.js.map +1 -0
  88. package/web/build/server/chunks/_server.ts-CRNhzL2a.js +62 -0
  89. package/web/build/server/chunks/_server.ts-CRNhzL2a.js.map +1 -0
  90. package/web/build/server/chunks/agent-sessions-kcUaLhK5.js +2819 -0
  91. package/web/build/server/chunks/agent-sessions-kcUaLhK5.js.map +1 -0
  92. package/web/build/server/chunks/{remote-1g4ayag-B67QMR_2.js → git-review-CXKcjYgZ.js} +70 -704
  93. package/web/build/server/chunks/git-review-CXKcjYgZ.js.map +1 -0
  94. package/web/build/server/chunks/{handler-B5P65tLk.js → handler-_E0NucKN.js} +3 -3
  95. package/web/build/server/chunks/{handler-B5P65tLk.js.map → handler-_E0NucKN.js.map} +1 -1
  96. package/web/build/server/chunks/hooks.server-D7yPODoc.js +42 -0
  97. package/web/build/server/chunks/hooks.server-D7yPODoc.js.map +1 -0
  98. package/web/build/server/chunks/{index.js-CL95e0Gl.js → index.js-lMtDM8ZQ.js} +3 -5
  99. package/web/build/server/chunks/index.js-lMtDM8ZQ.js.map +1 -0
  100. package/web/build/server/chunks/manifest.js-BIm6jot5.js +58 -0
  101. package/web/build/server/chunks/manifest.js-BIm6jot5.js.map +1 -0
  102. package/web/build/server/chunks/remote-1g4ayag-DmqxwUIz.js +144 -0
  103. package/web/build/server/chunks/remote-1g4ayag-DmqxwUIz.js.map +1 -0
  104. package/web/build/server/chunks/remote-wh6t6j-DZk0W9Pp.js +162 -0
  105. package/web/build/server/chunks/remote-wh6t6j-DZk0W9Pp.js.map +1 -0
  106. package/web/build/server/chunks/validate-BsTKyhsq.js +1073 -0
  107. package/web/build/server/chunks/validate-BsTKyhsq.js.map +1 -0
  108. package/web/build/client/_app/immutable/assets/0.CLQiq3Ed.css.br +0 -0
  109. package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css +0 -1
  110. package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.br +0 -0
  111. package/web/build/client/_app/immutable/assets/2.CNFDEnjx.css.gz +0 -0
  112. package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css +0 -1
  113. package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.br +0 -0
  114. package/web/build/client/_app/immutable/assets/3.PHTPLlZ7.css.gz +0 -0
  115. package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css +0 -1
  116. package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.br +0 -0
  117. package/web/build/client/_app/immutable/assets/ThemeToggle.DcT79vPK.css.gz +0 -0
  118. package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.br +0 -0
  119. package/web/build/client/_app/immutable/chunks/BBWAo_yW.js.gz +0 -0
  120. package/web/build/client/_app/immutable/chunks/Dur-vdUf.js +0 -3
  121. package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.br +0 -0
  122. package/web/build/client/_app/immutable/chunks/Dur-vdUf.js.gz +0 -0
  123. package/web/build/client/_app/immutable/chunks/QdZF3nkf.js +0 -176
  124. package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.br +0 -0
  125. package/web/build/client/_app/immutable/chunks/QdZF3nkf.js.gz +0 -0
  126. package/web/build/client/_app/immutable/chunks/tYeNsyIK.js +0 -1
  127. package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.br +0 -0
  128. package/web/build/client/_app/immutable/chunks/tYeNsyIK.js.gz +0 -0
  129. package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js +0 -2
  130. package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.br +0 -0
  131. package/web/build/client/_app/immutable/entry/app.Dce6uJU3.js.gz +0 -0
  132. package/web/build/client/_app/immutable/entry/start.DnBSC38l.js +0 -1
  133. package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.br +0 -0
  134. package/web/build/client/_app/immutable/entry/start.DnBSC38l.js.gz +0 -0
  135. package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js +0 -1
  136. package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.br +0 -1
  137. package/web/build/client/_app/immutable/nodes/0.s5xmjXoS.js.gz +0 -0
  138. package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js +0 -1
  139. package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.br +0 -0
  140. package/web/build/client/_app/immutable/nodes/1.5Iv4ssDT.js.gz +0 -0
  141. package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js +0 -66
  142. package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.br +0 -0
  143. package/web/build/client/_app/immutable/nodes/2.BomzYVBF.js.gz +0 -0
  144. package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js +0 -170
  145. package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.br +0 -0
  146. package/web/build/client/_app/immutable/nodes/3.CeUs4lBX.js.gz +0 -0
  147. package/web/build/server/chunks/0-SseDy__O.js +0 -11
  148. package/web/build/server/chunks/1-2OO2bb7H.js +0 -7
  149. package/web/build/server/chunks/1-2OO2bb7H.js.map +0 -1
  150. package/web/build/server/chunks/2-BJvuqKN6.js +0 -7
  151. package/web/build/server/chunks/2-BJvuqKN6.js.map +0 -1
  152. package/web/build/server/chunks/3-ZIl1rHef.js +0 -7
  153. package/web/build/server/chunks/3-ZIl1rHef.js.map +0 -1
  154. package/web/build/server/chunks/hooks.server-DERFlOe1.js +0 -14
  155. package/web/build/server/chunks/hooks.server-DERFlOe1.js.map +0 -1
  156. package/web/build/server/chunks/index.js-CL95e0Gl.js.map +0 -1
  157. package/web/build/server/chunks/manifest.js-uD_a0079.js +0 -50
  158. package/web/build/server/chunks/manifest.js-uD_a0079.js.map +0 -1
  159. package/web/build/server/chunks/remote-1g4ayag-B67QMR_2.js.map +0 -1
@@ -0,0 +1,1073 @@
1
+ import { mkdirSync, existsSync, writeFileSync, readFileSync, statSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { join, basename, resolve } from 'node:path';
4
+
5
+ //#region ../lib/fingerprint.mjs
6
+ /**
7
+ * Tips cortos de SHA. Sin APIs de Node: la UI lo importa para armar el nombre del JSON.
8
+ */
9
+ function shortSha(sha, n = 10) {
10
+ const hex = String(sha || "").toLowerCase().replace(/[^a-f0-9]/g, "");
11
+ return hex.length >= 7 ? hex.slice(0, n) : "";
12
+ }
13
+ function fingerprint(branchSha, baseSha) {
14
+ const a = shortSha(branchSha);
15
+ const b = shortSha(baseSha);
16
+ if (!a || !b) return null;
17
+ return `${a}-${b}`;
18
+ }
19
+ //#endregion
20
+ //#region ../lib/output-name.mjs
21
+ /**
22
+ * Nombre del JSON que escribe el agente.
23
+ * Preferido: tips cortos branch/base. Legacy (con stamp o sin tips) sigue válido al importar.
24
+ */
25
+ /** Prefijo del formato actual. */
26
+ var OUTPUT_PREFIX = "diff-rev";
27
+ /** Prefijo legacy (sin tips / con stamp). */
28
+ var OUTPUT_PREFIX_LEGACY = "diff-review-output";
29
+ /** Carpeta (en la raíz del repo revisado) donde va el JSON, el diff y la sesión del agente. */
30
+ var OUTPUT_DIR = ".diff-review";
31
+ /** Path relativo a la raíz del repo, con `/` (el que se le da al agente): `.diff-review/<nombre>`. */
32
+ function outputRelPath(filename) {
33
+ return `${OUTPUT_DIR}/${String(filename || "").replace(/^.*[\\/]/, "")}`;
34
+ }
35
+ /** Nombre legacy fijo. */
36
+ var OUTPUT_FILENAME_LEGACY = `${OUTPUT_PREFIX_LEGACY}.json`;
37
+ /**
38
+ * Válidos:
39
+ * - diff-rev_{branchTip}_{baseTip}.json
40
+ * - diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json (legacy)
41
+ * - diff-review-output.json / diff-review-output-YYYYMMDD-HHmmss.json (legacy)
42
+ */
43
+ var OUTPUT_FILENAME_RE = /^(?: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;
44
+ /**
45
+ * @param {{ branchSha?: string, baseSha?: string }} [input]
46
+ * @returns {string}
47
+ * con tips: diff-rev_aaaaaaaaaa_bbbbbbbbbb.json
48
+ * sin tips: diff-review-output.json
49
+ */
50
+ function makeOutputFilename(input = {}) {
51
+ const opts = input instanceof Date ? {} : input || {};
52
+ const fp = fingerprint(opts.branchSha, opts.baseSha);
53
+ if (fp) {
54
+ const [branchTip, baseTip] = fp.split("-");
55
+ if (branchTip && baseTip) return `${OUTPUT_PREFIX}_${branchTip}_${baseTip}.json`;
56
+ }
57
+ return OUTPUT_FILENAME_LEGACY;
58
+ }
59
+ function isOutputFilename(name) {
60
+ return OUTPUT_FILENAME_RE.test(String(name || "").trim());
61
+ }
62
+ //#endregion
63
+ //#region ../lib/config.mjs
64
+ var DEFAULT_IGNORE = [
65
+ "**/package-lock.json",
66
+ "**/pnpm-lock.yaml",
67
+ "**/yarn.lock",
68
+ "**/bun.lock",
69
+ "**/bun.lockb",
70
+ "**/*.g.cs",
71
+ "**/*Snapshot.cs",
72
+ "**/*.Designer.cs",
73
+ "**/node_modules/**",
74
+ "**/.svelte-kit/**",
75
+ "**/dist/**",
76
+ "**/build/**",
77
+ "**/*.min.js",
78
+ "**/*.min.css",
79
+ "**/CODE_REVIEW_CORRECTNESS_FINDINGS.md"
80
+ ];
81
+ var STORE_DIFF_MAX = 4e5;
82
+ var GUIDELINE_FILES = [
83
+ "REVIEW.md",
84
+ "CLAUDE.md",
85
+ "AGENTS.md"
86
+ ];
87
+ function unquote(s) {
88
+ const t = String(s || "").trim();
89
+ if (t.startsWith("\"") && t.endsWith("\"") || t.startsWith("'") && t.endsWith("'")) return t.slice(1, -1);
90
+ return t;
91
+ }
92
+ /** Minimal YAML: `key: value` and `key:\n - item`. */
93
+ function parseSimpleYaml(text) {
94
+ const result = {};
95
+ let key = null;
96
+ for (const raw of String(text || "").split(/\r?\n/)) {
97
+ if (!raw.trim() || raw.trim().startsWith("#")) continue;
98
+ const list = raw.match(/^\s+-\s+(.*)$/);
99
+ if (list && key) {
100
+ if (!Array.isArray(result[key])) result[key] = [];
101
+ result[key].push(unquote(list[1]));
102
+ continue;
103
+ }
104
+ const kv = raw.match(/^([A-Za-z][\w]*)\s*:\s*(.*)$/);
105
+ if (!kv) continue;
106
+ key = kv[1];
107
+ const v = kv[2].trim();
108
+ result[key] = v === "" ? [] : unquote(v);
109
+ }
110
+ return result;
111
+ }
112
+ function globToRegExp(glob) {
113
+ let g = String(glob || "").replace(/\\/g, "/");
114
+ if (!g.includes("/")) g = `**/${g}`;
115
+ let s = "";
116
+ for (let i = 0; i < g.length; i++) {
117
+ const c = g[i];
118
+ if (c === "*" && g[i + 1] === "*") if (g[i + 2] === "/") {
119
+ s += "(?:.*/)?";
120
+ i += 2;
121
+ } else {
122
+ s += ".*";
123
+ i += 1;
124
+ }
125
+ else if (c === "*") s += "[^/]*";
126
+ else if (c === "?") s += "[^/]";
127
+ else if ("+()[]{}|.^$".includes(c)) s += `\\${c}`;
128
+ else s += c;
129
+ }
130
+ return new RegExp(`^${s}$`, "i");
131
+ }
132
+ function globMatch(filePath, pattern) {
133
+ const norm = String(filePath || "").replace(/\\/g, "/");
134
+ return globToRegExp(pattern).test(norm);
135
+ }
136
+ function isIgnored(filePath, patterns) {
137
+ return (patterns || []).some((p) => globMatch(filePath, p));
138
+ }
139
+ function isTestPath(filePath) {
140
+ const p = String(filePath || "").replace(/\\/g, "/");
141
+ return /(^|\/)(__tests__|tests?|spec)s?(\/|$)/i.test(p) || /\.(test|spec)\.[^.]+$/i.test(p);
142
+ }
143
+ function readIfExists(path) {
144
+ try {
145
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ function loadReviewConfig(repo) {
151
+ const jsonText = readIfExists(join(repo, ".diff-review.json"));
152
+ const yamlText = readIfExists(join(repo, ".diff-review.yml")) || readIfExists(join(repo, ".diff-review.yaml"));
153
+ let extra = {};
154
+ if (jsonText) try {
155
+ extra = JSON.parse(jsonText);
156
+ } catch {
157
+ extra = {};
158
+ }
159
+ else if (yamlText) extra = parseSimpleYaml(yamlText);
160
+ const extraIgnore = Array.isArray(extra.ignore) ? extra.ignore.map(String) : [];
161
+ const rulesFile = extra.rulesFile ? String(extra.rulesFile) : "REVIEW.md";
162
+ const rules = readIfExists(join(repo, rulesFile)) || "";
163
+ const model = extra.model && extra.model !== "default" ? String(extra.model) : null;
164
+ return {
165
+ ignore: [...DEFAULT_IGNORE, ...extraIgnore],
166
+ model,
167
+ rulesFile,
168
+ rules: rules.trim()
169
+ };
170
+ }
171
+ /** CLAUDE.md / REVIEW.md / AGENTS.md from repo root and immediate parent dirs of touched files. */
172
+ function collectGuidelines(repo, filePaths, { rulesFile = "REVIEW.md", maxChars = 8e3, maxTotalChars = 12e3, maxDepth = 1, skipNames = [] } = {}) {
173
+ const skip = new Set(skipNames);
174
+ const names = [.../* @__PURE__ */ new Set([rulesFile, ...GUIDELINE_FILES])].filter((name) => !skip.has(name));
175
+ const seen = /* @__PURE__ */ new Set();
176
+ const out = [];
177
+ const addRel = (rel) => {
178
+ const norm = String(rel || "").replace(/\\/g, "/").replace(/^\/+/, "");
179
+ if (!norm || seen.has(norm)) return;
180
+ const text = readIfExists(join(repo, ...norm.split("/")));
181
+ if (!text || !text.trim()) return;
182
+ seen.add(norm);
183
+ out.push({
184
+ path: norm,
185
+ text: text.trim()
186
+ });
187
+ };
188
+ for (const name of names) addRel(name);
189
+ const depth = Math.max(1, Number(maxDepth) || 1);
190
+ for (const filePath of filePaths || []) {
191
+ const parts = String(filePath || "").replace(/\\/g, "/").split("/").filter(Boolean);
192
+ const minLen = Math.max(1, parts.length - depth);
193
+ for (let len = parts.length - 1; len >= minLen; len--) {
194
+ const dir = parts.slice(0, len).join("/");
195
+ for (const name of names) addRel(`${dir}/${name}`);
196
+ }
197
+ }
198
+ let total = 0;
199
+ const capped = [];
200
+ for (const item of out) {
201
+ if (total >= maxTotalChars) break;
202
+ const room = maxTotalChars - total;
203
+ const slice = item.text.slice(0, Math.min(maxChars, room));
204
+ if (!slice) continue;
205
+ capped.push({
206
+ path: item.path,
207
+ text: slice
208
+ });
209
+ total += slice.length;
210
+ }
211
+ return capped;
212
+ }
213
+ function capDiff(diff, max = STORE_DIFF_MAX) {
214
+ const text = String(diff || "");
215
+ if (text.length <= max) return {
216
+ diff: text,
217
+ diffChars: text.length,
218
+ truncated: false
219
+ };
220
+ return {
221
+ diff: `${text.slice(0, max)}\n\n… [diff recortado: se guardan ${max} de ${text.length} caracteres]`,
222
+ diffChars: text.length,
223
+ truncated: true
224
+ };
225
+ }
226
+ //#endregion
227
+ //#region ../lib/git.mjs
228
+ function assertGitRepo(repo) {
229
+ const raw = String(repo || "").trim();
230
+ if (!raw) throw new Error("Indicá la ruta absoluta de un repositorio git.");
231
+ if (!/[\\/]/.test(raw) && !/^[A-Za-z]:/.test(raw)) throw new Error(`"${raw}" no es una ruta. Pegá la ruta absoluta del repo (ej. C:\\tienda-argenta\\tienda-argenta-2).`);
232
+ const resolved = resolve(raw);
233
+ if (!resolved || resolved === ".") throw new Error("Indicá la ruta absoluta de un repositorio git.");
234
+ if (!existsSync(resolved)) throw new Error(`No existe la carpeta: ${resolved}`);
235
+ if (!existsSync(join(resolved, ".git"))) throw new Error(`No es un repositorio git (falta .git): ${resolved}`);
236
+ return resolved;
237
+ }
238
+ /**
239
+ * Si `cwd` está dentro de un repo git, devuelve la raíz absoluta; si no, null.
240
+ * Acepta subcarpetas (usa `git rev-parse --show-toplevel`).
241
+ */
242
+ function tryResolveGitRoot(cwd) {
243
+ const raw = String(cwd || "").trim();
244
+ if (!raw) return null;
245
+ const dir = resolve(raw);
246
+ if (!existsSync(dir)) return null;
247
+ try {
248
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
249
+ cwd: dir,
250
+ encoding: "utf8",
251
+ windowsHide: true,
252
+ stdio: [
253
+ "ignore",
254
+ "pipe",
255
+ "ignore"
256
+ ]
257
+ }).trim();
258
+ return root ? resolve(root) : null;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+ function git(repo, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
264
+ return execFileSync("git", args, {
265
+ cwd: repo,
266
+ encoding: "utf8",
267
+ maxBuffer,
268
+ windowsHide: true
269
+ });
270
+ }
271
+ function resolveRev(repo, ref) {
272
+ return git(repo, ["rev-parse", ref]).trim();
273
+ }
274
+ function branchTips(repo, branch, base) {
275
+ return {
276
+ branchSha: resolveRev(repo, branch),
277
+ baseSha: resolveRev(repo, base)
278
+ };
279
+ }
280
+ function commitLog(repo, mergeBase, branch, limit = 25) {
281
+ try {
282
+ return git(repo, [
283
+ "log",
284
+ "--oneline",
285
+ "--no-merges",
286
+ "-n",
287
+ String(limit),
288
+ `${mergeBase}..${branch}`
289
+ ]).trim().split("\n").filter(Boolean);
290
+ } catch {
291
+ return [];
292
+ }
293
+ }
294
+ var FILE_LINE_CACHE = /* @__PURE__ */ new Map();
295
+ var FILE_LINE_CACHE_MAX = 48;
296
+ function rememberFileLines(key, lines) {
297
+ if (FILE_LINE_CACHE.has(key)) FILE_LINE_CACHE.delete(key);
298
+ FILE_LINE_CACHE.set(key, lines);
299
+ while (FILE_LINE_CACHE.size > FILE_LINE_CACHE_MAX) {
300
+ const oldest = FILE_LINE_CACHE.keys().next().value;
301
+ FILE_LINE_CACHE.delete(oldest);
302
+ }
303
+ }
304
+ function showFileLines(repo, rev, path) {
305
+ const resolved = assertGitRepo(repo);
306
+ const safeRev = String(rev || "").trim();
307
+ const safePath = String(path || "").replace(/\\/g, "/").trim();
308
+ if (!safeRev || !/^[A-Za-z0-9._/\-]+$/.test(safeRev)) throw new Error("Rev inválido.");
309
+ if (!safePath || safePath.startsWith("/") || safePath.includes("..") || safePath.includes("\0")) throw new Error("Ruta inválida.");
310
+ const key = `${resolved}\0${safeRev}\0${safePath}`;
311
+ if (FILE_LINE_CACHE.has(key)) return FILE_LINE_CACHE.get(key);
312
+ let text;
313
+ try {
314
+ text = git(resolved, ["show", `${safeRev}:${safePath}`]);
315
+ } catch {
316
+ rememberFileLines(key, null);
317
+ return null;
318
+ }
319
+ if (text.includes("\0")) {
320
+ rememberFileLines(key, null);
321
+ return null;
322
+ }
323
+ const raw = text.replace(/\r\n/g, "\n");
324
+ const lines = raw.endsWith("\n") ? raw.slice(0, -1).split("\n") : raw.split("\n");
325
+ rememberFileLines(key, lines);
326
+ return lines;
327
+ }
328
+ function listBranches(repo) {
329
+ const names = git(repo, [
330
+ "for-each-ref",
331
+ "--sort=-committerdate",
332
+ "--format=%(refname:short)",
333
+ "refs/heads",
334
+ "refs/remotes"
335
+ ]).split("\n").map((s) => s.trim()).filter(Boolean).map((name) => name.replace(/^origin\//, "")).filter((name) => name !== "HEAD" && !name.endsWith("/HEAD"));
336
+ const unique = [...new Set(names)];
337
+ const current = git(repo, [
338
+ "rev-parse",
339
+ "--abbrev-ref",
340
+ "HEAD"
341
+ ]).trim();
342
+ const bases = unique.filter((n) => n === "develop" || n === "main" || n === "master");
343
+ return {
344
+ branches: unique,
345
+ current,
346
+ defaultBase: bases.includes("develop") ? "develop" : bases.includes("main") ? "main" : bases[0] || "develop"
347
+ };
348
+ }
349
+ /** URL del remote (origin, o el primero disponible). Vacío si no hay. */
350
+ function getRemoteUrl(repo, preferred = "origin") {
351
+ try {
352
+ const url = git(repo, [
353
+ "remote",
354
+ "get-url",
355
+ preferred
356
+ ]).trim();
357
+ if (url) return url;
358
+ } catch {}
359
+ try {
360
+ const remotes = git(repo, ["remote"]).split("\n").map((s) => s.trim()).filter(Boolean);
361
+ if (!remotes.length) return "";
362
+ return git(repo, [
363
+ "remote",
364
+ "get-url",
365
+ remotes[0]
366
+ ]).trim();
367
+ } catch {
368
+ return "";
369
+ }
370
+ }
371
+ //#endregion
372
+ //#region ../lib/review-dir.mjs
373
+ /**
374
+ * 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
375
+ * del agente, todo con el mismo nombre base (`diff-rev_<branchTip>_<baseTip>`). Se ignora sola con su propio
376
+ * `.gitignore`, así no deja cambios en archivos trackeados del repo.
377
+ *
378
+ * Los JSON viejos en la raíz del repo se siguen encontrando (`locateReviewFile`).
379
+ */
380
+ /** @typedef {{ agentId: string, sessionId: string, model?: string, updatedAt?: string }} SessionRef */
381
+ function reviewDir(repoRoot) {
382
+ return join(repoRoot, OUTPUT_DIR);
383
+ }
384
+ /** Crea `.diff-review/` (con su `.gitignore`) si no existe. */
385
+ function ensureReviewDir(repoRoot) {
386
+ const dir = reviewDir(repoRoot);
387
+ mkdirSync(dir, { recursive: true });
388
+ const ignore = join(dir, ".gitignore");
389
+ if (!existsSync(ignore)) writeFileSync(ignore, "# Generado por diff-review: nada de esta carpeta va al repo.\n*\n");
390
+ return dir;
391
+ }
392
+ /** `diff-rev_a_b.json` → `{ json, diff, session }` dentro de `.diff-review/`. */
393
+ function companionPaths(repoRoot, filename) {
394
+ const name = basename(String(filename || ""));
395
+ const stem = name.replace(/\.json$/i, "");
396
+ const dir = reviewDir(repoRoot);
397
+ return {
398
+ json: join(dir, name),
399
+ diff: join(dir, `${stem}.diff`),
400
+ session: join(dir, `${stem}.session.json`)
401
+ };
402
+ }
403
+ /** Dónde está el JSON de la review: `.diff-review/` primero y, si no, la raíz del repo (formato viejo). */
404
+ function locateReviewFile(repoRoot, filename) {
405
+ const name = basename(String(filename || ""));
406
+ if (!name) return null;
407
+ for (const candidate of [companionPaths(repoRoot, name).json, join(repoRoot, name)]) try {
408
+ if (statSync(candidate).isFile()) return candidate;
409
+ } catch {}
410
+ return null;
411
+ }
412
+ /**
413
+ * Guarda `git diff base...branch` junto al JSON (una vez por par de commits: el nombre ya trae los tips).
414
+ * @returns {string | null} el path, o null si git falló
415
+ */
416
+ function writeDiffCache(repoRoot, filename, { branch, base }) {
417
+ const { diff } = companionPaths(repoRoot, filename);
418
+ if (existsSync(diff)) return diff;
419
+ try {
420
+ const text = git(repoRoot, ["diff", `${base}...${branch}`]);
421
+ ensureReviewDir(repoRoot);
422
+ writeFileSync(diff, text);
423
+ return diff;
424
+ } catch {
425
+ return null;
426
+ }
427
+ }
428
+ /** Guarda un diff ya armado (p. ej. el que muestra la web), pisando el anterior. */
429
+ function writeDiffText(repoRoot, filename, text) {
430
+ ensureReviewDir(repoRoot);
431
+ const { diff } = companionPaths(repoRoot, filename);
432
+ writeFileSync(diff, String(text ?? ""));
433
+ return diff;
434
+ }
435
+ /** @returns {string | null} texto del companion `.diff`, o null si no existe / no se pudo leer */
436
+ function readDiffCache(repoRoot, filename) {
437
+ const { diff } = companionPaths(repoRoot, filename);
438
+ try {
439
+ if (!existsSync(diff)) return null;
440
+ return readFileSync(diff, "utf8");
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+ /** @param {SessionRef} ref */
446
+ function writeSessionRef(repoRoot, filename, ref) {
447
+ ensureReviewDir(repoRoot);
448
+ const full = {
449
+ ...ref,
450
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
451
+ };
452
+ writeFileSync(companionPaths(repoRoot, filename).session, `${JSON.stringify(full, null, 2)}\n`);
453
+ return full;
454
+ }
455
+ //#endregion
456
+ //#region ../lib/extract.mjs
457
+ /**
458
+ * Parte un `git diff` multi-archivo (texto de `writeDiffCache`) en entradas por path.
459
+ * @param {string} text
460
+ * @returns {Array<{ path: string, oldPath?: string, changeType: string, hunkDiff: string }>}
461
+ */
462
+ function parseGitDiffFiles(text) {
463
+ const raw = String(text || "").replace(/\r\n/g, "\n");
464
+ if (!raw.trim()) return [];
465
+ const parts = raw.split(/^diff --git /m);
466
+ /** @type {Array<{ path: string, oldPath?: string, changeType: string, hunkDiff: string }>} */
467
+ const out = [];
468
+ for (const part of parts) {
469
+ const chunk = part.trimStart();
470
+ if (!chunk) continue;
471
+ const firstNl = chunk.indexOf("\n");
472
+ const paths = (firstNl === -1 ? chunk : chunk.slice(0, firstNl)).trim().match(/^a\/(.+) b\/(.+)$/);
473
+ if (!paths) continue;
474
+ let oldPath = paths[1];
475
+ let path = paths[2];
476
+ const body = firstNl === -1 ? "" : chunk.slice(firstNl + 1);
477
+ let changeType = "modified";
478
+ if (/^new file mode /m.test(body) || /^--- \/dev\/null/m.test(body)) changeType = "added";
479
+ else if (/^deleted file mode /m.test(body) || /^\+\+\+ \/dev\/null/m.test(body)) changeType = "deleted";
480
+ else if (/^rename from /m.test(body) || /^rename to /m.test(body)) {
481
+ changeType = "renamed";
482
+ const from = body.match(/^rename from (.+)$/m);
483
+ const to = body.match(/^rename to (.+)$/m);
484
+ if (from) oldPath = from[1].trim();
485
+ if (to) path = to[1].trim();
486
+ }
487
+ let hunkDiff = "";
488
+ const idx = `\n${body}`.indexOf("\n@@");
489
+ if (idx !== -1) hunkDiff = `\n${body}`.slice(idx + 1);
490
+ else if (/^Binary files /m.test(body)) hunkDiff = "";
491
+ out.push({
492
+ path,
493
+ oldPath: oldPath !== path ? oldPath : void 0,
494
+ changeType,
495
+ hunkDiff
496
+ });
497
+ }
498
+ return out;
499
+ }
500
+ /**
501
+ * Stats baratos desde el texto del diff (sin volver a llamar a git).
502
+ * @param {string} text
503
+ */
504
+ function statsFromDiffText(text) {
505
+ let insertions = 0;
506
+ let deletions = 0;
507
+ for (const line of String(text || "").split("\n")) {
508
+ if (!line.length) continue;
509
+ if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff ") || line.startsWith("index ")) continue;
510
+ if (line.startsWith("+")) insertions += 1;
511
+ else if (line.startsWith("-")) deletions += 1;
512
+ }
513
+ return {
514
+ insertions,
515
+ deletions
516
+ };
517
+ }
518
+ /**
519
+ * Tips / merge-base si git responde; si no, strings vacíos (el contenido del diff ya viene del cache).
520
+ */
521
+ function tryTips(repo, branch, base) {
522
+ try {
523
+ return {
524
+ mergeBase: git(repo, [
525
+ "merge-base",
526
+ base,
527
+ branch
528
+ ]).trim(),
529
+ branchSha: git(repo, ["rev-parse", branch]).trim(),
530
+ baseSha: git(repo, ["rev-parse", base]).trim()
531
+ };
532
+ } catch {
533
+ return {
534
+ mergeBase: "",
535
+ branchSha: "",
536
+ baseSha: ""
537
+ };
538
+ }
539
+ }
540
+ /**
541
+ * Arma el mismo shape que `extractBranchDiff` a partir de un texto de `git diff` ya cacheado.
542
+ * No vuelve a correr `git diff`.
543
+ */
544
+ function skeletonFromCachedDiff(repo, branch, base, cachedDiff) {
545
+ const config = loadReviewConfig(repo);
546
+ const tips = tryTips(repo, branch, base);
547
+ const parsed = parseGitDiffFiles(cachedDiff);
548
+ const ignored = [];
549
+ const included = [];
550
+ for (const f of parsed) if (isIgnored(f.path, config.ignore)) ignored.push(f.path);
551
+ else included.push(f);
552
+ const files = included.map((f) => {
553
+ const capped = capDiff(f.hunkDiff);
554
+ return {
555
+ path: f.path,
556
+ oldPath: f.oldPath || void 0,
557
+ changeType: f.changeType,
558
+ diff: capped.diff,
559
+ diffChars: capped.diffChars,
560
+ truncated: capped.truncated,
561
+ modelChars: capped.diff.length,
562
+ group: ""
563
+ };
564
+ });
565
+ const { insertions, deletions } = statsFromDiffText(cachedDiff);
566
+ let commits = [];
567
+ if (tips.mergeBase) try {
568
+ commits = commitLog(repo, tips.mergeBase, branch);
569
+ } catch {
570
+ commits = [];
571
+ }
572
+ return {
573
+ version: 2,
574
+ repo,
575
+ branch,
576
+ baseBranch: base,
577
+ mergeBase: tips.mergeBase,
578
+ branchSha: tips.branchSha,
579
+ baseSha: tips.baseSha,
580
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
581
+ intent: "",
582
+ stats: {
583
+ filesChanged: files.length,
584
+ filesIgnored: ignored.length,
585
+ filesTruncated: files.filter((f) => f.truncated).length,
586
+ insertions,
587
+ deletions
588
+ },
589
+ context: {
590
+ commits,
591
+ rules: config.rules,
592
+ rulesFile: config.rulesFile,
593
+ ignored: ignored.slice(0, 40),
594
+ testsTouched: files.filter((f) => isTestPath(f.path)).map((f) => f.path),
595
+ testsMissing: files.filter((f) => !isTestPath(f.path)).length,
596
+ guidelines: collectGuidelines(repo, files.map((f) => f.path), {
597
+ rulesFile: config.rulesFile,
598
+ skipNames: config.rules.trim() ? [config.rulesFile] : [],
599
+ maxDepth: 1,
600
+ maxChars: 8e3,
601
+ maxTotalChars: 12e3
602
+ })
603
+ },
604
+ files,
605
+ groups: [],
606
+ blocks: [],
607
+ findings: [],
608
+ skipped: []
609
+ };
610
+ }
611
+ /**
612
+ * @param {{
613
+ * repo: string,
614
+ * branch: string,
615
+ * base?: string,
616
+ * cachedDiff?: string | null,
617
+ * }} opts
618
+ * `cachedDiff`: texto de `git diff base...branch` ya materializado; si viene, no se vuelve a calcular el diff.
619
+ */
620
+ function extractBranchDiff({ repo, branch, base = "develop", cachedDiff = null } = {}) {
621
+ if (!branch) throw new Error("Falta el branch a revisar.");
622
+ if (typeof cachedDiff === "string" && cachedDiff.length) return skeletonFromCachedDiff(repo, branch, base, cachedDiff);
623
+ const config = loadReviewConfig(repo);
624
+ const mergeBase = git(repo, [
625
+ "merge-base",
626
+ base,
627
+ branch
628
+ ]).trim();
629
+ const branchSha = git(repo, ["rev-parse", branch]).trim();
630
+ const baseSha = git(repo, ["rev-parse", base]).trim();
631
+ const nameStatus = git(repo, [
632
+ "diff",
633
+ mergeBase,
634
+ branch,
635
+ "--name-status"
636
+ ]).trim().split("\n").filter(Boolean).map((line) => {
637
+ const [status, ...rest] = line.split(" ");
638
+ const path = rest[rest.length - 1];
639
+ const oldPath = rest.length > 1 ? rest[0] : null;
640
+ let changeType = "modified";
641
+ if (status.startsWith("A")) changeType = "added";
642
+ else if (status.startsWith("D")) changeType = "deleted";
643
+ else if (status.startsWith("R")) changeType = "renamed";
644
+ return {
645
+ path,
646
+ oldPath,
647
+ changeType
648
+ };
649
+ });
650
+ const ignored = [];
651
+ const included = [];
652
+ for (const f of nameStatus) if (isIgnored(f.path, config.ignore)) ignored.push(f.path);
653
+ else included.push(f);
654
+ const files = included.map((f) => {
655
+ let raw = "";
656
+ try {
657
+ raw = git(repo, [
658
+ "diff",
659
+ mergeBase,
660
+ branch,
661
+ "--",
662
+ f.path
663
+ ]);
664
+ const idx = raw.indexOf("\n@@");
665
+ if (idx !== -1) raw = raw.slice(idx + 1);
666
+ } catch {
667
+ raw = "";
668
+ }
669
+ const capped = capDiff(raw);
670
+ return {
671
+ path: f.path,
672
+ oldPath: f.oldPath || void 0,
673
+ changeType: f.changeType,
674
+ diff: capped.diff,
675
+ diffChars: capped.diffChars,
676
+ truncated: capped.truncated,
677
+ modelChars: capped.diff.length,
678
+ group: ""
679
+ };
680
+ });
681
+ const shortstat = git(repo, [
682
+ "diff",
683
+ mergeBase,
684
+ branch,
685
+ "--shortstat"
686
+ ]).trim();
687
+ const insMatch = shortstat.match(/(\d+) insertion/);
688
+ const delMatch = shortstat.match(/(\d+) deletion/);
689
+ return {
690
+ version: 2,
691
+ repo,
692
+ branch,
693
+ baseBranch: base,
694
+ mergeBase,
695
+ branchSha,
696
+ baseSha,
697
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
698
+ intent: "",
699
+ stats: {
700
+ filesChanged: files.length,
701
+ filesIgnored: ignored.length,
702
+ filesTruncated: files.filter((f) => f.truncated).length,
703
+ insertions: insMatch ? Number(insMatch[1]) : 0,
704
+ deletions: delMatch ? Number(delMatch[1]) : 0
705
+ },
706
+ context: {
707
+ commits: commitLog(repo, mergeBase, branch),
708
+ rules: config.rules,
709
+ rulesFile: config.rulesFile,
710
+ ignored: ignored.slice(0, 40),
711
+ testsTouched: files.filter((f) => isTestPath(f.path)).map((f) => f.path),
712
+ testsMissing: files.filter((f) => !isTestPath(f.path)).length,
713
+ guidelines: collectGuidelines(repo, files.map((f) => f.path), {
714
+ rulesFile: config.rulesFile,
715
+ skipNames: config.rules.trim() ? [config.rulesFile] : [],
716
+ maxDepth: 1,
717
+ maxChars: 8e3,
718
+ maxTotalChars: 12e3
719
+ })
720
+ },
721
+ files,
722
+ groups: [],
723
+ blocks: [],
724
+ findings: [],
725
+ skipped: []
726
+ };
727
+ }
728
+ /**
729
+ * Preferí el companion `.diff` en `.diff-review/` si existe; si no, `git diff` vía `extractBranchDiff`.
730
+ * @param {{ repo: string, branch: string, base?: string, filename?: string }} opts
731
+ */
732
+ function loadBranchDiff({ repo, branch, base = "develop", filename } = {}) {
733
+ const name = String(filename || "").trim();
734
+ if (name) {
735
+ const cached = readDiffCache(repo, name);
736
+ if (cached != null && cached.length) return extractBranchDiff({
737
+ repo,
738
+ branch,
739
+ base,
740
+ cachedDiff: cached
741
+ });
742
+ }
743
+ return extractBranchDiff({
744
+ repo,
745
+ branch,
746
+ base
747
+ });
748
+ }
749
+ //#endregion
750
+ //#region ../lib/json-payload.mjs
751
+ function tryParseJson(s) {
752
+ try {
753
+ return JSON.parse(s);
754
+ } catch {
755
+ return null;
756
+ }
757
+ }
758
+ /** Extrae un objeto JSON del texto del modelo (fences, prosa alrededor, envelope). */
759
+ function decodePayload(text) {
760
+ const raw = String(text || "").trim();
761
+ if (!raw) throw new Error("Respuesta vacía.");
762
+ const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
763
+ if (fence) {
764
+ const parsed = tryParseJson(fence[1].trim());
765
+ if (parsed && typeof parsed === "object") return parsed;
766
+ }
767
+ const direct = tryParseJson(raw);
768
+ if (direct && typeof direct === "object") return direct;
769
+ const start = raw.indexOf("{");
770
+ const end = raw.lastIndexOf("}");
771
+ if (start >= 0 && end > start) {
772
+ const parsed = tryParseJson(raw.slice(start, end + 1));
773
+ if (parsed && typeof parsed === "object") return parsed;
774
+ }
775
+ throw new Error("No se pudo interpretar la respuesta como JSON.");
776
+ }
777
+ //#endregion
778
+ //#region ../lib/validate.mjs
779
+ var GROUP_KINDS = [
780
+ "feat",
781
+ "fix",
782
+ "refactor",
783
+ "perf",
784
+ "test",
785
+ "chore",
786
+ "docs",
787
+ "infra"
788
+ ];
789
+ var BLOCK_OPS = [
790
+ "add",
791
+ "mod",
792
+ "del",
793
+ "rename"
794
+ ];
795
+ var WHY_SOURCES = [
796
+ "code",
797
+ "commit",
798
+ "pr",
799
+ "issue",
800
+ "inferred"
801
+ ];
802
+ var FINDING_CLASSES = ["risk", "quality"];
803
+ var FINDING_SEVERITIES = [
804
+ "high",
805
+ "med",
806
+ "low",
807
+ "nit"
808
+ ];
809
+ var FINDING_KINDS = [
810
+ "bug",
811
+ "race",
812
+ "auth",
813
+ "security",
814
+ "api-break",
815
+ "missing-test",
816
+ "perf",
817
+ "maintainability",
818
+ "docs",
819
+ "other"
820
+ ];
821
+ var SKIP_REASONS = [
822
+ "generated",
823
+ "lockfile",
824
+ "format",
825
+ "vendored",
826
+ "binary",
827
+ "trivial",
828
+ "ignored"
829
+ ];
830
+ /** Lo que tiene que devolver el modelo en un turno. El resto lo agrega el pipeline. */
831
+ var REVIEW_SCHEMA = {
832
+ type: "object",
833
+ required: [
834
+ "meta",
835
+ "intent",
836
+ "groups",
837
+ "blocks"
838
+ ],
839
+ properties: {
840
+ meta: {
841
+ type: "object",
842
+ required: ["agent", "model"],
843
+ additionalProperties: true,
844
+ properties: {
845
+ agent: {
846
+ type: "string",
847
+ minLength: 1
848
+ },
849
+ model: {
850
+ type: "string",
851
+ minLength: 1
852
+ }
853
+ }
854
+ },
855
+ intent: {
856
+ type: "string",
857
+ minLength: 40
858
+ },
859
+ groups: {
860
+ type: "array",
861
+ minItems: 1,
862
+ items: {
863
+ type: "object",
864
+ required: [
865
+ "id",
866
+ "kind",
867
+ "title",
868
+ "intent"
869
+ ],
870
+ additionalProperties: true,
871
+ properties: {
872
+ id: {
873
+ type: "string",
874
+ minLength: 1
875
+ },
876
+ kind: {
877
+ type: "string",
878
+ enum: GROUP_KINDS
879
+ },
880
+ title: {
881
+ type: "string",
882
+ minLength: 1
883
+ },
884
+ intent: { type: "string" }
885
+ }
886
+ }
887
+ },
888
+ blocks: {
889
+ type: "array",
890
+ minItems: 1,
891
+ items: {
892
+ type: "object",
893
+ required: [
894
+ "id",
895
+ "group",
896
+ "file",
897
+ "lines",
898
+ "op",
899
+ "what"
900
+ ],
901
+ additionalProperties: true,
902
+ properties: {
903
+ id: {
904
+ type: "string",
905
+ minLength: 1
906
+ },
907
+ group: {
908
+ type: "string",
909
+ minLength: 1
910
+ },
911
+ file: {
912
+ type: "string",
913
+ minLength: 1
914
+ },
915
+ lines: {
916
+ type: "string",
917
+ minLength: 1
918
+ },
919
+ op: {
920
+ type: "string",
921
+ enum: BLOCK_OPS
922
+ },
923
+ what: {
924
+ type: "string",
925
+ minLength: 1
926
+ },
927
+ why: { type: "string" },
928
+ source: {
929
+ type: "string",
930
+ enum: WHY_SOURCES
931
+ }
932
+ }
933
+ }
934
+ },
935
+ findings: {
936
+ type: "array",
937
+ items: {
938
+ type: "object",
939
+ required: [
940
+ "class",
941
+ "severity",
942
+ "kind",
943
+ "file",
944
+ "what"
945
+ ],
946
+ additionalProperties: true,
947
+ properties: {
948
+ id: { type: "string" },
949
+ class: {
950
+ type: "string",
951
+ enum: FINDING_CLASSES
952
+ },
953
+ severity: {
954
+ type: "string",
955
+ enum: FINDING_SEVERITIES
956
+ },
957
+ blocking: { type: "boolean" },
958
+ kind: {
959
+ type: "string",
960
+ enum: FINDING_KINDS
961
+ },
962
+ file: {
963
+ type: "string",
964
+ minLength: 1
965
+ },
966
+ line: { type: ["integer", "null"] },
967
+ block: { type: "string" },
968
+ what: {
969
+ type: "string",
970
+ minLength: 1
971
+ },
972
+ fix: { type: "string" }
973
+ }
974
+ }
975
+ },
976
+ skipped: {
977
+ type: "array",
978
+ items: {
979
+ type: "object",
980
+ required: ["file", "reason"],
981
+ additionalProperties: true,
982
+ properties: {
983
+ file: {
984
+ type: "string",
985
+ minLength: 1
986
+ },
987
+ reason: {
988
+ type: "string",
989
+ enum: SKIP_REASONS
990
+ }
991
+ }
992
+ }
993
+ }
994
+ }
995
+ };
996
+ function typeOf(value) {
997
+ if (value === null) return "null";
998
+ if (Array.isArray(value)) return "array";
999
+ if (typeof value === "number" && Number.isInteger(value)) return "integer";
1000
+ return typeof value;
1001
+ }
1002
+ function typeMatches(expected, actual) {
1003
+ const options = Array.isArray(expected) ? expected : [expected];
1004
+ if (options.includes(actual)) return true;
1005
+ return options.includes("number") && actual === "integer";
1006
+ }
1007
+ /** Minimal JSON Schema checker (type, required, properties, items, enum, minLength, minItems). */
1008
+ function validateAgainst(schema, data, path = "$") {
1009
+ const errors = [];
1010
+ if (!schema || typeof schema !== "object") return errors;
1011
+ const actual = typeOf(data);
1012
+ if (schema.type && !typeMatches(schema.type, actual)) {
1013
+ errors.push(`${path}: se esperaba ${[].concat(schema.type).join("|")}, llegó ${actual}`);
1014
+ return errors;
1015
+ }
1016
+ if (actual === "string" && schema.minLength && data.length < schema.minLength) errors.push(`${path}: texto demasiado corto (mínimo ${schema.minLength})`);
1017
+ if (schema.enum && (actual === "string" || actual === "number") && !schema.enum.includes(data)) errors.push(`${path}: valor no permitido (${data}); usá uno de: ${schema.enum.join(", ")}`);
1018
+ if (actual === "array") {
1019
+ if (schema.minItems && data.length < schema.minItems) errors.push(`${path}: se esperaban al menos ${schema.minItems} ítems`);
1020
+ if (schema.items) data.forEach((item, i) => errors.push(...validateAgainst(schema.items, item, `${path}[${i}]`)));
1021
+ }
1022
+ if (actual === "object") {
1023
+ for (const key of schema.required || []) if (data[key] === void 0) errors.push(`${path}: falta ${key}`);
1024
+ for (const [key, child] of Object.entries(schema.properties || {})) {
1025
+ if (data[key] === void 0) continue;
1026
+ errors.push(...validateAgainst(child, data[key], `${path}.${key}`));
1027
+ }
1028
+ }
1029
+ return errors;
1030
+ }
1031
+ /**
1032
+ * Integridad referencial y cobertura. El schema solo mira formas; esto mira que
1033
+ * los ids cierren y que no haya quedado un archivo sin explicar, que es el
1034
+ * error que de verdad arruina la review.
1035
+ */
1036
+ function reviewIssues(payload, knownPaths = []) {
1037
+ const errors = [];
1038
+ const meta = payload?.meta && typeof payload.meta === "object" ? payload.meta : {};
1039
+ const agentName = typeof meta.agent === "string" ? meta.agent.trim() : "";
1040
+ const modelId = typeof meta.model === "string" ? meta.model.trim() : "";
1041
+ if (!agentName || agentName.startsWith("<")) errors.push("meta.agent es obligatorio: nombre del producto (Claude Code, Cursor, Codex, …), sin placeholder");
1042
+ if (!modelId || modelId.startsWith("<")) errors.push("meta.model es obligatorio: id exacto del modelo, sin placeholder");
1043
+ const groups = Array.isArray(payload?.groups) ? payload.groups : [];
1044
+ const blocks = Array.isArray(payload?.blocks) ? payload.blocks : [];
1045
+ const findings = Array.isArray(payload?.findings) ? payload.findings : [];
1046
+ const skipped = Array.isArray(payload?.skipped) ? payload.skipped : [];
1047
+ const groupIds = new Set(groups.map((g) => g.id));
1048
+ const blockIds = new Set(blocks.map((b) => b.id));
1049
+ const known = new Set(knownPaths);
1050
+ const dupGroups = groups.length - groupIds.size;
1051
+ if (dupGroups > 0) errors.push(`hay ${dupGroups} id de tema repetido`);
1052
+ const dupBlocks = blocks.length - blockIds.size;
1053
+ if (dupBlocks > 0) errors.push(`hay ${dupBlocks} id de bloque repetido`);
1054
+ for (const block of blocks) {
1055
+ if (!groupIds.has(block.group)) errors.push(`el bloque ${block.id} apunta al tema inexistente ${block.group}`);
1056
+ if (known.size && !known.has(block.file)) errors.push(`el bloque ${block.id} apunta a un archivo que no está en el diff: ${block.file}`);
1057
+ }
1058
+ const usedGroups = new Set(blocks.map((b) => b.group));
1059
+ for (const group of groups) if (!usedGroups.has(group.id)) errors.push(`el tema ${group.id} no tiene ningún bloque`);
1060
+ for (const finding of findings) {
1061
+ if (finding.block && !blockIds.has(finding.block)) errors.push(`el hallazgo ${finding.id || finding.what} apunta al bloque inexistente ${finding.block}`);
1062
+ if (known.size && !known.has(finding.file)) errors.push(`el hallazgo ${finding.id || finding.what} apunta a un archivo fuera del diff: ${finding.file}`);
1063
+ }
1064
+ if (known.size) {
1065
+ const covered = /* @__PURE__ */ new Set([...blocks.map((b) => b.file), ...skipped.map((s) => s.file)]);
1066
+ const missing = [...known].filter((path) => !covered.has(path));
1067
+ if (missing.length) errors.push(`quedaron ${missing.length} archivo(s) sin bloque ni skipped: ${missing.slice(0, 8).join(", ")}`);
1068
+ }
1069
+ return errors;
1070
+ }
1071
+
1072
+ export { globMatch as A, BLOCK_OPS as B, writeDiffText as C, FINDING_CLASSES as F, GROUP_KINDS as G, OUTPUT_DIR as O, REVIEW_SCHEMA as R, SKIP_REASONS as S, WHY_SOURCES as W, assertGitRepo as a, branchTips as b, locateReviewFile as c, decodePayload as d, ensureReviewDir as e, fingerprint as f, getRemoteUrl as g, companionPaths as h, isOutputFilename as i, loadBranchDiff as j, git as k, listBranches as l, makeOutputFilename as m, FINDING_SEVERITIES as n, FINDING_KINDS as o, readDiffCache as p, extractBranchDiff as q, reviewDir as r, showFileLines as s, tryResolveGitRoot as t, loadReviewConfig as u, outputRelPath as v, writeDiffCache as w, writeSessionRef as x, validateAgainst as y, reviewIssues as z };
1073
+ //# sourceMappingURL=validate-BsTKyhsq.js.map