@codraoss/core 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-XD6CGYHO.js → chunk-2TF4DSRX.js} +65 -42
- package/dist/chunk-2TF4DSRX.js.map +1 -0
- package/dist/{chunk-ILZUCIZH.js → chunk-754NO4DH.js} +7 -7
- package/dist/chunk-754NO4DH.js.map +1 -0
- package/dist/{chunk-RQ5S67XB.js → chunk-FKRCBUWZ.js} +8 -2
- package/dist/chunk-FKRCBUWZ.js.map +1 -0
- package/dist/{chunk-OIQRTDYR.js → chunk-I5S3NBFL.js} +20 -4
- package/dist/chunk-I5S3NBFL.js.map +1 -0
- package/dist/{chunk-U3VKVDZ7.js → chunk-IKVUW4WC.js} +92 -19
- package/dist/chunk-IKVUW4WC.js.map +1 -0
- package/dist/{chunk-R2HRWFRH.js → chunk-JAF7JIZQ.js} +1 -1
- package/dist/chunk-JAF7JIZQ.js.map +1 -0
- package/dist/{chunk-V3RF6GQW.js → chunk-KTDBDKL5.js} +1 -1
- package/dist/chunk-KTDBDKL5.js.map +1 -0
- package/dist/{chunk-MMVQPGK3.js → chunk-ONJZXUQT.js} +112 -24
- package/dist/chunk-ONJZXUQT.js.map +1 -0
- package/dist/{chunk-PVXFURDR.js → chunk-QPPLO2YV.js} +2 -2
- package/dist/chunk-QPPLO2YV.js.map +1 -0
- package/dist/{chunk-FGXA7JNR.js → chunk-W3RY755V.js} +14 -2
- package/dist/chunk-W3RY755V.js.map +1 -0
- package/dist/{chunk-NSTV4TRP.js → chunk-XPYVLHSX.js} +1 -1
- package/dist/chunk-XPYVLHSX.js.map +1 -0
- package/dist/{chunk-WSK45HW4.js → chunk-Z5B5X7QP.js} +1 -1
- package/dist/chunk-Z5B5X7QP.js.map +1 -0
- package/dist/claim-checks.d.ts +8 -13
- package/dist/claim-checks.js +2 -2
- package/dist/diff/index.d.ts +3 -3
- package/dist/diff/index.js +2 -1
- package/dist/fingerprint.js +1 -1
- package/dist/{index-BTDWXDck.d.ts → index-CrtHz4vN.d.ts} +3 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.js +240 -61
- package/dist/index.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/{model-gRDTk8yk.d.ts → model-BkqfyVh9.d.ts} +27 -6
- package/dist/model-output/index.d.ts +2 -2
- package/dist/model-output/index.js +9 -9
- package/dist/ports/index.d.ts +28 -6
- package/dist/{position-h_jdn6ZH.d.ts → position-zZl8KGVu.d.ts} +1 -1
- package/dist/prompts/file-review.d.ts +3 -3
- package/dist/prompts/file-review.js +11 -5
- package/dist/prompts/languages.d.ts +2 -1
- package/dist/prompts/languages.js +5 -3
- package/dist/prompts/verify.d.ts +1 -1
- package/dist/prompts/verify.js +1 -1
- package/dist/rules/detect.d.ts +1 -1
- package/dist/rules/detect.js +5 -5
- package/dist/rules/table.js +1 -1
- package/dist/token-tracker.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-FGXA7JNR.js.map +0 -1
- package/dist/chunk-ILZUCIZH.js.map +0 -1
- package/dist/chunk-MMVQPGK3.js.map +0 -1
- package/dist/chunk-NSTV4TRP.js.map +0 -1
- package/dist/chunk-OIQRTDYR.js.map +0 -1
- package/dist/chunk-PVXFURDR.js.map +0 -1
- package/dist/chunk-R2HRWFRH.js.map +0 -1
- package/dist/chunk-RQ5S67XB.js.map +0 -1
- package/dist/chunk-U3VKVDZ7.js.map +0 -1
- package/dist/chunk-V3RF6GQW.js.map +0 -1
- package/dist/chunk-WSK45HW4.js.map +0 -1
- package/dist/chunk-XD6CGYHO.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeDiffText
|
|
3
|
-
} from "./chunk-
|
|
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
|
|
108
|
-
if (
|
|
109
|
-
|
|
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
|
-
|
|
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.
|
|
251
|
-
return Math.abs(entry.
|
|
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.
|
|
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-
|
|
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":[]}
|
|
@@ -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":[]}
|
|
@@ -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
|
-
|
|
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-
|
|
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(
|
|
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:
|
|
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(
|
|
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.` :
|
|
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}}",
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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":[]}
|