@codraoss/core 0.9.4 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/{chunk-XD6CGYHO.js → chunk-4XEMXRNG.js} +62 -39
  2. package/dist/chunk-4XEMXRNG.js.map +1 -0
  3. package/dist/{chunk-ILZUCIZH.js → chunk-EQL5OOEF.js} +7 -7
  4. package/dist/chunk-EQL5OOEF.js.map +1 -0
  5. package/dist/{chunk-RQ5S67XB.js → chunk-FKRCBUWZ.js} +8 -2
  6. package/dist/chunk-FKRCBUWZ.js.map +1 -0
  7. package/dist/{chunk-JC74XH7Q.js → chunk-GDX2ZEBA.js} +1 -1
  8. package/dist/chunk-GDX2ZEBA.js.map +1 -0
  9. package/dist/{chunk-OIQRTDYR.js → chunk-I5S3NBFL.js} +20 -4
  10. package/dist/chunk-I5S3NBFL.js.map +1 -0
  11. package/dist/{chunk-U3VKVDZ7.js → chunk-IKVUW4WC.js} +92 -19
  12. package/dist/chunk-IKVUW4WC.js.map +1 -0
  13. package/dist/{chunk-R2HRWFRH.js → chunk-JAF7JIZQ.js} +1 -1
  14. package/dist/chunk-JAF7JIZQ.js.map +1 -0
  15. package/dist/{chunk-V3RF6GQW.js → chunk-KTDBDKL5.js} +1 -1
  16. package/dist/chunk-KTDBDKL5.js.map +1 -0
  17. package/dist/{chunk-MMVQPGK3.js → chunk-ONJZXUQT.js} +112 -24
  18. package/dist/chunk-ONJZXUQT.js.map +1 -0
  19. package/dist/{chunk-PVXFURDR.js → chunk-QPPLO2YV.js} +2 -2
  20. package/dist/chunk-QPPLO2YV.js.map +1 -0
  21. package/dist/{chunk-FGXA7JNR.js → chunk-W3RY755V.js} +14 -2
  22. package/dist/chunk-W3RY755V.js.map +1 -0
  23. package/dist/{chunk-NSTV4TRP.js → chunk-XPYVLHSX.js} +1 -1
  24. package/dist/chunk-XPYVLHSX.js.map +1 -0
  25. package/dist/{chunk-WSK45HW4.js → chunk-Z5B5X7QP.js} +1 -1
  26. package/dist/chunk-Z5B5X7QP.js.map +1 -0
  27. package/dist/claim-checks.d.ts +8 -13
  28. package/dist/claim-checks.js +2 -2
  29. package/dist/diff/index.d.ts +3 -3
  30. package/dist/diff/index.js +2 -1
  31. package/dist/fingerprint.js +1 -1
  32. package/dist/formatter-CDFpqbDP.d.ts +24 -0
  33. package/dist/formatter.d.ts +29 -0
  34. package/dist/formatter.js +111 -0
  35. package/dist/formatter.js.map +1 -0
  36. package/dist/{index-BTDWXDck.d.ts → index-CrtHz4vN.d.ts} +3 -1
  37. package/dist/index.d.ts +12 -7
  38. package/dist/index.js +239 -62
  39. package/dist/index.js.map +1 -1
  40. package/dist/logger.js +1 -1
  41. package/dist/{model-gRDTk8yk.d.ts → model-BkqfyVh9.d.ts} +27 -6
  42. package/dist/model-output/index.d.ts +2 -2
  43. package/dist/model-output/index.js +9 -9
  44. package/dist/ports/index.d.ts +22 -18
  45. package/dist/ports/index.js +1 -1
  46. package/dist/{position-h_jdn6ZH.d.ts → position-zZl8KGVu.d.ts} +1 -1
  47. package/dist/prompts/file-review.d.ts +3 -3
  48. package/dist/prompts/file-review.js +11 -5
  49. package/dist/prompts/languages.d.ts +2 -1
  50. package/dist/prompts/languages.js +5 -3
  51. package/dist/prompts/verify.d.ts +1 -1
  52. package/dist/prompts/verify.js +1 -1
  53. package/dist/rules/detect.d.ts +1 -1
  54. package/dist/rules/detect.js +5 -5
  55. package/dist/rules/table.js +1 -1
  56. package/dist/token-tracker.js +2 -2
  57. package/package.json +10 -2
  58. package/dist/chunk-FGXA7JNR.js.map +0 -1
  59. package/dist/chunk-ILZUCIZH.js.map +0 -1
  60. package/dist/chunk-JC74XH7Q.js.map +0 -1
  61. package/dist/chunk-MMVQPGK3.js.map +0 -1
  62. package/dist/chunk-NSTV4TRP.js.map +0 -1
  63. package/dist/chunk-OIQRTDYR.js.map +0 -1
  64. package/dist/chunk-PVXFURDR.js.map +0 -1
  65. package/dist/chunk-R2HRWFRH.js.map +0 -1
  66. package/dist/chunk-RQ5S67XB.js.map +0 -1
  67. package/dist/chunk-U3VKVDZ7.js.map +0 -1
  68. package/dist/chunk-V3RF6GQW.js.map +0 -1
  69. package/dist/chunk-WSK45HW4.js.map +0 -1
  70. package/dist/chunk-XD6CGYHO.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  normalizeDiffText
3
- } from "./chunk-NSTV4TRP.js";
3
+ } from "./chunk-XPYVLHSX.js";
4
4
 
5
5
  // src/claim-checks.ts
6
6
  var PROXIMITY_WINDOW_LINES = 25;
@@ -103,12 +103,73 @@ function isVersionClaimRefutedByPin(input) {
103
103
  if (!looksLikeExternalVersionClaim(input.title, input.body)) return false;
104
104
  return FULL_SHA_PATTERN.test(input.anchorContent);
105
105
  }
106
+ var HASH_COMMENT_EXTENSIONS = /* @__PURE__ */ new Set([
107
+ "py",
108
+ "pyi",
109
+ "rb",
110
+ "sh",
111
+ "bash",
112
+ "zsh",
113
+ "fish",
114
+ "ps1",
115
+ "yaml",
116
+ "yml",
117
+ "toml",
118
+ "ini",
119
+ "cfg",
120
+ "conf",
121
+ "tf",
122
+ "tfvars",
123
+ "hcl",
124
+ "pl",
125
+ "pm",
126
+ "r",
127
+ "jl",
128
+ "nim",
129
+ "cr",
130
+ "ex",
131
+ "exs",
132
+ "elixir",
133
+ "gemspec",
134
+ "dockerfile",
135
+ "containerfile",
136
+ "mk",
137
+ "cmake",
138
+ "gradle",
139
+ "properties",
140
+ "env",
141
+ "gitignore",
142
+ "dockerignore",
143
+ "editorconfig"
144
+ ]);
145
+ var HASH_COMMENT_FILENAMES = /* @__PURE__ */ new Set([
146
+ "dockerfile",
147
+ "containerfile",
148
+ "makefile",
149
+ "gnumakefile",
150
+ "rakefile",
151
+ "gemfile",
152
+ "brewfile",
153
+ "procfile",
154
+ "vagrantfile",
155
+ "justfile",
156
+ "cmakelists.txt",
157
+ ".gitignore",
158
+ ".dockerignore",
159
+ ".env"
160
+ ]);
106
161
  function commentSyntaxFor(path) {
107
- const ext = path.toLowerCase().split(".").pop() ?? "";
108
- if (ext === "py" || ext === "rb" || ext === "sh" || ext === "yaml" || ext === "yml" || ext === "toml") {
109
- return { line: ["#"], block: false };
110
- }
162
+ const name = path.toLowerCase().split("/").pop() ?? "";
163
+ if (HASH_COMMENT_FILENAMES.has(name)) return { line: ["#"], block: false };
164
+ const ext = name.includes(".") ? name.split(".").pop() ?? "" : "";
165
+ if (HASH_COMMENT_EXTENSIONS.has(ext)) return { line: ["#"], block: false };
111
166
  if (ext === "sql") return { line: ["--"], block: true };
167
+ if (ext === "lua") return { line: ["--"], block: true };
168
+ if (ext === "hs" || ext === "elm" || ext === "ada") return { line: ["--"], block: false };
169
+ if (ext === "vim") return { line: ['"'], block: false };
170
+ if (ext === "clj" || ext === "cljs" || ext === "edn" || ext === "lisp" || ext === "scm") {
171
+ return { line: [";"], block: false };
172
+ }
112
173
  return { line: ["//"], block: true };
113
174
  }
114
175
  function stripCommentsAndStrings(input, syntax) {
@@ -181,27 +242,39 @@ function scanTemplateLiteral(input, start) {
181
242
  return null;
182
243
  }
183
244
  var TOKEN_PATTERN = /[A-Za-z_$][\w$]*/g;
184
- function buildPresenceIndex(file) {
245
+ function buildPresenceIndex(file, fileContent) {
185
246
  const syntax = commentSyntaxFor(file.path);
186
247
  const byToken = /* @__PURE__ */ new Map();
187
248
  const entries = [];
188
249
  const hunkByLine = /* @__PURE__ */ new Map();
250
+ const add = (entry) => {
251
+ entries.push(entry);
252
+ for (const match of entry.code.matchAll(TOKEN_PATTERN)) {
253
+ const token = match[0];
254
+ const existing = byToken.get(token);
255
+ if (existing) existing.push(entry);
256
+ else byToken.set(token, [entry]);
257
+ }
258
+ };
189
259
  file.hunks.forEach((hunk, hunkIndex) => {
190
260
  for (const line of hunk.lines) {
191
261
  if (line.newLineNumber !== void 0) hunkByLine.set(line.newLineNumber, hunkIndex);
192
262
  if (line.kind === "del") continue;
193
263
  const code = stripCommentsAndStrings(normalizeDiffText(line.content), syntax);
194
264
  if (code === null) continue;
195
- const entry = { line, hunkIndex, code };
196
- entries.push(entry);
197
- for (const match of code.matchAll(TOKEN_PATTERN)) {
198
- const token = match[0];
199
- const existing = byToken.get(token);
200
- if (existing) existing.push(entry);
201
- else byToken.set(token, [entry]);
202
- }
265
+ add({ newLineNumber: line.newLineNumber, hunkIndex, code });
203
266
  }
204
267
  });
268
+ if (fileContent) {
269
+ const lines = fileContent.split("\n");
270
+ for (let i = 0; i < lines.length; i++) {
271
+ const newLineNumber = i + 1;
272
+ if (hunkByLine.has(newLineNumber)) continue;
273
+ const code = stripCommentsAndStrings(normalizeDiffText(lines[i]), syntax);
274
+ if (code === null) continue;
275
+ add({ newLineNumber, hunkIndex: null, code });
276
+ }
277
+ }
205
278
  return { byToken, entries, hunkByLine };
206
279
  }
207
280
  var SIMPLE_IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
@@ -246,12 +319,12 @@ ${input.body.slice(0, 600)}`;
246
319
  if (occurrences.length === 0) return { status: "unknown", reason: "not_present" };
247
320
  const anchorHunk = input.anchorLine !== void 0 ? input.index.hunkByLine.get(input.anchorLine) : void 0;
248
321
  const nearby = occurrences.find((entry) => {
249
- if (anchorHunk !== void 0 && entry.hunkIndex === anchorHunk) return true;
250
- if (input.anchorLine === void 0 || entry.line.newLineNumber === void 0) return false;
251
- return Math.abs(entry.line.newLineNumber - input.anchorLine) <= PROXIMITY_WINDOW_LINES;
322
+ if (anchorHunk !== void 0 && entry.hunkIndex !== null && entry.hunkIndex === anchorHunk) return true;
323
+ if (input.anchorLine === void 0 || entry.newLineNumber === void 0) return false;
324
+ return Math.abs(entry.newLineNumber - input.anchorLine) <= PROXIMITY_WINDOW_LINES;
252
325
  });
253
326
  if (!nearby) return { status: "unknown", reason: "out_of_window" };
254
- return { status: "refuted", identifier, line: nearby.line };
327
+ return { status: "refuted", identifier, line: nearby.newLineNumber };
255
328
  }
256
329
 
257
330
  export {
@@ -263,4 +336,4 @@ export {
263
336
  buildPresenceIndex,
264
337
  checkAbsenceClaim
265
338
  };
266
- //# sourceMappingURL=chunk-U3VKVDZ7.js.map
339
+ //# sourceMappingURL=chunk-IKVUW4WC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/claim-checks.ts"],"sourcesContent":["// SOUNDNESS, binding on every change: `refuted` asserts only that \"X does not appear\" is FALSE. There is no `confirmed` verdict, since a check that can confirm findings manufactures them. Losing a refutation is free; a wrong one silences a real defect.\nimport type { FileDiff } from './diff';\nimport { normalizeDiffText } from './fingerprint';\n\nconst PROXIMITY_WINDOW_LINES = 25;\n\nconst MIN_IDENTIFIER_LENGTH = 3;\n\nconst ABSENCE_PATTERNS: readonly RegExp[] = [\n /\\b(?:never|not|no longer)\\s+(?:being\\s+)?(?:passed|provided|supplied|forwarded|included|used|called|invoked|awaited|checked|set|declared|defined|imported)\\b/i,\n /\\bdoes not\\s+(?:pass|include|call|use|await|check|set|import)\\b/i,\n /\\bfails to\\s+(?:pass|include|call|await|check|import)\\b/i,\n /\\bwithout\\s+(?:passing|including|calling|awaiting|checking|importing)\\b/i,\n /\\b(?:missing|omitted|absent)\\b/i,\n /\\bis not defined\\b/i,\n];\n\nconst IDENTIFIER_STOPLIST = new Set([\n 'await', 'async', 'if', 'else', 'try', 'catch', 'finally', 'return', 'throw', 'new', 'const',\n 'let', 'var', 'function', 'class', 'this', 'super', 'import', 'export', 'from', 'default',\n 'null', 'undefined', 'true', 'false', 'void', 'typeof', 'instanceof', 'delete', 'yield',\n 'props', 'state', 'error', 'err', 'data', 'value', 'key', 'id', 'type', 'name', 'index',\n 'result', 'response', 'request', 'req', 'res', 'params', 'options', 'config', 'args',\n]);\n\nconst VERSION_CLAIM_PATTERNS: readonly RegExp[] = [\n /\\b(?:does not|doesn't|do not|don't)\\s+exist\\b/i,\n /\\b(?:non-?existent|nonexistent)\\b/i,\n /\\bis not a valid\\b/i,\n /\\blatest (?:major )?version\\b/i,\n /\\bno such (?:version|tag|release)\\b/i,\n /\\bnot a valid (?:configuration )?(?:option|key|property)\\b/i,\n /\\b(?:does not|doesn't|do not|don't)\\s+(?:expose|provide|have|support|include|offer)\\b/i,\n /\\bno such (?:function|method|export|property|api|field)\\b/i,\n /\\bis not (?:exposed|exported|available) (?:by|from|in)\\b/i,\n];\n\n// Same soundness rule as the absence checker above.\n\nconst CROSS_FILE_SUBJECT = /\\b(?:other|another|external|downstream|consuming|importing|dependent|calling)\\s+(?:module|file|component|caller|package|consumer|import)s?\\b/i;\nconst CROSS_FILE_CONSEQUENCE = /\\b(?:break|breaks|breaking|broken|fail|fails|failing|error|errors|cannot import|can't import|unable to|compilation|compile|prevent|prevents|preventing|block|blocks|blocking)\\b/i;\n\nconst ENVIRONMENT_HEDGE = /\\b(?:depending on|might not|may not|could be undefined|if (?:this|the|it)\\b[^.]{0,60}\\b(?:is )?(?:rendered|run|executed|used)\\b)/i;\nconst ENVIRONMENT_SUBJECT = /\\b(?:older|legacy|earlier|some)\\s+(?:node(?:\\.js)?|browsers?|runtimes?|environments?|engines?|versions?)\\b|\\bserver[- ]side\\b|\\bSSR\\b|\\bhydration\\b|\\bpolyfill\\b|\\bis not defined on the server\\b/i;\n\nconst CALLEE_FAILURE_CONDITION = /\\b(?:if|when|should|were)\\b(?:(?!\\.\\s)[^;!?]){0,62}\\b(?:fails?|failing|rejects?|rejecting|throws?|throwing|errors? out)\\b/i;\nconst CALLEE_CALL_SHAPE = /[\\w.$]{1,50}\\s*\\(\\s*\\)|`[\\w.$]{1,50}\\(/;\nconst CALLEE_UNHANDLED_OUTCOME = /\\bunhandled\\b|\\bunhandled promise\\b|\\bnot (?:caught|handled)\\b|\\bno (?:\\.)?catch\\b|\\bwithout (?:a )?(?:try|catch)\\b|\\bcrash\\b/i;\n\nexport type UndecidableClaimReason = 'cross-file' | 'environment' | 'callee-errors';\n\n/** Refutes a claim whose truth lives outside the diff; two signals per family, since one is ordinary. */\nexport function refuteUndecidableClaim(input: { title: string; body: string }): UndecidableClaimReason | null {\n const text = `${input.title}\\n${input.body}`;\n\n if (CROSS_FILE_SUBJECT.test(text) && CROSS_FILE_CONSEQUENCE.test(text)) return 'cross-file';\n if (ENVIRONMENT_HEDGE.test(text) && ENVIRONMENT_SUBJECT.test(text)) return 'environment';\n if (CALLEE_FAILURE_CONDITION.test(text) && CALLEE_CALL_SHAPE.test(text) && CALLEE_UNHANDLED_OUTCOME.test(text)) {\n return 'callee-errors';\n }\n\n return null;\n}\n\nconst FULL_SHA_PATTERN = /\\b[0-9a-f]{40}\\b/;\n\nexport function looksLikeExternalVersionClaim(title: string, body: string): boolean {\n const text = `${title}\\n${body}`;\n return VERSION_CLAIM_PATTERNS.some((pattern) => pattern.test(text));\n}\n\nexport function isVersionClaimRefutedByPin(input: { title: string; body: string; anchorContent: string }): boolean {\n if (!looksLikeExternalVersionClaim(input.title, input.body)) return false;\n return FULL_SHA_PATTERN.test(input.anchorContent);\n}\n\n/** One line the identifier could be found on; `hunkIndex` is null for lines from the post-image. */\ntype PresenceEntry = { newLineNumber: number | undefined; hunkIndex: number | null; code: string };\n\nexport type PresenceIndex = {\n byToken: Map<string, PresenceEntry[]>;\n entries: PresenceEntry[];\n hunkByLine: Map<number, number>;\n};\n\nexport type AbsenceClaimVerdict =\n | {\n status: 'unknown';\n reason:\n | 'not_absence_shaped'\n | 'no_identifier'\n | 'ambiguous_identifier'\n | 'stoplisted'\n | 'not_present'\n | 'out_of_window';\n }\n | { status: 'refuted'; identifier: string; line: number | undefined };\n\ntype CommentSyntax = { line: readonly string[]; block: boolean };\n\n// Must stay complete: a misclassified file keeps comment text as code, refuting real absence claims.\nconst HASH_COMMENT_EXTENSIONS = new Set([\n 'py', 'pyi', 'rb', 'sh', 'bash', 'zsh', 'fish', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf',\n 'tf', 'tfvars', 'hcl', 'pl', 'pm', 'r', 'jl', 'nim', 'cr', 'ex', 'exs', 'elixir', 'gemspec',\n 'dockerfile', 'containerfile', 'mk', 'cmake', 'gradle', 'properties', 'env', 'gitignore',\n 'dockerignore', 'editorconfig',\n]);\n\nconst HASH_COMMENT_FILENAMES = new Set([\n 'dockerfile', 'containerfile', 'makefile', 'gnumakefile', 'rakefile', 'gemfile', 'brewfile',\n 'procfile', 'vagrantfile', 'justfile', 'cmakelists.txt', '.gitignore', '.dockerignore', '.env',\n]);\n\nexport function commentSyntaxFor(path: string): CommentSyntax {\n const name = path.toLowerCase().split('/').pop() ?? '';\n if (HASH_COMMENT_FILENAMES.has(name)) return { line: ['#'], block: false };\n\n const ext = name.includes('.') ? name.split('.').pop() ?? '' : '';\n if (HASH_COMMENT_EXTENSIONS.has(ext)) return { line: ['#'], block: false };\n\n if (ext === 'sql') return { line: ['--'], block: true };\n if (ext === 'lua') return { line: ['--'], block: true };\n if (ext === 'hs' || ext === 'elm' || ext === 'ada') return { line: ['--'], block: false };\n if (ext === 'vim') return { line: ['\"'], block: false };\n if (ext === 'clj' || ext === 'cljs' || ext === 'edn' || ext === 'lisp' || ext === 'scm') {\n return { line: [';'], block: false };\n }\n return { line: ['//'], block: true };\n}\n\nexport function stripCommentsAndStrings(input: string, syntax: CommentSyntax): string | null {\n let out = '';\n let i = 0;\n\n while (i < input.length) {\n const rest = input.slice(i);\n\n if (syntax.line.some((token) => rest.startsWith(token))) break;\n\n if (syntax.block && rest.startsWith('/*')) {\n const end = input.indexOf('*/', i + 2);\n if (end === -1) return null;\n out += ' ';\n i = end + 2;\n continue;\n }\n\n const char = input[i];\n\n if (char === \"'\" || char === '\"') {\n const close = findStringEnd(input, i + 1, char);\n if (close === -1) return null;\n out += ' ';\n i = close + 1;\n continue;\n }\n\n if (char === '`') {\n const scanned = scanTemplateLiteral(input, i);\n if (!scanned) return null;\n out += scanned.code;\n i = scanned.next;\n continue;\n }\n\n out += char;\n i += 1;\n }\n\n return out;\n}\n\nfunction findStringEnd(input: string, start: number, quote: string): number {\n for (let i = start; i < input.length; i++) {\n if (input[i] === '\\\\') {\n i += 1;\n continue;\n }\n if (input[i] === quote) return i;\n }\n return -1;\n}\n\nfunction scanTemplateLiteral(input: string, start: number): { code: string; next: number } | null {\n let code = ' ';\n let i = start + 1;\n\n while (i < input.length) {\n if (input[i] === '\\\\') {\n i += 2;\n continue;\n }\n if (input[i] === '`') return { code, next: i + 1 };\n if (input[i] === '$' && input[i + 1] === '{') {\n let depth = 1;\n let j = i + 2;\n while (j < input.length && depth > 0) {\n if (input[j] === '{') depth += 1;\n else if (input[j] === '}') depth -= 1;\n j += 1;\n }\n if (depth !== 0) return null;\n code += ` ${input.slice(i + 2, j - 1)} `;\n i = j;\n continue;\n }\n i += 1;\n }\n\n return null;\n}\n\n// MEASURED AND REJECTED: a call-site/reachability gate. The withheld slice had precision 27.3% vs an\n// 18.7% pooled baseline, and \"mentions callers\" scored -0.3 on the codra-only subset. Any gate proposed\n// from this corpus must be re-checked on the codra-only subset -- pooled signals do not survive it.\n\nconst TOKEN_PATTERN = /[A-Za-z_$][\\w$]*/g;\n\n/** Where each identifier appears after the change; without a post-image the index sees only the diff. */\nexport function buildPresenceIndex(file: FileDiff, fileContent?: string | null): PresenceIndex {\n const syntax = commentSyntaxFor(file.path);\n const byToken = new Map<string, PresenceEntry[]>();\n const entries: PresenceEntry[] = [];\n const hunkByLine = new Map<number, number>();\n\n const add = (entry: PresenceEntry) => {\n entries.push(entry);\n for (const match of entry.code.matchAll(TOKEN_PATTERN)) {\n const token = match[0];\n const existing = byToken.get(token);\n if (existing) existing.push(entry);\n else byToken.set(token, [entry]);\n }\n };\n\n file.hunks.forEach((hunk, hunkIndex) => {\n for (const line of hunk.lines) {\n if (line.newLineNumber !== undefined) hunkByLine.set(line.newLineNumber, hunkIndex);\n\n if (line.kind === 'del') continue;\n\n const code = stripCommentsAndStrings(normalizeDiffText(line.content), syntax);\n if (code === null) continue;\n\n add({ newLineNumber: line.newLineNumber, hunkIndex, code });\n }\n });\n\n if (fileContent) {\n const lines = fileContent.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n const newLineNumber = i + 1;\n if (hunkByLine.has(newLineNumber)) continue;\n\n const code = stripCommentsAndStrings(normalizeDiffText(lines[i]), syntax);\n if (code === null) continue;\n\n add({ newLineNumber, hunkIndex: null, code });\n }\n }\n\n return { byToken, entries, hunkByLine };\n}\n\nconst SIMPLE_IDENTIFIER = /^[A-Za-z_$][\\w$]*$/;\nconst DOTTED_IDENTIFIER = /^[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)+$/;\n\nfunction extractIdentifier(sentence: string): { identifier: string } | 'none' | 'ambiguous' {\n const spans = [\n ...sentence.matchAll(/`([^`]+)`/g),\n ...sentence.matchAll(/'([^']+)'/g),\n ...sentence.matchAll(/\"([^\"]+)\"/g),\n ].map((match) => match[1].trim());\n\n const candidates = new Set(\n spans.filter((span) => SIMPLE_IDENTIFIER.test(span) || DOTTED_IDENTIFIER.test(span)),\n );\n\n if (candidates.size === 0) return 'none';\n if (candidates.size > 1) return 'ambiguous';\n return { identifier: [...candidates][0] };\n}\n\nfunction absenceSentences(text: string): string[] {\n return text.split(/[.;\\n]/).filter((sentence) => ABSENCE_PATTERNS.some((pattern) => pattern.test(sentence)));\n}\n\nexport function checkAbsenceClaim(input: {\n title: string;\n body: string;\n anchorLine: number | undefined;\n index: PresenceIndex;\n}): AbsenceClaimVerdict {\n const text = `${input.title}\\n${input.body.slice(0, 600)}`;\n\n const sentences = absenceSentences(text);\n if (sentences.length === 0) return { status: 'unknown', reason: 'not_absence_shaped' };\n\n let identifier: string | undefined;\n for (const sentence of sentences) {\n const extracted = extractIdentifier(sentence);\n if (extracted === 'ambiguous') return { status: 'unknown', reason: 'ambiguous_identifier' };\n if (extracted !== 'none') {\n identifier = extracted.identifier;\n break;\n }\n }\n if (!identifier) return { status: 'unknown', reason: 'no_identifier' };\n\n const head = identifier.split('.')[0];\n if (identifier.length < MIN_IDENTIFIER_LENGTH) return { status: 'unknown', reason: 'stoplisted' };\n if (IDENTIFIER_STOPLIST.has(identifier.toLowerCase()) || IDENTIFIER_STOPLIST.has(head.toLowerCase())) {\n return { status: 'unknown', reason: 'stoplisted' };\n }\n\n const occurrences = identifier.includes('.')\n ? input.index.entries.filter((entry) => entry.code.replace(/\\s*\\.\\s*/g, '.').includes(identifier))\n : (input.index.byToken.get(identifier) ?? []);\n\n if (occurrences.length === 0) return { status: 'unknown', reason: 'not_present' };\n\n const anchorHunk = input.anchorLine !== undefined ? input.index.hunkByLine.get(input.anchorLine) : undefined;\n const nearby = occurrences.find((entry) => {\n if (anchorHunk !== undefined && entry.hunkIndex !== null && entry.hunkIndex === anchorHunk) return true;\n if (input.anchorLine === undefined || entry.newLineNumber === undefined) return false;\n return Math.abs(entry.newLineNumber - input.anchorLine) <= PROXIMITY_WINDOW_LINES;\n });\n\n if (!nearby) return { status: 'unknown', reason: 'out_of_window' };\n return { status: 'refuted', identifier, line: nearby.newLineNumber };\n}\n"],"mappings":"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","names":[]}
@@ -105,4 +105,4 @@ var RULES = [
105
105
  export {
106
106
  RULES
107
107
  };
108
- //# sourceMappingURL=chunk-R2HRWFRH.js.map
108
+ //# sourceMappingURL=chunk-JAF7JIZQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules/table.ts"],"sourcesContent":["import type { ClaimType, reviewSeverities } from '@codraoss/schema';\n\ntype ReviewSeverity = typeof reviewSeverities[number];\n\nexport type Rule = {\n id: string;\n claimType: ClaimType;\n severity: ReviewSeverity;\n title: string;\n body: string;\n triggers: readonly string[];\n pattern: RegExp;\n rejectRaw?: RegExp;\n extensions?: readonly string[];\n enabled: boolean;\n};\n\nconst ts = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'] as const;\n\nexport const RULES: readonly Rule[] = [\n {\n id: 'empty-catch',\n claimType: 'swallowed_error',\n severity: 'P2',\n title: 'Empty catch block swallows the error',\n body: 'This `catch` has no body, so the error is discarded with no log, no rethrow and no recovery. '\n + 'A failure here becomes silent. If the error is genuinely expected, say so in a comment inside the block.',\n triggers: ['catch'],\n pattern: /\\bcatch\\s*(\\([^)]*\\))?\\s*\\{\\s*\\}/,\n rejectRaw: /\\bcatch\\s*(\\([^)]*\\))?\\s*\\{\\s*(?:\\/\\/|\\/\\*)/,\n extensions: ts,\n enabled: true,\n },\n {\n id: 'debugger-statement',\n claimType: 'other',\n severity: 'P1',\n title: '`debugger` statement left in the diff',\n body: 'A `debugger` statement halts execution whenever devtools are open. This is almost always '\n + 'a leftover from local debugging.',\n triggers: ['debugger'],\n pattern: /^\\s*debugger\\s*;?\\s*$/,\n extensions: ts,\n enabled: true,\n },\n {\n id: 'focused-test',\n claimType: 'other',\n severity: 'P1',\n title: 'Focused test will skip the rest of the suite',\n body: 'A focused test (`.only`) silently prevents every other test in the file from running, so '\n + 'CI stays green while covering almost nothing.',\n triggers: ['.only'],\n pattern: /\\b(?:describe|it|test|context|suite)\\s*\\.\\s*only\\s*\\(/,\n extensions: ts,\n enabled: true,\n },\n {\n id: 'dynamic-code-exec',\n claimType: 'unsafe_dynamic_code',\n severity: 'P1',\n title: 'Dynamic code execution',\n body: '`eval` and the `Function` constructor execute arbitrary strings as code. If any part of '\n + 'that string can be influenced by input, this is remote code execution.',\n triggers: ['eval(', 'Function('],\n pattern: /(?:^|[^.\\w])eval\\s*\\(|new\\s+Function\\s*\\(/,\n extensions: ts,\n enabled: true,\n },\n {\n id: 'dynamic-html-sink',\n claimType: 'unsafe_dom_sink',\n severity: 'P1',\n title: 'Unsanitized value assigned to an HTML sink',\n body: 'Assigning a non-literal to `innerHTML`/`outerHTML` (or passing one to `insertAdjacentHTML`) '\n + 'executes any markup it contains. If the value can carry user input this is XSS.',\n triggers: ['innerHTML', 'outerHTML', 'insertAdjacentHTML'],\n pattern: /\\.(?:inner|outer)HTML\\s*=\\s*[A-Za-z_$][\\w$.[\\]()]*|insertAdjacentHTML\\s*\\([^)]*,\\s*[A-Za-z_$]/,\n extensions: ts,\n enabled: true,\n },\n {\n id: 'mutable-default-arg',\n claimType: 'mutable_default_arg',\n severity: 'P2',\n title: 'Mutable default argument',\n body: 'Python evaluates a default argument once, at definition time, so this list/dict/set is '\n + 'shared by every call. Mutating it leaks state between invocations. Use `None` and build the '\n + 'value inside the function.',\n triggers: ['def '],\n pattern: /\\bdef\\s+\\w+\\s*\\([^)]*=\\s*(?:\\[\\s*\\]|\\{\\s*\\}|set\\s*\\(\\s*\\)|dict\\s*\\(\\s*\\)|list\\s*\\(\\s*\\))/,\n extensions: ['py'],\n enabled: true,\n },\n {\n id: 'destructive-migration',\n claimType: 'destructive_migration',\n severity: 'P1',\n title: 'Destructive migration statement',\n body: 'This statement discards data irreversibly. On a forward-only migration chain there is no '\n + 'rollback: confirm the column/table is genuinely unused and that a backup exists.',\n triggers: ['DROP', 'TRUNCATE', 'drop', 'truncate'],\n pattern: /\\b(?:drop\\s+(?:column|table)|truncate\\s+table|truncate\\s+\\w)/i,\n extensions: ['sql'],\n enabled: true,\n },\n\n\n {\n id: 'hardcoded-secret',\n claimType: 'hardcoded_secret',\n severity: 'P0',\n title: 'Possible hardcoded credential',\n body: 'This looks like a literal credential committed to the repository. If it is real, rotate it '\n + 'and move it to a secret binding.',\n triggers: ['sk-', 'AIza', 'ghp_', 'AKIA'],\n pattern: /\\b(?:sk-[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{30,}|gh[pousr]_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16})\\b/,\n enabled: false,\n },\n {\n id: 'insecure-random',\n claimType: 'insecure_randomness',\n severity: 'P2',\n title: '`Math.random()` used for a security-sensitive value',\n body: '`Math.random()` is not cryptographically secure and its output is predictable. Use '\n + '`crypto.getRandomValues()` for tokens, ids or anything an attacker should not guess.',\n triggers: ['Math.random'],\n pattern: /\\b(?:token|secret|key|nonce|salt|password|session|id)\\w*\\s*=[^=]*Math\\.random\\s*\\(/i,\n extensions: ts,\n enabled: false,\n },\n];\n\n"],"mappings":";AAiBA,IAAM,KAAK,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK;AAE3C,IAAM,QAAyB;AAAA,EACpC;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,OAAO;AAAA,IAClB,SAAS;AAAA,IACT,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,UAAU;AAAA,IACrB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,OAAO;AAAA,IAClB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,SAAS,WAAW;AAAA,IAC/B,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,aAAa,aAAa,oBAAoB;AAAA,IACzD,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAGN,UAAU,CAAC,MAAM;AAAA,IACjB,SAAS;AAAA,IACT,YAAY,CAAC,IAAI;AAAA,IACjB,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,QAAQ,YAAY,QAAQ,UAAU;AAAA,IACjD,SAAS;AAAA,IACT,YAAY,CAAC,KAAK;AAAA,IAClB,SAAS;AAAA,EACX;AAAA,EAGA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,OAAO,QAAQ,QAAQ,MAAM;AAAA,IACxC,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO;AAAA,IACP,MAAM;AAAA,IAEN,UAAU,CAAC,aAAa;AAAA,IACxB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AACF;","names":[]}
@@ -134,4 +134,4 @@ export {
134
134
  renderDiffSnippet,
135
135
  parseVerifyResponse
136
136
  };
137
- //# sourceMappingURL=chunk-V3RF6GQW.js.map
137
+ //# sourceMappingURL=chunk-KTDBDKL5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prompts/verify.ts"],"sourcesContent":["import { z } from 'zod';\nimport { jsonrepair } from 'jsonrepair';\nimport type { FileDiff } from '../diff';\n\nexport type VerifyCandidate = {\n index: number;\n path: string;\n line: number | null;\n title: string;\n body: string;\n snippet: string;\n evidence?: string | null;\n};\n\nconst verifyResultSchema = z.object({\n results: z\n .array(\n z.object({\n index: z.number().int(),\n reason: z.string().optional(),\n // decidable\" -- only an explicit `false` costs a finding. See the note on the prompt below.\n decidable: z.boolean().optional(),\n verdict: z.enum(['keep', 'drop']),\n confidence: z.number().min(0).max(1).optional(),\n }),\n )\n .default([]),\n});\n\nexport type VerifyResult = z.infer<typeof verifyResultSchema>['results'][number];\n\nexport const VERIFY_RESPONSE_SCHEMA = {\n name: 'codra_verify_findings',\n schema: {\n type: 'object',\n additionalProperties: false,\n required: ['results'],\n properties: {\n results: {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['index', 'reason', 'decidable', 'verdict'],\n properties: {\n index: { type: 'integer', minimum: 0 },\n reason: { type: 'string', maxLength: 300 },\n decidable: { type: 'boolean' },\n verdict: { type: 'string', enum: ['keep', 'drop'] },\n confidence: { type: 'number', minimum: 0, maximum: 1 },\n },\n },\n },\n },\n },\n} as const;\n\nexport const VERIFY_SYSTEM_PROMPT = `You are a meticulous senior engineer checking whether each candidate code-review finding is actually supported by the code it points at.\n\nFor EACH finding you are given the claim and a SHORT WINDOW of diff context around the line it was anchored to. That window is all you have: you cannot see the rest of the file, any other file, the project's dependencies and their versions, its build target, or its runtime.\n\nAnswer two questions per finding, in this order.\n\n1. \"decidable\": can this claim be settled from the window you were given?\n - true - the window contains everything needed to say whether the claim holds.\n - false - settling it would need something outside the window: which files import this one, what a function defined elsewhere does, which version of a dependency is installed, what engine or renderer the code runs on, or how a caller uses the result.\n Watch for claims that assert a CONSEQUENCE somewhere you cannot see: \"this breaks importers\", \"this throws on older runtimes\", \"this fails during server rendering\", \"the caller will not await this\". The anchored line can be exactly as quoted and the consequence still be unverifiable - confirming that the quote is real is NOT confirming the claim.\n When \"decidable\" is false, say in \"reason\" what you would have to look at, e.g. \"would need the importers of this module\".\n\n Two rules, because both have been got wrong on real reviews:\n\n a) A claim of the form \"if X() fails / rejects / throws, this is unhandled\" is NOT decidable unless the\n BODY of X is inside your window. A function whose body you cannot see may well handle its own\n errors, in which case there is nothing to report. Seeing the CALL is not seeing the body. Mark it\n not decidable and say you would need that function's implementation.\n\n b) Read the diff markers before you agree that something was removed or changed. A line prefixed \"-\"\n is the OLD code and a line prefixed \"+\" is the NEW code. A claim that says \"X was replaced by Y\" is\n false if the diff shows Y being replaced by X, and a claim that a safeguard was \"removed\" is false\n if the \"+\" line still carries an equivalent one under a different name. State the direction in your\n reason: \"the + line adds strict validation, so the claim is backwards\".\n\n2. \"verdict\":\n - \"keep\": the code in the window genuinely exhibits the problem the claim describes.\n - \"drop\": the claim is not supported by the code shown - it describes something that isn't there, it is speculative, it is a subjective style preference, or it is not decidable from this window.\n A claim you marked not decidable is always a \"drop\".\n\nJudge the CLAIM against the CODE. Do not defer to the claim's confidence or phrasing; a well-written claim about code that doesn't do what it says is still a drop.\nBe strict: when in doubt, \"drop\". It is better to drop a borderline finding than to keep a wrong one.\n\nOutput MUST be valid JSON, exactly one object, no prose before or after:\n{\n \"results\": [\n { \"index\": <number>, \"reason\": \"<why, and if not decidable what you would need to see>\", \"decidable\": true | false, \"verdict\": \"keep\" | \"drop\", \"confidence\": <float 0.0-1.0> }\n ]\n}\nInclude exactly one result object for every finding index provided, and use the same index numbers you were given.`;\n\nexport function buildVerifyPrompt(candidates: VerifyCandidate[]): string {\n const blocks = candidates.map((c) => {\n const location = c.line != null ? `${c.path}:${c.line}` : c.path;\n return [\n `### Finding index ${c.index}`,\n `Location: ${location}`,\n `Title: ${c.title}`,\n `Claim: ${c.body}`,\n ...(c.evidence ? [`Code the claim cites: ${c.evidence}`] : []),\n 'Relevant diff:',\n c.snippet || '(no diff context available for this location)',\n ].join('\\n');\n });\n\n return [\n 'Validate each finding below against its diff context. Return a verdict for every index.',\n '',\n blocks.join('\\n\\n'),\n ].join('\\n');\n}\n\nexport function renderDiffSnippet(file: FileDiff | undefined, line: number | undefined, radius = 12): string {\n if (!file) return '';\n const flat = file.hunks.flatMap((hunk) => hunk.lines);\n if (flat.length === 0) return '';\n\n if (line == null) return '';\n\n const byNewLine = flat.findIndex((l) => l.newLineNumber === line);\n const anchor = byNewLine !== -1 ? byNewLine : flat.findIndex((l) => l.oldLineNumber === line);\n if (anchor === -1) return '';\n\n const start = Math.max(0, anchor - radius);\n const end = Math.min(flat.length, anchor + radius + 1);\n\n return flat\n .slice(start, end)\n .map((l) => {\n const prefix = l.kind === 'add' ? '+' : l.kind === 'del' ? '-' : ' ';\n const gutter = String(l.newLineNumber ?? l.oldLineNumber ?? '').padStart(4, ' ');\n return `${gutter} ${prefix}${l.content}`;\n })\n .join('\\n');\n}\n\nexport function parseVerifyResponse(raw: string): VerifyResult[] {\n const trimmed = raw.trim();\n const start = trimmed.indexOf('{');\n const end = trimmed.lastIndexOf('}');\n const candidate = start !== -1 && end !== -1 && end > start ? trimmed.slice(start, end + 1) : trimmed;\n\n let json: unknown;\n try {\n json = JSON.parse(candidate);\n } catch {\n json = JSON.parse(jsonrepair(candidate));\n }\n\n return verifyResultSchema.parse(json).results;\n}\n"],"mappings":"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","names":[]}
@@ -1,16 +1,102 @@
1
1
  import {
2
- getLanguageForFile
3
- } from "./chunk-RQ5S67XB.js";
2
+ getLanguageForFile,
3
+ isChangelogPath
4
+ } from "./chunk-FKRCBUWZ.js";
4
5
  import {
6
+ CHANGELOG_EXCERPT_CHARS,
5
7
  EXEMPLAR_BLOCK_CHARS,
8
+ FILE_CONTEXT_CHAR_BUDGET,
9
+ FILE_CONTEXT_WINDOW_LINES,
10
+ FRAGMENTED_HUNK_THRESHOLD,
11
+ FRAGMENTED_MIN_LINES,
12
+ PACKABLE_MAX_DIFF_LINES,
6
13
  PR_DESCRIPTION_CHARS
7
- } from "./chunk-FGXA7JNR.js";
14
+ } from "./chunk-W3RY755V.js";
8
15
 
9
16
  // src/prompts/file-review.ts
10
17
  import { claimTypes } from "@codraoss/schema";
18
+
19
+ // src/prompts/review-context.ts
20
+ function clip(text, limit) {
21
+ return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
22
+ }
23
+ function renderIntentBlock(input) {
24
+ const description = input.prDescription?.trim();
25
+ const changelog = input.changelogExcerpt?.trim();
26
+ return [
27
+ "## PR INTENT (what the author set out to do)",
28
+ `Title: ${input.prTitle ?? "Untitled PR"}`,
29
+ ...description ? ["Description:", clip(description, PR_DESCRIPTION_CHARS)] : [],
30
+ ...changelog ? ["Changelog lines added by this PR:", clip(changelog, CHANGELOG_EXCERPT_CHARS)] : [],
31
+ "Behaviour that serves this stated intent is deliberate. Do not report it as an accident, an oversight, or a regression."
32
+ ].join("\n");
33
+ }
34
+ function changelogExcerptFromDiff(files) {
35
+ const added = [];
36
+ let used = 0;
37
+ for (const file of files) {
38
+ if (!isChangelogPath(file.path)) continue;
39
+ for (const hunk of file.hunks) {
40
+ for (const line of hunk.lines) {
41
+ if (line.kind !== "add") continue;
42
+ const text = line.content.trim();
43
+ if (!text) continue;
44
+ if (used + text.length > CHANGELOG_EXCERPT_CHARS) {
45
+ return added.length > 0 ? added.join("\n") : null;
46
+ }
47
+ added.push(text);
48
+ used += text.length + 1;
49
+ }
50
+ }
51
+ }
52
+ return added.length > 0 ? added.join("\n") : null;
53
+ }
54
+ function wantsFileContext(file, fullFileContext, gate = {}) {
55
+ if (!fullFileContext) return false;
56
+ if (gate.compactPrompt) return false;
57
+ if (file.isNew || file.isDeleted || file.isBinary) return false;
58
+ if (file.lineCount > PACKABLE_MAX_DIFF_LINES) return true;
59
+ return (file.hunks?.length ?? 0) >= FRAGMENTED_HUNK_THRESHOLD && file.lineCount >= FRAGMENTED_MIN_LINES;
60
+ }
61
+ function renderFileContext(file, content) {
62
+ const lines = content.split("\n");
63
+ let lowest = Number.POSITIVE_INFINITY;
64
+ let highest = 0;
65
+ for (const hunk of file.hunks) {
66
+ for (const line of hunk.lines) {
67
+ if (typeof line.newLineNumber !== "number") continue;
68
+ lowest = Math.min(lowest, line.newLineNumber);
69
+ highest = Math.max(highest, line.newLineNumber);
70
+ }
71
+ }
72
+ if (!Number.isFinite(lowest)) return null;
73
+ const start = Math.max(1, lowest - FILE_CONTEXT_WINDOW_LINES);
74
+ const end = Math.min(lines.length, highest + FILE_CONTEXT_WINDOW_LINES);
75
+ const numbered = [];
76
+ let used = 0;
77
+ for (let n = start; n <= end; n++) {
78
+ const rendered = `${n} ${lines[n - 1] ?? ""}`;
79
+ if (used + rendered.length > FILE_CONTEXT_CHAR_BUDGET) break;
80
+ numbered.push(rendered);
81
+ used += rendered.length + 1;
82
+ }
83
+ if (numbered.length === 0) return null;
84
+ const last = start + numbered.length - 1;
85
+ const partial = start > 1 || last < lines.length;
86
+ return [
87
+ `Full file after the change, lines ${start}-${last}${partial ? ` of ${lines.length}` : ""} (CONTEXT ONLY, not reviewable):`,
88
+ ...numbered
89
+ ].join("\n");
90
+ }
91
+ var INTENT_CHECK_INSTRUCTION = "Intent check: every finding must survive a comparison with the PR INTENT above. If what you are about to flag IS the stated intent, it is not a finding - drop it. Otherwise open the `body` with one line saying how the problem differs from what the author set out to do.";
92
+
93
+ // src/prompts/file-review.ts
11
94
  function generatorFindingCap(maxComments) {
12
95
  return Math.max(1, maxComments * 2);
13
96
  }
97
+ function reviewBreadth(config) {
98
+ return config.review_breadth ?? generatorFindingCap(config.max_comments);
99
+ }
14
100
  function findingItemSchema() {
15
101
  return {
16
102
  type: "object",
@@ -47,7 +133,7 @@ function findingItemSchema() {
47
133
  }
48
134
  };
49
135
  }
50
- function buildReviewResponseSchema(maxComments) {
136
+ function buildReviewResponseSchema(findingCap) {
51
137
  return {
52
138
  name: "codra_file_review",
53
139
  schema: {
@@ -57,7 +143,7 @@ function buildReviewResponseSchema(maxComments) {
57
143
  properties: {
58
144
  findings: {
59
145
  type: "array",
60
- maxItems: generatorFindingCap(maxComments),
146
+ maxItems: Math.max(1, findingCap),
61
147
  items: findingItemSchema()
62
148
  },
63
149
  overall_explanation: { type: "string" },
@@ -67,7 +153,7 @@ function buildReviewResponseSchema(maxComments) {
67
153
  }
68
154
  };
69
155
  }
70
- function buildBatchReviewResponseSchema(maxComments, fileCount) {
156
+ function buildBatchReviewResponseSchema(findingCap, fileCount) {
71
157
  return {
72
158
  name: "codra_batch_review",
73
159
  schema: {
@@ -141,8 +227,9 @@ var MULTI_FILE_SCHEMA_FORMAT = `{
141
227
  }`;
142
228
  function buildFileReviewSystemPromptBase(opts) {
143
229
  const multi = opts?.multiFile === true;
230
+ const singleFileScope = opts?.fileContext === true ? "- You can see the diff below and, after it, the full content of that one file. You cannot see the rest of the repository. Findings must still be about lines the diff CHANGED; the file content is there to tell you what the surrounding code does, not to be reviewed." : "- You can see ONLY the diff below, not the whole file or the rest of the repository.";
144
231
  const contextScope = multi ? `- You can see ONLY the diffs below, not the whole files or the rest of the repository.
145
- - Each file below is INDEPENDENT. A finding about one file must be grounded in a line from THAT file's diff, and must be reported inside that file's entry. Never carry a claim from one file to another, and never assume two files interact unless both diffs show it.` : "- You can see ONLY the diff below, not the whole file or the rest of the repository.";
232
+ - Each file below is INDEPENDENT. A finding about one file must be grounded in a line from THAT file's diff, and must be reported inside that file's entry. Never carry a claim from one file to another, and never assume two files interact unless both diffs show it.` : singleFileScope;
146
233
  const evidenceSource = multi ? `the single line of code the finding is about, copied VERBATIM from that file's diff below.` : "the single line of code the finding is about, copied VERBATIM from the diff below.";
147
234
  const capRule = multi ? "4. Return at most {{MAX_COMMENTS}} findings PER FILE, most severe first. Keep each body under 160 words." : "4. Return at most {{MAX_COMMENTS}} findings, most severe first. Keep each body under 160 words.";
148
235
  const emptyRule = multi ? `5. Return exactly one entry per file listed below, in the same order, and never omit a file. Review each file's diff with the same care you would give it if it were the only file in front of you. An empty findings array is a positive claim that this diff introduces no defect, so return one only when that is true. Do not pad, and do not withhold.` : "5. If the diff genuinely introduces no defect, return an empty findings array and a short explanation. Do not pad, and do not withhold.";
@@ -189,7 +276,7 @@ Identify security risks such as XSS, SQLi, CSRF, insecure randomness, and data l
189
276
  var fileReviewSystemPromptBase = buildFileReviewSystemPromptBase();
190
277
  function buildFileReviewSystemPrompt(config, languagePersona, opts) {
191
278
  const persona = languagePersona ? ` as ${languagePersona}` : "";
192
- const prompt = buildFileReviewSystemPromptBase(opts).replace("{{MAX_COMMENTS}}", generatorFindingCap(config.max_comments).toString());
279
+ const prompt = buildFileReviewSystemPromptBase(opts).replace("{{MAX_COMMENTS}}", reviewBreadth(config).toString());
193
280
  return `You are a world-class professional senior code reviewer${persona}. ${prompt}`;
194
281
  }
195
282
  function renderExemplars(exemplars) {
@@ -206,12 +293,6 @@ function renderExemplars(exemplars) {
206
293
  const heading = "Findings a reviewer on THIS repository has already rejected. Do not report things like these:";
207
294
  return [heading, ...lines].join("\n");
208
295
  }
209
- function renderPrContext(prDescription) {
210
- const trimmed = prDescription?.trim();
211
- if (!trimmed) return null;
212
- return `PR description (author intent - use to judge whether a change is deliberate):
213
- ${trimmed.slice(0, PR_DESCRIPTION_CHARS)}${trimmed.length > PR_DESCRIPTION_CHARS ? "\u2026" : ""}`;
214
- }
215
296
  function renderCustomRules(config) {
216
297
  const rules = config.custom_rules.length > 0 ? config.custom_rules.map((rule) => `- ${rule}`).join("\n") : "- None";
217
298
  return `Custom rules:
@@ -227,13 +308,15 @@ ${languageInfo.guidelines.map((g) => `- ${g}`).join("\n")}` : "Language: Generic
227
308
  function buildFileReviewPrompts(input) {
228
309
  const languageInfo = getLanguageForFile(input.file.path);
229
310
  const rules = input.config.custom_rules.length > 0 ? input.config.custom_rules.map((rule) => `- ${rule}`).join("\n") : "- None";
230
- const systemPrompt = buildFileReviewSystemPrompt(input.config, languageInfo?.persona);
311
+ const intentBlock = renderIntentBlock(input);
312
+ const fileContext = input.fileContext ? renderFileContext(input.file, input.fileContext) : null;
313
+ const systemPrompt = buildFileReviewSystemPrompt(input.config, languageInfo?.persona, {
314
+ fileContext: fileContext !== null
315
+ });
231
316
  const languageGuidelines = renderLanguageGuidelines(input.file.path);
232
- const prContext = renderPrContext(input.prDescription);
233
317
  const exemplars = renderExemplars(input.rejectedExemplars);
234
318
  const userPrompt = [
235
- `PR title: ${input.prTitle ?? "Untitled PR"}`,
236
- ...prContext ? [prContext] : [],
319
+ intentBlock,
237
320
  ...exemplars ? [exemplars] : [],
238
321
  `File path: ${input.file.path}`,
239
322
  languageGuidelines,
@@ -241,7 +324,8 @@ function buildFileReviewPrompts(input) {
241
324
  ${rules}`,
242
325
  "Review ONLY the diff shown below. You cannot see the rest of the file or repository - do not report something as undefined, unimported, unused, or missing just because it is not in the diff. If the diff note says it was truncated, do not infer issues from omitted lines.",
243
326
  "Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in the diff. For a removed line, cite the nearest NEW line number shown next to it.",
244
- "Evidence: every finding must carry an `evidence` string containing the exact code of the line it is about, copied character-for-character from the diff below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in the diff will be discarded.",
327
+ `Evidence: every finding must carry an \`evidence\` string containing the exact code of the line it is about, copied character-for-character from the UNIFIED DIFF below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in the diff will be discarded${fileContext ? ", and the full-file context below is NOT the diff -- a line quoted from it counts as no evidence at all" : ""}.`,
328
+ INTENT_CHECK_INSTRUCTION,
245
329
  "Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.",
246
330
  "",
247
331
  `## Output JSON Schema (STRICTLY REQUIRED)`,
@@ -267,7 +351,8 @@ ${rules}`,
267
351
  }`,
268
352
  "",
269
353
  "Unified diff:",
270
- renderFileDiff(input.file)
354
+ renderFileDiff(input.file),
355
+ ...fileContext ? ["", fileContext] : []
271
356
  ].join("\n");
272
357
  return { systemPrompt, userPrompt };
273
358
  }
@@ -279,7 +364,7 @@ function buildBatchReviewPrompts(input) {
279
364
  const languages = new Set(files.map((file) => getLanguageForFile(file.path)));
280
365
  const uniformLanguage = languages.size === 1 ? [...languages][0] : void 0;
281
366
  const systemPrompt = buildFileReviewSystemPrompt(input.config, uniformLanguage?.persona, { multiFile: true });
282
- const prContext = renderPrContext(input.prDescription);
367
+ const intentBlock = renderIntentBlock(input);
283
368
  const exemplars = renderExemplars(input.rejectedExemplars);
284
369
  const pathList = files.map((file) => `- ${file.path}`).join("\n");
285
370
  const fileBlocks = files.flatMap((file, index) => [
@@ -290,8 +375,7 @@ function buildBatchReviewPrompts(input) {
290
375
  renderFileDiff(file)
291
376
  ]);
292
377
  const userPrompt = [
293
- `PR title: ${input.prTitle ?? "Untitled PR"}`,
294
- ...prContext ? [prContext] : [],
378
+ intentBlock,
295
379
  ...exemplars ? [exemplars] : [],
296
380
  `You are reviewing ${files.length} files in ONE response. Return exactly ${files.length} entries in "files", one per path, in this order:
297
381
  ${pathList}`,
@@ -301,6 +385,7 @@ ${pathList}`,
301
385
  "File scoping: each finding belongs to exactly ONE file. Put it inside that file's entry, set that file's path in `absolute_file_path`, and quote evidence from that file's diff only. Never report a finding about one file inside another file's entry, and never quote a line from a different file.",
302
386
  "Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in that file's diff. For a removed line, cite the nearest NEW line number shown next to it.",
303
387
  "Evidence: every finding must carry an `evidence` string containing the exact code of the line it is about, copied character-for-character from its own file's diff below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in that file's diff will be discarded.",
388
+ INTENT_CHECK_INSTRUCTION,
304
389
  "Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.",
305
390
  "",
306
391
  `## Output JSON Schema (STRICTLY REQUIRED)`,
@@ -328,7 +413,10 @@ function renderFileDiff(file) {
328
413
  }
329
414
 
330
415
  export {
416
+ changelogExcerptFromDiff,
417
+ wantsFileContext,
331
418
  generatorFindingCap,
419
+ reviewBreadth,
332
420
  buildReviewResponseSchema,
333
421
  buildBatchReviewResponseSchema,
334
422
  buildFileReviewSystemPromptBase,
@@ -338,4 +426,4 @@ export {
338
426
  buildBatchReviewPrompts,
339
427
  renderFileDiff
340
428
  };
341
- //# sourceMappingURL=chunk-MMVQPGK3.js.map
429
+ //# sourceMappingURL=chunk-ONJZXUQT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prompts/file-review.ts","../src/prompts/review-context.ts"],"sourcesContent":["import { claimTypes, type RepoConfig } from '@codraoss/schema';\nimport type { FileDiff } from '../diff';\nimport type { ModelResponseSchema } from '../ports/model';\nimport { getLanguageForFile } from './languages';\nimport {\n INTENT_CHECK_INSTRUCTION,\n renderFileContext,\n renderIntentBlock,\n} from './review-context';\nimport {\n EXEMPLAR_BLOCK_CHARS,\n} from '../constants';\n\nexport { changelogExcerptFromDiff, wantsFileContext } from './review-context';\n\n// Pre-review_breadth fallback: generator was allowed ~2x the posted cap.\nexport function generatorFindingCap(maxComments: number): number {\n return Math.max(1, maxComments * 2);\n}\n\n/** Internal candidate cap upstream of posting; falls back for job snapshots queued before this field existed. */\nexport function reviewBreadth(config: Pick<RepoConfig['review'], 'max_comments'> & { review_breadth?: number }): number {\n return config.review_breadth ?? generatorFindingCap(config.max_comments);\n}\n\nfunction findingItemSchema() {\n return {\n type: 'object',\n additionalProperties: false,\n required: ['evidence', 'code_location', 'claim_type', 'title', 'body', 'priority'],\n properties: {\n evidence: { type: 'string' },\n code_location: {\n type: 'object',\n additionalProperties: false,\n properties: {\n absolute_file_path: { type: 'string' },\n line: { type: 'integer', minimum: 1 },\n line_range: {\n type: 'object',\n additionalProperties: false,\n required: ['start', 'end'],\n properties: {\n start: { type: 'integer', minimum: 1 },\n end: { type: 'integer', minimum: 1 },\n },\n },\n },\n anyOf: [\n { required: ['line'] },\n { required: ['line_range'] },\n ],\n },\n claim_type: { type: 'string', enum: [...claimTypes] },\n title: { type: 'string', maxLength: 100 },\n body: { type: 'string' },\n priority: { type: 'integer', minimum: 0, maximum: 4 },\n code_suggestion: { type: 'string' },\n },\n };\n}\n\nexport function buildReviewResponseSchema(findingCap: number): ModelResponseSchema {\n return {\n name: 'codra_file_review',\n schema: {\n type: 'object',\n additionalProperties: false,\n required: ['findings', 'overall_explanation', 'overall_correctness', 'overall_confidence_score'],\n properties: {\n findings: {\n type: 'array',\n maxItems: Math.max(1, findingCap),\n items: findingItemSchema(),\n },\n overall_explanation: { type: 'string' },\n overall_correctness: { type: 'string', enum: ['patch is correct', 'patch is incorrect'] },\n overall_confidence_score: { type: 'number', minimum: 0, maximum: 1 },\n },\n },\n };\n}\n\nexport function buildBatchReviewResponseSchema(findingCap: number, fileCount: number): ModelResponseSchema {\n return {\n name: 'codra_batch_review',\n schema: {\n type: 'object',\n additionalProperties: false,\n required: ['files', 'overall_confidence_score'],\n properties: {\n files: {\n type: 'array',\n maxItems: fileCount,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['absolute_file_path', 'findings', 'overall_explanation', 'overall_correctness'],\n properties: {\n absolute_file_path: { type: 'string' },\n findings: { type: 'array', items: findingItemSchema() },\n overall_explanation: { type: 'string' },\n overall_correctness: { type: 'string', enum: ['patch is correct', 'patch is incorrect'] },\n },\n },\n },\n overall_confidence_score: { type: 'number', minimum: 0, maximum: 1 },\n },\n },\n };\n}\n\nconst SINGLE_FILE_SCHEMA_FORMAT = `{\n \"findings\": [\n {\n \"evidence\": \"<the exact line of code from the diff this finding is about>\",\n \"code_location\": {\n \"line\": number,\n \"line_range\": { \"start\": number, \"end\": number }\n },\n \"claim_type\": \"<one of the claim types listed above>\",\n \"title\": \"<Plain title, NO tags/emoji>\",\n \"body\": \"<Explanation of the concrete defect and its impact>\",\n \"priority\": 0 | 1 | 2 | 3 | 4,\n \"code_suggestion\": \"Optional replacement code\"\n }\n ],\n \"overall_explanation\": \"Summary\",\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\",\n \"overall_confidence_score\": number (0 to 1)\n}`;\n\nconst MULTI_FILE_SCHEMA_FORMAT = `{\n \"files\": [\n {\n \"absolute_file_path\": \"<exact path of one of the files listed below, copied character-for-character>\",\n \"findings\": [\n {\n \"evidence\": \"<the exact line of code from THIS file's diff this finding is about>\",\n \"code_location\": {\n \"absolute_file_path\": \"<the same path as above>\",\n \"line\": number,\n \"line_range\": { \"start\": number, \"end\": number }\n },\n \"claim_type\": \"<one of the claim types listed above>\",\n \"title\": \"<Plain title, NO tags/emoji>\",\n \"body\": \"<Explanation of the concrete defect and its impact>\",\n \"priority\": 0 | 1 | 2 | 3 | 4,\n \"code_suggestion\": \"Optional replacement code\"\n }\n ],\n \"overall_explanation\": \"Summary for THIS file\",\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\"\n }\n ],\n \"overall_confidence_score\": number (0 to 1)\n}`;\n\nexport function buildFileReviewSystemPromptBase(opts?: { multiFile?: boolean; fileContext?: boolean }): string {\n const multi = opts?.multiFile === true;\n\n const singleFileScope = opts?.fileContext === true\n ? '- You can see the diff below and, after it, the full content of that one file. You cannot see the rest of the repository. Findings must still be about lines the diff CHANGED; the file content is there to tell you what the surrounding code does, not to be reviewed.'\n : '- You can see ONLY the diff below, not the whole file or the rest of the repository.';\n\n const contextScope = multi\n ? `- You can see ONLY the diffs below, not the whole files or the rest of the repository.\n- Each file below is INDEPENDENT. A finding about one file must be grounded in a line from THAT file's diff, and must be reported inside that file's entry. Never carry a claim from one file to another, and never assume two files interact unless both diffs show it.`\n : singleFileScope;\n\n const evidenceSource = multi\n ? `the single line of code the finding is about, copied VERBATIM from that file's diff below.`\n : 'the single line of code the finding is about, copied VERBATIM from the diff below.';\n\n const capRule = multi\n ? '4. Return at most {{MAX_COMMENTS}} findings PER FILE, most severe first. Keep each body under 160 words.'\n : '4. Return at most {{MAX_COMMENTS}} findings, most severe first. Keep each body under 160 words.';\n\n const emptyRule = multi\n ? `5. Return exactly one entry per file listed below, in the same order, and never omit a file. Review each file's diff with the same care you would give it if it were the only file in front of you. An empty findings array is a positive claim that this diff introduces no defect, so return one only when that is true. Do not pad, and do not withhold.`\n : '5. If the diff genuinely introduces no defect, return an empty findings array and a short explanation. Do not pad, and do not withhold.';\n\n return `You are a world-class software engineer performing a precise, high-signal code review.\nYour goal is to find REAL defects (bugs, security vulnerabilities, and performance problems) introduced by the diff. Every finding must be grounded in a line you can quote from the diff.\n\n### CONTEXT EXTENDS (read carefully, this prevents false positives):\n${contextScope}\n- You cannot see which files import this one. Never predict that a change breaks callers, importers, \"other modules\" or \"external files\" -- a removed \\`export\\`, a renamed symbol or a changed signature may have no consumers at all, and you have no way to check. The same applies in reverse to a function whose body is not shown: do not assume what it does with its errors or its return value.\n- Assume every third-party package is at the version this project pins, and that its API is whatever that version provides. Never claim a library \"does not expose\", \"does not provide\" or \"does not support\" something; your training data predates the installed version.\n- Assume the language, runtime and build target are whatever the project already uses successfully. A syntax or standard-library method appearing in the diff is available in this project by construction -- the code around it already compiles and ships. Do not raise compatibility, polyfill, transpilation, engine-version or server-side-rendering concerns unless the diff itself shows the incompatibility.\n- Two async facts that are frequently misread. \\`return somePromise()\\` inside an \\`async\\` function IS awaited by whoever awaits that function; it is equivalent to \\`return await\\` except inside \\`try\\`/\\`finally\\`, so it is not a missing await and not a floating promise. And \\`void someAsyncCall()\\` is deliberate fire-and-forget: if the called function handles its own errors, there is no unhandled rejection to report.\n\n### WHAT TO REPORT:\n- Report anything a senior engineer reviewing this diff would want to investigate: a bug, a security hole, a performance problem, a resource leak, an unhandled failure, a broken invariant.\n- You do not need to be certain. A finding you can ground in a quoted line is worth raising; every finding is independently checked against the diff afterwards, and a wrong one is discarded at no cost to you. A defect you decline to mention is simply lost.\n\n### EVIDENCE (mandatory, a finding without it cannot be posted):\n- Every finding MUST include \"evidence\": ${evidenceSource}\n- Copy the code exactly as it appears. Do NOT include the two line-number columns or the +/- marker, do NOT paraphrase, reformat, shorten, or invent code.\n- If you cannot quote a specific line from the diff that exhibits the problem, you do not have a finding. Omit it.\n\n### CLAIM TYPE (required, pick the one that fits, or \"other\"):\n${claimTypes.join(', ')}\n- This is a label for the KIND of defect. It does not license the claim: only report a type if the\n diff actually shows it. Picking a type the code cannot exhibit makes the finding easy to discard.\n- If nothing fits, use \"other\". Do not stretch a label to fit.\n- NEVER claim that a package, action, tag or version \"does not exist\", or that a config key is invalid. You cannot know what was released after your training data, and a step pinned to a commit SHA resolves by that SHA regardless of the version written beside it. Such claims are discarded.\n- Label honestly. The type you choose does not affect whether a finding is accepted; an inaccurate label only makes a real defect harder to act on.\n\n### OUTPUT RULES:\n1. Output MUST be valid JSON, EXACTLY ONE object matching the schema below.\n2. DO NOT output any conversational text, source code, or diff hunks before or after the JSON.\n3. Prioritize by severity: 0 = P0 critical, 1 = P1 high, 2 = P2 medium, 3 = P3 low, 4 = nit (cosmetic/trivial). Set priority honestly; do not inflate. Use 4 for anything a reviewer would prefix with \"nit:\".\n A finding that rests on a condition you cannot check from the diff -- \"if this runs on an older engine\", \"if another module imports this\", \"depending on the caller\" -- is at most priority 3, never 0 or 1, however serious the consequence would be if the condition held. Certainty about the consequence is not certainty about the premise.\n${capRule}\n${emptyRule}\n\n### SCHEMA FORMAT:\n${multi ? MULTI_FILE_SCHEMA_FORMAT : SINGLE_FILE_SCHEMA_FORMAT}\n\nIdentify security risks such as XSS, SQLi, CSRF, insecure randomness, and data leaks that the diff actually introduces.`;\n}\n\nexport const fileReviewSystemPromptBase = buildFileReviewSystemPromptBase();\n\nexport function buildFileReviewSystemPrompt(\n config: RepoConfig['review'],\n languagePersona?: string,\n opts?: { multiFile?: boolean; fileContext?: boolean },\n) {\n const persona = languagePersona ? ` as ${languagePersona}` : '';\n const prompt = buildFileReviewSystemPromptBase(opts)\n .replace('{{MAX_COMMENTS}}', reviewBreadth(config).toString());\n return `You are a world-class professional senior code reviewer${persona}. ${prompt}`;\n}\n\nexport type RejectedExemplar = { title: string; claimType?: string | null };\n\n\n\nfunction renderExemplars(exemplars: readonly RejectedExemplar[] | undefined): string | null {\n if (!exemplars?.length) return null;\n\n const lines: string[] = [];\n let used = 0;\n for (const exemplar of exemplars) {\n const line = `- ${exemplar.title}${exemplar.claimType ? ` (${exemplar.claimType})` : ''}`;\n if (used + line.length > EXEMPLAR_BLOCK_CHARS) break;\n lines.push(line);\n used += line.length;\n }\n if (lines.length === 0) return null;\n\n const heading = 'Findings a reviewer on THIS repository has already rejected. Do not report things like these:';\n return [heading, ...lines].join('\\n');\n}\nfunction renderCustomRules(config: RepoConfig['review']): string {\n const rules = config.custom_rules.length > 0 ? config.custom_rules.map((rule) => `- ${rule}`).join('\\n') : '- None';\n return `Custom rules:\\n${rules}`;\n}\n\nfunction renderLanguageGuidelines(path: string): string {\n const languageInfo = getLanguageForFile(path);\n const guidelineHeader = 'Specific Guidelines (check the diff against each of these)';\n return languageInfo\n ? `Language: ${languageInfo.language}\\n${guidelineHeader}:\\n${languageInfo.guidelines.map(g => `- ${g}`).join('\\n')}`\n : 'Language: Generic\\nSpecific Guidelines: Follow general best practices.';\n}\n\nexport function buildFileReviewPrompts(input: {\n file: FileDiff;\n fileContext?: string | null;\n prTitle: string | null;\n prDescription: string | null;\n changelogExcerpt?: string | null;\n config: RepoConfig['review'];\n rejectedExemplars?: readonly RejectedExemplar[];\n}) {\n const languageInfo = getLanguageForFile(input.file.path);\n const rules = input.config.custom_rules.length > 0 ? input.config.custom_rules.map((rule) => `- ${rule}`).join('\\n') : '- None';\n const intentBlock = renderIntentBlock(input);\n const fileContext = input.fileContext ? renderFileContext(input.file, input.fileContext) : null;\n\n const systemPrompt = buildFileReviewSystemPrompt(input.config, languageInfo?.persona, {\n fileContext: fileContext !== null,\n });\n const languageGuidelines = renderLanguageGuidelines(input.file.path);\n\n const exemplars = renderExemplars(input.rejectedExemplars);\n\n const userPrompt = [\n intentBlock,\n ...(exemplars ? [exemplars] : []),\n `File path: ${input.file.path}`,\n languageGuidelines,\n `Custom rules:\\n${rules}`,\n 'Review ONLY the diff shown below. You cannot see the rest of the file or repository - do not report something as undefined, unimported, unused, or missing just because it is not in the diff. If the diff note says it was truncated, do not infer issues from omitted lines.',\n 'Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in the diff. For a removed line, cite the nearest NEW line number shown next to it.',\n `Evidence: every finding must carry an \\`evidence\\` string containing the exact code of the line it is about, copied character-for-character from the UNIFIED DIFF below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in the diff will be discarded${fileContext ? ', and the full-file context below is NOT the diff -- a line quoted from it counts as no evidence at all' : ''}.`,\n INTENT_CHECK_INSTRUCTION,\n 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.',\n '',\n `## Output JSON Schema (STRICTLY REQUIRED)`,\n `{\n \"findings\": [\n {\n \"evidence\": \"<exact line of code copied verbatim from the diff below, without the line-number columns or +/- marker>\",\n \"code_location\": {\n \"absolute_file_path\": \"${input.file.path}\",\n \"line\": <int>,\n \"line_range\": {\"start\": <int>, \"end\": <int>}\n },\n \"claim_type\": \"<${claimTypes.join(' | ')}>\",\n \"title\": \"<Plain title>\",\n \"body\": \"<Technical explanation>\",\n \"priority\": <0|1|2|3|4>,\n \"code_suggestion\": \"string\"\n }\n ],\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\",\n \"overall_explanation\": \"Summary\",\n \"overall_confidence_score\": <float 0.0-1.0>\n}`,\n '',\n 'Unified diff:',\n renderFileDiff(input.file),\n ...(fileContext ? ['', fileContext] : []),\n ].join('\\n');\n\n return { systemPrompt, userPrompt };\n}\n\nfunction packFileHeader(file: FileDiff, index: number, total: number): string {\n return `===== FILE ${index + 1} of ${total}: ${file.path} =====`;\n}\n\nexport function buildBatchReviewPrompts(input: {\n files: readonly FileDiff[];\n prTitle: string | null;\n prDescription: string | null;\n changelogExcerpt?: string | null;\n config: RepoConfig['review'];\n rejectedExemplars?: readonly RejectedExemplar[];\n}) {\n const files = input.files;\n\n const languages = new Set(files.map((file) => getLanguageForFile(file.path)));\n const uniformLanguage = languages.size === 1 ? [...languages][0] : undefined;\n\n const systemPrompt = buildFileReviewSystemPrompt(input.config, uniformLanguage?.persona, { multiFile: true });\n\n const intentBlock = renderIntentBlock(input);\n const exemplars = renderExemplars(input.rejectedExemplars);\n const pathList = files.map((file) => `- ${file.path}`).join('\\n');\n\n const fileBlocks = files.flatMap((file, index) => [\n '',\n packFileHeader(file, index, files.length),\n ...(uniformLanguage ? [] : [renderLanguageGuidelines(file.path)]),\n 'Unified diff:',\n renderFileDiff(file),\n ]);\n\n const userPrompt = [\n intentBlock,\n ...(exemplars ? [exemplars] : []),\n `You are reviewing ${files.length} files in ONE response. Return exactly ${files.length} entries in \"files\", one per path, in this order:\\n${pathList}`,\n ...(uniformLanguage ? [renderLanguageGuidelines(files[0].path)] : []),\n renderCustomRules(input.config),\n 'Review ONLY the diffs shown below. You cannot see the rest of any file or the repository - do not report something as undefined, unimported, unused, or missing just because it is not in a diff. If a diff note says it was truncated, do not infer issues from omitted lines.',\n 'File scoping: each finding belongs to exactly ONE file. Put it inside that file\\'s entry, set that file\\'s path in `absolute_file_path`, and quote evidence from that file\\'s diff only. Never report a finding about one file inside another file\\'s entry, and never quote a line from a different file.',\n 'Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in that file\\'s diff. For a removed line, cite the nearest NEW line number shown next to it.',\n 'Evidence: every finding must carry an `evidence` string containing the exact code of the line it is about, copied character-for-character from its own file\\'s diff below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in that file\\'s diff will be discarded.',\n INTENT_CHECK_INSTRUCTION,\n 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.',\n '',\n `## Output JSON Schema (STRICTLY REQUIRED)`,\n MULTI_FILE_SCHEMA_FORMAT,\n ...fileBlocks,\n ].join('\\n');\n\n return { systemPrompt, userPrompt };\n}\n\nexport function renderFileDiff(file: FileDiff) {\n const lines = [`diff --git a/${file.previousPath ?? file.path} b/${file.path}`];\n for (const hunk of file.hunks) {\n lines.push(hunk.header);\n for (const line of hunk.lines) {\n const prefix = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';\n const left = line.oldLineNumber ?? '';\n const right = line.newLineNumber ?? '';\n lines.push(`${String(left).padStart(4, ' ')} ${String(right).padStart(4, ' ')} ${prefix}${line.content}`);\n }\n }\n\n if (file.isTruncated) {\n lines.push('');\n lines.push(`[NOTE: This diff has been truncated from ${file.originalLineCount} lines to ${file.lineCount} lines for brevity.]`);\n }\n\n return lines.join('\\n');\n}\n","import type { FileDiff } from '../diff';\nimport { isChangelogPath } from './languages';\nimport {\n CHANGELOG_EXCERPT_CHARS,\n FILE_CONTEXT_CHAR_BUDGET,\n FILE_CONTEXT_WINDOW_LINES,\n FRAGMENTED_HUNK_THRESHOLD,\n FRAGMENTED_MIN_LINES,\n PACKABLE_MAX_DIFF_LINES,\n PR_DESCRIPTION_CHARS,\n} from '../constants';\n\n// Prompt blocks that surround a diff: what the change is for, and what the rest of the file looks like.\n\n\n\n\nfunction clip(text: string, limit: number): string {\n return text.length > limit ? `${text.slice(0, limit)}…` : text;\n}\n\n// Must avoid the `===== FILE ` delimiter and lines starting `Language: `: the batch prompt splits on those.\nexport function renderIntentBlock(input: {\n prTitle: string | null;\n prDescription: string | null;\n changelogExcerpt?: string | null;\n}): string {\n const description = input.prDescription?.trim();\n const changelog = input.changelogExcerpt?.trim();\n\n return [\n '## PR INTENT (what the author set out to do)',\n `Title: ${input.prTitle ?? 'Untitled PR'}`,\n ...(description ? ['Description:', clip(description, PR_DESCRIPTION_CHARS)] : []),\n ...(changelog ? ['Changelog lines added by this PR:', clip(changelog, CHANGELOG_EXCERPT_CHARS)] : []),\n 'Behaviour that serves this stated intent is deliberate. Do not report it as an accident, an oversight, or a regression.',\n ].join('\\n');\n}\n\nexport function changelogExcerptFromDiff(files: readonly FileDiff[]): string | null {\n const added: string[] = [];\n let used = 0;\n\n for (const file of files) {\n if (!isChangelogPath(file.path)) continue;\n for (const hunk of file.hunks) {\n for (const line of hunk.lines) {\n if (line.kind !== 'add') continue;\n const text = line.content.trim();\n if (!text) continue;\n if (used + text.length > CHANGELOG_EXCERPT_CHARS) {\n return added.length > 0 ? added.join('\\n') : null;\n }\n added.push(text);\n used += text.length + 1;\n }\n }\n }\n\n return added.length > 0 ? added.join('\\n') : null;\n}\n\nexport function wantsFileContext(\n file: Pick<FileDiff, 'lineCount' | 'isNew' | 'isDeleted' | 'isBinary'> & { hunks?: FileDiff['hunks'] },\n fullFileContext: boolean,\n gate: { compactPrompt?: boolean } = {},\n): boolean {\n if (!fullFileContext) return false;\n if (gate.compactPrompt) return false;\n if (file.isNew || file.isDeleted || file.isBinary) return false;\n if (file.lineCount > PACKABLE_MAX_DIFF_LINES) return true;\n\n // Must stay in step with `planReviewUnits`: a promoted file given no context wastes a model call.\n return (file.hunks?.length ?? 0) >= FRAGMENTED_HUNK_THRESHOLD && file.lineCount >= FRAGMENTED_MIN_LINES;\n}\n\n// Windowed per chunk's own hunks so a chunked file doesn't repeat the whole block MAX_CHUNKS times.\nexport function renderFileContext(file: FileDiff, content: string): string | null {\n const lines = content.split('\\n');\n\n let lowest = Number.POSITIVE_INFINITY;\n let highest = 0;\n for (const hunk of file.hunks) {\n for (const line of hunk.lines) {\n if (typeof line.newLineNumber !== 'number') continue;\n lowest = Math.min(lowest, line.newLineNumber);\n highest = Math.max(highest, line.newLineNumber);\n }\n }\n if (!Number.isFinite(lowest)) return null;\n\n const start = Math.max(1, lowest - FILE_CONTEXT_WINDOW_LINES);\n const end = Math.min(lines.length, highest + FILE_CONTEXT_WINDOW_LINES);\n\n const numbered: string[] = [];\n let used = 0;\n for (let n = start; n <= end; n++) {\n const rendered = `${n}\\t${lines[n - 1] ?? ''}`;\n if (used + rendered.length > FILE_CONTEXT_CHAR_BUDGET) break;\n numbered.push(rendered);\n used += rendered.length + 1;\n }\n if (numbered.length === 0) return null;\n\n const last = start + numbered.length - 1;\n const partial = start > 1 || last < lines.length;\n return [\n `Full file after the change, lines ${start}-${last}${partial ? ` of ${lines.length}` : ''} (CONTEXT ONLY, not reviewable):`,\n ...numbered,\n ].join('\\n');\n}\n\nexport const INTENT_CHECK_INSTRUCTION =\n 'Intent check: every finding must survive a comparison with the PR INTENT above. If what you are about to flag IS the stated intent, it is not a finding - drop it. Otherwise open the `body` with one line saying how the problem differs from what the author set out to do.';\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-WSK45HW4.js";
3
+ } from "./chunk-Z5B5X7QP.js";
4
4
 
5
5
  // src/token-tracker.ts
6
6
  var TokenTracker = class {
@@ -87,4 +87,4 @@ var TokenTracker = class {
87
87
  export {
88
88
  TokenTracker
89
89
  };
90
- //# sourceMappingURL=chunk-PVXFURDR.js.map
90
+ //# sourceMappingURL=chunk-QPPLO2YV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/token-tracker.ts"],"sourcesContent":["import { logger } from './logger';\n\nexport interface TokenUsage {\n input: number;\n output: number;\n}\n\nexport interface ModelUsage extends TokenUsage {\n model: string;\n calls: number;\n}\n\nexport type WastedAttemptReason = 'rate-limited' | 'error';\n\nexport interface WastedUsage {\n attempts: number;\n estimatedInput: number;\n skips: number;\n byReason: Record<string, number>;\n}\n\nexport class TokenTracker {\n private usage: Map<string, ModelUsage> = new Map();\n private wasted = { attempts: 0, estimatedInput: 0, skips: 0 };\n private wastedByReason: Map<string, number> = new Map();\n private subrequests = 0;\n private readonly MAX_SUBREQUESTS = 50;\n private readonly SAFE_MARGIN = 25;\n\n incrementSubrequests(count = 1) {\n this.subrequests += count;\n }\n\n getSubrequestCount() {\n return this.subrequests;\n }\n\n hasRemainingSubrequests(needed = 1) {\n return this.subrequests + needed <= this.MAX_SUBREQUESTS;\n }\n\n isNearLimit() {\n return this.subrequests >= this.MAX_SUBREQUESTS - this.SAFE_MARGIN;\n }\n\n remainingSafeBudget() {\n return Math.max(0, this.MAX_SUBREQUESTS - this.SAFE_MARGIN - this.subrequests);\n }\n\n record(model: string, input: number, output: number) {\n const existing = this.usage.get(model) || { model, input: 0, output: 0, calls: 0 };\n \n this.usage.set(model, {\n model,\n input: existing.input + input,\n output: existing.output + output,\n calls: existing.calls + 1,\n });\n\n logger.debug(`Token usage recorded for ${model}`, { \n input, \n output, \n totalInput: existing.input + input,\n totalOutput: existing.output + output\n });\n }\n\n recordFailedAttempt(model: string, estimatedInputTokens: number, reason: WastedAttemptReason) {\n this.wasted.attempts += 1;\n this.wasted.estimatedInput += estimatedInputTokens;\n this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + 1);\n\n logger.debug(`Wasted model attempt on ${model}`, { estimatedInput: estimatedInputTokens, reason });\n }\n\n recordSkippedCall(model: string, reason: string) {\n this.wasted.skips += 1;\n\n logger.debug(`Skipped model call on ${model}`, { reason });\n }\n\n getWasted(): WastedUsage {\n return { ...this.wasted, byReason: Object.fromEntries(this.wastedByReason) };\n }\n\n getTotalUsage(): TokenUsage {\n let input = 0;\n let output = 0;\n for (const modelUsage of this.usage.values()) {\n input += modelUsage.input;\n output += modelUsage.output;\n }\n return { input, output };\n }\n\n getBreakdown(): ModelUsage[] {\n return Array.from(this.usage.values());\n }\n\n merge(other: TokenTracker) {\n for (const usage of other.getBreakdown()) {\n this.record(usage.model, usage.input, usage.output);\n }\n\n const otherWasted = other.getWasted();\n this.wasted.attempts += otherWasted.attempts;\n this.wasted.estimatedInput += otherWasted.estimatedInput;\n this.wasted.skips += otherWasted.skips;\n for (const [reason, count] of Object.entries(otherWasted.byReason)) {\n this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + count);\n }\n }\n\n reset() {\n this.usage.clear();\n this.wasted = { attempts: 0, estimatedInput: 0, skips: 0 };\n this.wastedByReason.clear();\n }\n}\n"],"mappings":";;;;;AAqBO,IAAM,eAAN,MAAmB;AAAA,EAChB,QAAiC,oBAAI,IAAI;AAAA,EACzC,SAAS,EAAE,UAAU,GAAG,gBAAgB,GAAG,OAAO,EAAE;AAAA,EACpD,iBAAsC,oBAAI,IAAI;AAAA,EAC9C,cAAc;AAAA,EACL,kBAAkB;AAAA,EAClB,cAAc;AAAA,EAE/B,qBAAqB,QAAQ,GAAG;AAC9B,SAAK,eAAe;AAAA,EACtB;AAAA,EAEA,qBAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,wBAAwB,SAAS,GAAG;AAClC,WAAO,KAAK,cAAc,UAAU,KAAK;AAAA,EAC3C;AAAA,EAEA,cAAc;AACZ,WAAO,KAAK,eAAe,KAAK,kBAAkB,KAAK;AAAA,EACzD;AAAA,EAEA,sBAAsB;AACpB,WAAO,KAAK,IAAI,GAAG,KAAK,kBAAkB,KAAK,cAAc,KAAK,WAAW;AAAA,EAC/E;AAAA,EAEA,OAAO,OAAe,OAAe,QAAgB;AACnD,UAAM,WAAW,KAAK,MAAM,IAAI,KAAK,KAAK,EAAE,OAAO,OAAO,GAAG,QAAQ,GAAG,OAAO,EAAE;AAEjF,SAAK,MAAM,IAAI,OAAO;AAAA,MACpB;AAAA,MACA,OAAO,SAAS,QAAQ;AAAA,MACxB,QAAQ,SAAS,SAAS;AAAA,MAC1B,OAAO,SAAS,QAAQ;AAAA,IAC1B,CAAC;AAED,WAAO,MAAM,4BAA4B,KAAK,IAAI;AAAA,MAChD;AAAA,MACA;AAAA,MACA,YAAY,SAAS,QAAQ;AAAA,MAC7B,aAAa,SAAS,SAAS;AAAA,IACjC,CAAC;AAAA,EACH;AAAA,EAEA,oBAAoB,OAAe,sBAA8B,QAA6B;AAC5F,SAAK,OAAO,YAAY;AACxB,SAAK,OAAO,kBAAkB;AAC9B,SAAK,eAAe,IAAI,SAAS,KAAK,eAAe,IAAI,MAAM,KAAK,KAAK,CAAC;AAE1E,WAAO,MAAM,2BAA2B,KAAK,IAAI,EAAE,gBAAgB,sBAAsB,OAAO,CAAC;AAAA,EACnG;AAAA,EAEA,kBAAkB,OAAe,QAAgB;AAC/C,SAAK,OAAO,SAAS;AAErB,WAAO,MAAM,yBAAyB,KAAK,IAAI,EAAE,OAAO,CAAC;AAAA,EAC3D;AAAA,EAEA,YAAyB;AACvB,WAAO,EAAE,GAAG,KAAK,QAAQ,UAAU,OAAO,YAAY,KAAK,cAAc,EAAE;AAAA,EAC7E;AAAA,EAEA,gBAA4B;AAC1B,QAAI,QAAQ;AACZ,QAAI,SAAS;AACb,eAAW,cAAc,KAAK,MAAM,OAAO,GAAG;AAC5C,eAAS,WAAW;AACpB,gBAAU,WAAW;AAAA,IACvB;AACA,WAAO,EAAE,OAAO,OAAO;AAAA,EACzB;AAAA,EAEA,eAA6B;AAC3B,WAAO,MAAM,KAAK,KAAK,MAAM,OAAO,CAAC;AAAA,EACvC;AAAA,EAEA,MAAM,OAAqB;AACzB,eAAW,SAAS,MAAM,aAAa,GAAG;AACxC,WAAK,OAAO,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM;AAAA,IACpD;AAEA,UAAM,cAAc,MAAM,UAAU;AACpC,SAAK,OAAO,YAAY,YAAY;AACpC,SAAK,OAAO,kBAAkB,YAAY;AAC1C,SAAK,OAAO,SAAS,YAAY;AACjC,eAAW,CAAC,QAAQ,KAAK,KAAK,OAAO,QAAQ,YAAY,QAAQ,GAAG;AAClE,WAAK,eAAe,IAAI,SAAS,KAAK,eAAe,IAAI,MAAM,KAAK,KAAK,KAAK;AAAA,IAChF;AAAA,EACF;AAAA,EAEA,QAAQ;AACN,SAAK,MAAM,MAAM;AACjB,SAAK,SAAS,EAAE,UAAU,GAAG,gBAAgB,GAAG,OAAO,EAAE;AACzD,SAAK,eAAe,MAAM;AAAA,EAC5B;AACF;","names":[]}