|
1 | 1 | /** |
2 | | - * ์๊ฐ/๊ณต๊ฐ ๋ณต์ก๋ ์๋ ๋ถ์. |
3 | | - * PR opened/reopened/synchronize ์ ํธ์ถ๋๋ค. |
| 2 | + * ์๊ฐ/๊ณต๊ฐ ๋ณต์ก๋ ๋ถ์ (๋ถ์ ํจ์ + ์์ ํฌํผ). |
| 3 | + * |
| 4 | + * ์ค์ผ์คํธ๋ ์ด์
๊ณผ ๋๊ธ ๊ฒ์๋ tag-patterns ํธ๋ค๋ฌ๊ฐ ๋ด๋นํ๋ฉฐ, ์ด ๋ชจ๋์ |
| 5 | + * OpenAI ํธ์ถ(`callComplexityAnalysis`)๊ณผ ์ฌ์ฉ์ ์ฃผ์ ์ฒ๋ฆฌ/๋งค์นญ ํฌํผ, |
| 6 | + * ํ ํ์ผ๋ถ ์น์
๋ ๋๋ฌ(`renderComplexitySection`)๋ง ์ ๊ณตํ๋ค. |
4 | 7 | * |
5 | 8 | * ์ฑ
์ ๋ถ๋ด (plan v4): |
6 | 9 | * - ์ฝ๋: ์ฌ์ฉ์ ๋ณต์ก๋ ์ฃผ์ ์ ๊ฑฐ / ์ถ์ถ / matches ํ์ . |
7 | 10 | * - LLM : actualTime / actualSpace / feedback / suggestion / headerLine ๋ง ์ฑ
์. |
8 | 11 | */ |
9 | 12 |
|
10 | | -import { getGitHubHeaders } from "../utils/github.js"; |
11 | | -import { hasMaintenanceLabel } from "../utils/validation.js"; |
12 | | - |
13 | 13 | // โโ ์์ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
14 | 14 |
|
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; |
19 | 15 | const FILE_DELIMITER = "====="; |
20 | 16 |
|
21 | 17 | const COMMENT_START_PATTERN = /^(?:\/\/|#|--|;|\/\*|\*(?!\/)|"""|''')/; |
@@ -244,7 +240,7 @@ export function composeSolution(modelSol, originalContent) { |
244 | 240 | }; |
245 | 241 | } |
246 | 242 |
|
247 | | -async function callComplexityAnalysis(fileEntries, apiKey) { |
| 243 | +export async function callComplexityAnalysis(fileEntries, apiKey) { |
248 | 244 | const userPrompt = fileEntries |
249 | 245 | .map( |
250 | 246 | (f) => |
@@ -356,225 +352,59 @@ function buildSolutionBody(solution) { |
356 | 352 | return lines; |
357 | 353 | } |
358 | 354 |
|
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) { |
360 | 364 | const lines = []; |
361 | | - lines.push(COMPLEXITY_COMMENT_MARKER); |
362 | 365 | lines.push("### ๐ ์๊ฐ/๊ณต๊ฐ ๋ณต์ก๋ ๋ถ์"); |
363 | 366 | lines.push(""); |
364 | 367 |
|
365 | | - for (const { problemName, solutions } of entries) { |
366 | | - lines.push(`### ${problemName}`); |
367 | | - lines.push(""); |
| 368 | + const solutions = entry?.solutions || []; |
368 | 369 |
|
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 | + } |
374 | 374 |
|
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}๊ฐ์ง ํ์ด**๊ฐ ํฌํจ๋์ด ์์ด ๊ฐ๊ฐ ๋ถ์ํฉ๋๋ค.` |
378 | 381 | ); |
| 382 | + lines.push(""); |
379 | 383 |
|
380 | | - if (isMulti) { |
| 384 | + solutions.forEach((sol, idx) => { |
| 385 | + const summaryResult = buildSummaryResult(sol); |
| 386 | + lines.push(`<details>`); |
381 | 387 | lines.push( |
382 | | - `> โน๏ธ ์ด ํ์ผ์๋ **${solutions.length}๊ฐ์ง ํ์ด**๊ฐ ํฌํจ๋์ด ์์ด ๊ฐ๊ฐ ๋ถ์ํฉ๋๋ค.` |
| 388 | + `<summary>ํ์ด ${idx + 1}: <code>${sol.name}</code> โ ${summaryResult}</summary>` |
383 | 389 | ); |
384 | 390 | 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>`); |
403 | 393 | 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 }), |
451 | 394 | }); |
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 | | - ); |
460 | 395 | } 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])); |
517 | 397 | } |
518 | 398 |
|
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("> ๐ก ํ์ด์ ์๊ฐ/๊ณต๊ฐ ๋ณต์ก๋๋ฅผ ์ฃผ์์ผ๋ก ๋จ๊ฒจ๋ณด์ธ์!"); |
552 | 401 | } |
553 | 402 |
|
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(); |
556 | 406 | } |
557 | 407 |
|
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"); |
580 | 409 | } |
| 410 | + |
0 commit comments