@codraoss/core 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +625 -0
- package/README.md +15 -0
- package/dist/chunk-FGXA7JNR.js +57 -0
- package/dist/chunk-FGXA7JNR.js.map +1 -0
- package/dist/chunk-ILZUCIZH.js +117 -0
- package/dist/chunk-ILZUCIZH.js.map +1 -0
- package/dist/chunk-JC74XH7Q.js +67 -0
- package/dist/chunk-JC74XH7Q.js.map +1 -0
- package/dist/chunk-MMVQPGK3.js +341 -0
- package/dist/chunk-MMVQPGK3.js.map +1 -0
- package/dist/chunk-NSTV4TRP.js +39 -0
- package/dist/chunk-NSTV4TRP.js.map +1 -0
- package/dist/chunk-OIQRTDYR.js +337 -0
- package/dist/chunk-OIQRTDYR.js.map +1 -0
- package/dist/chunk-PVXFURDR.js +90 -0
- package/dist/chunk-PVXFURDR.js.map +1 -0
- package/dist/chunk-R2HRWFRH.js +108 -0
- package/dist/chunk-R2HRWFRH.js.map +1 -0
- package/dist/chunk-RQ5S67XB.js +82 -0
- package/dist/chunk-RQ5S67XB.js.map +1 -0
- package/dist/chunk-U3VKVDZ7.js +266 -0
- package/dist/chunk-U3VKVDZ7.js.map +1 -0
- package/dist/chunk-V3RF6GQW.js +137 -0
- package/dist/chunk-V3RF6GQW.js.map +1 -0
- package/dist/chunk-WSK45HW4.js +79 -0
- package/dist/chunk-WSK45HW4.js.map +1 -0
- package/dist/chunk-XD6CGYHO.js +893 -0
- package/dist/chunk-XD6CGYHO.js.map +1 -0
- package/dist/claim-checks.d.ts +54 -0
- package/dist/claim-checks.js +20 -0
- package/dist/claim-checks.js.map +1 -0
- package/dist/diff/index.d.ts +22 -0
- package/dist/diff/index.js +25 -0
- package/dist/diff/index.js.map +1 -0
- package/dist/fingerprint.d.ts +9 -0
- package/dist/fingerprint.js +19 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index-BTDWXDck.d.ts +93 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +1755 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +32 -0
- package/dist/logger.js +17 -0
- package/dist/logger.js.map +1 -0
- package/dist/model-gRDTk8yk.d.ts +117 -0
- package/dist/model-output/index.d.ts +4 -0
- package/dist/model-output/index.js +32 -0
- package/dist/model-output/index.js.map +1 -0
- package/dist/ports/index.d.ts +512 -0
- package/dist/ports/index.js +13 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/position-h_jdn6ZH.d.ts +30 -0
- package/dist/prompts/file-review.d.ts +6 -0
- package/dist/prompts/file-review.js +25 -0
- package/dist/prompts/file-review.js.map +1 -0
- package/dist/prompts/languages.d.ts +9 -0
- package/dist/prompts/languages.js +7 -0
- package/dist/prompts/languages.js.map +1 -0
- package/dist/prompts/summary.d.ts +12 -0
- package/dist/prompts/summary.js +50 -0
- package/dist/prompts/summary.js.map +1 -0
- package/dist/prompts/verify.d.ts +71 -0
- package/dist/prompts/verify.js +15 -0
- package/dist/prompts/verify.js.map +1 -0
- package/dist/rules/detect.d.ts +32 -0
- package/dist/rules/detect.js +13 -0
- package/dist/rules/detect.js.map +1 -0
- package/dist/rules/table.d.ts +18 -0
- package/dist/rules/table.js +7 -0
- package/dist/rules/table.js.map +1 -0
- package/dist/timeout.d.ts +6 -0
- package/dist/timeout.js +26 -0
- package/dist/timeout.js.map +1 -0
- package/dist/token-tracker.d.ts +38 -0
- package/dist/token-tracker.js +8 -0
- package/dist/token-tracker.js.map +1 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +21 -0
- package/dist/verify.js.map +1 -0
- package/package.json +181 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/prompts/file-review.ts"],"sourcesContent":["import { claimTypes, type RepoConfig } from '@codraoss/schema';\r\nimport type { FileDiff } from '../diff';\r\nimport type { ModelResponseSchema } from '../ports/model';\r\nimport { getLanguageForFile } from './languages';\r\nimport {\r\n EXEMPLAR_BLOCK_CHARS,\r\n PR_DESCRIPTION_CHARS,\r\n} from '../constants';\r\n\r\nexport function generatorFindingCap(maxComments: number): number {\r\n return Math.max(1, maxComments * 2);\r\n}\r\n\r\nfunction findingItemSchema() {\r\n return {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['evidence', 'code_location', 'claim_type', 'title', 'body', 'priority'],\r\n properties: {\r\n evidence: { type: 'string' },\r\n code_location: {\r\n type: 'object',\r\n additionalProperties: false,\r\n properties: {\r\n absolute_file_path: { type: 'string' },\r\n line: { type: 'integer', minimum: 1 },\r\n line_range: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['start', 'end'],\r\n properties: {\r\n start: { type: 'integer', minimum: 1 },\r\n end: { type: 'integer', minimum: 1 },\r\n },\r\n },\r\n },\r\n anyOf: [\r\n { required: ['line'] },\r\n { required: ['line_range'] },\r\n ],\r\n },\r\n claim_type: { type: 'string', enum: [...claimTypes] },\r\n title: { type: 'string', maxLength: 100 },\r\n body: { type: 'string' },\r\n priority: { type: 'integer', minimum: 0, maximum: 4 },\r\n code_suggestion: { type: 'string' },\r\n },\r\n };\r\n}\r\n\r\nexport function buildReviewResponseSchema(maxComments: number): ModelResponseSchema {\r\n return {\r\n name: 'codra_file_review',\r\n schema: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['findings', 'overall_explanation', 'overall_correctness', 'overall_confidence_score'],\r\n properties: {\r\n findings: {\r\n type: 'array',\r\n maxItems: generatorFindingCap(maxComments),\r\n items: findingItemSchema(),\r\n },\r\n overall_explanation: { type: 'string' },\r\n overall_correctness: { type: 'string', enum: ['patch is correct', 'patch is incorrect'] },\r\n overall_confidence_score: { type: 'number', minimum: 0, maximum: 1 },\r\n },\r\n },\r\n };\r\n}\r\n\r\nexport function buildBatchReviewResponseSchema(maxComments: number, fileCount: number): ModelResponseSchema {\r\n return {\r\n name: 'codra_batch_review',\r\n schema: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['files', 'overall_confidence_score'],\r\n properties: {\r\n files: {\r\n type: 'array',\r\n maxItems: fileCount,\r\n items: {\r\n type: 'object',\r\n additionalProperties: false,\r\n required: ['absolute_file_path', 'findings', 'overall_explanation', 'overall_correctness'],\r\n properties: {\r\n absolute_file_path: { type: 'string' },\r\n findings: { type: 'array', items: findingItemSchema() },\r\n overall_explanation: { type: 'string' },\r\n overall_correctness: { type: 'string', enum: ['patch is correct', 'patch is incorrect'] },\r\n },\r\n },\r\n },\r\n overall_confidence_score: { type: 'number', minimum: 0, maximum: 1 },\r\n },\r\n },\r\n };\r\n}\r\n\r\nconst SINGLE_FILE_SCHEMA_FORMAT = `{\r\n \"findings\": [\r\n {\r\n \"evidence\": \"<the exact line of code from the diff this finding is about>\",\r\n \"code_location\": {\r\n \"line\": number,\r\n \"line_range\": { \"start\": number, \"end\": number }\r\n },\r\n \"claim_type\": \"<one of the claim types listed above>\",\r\n \"title\": \"<Plain title, NO tags/emoji>\",\r\n \"body\": \"<Explanation of the concrete defect and its impact>\",\r\n \"priority\": 0 | 1 | 2 | 3 | 4,\r\n \"code_suggestion\": \"Optional replacement code\"\r\n }\r\n ],\r\n \"overall_explanation\": \"Summary\",\r\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\",\r\n \"overall_confidence_score\": number (0 to 1)\r\n}`;\r\n\r\nconst MULTI_FILE_SCHEMA_FORMAT = `{\r\n \"files\": [\r\n {\r\n \"absolute_file_path\": \"<exact path of one of the files listed below, copied character-for-character>\",\r\n \"findings\": [\r\n {\r\n \"evidence\": \"<the exact line of code from THIS file's diff this finding is about>\",\r\n \"code_location\": {\r\n \"absolute_file_path\": \"<the same path as above>\",\r\n \"line\": number,\r\n \"line_range\": { \"start\": number, \"end\": number }\r\n },\r\n \"claim_type\": \"<one of the claim types listed above>\",\r\n \"title\": \"<Plain title, NO tags/emoji>\",\r\n \"body\": \"<Explanation of the concrete defect and its impact>\",\r\n \"priority\": 0 | 1 | 2 | 3 | 4,\r\n \"code_suggestion\": \"Optional replacement code\"\r\n }\r\n ],\r\n \"overall_explanation\": \"Summary for THIS file\",\r\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\"\r\n }\r\n ],\r\n \"overall_confidence_score\": number (0 to 1)\r\n}`;\r\n\r\nexport function buildFileReviewSystemPromptBase(opts?: { multiFile?: boolean }): string {\r\n const multi = opts?.multiFile === true;\r\n\r\n const contextScope = multi\r\n ? `- You can see ONLY the diffs below, not the whole files or the rest of the repository.\r\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.`\r\n : '- You can see ONLY the diff below, not the whole file or the rest of the repository.';\r\n\r\n const evidenceSource = multi\r\n ? `the single line of code the finding is about, copied VERBATIM from that file's diff below.`\r\n : 'the single line of code the finding is about, copied VERBATIM from the diff below.';\r\n\r\n const capRule = multi\r\n ? '4. Return at most {{MAX_COMMENTS}} findings PER FILE, most severe first. Keep each body under 160 words.'\r\n : '4. Return at most {{MAX_COMMENTS}} findings, most severe first. Keep each body under 160 words.';\r\n\r\n // presupposed clean files in every bin and reintroduced exactly the restraint language the note above\r\n const emptyRule = multi\r\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.`\r\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.';\r\n\r\n return `You are a world-class software engineer performing a precise, high-signal code review.\r\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.\r\n\r\n### CONTEXT EXTENDS (read carefully, this prevents false positives):\r\n${contextScope}\r\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.\r\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.\r\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.\r\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.\r\n\r\n### WHAT TO REPORT:\r\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.\r\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.\r\n\r\n### EVIDENCE (mandatory, a finding without it cannot be posted):\r\n- Every finding MUST include \"evidence\": ${evidenceSource}\r\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.\r\n- If you cannot quote a specific line from the diff that exhibits the problem, you do not have a finding. Omit it.\r\n\r\n### CLAIM TYPE (required, pick the one that fits, or \"other\"):\r\n${claimTypes.join(', ')}\r\n- This is a label for the KIND of defect. It does not license the claim: only report a type if the\r\n diff actually shows it. Picking a type the code cannot exhibit makes the finding easy to discard.\r\n- If nothing fits, use \"other\". Do not stretch a label to fit.\r\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.\r\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.\r\n\r\n### OUTPUT RULES:\r\n1. Output MUST be valid JSON, EXACTLY ONE object matching the schema below.\r\n2. DO NOT output any conversational text, source code, or diff hunks before or after the JSON.\r\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:\".\r\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.\r\n${capRule}\r\n${emptyRule}\r\n\r\n### SCHEMA FORMAT:\r\n${multi ? MULTI_FILE_SCHEMA_FORMAT : SINGLE_FILE_SCHEMA_FORMAT}\r\n\r\nIdentify security risks such as XSS, SQLi, CSRF, insecure randomness, and data leaks that the diff actually introduces.`;\r\n}\r\n\r\nexport const fileReviewSystemPromptBase = buildFileReviewSystemPromptBase();\r\n\r\nexport function buildFileReviewSystemPrompt(\r\n config: RepoConfig['review'],\r\n languagePersona?: string,\r\n opts?: { multiFile?: boolean },\r\n) {\r\n const persona = languagePersona ? ` as ${languagePersona}` : '';\r\n const prompt = buildFileReviewSystemPromptBase(opts)\r\n .replace('{{MAX_COMMENTS}}', generatorFindingCap(config.max_comments).toString());\r\n return `You are a world-class professional senior code reviewer${persona}. ${prompt}`;\r\n}\r\n\r\nexport type RejectedExemplar = { title: string; claimType?: string | null };\r\n\r\n\r\n\r\nfunction renderExemplars(exemplars: readonly RejectedExemplar[] | undefined): string | null {\r\n if (!exemplars?.length) return null;\r\n\r\n const lines: string[] = [];\r\n let used = 0;\r\n for (const exemplar of exemplars) {\r\n const line = `- ${exemplar.title}${exemplar.claimType ? ` (${exemplar.claimType})` : ''}`;\r\n if (used + line.length > EXEMPLAR_BLOCK_CHARS) break;\r\n lines.push(line);\r\n used += line.length;\r\n }\r\n if (lines.length === 0) return null;\r\n\r\n const heading = 'Findings a reviewer on THIS repository has already rejected. Do not report things like these:';\r\n return [heading, ...lines].join('\\n');\r\n}\r\n\r\n\r\n\r\nfunction renderPrContext(prDescription: string | null): string | null {\r\n const trimmed = prDescription?.trim();\r\n if (!trimmed) return null;\r\n return `PR description (author intent - use to judge whether a change is deliberate):\\n${trimmed.slice(0, PR_DESCRIPTION_CHARS)}${trimmed.length > PR_DESCRIPTION_CHARS ? '…' : ''}`;\r\n}\r\n\r\nfunction renderCustomRules(config: RepoConfig['review']): string {\r\n const rules = config.custom_rules.length > 0 ? config.custom_rules.map((rule) => `- ${rule}`).join('\\n') : '- None';\r\n return `Custom rules:\\n${rules}`;\r\n}\r\n\r\nfunction renderLanguageGuidelines(path: string): string {\r\n const languageInfo = getLanguageForFile(path);\r\n const guidelineHeader = 'Specific Guidelines (check the diff against each of these)';\r\n return languageInfo\r\n ? `Language: ${languageInfo.language}\\n${guidelineHeader}:\\n${languageInfo.guidelines.map(g => `- ${g}`).join('\\n')}`\r\n : 'Language: Generic\\nSpecific Guidelines: Follow general best practices.';\r\n}\r\n\r\nexport function buildFileReviewPrompts(input: {\r\n file: FileDiff;\r\n prTitle: string | null;\r\n prDescription: string | null;\r\n config: RepoConfig['review'];\r\n rejectedExemplars?: readonly RejectedExemplar[];\r\n}) {\r\n const languageInfo = getLanguageForFile(input.file.path);\r\n const rules = input.config.custom_rules.length > 0 ? input.config.custom_rules.map((rule) => `- ${rule}`).join('\\n') : '- None';\r\n const systemPrompt = buildFileReviewSystemPrompt(input.config, languageInfo?.persona);\r\n const languageGuidelines = renderLanguageGuidelines(input.file.path);\r\n\r\n const prContext = renderPrContext(input.prDescription);\r\n\r\n const exemplars = renderExemplars(input.rejectedExemplars);\r\n\r\n const userPrompt = [\r\n `PR title: ${input.prTitle ?? 'Untitled PR'}`,\r\n ...(prContext ? [prContext] : []),\r\n ...(exemplars ? [exemplars] : []),\r\n `File path: ${input.file.path}`,\r\n languageGuidelines,\r\n `Custom rules:\\n${rules}`,\r\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.',\r\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.',\r\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 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.',\r\n 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.',\r\n '',\r\n `## Output JSON Schema (STRICTLY REQUIRED)`,\r\n `{\r\n \"findings\": [\r\n {\r\n \"evidence\": \"<exact line of code copied verbatim from the diff below, without the line-number columns or +/- marker>\",\r\n \"code_location\": {\r\n \"absolute_file_path\": \"${input.file.path}\",\r\n \"line\": <int>,\r\n \"line_range\": {\"start\": <int>, \"end\": <int>}\r\n },\r\n \"claim_type\": \"<${claimTypes.join(' | ')}>\",\r\n \"title\": \"<Plain title>\",\r\n \"body\": \"<Technical explanation>\",\r\n \"priority\": <0|1|2|3|4>,\r\n \"code_suggestion\": \"string\"\r\n }\r\n ],\r\n \"overall_correctness\": \"patch is correct\" | \"patch is incorrect\",\r\n \"overall_explanation\": \"Summary\",\r\n \"overall_confidence_score\": <float 0.0-1.0>\r\n}`,\r\n '',\r\n 'Unified diff:',\r\n renderFileDiff(input.file),\r\n ].join('\\n');\r\n\r\n return { systemPrompt, userPrompt };\r\n}\r\n\r\nfunction packFileHeader(file: FileDiff, index: number, total: number): string {\r\n return `===== FILE ${index + 1} of ${total}: ${file.path} =====`;\r\n}\r\n\r\nexport function buildBatchReviewPrompts(input: {\r\n files: readonly FileDiff[];\r\n prTitle: string | null;\r\n prDescription: string | null;\r\n config: RepoConfig['review'];\r\n rejectedExemplars?: readonly RejectedExemplar[];\r\n}) {\r\n const files = input.files;\r\n\r\n const languages = new Set(files.map((file) => getLanguageForFile(file.path)));\r\n const uniformLanguage = languages.size === 1 ? [...languages][0] : undefined;\r\n\r\n const systemPrompt = buildFileReviewSystemPrompt(input.config, uniformLanguage?.persona, { multiFile: true });\r\n\r\n const prContext = renderPrContext(input.prDescription);\r\n const exemplars = renderExemplars(input.rejectedExemplars);\r\n const pathList = files.map((file) => `- ${file.path}`).join('\\n');\r\n\r\n const fileBlocks = files.flatMap((file, index) => [\r\n '',\r\n packFileHeader(file, index, files.length),\r\n ...(uniformLanguage ? [] : [renderLanguageGuidelines(file.path)]),\r\n 'Unified diff:',\r\n renderFileDiff(file),\r\n ]);\r\n\r\n const userPrompt = [\r\n `PR title: ${input.prTitle ?? 'Untitled PR'}`,\r\n ...(prContext ? [prContext] : []),\r\n ...(exemplars ? [exemplars] : []),\r\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}`,\r\n ...(uniformLanguage ? [renderLanguageGuidelines(files[0].path)] : []),\r\n renderCustomRules(input.config),\r\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.',\r\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.',\r\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.',\r\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.',\r\n 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.',\r\n '',\r\n `## Output JSON Schema (STRICTLY REQUIRED)`,\r\n MULTI_FILE_SCHEMA_FORMAT,\r\n ...fileBlocks,\r\n ].join('\\n');\r\n\r\n return { systemPrompt, userPrompt };\r\n}\r\n\r\nexport function renderFileDiff(file: FileDiff) {\r\n const lines = [`diff --git a/${file.previousPath ?? file.path} b/${file.path}`];\r\n for (const hunk of file.hunks) {\r\n lines.push(hunk.header);\r\n for (const line of hunk.lines) {\r\n const prefix = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';\r\n const left = line.oldLineNumber ?? '';\r\n const right = line.newLineNumber ?? '';\r\n lines.push(`${String(left).padStart(4, ' ')} ${String(right).padStart(4, ' ')} ${prefix}${line.content}`);\r\n }\r\n }\r\n\r\n if (file.isTruncated) {\r\n lines.push('');\r\n lines.push(`[NOTE: This diff has been truncated from ${file.originalLineCount} lines to ${file.lineCount} lines for brevity.]`);\r\n }\r\n\r\n return lines.join('\\n');\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/fingerprint.ts
|
|
2
|
+
function fnv1a32Hex(input) {
|
|
3
|
+
let hash = 2166136261;
|
|
4
|
+
for (let i = 0; i < input.length; i++) {
|
|
5
|
+
hash ^= input.charCodeAt(i);
|
|
6
|
+
hash = hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0;
|
|
7
|
+
}
|
|
8
|
+
return hash.toString(16).padStart(8, "0");
|
|
9
|
+
}
|
|
10
|
+
function normalizeDiffText(input) {
|
|
11
|
+
return input.replace(/^\s*\d*\s+\d*\s*[+\- ]?/, "").replace(/\s+/g, " ").trim();
|
|
12
|
+
}
|
|
13
|
+
function foldEvidenceText(input) {
|
|
14
|
+
return normalizeDiffText(input).replace(/[‘’‚‛′]/g, "'").replace(/[“”„‟″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...");
|
|
15
|
+
}
|
|
16
|
+
function normalizeFindingTitle(title) {
|
|
17
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
18
|
+
}
|
|
19
|
+
function buildFindingFingerprint(path, title) {
|
|
20
|
+
return fnv1a32Hex(`${path}\0${normalizeFindingTitle(title)}`);
|
|
21
|
+
}
|
|
22
|
+
function buildFindingFingerprintV2(path, claimType, anchorHash) {
|
|
23
|
+
if (!anchorHash) return null;
|
|
24
|
+
return fnv1a32Hex(`v2 ${path} ${claimType ?? "other"} ${anchorHash}`);
|
|
25
|
+
}
|
|
26
|
+
function buildAnchorHash(lineContent) {
|
|
27
|
+
return fnv1a32Hex(normalizeDiffText(lineContent));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
fnv1a32Hex,
|
|
32
|
+
normalizeDiffText,
|
|
33
|
+
foldEvidenceText,
|
|
34
|
+
normalizeFindingTitle,
|
|
35
|
+
buildFindingFingerprint,
|
|
36
|
+
buildFindingFingerprintV2,
|
|
37
|
+
buildAnchorHash
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=chunk-NSTV4TRP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fingerprint.ts"],"sourcesContent":["\r\nexport function fnv1a32Hex(input: string): string {\r\n let hash = 0x811c9dc5;\r\n for (let i = 0; i < input.length; i++) {\r\n hash ^= input.charCodeAt(i);\r\n hash = (hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24))) >>> 0;\r\n }\r\n return hash.toString(16).padStart(8, '0');\r\n}\r\n\r\nexport function normalizeDiffText(input: string): string {\r\n return input\r\n .replace(/^\\s*\\d*\\s+\\d*\\s*[+\\- ]?/, '')\r\n .replace(/\\s+/g, ' ')\r\n .trim();\r\n}\r\n\r\nexport function foldEvidenceText(input: string): string {\r\n return normalizeDiffText(input)\r\n .replace(/[‘’‚‛′]/g, \"'\")\r\n .replace(/[“”„‟″]/g, '\"')\r\n .replace(/[‐-―−]/g, '-')\r\n .replace(/…/g, '...');\r\n}\r\n\r\nexport function normalizeFindingTitle(title: string): string {\r\n return title.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();\r\n}\r\n\r\nexport function buildFindingFingerprint(path: string, title: string): string {\r\n return fnv1a32Hex(`${path}\\u0000${normalizeFindingTitle(title)}`);\r\n}\r\n\r\nexport function buildFindingFingerprintV2(\r\n path: string,\r\n claimType: string | null | undefined,\r\n anchorHash: string | null | undefined,\r\n): string | null {\r\n if (!anchorHash) return null;\r\n return fnv1a32Hex(`v2 ${path} ${claimType ?? 'other'} ${anchorHash}`);\r\n}\r\n\r\nexport function buildAnchorHash(lineContent: string): string {\r\n return fnv1a32Hex(normalizeDiffText(lineContent));\r\n}\r\n"],"mappings":";AACO,SAAS,WAAW,OAAuB;AAChD,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAQ,MAAM,WAAW,CAAC;AAC1B,WAAQ,SAAS,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,SAAU;AAAA,EAC7F;AACA,SAAO,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC1C;AAEO,SAAS,kBAAkB,OAAuB;AACvD,SAAO,MACJ,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACV;AAEO,SAAS,iBAAiB,OAAuB;AACtD,SAAO,kBAAkB,KAAK,EAC3B,QAAQ,YAAY,GAAG,EACvB,QAAQ,YAAY,GAAG,EACvB,QAAQ,WAAW,GAAG,EACtB,QAAQ,MAAM,KAAK;AACxB;AAEO,SAAS,sBAAsB,OAAuB;AAC3D,SAAO,MAAM,YAAY,EAAE,QAAQ,eAAe,GAAG,EAAE,KAAK;AAC9D;AAEO,SAAS,wBAAwB,MAAc,OAAuB;AAC3E,SAAO,WAAW,GAAG,IAAI,KAAS,sBAAsB,KAAK,CAAC,EAAE;AAClE;AAEO,SAAS,0BACd,MACA,WACA,YACe;AACf,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,WAAW,MAAM,IAAI,IAAI,aAAa,OAAO,IAAI,UAAU,EAAE;AACtE;AAEO,SAAS,gBAAgB,aAA6B;AAC3D,SAAO,WAAW,kBAAkB,WAAW,CAAC;AAClD;","names":[]}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// src/diff/index.ts
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
|
|
4
|
+
// src/diff/position.ts
|
|
5
|
+
function getValidNewLines(file) {
|
|
6
|
+
const newLines = /* @__PURE__ */ new Set();
|
|
7
|
+
for (const hunk of file.hunks) {
|
|
8
|
+
for (const line of hunk.lines) {
|
|
9
|
+
if (line.kind !== "del" && line.newLineNumber !== void 0) {
|
|
10
|
+
newLines.add(line.newLineNumber);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return newLines;
|
|
15
|
+
}
|
|
16
|
+
function getValidPositions(file) {
|
|
17
|
+
const positions = /* @__PURE__ */ new Set();
|
|
18
|
+
for (const hunk of file.hunks) {
|
|
19
|
+
for (const line of hunk.lines) {
|
|
20
|
+
if (line.kind !== "del") {
|
|
21
|
+
positions.add(line.position);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return positions;
|
|
26
|
+
}
|
|
27
|
+
function findPositionForLine(file, lineNumber) {
|
|
28
|
+
for (const hunk of file.hunks) {
|
|
29
|
+
for (const line of hunk.lines) {
|
|
30
|
+
if (line.newLineNumber === lineNumber && line.kind !== "del") {
|
|
31
|
+
return line.position;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
function truncateFileDiff(file, maxLines) {
|
|
38
|
+
if (file.lineCount <= maxLines) {
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
41
|
+
let currentLines = 0;
|
|
42
|
+
const keptHunks = [];
|
|
43
|
+
for (const hunk of file.hunks) {
|
|
44
|
+
const remainingLines = maxLines - currentLines;
|
|
45
|
+
if (remainingLines <= 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (hunk.lines.length <= remainingLines) {
|
|
49
|
+
keptHunks.push(hunk);
|
|
50
|
+
currentLines += hunk.lines.length;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
keptHunks.push({
|
|
54
|
+
...hunk,
|
|
55
|
+
lines: hunk.lines.slice(0, remainingLines)
|
|
56
|
+
});
|
|
57
|
+
currentLines += remainingLines;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...file,
|
|
62
|
+
hunks: keptHunks,
|
|
63
|
+
lineCount: currentLines,
|
|
64
|
+
isTruncated: true,
|
|
65
|
+
originalLineCount: file.lineCount
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function chunkFileDiff(file, maxLinesPerChunk) {
|
|
69
|
+
if (file.lineCount <= maxLinesPerChunk) {
|
|
70
|
+
return [file];
|
|
71
|
+
}
|
|
72
|
+
const chunks = [];
|
|
73
|
+
let currentHunks = [];
|
|
74
|
+
let currentLines = 0;
|
|
75
|
+
for (const hunk of file.hunks) {
|
|
76
|
+
let linesRemainingInHunk = hunk.lines;
|
|
77
|
+
while (linesRemainingInHunk.length > 0) {
|
|
78
|
+
const roomInChunk = maxLinesPerChunk - currentLines;
|
|
79
|
+
if (roomInChunk <= 0) {
|
|
80
|
+
chunks.push({
|
|
81
|
+
...file,
|
|
82
|
+
hunks: currentHunks,
|
|
83
|
+
lineCount: currentLines,
|
|
84
|
+
isTruncated: true,
|
|
85
|
+
originalLineCount: file.lineCount
|
|
86
|
+
});
|
|
87
|
+
currentHunks = [];
|
|
88
|
+
currentLines = 0;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (linesRemainingInHunk.length <= roomInChunk) {
|
|
92
|
+
currentHunks.push({
|
|
93
|
+
...hunk,
|
|
94
|
+
lines: linesRemainingInHunk
|
|
95
|
+
});
|
|
96
|
+
currentLines += linesRemainingInHunk.length;
|
|
97
|
+
linesRemainingInHunk = [];
|
|
98
|
+
} else {
|
|
99
|
+
currentHunks.push({
|
|
100
|
+
...hunk,
|
|
101
|
+
lines: linesRemainingInHunk.slice(0, roomInChunk)
|
|
102
|
+
});
|
|
103
|
+
currentLines += roomInChunk;
|
|
104
|
+
linesRemainingInHunk = linesRemainingInHunk.slice(roomInChunk);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (currentHunks.length > 0) {
|
|
109
|
+
chunks.push({
|
|
110
|
+
...file,
|
|
111
|
+
hunks: currentHunks,
|
|
112
|
+
lineCount: currentLines,
|
|
113
|
+
isTruncated: true,
|
|
114
|
+
originalLineCount: file.lineCount
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return chunks;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/diff/index.ts
|
|
121
|
+
var defaultSkipMatchers = ["**/*.lock", "**/package-lock.json", "**/pnpm-lock.yaml", "**/yarn.lock", "**/*.min.js"].map(
|
|
122
|
+
(pattern) => picomatch(pattern, { dot: true })
|
|
123
|
+
);
|
|
124
|
+
function isReviewableFile(path, customMatchers) {
|
|
125
|
+
if (defaultSkipMatchers.some((matcher) => matcher(path))) return false;
|
|
126
|
+
if (customMatchers.some((matcher) => matcher(path))) return false;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
function parseDiffHeaderPath(line) {
|
|
130
|
+
const rest = line.slice("diff --git ".length);
|
|
131
|
+
if (rest.startsWith("a/")) {
|
|
132
|
+
const n = (rest.length - 5) / 2;
|
|
133
|
+
if (Number.isInteger(n) && n > 0 && rest[2 + n] === " " && rest.startsWith("b/", 3 + n)) {
|
|
134
|
+
const a = rest.slice(2, 2 + n);
|
|
135
|
+
if (a === rest.slice(5 + n)) return a;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const bStart = rest.indexOf(" b/", rest.startsWith("a/") ? 2 : 0);
|
|
139
|
+
const bPath = bStart === -1 ? rest.slice(rest.lastIndexOf(" ") + 1) : rest.slice(bStart + 3);
|
|
140
|
+
return bPath.startsWith("b/") ? bPath.slice(2) : bPath;
|
|
141
|
+
}
|
|
142
|
+
function parseHunkHeader(line) {
|
|
143
|
+
const match = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
144
|
+
if (!match) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
oldLine: Number.parseInt(match[1], 10),
|
|
149
|
+
newLine: Number.parseInt(match[2], 10)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function classifyDiffLine(prefix, content, oldLine, newLine, position) {
|
|
153
|
+
if (prefix === " ") {
|
|
154
|
+
return { kind: "context", content, oldLineNumber: oldLine, newLineNumber: newLine, position };
|
|
155
|
+
}
|
|
156
|
+
if (prefix === "+") {
|
|
157
|
+
return { kind: "add", content, newLineNumber: newLine, position };
|
|
158
|
+
}
|
|
159
|
+
return { kind: "del", content, oldLineNumber: oldLine, position };
|
|
160
|
+
}
|
|
161
|
+
function finishFile(files, currentFile) {
|
|
162
|
+
if (currentFile) {
|
|
163
|
+
files.push(currentFile);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function parseUnifiedDiff(rawDiff, reviewConfig) {
|
|
167
|
+
const files = [];
|
|
168
|
+
const customMatchers = reviewConfig?.skip_files?.map((pattern) => picomatch(pattern, { dot: true })) ?? [];
|
|
169
|
+
let currentFile = null;
|
|
170
|
+
let currentHunk = null;
|
|
171
|
+
let oldLine = 0;
|
|
172
|
+
let newLine = 0;
|
|
173
|
+
let position = 0;
|
|
174
|
+
let isIgnored = false;
|
|
175
|
+
const pushCurrentFile = () => {
|
|
176
|
+
finishFile(files, currentFile);
|
|
177
|
+
currentFile = null;
|
|
178
|
+
currentHunk = null;
|
|
179
|
+
oldLine = 0;
|
|
180
|
+
newLine = 0;
|
|
181
|
+
position = 0;
|
|
182
|
+
isIgnored = false;
|
|
183
|
+
};
|
|
184
|
+
let startIndex = 0;
|
|
185
|
+
const length = rawDiff.length;
|
|
186
|
+
while (startIndex < length) {
|
|
187
|
+
let endIndex = rawDiff.indexOf("\n", startIndex);
|
|
188
|
+
if (endIndex === -1) {
|
|
189
|
+
endIndex = length;
|
|
190
|
+
}
|
|
191
|
+
let line = rawDiff.substring(startIndex, endIndex);
|
|
192
|
+
if (line.charCodeAt(line.length - 1) === 13) {
|
|
193
|
+
line = line.slice(0, -1);
|
|
194
|
+
}
|
|
195
|
+
startIndex = endIndex + 1;
|
|
196
|
+
if (line.startsWith("diff --git ")) {
|
|
197
|
+
pushCurrentFile();
|
|
198
|
+
const path = parseDiffHeaderPath(line);
|
|
199
|
+
currentFile = {
|
|
200
|
+
path,
|
|
201
|
+
previousPath: null,
|
|
202
|
+
isNew: false,
|
|
203
|
+
isDeleted: false,
|
|
204
|
+
isBinary: false,
|
|
205
|
+
lineCount: 0,
|
|
206
|
+
hunks: []
|
|
207
|
+
};
|
|
208
|
+
if (reviewConfig) {
|
|
209
|
+
isIgnored = !isReviewableFile(path, customMatchers);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!currentFile) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (line.startsWith("rename from ")) {
|
|
217
|
+
currentFile.previousPath = line.slice(12);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (line.startsWith("rename to ")) {
|
|
221
|
+
const nextPath = line.slice(10);
|
|
222
|
+
currentFile.path = nextPath.startsWith("b/") ? nextPath.slice(2) : nextPath;
|
|
223
|
+
if (reviewConfig) {
|
|
224
|
+
isIgnored = !isReviewableFile(currentFile.path, customMatchers);
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (line.startsWith("new file mode ")) {
|
|
229
|
+
currentFile.isNew = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (line.startsWith("deleted file mode ")) {
|
|
233
|
+
currentFile.isDeleted = true;
|
|
234
|
+
isIgnored = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
|
|
238
|
+
currentFile.isBinary = true;
|
|
239
|
+
isIgnored = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (line.startsWith("+++ ")) {
|
|
243
|
+
const nextPath = line.slice(4);
|
|
244
|
+
currentFile.path = nextPath.startsWith("b/") ? nextPath.slice(2) : nextPath;
|
|
245
|
+
if (reviewConfig) {
|
|
246
|
+
isIgnored = !isReviewableFile(currentFile.path, customMatchers);
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (isIgnored) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (line.startsWith("--- ")) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (line.startsWith("@@ ")) {
|
|
257
|
+
const header = parseHunkHeader(line);
|
|
258
|
+
if (!header) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
oldLine = header.oldLine;
|
|
262
|
+
newLine = header.newLine;
|
|
263
|
+
currentHunk = { header: line, lines: [] };
|
|
264
|
+
currentFile.hunks.push(currentHunk);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (!currentHunk) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const prefix = line[0];
|
|
271
|
+
if (prefix !== " " && prefix !== "+" && prefix !== "-") {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
position += 1;
|
|
275
|
+
const diffLine = classifyDiffLine(prefix, line.slice(1), oldLine, newLine, position);
|
|
276
|
+
currentHunk.lines.push(diffLine);
|
|
277
|
+
currentFile.lineCount += 1;
|
|
278
|
+
if (diffLine.kind !== "del") newLine += 1;
|
|
279
|
+
if (diffLine.kind !== "add") oldLine += 1;
|
|
280
|
+
}
|
|
281
|
+
pushCurrentFile();
|
|
282
|
+
return files.filter((file) => file.path);
|
|
283
|
+
}
|
|
284
|
+
function buildUnifiedDiffFromFiles(files) {
|
|
285
|
+
const out = [];
|
|
286
|
+
for (const file of files) {
|
|
287
|
+
const newPath = file.filename;
|
|
288
|
+
const oldPath = file.previous_filename || file.filename;
|
|
289
|
+
const isAdded = file.status === "added";
|
|
290
|
+
const isRemoved = file.status === "removed";
|
|
291
|
+
out.push(`diff --git a/${oldPath} b/${newPath}`);
|
|
292
|
+
if (isAdded) out.push("new file mode 100644");
|
|
293
|
+
if (isRemoved) out.push("deleted file mode 100644");
|
|
294
|
+
if (file.previous_filename && file.previous_filename !== newPath) {
|
|
295
|
+
out.push(`rename from ${file.previous_filename}`);
|
|
296
|
+
out.push(`rename to ${newPath}`);
|
|
297
|
+
}
|
|
298
|
+
if (!file.patch) {
|
|
299
|
+
out.push(`Binary files a/${oldPath} and b/${newPath} differ`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
out.push(isAdded ? "--- /dev/null" : `--- a/${oldPath}`);
|
|
303
|
+
out.push(isRemoved ? "+++ /dev/null" : `+++ b/${newPath}`);
|
|
304
|
+
out.push(file.patch);
|
|
305
|
+
}
|
|
306
|
+
return out.length > 0 ? `${out.join("\n")}
|
|
307
|
+
` : "";
|
|
308
|
+
}
|
|
309
|
+
function filterReviewableFiles(files, config, maxFiles) {
|
|
310
|
+
const customMatchers = config.skip_files.map((pattern) => picomatch(pattern, { dot: true }));
|
|
311
|
+
const reviewable = [];
|
|
312
|
+
for (const file of files) {
|
|
313
|
+
if (file.isDeleted || file.isBinary) continue;
|
|
314
|
+
if (defaultSkipMatchers.some((matcher) => matcher(file.path))) continue;
|
|
315
|
+
if (customMatchers.some((matcher) => matcher(file.path))) continue;
|
|
316
|
+
reviewable.push(file);
|
|
317
|
+
}
|
|
318
|
+
reviewable.sort((left, right) => Number(left.isNew) - Number(right.isNew) || left.path.localeCompare(right.path));
|
|
319
|
+
return {
|
|
320
|
+
files: reviewable.slice(0, maxFiles),
|
|
321
|
+
skipped: Math.max(0, reviewable.length - maxFiles)
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export {
|
|
326
|
+
getValidNewLines,
|
|
327
|
+
getValidPositions,
|
|
328
|
+
findPositionForLine,
|
|
329
|
+
truncateFileDiff,
|
|
330
|
+
chunkFileDiff,
|
|
331
|
+
isReviewableFile,
|
|
332
|
+
parseDiffHeaderPath,
|
|
333
|
+
parseUnifiedDiff,
|
|
334
|
+
buildUnifiedDiffFromFiles,
|
|
335
|
+
filterReviewableFiles
|
|
336
|
+
};
|
|
337
|
+
//# sourceMappingURL=chunk-OIQRTDYR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/diff/index.ts","../src/diff/position.ts"],"sourcesContent":["import picomatch from 'picomatch';\r\nimport type { RepoConfig } from '@codraoss/schema';\r\nimport {\r\n type DiffLineKind,\r\n type DiffLine,\r\n type DiffHunk,\r\n type FileDiff,\r\n getValidNewLines,\r\n getValidPositions,\r\n findPositionForLine,\r\n truncateFileDiff,\r\n chunkFileDiff,\r\n} from './position';\r\n\r\nexport {\r\n type DiffLineKind,\r\n type DiffLine,\r\n type DiffHunk,\r\n type FileDiff,\r\n getValidNewLines,\r\n getValidPositions,\r\n findPositionForLine,\r\n truncateFileDiff,\r\n chunkFileDiff,\r\n};\r\n\r\nconst defaultSkipMatchers = ['**/*.lock', '**/package-lock.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/*.min.js'].map((pattern) =>\r\n picomatch(pattern, { dot: true }),\r\n);\r\n\r\nexport function isReviewableFile(path: string, customMatchers: ReturnType<typeof picomatch>[]) {\r\n if (defaultSkipMatchers.some((matcher) => matcher(path))) return false;\r\n if (customMatchers.some((matcher) => matcher(path))) return false;\r\n return true;\r\n}\r\n\r\nexport function parseDiffHeaderPath(line: string) {\r\n const rest = line.slice('diff --git '.length);\r\n\r\n if (rest.startsWith('a/')) {\r\n const n = (rest.length - 5) / 2;\r\n if (Number.isInteger(n) && n > 0 && rest[2 + n] === ' ' && rest.startsWith('b/', 3 + n)) {\r\n const a = rest.slice(2, 2 + n);\r\n if (a === rest.slice(5 + n)) return a;\r\n }\r\n }\r\n\r\n const bStart = rest.indexOf(' b/', rest.startsWith('a/') ? 2 : 0);\r\n const bPath = bStart === -1 ? rest.slice(rest.lastIndexOf(' ') + 1) : rest.slice(bStart + 3);\r\n return bPath.startsWith('b/') ? bPath.slice(2) : bPath;\r\n}\r\n\r\nfunction parseHunkHeader(line: string): { oldLine: number; newLine: number } | null {\r\n const match = /^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/.exec(line);\r\n if (!match) {\r\n return null;\r\n }\r\n\r\n return {\r\n oldLine: Number.parseInt(match[1], 10),\r\n newLine: Number.parseInt(match[2], 10),\r\n };\r\n}\r\n\r\nfunction classifyDiffLine(prefix: ' ' | '+' | '-', content: string, oldLine: number, newLine: number, position: number): DiffLine {\r\n if (prefix === ' ') {\r\n return { kind: 'context', content, oldLineNumber: oldLine, newLineNumber: newLine, position };\r\n }\r\n\r\n if (prefix === '+') {\r\n return { kind: 'add', content, newLineNumber: newLine, position };\r\n }\r\n\r\n return { kind: 'del', content, oldLineNumber: oldLine, position };\r\n}\r\n\r\nfunction finishFile(files: FileDiff[], currentFile: FileDiff | null) {\r\n if (currentFile) {\r\n files.push(currentFile);\r\n }\r\n}\r\n\r\nexport function parseUnifiedDiff(rawDiff: string, reviewConfig?: RepoConfig['review']): FileDiff[] {\r\n const files: FileDiff[] = [];\r\n const customMatchers = reviewConfig?.skip_files?.map((pattern) => picomatch(pattern, { dot: true })) ?? [];\r\n\r\n let currentFile: FileDiff | null = null;\r\n let currentHunk: DiffHunk | null = null;\r\n let oldLine = 0;\r\n let newLine = 0;\r\n let position = 0;\r\n let isIgnored = false;\r\n\r\n const pushCurrentFile = () => {\r\n finishFile(files, currentFile);\r\n currentFile = null;\r\n currentHunk = null;\r\n oldLine = 0;\r\n newLine = 0;\r\n position = 0;\r\n isIgnored = false;\r\n };\r\n\r\n let startIndex = 0;\r\n const length = rawDiff.length;\r\n\r\n while (startIndex < length) {\r\n let endIndex = rawDiff.indexOf('\\n', startIndex);\r\n if (endIndex === -1) {\r\n endIndex = length;\r\n }\r\n\r\n let line = rawDiff.substring(startIndex, endIndex);\r\n if (line.charCodeAt(line.length - 1) === 13) {\r\n line = line.slice(0, -1);\r\n }\r\n\r\n startIndex = endIndex + 1;\r\n\r\n if (line.startsWith('diff --git ')) {\r\n pushCurrentFile();\r\n const path = parseDiffHeaderPath(line);\r\n\r\n currentFile = {\r\n path,\r\n previousPath: null,\r\n isNew: false,\r\n isDeleted: false,\r\n isBinary: false,\r\n lineCount: 0,\r\n hunks: [],\r\n };\r\n\r\n if (reviewConfig) {\r\n isIgnored = !isReviewableFile(path, customMatchers);\r\n }\r\n continue;\r\n }\r\n\r\n if (!currentFile) {\r\n continue;\r\n }\r\n\r\n if (line.startsWith('rename from ')) {\r\n currentFile.previousPath = line.slice(12);\r\n continue;\r\n }\r\n\r\n if (line.startsWith('rename to ')) {\r\n const nextPath = line.slice(10);\r\n currentFile.path = nextPath.startsWith('b/') ? nextPath.slice(2) : nextPath;\r\n if (reviewConfig) {\r\n isIgnored = !isReviewableFile(currentFile.path, customMatchers);\r\n }\r\n continue;\r\n }\r\n\r\n if (line.startsWith('new file mode ')) {\r\n currentFile.isNew = true;\r\n continue;\r\n }\r\n\r\n if (line.startsWith('deleted file mode ')) {\r\n currentFile.isDeleted = true;\r\n isIgnored = true;\r\n continue;\r\n }\r\n\r\n if (line.startsWith('Binary files ') || line.startsWith('GIT binary patch')) {\r\n currentFile.isBinary = true;\r\n isIgnored = true;\r\n continue;\r\n }\r\n\r\n if (line.startsWith('+++ ')) {\r\n const nextPath = line.slice(4);\r\n currentFile.path = nextPath.startsWith('b/') ? nextPath.slice(2) : nextPath;\r\n if (reviewConfig) {\r\n isIgnored = !isReviewableFile(currentFile.path, customMatchers);\r\n }\r\n continue;\r\n }\r\n\r\n if (isIgnored) {\r\n continue;\r\n }\r\n\r\n if (line.startsWith('--- ')) {\r\n continue;\r\n }\r\n\r\n if (line.startsWith('@@ ')) {\r\n const header = parseHunkHeader(line);\r\n if (!header) {\r\n continue;\r\n }\r\n\r\n oldLine = header.oldLine;\r\n newLine = header.newLine;\r\n currentHunk = { header: line, lines: [] };\r\n currentFile.hunks.push(currentHunk);\r\n continue;\r\n }\r\n\r\n if (!currentHunk) {\r\n continue;\r\n }\r\n\r\n const prefix = line[0];\r\n if (prefix !== ' ' && prefix !== '+' && prefix !== '-') {\r\n continue;\r\n }\r\n\r\n position += 1;\r\n const diffLine = classifyDiffLine(prefix, line.slice(1), oldLine, newLine, position);\r\n currentHunk.lines.push(diffLine);\r\n currentFile.lineCount += 1;\r\n\r\n if (diffLine.kind !== 'del') newLine += 1;\r\n if (diffLine.kind !== 'add') oldLine += 1;\r\n }\r\n\r\n pushCurrentFile();\r\n\r\n return files.filter((file) => file.path);\r\n}\r\n\r\n/** Shape returned by forge compare/files endpoints. Maps to GitHub's pulls/files JSON. */\r\nexport type DiffFileEntry = {\r\n filename: string;\r\n previous_filename?: string | null;\r\n status?: string;\r\n patch?: string | null;\r\n};\r\n\r\nexport function buildUnifiedDiffFromFiles(files: DiffFileEntry[]): string {\r\n const out: string[] = [];\r\n\r\n for (const file of files) {\r\n const newPath = file.filename;\r\n const oldPath = file.previous_filename || file.filename;\r\n const isAdded = file.status === 'added';\r\n const isRemoved = file.status === 'removed';\r\n\r\n out.push(`diff --git a/${oldPath} b/${newPath}`);\r\n if (isAdded) out.push('new file mode 100644');\r\n if (isRemoved) out.push('deleted file mode 100644');\r\n if (file.previous_filename && file.previous_filename !== newPath) {\r\n out.push(`rename from ${file.previous_filename}`);\r\n out.push(`rename to ${newPath}`);\r\n }\r\n\r\n if (!file.patch) {\r\n out.push(`Binary files a/${oldPath} and b/${newPath} differ`);\r\n continue;\r\n }\r\n\r\n out.push(isAdded ? '--- /dev/null' : `--- a/${oldPath}`);\r\n out.push(isRemoved ? '+++ /dev/null' : `+++ b/${newPath}`);\r\n out.push(file.patch);\r\n }\r\n\r\n return out.length > 0 ? `${out.join('\\n')}\\n` : '';\r\n}\r\n\r\nexport function filterReviewableFiles(\r\n files: FileDiff[],\r\n config: RepoConfig['review'],\r\n maxFiles: number,\r\n): { files: FileDiff[]; skipped: number } {\r\n const customMatchers = config.skip_files.map((pattern) => picomatch(pattern, { dot: true }));\r\n\r\n const reviewable: FileDiff[] = [];\r\n for (const file of files) {\r\n if (file.isDeleted || file.isBinary) continue;\r\n if (defaultSkipMatchers.some((matcher) => matcher(file.path))) continue;\r\n if (customMatchers.some((matcher) => matcher(file.path))) continue;\r\n reviewable.push(file);\r\n }\r\n reviewable.sort((left, right) => Number(left.isNew) - Number(right.isNew) || left.path.localeCompare(right.path));\r\n\r\n return {\r\n files: reviewable.slice(0, maxFiles),\r\n skipped: Math.max(0, reviewable.length - maxFiles),\r\n };\r\n}\r\n","export type DiffLineKind = 'context' | 'add' | 'del';\r\n\r\nexport type DiffLine = {\r\n kind: DiffLineKind;\r\n content: string;\r\n oldLineNumber?: number;\r\n newLineNumber?: number;\r\n position: number;\r\n};\r\n\r\nexport type DiffHunk = {\r\n header: string;\r\n lines: DiffLine[];\r\n};\r\n\r\nexport type FileDiff = {\r\n path: string;\r\n previousPath: string | null;\r\n isNew: boolean;\r\n isDeleted: boolean;\r\n isBinary: boolean;\r\n lineCount: number;\r\n hunks: DiffHunk[];\r\n isTruncated?: boolean;\r\n originalLineCount?: number;\r\n};\r\n\r\nexport function getValidNewLines(file: FileDiff) {\r\n const newLines = new Set<number>();\r\n for (const hunk of file.hunks) {\r\n for (const line of hunk.lines) {\r\n if (line.kind !== 'del' && line.newLineNumber !== undefined) {\r\n newLines.add(line.newLineNumber);\r\n }\r\n }\r\n }\r\n\r\n return newLines;\r\n}\r\n\r\nexport function getValidPositions(file: FileDiff) {\r\n const positions = new Set<number>();\r\n for (const hunk of file.hunks) {\r\n for (const line of hunk.lines) {\r\n if (line.kind !== 'del') {\r\n positions.add(line.position);\r\n }\r\n }\r\n }\r\n\r\n return positions;\r\n}\r\n\r\nexport function findPositionForLine(file: FileDiff, lineNumber: number) {\r\n for (const hunk of file.hunks) {\r\n for (const line of hunk.lines) {\r\n if (line.newLineNumber === lineNumber && line.kind !== 'del') {\r\n return line.position;\r\n }\r\n }\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\n\r\nexport function truncateFileDiff(file: FileDiff, maxLines: number): FileDiff {\r\n if (file.lineCount <= maxLines) {\r\n return file;\r\n }\r\n\r\n let currentLines = 0;\r\n const keptHunks: DiffHunk[] = [];\r\n\r\n for (const hunk of file.hunks) {\r\n const remainingLines = maxLines - currentLines;\r\n if (remainingLines <= 0) {\r\n break;\r\n }\r\n\r\n if (hunk.lines.length <= remainingLines) {\r\n keptHunks.push(hunk);\r\n currentLines += hunk.lines.length;\r\n continue;\r\n }\r\n\r\n keptHunks.push({\r\n ...hunk,\r\n lines: hunk.lines.slice(0, remainingLines),\r\n });\r\n currentLines += remainingLines;\r\n break;\r\n }\r\n\r\n return {\r\n ...file,\r\n hunks: keptHunks,\r\n lineCount: currentLines,\r\n isTruncated: true,\r\n originalLineCount: file.lineCount,\r\n };\r\n}\r\n\r\nexport function chunkFileDiff(file: FileDiff, maxLinesPerChunk: number): FileDiff[] {\r\n if (file.lineCount <= maxLinesPerChunk) {\r\n return [file];\r\n }\r\n\r\n const chunks: FileDiff[] = [];\r\n let currentHunks: DiffHunk[] = [];\r\n let currentLines = 0;\r\n\r\n for (const hunk of file.hunks) {\r\n let linesRemainingInHunk = hunk.lines;\r\n\r\n while (linesRemainingInHunk.length > 0) {\r\n const roomInChunk = maxLinesPerChunk - currentLines;\r\n\r\n if (roomInChunk <= 0) {\r\n chunks.push({\r\n ...file,\r\n hunks: currentHunks,\r\n lineCount: currentLines,\r\n isTruncated: true,\r\n originalLineCount: file.lineCount,\r\n });\r\n currentHunks = [];\r\n currentLines = 0;\r\n continue;\r\n }\r\n\r\n if (linesRemainingInHunk.length <= roomInChunk) {\r\n currentHunks.push({\r\n ...hunk,\r\n lines: linesRemainingInHunk,\r\n });\r\n currentLines += linesRemainingInHunk.length;\r\n linesRemainingInHunk = [];\r\n } else {\r\n currentHunks.push({\r\n ...hunk,\r\n lines: linesRemainingInHunk.slice(0, roomInChunk),\r\n });\r\n currentLines += roomInChunk;\r\n linesRemainingInHunk = linesRemainingInHunk.slice(roomInChunk);\r\n }\r\n }\r\n }\r\n\r\n if (currentHunks.length > 0) {\r\n chunks.push({\r\n ...file,\r\n hunks: currentHunks,\r\n lineCount: currentLines,\r\n isTruncated: true,\r\n originalLineCount: file.lineCount,\r\n });\r\n }\r\n\r\n return chunks;\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-WSK45HW4.js";
|
|
4
|
+
|
|
5
|
+
// src/token-tracker.ts
|
|
6
|
+
var TokenTracker = class {
|
|
7
|
+
usage = /* @__PURE__ */ new Map();
|
|
8
|
+
wasted = { attempts: 0, estimatedInput: 0, skips: 0 };
|
|
9
|
+
wastedByReason = /* @__PURE__ */ new Map();
|
|
10
|
+
subrequests = 0;
|
|
11
|
+
MAX_SUBREQUESTS = 50;
|
|
12
|
+
SAFE_MARGIN = 25;
|
|
13
|
+
incrementSubrequests(count = 1) {
|
|
14
|
+
this.subrequests += count;
|
|
15
|
+
}
|
|
16
|
+
getSubrequestCount() {
|
|
17
|
+
return this.subrequests;
|
|
18
|
+
}
|
|
19
|
+
hasRemainingSubrequests(needed = 1) {
|
|
20
|
+
return this.subrequests + needed <= this.MAX_SUBREQUESTS;
|
|
21
|
+
}
|
|
22
|
+
isNearLimit() {
|
|
23
|
+
return this.subrequests >= this.MAX_SUBREQUESTS - this.SAFE_MARGIN;
|
|
24
|
+
}
|
|
25
|
+
remainingSafeBudget() {
|
|
26
|
+
return Math.max(0, this.MAX_SUBREQUESTS - this.SAFE_MARGIN - this.subrequests);
|
|
27
|
+
}
|
|
28
|
+
record(model, input, output) {
|
|
29
|
+
const existing = this.usage.get(model) || { model, input: 0, output: 0, calls: 0 };
|
|
30
|
+
this.usage.set(model, {
|
|
31
|
+
model,
|
|
32
|
+
input: existing.input + input,
|
|
33
|
+
output: existing.output + output,
|
|
34
|
+
calls: existing.calls + 1
|
|
35
|
+
});
|
|
36
|
+
logger.debug(`Token usage recorded for ${model}`, {
|
|
37
|
+
input,
|
|
38
|
+
output,
|
|
39
|
+
totalInput: existing.input + input,
|
|
40
|
+
totalOutput: existing.output + output
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
recordFailedAttempt(model, estimatedInputTokens, reason) {
|
|
44
|
+
this.wasted.attempts += 1;
|
|
45
|
+
this.wasted.estimatedInput += estimatedInputTokens;
|
|
46
|
+
this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + 1);
|
|
47
|
+
logger.debug(`Wasted model attempt on ${model}`, { estimatedInput: estimatedInputTokens, reason });
|
|
48
|
+
}
|
|
49
|
+
recordSkippedCall(model, reason) {
|
|
50
|
+
this.wasted.skips += 1;
|
|
51
|
+
logger.debug(`Skipped model call on ${model}`, { reason });
|
|
52
|
+
}
|
|
53
|
+
getWasted() {
|
|
54
|
+
return { ...this.wasted, byReason: Object.fromEntries(this.wastedByReason) };
|
|
55
|
+
}
|
|
56
|
+
getTotalUsage() {
|
|
57
|
+
let input = 0;
|
|
58
|
+
let output = 0;
|
|
59
|
+
for (const modelUsage of this.usage.values()) {
|
|
60
|
+
input += modelUsage.input;
|
|
61
|
+
output += modelUsage.output;
|
|
62
|
+
}
|
|
63
|
+
return { input, output };
|
|
64
|
+
}
|
|
65
|
+
getBreakdown() {
|
|
66
|
+
return Array.from(this.usage.values());
|
|
67
|
+
}
|
|
68
|
+
merge(other) {
|
|
69
|
+
for (const usage of other.getBreakdown()) {
|
|
70
|
+
this.record(usage.model, usage.input, usage.output);
|
|
71
|
+
}
|
|
72
|
+
const otherWasted = other.getWasted();
|
|
73
|
+
this.wasted.attempts += otherWasted.attempts;
|
|
74
|
+
this.wasted.estimatedInput += otherWasted.estimatedInput;
|
|
75
|
+
this.wasted.skips += otherWasted.skips;
|
|
76
|
+
for (const [reason, count] of Object.entries(otherWasted.byReason)) {
|
|
77
|
+
this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + count);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
reset() {
|
|
81
|
+
this.usage.clear();
|
|
82
|
+
this.wasted = { attempts: 0, estimatedInput: 0, skips: 0 };
|
|
83
|
+
this.wastedByReason.clear();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
TokenTracker
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=chunk-PVXFURDR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/token-tracker.ts"],"sourcesContent":["import { logger } from './logger';\r\n\r\nexport interface TokenUsage {\r\n input: number;\r\n output: number;\r\n}\r\n\r\nexport interface ModelUsage extends TokenUsage {\r\n model: string;\r\n calls: number;\r\n}\r\n\r\nexport type WastedAttemptReason = 'rate-limited' | 'error';\r\n\r\nexport interface WastedUsage {\r\n attempts: number;\r\n estimatedInput: number;\r\n skips: number;\r\n byReason: Record<string, number>;\r\n}\r\n\r\nexport class TokenTracker {\r\n private usage: Map<string, ModelUsage> = new Map();\r\n private wasted = { attempts: 0, estimatedInput: 0, skips: 0 };\r\n private wastedByReason: Map<string, number> = new Map();\r\n private subrequests = 0;\r\n private readonly MAX_SUBREQUESTS = 50;\r\n private readonly SAFE_MARGIN = 25;\r\n\r\n incrementSubrequests(count = 1) {\r\n this.subrequests += count;\r\n }\r\n\r\n getSubrequestCount() {\r\n return this.subrequests;\r\n }\r\n\r\n hasRemainingSubrequests(needed = 1) {\r\n return this.subrequests + needed <= this.MAX_SUBREQUESTS;\r\n }\r\n\r\n isNearLimit() {\r\n return this.subrequests >= this.MAX_SUBREQUESTS - this.SAFE_MARGIN;\r\n }\r\n\r\n remainingSafeBudget() {\r\n return Math.max(0, this.MAX_SUBREQUESTS - this.SAFE_MARGIN - this.subrequests);\r\n }\r\n\r\n record(model: string, input: number, output: number) {\r\n const existing = this.usage.get(model) || { model, input: 0, output: 0, calls: 0 };\r\n \r\n this.usage.set(model, {\r\n model,\r\n input: existing.input + input,\r\n output: existing.output + output,\r\n calls: existing.calls + 1,\r\n });\r\n\r\n logger.debug(`Token usage recorded for ${model}`, { \r\n input, \r\n output, \r\n totalInput: existing.input + input,\r\n totalOutput: existing.output + output\r\n });\r\n }\r\n\r\n recordFailedAttempt(model: string, estimatedInputTokens: number, reason: WastedAttemptReason) {\r\n this.wasted.attempts += 1;\r\n this.wasted.estimatedInput += estimatedInputTokens;\r\n this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + 1);\r\n\r\n logger.debug(`Wasted model attempt on ${model}`, { estimatedInput: estimatedInputTokens, reason });\r\n }\r\n\r\n recordSkippedCall(model: string, reason: string) {\r\n this.wasted.skips += 1;\r\n\r\n logger.debug(`Skipped model call on ${model}`, { reason });\r\n }\r\n\r\n getWasted(): WastedUsage {\r\n return { ...this.wasted, byReason: Object.fromEntries(this.wastedByReason) };\r\n }\r\n\r\n getTotalUsage(): TokenUsage {\r\n let input = 0;\r\n let output = 0;\r\n for (const modelUsage of this.usage.values()) {\r\n input += modelUsage.input;\r\n output += modelUsage.output;\r\n }\r\n return { input, output };\r\n }\r\n\r\n getBreakdown(): ModelUsage[] {\r\n return Array.from(this.usage.values());\r\n }\r\n\r\n merge(other: TokenTracker) {\r\n for (const usage of other.getBreakdown()) {\r\n this.record(usage.model, usage.input, usage.output);\r\n }\r\n\r\n const otherWasted = other.getWasted();\r\n this.wasted.attempts += otherWasted.attempts;\r\n this.wasted.estimatedInput += otherWasted.estimatedInput;\r\n this.wasted.skips += otherWasted.skips;\r\n for (const [reason, count] of Object.entries(otherWasted.byReason)) {\r\n this.wastedByReason.set(reason, (this.wastedByReason.get(reason) ?? 0) + count);\r\n }\r\n }\r\n\r\n reset() {\r\n this.usage.clear();\r\n this.wasted = { attempts: 0, estimatedInput: 0, skips: 0 };\r\n this.wastedByReason.clear();\r\n }\r\n}\r\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":[]}
|