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.
- package/package.json +1 -1
- package/src/app/projects/[id]/page.tsx +0 -4
- package/src/components/brainstorm/Editor.tsx +1 -84
- package/src/lib/ai/client.ts +0 -123
- package/src/lib/db/schema.ts +0 -70
- package/src/types/index.ts +0 -90
- package/src/app/api/projects/[id]/cleanup/route.ts +0 -32
- package/src/app/api/projects/[id]/conversations/route.ts +0 -50
- package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +0 -51
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +0 -36
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +0 -95
- package/src/app/api/projects/[id]/items/route.ts +0 -67
- package/src/app/api/projects/[id]/memos/route.ts +0 -18
- package/src/app/api/projects/[id]/scan/route.ts +0 -73
- package/src/app/api/projects/[id]/scan/stream/route.ts +0 -112
- package/src/app/api/projects/[id]/structure/route.ts +0 -59
- package/src/app/api/projects/[id]/structure/stream/route.ts +0 -157
- package/src/components/ScanPanel.tsx +0 -743
- package/src/components/brainstorm/MemoPin.tsx +0 -117
- package/src/components/tree/CardView.tsx +0 -206
- package/src/components/tree/ItemDetail.tsx +0 -196
- package/src/components/tree/LockToggle.tsx +0 -23
- package/src/components/tree/RefinePopover.tsx +0 -157
- package/src/components/tree/StatusBadge.tsx +0 -32
- package/src/components/tree/TreeNode.tsx +0 -227
- package/src/components/tree/TreeView.tsx +0 -304
- package/src/lib/ai/chat-responder.ts +0 -71
- package/src/lib/ai/cleanup.ts +0 -87
- package/src/lib/ai/prompter.ts +0 -78
- package/src/lib/ai/refiner.ts +0 -128
- package/src/lib/ai/structurer.ts +0 -403
- package/src/lib/db/queries/context.ts +0 -76
- package/src/lib/db/queries/conversations.ts +0 -46
- package/src/lib/db/queries/items.ts +0 -268
- package/src/lib/db/queries/memos.ts +0 -66
- package/src/lib/db/queries/prompts.ts +0 -68
- package/src/lib/scanner.ts +0 -573
- package/src/lib/task-store.ts +0 -97
package/src/lib/ai/cleanup.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/ai/prompter.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/ai/refiner.ts
DELETED
|
@@ -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
|
-
}
|
package/src/lib/ai/structurer.ts
DELETED
|
@@ -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
|
-
}
|