idea-manager 0.4.0 → 0.5.0

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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/app/projects/[id]/page.tsx +0 -4
  3. package/src/components/brainstorm/Editor.tsx +1 -84
  4. package/src/lib/ai/client.ts +0 -123
  5. package/src/lib/db/schema.ts +0 -70
  6. package/src/types/index.ts +0 -90
  7. package/src/app/api/projects/[id]/cleanup/route.ts +0 -32
  8. package/src/app/api/projects/[id]/conversations/route.ts +0 -50
  9. package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +0 -51
  10. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +0 -36
  11. package/src/app/api/projects/[id]/items/[itemId]/route.ts +0 -95
  12. package/src/app/api/projects/[id]/items/route.ts +0 -67
  13. package/src/app/api/projects/[id]/memos/route.ts +0 -18
  14. package/src/app/api/projects/[id]/scan/route.ts +0 -73
  15. package/src/app/api/projects/[id]/scan/stream/route.ts +0 -112
  16. package/src/app/api/projects/[id]/structure/route.ts +0 -59
  17. package/src/app/api/projects/[id]/structure/stream/route.ts +0 -157
  18. package/src/components/ScanPanel.tsx +0 -743
  19. package/src/components/brainstorm/MemoPin.tsx +0 -117
  20. package/src/components/tree/CardView.tsx +0 -206
  21. package/src/components/tree/ItemDetail.tsx +0 -196
  22. package/src/components/tree/LockToggle.tsx +0 -23
  23. package/src/components/tree/RefinePopover.tsx +0 -157
  24. package/src/components/tree/StatusBadge.tsx +0 -32
  25. package/src/components/tree/TreeNode.tsx +0 -227
  26. package/src/components/tree/TreeView.tsx +0 -304
  27. package/src/lib/ai/chat-responder.ts +0 -71
  28. package/src/lib/ai/cleanup.ts +0 -87
  29. package/src/lib/ai/prompter.ts +0 -78
  30. package/src/lib/ai/refiner.ts +0 -128
  31. package/src/lib/ai/structurer.ts +0 -403
  32. package/src/lib/db/queries/context.ts +0 -76
  33. package/src/lib/db/queries/conversations.ts +0 -46
  34. package/src/lib/db/queries/items.ts +0 -268
  35. package/src/lib/db/queries/memos.ts +0 -66
  36. package/src/lib/db/queries/prompts.ts +0 -68
  37. package/src/lib/scanner.ts +0 -573
  38. package/src/lib/task-store.ts +0 -97
@@ -1,87 +0,0 @@
1
- import { runClaude, extractJson, type IStructuredItem } from './client';
2
- import { replaceItems, getItemTree } from '../db/queries/items';
3
- import type { IItemTree } from '@/types';
4
-
5
- function serializeItems(items: IItemTree[], depth = 0): string {
6
- const lines: string[] = [];
7
- for (const item of items) {
8
- const indent = ' '.repeat(depth);
9
- const status = item.status || 'pending';
10
- lines.push(`${indent}- [${item.item_type}/${item.priority}/${status}] ${item.title}: ${item.description || ''}`);
11
- if (item.children && item.children.length > 0) {
12
- lines.push(serializeItems(item.children, depth + 1));
13
- }
14
- }
15
- return lines.join('\n');
16
- }
17
-
18
- function countItems(items: IItemTree[]): number {
19
- let count = 0;
20
- for (const item of items) {
21
- count++;
22
- if (item.children) count += countItems(item.children);
23
- }
24
- return count;
25
- }
26
-
27
- function mapToDbFormat(items: IStructuredItem[]): Parameters<typeof replaceItems>[2] {
28
- return items.map((item) => ({
29
- parent_id: null,
30
- title: item.title,
31
- description: item.description,
32
- item_type: item.item_type,
33
- priority: item.priority,
34
- status: item.status,
35
- children: item.children ? mapToDbFormat(item.children) : undefined,
36
- }));
37
- }
38
-
39
- export async function cleanupItems(
40
- projectId: string,
41
- brainstormId: string,
42
- items: IItemTree[],
43
- brainstormContent: string,
44
- ): Promise<{ items: IItemTree[]; changed: boolean }> {
45
- const serialized = serializeItems(items);
46
- const beforeCount = countItems(items);
47
-
48
- const prompt = `You are a JSON-only deduplication machine. You NEVER respond with text, explanations, or conversation.
49
- You ALWAYS output ONLY a raw JSON array, nothing else.
50
-
51
- Your job: clean up the structured item tree below by removing duplicates and merging similar items.
52
-
53
- Schema per item:
54
- { "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "status": "pending"|"in_progress"|"done", "children": [same schema] }
55
-
56
- Rules:
57
- - Output MUST start with [ and end with ]
58
- - No markdown fences, no explanation, no text before or after the JSON
59
- - MERGE items that describe the same concept (combine their descriptions, keep the more specific title)
60
- - REMOVE exact or near-exact duplicates (keep the one with more detail)
61
- - PRESERVE the status of items — if one copy is "done" and another is "pending", keep "done"
62
- - PRESERVE the hierarchy — keep parent-child relationships logical
63
- - Keep titles concise (under 50 chars)
64
- - Do NOT add new items that weren't in the original
65
- - Do NOT remove items just because they seem unimportant — only remove TRUE duplicates
66
- - If the brainstorming context is provided, use it to understand which items are actually the same concept
67
-
68
- ${brainstormContent ? `사용자의 브레인스토밍 메모:\n${brainstormContent}\n\n` : ''}현재 구조화된 항목 (중복 제거 및 병합하세요):
69
- ${serialized}`;
70
-
71
- const resultText = await runClaude(prompt);
72
- const json = extractJson(resultText, 'array');
73
- const cleaned = JSON.parse(json) as IStructuredItem[];
74
-
75
- const afterCount = cleaned.reduce((sum, item) => sum + 1 + countStructuredChildren(item), 0);
76
- const changed = afterCount !== beforeCount;
77
-
78
- const dbItems = mapToDbFormat(cleaned);
79
- const tree = replaceItems(projectId, brainstormId, dbItems);
80
-
81
- return { items: tree, changed };
82
- }
83
-
84
- function countStructuredChildren(item: IStructuredItem): number {
85
- if (!item.children) return 0;
86
- return item.children.reduce((sum, child) => sum + 1 + countStructuredChildren(child), 0);
87
- }
@@ -1,78 +0,0 @@
1
- import { runClaude } from './client';
2
- import { getPrompt, createPrompt } from '../db/queries/prompts';
3
- import { getBrainstorm } from '../db/queries/brainstorms';
4
- import { getRecentConversations } from '../db/queries/conversations';
5
- import { getProjectContextSummary } from '../db/queries/context';
6
- import type { IPrompt, IItem } from '@/types';
7
-
8
- export async function generatePrompt(
9
- item: IItem,
10
- projectContext?: { brainstormContent?: string; conversationHistory?: string; projectDocs?: string },
11
- ): Promise<IPrompt> {
12
- const existing = getPrompt(item.id);
13
- if (existing?.prompt_type === 'manual') {
14
- return existing;
15
- }
16
-
17
- const systemPrompt = `You are a prompt engineering expert. Generate a clear, actionable prompt for a coding assistant (like Cursor or Claude Code) to implement the given task.
18
-
19
- Rules:
20
- - Output ONLY the prompt text, nothing else
21
- - Write in Korean
22
- - Be specific and actionable
23
- - Include conditions, constraints, and requirements
24
- - Include relevant context from the brainstorming, conversation, and project documentation
25
- - The prompt should be ready to paste into a coding tool
26
- - Keep it concise but complete (under 500 chars)
27
- - Do NOT include markdown fences or extra formatting`;
28
-
29
- const CONTEXT_LIMIT = 30_000; // 30KB max for prompt generation context
30
- let context = '';
31
- if (projectContext?.brainstormContent) {
32
- context += `\n\n브레인스토밍 원문:\n${projectContext.brainstormContent.slice(0, 3000)}`;
33
- }
34
- if (projectContext?.conversationHistory) {
35
- context += `\n\nAI 대화 이력:\n${projectContext.conversationHistory.slice(0, 5000)}`;
36
- }
37
- if (projectContext?.projectDocs) {
38
- const remaining = CONTEXT_LIMIT - context.length;
39
- if (remaining > 1000) {
40
- context += `\n\n프로젝트 문서:\n${projectContext.projectDocs.slice(0, remaining)}`;
41
- }
42
- }
43
-
44
- const prompt = `${systemPrompt}\n\n다음 항목에 대한 실행 프롬프트를 생성하세요:
45
-
46
- 제목: ${item.title}
47
- 설명: ${item.description}
48
- 유형: ${item.item_type}
49
- 우선순위: ${item.priority}${context}`;
50
-
51
- const resultText = await runClaude(prompt);
52
-
53
- return createPrompt({
54
- project_id: item.project_id,
55
- item_id: item.id,
56
- content: resultText.trim(),
57
- prompt_type: 'auto',
58
- });
59
- }
60
-
61
- export async function generatePromptForItem(
62
- item: IItem,
63
- ): Promise<IPrompt> {
64
- const brainstorm = getBrainstorm(item.project_id);
65
- const conversations = getRecentConversations(item.project_id, 20);
66
-
67
- const conversationHistory = conversations.length > 0
68
- ? conversations.map(c => `${c.role === 'user' ? '사용자' : 'AI'}: ${c.content}`).join('\n')
69
- : undefined;
70
-
71
- const projectDocs = getProjectContextSummary(item.project_id) || undefined;
72
-
73
- return generatePrompt(item, {
74
- brainstormContent: brainstorm?.content,
75
- conversationHistory,
76
- projectDocs,
77
- });
78
- }
@@ -1,128 +0,0 @@
1
- import { runClaude } from './client';
2
- import { updateItem, addChildItems, getItemTree, deleteItem } from '../db/queries/items';
3
- import { getBrainstorm } from '../db/queries/brainstorms';
4
- import { getDb } from '../db/index';
5
- import type { IItem, IItemTree } from '@/types';
6
-
7
- interface RefineChild {
8
- title: string;
9
- description: string;
10
- item_type: 'feature' | 'task' | 'bug' | 'idea' | 'note';
11
- priority: 'high' | 'medium' | 'low';
12
- children?: RefineChild[];
13
- }
14
-
15
- interface RefineResult {
16
- title: string;
17
- description: string;
18
- children?: RefineChild[];
19
- remove_children?: boolean;
20
- }
21
-
22
- export async function refineItem(
23
- item: IItem,
24
- userMessage: string,
25
- ): Promise<{ title: string; description: string; tree: IItemTree[] }> {
26
- const brainstorm = getBrainstorm(item.project_id);
27
-
28
- // Get existing children info
29
- const db = getDb();
30
- const existingChildren = db.prepare(
31
- 'SELECT id, title, description, item_type, priority FROM items WHERE parent_id = ?'
32
- ).all(item.id) as Pick<IItem, 'id' | 'title' | 'description' | 'item_type' | 'priority'>[];
33
-
34
- const childrenInfo = existingChildren.length > 0
35
- ? `\n\n현재 하위 항목 (${existingChildren.length}개):\n${existingChildren.map((c, i) => `${i + 1}. [${c.item_type}] ${c.title}: ${c.description}`).join('\n')}`
36
- : '\n\n현재 하위 항목: 없음';
37
-
38
- const systemPrompt = `You are a task refinement assistant. The user wants to modify a task item and potentially its sub-items.
39
- You ALWAYS output ONLY a raw JSON object, nothing else.
40
-
41
- Output schema:
42
- {
43
- "title": string,
44
- "description": string,
45
- "children": [{ "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "children": [...] }],
46
- "remove_children": boolean
47
- }
48
-
49
- Rules:
50
- - Output MUST be a JSON object
51
- - No markdown fences, no explanation, no text before or after the JSON
52
- - Keep titles concise (under 50 chars)
53
- - Write in Korean
54
- - description should be detailed and actionable
55
- - "children" is OPTIONAL: include it only if the user asks to add/restructure sub-items
56
- - "remove_children" is OPTIONAL: set true only if the user explicitly asks to remove existing children before adding new ones
57
- - If the user just wants to modify the title/description, omit "children"
58
- - If the user asks to break down, detail, or expand the item, provide "children" array with sub-items`;
59
-
60
- const context = brainstorm?.content ? `\n\n브레인스토밍 원문:\n${brainstorm.content}` : '';
61
-
62
- const prompt = `${systemPrompt}\n\n현재 항목:
63
- 제목: ${item.title}
64
- 설명: ${item.description}
65
- 유형: ${item.item_type}
66
- 우선순위: ${item.priority}${childrenInfo}${context}
67
-
68
- 사용자 요청: ${userMessage}`;
69
-
70
- const resultText = await runClaude(prompt);
71
-
72
- const cleaned = resultText.replace(/```(?:json)?\s*/g, '').replace(/```\s*/g, '').trim();
73
- const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
74
- if (!jsonMatch) {
75
- throw new Error('AI did not return valid JSON');
76
- }
77
-
78
- const parsed = JSON.parse(jsonMatch[0]) as RefineResult;
79
-
80
- // Update the item itself
81
- updateItem(item.id, {
82
- title: parsed.title,
83
- description: parsed.description,
84
- });
85
-
86
- // Handle children changes
87
- if (parsed.children && parsed.children.length > 0) {
88
- // If remove_children is set, delete existing children first
89
- if (parsed.remove_children && existingChildren.length > 0) {
90
- for (const child of existingChildren) {
91
- deleteItem(child.id);
92
- }
93
- }
94
-
95
- // Add new children
96
- addChildItems(item.project_id, item.id, parsed.children.map(c => ({
97
- parent_id: item.id,
98
- title: c.title,
99
- description: c.description,
100
- item_type: c.item_type,
101
- priority: c.priority,
102
- children: c.children?.map(mapChild) ?? undefined,
103
- })));
104
- }
105
-
106
- // Return updated tree
107
- const tree = getItemTree(item.project_id);
108
-
109
- return { title: parsed.title, description: parsed.description, tree };
110
- }
111
-
112
- function mapChild(c: RefineChild): {
113
- parent_id: null;
114
- title: string;
115
- description: string;
116
- item_type: RefineChild['item_type'];
117
- priority: RefineChild['priority'];
118
- children?: ReturnType<typeof mapChild>[];
119
- } {
120
- return {
121
- parent_id: null,
122
- title: c.title,
123
- description: c.description,
124
- item_type: c.item_type,
125
- priority: c.priority,
126
- children: c.children?.map(mapChild),
127
- };
128
- }
@@ -1,403 +0,0 @@
1
- import { runStructure, runStructureWithQuestions, runAnalysis, runClaude, extractJson, type IStructuredItem, type OnTextChunk, type OnRawEvent } from './client';
2
- import { replaceItems, appendItems, getItemTree } from '../db/queries/items';
3
- import { getRecentConversations, addMessage } from '../db/queries/conversations';
4
- import {
5
- getProjectContextSummary,
6
- getProjectContextsBySubProject,
7
- buildSubProjectSummary,
8
- } from '../db/queries/context';
9
- import { resolveMemos, createMemosFromQuestions } from '../db/queries/memos';
10
- import type { IItemTree, IMemo, IConversation } from '@/types';
11
-
12
- const AI_CONTEXT_LIMIT = 150_000; // 150KB - threshold for chunking
13
- const PHASE3_CONTEXT_LIMIT = 300_000; // 300KB - Phase 3 final structuring (hub doc can be large)
14
- const AI_CHUNK_LIMIT = 80_000; // 80KB - max context per AI call
15
-
16
- export async function structureBrainstorm(
17
- projectId: string,
18
- brainstormId: string,
19
- content: string,
20
- ): Promise<IItemTree[]> {
21
- if (!content.trim()) {
22
- return [];
23
- }
24
-
25
- const projectContext = getProjectContextSummary(projectId) || undefined;
26
- const structured = await runStructure(content, projectContext);
27
-
28
- const dbItems = mapToDbFormat(structured);
29
-
30
- return replaceItems(projectId, brainstormId, dbItems);
31
- }
32
-
33
- export async function structureWithChat(
34
- projectId: string,
35
- brainstormId: string,
36
- content: string,
37
- ): Promise<{ items: IItemTree[]; memos: IMemo[]; message: IConversation | null }> {
38
- // Always use single mode for auto-structuring (triggered by brainstorming edits).
39
- // Multi-agent analysis is only used via streaming endpoint (structureWithChatDirect).
40
- const projectContext = getProjectContextSummary(projectId) || undefined;
41
- const safeContext = projectContext ? truncateContext(projectContext, AI_CONTEXT_LIMIT) : undefined;
42
- return structureSingle(projectId, brainstormId, content, safeContext);
43
- }
44
-
45
- /**
46
- * Streaming structure with direct SSE callback.
47
- * Instead of async generator, takes a `send` callback to emit SSE events directly.
48
- * This avoids timing issues with generator yield + async queue.
49
- */
50
- export async function structureWithChatDirect(
51
- projectId: string,
52
- brainstormId: string,
53
- content: string,
54
- send: (event: string, data: unknown) => void | Promise<void>,
55
- ): Promise<void> {
56
- const projectContext = getProjectContextSummary(projectId) || undefined;
57
- const contextSize = projectContext?.length || 0;
58
-
59
- await send('status', { message: '컨텍스트 크기 확인 중...' });
60
-
61
- const onText: OnTextChunk = (text) => {
62
- send('ai_text', { text });
63
- };
64
-
65
- const onRawEvent: OnRawEvent = (event) => {
66
- send('ai_event', event);
67
- };
68
-
69
- if (contextSize <= AI_CONTEXT_LIMIT) {
70
- await send('status', { message: 'AI 구조화 중...', mode: 'single' });
71
-
72
- const history = getRecentConversations(projectId, 20);
73
- const historyForAi = history.map(h => ({ role: h.role, content: h.content }));
74
- const safeContext = projectContext ? truncateContext(projectContext, AI_CONTEXT_LIMIT) : undefined;
75
-
76
- const existingItems = getItemTree(projectId);
77
- const existingContext = existingItems.length > 0
78
- ? serializeExistingItems(existingItems)
79
- : undefined;
80
-
81
- const result = await runStructureWithQuestions(content, historyForAi, safeContext, onText, onRawEvent, existingContext);
82
-
83
- const dbItems = mapToDbFormat(result.items as IStructuredItem[]);
84
- const tree = replaceItems(projectId, brainstormId, dbItems);
85
- resolveMemos(projectId);
86
-
87
- let aiMessage: IConversation | null = null;
88
- let memos: IMemo[] = [];
89
- if (result.questions.length > 0) {
90
- const messageContent = result.questions
91
- .map((q, i) => `${i + 1}. ${q.question}`)
92
- .join('\n');
93
- aiMessage = addMessage(projectId, 'assistant', messageContent);
94
- memos = createMemosFromQuestions(projectId, aiMessage.id, result.questions);
95
- }
96
-
97
- await send('done', { items: tree, memos, message: aiMessage });
98
- return;
99
- }
100
-
101
- // ============================================================
102
- // 3-Phase Multi-Agent Analysis
103
- // ============================================================
104
- const subProjects = getProjectContextsBySubProject(projectId);
105
- const brainstormContext = content.trim()
106
- ? `\n\n사용자의 브레인스토밍 메모:\n${content}`
107
- : '';
108
-
109
- // Send phase list to frontend
110
- await send('phase_list', {
111
- phases: [
112
- { name: '전체 아키텍처 분석', status: 'pending' },
113
- { name: `서브 프로젝트 병렬 분석 (${subProjects.length}개)`, status: 'pending' },
114
- { name: '최종 구조화', status: 'pending' },
115
- ],
116
- });
117
-
118
- // Send sub-project list
119
- await send('chunk_list', {
120
- chunks: subProjects.map((c, i) => ({
121
- name: c.name,
122
- index: i + 1,
123
- fileCount: c.contexts.length,
124
- })),
125
- total: subProjects.length,
126
- });
127
-
128
- // ----------------------------------------------------------
129
- // Phase 1: Build hub document from docs/configs
130
- // ----------------------------------------------------------
131
- await send('phase_update', { index: 0, status: 'active' });
132
- await send('status', { message: 'Phase 1: 문서/설정 기반 아키텍처 분석 중...' });
133
-
134
- // Collect root files + all docs/configs for overview
135
- const rootSub = subProjects.find(s => s.name === '(root)');
136
- const rootContext = rootSub ? truncateContext(buildSubProjectSummary(rootSub), AI_CHUNK_LIMIT) : '';
137
-
138
- // Also collect file listing for all sub-projects
139
- const projectFileTree = subProjects
140
- .map(s => `[${s.name}] (${s.contexts.length}개 파일)\n${s.contexts.map(c => ` ${c.file_path}`).join('\n')}`)
141
- .join('\n\n');
142
-
143
- const phase1Prompt = `당신은 소프트웨어 프로젝트 아키텍트입니다. 아래 프로젝트의 문서와 설정 파일을 분석하여 "프로젝트 개요 문서"를 작성하세요.
144
-
145
- 이 문서는 다른 AI 에이전트들이 각 서브 프로젝트를 분석할 때 참조하는 중추 문서가 됩니다.
146
-
147
- 다음 내용을 포함해주세요:
148
- 1. 프로젝트 전체 목적과 구조
149
- 2. 기술 스택 (프레임워크, 언어, 주요 라이브러리)
150
- 3. 서브 프로젝트 간의 관계와 의존성
151
- 4. 아키텍처 패턴 (모노레포, 마이크로서비스, 등)
152
- 5. 주요 컨벤션과 규칙
153
- 6. 배포/인프라 구조 (파악 가능한 경우)
154
-
155
- 한국어로 작성하세요. Markdown 형식으로 작성하세요.
156
- ${brainstormContext}
157
-
158
- === 프로젝트 파일 트리 ===
159
- ${projectFileTree}
160
-
161
- === 루트 문서/설정 파일 ===
162
- ${rootContext}`;
163
-
164
- let hubDocument = '';
165
- try {
166
- hubDocument = await runAnalysis(phase1Prompt, onText, onRawEvent);
167
- } catch (err) {
168
- const errMsg = err instanceof Error ? err.message : String(err);
169
- hubDocument = `# 프로젝트 개요 (자동 생성 실패)\n오류: ${errMsg.slice(0, 300)}\n\n## 서브 프로젝트 목록\n${subProjects.map(s => `- ${s.name}`).join('\n')}`;
170
- }
171
-
172
- await send('phase_update', { index: 0, status: 'done' });
173
- await send('hub_document', { content: hubDocument });
174
-
175
- // ----------------------------------------------------------
176
- // Phase 2: Parallel sub-project analysis
177
- // ----------------------------------------------------------
178
- await send('phase_update', { index: 1, status: 'active' });
179
- await send('status', { message: 'Phase 2: 서브 프로젝트 병렬 분석 중...' });
180
-
181
- const CONCURRENCY = 2;
182
- const subAnalyses = new Map<string, string>();
183
- const nonRootSubs = subProjects.filter(s => s.name !== '(root)');
184
-
185
- // Process in batches of CONCURRENCY
186
- for (let batch = 0; batch < nonRootSubs.length; batch += CONCURRENCY) {
187
- const batchSubs = nonRootSubs.slice(batch, batch + CONCURRENCY);
188
-
189
- const batchPromises = batchSubs.map(async (sub, batchIdx) => {
190
- const globalIdx = batch + batchIdx;
191
- const subIdx = subProjects.indexOf(sub);
192
-
193
- await send('structuring_sub', {
194
- subProject: sub.name,
195
- current: globalIdx + 1,
196
- total: nonRootSubs.length,
197
- files: sub.contexts.map(c => c.file_path),
198
- });
199
-
200
- const subContext = truncateContext(buildSubProjectSummary(sub), AI_CHUNK_LIMIT);
201
- const phase2Prompt = `당신은 소프트웨어 분석가입니다. 아래 "프로젝트 개요 문서"를 참조하여 서브 프로젝트 "${sub.name}"의 소스코드를 분석하세요.
202
-
203
- 분석 결과를 다음 형식으로 작성하세요:
204
- 1. **역할**: 이 서브 프로젝트가 전체 시스템에서 하는 역할
205
- 2. **주요 기능**: 구현된 핵심 기능 목록 (각 기능의 구현 상태 포함)
206
- 3. **기술 스택**: 사용 중인 기술/라이브러리
207
- 4. **구현 상태**: done/in_progress/pending 판단 근거
208
- 5. **TODO/개선점**: 코드에서 발견된 TODO, 미구현 부분, 개선 가능 사항
209
- 6. **다른 서브 프로젝트와의 관계**: 의존하거나 의존받는 프로젝트
210
-
211
- 한국어로 작성하세요. Markdown 형식으로 작성하세요.
212
-
213
- === 프로젝트 개요 문서 (중추) ===
214
- ${truncateContext(hubDocument, 30_000)}
215
-
216
- === ${sub.name} 소스코드 ===
217
- ${subContext}`;
218
-
219
- try {
220
- // Each sub gets its own text stream tagged with sub name
221
- const subOnText: OnTextChunk = (text) => {
222
- send('ai_text', { text, subProject: sub.name });
223
- };
224
-
225
- const analysis = await runAnalysis(phase2Prompt, subOnText);
226
- subAnalyses.set(sub.name, analysis);
227
-
228
- await send('structuring_sub_done', {
229
- subProject: sub.name,
230
- current: globalIdx + 1,
231
- total: nonRootSubs.length,
232
- itemCount: 0,
233
- });
234
- } catch (err) {
235
- const errMsg = err instanceof Error ? err.message : String(err);
236
- console.error(`[structurer] Phase 2 "${sub.name}" failed:`, errMsg);
237
- subAnalyses.set(sub.name, `# ${sub.name} (분석 실패)\n오류: ${errMsg.slice(0, 300)}`);
238
-
239
- await send('structuring_sub_done', {
240
- subProject: sub.name,
241
- current: globalIdx + 1,
242
- total: nonRootSubs.length,
243
- error: errMsg.slice(0, 200),
244
- });
245
- }
246
- });
247
-
248
- await Promise.all(batchPromises);
249
- }
250
-
251
- // Build complete hub document with all analyses
252
- const completeDocument = `${hubDocument}\n\n---\n\n# 서브 프로젝트 상세 분석\n\n${
253
- Array.from(subAnalyses.entries())
254
- .map(([name, analysis]) => `## ${name}\n\n${analysis}`)
255
- .join('\n\n---\n\n')
256
- }`;
257
-
258
- await send('phase_update', { index: 1, status: 'done' });
259
- await send('hub_document', { content: completeDocument });
260
-
261
- // ----------------------------------------------------------
262
- // Phase 3: Final structuring from complete hub document
263
- // ----------------------------------------------------------
264
- await send('phase_update', { index: 2, status: 'active' });
265
- await send('status', { message: 'Phase 3: 중추 문서 기반 최종 구조화 중...' });
266
- await send('ai_text_reset', {});
267
-
268
- const phase3Prompt = `You are a JSON-only structuring machine. You NEVER respond with text, explanations, or conversation.
269
- You ALWAYS output ONLY a raw JSON array, nothing else.
270
-
271
- Your job: convert the comprehensive project analysis document below into a structured JSON tree.
272
-
273
- Schema per item:
274
- { "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "status": "pending"|"in_progress"|"done", "children": [same schema] }
275
-
276
- Rules:
277
- - Output MUST start with [ and end with ]
278
- - No markdown fences, no explanation, no text before or after the JSON
279
- - Top-level items should be sub-projects (one per analyzed project)
280
- - Each top-level item should have children representing features/tasks/bugs found in that sub-project
281
- - Keep titles concise (under 50 chars)
282
- - Judge status based on the analysis:
283
- - "done": fully implemented as described
284
- - "in_progress": partially implemented or has TODOs
285
- - "pending": not yet started or only planned
286
- - Prioritize items that have TODOs or are in_progress as "high" priority
287
- - Include bugs, improvements, and missing features mentioned in the analysis
288
- ${brainstormContext}
289
-
290
- === 프로젝트 분석 문서 ===
291
- ${truncateContext(completeDocument, PHASE3_CONTEXT_LIMIT)}`;
292
-
293
- try {
294
- const resultText = await runClaude(phase3Prompt, onText, onRawEvent);
295
- const json = extractJson(resultText, 'array');
296
- const structured = JSON.parse(json) as IStructuredItem[];
297
-
298
- const dbItems = mapToDbFormat(structured);
299
- const tree = appendItems(projectId, brainstormId, dbItems);
300
- resolveMemos(projectId);
301
-
302
- const summaryMsg = addMessage(
303
- projectId,
304
- 'assistant',
305
- `3단계 분석 완료: ${nonRootSubs.length}개 서브 프로젝트를 병렬 분석 후 구조화했습니다.`,
306
- );
307
-
308
- await send('phase_update', { index: 2, status: 'done' });
309
- await send('done', { items: tree, memos: [], message: summaryMsg });
310
- } catch (err) {
311
- const errMsg = err instanceof Error ? err.message : String(err);
312
- console.error('[structurer] Phase 3 failed:', errMsg);
313
-
314
- await send('phase_update', { index: 2, status: 'error' });
315
- await send('error', { error: `Phase 3 구조화 실패: ${errMsg.slice(0, 300)}` });
316
- }
317
- }
318
-
319
- // ============================================================
320
- // Internal helpers
321
- // ============================================================
322
-
323
- async function structureSingle(
324
- projectId: string,
325
- brainstormId: string,
326
- content: string,
327
- projectContext?: string,
328
- ): Promise<{ items: IItemTree[]; memos: IMemo[]; message: IConversation | null }> {
329
- const history = getRecentConversations(projectId, 20);
330
- const historyForAi = history.map(h => ({
331
- role: h.role,
332
- content: h.content,
333
- }));
334
-
335
- const existingItems = getItemTree(projectId);
336
- const existingContext = existingItems.length > 0
337
- ? serializeExistingItems(existingItems)
338
- : undefined;
339
-
340
- const safeContext = projectContext ? truncateContext(projectContext, AI_CONTEXT_LIMIT) : undefined;
341
- const result = await runStructureWithQuestions(content, historyForAi, safeContext, undefined, undefined, existingContext);
342
-
343
- const dbItems = mapToDbFormat(result.items as IStructuredItem[]);
344
- const tree = replaceItems(projectId, brainstormId, dbItems);
345
-
346
- resolveMemos(projectId);
347
-
348
- let aiMessage: IConversation | null = null;
349
- let memos: IMemo[] = [];
350
-
351
- if (result.questions.length > 0) {
352
- const messageContent = result.questions
353
- .map((q, i) => `${i + 1}. ${q.question}`)
354
- .join('\n');
355
-
356
- aiMessage = addMessage(projectId, 'assistant', messageContent);
357
- memos = createMemosFromQuestions(projectId, aiMessage.id, result.questions);
358
- }
359
-
360
- return { items: tree, memos, message: aiMessage };
361
- }
362
-
363
- function serializeExistingItems(items: IItemTree[], depth = 0): string {
364
- if (items.length === 0) return '';
365
- const lines: string[] = [];
366
- for (const item of items) {
367
- const indent = ' '.repeat(depth);
368
- lines.push(`${indent}- [${item.item_type}/${item.priority}] ${item.title}: ${item.description || ''}`);
369
- if (item.children && item.children.length > 0) {
370
- lines.push(serializeExistingItems(item.children, depth + 1));
371
- }
372
- }
373
- return lines.join('\n');
374
- }
375
-
376
- function truncateContext(context: string, limit: number): string {
377
- if (context.length <= limit) return context;
378
-
379
- const fileSections = context.split(/(?=--- .+ ---\n)/);
380
- let result = '';
381
-
382
- for (const section of fileSections) {
383
- if (result.length + section.length > limit) {
384
- result += `\n\n--- (${fileSections.length - result.split('---').length / 2}개 파일 생략됨, 컨텍스트 크기 제한) ---\n`;
385
- break;
386
- }
387
- result += section;
388
- }
389
-
390
- return result;
391
- }
392
-
393
- function mapToDbFormat(items: IStructuredItem[]): Parameters<typeof replaceItems>[2] {
394
- return items.map((item) => ({
395
- parent_id: null,
396
- title: item.title,
397
- description: item.description,
398
- item_type: item.item_type,
399
- priority: item.priority,
400
- status: item.status,
401
- children: item.children ? mapToDbFormat(item.children) : undefined,
402
- }));
403
- }