Skip to content

Commit f3c1f69

Browse files
authored
Merge pull request #43 from DaleStudy/31-merger-pattern-big-o
feat: ํŒจํ„ด ํƒœ๊น… ๋Œ“๊ธ€์— ๋ณต์žก๋„ ๋ถ„์„ ํ•ฉ๋ณธ (closes #31)
2 parents a221a48 + 8bf34b1 commit f3c1f69

9 files changed

Lines changed: 1021 additions & 1250 deletions

โ€Žhandlers/complexity-analysis.jsโ€Ž

Lines changed: 41 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
/**
2-
* ์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„ ์ž๋™ ๋ถ„์„.
3-
* PR opened/reopened/synchronize ์‹œ ํ˜ธ์ถœ๋œ๋‹ค.
2+
* ์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„ ๋ถ„์„ (๋ถ„์„ ํ•จ์ˆ˜ + ์ˆœ์ˆ˜ ํ—ฌํผ).
3+
*
4+
* ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜๊ณผ ๋Œ“๊ธ€ ๊ฒŒ์‹œ๋Š” tag-patterns ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋‹ด๋‹นํ•˜๋ฉฐ, ์ด ๋ชจ๋“ˆ์€
5+
* OpenAI ํ˜ธ์ถœ(`callComplexityAnalysis`)๊ณผ ์‚ฌ์šฉ์ž ์ฃผ์„ ์ฒ˜๋ฆฌ/๋งค์นญ ํ—ฌํผ,
6+
* ํ•œ ํŒŒ์ผ๋ถ„ ์„น์…˜ ๋ Œ๋”๋Ÿฌ(`renderComplexitySection`)๋งŒ ์ œ๊ณตํ•œ๋‹ค.
47
*
58
* ์ฑ…์ž„ ๋ถ„๋‹ด (plan v4):
69
* - ์ฝ”๋“œ: ์‚ฌ์šฉ์ž ๋ณต์žก๋„ ์ฃผ์„ ์ œ๊ฑฐ / ์ถ”์ถœ / matches ํŒ์ •.
710
* - LLM : actualTime / actualSpace / feedback / suggestion / headerLine ๋งŒ ์ฑ…์ž„.
811
*/
912

10-
import { getGitHubHeaders } from "../utils/github.js";
11-
import { hasMaintenanceLabel } from "../utils/validation.js";
12-
1313
// โ”€โ”€ ์ƒ์ˆ˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1414

15-
const SOLUTION_PATH_REGEX = /^[^/]+\/[^/]+\.[^.]+$/;
16-
const COMPLEXITY_COMMENT_MARKER = "<!-- dalestudy-complexity-analysis -->";
17-
const MAX_FILE_SIZE = 15000;
18-
const MAX_TOTAL_SIZE = 60000;
1915
const FILE_DELIMITER = "=====";
2016

2117
const COMMENT_START_PATTERN = /^(?:\/\/|#|--|;|\/\*|\*(?!\/)|"""|''')/;
@@ -244,7 +240,7 @@ export function composeSolution(modelSol, originalContent) {
244240
};
245241
}
246242

247-
async function callComplexityAnalysis(fileEntries, apiKey) {
243+
export async function callComplexityAnalysis(fileEntries, apiKey) {
248244
const userPrompt = fileEntries
249245
.map(
250246
(f) =>
@@ -356,225 +352,59 @@ function buildSolutionBody(solution) {
356352
return lines;
357353
}
358354

359-
function formatComplexityCommentBody(entries) {
355+
/**
356+
* ํ•œ ํŒŒ์ผ๋ถ„ ๋ณต์žก๋„ ์„น์…˜์„ ๋ Œ๋”๋งํ•œ๋‹ค.
357+
* ํŒจํ„ด ํƒœ๊ทธ ์ฝ”๋ฉ˜ํŠธ์— ๋ฌป์–ด๊ฐ€๋Š” ํ˜•ํƒœ์ด๋ฏ€๋กœ ๋งˆ์ปค/ํ‘ธํ„ฐ ์—†์ด
358+
* `### ๐Ÿ“Š ...` ํ—ค๋”๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค.
359+
*
360+
* @param {{problemName: string, solutions: Array}} entry
361+
* @returns {string} ๋ Œ๋”๋ง๋œ ์„น์…˜ (๋งˆ์ง€๋ง‰์— \n ์—†์Œ)
362+
*/
363+
export function renderComplexitySection(entry) {
360364
const lines = [];
361-
lines.push(COMPLEXITY_COMMENT_MARKER);
362365
lines.push("### ๐Ÿ“Š ์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„ ๋ถ„์„");
363366
lines.push("");
364367

365-
for (const { problemName, solutions } of entries) {
366-
lines.push(`### ${problemName}`);
367-
lines.push("");
368+
const solutions = entry?.solutions || [];
368369

369-
if (!solutions || solutions.length === 0) {
370-
lines.push(`> โš ๏ธ ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`);
371-
lines.push("");
372-
continue;
373-
}
370+
if (solutions.length === 0) {
371+
lines.push(`> โš ๏ธ ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`);
372+
return lines.join("\n");
373+
}
374374

375-
const isMulti = solutions.length > 1;
376-
const hasAnyAnnotationMissing = solutions.some(
377-
(s) => !s.hasUserAnnotation
375+
const isMulti = solutions.length > 1;
376+
const hasAnyAnnotationMissing = solutions.some((s) => !s.hasUserAnnotation);
377+
378+
if (isMulti) {
379+
lines.push(
380+
`> โ„น๏ธ ์ด ํŒŒ์ผ์—๋Š” **${solutions.length}๊ฐ€์ง€ ํ’€์ด**๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด ๊ฐ๊ฐ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.`
378381
);
382+
lines.push("");
379383

380-
if (isMulti) {
384+
solutions.forEach((sol, idx) => {
385+
const summaryResult = buildSummaryResult(sol);
386+
lines.push(`<details>`);
381387
lines.push(
382-
`> โ„น๏ธ ์ด ํŒŒ์ผ์—๋Š” **${solutions.length}๊ฐ€์ง€ ํ’€์ด**๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด ๊ฐ๊ฐ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.`
388+
`<summary>ํ’€์ด ${idx + 1}: <code>${sol.name}</code> โ€” ${summaryResult}</summary>`
383389
);
384390
lines.push("");
385-
386-
solutions.forEach((sol, idx) => {
387-
const summaryResult = buildSummaryResult(sol);
388-
lines.push(`<details>`);
389-
lines.push(
390-
`<summary>ํ’€์ด ${idx + 1}: <code>${sol.name}</code> โ€” ${summaryResult}</summary>`
391-
);
392-
lines.push("");
393-
lines.push(...buildSolutionBody(sol));
394-
lines.push(`</details>`);
395-
lines.push("");
396-
});
397-
} else {
398-
lines.push(...buildSolutionBody(solutions[0]));
399-
}
400-
401-
if (hasAnyAnnotationMissing) {
402-
lines.push("> ๐Ÿ’ก ํ’€์ด์— ์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„๋ฅผ ์ฃผ์„์œผ๋กœ ๋‚จ๊ฒจ๋ณด์„ธ์š”!");
391+
lines.push(...buildSolutionBody(sol));
392+
lines.push(`</details>`);
403393
lines.push("");
404-
}
405-
}
406-
407-
lines.push("---");
408-
lines.push("๐Ÿค– ์ด ๋Œ“๊ธ€์€ GitHub App์„ ํ†ตํ•ด ์ž๋™์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
409-
410-
return lines.join("\n") + "\n";
411-
}
412-
413-
// โ”€โ”€ ๋Œ“๊ธ€ upsert โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
414-
415-
async function upsertComplexityComment(
416-
repoOwner,
417-
repoName,
418-
prNumber,
419-
body,
420-
appToken
421-
) {
422-
const baseUrl = `https://api.github.com/repos/${repoOwner}/${repoName}`;
423-
424-
const listResponse = await fetch(
425-
`${baseUrl}/issues/${prNumber}/comments?per_page=100`,
426-
{ headers: getGitHubHeaders(appToken) }
427-
);
428-
if (!listResponse.ok) {
429-
throw new Error(
430-
`Failed to list comments: ${listResponse.status} ${listResponse.statusText}`
431-
);
432-
}
433-
434-
const comments = await listResponse.json();
435-
const existing = comments.find(
436-
(c) =>
437-
c.user?.type === "Bot" &&
438-
c.body?.includes(COMPLEXITY_COMMENT_MARKER)
439-
);
440-
441-
const headers = {
442-
...getGitHubHeaders(appToken),
443-
"Content-Type": "application/json",
444-
};
445-
446-
if (existing) {
447-
const res = await fetch(`${baseUrl}/issues/comments/${existing.id}`, {
448-
method: "PATCH",
449-
headers,
450-
body: JSON.stringify({ body }),
451394
});
452-
if (!res.ok) {
453-
throw new Error(
454-
`Failed to update complexity comment ${existing.id}: ${res.status}`
455-
);
456-
}
457-
console.log(
458-
`[complexity] Updated comment ${existing.id} on PR #${prNumber}`
459-
);
460395
} else {
461-
const res = await fetch(`${baseUrl}/issues/${prNumber}/comments`, {
462-
method: "POST",
463-
headers,
464-
body: JSON.stringify({ body }),
465-
});
466-
if (!res.ok) {
467-
throw new Error(`Failed to post complexity comment: ${res.status}`);
468-
}
469-
console.log(`[complexity] Created complexity comment on PR #${prNumber}`);
470-
}
471-
}
472-
473-
// โ”€โ”€ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ (export) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
474-
475-
export async function analyzeComplexity(
476-
repoOwner,
477-
repoName,
478-
prNumber,
479-
prData,
480-
appToken,
481-
openaiApiKey
482-
) {
483-
if (prData.draft === true) {
484-
console.log(`[complexity] Skipping PR #${prNumber}: draft`);
485-
return { skipped: "draft" };
486-
}
487-
const labels = (prData.labels || []).map((l) => l.name);
488-
if (hasMaintenanceLabel(labels)) {
489-
console.log(`[complexity] Skipping PR #${prNumber}: maintenance`);
490-
return { skipped: "maintenance" };
491-
}
492-
493-
// 1) PR files
494-
const filesRes = await fetch(
495-
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}/files?per_page=100`,
496-
{ headers: getGitHubHeaders(appToken) }
497-
);
498-
if (!filesRes.ok) {
499-
throw new Error(
500-
`Failed to list PR files: ${filesRes.status} ${filesRes.statusText}`
501-
);
502-
}
503-
const allFiles = await filesRes.json();
504-
505-
const solutionFiles = allFiles.filter(
506-
(f) =>
507-
(f.status === "added" || f.status === "modified") &&
508-
SOLUTION_PATH_REGEX.test(f.filename)
509-
);
510-
511-
console.log(
512-
`[complexity] PR #${prNumber}: ${allFiles.length} files, ${solutionFiles.length} solutions`
513-
);
514-
515-
if (solutionFiles.length === 0) {
516-
return { skipped: "no-solution-files" };
396+
lines.push(...buildSolutionBody(solutions[0]));
517397
}
518398

519-
// 2) ๋ชจ๋“  ์†”๋ฃจ์…˜ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
520-
const fileEntries = [];
521-
let totalSize = 0;
522-
523-
for (const file of solutionFiles) {
524-
const problemName = file.filename.split("/")[0];
525-
try {
526-
const rawRes = await fetch(file.raw_url);
527-
if (!rawRes.ok) {
528-
console.error(
529-
`[complexity] Failed to fetch ${file.filename}: ${rawRes.status}`
530-
);
531-
continue;
532-
}
533-
let content = await rawRes.text();
534-
if (content.length > MAX_FILE_SIZE) {
535-
content = content.slice(0, MAX_FILE_SIZE);
536-
}
537-
538-
if (totalSize + content.length > MAX_TOTAL_SIZE) {
539-
console.log(
540-
`[complexity] Reached MAX_TOTAL_SIZE, skipping remaining files`
541-
);
542-
break;
543-
}
544-
545-
totalSize += content.length;
546-
fileEntries.push({ problemName, content });
547-
} catch (error) {
548-
console.error(
549-
`[complexity] Failed to download ${file.filename}: ${error.message}`
550-
);
551-
}
399+
if (hasAnyAnnotationMissing) {
400+
lines.push("> ๐Ÿ’ก ํ’€์ด์— ์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„๋ฅผ ์ฃผ์„์œผ๋กœ ๋‚จ๊ฒจ๋ณด์„ธ์š”!");
552401
}
553402

554-
if (fileEntries.length === 0) {
555-
return { skipped: "all-downloads-failed" };
403+
// trailing ๋นˆ ์ค„ ์ œ๊ฑฐ
404+
while (lines.length > 0 && lines[lines.length - 1] === "") {
405+
lines.pop();
556406
}
557407

558-
// 3) OpenAI 1ํšŒ ํ˜ธ์ถœ๋กœ ๋ชจ๋“  ํŒŒ์ผ ๋ถ„์„
559-
const analysisResults = await callComplexityAnalysis(
560-
fileEntries,
561-
openaiApiKey
562-
);
563-
564-
// 4) ๊ฒฐ๊ณผ๋ฅผ fileEntries ์ˆœ์„œ์— ๋งž์ถฐ ๋งคํ•‘
565-
const entries = fileEntries.map((fe) => {
566-
const match = analysisResults.find(
567-
(r) => r.problemName === fe.problemName
568-
);
569-
return match || { problemName: fe.problemName, solutions: [] };
570-
});
571-
572-
// 5) ๋ณธ๋ฌธ ๋นŒ๋“œ + upsert
573-
const body = formatComplexityCommentBody(entries);
574-
await upsertComplexityComment(repoOwner, repoName, prNumber, body, appToken);
575-
576-
return {
577-
analyzed: entries.filter((e) => e.solutions.length > 0).length,
578-
total: fileEntries.length,
579-
};
408+
return lines.join("\n");
580409
}
410+

0 commit comments

Comments
ย (0)