Skip to content

Commit e69ebc2

Browse files
committed
fix: accept fileContents from bot directly — eliminate GITHUB_TOKEN dependency
1 parent 5451411 commit e69ebc2

1 file changed

Lines changed: 99 additions & 123 deletions

File tree

src/routes/bot.ts

Lines changed: 99 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import type { FastifyInstance, FastifyRequest } from 'fastify';
2-
import { Octokit } from '@octokit/rest';
32
import { analyzeCode } from '../services/analyzer/index.js';
43

4+
// ── Bot payload — bot now sends file content directly ────────────────────────
5+
interface FileContent {
6+
filename: string;
7+
status: string;
8+
headContent: string;
9+
baseContent: string;
10+
}
11+
512
interface BotPayload {
6-
owner: string;
7-
repo: string;
8-
prNumber: number;
9-
baseSha: string;
10-
headSha: string;
11-
files: { filename: string; status: string }[];
13+
owner: string;
14+
repo: string;
15+
prNumber: number;
16+
baseSha: string;
17+
headSha: string;
18+
fileContents: FileContent[];
1219
}
1320

1421
function getLanguageFromFilename(filename: string): 'typescript' | 'javascript' | null {
@@ -17,46 +24,45 @@ function getLanguageFromFilename(filename: string): 'typescript' | 'javascript'
1724
return null;
1825
}
1926

20-
// ── Inline pricing (no Redis dependency) ─────────────────────────────────────
27+
// ── Inline pricing (no Redis / no HTTP dependency) ───────────────────────────
2128
const OPENAI_PRICING: Record<string, { input: number; output: number }> = {
22-
'gpt-4o': { input: 0.005, output: 0.015 },
23-
'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
24-
'gpt-4-turbo': { input: 0.01, output: 0.03 },
25-
'gpt-4': { input: 0.03, output: 0.06 },
26-
'gpt-3.5-turbo': { input: 0.0005, output: 0.0015 },
29+
'gpt-4o': { input: 0.005, output: 0.015 },
30+
'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
31+
'gpt-4-turbo': { input: 0.01, output: 0.03 },
32+
'gpt-4': { input: 0.03, output: 0.06 },
33+
'gpt-3.5-turbo': { input: 0.0005, output: 0.0015 },
2734
};
2835

2936
const ANTHROPIC_PRICING: Record<string, { input: number; output: number }> = {
30-
'claude-3-5-sonnet': { input: 0.003, output: 0.015 },
31-
'claude-3-opus': { input: 0.015, output: 0.075 },
32-
'claude-3-sonnet': { input: 0.003, output: 0.015 },
33-
'claude-3-haiku': { input: 0.00025, output: 0.00125 },
34-
'claude-2': { input: 0.008, output: 0.024 },
37+
'claude-3-5-sonnet': { input: 0.003, output: 0.015 },
38+
'claude-3-opus': { input: 0.015, output: 0.075 },
39+
'claude-3-sonnet': { input: 0.003, output: 0.015 },
40+
'claude-3-haiku': { input: 0.00025, output: 0.00125 },
41+
'claude-2': { input: 0.008, output: 0.024 },
3542
};
3643

37-
function estimateCentsInline(service: string, model: string | undefined, callsPerMonth: number,
38-
inputTokens: number, outputTokens: number,
39-
storageGB?: number, operation?: string): number {
44+
function estimateCents(
45+
service: string, model: string | undefined,
46+
callsPerMonth: number, inputTokens: number, outputTokens: number,
47+
storageGB?: number, operation?: string,
48+
): number {
4049
if (service === 'openai' && model) {
4150
const p = OPENAI_PRICING[model] ?? OPENAI_PRICING['gpt-4o'];
4251
return Math.round(
43-
((inputTokens / 1_000) * p.input * callsPerMonth * 100) +
44-
((outputTokens / 1_000) * p.output * callsPerMonth * 100)
52+
(inputTokens / 1_000) * p.input * callsPerMonth * 100 +
53+
(outputTokens / 1_000) * p.output * callsPerMonth * 100,
4554
);
4655
}
4756
if (service === 'anthropic' && model) {
4857
const p = ANTHROPIC_PRICING[model] ?? ANTHROPIC_PRICING['claude-3-5-sonnet'];
4958
return Math.round(
50-
((inputTokens / 1_000) * p.input * callsPerMonth * 100) +
51-
((outputTokens / 1_000) * p.output * callsPerMonth * 100)
59+
(inputTokens / 1_000) * p.input * callsPerMonth * 100 +
60+
(outputTokens / 1_000) * p.output * callsPerMonth * 100,
5261
);
5362
}
5463
if (service === 'aws-lambda') {
55-
const calls = callsPerMonth;
56-
const memMB = 128;
57-
const durMs = 200;
58-
const gbSecs = (memMB / 1_024) * (durMs / 1_000) * calls;
59-
const billableReqs = Math.max(0, calls - 1_000_000);
64+
const gbSecs = (128 / 1_024) * (200 / 1_000) * callsPerMonth;
65+
const billableReqs = Math.max(0, callsPerMonth - 1_000_000);
6066
const billableGbSecs = Math.max(0, gbSecs - 400_000);
6167
return Math.round(billableReqs * 0.0000002 * 100 + billableGbSecs * 0.0000166667 * 100);
6268
}
@@ -65,35 +71,31 @@ function estimateCentsInline(service: string, model: string | undefined, callsPe
6571
const isWrite = operation === 'put';
6672
const storage = Math.round(gb * 0.023 * 100);
6773
const requests = isWrite
68-
? Math.round((callsPerMonth / 1_000) * 0.005 * 100)
74+
? Math.round((callsPerMonth / 1_000) * 0.005 * 100)
6975
: Math.round((callsPerMonth / 1_000) * 0.0004 * 100);
7076
return storage + requests;
7177
}
7278
if (service === 'dynamodb') {
7379
const perUnit = operation === 'write' ? 0.00000125 : 0.00000025;
7480
return Math.round(callsPerMonth * perUnit * 100);
7581
}
76-
if (service === 'api-gateway') {
77-
return Math.round((callsPerMonth / 1_000_000) * 3.5 * 100);
78-
}
79-
if (service === 'redis') return 1_224; // cache.t3.micro baseline
82+
if (service === 'api-gateway') return Math.round((callsPerMonth / 1_000_000) * 3.5 * 100);
83+
if (service === 'redis') return 1_224;
8084
return 0;
8185
}
8286

83-
// ── Route ────────────────────────────────────────────────────────────────────
87+
// ── Route ────────────────────────────────────────────────────────────────────
8488
export async function botRoutes(app: FastifyInstance) {
85-
app.post('/api/bot/analyze-pr', async (request: FastifyRequest<{ Body: BotPayload }>, reply) => {
86-
const { owner, repo, baseSha, headSha, files } = request.body;
87-
88-
const token = process.env.GITHUB_TOKEN || '';
89-
const octokit = new Octokit({ auth: token });
89+
app.post('/api/bot/analyze-pr', async (
90+
request: FastifyRequest<{ Body: BotPayload }>,
91+
reply,
92+
) => {
93+
const { fileContents = [] } = request.body;
9094

9195
let totalHeadCents = 0;
9296
let totalBaseCents = 0;
9397
let inLoop = 0;
9498
let handler = 0;
95-
const fetchErrors: string[] = [];
96-
const debugLines: string[] = [];
9799

98100
interface DetectionRow {
99101
service: string;
@@ -104,131 +106,109 @@ export async function botRoutes(app: FastifyInstance) {
104106
inLoop: boolean;
105107
}
106108
const detectionRows: DetectionRow[] = [];
109+
const debugLines: string[] = [];
107110

108-
for (const file of files) {
109-
if (file.status === 'removed') continue;
111+
for (const file of fileContents) {
110112
const language = getLanguageFromFilename(file.filename);
111113
if (!language) {
112114
debugLines.push(`skip (not JS/TS): ${file.filename}`);
113115
continue;
114116
}
115117

116-
// ── Fetch HEAD ─────────────────────────────────────────────────────────
117-
let headContent = '';
118-
try {
119-
const { data } = await octokit.rest.repos.getContent({
120-
owner, repo, path: file.filename, ref: headSha,
121-
});
122-
if ('content' in (data as any) && !Array.isArray(data)) {
123-
headContent = Buffer.from((data as any).content, 'base64').toString('utf-8');
124-
}
125-
} catch (e: any) {
126-
fetchErrors.push(`HEAD fetch failed for ${file.filename}: ${e.message}`);
127-
debugLines.push(`HEAD fetch error ${file.filename}: ${e.message}`);
128-
continue;
129-
}
118+
const headContent = file.headContent ?? '';
119+
const baseContent = file.baseContent ?? '';
130120

131121
if (!headContent.trim()) {
132-
debugLines.push(`HEAD content empty for ${file.filename}`);
122+
debugLines.push(`empty head content: ${file.filename}`);
133123
continue;
134124
}
135125

136-
// ── Fetch BASE ─────────────────────────────────────────────────────────
137-
let baseContent = '';
138-
if (file.status !== 'added') {
139-
try {
140-
const { data } = await octokit.rest.repos.getContent({
141-
owner, repo, path: file.filename, ref: baseSha,
142-
});
143-
if ('content' in (data as any) && !Array.isArray(data)) {
144-
baseContent = Buffer.from((data as any).content, 'base64').toString('utf-8');
145-
}
146-
} catch { /* treat as new file */ }
147-
}
148-
149-
debugLines.push(`analyzing ${file.filename} (${headContent.length} chars, lang=${language})`);
126+
debugLines.push(`analyzing: ${file.filename} (${headContent.length} chars, ${language})`);
150127

151-
// ── AST analysis ───────────────────────────────────────────────────────
152-
const headAnalysis = analyzeCode(headContent, language);
153-
const baseAnalysis = analyzeCode(baseContent || '', language);
128+
// ── AST analysis ────────────────────────────────────────────────────
129+
const headA = analyzeCode(headContent, language);
130+
const baseA = analyzeCode(baseContent, language);
154131

155-
debugLines.push(` head detections: ${headAnalysis.detections.length}`);
156-
if (headAnalysis.errors.length) debugLines.push(` AST errors: ${headAnalysis.errors.join('; ')}`);
132+
debugLines.push(` detections head=${headA.detections.length} base=${baseA.detections.length}`);
133+
if (headA.errors.length) debugLines.push(` AST errors: ${headA.errors.join('; ')}`);
157134

158-
if (headAnalysis.detections.length === 0) continue;
135+
if (headA.detections.length === 0) continue;
159136

160137
const fileHasLoop = /for\s*\(|while\s*\(|\.forEach\s*\(|\.map\s*\(/.test(headContent);
161138
if (fileHasLoop) inLoop += 1;
162139
handler += 1;
163140

164-
// ── Compute costs inline (no Redis) ────────────────────────────────────
141+
// ── Cost calculation ─────────────────────────────────────────────────
165142
let fileHeadCents = 0;
166143
let fileBaseCents = 0;
167144

168-
for (const d of headAnalysis.detections) {
169-
const cents = estimateCentsInline(
170-
d.service, d.model, d.callsPerMonth ?? 10_000,
171-
d.inputTokens ?? 500, d.outputTokens ?? 1_000,
145+
for (const d of headA.detections) {
146+
const cents = estimateCents(
147+
d.service, d.model,
148+
d.callsPerMonth ?? 10_000,
149+
d.inputTokens ?? 500,
150+
d.outputTokens ?? 1_000,
172151
d.storageGB, d.operation,
173152
);
174153
fileHeadCents += cents;
175-
176154
detectionRows.push({
177-
service: d.service,
178-
model: d.model,
179-
snippet: d.snippet,
180-
headCents: cents,
181-
deltaCents: 0, // filled in below
182-
inLoop: fileHasLoop,
155+
service: d.service,
156+
model: d.model,
157+
snippet: d.snippet,
158+
headCents: cents,
159+
deltaCents: 0, // filled below
160+
inLoop: fileHasLoop,
183161
});
184162
}
185163

186-
for (const d of baseAnalysis.detections) {
187-
fileBaseCents += estimateCentsInline(
188-
d.service, d.model, d.callsPerMonth ?? 10_000,
189-
d.inputTokens ?? 500, d.outputTokens ?? 1_000,
164+
for (const d of baseA.detections) {
165+
fileBaseCents += estimateCents(
166+
d.service, d.model,
167+
d.callsPerMonth ?? 10_000,
168+
d.inputTokens ?? 500,
169+
d.outputTokens ?? 1_000,
190170
d.storageGB, d.operation,
191171
);
192172
}
193173

194174
totalHeadCents += fileHeadCents;
195175
totalBaseCents += fileBaseCents;
196176

197-
// Distribute delta proportionally across detections in this file
177+
// Distribute file delta proportionally across detections
198178
const fileDelta = fileHeadCents - fileBaseCents;
199-
const startIdx = detectionRows.length - headAnalysis.detections.length;
179+
const startIdx = detectionRows.length - headA.detections.length;
200180
for (let i = startIdx; i < detectionRows.length; i++) {
201181
const share = fileHeadCents > 0 ? detectionRows[i].headCents / fileHeadCents : 0;
202182
detectionRows[i].deltaCents = Math.round(fileDelta * share);
203183
}
204184
}
205185

206186
// ── Build markdown ────────────────────────────────────────────────────────
207-
const totalDelta = totalHeadCents - totalBaseCents;
208-
const fmt = (c: number) => `$${(Math.abs(c) / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
209-
const sign = (c: number) => c > 0 ? '+' : c < 0 ? '-' : '';
187+
const totalDelta = totalHeadCents - totalBaseCents;
188+
const fmt = (c: number) => `$${(Math.abs(c) / 100).toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
189+
const sign = (c: number) => c > 0 ? '+' : c < 0 ? '-' : '';
210190
const uniqueServices = [...new Set(detectionRows.map(r => r.service))];
211191

212-
let criticalityBadge = '🟢 **Criticality: Low**';
213-
let qualityStatement = '';
192+
let critBadge = '🟢 **Criticality: Low**';
193+
let qualityNote = '';
214194
if (inLoop > 0) {
215-
criticalityBadge = '🔴 **Criticality: Major**';
216-
qualityStatement = '\n> ⚠️ **CRITICAL:** Cloud API calls detected inside a loop. Costs scale with every iteration — consider batching.\n';
195+
critBadge = '🔴 **Criticality: Major**';
196+
qualityNote = '\n> ⚠️ **CRITICAL:** Cloud API calls inside a loop — costs scale with every iteration. Consider batching.\n';
217197
} else if (totalHeadCents > 5_000) {
218-
criticalityBadge = '🟡 **Criticality: Minor**';
219-
qualityStatement = '\n> 💡 Significant cloud costs detected. Ensure these align with your budget.\n';
198+
critBadge = '🟡 **Criticality: Minor**';
199+
qualityNote = '\n> 💡 Significant cloud costs detected. Verify these align with your budget.\n';
220200
}
221201

222202
let md = `## 📊 CloudGauge Cost Impact Analysis\n\n`;
223-
md += `### 💰 ESTIMATED MONTHLY COST DELTA\n`;
224-
md += `# **${sign(totalDelta)}${fmt(totalDelta)}/mo**\n`;
225-
md += `${criticalityBadge}\n\n`;
226-
md += `> Total cloud cost **in changed files**: **${fmt(totalHeadCents)}/mo**`;
203+
md += `### 💰 ESTIMATED MONTHLY COST DELTA\n`;
204+
md += `# **${sign(totalDelta)}${fmt(totalDelta)}/mo**\n`;
205+
md += `${critBadge}\n\n`;
206+
md += `> Total cloud cost **in changed files**: **${fmt(totalHeadCents)}/mo**`;
227207
if (totalBaseCents > 0) md += ` *(was ${fmt(totalBaseCents)}/mo before this PR)*`;
228-
md += `\n\n`;
229-
md += `*Detected **${detectionRows.length}** cloud API call(s) across **${uniqueServices.length}** service(s).*\n`;
230-
md += qualityStatement + '\n';
231-
md += `---\n\n`;
208+
md += `\n\n`;
209+
md += `*Detected **${detectionRows.length}** cloud API call(s) across **${uniqueServices.length}** service(s).*\n`;
210+
md += qualityNote + '\n';
211+
md += `---\n\n`;
232212

233213
md += `### ⚙️ EXECUTION CONTEXT IMPACT\n`;
234214
md += `| 🔄 In Loop | 🌐 Handler | ⏱️ Scheduled | 📦 Batch | 📌 Direct |\n`;
@@ -243,18 +223,14 @@ export async function botRoutes(app: FastifyInstance) {
243223
for (const row of detectionRows) {
244224
const label = row.model ? `**${row.service}** \`${row.model}\`` : `**${row.service}**`;
245225
const badge = row.inLoop ? ' 🔄' : '';
246-
const delta = row.deltaCents !== 0 ? `**${sign(row.deltaCents)}${fmt(row.deltaCents)}/mo**` : `*(existing)*`;
226+
const delta = row.deltaCents !== 0
227+
? `**${sign(row.deltaCents)}${fmt(row.deltaCents)}/mo**`
228+
: `*(existing)*`;
247229
md += `| ${label}${badge} | \`${row.snippet.slice(0, 70)}\` | **${fmt(row.headCents)}/mo** | ${delta} |\n`;
248230
}
249231
} else {
250232
md += `*No cloud API calls detected in the changed files.*\n`;
251-
if (fetchErrors.length > 0) {
252-
md += `\n> ⚠️ **File fetch errors** (check GITHUB\\_TOKEN permissions):\n`;
253-
for (const e of fetchErrors) md += `> - \`${e}\`\n`;
254-
}
255-
if (debugLines.length > 0) {
256-
md += `\n<details><summary>🔍 Debug Info</summary>\n\n\`\`\`\n${debugLines.join('\n')}\n\`\`\`\n</details>\n`;
257-
}
233+
md += `\n<details><summary>🔍 Debug Info</summary>\n\n\`\`\`\n${debugLines.join('\n')}\n\`\`\`\n</details>\n`;
258234
}
259235

260236
md += `\n\n> *Powered by SentinelEngine CodeReview Bot.*`;

0 commit comments

Comments
 (0)