@codraoss/core 0.9.4

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 (80) hide show
  1. package/LICENSE +625 -0
  2. package/README.md +15 -0
  3. package/dist/chunk-FGXA7JNR.js +57 -0
  4. package/dist/chunk-FGXA7JNR.js.map +1 -0
  5. package/dist/chunk-ILZUCIZH.js +117 -0
  6. package/dist/chunk-ILZUCIZH.js.map +1 -0
  7. package/dist/chunk-JC74XH7Q.js +67 -0
  8. package/dist/chunk-JC74XH7Q.js.map +1 -0
  9. package/dist/chunk-MMVQPGK3.js +341 -0
  10. package/dist/chunk-MMVQPGK3.js.map +1 -0
  11. package/dist/chunk-NSTV4TRP.js +39 -0
  12. package/dist/chunk-NSTV4TRP.js.map +1 -0
  13. package/dist/chunk-OIQRTDYR.js +337 -0
  14. package/dist/chunk-OIQRTDYR.js.map +1 -0
  15. package/dist/chunk-PVXFURDR.js +90 -0
  16. package/dist/chunk-PVXFURDR.js.map +1 -0
  17. package/dist/chunk-R2HRWFRH.js +108 -0
  18. package/dist/chunk-R2HRWFRH.js.map +1 -0
  19. package/dist/chunk-RQ5S67XB.js +82 -0
  20. package/dist/chunk-RQ5S67XB.js.map +1 -0
  21. package/dist/chunk-U3VKVDZ7.js +266 -0
  22. package/dist/chunk-U3VKVDZ7.js.map +1 -0
  23. package/dist/chunk-V3RF6GQW.js +137 -0
  24. package/dist/chunk-V3RF6GQW.js.map +1 -0
  25. package/dist/chunk-WSK45HW4.js +79 -0
  26. package/dist/chunk-WSK45HW4.js.map +1 -0
  27. package/dist/chunk-XD6CGYHO.js +893 -0
  28. package/dist/chunk-XD6CGYHO.js.map +1 -0
  29. package/dist/claim-checks.d.ts +54 -0
  30. package/dist/claim-checks.js +20 -0
  31. package/dist/claim-checks.js.map +1 -0
  32. package/dist/diff/index.d.ts +22 -0
  33. package/dist/diff/index.js +25 -0
  34. package/dist/diff/index.js.map +1 -0
  35. package/dist/fingerprint.d.ts +9 -0
  36. package/dist/fingerprint.js +19 -0
  37. package/dist/fingerprint.js.map +1 -0
  38. package/dist/index-BTDWXDck.d.ts +93 -0
  39. package/dist/index.d.ts +138 -0
  40. package/dist/index.js +1755 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/logger.d.ts +32 -0
  43. package/dist/logger.js +17 -0
  44. package/dist/logger.js.map +1 -0
  45. package/dist/model-gRDTk8yk.d.ts +117 -0
  46. package/dist/model-output/index.d.ts +4 -0
  47. package/dist/model-output/index.js +32 -0
  48. package/dist/model-output/index.js.map +1 -0
  49. package/dist/ports/index.d.ts +512 -0
  50. package/dist/ports/index.js +13 -0
  51. package/dist/ports/index.js.map +1 -0
  52. package/dist/position-h_jdn6ZH.d.ts +30 -0
  53. package/dist/prompts/file-review.d.ts +6 -0
  54. package/dist/prompts/file-review.js +25 -0
  55. package/dist/prompts/file-review.js.map +1 -0
  56. package/dist/prompts/languages.d.ts +9 -0
  57. package/dist/prompts/languages.js +7 -0
  58. package/dist/prompts/languages.js.map +1 -0
  59. package/dist/prompts/summary.d.ts +12 -0
  60. package/dist/prompts/summary.js +50 -0
  61. package/dist/prompts/summary.js.map +1 -0
  62. package/dist/prompts/verify.d.ts +71 -0
  63. package/dist/prompts/verify.js +15 -0
  64. package/dist/prompts/verify.js.map +1 -0
  65. package/dist/rules/detect.d.ts +32 -0
  66. package/dist/rules/detect.js +13 -0
  67. package/dist/rules/detect.js.map +1 -0
  68. package/dist/rules/table.d.ts +18 -0
  69. package/dist/rules/table.js +7 -0
  70. package/dist/rules/table.js.map +1 -0
  71. package/dist/timeout.d.ts +6 -0
  72. package/dist/timeout.js +26 -0
  73. package/dist/timeout.js.map +1 -0
  74. package/dist/token-tracker.d.ts +38 -0
  75. package/dist/token-tracker.js +8 -0
  76. package/dist/token-tracker.js.map +1 -0
  77. package/dist/verify.d.ts +3 -0
  78. package/dist/verify.js +21 -0
  79. package/dist/verify.js.map +1 -0
  80. package/package.json +181 -0
@@ -0,0 +1,893 @@
1
+ import {
2
+ generatorFindingCap
3
+ } from "./chunk-MMVQPGK3.js";
4
+ import {
5
+ renderDiffSnippet
6
+ } from "./chunk-V3RF6GQW.js";
7
+ import {
8
+ MAX_LOGGED_JSON_CHARS,
9
+ MIN_DISCRIMINATING_EVIDENCE_CHARS,
10
+ NON_ANSWER_MAX_RESPONSE_CHARS,
11
+ NON_ANSWER_MIN_DIFF_LINES,
12
+ SEVERITY_ORDER
13
+ } from "./chunk-FGXA7JNR.js";
14
+ import {
15
+ buildPresenceIndex,
16
+ checkAbsenceClaim,
17
+ isVersionClaimRefutedByPin,
18
+ looksLikeExternalVersionClaim,
19
+ refuteUndecidableClaim
20
+ } from "./chunk-U3VKVDZ7.js";
21
+ import {
22
+ buildAnchorHash,
23
+ buildFindingFingerprint,
24
+ buildFindingFingerprintV2,
25
+ foldEvidenceText,
26
+ normalizeFindingTitle
27
+ } from "./chunk-NSTV4TRP.js";
28
+ import {
29
+ logger
30
+ } from "./chunk-WSK45HW4.js";
31
+ import {
32
+ findPositionForLine,
33
+ getValidPositions
34
+ } from "./chunk-OIQRTDYR.js";
35
+
36
+ // src/model-output/index.ts
37
+ import {
38
+ parsedReviewCommentSchema,
39
+ toClaimType,
40
+ CLAIM_TYPE_CATEGORY
41
+ } from "@codraoss/schema";
42
+ import { z as z3 } from "zod";
43
+
44
+ // src/model-output/json.ts
45
+ import { fileReviewModelOutputSchema } from "@codraoss/schema";
46
+ import { jsonrepair } from "jsonrepair";
47
+ import "zod";
48
+ function truncateJsonForLog(value) {
49
+ if (value.length <= MAX_LOGGED_JSON_CHARS) return value;
50
+ return `${value.slice(0, MAX_LOGGED_JSON_CHARS)}... [truncated ${value.length - MAX_LOGGED_JSON_CHARS} chars]`;
51
+ }
52
+ function hasReviewKeys(input) {
53
+ return /"(files|findings|overall_explanation|overall_correctness|overall_confidence_score|summary)"\s*:/.test(input);
54
+ }
55
+ function scanBalanced(raw, startIdx, open, close) {
56
+ let stack = 0;
57
+ let inString = false;
58
+ let escape = false;
59
+ for (let i = startIdx; i < raw.length; i++) {
60
+ const char = raw[i];
61
+ if (escape) {
62
+ escape = false;
63
+ continue;
64
+ }
65
+ if (char === "\\") {
66
+ escape = true;
67
+ continue;
68
+ }
69
+ if (char === '"') {
70
+ inString = !inString;
71
+ continue;
72
+ }
73
+ if (inString) continue;
74
+ if (char === open) stack++;
75
+ else if (char === close) {
76
+ stack--;
77
+ if (stack === 0) return raw.slice(startIdx, i + 1);
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ function extractJson(raw, anchorKey = "findings") {
83
+ const jsonBlocks = Array.from(raw.matchAll(/```json([\s\S]*?)```/gi));
84
+ if (jsonBlocks.length > 0) {
85
+ return jsonBlocks[jsonBlocks.length - 1][1].trim();
86
+ }
87
+ const genericBlocks = Array.from(raw.matchAll(/```(?:[\w+-]+)?([\s\S]*?)```/gi));
88
+ if (genericBlocks.length > 0) {
89
+ const candidates = genericBlocks.filter((b) => b[1].includes("{") && b[1].includes("}") && hasReviewKeys(b[1]));
90
+ if (candidates.length > 0) {
91
+ const content = candidates[candidates.length - 1][1].trim();
92
+ const start = content.indexOf("{");
93
+ const end = content.lastIndexOf("}");
94
+ if (start !== -1 && end !== -1 && end > start) {
95
+ return content.slice(start, end + 1);
96
+ }
97
+ return content;
98
+ }
99
+ }
100
+ const arrayStart = raw.indexOf("[");
101
+ if (arrayStart !== -1 && raw.slice(0, arrayStart).trim() === "") {
102
+ const matched = scanBalanced(raw, arrayStart, "[", "]");
103
+ if (matched && hasReviewKeys(matched)) return matched;
104
+ }
105
+ const anchorIdx = anchorKey === "files" ? raw.indexOf('"files"') : -1;
106
+ const findingsIdx = anchorIdx !== -1 ? anchorIdx : raw.indexOf('"findings"');
107
+ const summaryIdx = raw.indexOf('"summary"');
108
+ const targetIdx = findingsIdx !== -1 ? findingsIdx : summaryIdx !== -1 ? summaryIdx : -1;
109
+ let firstBrace = -1;
110
+ if (targetIdx !== -1) {
111
+ firstBrace = raw.lastIndexOf("{", targetIdx);
112
+ }
113
+ if (firstBrace === -1) {
114
+ const allBraces = Array.from(raw.matchAll(/\{/g));
115
+ let bestIdx = -1;
116
+ let bestScore = -1;
117
+ for (const match of allBraces) {
118
+ const idx = match.index;
119
+ const excerpt = raw.slice(idx, idx + 200);
120
+ let score = 0;
121
+ if (excerpt.includes('"files"')) score += 100;
122
+ if (excerpt.includes('"findings"')) score += 100;
123
+ if (excerpt.includes('"summary"')) score += 50;
124
+ if (excerpt.includes('"overall_explanation"')) score += 50;
125
+ if (excerpt.includes('" : ') || excerpt.includes('":')) score += 10;
126
+ if (excerpt.includes('"[')) score += 5;
127
+ if (excerpt.includes(": number;") || excerpt.includes(": string;")) score -= 80;
128
+ if (excerpt.includes("export ") || excerpt.includes("function ")) score -= 80;
129
+ if (excerpt.includes("interface ") || excerpt.includes("type ")) score -= 80;
130
+ if (excerpt.includes(" + ")) score -= 20;
131
+ if (score > bestScore) {
132
+ bestScore = score;
133
+ bestIdx = idx;
134
+ }
135
+ }
136
+ if (bestIdx !== -1 && bestScore > 0) {
137
+ firstBrace = bestIdx;
138
+ }
139
+ }
140
+ if (firstBrace === -1) {
141
+ const start = raw.indexOf("{");
142
+ if (start !== -1) {
143
+ const excerpt = raw.slice(start, start + 50);
144
+ if (excerpt.includes('"') && excerpt.includes(":")) {
145
+ firstBrace = start;
146
+ }
147
+ }
148
+ }
149
+ if (firstBrace !== -1) {
150
+ let stack = 0;
151
+ let inString = false;
152
+ let escape = false;
153
+ for (let i = firstBrace; i < raw.length; i++) {
154
+ const char = raw[i];
155
+ if (escape) {
156
+ escape = false;
157
+ continue;
158
+ }
159
+ if (char === "\\") {
160
+ escape = true;
161
+ continue;
162
+ }
163
+ if (char === '"') {
164
+ inString = !inString;
165
+ continue;
166
+ }
167
+ if (!inString) {
168
+ if (char === "{") stack++;
169
+ else if (char === "}") {
170
+ stack--;
171
+ if (stack === 0) {
172
+ return raw.slice(firstBrace, i + 1);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ const partial = raw.slice(firstBrace).trim();
178
+ let closing = "";
179
+ if (inString) closing += '"';
180
+ closing += "}".repeat(Math.max(1, stack));
181
+ return `${partial}${closing}`;
182
+ }
183
+ return raw.trim();
184
+ }
185
+ function preprocessJson(json) {
186
+ let result = "";
187
+ let inString = false;
188
+ let escape = false;
189
+ for (let i = 0; i < json.length; i++) {
190
+ const char = json[i];
191
+ if (escape) {
192
+ result += char;
193
+ escape = false;
194
+ continue;
195
+ }
196
+ if (char === "\\") {
197
+ result += char;
198
+ escape = true;
199
+ continue;
200
+ }
201
+ if (char === '"') {
202
+ inString = !inString;
203
+ result += char;
204
+ continue;
205
+ }
206
+ if (inString) {
207
+ if (char === "\n") {
208
+ result += "\\n";
209
+ } else if (char === "\r") {
210
+ result += "\\r";
211
+ } else {
212
+ result += char;
213
+ }
214
+ } else {
215
+ result += char;
216
+ }
217
+ }
218
+ return result;
219
+ }
220
+ function stripNulls(value) {
221
+ if (Array.isArray(value)) return value.map(stripNulls);
222
+ if (value === null || typeof value !== "object") return value;
223
+ const out = {};
224
+ for (const [key, entry] of Object.entries(value)) {
225
+ if (entry === null) continue;
226
+ out[key] = stripNulls(entry);
227
+ }
228
+ return out;
229
+ }
230
+ function isPlaceholderString(value) {
231
+ return typeof value === "string" && /^<[^>]+>$/.test(value.trim());
232
+ }
233
+ function coerceReviewNumber(value) {
234
+ if (typeof value === "number" && Number.isFinite(value)) return value;
235
+ if (typeof value === "string" && !isPlaceholderString(value)) {
236
+ const parsed = Number(value);
237
+ if (Number.isFinite(parsed)) return parsed;
238
+ }
239
+ return void 0;
240
+ }
241
+ function normalizeFinding(finding) {
242
+ if (!finding || typeof finding !== "object") return null;
243
+ const f = finding;
244
+ if (isPlaceholderString(f.title) || isPlaceholderString(f.body) || isPlaceholderString(f.evidence)) return null;
245
+ const location = f.code_location && typeof f.code_location === "object" ? f.code_location : {};
246
+ const line = coerceReviewNumber(location.line);
247
+ const start = coerceReviewNumber(location.line_range && typeof location.line_range === "object" ? location.line_range.start : void 0);
248
+ const end = coerceReviewNumber(location.line_range && typeof location.line_range === "object" ? location.line_range.end : void 0);
249
+ const priority = coerceReviewNumber(f.priority);
250
+ const codeLocation = {
251
+ absolute_file_path: location.absolute_file_path || f.path || ""
252
+ };
253
+ if (line !== void 0) {
254
+ codeLocation.line = Math.trunc(line);
255
+ }
256
+ if (start !== void 0 || end !== void 0) {
257
+ codeLocation.line_range = {
258
+ start: Math.trunc(start ?? end),
259
+ end: Math.trunc(end ?? start)
260
+ };
261
+ }
262
+ return {
263
+ ...f,
264
+ title: (f.title ? String(f.title) : "").trim().slice(0, 100).replace(/[\uD800-\uDBFF]$/, "") || "Code finding",
265
+ priority: priority === void 0 ? void 0 : Math.max(0, Math.min(4, Math.trunc(priority))),
266
+ code_location: codeLocation,
267
+ confidence_score: typeof f.confidence_score === "number" ? Math.max(0, Math.min(1, f.confidence_score > 1 ? f.confidence_score / 10 : f.confidence_score)) : void 0
268
+ };
269
+ }
270
+ function parseRawPayload(raw) {
271
+ let extracted;
272
+ try {
273
+ extracted = extractJson(raw);
274
+ if (!hasReviewKeys(extracted)) {
275
+ throw new Error("Model response did not contain review JSON keys.");
276
+ }
277
+ } catch (e) {
278
+ logger.error("Failed to extract JSON from model response", {
279
+ rawLength: raw.length,
280
+ rawPrefix: raw.slice(0, 500),
281
+ error: e instanceof Error ? e.message : String(e)
282
+ });
283
+ throw new Error("Could not find JSON root in model response.", { cause: e });
284
+ }
285
+ let preprocessed;
286
+ try {
287
+ preprocessed = preprocessJson(extracted);
288
+ } catch (e) {
289
+ logger.warn("JSON preprocessing partially failed, continuing...", { extracted, error: e });
290
+ preprocessed = extracted;
291
+ }
292
+ let repaired = preprocessed;
293
+ try {
294
+ repaired = jsonrepair(preprocessed);
295
+ } catch (e) {
296
+ logger.warn("jsonrepair failed to fix model output, using preprocessed text", { preprocessed: truncateJsonForLog(preprocessed), error: e });
297
+ }
298
+ let parsedJson;
299
+ try {
300
+ parsedJson = stripNulls(JSON.parse(repaired));
301
+ } catch (e) {
302
+ logger.error("Critical JSON parse error after extraction and repair", { repaired: truncateJsonForLog(repaired), error: e });
303
+ throw new Error(`Invalid JSON format: ${e instanceof Error ? e.message : "Unknown error"}`, { cause: e });
304
+ }
305
+ try {
306
+ const findReviewObject = (arr) => {
307
+ const best = arr.find((i) => i && typeof i === "object" && Array.isArray(i.findings) && typeof i.summary === "string");
308
+ if (best) return best;
309
+ const good = arr.find((i) => i && typeof i === "object" && Array.isArray(i.findings));
310
+ if (good) return good;
311
+ return arr.find(
312
+ (i) => i && typeof i === "object" && ("findings" in i || "overall_explanation" in i || "summary" in i || "overall_correctness" in i)
313
+ );
314
+ };
315
+ let data = Array.isArray(parsedJson) ? findReviewObject(parsedJson) || parsedJson[0] || {} : parsedJson;
316
+ if (data && typeof data === "object") {
317
+ const obj = data;
318
+ if (!obj.findings) obj.findings = [];
319
+ if (!obj.overall_explanation) obj.overall_explanation = "No explanation provided.";
320
+ if (!obj.overall_correctness) obj.overall_correctness = "Uncertain";
321
+ if (typeof obj.overall_confidence_score === "number") {
322
+ if (obj.overall_confidence_score > 1) {
323
+ obj.overall_confidence_score = Math.min(obj.overall_confidence_score / 10, 1);
324
+ } else if (obj.overall_confidence_score < 0) {
325
+ obj.overall_confidence_score = 0;
326
+ }
327
+ } else {
328
+ obj.overall_confidence_score = 0.5;
329
+ }
330
+ if (Array.isArray(obj.findings)) {
331
+ obj.findings = obj.findings.flatMap((finding) => {
332
+ const normalized = normalizeFinding(finding);
333
+ return normalized ? [normalized] : [];
334
+ });
335
+ }
336
+ data = obj;
337
+ }
338
+ return fileReviewModelOutputSchema.parse(data);
339
+ } catch (e) {
340
+ logger.error("Model response failed schema validation", { parsedJson, error: e });
341
+ throw new Error(`Response schema mismatch: ${e instanceof Error ? e.message : "Check logs"}`, { cause: e });
342
+ }
343
+ }
344
+
345
+ // src/model-output/evidence.ts
346
+ function buildEvidenceIndex(file) {
347
+ const byContent = /* @__PURE__ */ new Map();
348
+ const lines = [];
349
+ for (const hunk of file.hunks) {
350
+ const postable = hunk.lines.filter((line) => line.kind !== "del" && line.newLineNumber !== void 0);
351
+ if (postable.length === 0) continue;
352
+ hunk.lines.forEach((line, lineIndex) => {
353
+ const normalized = foldEvidenceText(line.content);
354
+ if (!normalized) return;
355
+ let anchor = line;
356
+ if (line.kind === "del" || line.newLineNumber === void 0) {
357
+ anchor = hunk.lines.slice(lineIndex + 1).find((l) => l.kind !== "del" && l.newLineNumber !== void 0) ?? hunk.lines.slice(0, lineIndex).reverse().find((l) => l.kind !== "del" && l.newLineNumber !== void 0) ?? postable[0];
358
+ }
359
+ lines.push({ normalized, line: anchor });
360
+ const existing = byContent.get(normalized);
361
+ if (existing) existing.push(anchor);
362
+ else byContent.set(normalized, [anchor]);
363
+ });
364
+ }
365
+ return { byContent, lines };
366
+ }
367
+ function foldFirstEvidenceLine(evidence) {
368
+ if (typeof evidence !== "string") return null;
369
+ return evidence.split("\n").map(foldEvidenceText).find((l) => l.length > 0) ?? null;
370
+ }
371
+ function buildBinAmbiguityIndex(files) {
372
+ const filesPerLine = /* @__PURE__ */ new Map();
373
+ for (const file of files) {
374
+ for (const hunk of file.hunks) {
375
+ for (const line of hunk.lines) {
376
+ const normalized = foldEvidenceText(line.content);
377
+ if (normalized.length < MIN_DISCRIMINATING_EVIDENCE_CHARS) continue;
378
+ const paths = filesPerLine.get(normalized);
379
+ if (paths) paths.add(file.path);
380
+ else filesPerLine.set(normalized, /* @__PURE__ */ new Set([file.path]));
381
+ }
382
+ }
383
+ }
384
+ const index = /* @__PURE__ */ new Map();
385
+ for (const [normalized, paths] of filesPerLine) {
386
+ if (paths.size > 1) index.set(normalized, paths.size);
387
+ }
388
+ return index;
389
+ }
390
+ function resolveEvidence(evidence, index, reportedLine) {
391
+ if (typeof evidence !== "string") return { status: "absent" };
392
+ const firstLine = foldFirstEvidenceLine(evidence);
393
+ if (!firstLine) return { status: "absent" };
394
+ if (firstLine.length < MIN_DISCRIMINATING_EVIDENCE_CHARS) return { status: "weak" };
395
+ const nearest = (candidates) => {
396
+ if (reportedLine === void 0) return candidates[0];
397
+ return candidates.reduce(
398
+ (best, candidate) => Math.abs((candidate.newLineNumber ?? 0) - reportedLine) < Math.abs((best.newLineNumber ?? 0) - reportedLine) ? candidate : best
399
+ );
400
+ };
401
+ const exact = index.byContent.get(firstLine);
402
+ if (exact && exact.length > 0) return { status: "matched", line: nearest(exact) };
403
+ const contained = index.lines.flatMap(({ normalized, line }) => normalized.length >= MIN_DISCRIMINATING_EVIDENCE_CHARS && (normalized.includes(firstLine) || firstLine.includes(normalized)) ? [line] : []);
404
+ if (contained.length > 0) return { status: "matched", line: nearest(contained) };
405
+ return { status: "unmatched" };
406
+ }
407
+
408
+ // src/model-output/dedupe.ts
409
+ var SEVERITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3, nit: 4 };
410
+ var NUL = String.fromCharCode(0);
411
+ function dedupeFindings(comments) {
412
+ const best = /* @__PURE__ */ new Map();
413
+ for (const comment of comments) {
414
+ const key = comment.source === "rule" ? `rule${NUL}${comment.ruleId ?? ""}${NUL}${comment.path}${NUL}${comment.anchorHash ?? ""}` : normalizeFindingTitle(comment.title);
415
+ if (!key) {
416
+ best.set(`__unique__${best.size}`, comment);
417
+ continue;
418
+ }
419
+ const existing = best.get(key);
420
+ if (!existing) {
421
+ best.set(key, comment);
422
+ continue;
423
+ }
424
+ const rank = SEVERITY_RANK[comment.severity] ?? 4;
425
+ const existingRank = SEVERITY_RANK[existing.severity] ?? 4;
426
+ const isBetter = rank < existingRank || rank === existingRank && (comment.confidenceScore ?? 0) > (existing.confidenceScore ?? 0);
427
+ if (isBetter) best.set(key, comment);
428
+ }
429
+ return Array.from(best.values());
430
+ }
431
+
432
+ // src/model-output/non-answer.ts
433
+ function isNonAnswerReview(input) {
434
+ if (input.findingCount > 0) return false;
435
+ if (input.file.lineCount < (input.minDiffLines ?? NON_ANSWER_MIN_DIFF_LINES)) return false;
436
+ return input.rawText.trim().length < NON_ANSWER_MAX_RESPONSE_CHARS;
437
+ }
438
+
439
+ // src/model-output/json-batch.ts
440
+ import { batchReviewModelOutputSchema } from "@codraoss/schema";
441
+ import { jsonrepair as jsonrepair2 } from "jsonrepair";
442
+ import "zod";
443
+ function readEntryPath(entry) {
444
+ for (const key of ["absolute_file_path", "path", "file", "file_path", "filename"]) {
445
+ const value = entry[key];
446
+ if (typeof value === "string" && value.trim()) return value.trim();
447
+ }
448
+ return null;
449
+ }
450
+ function normalizeConfidence(value) {
451
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
452
+ if (value > 1) return Math.min(value / 10, 1);
453
+ if (value < 0) return 0;
454
+ return value;
455
+ }
456
+ function normalizeBatchFileEntry(entry, fallbackPath) {
457
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
458
+ const e = entry;
459
+ const path = readEntryPath(e) ?? fallbackPath;
460
+ if (!path) return null;
461
+ if (!Array.isArray(e.findings)) return null;
462
+ return {
463
+ absolute_file_path: path,
464
+ findings: e.findings.flatMap((finding) => {
465
+ const normalized = normalizeFinding(finding);
466
+ return normalized ? [normalized] : [];
467
+ }),
468
+ overall_correctness: typeof e.overall_correctness === "string" && e.overall_correctness ? e.overall_correctness : void 0,
469
+ overall_explanation: typeof e.overall_explanation === "string" && e.overall_explanation ? e.overall_explanation : void 0,
470
+ overall_confidence_score: normalizeConfidence(e.overall_confidence_score)
471
+ };
472
+ }
473
+ function collectBatchEntries(parsedJson) {
474
+ const root = parsedJson && typeof parsedJson === "object" ? parsedJson : null;
475
+ const files = root?.files ?? (Array.isArray(parsedJson) ? parsedJson : void 0);
476
+ if (Array.isArray(files)) {
477
+ return files.flatMap((entry) => {
478
+ const normalized = normalizeBatchFileEntry(entry);
479
+ return normalized ? [normalized] : [];
480
+ });
481
+ }
482
+ if (files && typeof files === "object") {
483
+ return Object.entries(files).flatMap(([path, entry]) => {
484
+ const normalized = normalizeBatchFileEntry(entry, path);
485
+ return normalized ? [normalized] : [];
486
+ });
487
+ }
488
+ return null;
489
+ }
490
+ function parseRawBatchPayload(raw) {
491
+ let extracted;
492
+ try {
493
+ extracted = extractJson(raw, "files");
494
+ if (!hasReviewKeys(extracted)) {
495
+ throw new Error("Model response did not contain review JSON keys.");
496
+ }
497
+ } catch (e) {
498
+ logger.error("Failed to extract JSON from batched model response", {
499
+ rawLength: raw.length,
500
+ rawPrefix: raw.slice(0, 500),
501
+ error: e instanceof Error ? e.message : String(e)
502
+ });
503
+ throw new Error("Could not find JSON root in batched model response.", { cause: e });
504
+ }
505
+ let preprocessed;
506
+ try {
507
+ preprocessed = preprocessJson(extracted);
508
+ } catch (e) {
509
+ logger.warn("JSON preprocessing partially failed, continuing...", { extracted, error: e });
510
+ preprocessed = extracted;
511
+ }
512
+ let repaired = preprocessed;
513
+ try {
514
+ repaired = jsonrepair2(preprocessed);
515
+ } catch (e) {
516
+ logger.warn("jsonrepair failed to fix batched model output, using preprocessed text", { preprocessed: truncateJsonForLog(preprocessed), error: e });
517
+ }
518
+ let parsedJson;
519
+ try {
520
+ parsedJson = stripNulls(JSON.parse(repaired));
521
+ } catch (e) {
522
+ logger.error("Critical JSON parse error after extraction and repair", { repaired: truncateJsonForLog(repaired), error: e });
523
+ throw new Error(`Invalid JSON format: ${e instanceof Error ? e.message : "Unknown error"}`, { cause: e });
524
+ }
525
+ const entries = collectBatchEntries(parsedJson);
526
+ const root = parsedJson && typeof parsedJson === "object" && !Array.isArray(parsedJson) ? parsedJson : {};
527
+ if (!entries?.length) {
528
+ if (Array.isArray(root.findings)) return { shape: "flat", data: parseRawPayload(raw) };
529
+ logger.error("Batched model response contained no recognisable file entries", {
530
+ parsedJson: truncateJsonForLog(JSON.stringify(parsedJson ?? null))
531
+ });
532
+ throw new Error("Batched response contained no recognisable file entries.");
533
+ }
534
+ try {
535
+ return {
536
+ shape: "nested",
537
+ data: batchReviewModelOutputSchema.parse({
538
+ files: entries,
539
+ overall_confidence_score: normalizeConfidence(root.overall_confidence_score) ?? 0.5
540
+ })
541
+ };
542
+ } catch (e) {
543
+ logger.error("Batched model response failed schema validation", { parsedJson, error: e });
544
+ throw new Error(`Batched response schema mismatch: ${e instanceof Error ? e.message : "Check logs"}`, { cause: e });
545
+ }
546
+ }
547
+
548
+ // src/model-output/batch.ts
549
+ var basename = (path) => path.split("/").pop() ?? path;
550
+ function resolveEntryPath(reported, candidates, claimed) {
551
+ const unclaimed = (matches) => matches.find((f) => !claimed.has(f.path)) ?? null;
552
+ const exact = candidates.filter((f) => samePath(f.path, reported));
553
+ const renamed = candidates.filter((f) => f.previousPath && samePath(f.previousPath, reported));
554
+ if (exact.length > 0 || renamed.length > 0) return unclaimed(exact) ?? unclaimed(renamed);
555
+ const stripped = reported.trim().replace(/^\.\//, "").replace(/^[ab]\//, "").replace(/^\//, "");
556
+ const suffixed = candidates.filter((f) => f.path.endsWith(`/${stripped}`));
557
+ if (suffixed.length === 1) return unclaimed(suffixed);
558
+ const named = candidates.filter((f) => basename(f.path) === basename(stripped));
559
+ return named.length === 1 ? unclaimed(named) : null;
560
+ }
561
+ function groundEntry(file, entry, deniedClaimTypes, ambiguity, confidenceScore, stats) {
562
+ for (const finding of entry.findings) {
563
+ const claimed = finding.code_location?.absolute_file_path?.trim();
564
+ if (claimed && !samePath(claimed, file.path)) stats.pathMismatchFindings += 1;
565
+ }
566
+ return groundParsedFindings(
567
+ { ...entry, findings: entry.findings, overall_confidence_score: confidenceScore },
568
+ file,
569
+ { deniedClaimTypes, ambiguity: { index: ambiguity.index, filePath: file.path, stats: ambiguity.stats } }
570
+ );
571
+ }
572
+ function trimOverCap(reviews, cap, stats) {
573
+ for (const [path, review] of reviews) {
574
+ if (review.comments.length <= cap) continue;
575
+ const ranked = review.comments.toSorted((a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity));
576
+ const dropped = ranked.slice(cap);
577
+ stats.overCap += dropped.length;
578
+ const header = "### Additional Comments (Off-diff)";
579
+ const bullets = dropped.map((c) => `- **[over-cap] ${c.title}:** ${c.body}`).join("\n");
580
+ reviews.set(path, {
581
+ ...review,
582
+ comments: ranked.slice(0, cap),
583
+ fileSummary: review.fileSummary.includes(header) ? `${review.fileSummary}
584
+ ${bullets}` : `${review.fileSummary}
585
+
586
+ ${header}
587
+ ${bullets}`
588
+ });
589
+ }
590
+ }
591
+ function parseBatchReviewResponse(raw, files, options) {
592
+ const stats = {
593
+ unroutableEntries: 0,
594
+ pathMismatchFindings: 0,
595
+ ambiguousAcrossBin: 0,
596
+ flatFallback: 0,
597
+ overCap: 0,
598
+ entriesReturned: 0
599
+ };
600
+ const payload = parseRawBatchPayload(raw);
601
+ const reviews = /* @__PURE__ */ new Map();
602
+ const ambiguity = { index: buildBinAmbiguityIndex(files), stats: { ambiguousAcrossBin: 0 } };
603
+ const claimed = /* @__PURE__ */ new Set();
604
+ const ground = (file, entry, confidence) => reviews.set(file.path, groundEntry(file, entry, options?.deniedClaimTypes, ambiguity, confidence, stats));
605
+ if (payload.shape === "flat") {
606
+ stats.flatFallback = 1;
607
+ stats.entriesReturned = 1;
608
+ const byFile = /* @__PURE__ */ new Map();
609
+ for (const finding of payload.data.findings) {
610
+ const reported = finding.code_location.absolute_file_path?.trim();
611
+ const target = !reported && files.length === 1 ? files[0] : resolveEntryPath(reported ?? "", files, claimed);
612
+ if (!target) {
613
+ stats.unroutableEntries += 1;
614
+ continue;
615
+ }
616
+ const bucket = byFile.get(target.path);
617
+ if (bucket) bucket.findings.push(finding);
618
+ else byFile.set(target.path, { file: target, findings: [finding] });
619
+ }
620
+ for (const { file, findings } of byFile.values()) {
621
+ ground(file, {
622
+ findings,
623
+ overall_correctness: payload.data.overall_correctness,
624
+ overall_explanation: payload.data.overall_explanation
625
+ }, payload.data.overall_confidence_score);
626
+ }
627
+ } else {
628
+ stats.entriesReturned = payload.data.files.length;
629
+ for (const entry of payload.data.files) {
630
+ const file = resolveEntryPath(entry.absolute_file_path, files, claimed);
631
+ if (!file) {
632
+ stats.unroutableEntries += 1;
633
+ logger.warn("Batched review returned an entry for an unknown path", { reported: entry.absolute_file_path });
634
+ continue;
635
+ }
636
+ claimed.add(file.path);
637
+ ground(file, entry, entry.overall_confidence_score ?? payload.data.overall_confidence_score);
638
+ }
639
+ }
640
+ stats.ambiguousAcrossBin = ambiguity.stats.ambiguousAcrossBin;
641
+ if (options?.maxCommentsPerFile) trimOverCap(reviews, generatorFindingCap(options.maxCommentsPerFile), stats);
642
+ return { reviews, missing: files.flatMap((f) => reviews.has(f.path) ? [] : [f.path]), stats };
643
+ }
644
+
645
+ // src/model-output/index.ts
646
+ function samePath(a, b) {
647
+ const strip = (p) => p.trim().replace(/^\.\//, "").replace(/^[ab]\//, "").replace(/^\//, "");
648
+ return strip(a) === strip(b);
649
+ }
650
+ function withSuggestion(body, codeSuggestion) {
651
+ if (!codeSuggestion) return body;
652
+ const cleanSuggestion = codeSuggestion.replace(/```suggestion\n?|```/g, "").trim();
653
+ const cleanBody = body.split("```suggestion")[0].trim();
654
+ return `${cleanBody}
655
+
656
+ \`\`\`suggestion
657
+ ${cleanSuggestion}
658
+ \`\`\``;
659
+ }
660
+ var CLAIM_TYPE_REPAIRS = [
661
+ { pattern: /dependenc(?:y|ies)\s+array|exhaustive[- ]deps/i, claimType: "react_hook_missing_deps" },
662
+ { pattern: /redos|catastrophic backtrack|exponential backtrack/i, claimType: "redos_regex" }
663
+ ];
664
+ function repairClaimType(claimType, title, body, onRepair) {
665
+ if (claimType !== "other") return claimType;
666
+ const text = `${title}
667
+ ${body}`;
668
+ if (looksLikeExternalVersionClaim(title, body)) {
669
+ onRepair();
670
+ return "external_version_claim";
671
+ }
672
+ for (const { pattern, claimType: repaired } of CLAIM_TYPE_REPAIRS) {
673
+ if (pattern.test(text)) {
674
+ onRepair();
675
+ return repaired;
676
+ }
677
+ }
678
+ return claimType;
679
+ }
680
+ function formatWithheld(w) {
681
+ return w.tag ? `- **[${w.tag}] ${w.title}:** ${w.body}` : `- **${w.title}:** ${w.body}`;
682
+ }
683
+ function groundFindingInEvidence(finding, evidenceIndex, evidenceStats, ambiguity) {
684
+ const reportedLine = finding.code_location.line || finding.code_location.line_range?.start;
685
+ evidenceStats.total += 1;
686
+ const evidence = resolveEvidence(finding.evidence, evidenceIndex, reportedLine);
687
+ if (evidence.status === "matched") evidenceStats.matched += 1;
688
+ else if (evidence.status === "unmatched") evidenceStats.unmatched += 1;
689
+ else if (evidence.status === "weak") evidenceStats.weak += 1;
690
+ else if (evidence.status === "absent") evidenceStats.absent += 1;
691
+ if (evidence.status !== "matched") {
692
+ return { withheld: { title: finding.title, body: finding.body, tag: `unverified:${evidence.status}` } };
693
+ }
694
+ if (ambiguity) {
695
+ const firstLine = foldFirstEvidenceLine(finding.evidence);
696
+ const claimedPath = finding.code_location.absolute_file_path?.trim();
697
+ const ambiguousAcrossBin = firstLine ? (ambiguity.index.get(firstLine) ?? 0) > 1 : false;
698
+ if (ambiguousAcrossBin && claimedPath && !samePath(claimedPath, ambiguity.filePath)) {
699
+ ambiguity.stats.ambiguousAcrossBin += 1;
700
+ return {
701
+ withheld: {
702
+ title: finding.title,
703
+ body: finding.body,
704
+ tag: "unverified:ambiguous-across-bin"
705
+ }
706
+ };
707
+ }
708
+ }
709
+ return { diffLine: evidence.line };
710
+ }
711
+ function anchorToDiffPosition(file, diffLine, validPositions, finding) {
712
+ const line = diffLine.newLineNumber;
713
+ const position = findPositionForLine(file, line);
714
+ if (position === void 0 || !validPositions.has(position)) {
715
+ return { withheld: { title: finding.title, body: finding.body } };
716
+ }
717
+ return { line, position };
718
+ }
719
+ function validateFindingShape(finding) {
720
+ const priorityMap = {
721
+ 0: "P0",
722
+ 1: "P1",
723
+ 2: "P2",
724
+ 3: "P3",
725
+ 4: "nit"
726
+ };
727
+ const severity = finding.priority !== void 0 ? priorityMap[finding.priority] || "P3" : "P3";
728
+ const cleanText = (text) => {
729
+ let current = text.trim();
730
+ let prev = "";
731
+ while (current !== prev) {
732
+ prev = current;
733
+ current = current.replace(/^(?:[^\w\s]+|(?:QUALITY|SECURITY|BUG|PERFORMANCE|CORRECTNESS|P[0-3]|NIT)\b)+/giu, "").replace(/\n\s*/g, " ").trim();
734
+ }
735
+ return current;
736
+ };
737
+ const title = cleanText(finding.title);
738
+ let body = cleanText(finding.body);
739
+ const bodyPrefix = cleanText(body.split("\n")[0]);
740
+ if (bodyPrefix.toLowerCase().startsWith(title.toLowerCase()) || title.toLowerCase().startsWith(bodyPrefix.toLowerCase())) {
741
+ body = cleanText(body.slice(body.split("\n")[0].length));
742
+ }
743
+ return { severity, title, body };
744
+ }
745
+ function applyClaimGate(finding, title, body, anchorContent, deniedClaimTypes, claimTypeCounts, deniedClaimCounts) {
746
+ const claimType = repairClaimType(toClaimType(finding.claim_type), title, body, () => {
747
+ claimTypeCounts.__repaired = (claimTypeCounts.__repaired ?? 0) + 1;
748
+ });
749
+ claimTypeCounts[claimType] = (claimTypeCounts[claimType] ?? 0) + 1;
750
+ if (deniedClaimTypes.has(claimType)) {
751
+ deniedClaimCounts[claimType] = (deniedClaimCounts[claimType] ?? 0) + 1;
752
+ return { withheld: { title, body, tag: `claim-denied:${claimType}` } };
753
+ }
754
+ if (isVersionClaimRefutedByPin({ title, body, anchorContent })) {
755
+ deniedClaimCounts.version_claim_on_pinned_sha = (deniedClaimCounts.version_claim_on_pinned_sha ?? 0) + 1;
756
+ return { withheld: { title, body, tag: "refuted:pinned-sha" } };
757
+ }
758
+ const undecidable = refuteUndecidableClaim({ title, body });
759
+ if (undecidable) {
760
+ const key = `undecidable_${undecidable.replace("-", "_")}`;
761
+ deniedClaimCounts[key] = (deniedClaimCounts[key] ?? 0) + 1;
762
+ return { withheld: { title, body, tag: `refuted:${undecidable}` } };
763
+ }
764
+ return { claimType };
765
+ }
766
+ function buildParsedComment(params) {
767
+ const { file, line, position, severity, title, body, claimType, anchorContent, finding, presenceIndex, absenceCheckStats } = params;
768
+ const absence = checkAbsenceClaim({ title, body, anchorLine: line, index: presenceIndex });
769
+ if (absence.status === "refuted") {
770
+ absenceCheckStats.absenceShaped += 1;
771
+ absenceCheckStats.identifierExtracted += 1;
772
+ absenceCheckStats.refuted += 1;
773
+ } else if (absence.reason !== "not_absence_shaped") {
774
+ absenceCheckStats.absenceShaped += 1;
775
+ if (absence.reason !== "no_identifier" && absence.reason !== "ambiguous_identifier") {
776
+ absenceCheckStats.identifierExtracted += 1;
777
+ }
778
+ }
779
+ const confidenceScore = typeof finding.confidence_score === "number" ? finding.confidence_score : 0;
780
+ const codeSuggestion = typeof finding.code_suggestion === "string" && finding.code_suggestion.trim() ? finding.code_suggestion : void 0;
781
+ return parsedReviewCommentSchema.parse({
782
+ path: file.path,
783
+ line,
784
+ position,
785
+ severity,
786
+ category: CLAIM_TYPE_CATEGORY[claimType],
787
+ claimType,
788
+ contextSnippet: renderDiffSnippet(file, line) || void 0,
789
+ title,
790
+ body: withSuggestion(body, codeSuggestion),
791
+ codeSuggestion,
792
+ confidenceScore,
793
+ evidence: typeof finding.evidence === "string" && finding.evidence.trim() ? finding.evidence.trim() : void 0,
794
+ fingerprint: buildFindingFingerprint(file.path, title),
795
+ anchorHash: anchorContent ? buildAnchorHash(anchorContent) : void 0,
796
+ fingerprintV2: buildFindingFingerprintV2(
797
+ file.path,
798
+ claimType,
799
+ anchorContent ? buildAnchorHash(anchorContent) : void 0
800
+ ) ?? void 0
801
+ });
802
+ }
803
+ function groundParsedFindings(parsed, file, options) {
804
+ const validPositions = getValidPositions(file);
805
+ const evidenceIndex = buildEvidenceIndex(file);
806
+ const evidenceStats = { total: 0, matched: 0, unmatched: 0, weak: 0, absent: 0 };
807
+ const claimTypeCounts = {};
808
+ const deniedClaimCounts = {};
809
+ const deniedClaimTypes = new Set(options?.deniedClaimTypes ?? []);
810
+ const presenceIndex = buildPresenceIndex(file);
811
+ const absenceCheckStats = { absenceShaped: 0, identifierExtracted: 0, refuted: 0 };
812
+ const orphanedComments = [];
813
+ const comments = (parsed.findings || []).map((finding) => {
814
+ const grounded = groundFindingInEvidence(finding, evidenceIndex, evidenceStats, options?.ambiguity);
815
+ if ("withheld" in grounded) {
816
+ orphanedComments.push(formatWithheld(grounded.withheld));
817
+ return null;
818
+ }
819
+ const anchored = anchorToDiffPosition(file, grounded.diffLine, validPositions, finding);
820
+ if ("withheld" in anchored) {
821
+ orphanedComments.push(formatWithheld(anchored.withheld));
822
+ return null;
823
+ }
824
+ const { severity, title, body } = validateFindingShape(finding);
825
+ const anchorContent = grounded.diffLine.content ?? file.hunks.flatMap((h) => h.lines).find((l) => l.newLineNumber === anchored.line)?.content ?? "";
826
+ const gated = applyClaimGate(finding, title, body, anchorContent, deniedClaimTypes, claimTypeCounts, deniedClaimCounts);
827
+ if ("withheld" in gated) {
828
+ orphanedComments.push(formatWithheld(gated.withheld));
829
+ return null;
830
+ }
831
+ try {
832
+ return buildParsedComment({
833
+ file,
834
+ line: anchored.line,
835
+ position: anchored.position,
836
+ severity,
837
+ title,
838
+ body,
839
+ claimType: gated.claimType,
840
+ anchorContent,
841
+ finding,
842
+ presenceIndex,
843
+ absenceCheckStats
844
+ });
845
+ } catch (error) {
846
+ if (!(error instanceof z3.ZodError)) throw error;
847
+ orphanedComments.push(formatWithheld({
848
+ title: finding.title,
849
+ body: finding.body,
850
+ tag: "unverified:unassemblable"
851
+ }));
852
+ logger.warn("Dropped a finding that could not be assembled", {
853
+ path: file.path,
854
+ title: finding.title,
855
+ error: error.message
856
+ });
857
+ return null;
858
+ }
859
+ }).filter((comment) => Boolean(comment));
860
+ const verdict = parsed.overall_correctness.toLowerCase().includes("patch is correct") ? "approve" : "comment";
861
+ let fileSummary = parsed.overall_explanation;
862
+ if (orphanedComments.length > 0) {
863
+ fileSummary += `
864
+
865
+ ### Additional Comments (Off-diff)
866
+ ${orphanedComments.join("\n")}`;
867
+ }
868
+ return {
869
+ comments,
870
+ verdict: comments.length > 0 ? "comment" : verdict,
871
+ fileSummary,
872
+ overallCorrectness: parsed.overall_correctness,
873
+ confidenceScore: parsed.overall_confidence_score,
874
+ evidenceStats,
875
+ claimTypeCounts,
876
+ deniedClaimCounts,
877
+ absenceCheckStats
878
+ };
879
+ }
880
+ function parseFileReviewResponse(raw, file, options) {
881
+ return groundParsedFindings(parseRawPayload(raw), file, options);
882
+ }
883
+
884
+ export {
885
+ dedupeFindings,
886
+ isNonAnswerReview,
887
+ parseRawBatchPayload,
888
+ parseBatchReviewResponse,
889
+ samePath,
890
+ groundParsedFindings,
891
+ parseFileReviewResponse
892
+ };
893
+ //# sourceMappingURL=chunk-XD6CGYHO.js.map