diff --git a/PATCHNOTES.md b/PATCHNOTES.md index bb42a8b..4ee4592 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,12 @@ # Astra Patch Notes +## v2.2.255 (2026-06-18) +### ๐Ÿงฉ `/review` โ€” ์ฝ”๋“œ ๋ฆฌ๋ทฐ map-reduce ์ฒญํ‚น (์•ฝํ•œ ๋ชจ๋ธ๋„ ํฐ ์ฝ”๋“œ๋ฒ ์ด์Šค ์ฒ˜๋ฆฌ) +- ์ผ๋ฐ˜ ์—์ด์ „ํŠธ ์ฑ„ํŒ…์€ ์ฝ”๋“œ ๋ฆฌ๋ทฐ์ฒ˜๋Ÿผ ์ž…๋ ฅ์ด ํฐ ์ž‘์—…์„ ๋‹จ์ผ ํ˜ธ์ถœ๋กœ ์ฒ˜๋ฆฌํ•˜๋‹ค ์•ฝํ•œ ๋กœ์ปฌ ๋ชจ๋ธ์—์„œ ๋นˆ ์‘๋‹ต(์ฒซ ํ† ํฐ EOS)์œผ๋กœ ๋ฌด๋„ˆ์ง„๋‹ค. `/meet` ์˜ ๊ฒ€์ฆ๋œ map-reduce ๋ฅผ ์ฝ”๋“œ ๋ฆฌ๋ทฐ์— ์ ์šฉํ•œ **`/review <๋””๋ ‰ํ„ฐ๋ฆฌ|ํŒŒ์ผ> [์ดˆ์ ]`** ๋ช…๋ น ์‹ ์„ค. ์ฝ”์–ด ์ฑ„ํŒ… ๊ฒฝ๋กœ๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Œ. +- **Map**: ์†Œ์Šค ํŒŒ์ผ์„ ํ•˜๋‚˜์”ฉ ๋…๋ฆฝ ๋ฆฌ๋ทฐ(๋ฒ„๊ทธยท๋ณด์•ˆยท์„ฑ๋Šฅยท์„ค๊ณ„ยท๊ฐ€๋…์„ฑ, ๋ผ์ธ ์ธ์šฉ ๊ทผ๊ฑฐ ํ•„์ˆ˜) โ†’ ํŒŒ์ผ๋ณ„ ๋…ธํŠธ. `callLmSynthesis` ์˜ ์žฌ์‹œ๋„/์ถœ๋ ฅ๋ถ•๊ดด ๊ฐ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ํ™œ์šฉ. ํ•œ ํŒŒ์ผ์ด ์‹คํŒจํ•ด๋„ ์ „์ฒด๋ฅผ ํฌ๊ธฐํ•˜์ง€ ์•Š๊ณ  ๋ถ€๋ถ„ ๋ฆฌ๋ทฐ๋กœ ์ง„ํ–‰. +- **Reduce**: ๋…ธํŠธ๋ฅผ ํ†ตํ•ฉํ•ด ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋งค๊ฒจ์ง„ ๋ณด๊ณ ์„œ(์ดํ‰ยท์šฐ์„ ๊ฐœ์„ ยท๋ถ„๋ฅ˜๋ณ„ยท์ž˜๋œ์ ยท๋‹ค์Œ๋‹จ๊ณ„). ๋…ธํŠธ๊ฐ€ ํฌ๋ฉด ๋ฐฐ์น˜๋กœ ์ ‘๋Š” **hierarchical fold** ๋กœ reduce ์ž…๋ ฅ๋„ ์•ฝํ•œ ๋ชจ๋ธ ํ•œ๋„(16K) ์•ˆ์— ์œ ์ง€. +- ์˜์กด์„ฑยท๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ ์ž๋™ ์ œ์™ธ(`node_modules`/`dist`/`.d.ts`/`.min.js`/lock ๋“ฑ), ํŒŒ์ผ 30๊ฐœยท400KB ์ƒํ•œ(์ดˆ๊ณผ ์‹œ ๊ฒฝ๊ณ ), ๊ฒฐ๊ณผ๋Š” wiki ์— ์ €์žฅ. ์‹ ๊ทœ: [reviewPrompt.ts](src/features/datacollect/prompts/reviewPrompt.ts) ยท [reviewFiles.ts](src/features/datacollect/reviewFiles.ts). ํ…Œ์ŠคํŠธ +5๊ฑด(์ „์ฒด 667 ํ†ต๊ณผ). + ## v2.2.254 (2026-06-18) ### ๐Ÿ”Ž ๋นˆ ์‘๋‹ต(empty response) ์ง„๋‹จ ์ •ํ™•๋„ โ€” MoE ํ™œ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ ์ธ์‹ - ์ผ๋ฐ˜ ์—์ด์ „ํŠธ ์ฑ„ํŒ…์—์„œ ์•ฝํ•œ ๋ชจ๋ธ์ด ํฐ ์ž…๋ ฅ์— ์ฒซ ํ† ํฐ EOS ๋กœ ๋ฌด๋„ˆ์ ธ **๋นˆ ์‘๋‹ต**์ด ๋‚  ๋•Œ, ๋ชจ๋ธ๋ช… ํŒŒ์„œ๊ฐ€ `gemma-4-26b-a4b` ๋ฅผ "26B ํฐ ๋ชจ๋ธ"๋กœ ์˜คํŒํ•ด ์—‰๋šฑํ•œ ์•ˆ๋‚ด๋ฅผ ํ•˜๋˜ ๋ฌธ์ œ. **ํ™œ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”์ •**(`estimateActiveParamsB`: `a4b`โ†’4, `A3B`โ†’3, `e2b`โ†’2) ์ถ”๊ฐ€ โ†’ MoE ๋ฅผ ์ •ํ™•ํžˆ ์‹๋ณ„. ([contextManager.ts](src/lib/contextManager.ts)) diff --git a/package.json b/package.json index 6e8e981..abd6d25 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.254", + "version": "2.2.255", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/features/datacollect/handlers.ts b/src/features/datacollect/handlers.ts index 81a0ae9..afadf9c 100644 --- a/src/features/datacollect/handlers.ts +++ b/src/features/datacollect/handlers.ts @@ -12,9 +12,12 @@ import * as vscode from 'vscode'; import { promises as fsp } from 'fs'; +import * as path from 'path'; import { registerSlashCommand, chunk, type Webview } from './slashRouter'; import { callLmSynthesis } from './llm'; import { bridgeFetch, BRIDGE_API } from './bridgeClient'; +import { collectSourceFiles } from './reviewFiles'; +import { buildReviewFilePrompt, buildReviewReducePrompt } from './prompts/reviewPrompt'; import { type SynthesisPart, buildSynthesisPrompt } from './prompts/synthesisPrompt'; import { type YoutubeAnalysisMode, @@ -859,6 +862,198 @@ async function runMeetConfirm(arg: string, view: Webview | undefined, context?: chunk(view, '\n' + lines.map(l => ` ${l}`).join('\n') + '\n'); } +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /review โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +/** + * `/review <๋””๋ ‰ํ„ฐ๋ฆฌ|ํŒŒ์ผ ๊ฒฝ๋กœ> [์ดˆ์ ]` โ€” ์ฝ”๋“œ ๋ฆฌ๋ทฐ map-reduce. + * + * ์ผ๋ฐ˜ ์—์ด์ „ํŠธ ์ฑ„ํŒ…์€ ํฐ ์ฝ”๋“œ๋ฒ ์ด์Šค ๋ฆฌ๋ทฐ๋ฅผ ๋‹จ์ผ ํ˜ธ์ถœ๋กœ ์ฒ˜๋ฆฌํ•˜๋‹ค ์•ฝํ•œ ๋กœ์ปฌ ๋ชจ๋ธ์—์„œ + * ๋นˆ ์‘๋‹ต(์ฒซ ํ† ํฐ EOS)์œผ๋กœ ๋ฌด๋„ˆ์ง„๋‹ค. /review ๋Š” /meet ์™€ ๊ฐ™์€ map-reduce ๋กœ ์šฐํšŒ: + * - Map : ํŒŒ์ผ ํ•˜๋‚˜์”ฉ ๋…๋ฆฝ ๋ฆฌ๋ทฐ โ†’ ํŒŒ์ผ๋ณ„ ๋ฐœ๊ฒฌ์‚ฌํ•ญ ๋…ธํŠธ(callLmSynthesis ๊ฐ€ ์žฌ์‹œ๋„/๋ถ•๊ดด + * ๊ฐ์ง€๊นŒ์ง€ ๋‚ด์žฅ) + * - Reduce: ๋…ธํŠธ๋ฅผ ํ†ตํ•ฉ โ†’ ์šฐ์„ ์ˆœ์œ„ ๋งค๊ฒจ์ง„ ์ตœ์ข… ๋ณด๊ณ ์„œ. ๋…ธํŠธ๊ฐ€ ํฌ๋ฉด ๋ฐฐ์น˜๋กœ ์ ‘์–ด + * (hierarchical fold) reduce ์ž…๋ ฅ๋„ ์•ฝํ•œ ๋ชจ๋ธ ํ•œ๋„ ์•ˆ์— ๋‘”๋‹ค. + * ํ•œ ์กฐ๊ฐ์ด ๋๋‚ด ์‹คํŒจํ•ด๋„ ์ „์ฒด๋ฅผ ํฌ๊ธฐํ•˜์ง€ ์•Š๊ณ  ๋ถ€๋ถ„ ๋ฆฌ๋ทฐ๋กœ ์ง„ํ–‰ํ•œ๋‹ค(/meet ์™€ ๋™์ผ ์ •์ฑ…). + */ +const REVIEW_MAX_FILES = 30; // 1ํšŒ ๋ฆฌ๋ทฐ ํŒŒ์ผ ์ƒํ•œ (์ดˆ๊ณผ๋ถ„์€ ์ž˜๋ฆผ + ๊ฒฝ๊ณ ) +const REVIEW_MAX_FILE_BYTES = 400_000; // ์ด๋ณด๋‹ค ํฐ ํŒŒ์ผ์€ ์ƒ์„ฑ๋ฌผ๋กœ ๋ณด๊ณ  ์ œ์™ธ +const REVIEW_PER_FILE_CHARS = 16_000; // ํŒŒ์ผ 1๊ฐœ์—์„œ ๋ชจ๋ธ์— ๋ณด๋‚ผ ๋ณธ๋ฌธ ์ƒํ•œ (์ดˆ๊ณผ ์‹œ ์•ž๋ถ€๋ถ„๋งŒ) +const REVIEW_REDUCE_BUDGET = 16_000; // reduce 1ํšŒ ์ž…๋ ฅ(๋…ธํŠธ) ์ƒํ•œ โ€” ์•ฝํ•œ ๋ชจ๋ธ ์•ˆ์ „์„  + +async function runReview(arg: string, view: Webview | undefined, _context?: vscode.ExtensionContext): Promise { + const trimmed = arg.trim(); + // ๊ฒฝ๋กœ ํŒŒ์‹ฑ โ€” /meet ์™€ ๋™์ผํ•˜๊ฒŒ ๋”ฐ์˜ดํ‘œ ๊ฐ์‹ผ ๊ฒฝ๋กœ + ๋’ค๋”ฐ๋ฅด๋Š” ์ดˆ์  ํ…์ŠคํŠธ ์ง€์›. + let targetPath = ''; + let focus = ''; + if (trimmed.startsWith('"')) { + const end = trimmed.indexOf('"', 1); + if (end > 0) { targetPath = trimmed.slice(1, end); focus = trimmed.slice(end + 1).trim(); } + } + if (!targetPath) { + const sp = trimmed.indexOf(' '); + if (sp === -1) targetPath = trimmed; + else { targetPath = trimmed.slice(0, sp); focus = trimmed.slice(sp + 1).trim(); } + } + if (!targetPath) { + chunk(view, '์‚ฌ์šฉ๋ฒ•: `/review <๋””๋ ‰ํ„ฐ๋ฆฌ ๋˜๋Š” ํŒŒ์ผ ๊ฒฝ๋กœ> [๋ฆฌ๋ทฐ ์ดˆ์ ]`\n์˜ˆ: `/review E:\\Wiki\\astraai`\n๊ฒฝ๋กœ์— ๊ณต๋ฐฑ์ด ์žˆ์œผ๋ฉด ๋”ฐ์˜ดํ‘œ๋กœ: `/review "E:\\my proj\\src" ๋ณด์•ˆ ์œ„์ฃผ๋กœ`\n'); + return true; + } + + // ๋Œ€์ƒ ํŒ๋ณ„ โ€” ํŒŒ์ผ 1๊ฐœ vs ๋””๋ ‰ํ„ฐ๋ฆฌ. + let stat; + try { + stat = await fsp.stat(targetPath); + } catch (e: any) { + chunk(view, `\nโŒ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${e?.message || String(e)}\n`); + return true; + } + + const projectLabel = targetPath.replace(/[\\/]+$/, '').replace(/^.*[\\/]/, '') || targetPath; + chunk(view, `๐Ÿ” **์ฝ”๋“œ ๋ฆฌ๋ทฐ**: ${targetPath}${focus ? `\n์ดˆ์ : ${focus}` : ''}\n\n`); + + // โ”€โ”€ ๋Œ€์ƒ ํŒŒ์ผ ์ˆ˜์ง‘ โ”€โ”€ + interface RFile { absPath: string; relPath: string; } + let files: RFile[] = []; + let truncatedFiles = false; + let totalCandidates = 0; + if (stat.isDirectory()) { + const collected = await collectSourceFiles(targetPath, { maxFiles: REVIEW_MAX_FILES, maxFileBytes: REVIEW_MAX_FILE_BYTES }); + files = collected.files.map(f => ({ absPath: f.absPath, relPath: f.relPath })); + truncatedFiles = collected.truncated; + totalCandidates = collected.totalCandidates; + } else { + files = [{ absPath: targetPath, relPath: path.basename(targetPath) }]; + totalCandidates = 1; + } + if (files.length === 0) { + chunk(view, `\nโ„น๏ธ ๋ฆฌ๋ทฐํ•  ์†Œ์Šค ํŒŒ์ผ์„ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. (์˜์กด์„ฑยท๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ์€ ์ œ์™ธ๋ฉ๋‹ˆ๋‹ค)\n`); + return true; + } + chunk(view, `๐Ÿ“‚ ์†Œ์Šค ํŒŒ์ผ ${files.length}๊ฐœ ๋ฆฌ๋ทฐ ๋Œ€์ƒ${truncatedFiles ? ` (ํ›„๋ณด ${totalCandidates}๊ฐœ ์ค‘ ์ƒ์œ„ ${files.length}๊ฐœ๋งŒ โ€” ์ƒํ•œ ${REVIEW_MAX_FILES}; ๋ฒ”์œ„๋ฅผ ์ขํ˜€ ๋‹ค์‹œ ์‹คํ–‰ ๊ถŒ์žฅ)` : ''}\n\n`); + + const reviewSystem = '๋‹น์‹ ์€ ์‹œ๋‹ˆ์–ด ์ฝ”๋“œ ๋ฆฌ๋ทฐ์–ด์ž…๋‹ˆ๋‹ค. ์ œ๊ณต๋œ ์ฝ”๋“œ๋งŒ ๊ทผ๊ฑฐ๋กœ ์‚ฌ์‹ค ๊ธฐ๋ฐ˜์˜ ๋ฐœ๊ฒฌ์‚ฌํ•ญ์„ ์ถ”์ถœยทํ†ตํ•ฉํ•˜๋ฉฐ, ์—†๋Š” ์ฝ”๋“œยท์ทจ์•ฝ์ ์„ ์ง€์–ด๋‚ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์ถœ๋ ฅ์€ ํ•œ๊ตญ์–ด์ž…๋‹ˆ๋‹ค.'; + + // โ”€โ”€ Map: ํŒŒ์ผ๋ณ„ ๋…๋ฆฝ ๋ฆฌ๋ทฐ โ”€โ”€ + const noteBlocks: string[] = []; + let failed = 0; + for (let i = 0; i < files.length; i++) { + const f = files[i]; + chunk(view, ` โณ (${i + 1}/${files.length}) ${f.relPath} ๋ฆฌ๋ทฐ ์ค‘โ€ฆ\n`); + let content: string; + try { + content = await fsp.readFile(f.absPath, 'utf-8'); + } catch (e: any) { + failed++; + chunk(view, ` โš ๏ธ ์ฝ๊ธฐ ์‹คํŒจ(๊ฑด๋„ˆ๋œ€): ${e?.message || String(e)}\n`); + continue; + } + if (!content.trim()) { continue; } // ๋นˆ ํŒŒ์ผ์€ ์กฐ์šฉํžˆ ์Šคํ‚ต + let truncNote = ''; + if (content.length > REVIEW_PER_FILE_CHARS) { + content = content.slice(0, REVIEW_PER_FILE_CHARS); + truncNote = `\n(ํŒŒ์ผ์ด ์ปค์„œ ์•ž ${REVIEW_PER_FILE_CHARS.toLocaleString()}์ž๋งŒ ๋ฆฌ๋ทฐํ•จ)`; + } + try { + const note = await callLmSynthesis( + buildReviewFilePrompt(f.relPath, content, i + 1, files.length, focus), + reviewSystem, + ); + if (!note) throw new Error('๋ฆฌ๋ทฐ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.'); + noteBlocks.push(`### โ”€โ”€โ”€ ${f.relPath} โ”€โ”€โ”€\n${note.trim()}${truncNote}`); + chunk(view, ` โœ“ ์™„๋ฃŒ\n`); + } catch (e: any) { + failed++; + noteBlocks.push(`### โ”€โ”€โ”€ ${f.relPath} โ”€โ”€โ”€\n(์ด ํŒŒ์ผ์€ ๋ชจ๋ธ ์ถœ๋ ฅ ์˜ค๋ฅ˜๋กœ ๋ฆฌ๋ทฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: ${e?.message || String(e)})`); + chunk(view, ` โš ๏ธ ๋ฆฌ๋ทฐ ์‹คํŒจ(๊ฑด๋„ˆ๋œ€): ${e?.message || String(e)}\n`); + } + } + if (failed === files.length) { + chunk(view, `\nโŒ ๋ชจ๋“  ํŒŒ์ผ ๋ฆฌ๋ทฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค โ€” ๋ชจ๋ธ ์ถœ๋ ฅ์ด ๊ณ„์† ๋ถ•๊ดดํ•ฉ๋‹ˆ๋‹ค. ๋” ํฐ ๋ชจ๋ธ(ํ™œ์„ฑ 7B+) ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.\n`); + return true; + } + if (failed > 0) { + chunk(view, `\nโš ๏ธ ${files.length}๊ฐœ ์ค‘ ${failed}๊ฐœ ํŒŒ์ผ์„ ๋ฆฌ๋ทฐํ•˜์ง€ ๋ชปํ•ด **๋ถ€๋ถ„ ๋ฆฌ๋ทฐ**๋กœ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n`); + } + + // โ”€โ”€ Reduce: ๋…ธํŠธ ํ†ตํ•ฉ (๋…ธํŠธ๊ฐ€ ํฌ๋ฉด ๋ฐฐ์น˜๋กœ ์ ‘์–ด ์•ฝํ•œ ๋ชจ๋ธ ํ•œ๋„ ์•ˆ์— ์œ ์ง€) โ”€โ”€ + chunk(view, `\n ๐Ÿงช ๋ฐœ๊ฒฌ์‚ฌํ•ญ ํ†ตํ•ฉ ์ค‘โ€ฆ\n`); + let report: string; + try { + report = await reduceReviewNotes(noteBlocks, projectLabel, files.length, focus, reviewSystem, view); + if (!report) throw new Error('ํ†ตํ•ฉ ๋‹จ๊ณ„ ์‘๋‹ต์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.'); + } catch (e: any) { + chunk(view, `\nโš ๏ธ ํ†ตํ•ฉ ์‹คํŒจ: ${e?.message || String(e)}\n์•ฝํ•œ ๋ชจ๋ธ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค โ€” ํ™œ์„ฑ 7B+ ๋ชจ๋ธ ๋˜๋Š” ๋ฒ”์œ„๋ฅผ ์ขํ˜€ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.\n`); + return true; + } + + chunk(view, '\n' + report + '\n\n'); + + // โ”€โ”€ ์ €์žฅ (wiki) โ€” /meet ์™€ ๋™์ผ ๊ฒฝ๋กœ โ”€โ”€ + try { + const cfg = vscode.workspace.getConfiguration('g1nation'); + const today = new Date().toISOString().slice(0, 10); + const title = `์ฝ”๋“œ๋ฆฌ๋ทฐ ${projectLabel} ${today}`; + const savePath = (cfg.get('datacollectSavePath', '') || '').trim(); + const body: Record = { title, content: report }; + if (savePath) body.saveDir = savePath; + const saved = await bridgeFetch<{ success: boolean; path?: string }>( + BRIDGE_API.wiki.save, + { method: 'POST', body: JSON.stringify(body) }, + { timeoutMs: 30_000 }, + ); + chunk(view, `๐Ÿ’พ **๋ฆฌ๋ทฐ ์ €์žฅ ์™„๋ฃŒ**: \`${saved?.path || '(๊ฒฝ๋กœ ๋ฏธํ™•์ธ)'}\`\n`); + } catch (e: any) { + chunk(view, `โš ๏ธ ๋ฆฌ๋ทฐ ์ €์žฅ ์‹คํŒจ(๋ณด๊ณ ์„œ๋Š” ์œ„์— ์ถœ๋ ฅ๋จ): ${e?.message || String(e)}\n`); + } + return true; +} + +/** + * ํŒŒ์ผ๋ณ„ ๋…ธํŠธ๋ฅผ ์ตœ์ข… ๋ณด๊ณ ์„œ๋กœ ํ†ตํ•ฉ. ๋…ธํŠธ ํ•ฉ๊ณ„๊ฐ€ REVIEW_REDUCE_BUDGET ๋ฅผ ๋„˜์œผ๋ฉด + * ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ  ๊ฐ ๋ฐฐ์น˜๋ฅผ reduce ํ•œ ๋’ค(๋ถ€๋ถ„ ๋ณด๊ณ ์„œ) ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ reduce ํ•˜๋Š” fold + * ๋กœ ์ˆ˜๋ ด์‹œํ‚จ๋‹ค โ€” reduce ์ž…๋ ฅ์ด ํ•ญ์ƒ ์•ฝํ•œ ๋ชจ๋ธ ํ•œ๋„ ์•ˆ์— ๋“ค์–ด์˜ค๊ฒŒ. + */ +async function reduceReviewNotes( + noteBlocks: string[], projectLabel: string, fileCount: number, focus: string, + reviewSystem: string, view: Webview | undefined, +): Promise { + // ๊ธ€์ž ์˜ˆ์‚ฐ์œผ๋กœ ๋ฐฐ์น˜ ๋ฌถ๊ธฐ โ€” ํ•œ ๋ธ”๋ก์ด ์˜ˆ์‚ฐ๋ณด๋‹ค ์ปค๋„ ๋‹จ๋… ๋ฐฐ์น˜๋กœ ํ—ˆ์šฉ. + const packBatches = (blocks: string[]): string[][] => { + const batches: string[][] = []; + let cur: string[] = []; + let curLen = 0; + for (const b of blocks) { + if (cur.length && curLen + b.length > REVIEW_REDUCE_BUDGET) { batches.push(cur); cur = []; curLen = 0; } + cur.push(b); curLen += b.length + 2; + } + if (cur.length) batches.push(cur); + return batches; + }; + + let level = noteBlocks; + let pass = 0; + while (true) { + const batches = packBatches(level); + if (batches.length === 1) { + return await callLmSynthesis( + buildReviewReducePrompt(batches[0].join('\n\n'), projectLabel, fileCount, focus), + reviewSystem, + ); + } + pass++; + chunk(view, ` ยท ํ†ตํ•ฉ ${pass}๋‹จ๊ณ„ โ€” ${level.length}๊ฐœ ๋…ธํŠธ๋ฅผ ${batches.length}๊ฐœ ๋ฐฐ์น˜๋กœ ์ ‘๋Š” ์ค‘โ€ฆ\n`); + const partials: string[] = []; + for (let i = 0; i < batches.length; i++) { + const r = await callLmSynthesis( + buildReviewReducePrompt(batches[i].join('\n\n'), projectLabel, fileCount, focus), + reviewSystem, + ); + if (r && r.trim()) partials.push(`### โ”€โ”€โ”€ ๋ถ€๋ถ„ ํ†ตํ•ฉ ${pass}-${i + 1} โ”€โ”€โ”€\n${r.trim()}`); + } + if (partials.length === 0) throw new Error('๋ฐฐ์น˜ ํ†ตํ•ฉ์ด ๋ชจ๋‘ ๋น„์—ˆ์Šต๋‹ˆ๋‹ค.'); + level = partials; + } +} + // โ”€โ”€โ”€ ๋“ฑ๋ก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // /research(NotebookLM Deep Research)๋Š” v2.2.205 ์—์„œ ์ œ๊ฑฐ โ€” NotebookLM ์€ ๋กœ์ปฌ @@ -869,3 +1064,4 @@ registerSlashCommand({ name: '/youtube', description: 'YouTube ๋‹จ์ผ ์˜์ƒ ๋˜ registerSlashCommand({ name: '/blog', description: 'Blog Pipeline ์•ˆ๋‚ด (Datacollect ๋ณ„๋„ ํ๋ฆ„)', handler: runBlog }); registerSlashCommand({ name: '/wikify', description: '์›น URL โ†’ P-Reinforce v3.0 ์œ„ํ‚ค ํ•ฉ์„ฑยท์ €์žฅ', handler: runWikify }); registerSlashCommand({ name: '/meet', description: 'ํšŒ์˜ transcript โ†’ ํšŒ์˜๋ก ํ•ฉ์„ฑ + ์บ˜๋ฆฐ๋”ยทtask ๋“ฑ๋ก', handler: runMeet }); +registerSlashCommand({ name: '/review', description: '์ฝ”๋“œ ๋ฆฌ๋ทฐ โ€” ๋””๋ ‰ํ„ฐ๋ฆฌ/ํŒŒ์ผ์„ ํŒŒ์ผ๋ณ„ ๋ฆฌ๋ทฐ(map) ํ›„ ํ†ตํ•ฉ(reduce). ์•ฝํ•œ ๋ชจ๋ธ๋„ ์ฒ˜๋ฆฌ', handler: runReview }); diff --git a/src/features/datacollect/prompts/reviewPrompt.ts b/src/features/datacollect/prompts/reviewPrompt.ts new file mode 100644 index 0000000..577c7bf --- /dev/null +++ b/src/features/datacollect/prompts/reviewPrompt.ts @@ -0,0 +1,93 @@ +/** + * ์ฝ”๋“œ ๋ฆฌ๋ทฐ map-reduce ํ”„๋กฌํ”„ํŠธ. + * + * ์ผ๋ฐ˜ ์—์ด์ „ํŠธ ์ฑ„ํŒ…์€ ์ฝ”๋“œ ๋ฆฌ๋ทฐ์ฒ˜๋Ÿผ ์ž…๋ ฅ์ด ํฐ ์ž‘์—…์„ ๋‹จ์ผ ํ˜ธ์ถœ๋กœ ์ฒ˜๋ฆฌํ•˜๋‹ค + * ์•ฝํ•œ ๋กœ์ปฌ ๋ชจ๋ธ์—์„œ ๋นˆ ์‘๋‹ต(์ฒซ ํ† ํฐ EOS)์œผ๋กœ ๋ฌด๋„ˆ์ง„๋‹ค. /review ๋Š” /meet ์™€ ๊ฐ™์€ + * map-reduce ๋กœ ์ด๋ฅผ ์šฐํšŒํ•œ๋‹ค: + * - Map : ํŒŒ์ผ ํ•˜๋‚˜์”ฉ ๋…๋ฆฝ ๋ฆฌ๋ทฐ โ†’ ํŒŒ์ผ๋ณ„ ๋ฐœ๊ฒฌ์‚ฌํ•ญ(๊ทผ๊ฑฐ ์ธ์šฉ) + * - Reduce: ํŒŒ์ผ๋ณ„ ๋…ธํŠธ๋ฅผ ํ†ตํ•ฉ โ†’ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋งค๊ฒจ์ง„ ์ตœ์ข… ๋ฆฌ๋ทฐ ๋ณด๊ณ ์„œ + * ์ž…๋ ฅ์ด ์ž‘๊ฒŒ ์ชผ๊ฐœ์ง€๋ฏ€๋กœ ์•ฝํ•œ ๋ชจ๋ธ๋„ ๋๊นŒ์ง€ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๊ณ , lost-in-the-middle ๋„ ์ค€๋‹ค. + */ + +/** [Map] ํŒŒ์ผ 1๊ฐœ๋ฅผ ๋ฆฌ๋ทฐํ•ด ๋ฐœ๊ฒฌ์‚ฌํ•ญ๋งŒ ์ถ”์ถœ. ๊ทผ๊ฑฐ(๋ผ์ธ/์ฝ”๋“œ ์ธ์šฉ) ํ•„์ˆ˜, ๋‚ ์กฐ ๊ธˆ์ง€. */ +export function buildReviewFilePrompt(relPath: string, content: string, idx: number, total: number, focus: string): string { + const focusBlock = focus.trim() + ? `\n# ๋ฆฌ๋ทฐ ์ดˆ์  (์‚ฌ์šฉ์ž ์ง€์ • โ€” ์šฐ์„  ์ ๊ฒ€)\n${focus.trim()}\n` + : ''; + // ๋ผ์ธ ๋ฒˆํ˜ธ๋ฅผ ๋ถ™์—ฌ ๋ชจ๋ธ์ด ์ •ํ™•ํ•œ ์œ„์น˜๋ฅผ ์ธ์šฉํ•˜๊ฒŒ ํ•œ๋‹ค(ํ™˜๊ฐ ์œ„์น˜ ๋ฐฉ์ง€). + const numbered = content.split('\n').map((l, i) => `${String(i + 1).padStart(4, ' ')}| ${l}`).join('\n'); + return `# ์ž„๋ฌด +๋‹น์‹ ์€ ์‹œ๋‹ˆ์–ด ์ฝ”๋“œ ๋ฆฌ๋ทฐ์–ด๋‹ค. ์•„๋ž˜ **๋‹จ์ผ ํŒŒ์ผ**(${idx}/${total}๋ฒˆ์งธ)์„ ๋ฆฌ๋ทฐํ•ด ๋ฐœ๊ฒฌ์‚ฌํ•ญ๋งŒ ์ถ”์ถœํ•˜๋ผ. +์ตœ์ข… ๋ณด๊ณ ์„œ๋Š” ๋‚˜์ค‘์— ๋ชจ๋“  ํŒŒ์ผ ๋…ธํŠธ๋ฅผ ํ•ฉ์ณ ์ž‘์„ฑํ•˜๋ฏ€๋กœ, ์—ฌ๊ธฐ์„œ๋Š” ์ด ํŒŒ์ผ์— ๋Œ€ํ•œ ์‚ฌ์‹ค ๊ธฐ๋ฐ˜ ๋ฐœ๊ฒฌ์‚ฌํ•ญ์„ **๋ˆ„๋ฝ ์—†์ด** ๋ฝ‘๋Š” ๊ฒƒ์ด ์ž„๋ฌด๋‹ค. +${focusBlock} +# ์ ๊ฒ€ ํ•ญ๋ชฉ +- **๋ฒ„๊ทธ/์˜ค๋ฅ˜**: ๋…ผ๋ฆฌ ์˜ค๋ฅ˜, ๊ฒฝ๊ณ„ ์กฐ๊ฑด, null/undefined, ์˜ˆ์™ธ ๋ฏธ์ฒ˜๋ฆฌ, ๊ฒฝ์Ÿ ์กฐ๊ฑด, ์ž์› ๋ˆ„์ˆ˜ +- **๋ณด์•ˆ**: ์ž…๋ ฅ ๊ฒ€์ฆ ๋ˆ„๋ฝ, ์ธ์ ์…˜, ๋น„๋ฐ€์ •๋ณด ํ•˜๋“œ์ฝ”๋”ฉ, ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์—ญ์ง๋ ฌํ™”/๊ถŒํ•œ +- **์„ฑ๋Šฅ**: ๋ถˆํ•„์š”ํ•œ ๋ฐ˜๋ณตยทํ• ๋‹น, N+1, ๋™๊ธฐ ๋ธ”๋กœํ‚น, ๋น„ํšจ์œจ ์ž๋ฃŒ๊ตฌ์กฐ +- **์„ค๊ณ„/๊ตฌ์กฐ**: ์ฑ…์ž„ ๋ถ„๋ฆฌ, ๊ฒฐํ•ฉ๋„, ์ค‘๋ณต, ์ถ”์ƒํ™” ๋ˆ„๋ฝ/๊ณผ์ž‰ +- **๊ฐ€๋…์„ฑ/์œ ์ง€๋ณด์ˆ˜**: ๋„ค์ด๋ฐ, ์ฃฝ์€ ์ฝ”๋“œ, ๋งค์ง ๋„˜๋ฒ„, ์ฃผ์„/ํƒ€์ž… ๋ถ€์žฌ + +# ๊ทœ์น™ (ํ• ๋ฃจ์‹œ๋„ค์ด์…˜ ๋ฐฉ์ง€ โ€” ๋ฐ˜๋“œ์‹œ ์ค€์ˆ˜) +- **์ด ํŒŒ์ผ์— ์‹ค์ œ๋กœ ์žˆ๋Š” ์ฝ”๋“œ๋งŒ** ๊ทผ๊ฑฐ๋กœ ์‚ผ๋Š”๋‹ค. ์—†๋Š” ํ•จ์ˆ˜ยทํ˜ธ์ถœยท์ทจ์•ฝ์ ์„ ์ง€์–ด๋‚ด์ง€ ๋ง ๊ฒƒ. +- ๊ฐ ๋ฐœ๊ฒฌ์‚ฌํ•ญ์—๋Š” **์œ„์น˜(๋ผ์ธ ๋ฒˆํ˜ธ)** ์™€ **๊ทผ๊ฑฐ๊ฐ€ ๋˜๋Š” ์ฝ”๋“œ ์ผ๋ถ€(์งง๊ฒŒ ์ธ์šฉ)** ๋ฅผ ๋ถ™์ธ๋‹ค. +- ํ™•์‹คํ•˜์ง€ ์•Š์œผ๋ฉด ๋‹จ์ •ํ•˜์ง€ ๋ง๊ณ  "ํ™•์ธ ํ•„์š”"๋กœ ํ‘œ์‹œํ•œ๋‹ค. +- ์ด ํŒŒ์ผ์ด ์ „๋ฐ˜์ ์œผ๋กœ ์–‘ํ˜ธํ•˜๋ฉด ๋ฐœ๊ฒฌ์‚ฌํ•ญ์„ ์–ต์ง€๋กœ ๋งŒ๋“ค์ง€ ๋ง๊ณ  "ํŠน์ด์‚ฌํ•ญ ์—†์Œ"์ด๋ผ๊ณ  ์ ๋Š”๋‹ค. +- ๋‹ค๋ฅธ ํŒŒ์ผยท์™ธ๋ถ€ ๋งฅ๋ฝ์„ ๊ฐ€์ •ํ•˜์ง€ ๋ง ๊ฒƒ(์ด ํŒŒ์ผ๋งŒ ๋ณธ๋‹ค). + +[ํŒŒ์ผ ๊ฒฝ๋กœ] ${relPath} + +[ํŒŒ์ผ ๋‚ด์šฉ โ€” "๋ผ์ธ๋ฒˆํ˜ธ| ์ฝ”๋“œ" ํ˜•์‹] +\`\`\` +${numbered} +\`\`\` + +# ์ถœ๋ ฅ ํ˜•์‹ (์ด ํŒŒ์ผ์— ํ•ด๋‹น ํ•ญ๋ชฉ์ด ์—†์œผ๋ฉด "์—†์Œ") +## ํŒŒ์ผ: ${relPath} +### ๋ฐœ๊ฒฌ์‚ฌํ•ญ +- [์‹ฌ๊ฐ๋„: ๋†’์Œ|์ค‘๊ฐ„|๋‚ฎ์Œ] [๋ถ„๋ฅ˜: ๋ฒ„๊ทธ|๋ณด์•ˆ|์„ฑ๋Šฅ|์„ค๊ณ„|๊ฐ€๋…์„ฑ] (L<๋ผ์ธ>) ์„ค๋ช… โ€” ๊ทผ๊ฑฐ: "์ฝ”๋“œ ์ผ๋ถ€" + ยท ๊ฐœ์„  ์ œ์•ˆ: ๊ตฌ์ฒด์ ์œผ๋กœ ๋ฌด์—‡์„ ์–ด๋–ป๊ฒŒ ๋ฐ”๊ฟ€์ง€ +(ํŠน์ด์‚ฌํ•ญ์ด ์—†์œผ๋ฉด "- ํŠน์ด์‚ฌํ•ญ ์—†์Œ") +### ํ•œ์ค„ ์š”์•ฝ +์ด ํŒŒ์ผ์˜ ์—ญํ• ๊ณผ ์ „๋ฐ˜ ์ƒํƒœ๋ฅผ ํ•œ ๋ฌธ์žฅ์œผ๋กœ.`; +} + +/** [Reduce] ํŒŒ์ผ๋ณ„ ๋…ธํŠธ๋ฅผ ํ†ตํ•ฉํ•ด ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋งค๊ฒจ์ง„ ์ตœ์ข… ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ณด๊ณ ์„œ ์ž‘์„ฑ. */ +export function buildReviewReducePrompt(notes: string, projectLabel: string, fileCount: number, focus: string): string { + const focusBlock = focus.trim() ? `\n# ๋ฆฌ๋ทฐ ์ดˆ์  (์‚ฌ์šฉ์ž ์ง€์ •)\n${focus.trim()}\n` : ''; + return `# ์ž„๋ฌด +์•„๋ž˜๋Š” \`${projectLabel}\` ์˜ ํŒŒ์ผ ${fileCount}๊ฐœ๋ฅผ ํŒŒ์ผ๋ณ„๋กœ ๋ฆฌ๋ทฐํ•œ ๋…ธํŠธ๋‹ค. ์ด ๋…ธํŠธ๋งŒ ๊ทผ๊ฑฐ๋กœ **ํ†ตํ•ฉ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ณด๊ณ ์„œ**๋ฅผ ์ž‘์„ฑํ•˜๋ผ. +${focusBlock} +# ๊ทœ์น™ +- **๋…ธํŠธ์— ์žˆ๋Š” ๋ฐœ๊ฒฌ์‚ฌํ•ญ๋งŒ** ์‚ฌ์šฉํ•œ๋‹ค. ๋…ธํŠธ์— ์—†๋Š” ๋ฌธ์ œ๋ฅผ ์ƒˆ๋กœ ์ง€์–ด๋‚ด์ง€ ๋ง ๊ฒƒ. +- ์—ฌ๋Ÿฌ ํŒŒ์ผ์— ๊ณตํ†ต์œผ๋กœ ๋‚˜ํƒ€๋‚˜๋Š” ๋ฌธ์ œ๋Š” ๋ฌถ์–ด์„œ "๊ณตํ†ต/๊ตฌ์กฐ์  ์ด์Šˆ"๋กœ ์ •๋ฆฌํ•œ๋‹ค. +- ์‹ฌ๊ฐ๋„์™€ ์˜ํ–ฅ ๋ฒ”์œ„๋ฅผ ๊ธฐ์ค€์œผ๋กœ **์šฐ์„ ์ˆœ์œ„**๋ฅผ ๋งค๊ธด๋‹ค(๋†’์€ ์‹ฌ๊ฐ๋„ยท๋„“์€ ์˜ํ–ฅ ์šฐ์„ ). +- ๊ฐ ์ด์Šˆ์—๋Š” ํŒŒ์ผยท๋ผ์ธ ์œ„์น˜๋ฅผ ์œ ์ง€ํ•œ๋‹ค(๊ทผ๊ฑฐ ์ถ”์  ๊ฐ€๋Šฅํ•˜๊ฒŒ). +- ๋น„ํŒ๋งŒ ํ•˜์ง€ ๋ง๊ณ  ์ž˜ ์„ค๊ณ„๋œ ์ ๋„ ์งš๋Š”๋‹ค. + +[ํŒŒ์ผ๋ณ„ ๋ฆฌ๋ทฐ ๋…ธํŠธ] +${notes} + +# ์ถœ๋ ฅ ํ˜•์‹ (์ •ํ™•ํžˆ ์ด ๊ตฌ์กฐ, ํ•œ๊ตญ์–ด) + +# ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ณด๊ณ ์„œ โ€” ${projectLabel} + +## 1. ์ดํ‰ +์ „๋ฐ˜์  ํ’ˆ์งˆยท๊ตฌ์กฐยท์ฃผ์š” ์œ„ํ—˜์„ 3~5์ค„๋กœ ์š”์•ฝ. ๋น„์ฐธ์„์ž๋„ ์ƒํƒœ๋ฅผ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ฒŒ. + +## 2. ์šฐ์„  ๊ฐœ์„  ์‚ฌํ•ญ (Top ์šฐ์„ ์ˆœ์œ„) +๊ฐ€์žฅ ์‹œ๊ธ‰ํ•œ ๊ฒƒ๋ถ€ํ„ฐ ๋ฒˆํ˜ธ ๋งค๊ฒจ ์ •๋ฆฌ. ๊ฐ ํ•ญ๋ชฉ: [์‹ฌ๊ฐ๋„] ๋ฌธ์ œ โ€” ์œ„์น˜(ํŒŒ์ผ:๋ผ์ธ) โ€” ๊ฐœ์„  ๋ฐฉํ–ฅ. + +## 3. ๋ถ„๋ฅ˜๋ณ„ ๋ฐœ๊ฒฌ์‚ฌํ•ญ +### ๐Ÿž ๋ฒ„๊ทธ/์˜ค๋ฅ˜ +### ๐Ÿ”’ ๋ณด์•ˆ +### โšก ์„ฑ๋Šฅ +### ๐Ÿงฉ ์„ค๊ณ„/๊ตฌ์กฐ (๊ณตํ†ต ์ด์Šˆ ํฌํ•จ) +### ๐Ÿ“– ๊ฐ€๋…์„ฑ/์œ ์ง€๋ณด์ˆ˜ +(๊ฐ ํ•ญ๋ชฉ์— ํŒŒ์ผ:๋ผ์ธ + ํ•œ ์ค„ ๊ฐœ์„  ์ œ์•ˆ. ํ•ด๋‹น ๋ถ„๋ฅ˜์— ๋ฐœ๊ฒฌ์‚ฌํ•ญ์ด ์—†์œผ๋ฉด "๋ฐœ๊ฒฌ์‚ฌํ•ญ ์—†์Œ".) + +## 4. ์ž˜๋œ ์  +์œ ์ง€ยท๊ฐ•ํ™”ํ•  ๊ฐ€์น˜๊ฐ€ ์žˆ๋Š” ์„ค๊ณ„ยทํŒจํ„ด. + +## 5. ๊ถŒ์žฅ ๋‹ค์Œ ๋‹จ๊ณ„ +์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์กฐ์น˜๋ฅผ ์šฐ์„ ์ˆœ์œ„ ์ˆœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ.`; +} diff --git a/src/features/datacollect/reviewFiles.ts b/src/features/datacollect/reviewFiles.ts new file mode 100644 index 0000000..b8c77cd --- /dev/null +++ b/src/features/datacollect/reviewFiles.ts @@ -0,0 +1,102 @@ +/** + * /review ๋Œ€์ƒ ์†Œ์Šค ํŒŒ์ผ ์ˆ˜์ง‘๊ธฐ. + * + * ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์žฌ๊ท€ ์ˆœํšŒํ•˜๋ฉฐ "๋ฆฌ๋ทฐํ•  ๊ฐ€์น˜๊ฐ€ ์žˆ๋Š” ์†Œ์Šค ํŒŒ์ผ"๋งŒ ๊ณจ๋ผ๋‚ธ๋‹ค. + * ์˜์กด์„ฑยท๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผยท๋ฐ”์ด๋„ˆ๋ฆฌยท์ƒ์„ฑ๋ฌผ์€ ์ œ์™ธํ•œ๋‹ค. ์ˆœ์ˆ˜ ํŒ์ • ๋กœ์ง(shouldReviewFile)์€ + * fs ์™€ ๋ถ„๋ฆฌํ•ด ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋‘”๋‹ค. + */ +import { promises as fsp } from 'fs'; +import * as path from 'path'; + +/** ๋ฆฌ๋ทฐ ๋Œ€์ƒ ์†Œ์Šค ํ™•์žฅ์ž (์†Œ๋ฌธ์ž, ์  ํฌํ•จ). */ +export const REVIEW_EXTENSIONS = new Set([ + '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', + '.py', '.java', '.go', '.rs', '.rb', '.php', + '.c', '.cc', '.cpp', '.h', '.hpp', '.cs', '.kt', '.swift', '.scala', + '.vue', '.svelte', '.sql', '.sh', +]); + +/** ์ˆœํšŒ์—์„œ ํ†ต์งธ๋กœ ๊ฑด๋„ˆ๋›ธ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ด๋ฆ„. */ +export const SKIP_DIRS = new Set([ + 'node_modules', '.git', 'out', 'dist', 'build', '.next', 'coverage', + 'vendor', '.venv', 'venv', '__pycache__', '.astra', '.secondbrain', + '.vscode', '.idea', 'bin', 'obj', 'target', 'media', 'assets', +]); + +/** + * ์ƒ๋Œ€ ๊ฒฝ๋กœ(์Šฌ๋ž˜์‹œ ์ •๊ทœํ™”)๊ฐ€ ๋ฆฌ๋ทฐ ๋Œ€์ƒ์ธ์ง€ ํŒ์ •. ๋””๋ ‰ํ„ฐ๋ฆฌ ์Šคํ‚ตยทํ™•์žฅ์ž ํ•„ํ„ฐยท + * ์ƒ์„ฑ๋ฌผ(.min.js / .d.ts / *.map / lock) ์ œ์™ธ๋ฅผ ํ•œ๊ณณ์—์„œ ๊ฒฐ์ •ํ•œ๋‹ค. + */ +export function shouldReviewFile(relPathPosix: string): boolean { + const parts = relPathPosix.split('/'); + const base = parts[parts.length - 1].toLowerCase(); + // ์Šคํ‚ต ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ ๊ฒฝ๋กœ ์–ด๋”˜๊ฐ€์— ์žˆ์œผ๋ฉด ์ œ์™ธ + if (parts.slice(0, -1).some((seg) => SKIP_DIRS.has(seg))) return false; + // ์ƒ์„ฑ๋ฌผยท๋…ธ์ด์ฆˆ ์ œ์™ธ + if (base.endsWith('.min.js') || base.endsWith('.d.ts') || base.endsWith('.map')) return false; + if (base === 'package-lock.json' || base === 'yarn.lock' || base === 'pnpm-lock.yaml') return false; + const ext = base.includes('.') ? base.slice(base.lastIndexOf('.')) : ''; + return REVIEW_EXTENSIONS.has(ext); +} + +export interface CollectedFile { + /** ์ ˆ๋Œ€ ๊ฒฝ๋กœ. */ + absPath: string; + /** ๋ฃจํŠธ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ(์Šฌ๋ž˜์‹œ). */ + relPath: string; + /** ๋ฐ”์ดํŠธ ํฌ๊ธฐ. */ + size: number; +} + +export interface CollectOptions { + /** ์ˆ˜์ง‘ ํŒŒ์ผ ์ˆ˜ ์ƒํ•œ. ์ดˆ๊ณผ๋ถ„์€ ์ž˜๋ฆฌ๋ฉฐ truncated=true. */ + maxFiles: number; + /** ํŒŒ์ผ 1๊ฐœ ์ตœ๋Œ€ ๋ฐ”์ดํŠธ(์ด๋ณด๋‹ค ํฌ๋ฉด ์ œ์™ธ โ€” ๊ฑฐ๋Œ€ ์ƒ์„ฑ๋ฌผ ๋ฐฉ์–ด). */ + maxFileBytes: number; +} + +export interface CollectResult { + files: CollectedFile[]; + /** maxFiles ์ดˆ๊ณผ๋กœ ์ž˜๋ ธ๋Š”๊ฐ€. */ + truncated: boolean; + /** ์ˆœํšŒ ์ค‘ ๋ณธ ๋ฆฌ๋ทฐ ๋Œ€์ƒ ํ›„๋ณด ์ด์ˆ˜(์ƒํ•œ ์ ์šฉ ์ „). */ + totalCandidates: number; +} + +/** + * root ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์žฌ๊ท€ ์ˆœํšŒํ•ด ๋ฆฌ๋ทฐ ๋Œ€์ƒ ํŒŒ์ผ์„ ์ˆ˜์ง‘ํ•œ๋‹ค. + * ๊ฒฐ์ •์  ์ˆœ์„œ(๊ฒฝ๋กœ ์ •๋ ฌ)๋กœ ๋ฐ˜ํ™˜ํ•ด ์žฌ์‹คํ–‰ ์‹œ ๋™์ผ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ํ•œ๋‹ค. + */ +export async function collectSourceFiles(root: string, opts: CollectOptions): Promise { + const found: CollectedFile[] = []; + const walk = async (dir: string): Promise => { + let entries: import('fs').Dirent[]; + try { + entries = await fsp.readdir(dir, { withFileTypes: true }); + } catch { + return; // ๊ถŒํ•œ ๋“ฑ์œผ๋กœ ๋ชป ์ฝ๋Š” ๋””๋ ‰ํ„ฐ๋ฆฌ๋Š” ๊ฑด๋„ˆ๋œ€ + } + for (const ent of entries) { + const abs = path.join(dir, ent.name); + if (ent.isDirectory()) { + if (SKIP_DIRS.has(ent.name)) continue; + await walk(abs); + } else if (ent.isFile()) { + const rel = path.relative(root, abs).split(path.sep).join('/'); + if (!shouldReviewFile(rel)) continue; + let size = 0; + try { size = (await fsp.stat(abs)).size; } catch { continue; } + if (size > opts.maxFileBytes) continue; + found.push({ absPath: abs, relPath: rel, size }); + } + } + }; + await walk(root); + found.sort((a, b) => a.relPath.localeCompare(b.relPath)); + const truncated = found.length > opts.maxFiles; + return { + files: truncated ? found.slice(0, opts.maxFiles) : found, + truncated, + totalCandidates: found.length, + }; +} diff --git a/tests/reviewFiles.test.ts b/tests/reviewFiles.test.ts new file mode 100644 index 0000000..188e5a8 --- /dev/null +++ b/tests/reviewFiles.test.ts @@ -0,0 +1,39 @@ +/** + * /review ์†Œ์Šค ํŒŒ์ผ ์ˆ˜์ง‘ ํŒ์ •(shouldReviewFile) ํ…Œ์ŠคํŠธ โ€” ๋””๋ ‰ํ„ฐ๋ฆฌ ์Šคํ‚ตยทํ™•์žฅ์ž + * ํ•„ํ„ฐยท์ƒ์„ฑ๋ฌผ ์ œ์™ธ์˜ ๊ฒฐ์ •์  ๋™์ž‘์„ ๊ณ ์ •ํ•œ๋‹ค. + */ +import { shouldReviewFile, REVIEW_EXTENSIONS, SKIP_DIRS } from '../src/features/datacollect/reviewFiles'; + +describe('shouldReviewFile', () => { + it('์†Œ์Šค ํ™•์žฅ์ž๋Š” ํ†ต๊ณผ', () => { + expect(shouldReviewFile('src/agent.ts')).toBe(true); + expect(shouldReviewFile('app/main.py')).toBe(true); + expect(shouldReviewFile('pkg/server.go')).toBe(true); + expect(shouldReviewFile('ui/App.tsx')).toBe(true); + }); + + it('๋น„์†Œ์Šค/๋ฌธ์„œ/๋ฐ”์ด๋„ˆ๋ฆฌ๋Š” ์ œ์™ธ', () => { + expect(shouldReviewFile('README.md')).toBe(false); + expect(shouldReviewFile('assets/icon.png')).toBe(false); + expect(shouldReviewFile('data.json')).toBe(false); + }); + + it('์Šคํ‚ต ๋””๋ ‰ํ„ฐ๋ฆฌ ํ•˜์œ„๋Š” ์ œ์™ธ', () => { + expect(shouldReviewFile('node_modules/foo/index.js')).toBe(false); + expect(shouldReviewFile('out/extension.js')).toBe(false); + expect(shouldReviewFile('.git/hooks/pre-commit.sample')).toBe(false); + expect(shouldReviewFile('src/.astra/cache/x.ts')).toBe(false); // ์ค‘๊ฐ„ ๊ฒฝ๋กœ์— ์Šคํ‚ต ๋””๋ ‰ํ„ฐ๋ฆฌ + }); + + it('์ƒ์„ฑ๋ฌผยท๋…ธ์ด์ฆˆ๋Š” ์ œ์™ธ', () => { + expect(shouldReviewFile('dist/bundle.min.js')).toBe(false); // dist + .min.js ๋‘˜ ๋‹ค + expect(shouldReviewFile('src/types.d.ts')).toBe(false); + expect(shouldReviewFile('src/app.js.map')).toBe(false); + expect(shouldReviewFile('package-lock.json')).toBe(false); + }); + + it('๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒ์ˆ˜ ์ •ํ•ฉ์„ฑ', () => { + expect(REVIEW_EXTENSIONS.has('.ts')).toBe(true); + expect(SKIP_DIRS.has('node_modules')).toBe(true); + }); +});