syr-d2c-workflow-mcp 0.3.0 → 0.4.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/README.md +377 -16
- package/dist/index.js +533 -228
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,10 +5,126 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { glob } from "glob";
|
|
7
7
|
import * as fs from "fs/promises";
|
|
8
|
+
import * as path from "path";
|
|
8
9
|
// 환경 변수에서 설정 읽기
|
|
9
10
|
const RULES_PATHS = process.env.RULES_PATHS?.split(",").map((p) => p.trim()) || [];
|
|
10
11
|
const RULES_GLOB = process.env.RULES_GLOB || "";
|
|
11
12
|
const CONFIG_PATH = process.env.D2C_CONFIG_PATH || "";
|
|
13
|
+
const PROJECT_ROOT = process.env.D2C_PROJECT_ROOT || process.cwd();
|
|
14
|
+
// OpenSpec 규칙 탐지 경로
|
|
15
|
+
const OPENSPEC_SEARCH_PATHS = [
|
|
16
|
+
"openspec/specs/*/spec.md",
|
|
17
|
+
".cursor/openspec/specs/*/spec.md",
|
|
18
|
+
"docs/openspec/specs/*/spec.md",
|
|
19
|
+
];
|
|
20
|
+
// OpenSpec 규칙 캐시
|
|
21
|
+
let cachedOpenSpecRules = null;
|
|
22
|
+
// OpenSpec spec.md 파싱
|
|
23
|
+
async function parseOpenSpecFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
26
|
+
const specName = path.basename(path.dirname(filePath));
|
|
27
|
+
const requirements = [];
|
|
28
|
+
// Requirement 섹션 파싱
|
|
29
|
+
const reqRegex = /### Requirement: (.+?)\n\n([\s\S]*?)(?=### Requirement:|---|\n## |$)/g;
|
|
30
|
+
let reqMatch;
|
|
31
|
+
while ((reqMatch = reqRegex.exec(content)) !== null) {
|
|
32
|
+
const reqName = reqMatch[1].trim();
|
|
33
|
+
const reqContent = reqMatch[2];
|
|
34
|
+
// Scenario 파싱
|
|
35
|
+
const scenarios = [];
|
|
36
|
+
const scenarioRegex = /#### Scenario: (.+?)\n\n([\s\S]*?)(?=#### Scenario:|### Requirement:|---|\n## |$)/g;
|
|
37
|
+
let scenarioMatch;
|
|
38
|
+
while ((scenarioMatch = scenarioRegex.exec(reqContent)) !== null) {
|
|
39
|
+
const scenarioName = scenarioMatch[1].trim();
|
|
40
|
+
const scenarioContent = scenarioMatch[2];
|
|
41
|
+
const givenMatch = scenarioContent.match(/- \*\*GIVEN\*\* (.+)/);
|
|
42
|
+
const whenMatch = scenarioContent.match(/- \*\*WHEN\*\* (.+)/);
|
|
43
|
+
const thenMatch = scenarioContent.match(/- \*\*THEN\*\* (.+)/);
|
|
44
|
+
scenarios.push({
|
|
45
|
+
name: scenarioName,
|
|
46
|
+
given: givenMatch?.[1] || "",
|
|
47
|
+
when: whenMatch?.[1] || "",
|
|
48
|
+
then: thenMatch?.[1] || "",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// 설명 추출 (첫 번째 문단)
|
|
52
|
+
const descMatch = reqContent.match(/^(.+?)(?:\n\n|$)/);
|
|
53
|
+
requirements.push({
|
|
54
|
+
name: reqName,
|
|
55
|
+
description: descMatch?.[1]?.trim() || "",
|
|
56
|
+
scenarios,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
specName,
|
|
61
|
+
filePath,
|
|
62
|
+
requirements,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
console.error(`Failed to parse OpenSpec file: ${filePath}`, e);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// OpenSpec 규칙 탐지 및 로드
|
|
71
|
+
async function loadOpenSpecRules(forceReload = false) {
|
|
72
|
+
if (cachedOpenSpecRules && !forceReload) {
|
|
73
|
+
return cachedOpenSpecRules;
|
|
74
|
+
}
|
|
75
|
+
const rules = [];
|
|
76
|
+
for (const searchPath of OPENSPEC_SEARCH_PATHS) {
|
|
77
|
+
const fullPattern = path.join(PROJECT_ROOT, searchPath);
|
|
78
|
+
const files = await glob(fullPattern);
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const rule = await parseOpenSpecFile(file);
|
|
81
|
+
if (rule) {
|
|
82
|
+
rules.push(rule);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
cachedOpenSpecRules = rules;
|
|
87
|
+
return rules;
|
|
88
|
+
}
|
|
89
|
+
// Phase별 Tasks 정의
|
|
90
|
+
const PHASE_TASKS = {
|
|
91
|
+
1: {
|
|
92
|
+
name: "Phase 1: Figma MCP 추출",
|
|
93
|
+
target: 60,
|
|
94
|
+
tasks: [
|
|
95
|
+
{ id: "1.1", content: "Figma 디자인 컨텍스트 가져오기" },
|
|
96
|
+
{ id: "1.2", content: "Figma MCP로 코드 추출" },
|
|
97
|
+
{ id: "1.3", content: "Playwright 렌더링" },
|
|
98
|
+
{ id: "1.4", content: "스크린샷 비교 (toHaveScreenshot)" },
|
|
99
|
+
{ id: "1.5", content: "d2c_phase1_compare 호출" },
|
|
100
|
+
{ id: "1.6", content: "HITL 확인" },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
2: {
|
|
104
|
+
name: "Phase 2: LLM 이미지 Diff",
|
|
105
|
+
target: 70,
|
|
106
|
+
tasks: [
|
|
107
|
+
{ id: "2.1", content: "Playwright 이미지 diff 분석" },
|
|
108
|
+
{ id: "2.2", content: "diff 영역 식별" },
|
|
109
|
+
{ id: "2.3", content: "LLM이 코드 수정" },
|
|
110
|
+
{ id: "2.4", content: "렌더링 후 스크린샷 비교" },
|
|
111
|
+
{ id: "2.5", content: "d2c_phase2_image_diff 호출" },
|
|
112
|
+
{ id: "2.6", content: "HITL 확인" },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
3: {
|
|
116
|
+
name: "Phase 3: LLM DOM 비교",
|
|
117
|
+
target: 90,
|
|
118
|
+
tasks: [
|
|
119
|
+
{ id: "3.1", content: "Playwright DOM 스냅샷 추출" },
|
|
120
|
+
{ id: "3.2", content: "DOM 구조 비교" },
|
|
121
|
+
{ id: "3.3", content: "LLM이 DOM 기반 수정" },
|
|
122
|
+
{ id: "3.4", content: "렌더링 후 DOM 비교" },
|
|
123
|
+
{ id: "3.5", content: "d2c_phase3_dom_compare 호출" },
|
|
124
|
+
{ id: "3.6", content: "HITL 확인" },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
12
128
|
// 서비스 식별자 - AI가 이 키워드를 감지하면 이 MCP를 사용
|
|
13
129
|
const SERVICE_IDENTIFIERS = `
|
|
14
130
|
🎯 **서비스 식별자**: 다음 키워드가 포함되면 이 MCP를 사용하세요:
|
|
@@ -104,7 +220,7 @@ const DEFAULT_RULES = `
|
|
|
104
220
|
// MCP 서버 생성
|
|
105
221
|
const server = new Server({
|
|
106
222
|
name: "syr-d2c-workflow-mcp",
|
|
107
|
-
version: "0.
|
|
223
|
+
version: "0.4.0",
|
|
108
224
|
}, {
|
|
109
225
|
capabilities: {
|
|
110
226
|
tools: {},
|
|
@@ -193,60 +309,6 @@ ${SERVICE_IDENTIFIERS}
|
|
|
193
309
|
required: ["code", "componentName"],
|
|
194
310
|
},
|
|
195
311
|
},
|
|
196
|
-
// compare_with_design - 디자인 비교
|
|
197
|
-
{
|
|
198
|
-
name: "d2c_compare_with_design",
|
|
199
|
-
description: `Figma 디자인 스크린샷과 렌더링 결과를 비교 분석합니다.
|
|
200
|
-
${SERVICE_IDENTIFIERS}
|
|
201
|
-
|
|
202
|
-
📊 **비교 항목 (각 0-100점)**:
|
|
203
|
-
- 레이아웃 일치도
|
|
204
|
-
- 색상/타이포그래피 일치도
|
|
205
|
-
- 간격/여백 일치도
|
|
206
|
-
- 누락된 요소
|
|
207
|
-
|
|
208
|
-
💡 **사용법**:
|
|
209
|
-
1. figma-mcp.get_screenshot으로 원본 이미지 획득
|
|
210
|
-
2. playwright-mcp로 렌더링 결과 스크린샷
|
|
211
|
-
3. 이 도구로 비교 분석 (scores 필수 입력)`,
|
|
212
|
-
inputSchema: {
|
|
213
|
-
type: "object",
|
|
214
|
-
properties: {
|
|
215
|
-
designDescription: {
|
|
216
|
-
type: "string",
|
|
217
|
-
description: "Figma 디자인 설명 (get_design_context 결과)",
|
|
218
|
-
},
|
|
219
|
-
renderedDescription: {
|
|
220
|
-
type: "string",
|
|
221
|
-
description: "렌더링된 결과 설명",
|
|
222
|
-
},
|
|
223
|
-
differences: {
|
|
224
|
-
type: "array",
|
|
225
|
-
items: { type: "string" },
|
|
226
|
-
description: "발견된 차이점 목록",
|
|
227
|
-
},
|
|
228
|
-
iteration: {
|
|
229
|
-
type: "number",
|
|
230
|
-
description: "현재 반복 횟수",
|
|
231
|
-
},
|
|
232
|
-
maxIterations: {
|
|
233
|
-
type: "number",
|
|
234
|
-
description: "최대 반복 횟수 (기본: 5)",
|
|
235
|
-
},
|
|
236
|
-
scores: {
|
|
237
|
-
type: "object",
|
|
238
|
-
properties: {
|
|
239
|
-
layout: { type: "number", description: "레이아웃 점수 (0-100)" },
|
|
240
|
-
colors: { type: "number", description: "색상 점수 (0-100)" },
|
|
241
|
-
typography: { type: "number", description: "타이포그래피 점수 (0-100)" },
|
|
242
|
-
spacing: { type: "number", description: "간격 점수 (0-100)" },
|
|
243
|
-
},
|
|
244
|
-
description: "항목별 점수 (0-100)",
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
required: ["designDescription", "renderedDescription", "scores"],
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
312
|
// log_step - 실시간 진행 로그
|
|
251
313
|
{
|
|
252
314
|
name: "d2c_log_step",
|
|
@@ -282,44 +344,6 @@ ${SERVICE_IDENTIFIERS}
|
|
|
282
344
|
required: ["step", "stepName", "status"],
|
|
283
345
|
},
|
|
284
346
|
},
|
|
285
|
-
// iteration_check - 반복 제어
|
|
286
|
-
{
|
|
287
|
-
name: "d2c_iteration_check",
|
|
288
|
-
description: `반복 계속 여부를 판단합니다.
|
|
289
|
-
${SERVICE_IDENTIFIERS}
|
|
290
|
-
|
|
291
|
-
📊 **판단 기준**:
|
|
292
|
-
- 70점 미만: 자동으로 계속 진행
|
|
293
|
-
- 70점 이상: 사용자 확인 필요
|
|
294
|
-
- 최대 반복 도달 또는 점수 하락: 중단 권장`,
|
|
295
|
-
inputSchema: {
|
|
296
|
-
type: "object",
|
|
297
|
-
properties: {
|
|
298
|
-
currentScore: {
|
|
299
|
-
type: "number",
|
|
300
|
-
description: "현재 종합 점수 (0-100)",
|
|
301
|
-
},
|
|
302
|
-
targetScore: {
|
|
303
|
-
type: "number",
|
|
304
|
-
description: "목표 점수 (기본: 70)",
|
|
305
|
-
},
|
|
306
|
-
iteration: {
|
|
307
|
-
type: "number",
|
|
308
|
-
description: "현재 반복 횟수",
|
|
309
|
-
},
|
|
310
|
-
maxIterations: {
|
|
311
|
-
type: "number",
|
|
312
|
-
description: "최대 반복 횟수 (기본: 5)",
|
|
313
|
-
},
|
|
314
|
-
previousScores: {
|
|
315
|
-
type: "array",
|
|
316
|
-
items: { type: "number" },
|
|
317
|
-
description: "이전 반복의 점수들",
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
required: ["currentScore", "iteration"],
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
347
|
// ============ 3단계 PHASE 도구들 ============
|
|
324
348
|
// Phase 1: Figma MCP 기반 스크린샷 비교
|
|
325
349
|
{
|
|
@@ -510,6 +534,97 @@ ${SERVICE_IDENTIFIERS}
|
|
|
510
534
|
required: ["currentPhase"],
|
|
511
535
|
},
|
|
512
536
|
},
|
|
537
|
+
// ============ OpenSpec 통합 도구들 ============
|
|
538
|
+
// OpenSpec 규칙 로드
|
|
539
|
+
{
|
|
540
|
+
name: "d2c_load_openspec_rules",
|
|
541
|
+
description: `사용자 프로젝트의 OpenSpec 규칙을 자동으로 탐지하고 로드합니다.
|
|
542
|
+
${SERVICE_IDENTIFIERS}
|
|
543
|
+
|
|
544
|
+
📋 **탐지 경로**:
|
|
545
|
+
- ./openspec/specs/*/spec.md
|
|
546
|
+
- ./.cursor/openspec/specs/*/spec.md
|
|
547
|
+
- ./docs/openspec/specs/*/spec.md
|
|
548
|
+
|
|
549
|
+
🔍 **반환 정보**:
|
|
550
|
+
- 발견된 spec 이름 및 경로
|
|
551
|
+
- 각 spec의 Requirements 목록
|
|
552
|
+
- 각 Requirement의 Scenarios`,
|
|
553
|
+
inputSchema: {
|
|
554
|
+
type: "object",
|
|
555
|
+
properties: {
|
|
556
|
+
forceReload: {
|
|
557
|
+
type: "boolean",
|
|
558
|
+
description: "캐시 무시하고 다시 로드 (기본: false)",
|
|
559
|
+
},
|
|
560
|
+
specNames: {
|
|
561
|
+
type: "array",
|
|
562
|
+
items: { type: "string" },
|
|
563
|
+
description: "특정 spec만 필터링 (예: ['figma-standard', 'design-rules'])",
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
// 워크플로우 Tasks 체크리스트
|
|
569
|
+
{
|
|
570
|
+
name: "d2c_get_workflow_tasks",
|
|
571
|
+
description: `현재 Phase에 맞는 tasks.md 형식 체크리스트를 반환합니다.
|
|
572
|
+
${SERVICE_IDENTIFIERS}
|
|
573
|
+
|
|
574
|
+
📋 **체크리스트 포함 내용**:
|
|
575
|
+
- Phase 이름 및 목표 성공률
|
|
576
|
+
- 세부 Task 목록 (완료 상태 표시)
|
|
577
|
+
- 적용될 OpenSpec 규칙 목록`,
|
|
578
|
+
inputSchema: {
|
|
579
|
+
type: "object",
|
|
580
|
+
properties: {
|
|
581
|
+
phase: {
|
|
582
|
+
type: "number",
|
|
583
|
+
enum: [1, 2, 3],
|
|
584
|
+
description: "현재 Phase (1, 2, 3)",
|
|
585
|
+
},
|
|
586
|
+
completedTasks: {
|
|
587
|
+
type: "array",
|
|
588
|
+
items: { type: "string" },
|
|
589
|
+
description: "완료된 task ID 목록 (예: ['1.1', '1.2'])",
|
|
590
|
+
},
|
|
591
|
+
includeRules: {
|
|
592
|
+
type: "boolean",
|
|
593
|
+
description: "적용 규칙 목록 포함 (기본: true)",
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
required: ["phase"],
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
// OpenSpec 규칙 기반 검증
|
|
600
|
+
{
|
|
601
|
+
name: "d2c_validate_against_spec",
|
|
602
|
+
description: `생성된 코드가 OpenSpec 규칙을 준수하는지 검증합니다.
|
|
603
|
+
${SERVICE_IDENTIFIERS}
|
|
604
|
+
|
|
605
|
+
🔍 **검증 내용**:
|
|
606
|
+
- 각 Requirement별 pass/fail/warn 상태
|
|
607
|
+
- 위반 시 구체적인 메시지
|
|
608
|
+
- 수정 가이드 제공`,
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: "object",
|
|
611
|
+
properties: {
|
|
612
|
+
code: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "검증할 코드",
|
|
615
|
+
},
|
|
616
|
+
specName: {
|
|
617
|
+
type: "string",
|
|
618
|
+
description: "검증에 사용할 spec 이름 (없으면 모든 spec 적용)",
|
|
619
|
+
},
|
|
620
|
+
componentName: {
|
|
621
|
+
type: "string",
|
|
622
|
+
description: "컴포넌트 이름",
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
required: ["code"],
|
|
626
|
+
},
|
|
627
|
+
},
|
|
513
628
|
// get_component_template - 템플릿 생성
|
|
514
629
|
{
|
|
515
630
|
name: "d2c_get_component_template",
|
|
@@ -751,113 +866,6 @@ ${input.message ? ` → ${input.message}` : ""}
|
|
|
751
866
|
],
|
|
752
867
|
};
|
|
753
868
|
}
|
|
754
|
-
case "d2c_iteration_check": {
|
|
755
|
-
const input = z
|
|
756
|
-
.object({
|
|
757
|
-
currentScore: z.number(),
|
|
758
|
-
targetScore: z.number().optional().default(70),
|
|
759
|
-
iteration: z.number(),
|
|
760
|
-
maxIterations: z.number().optional().default(5),
|
|
761
|
-
previousScores: z.array(z.number()).optional(),
|
|
762
|
-
})
|
|
763
|
-
.parse(args);
|
|
764
|
-
const { currentScore, targetScore, iteration, maxIterations, previousScores } = input;
|
|
765
|
-
// 점수 변화 계산
|
|
766
|
-
const lastScore = previousScores?.length ? previousScores[previousScores.length - 1] : null;
|
|
767
|
-
const scoreDiff = lastScore !== null ? currentScore - lastScore : null;
|
|
768
|
-
const isImproving = scoreDiff === null || scoreDiff >= 0;
|
|
769
|
-
// 판단 로직
|
|
770
|
-
let recommendation;
|
|
771
|
-
let reason;
|
|
772
|
-
if (iteration >= maxIterations) {
|
|
773
|
-
recommendation = "stop";
|
|
774
|
-
reason = `최대 반복 횟수(${maxIterations}회) 도달`;
|
|
775
|
-
}
|
|
776
|
-
else if (!isImproving && scoreDiff !== null && scoreDiff < -10) {
|
|
777
|
-
recommendation = "stop";
|
|
778
|
-
reason = `점수 하락 감지 (${scoreDiff}점)`;
|
|
779
|
-
}
|
|
780
|
-
else if (currentScore >= targetScore) {
|
|
781
|
-
recommendation = "user_confirm";
|
|
782
|
-
reason = `목표 점수(${targetScore}점) 달성! 사용자 확인 필요`;
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
recommendation = "continue";
|
|
786
|
-
reason = `목표 점수(${targetScore}점) 미달, 자동 계속`;
|
|
787
|
-
}
|
|
788
|
-
const statusEmoji = recommendation === "continue" ? "🔄" : recommendation === "user_confirm" ? "✋" : "🛑";
|
|
789
|
-
const diffText = scoreDiff !== null ? ` (${scoreDiff >= 0 ? "+" : ""}${scoreDiff})` : "";
|
|
790
|
-
return {
|
|
791
|
-
content: [
|
|
792
|
-
{
|
|
793
|
-
type: "text",
|
|
794
|
-
text: `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
795
|
-
${statusEmoji} **반복 ${iteration}/${maxIterations} 판단 결과**
|
|
796
|
-
|
|
797
|
-
📊 현재 점수: **${currentScore}점**${diffText}
|
|
798
|
-
🎯 목표 점수: ${targetScore}점
|
|
799
|
-
|
|
800
|
-
**권장**: ${recommendation === "continue" ? "계속 진행" : recommendation === "user_confirm" ? "사용자 확인" : "중단"}
|
|
801
|
-
**이유**: ${reason}
|
|
802
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
803
|
-
},
|
|
804
|
-
],
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
case "d2c_compare_with_design": {
|
|
808
|
-
const input = z
|
|
809
|
-
.object({
|
|
810
|
-
designDescription: z.string(),
|
|
811
|
-
renderedDescription: z.string(),
|
|
812
|
-
differences: z.array(z.string()).optional(),
|
|
813
|
-
iteration: z.number().optional(),
|
|
814
|
-
maxIterations: z.number().optional().default(5),
|
|
815
|
-
scores: z.object({
|
|
816
|
-
layout: z.number(),
|
|
817
|
-
colors: z.number(),
|
|
818
|
-
typography: z.number(),
|
|
819
|
-
spacing: z.number(),
|
|
820
|
-
}),
|
|
821
|
-
})
|
|
822
|
-
.parse(args);
|
|
823
|
-
const { scores, iteration, maxIterations } = input;
|
|
824
|
-
const avgScore = Math.round((scores.layout + scores.colors + scores.typography + scores.spacing) / 4);
|
|
825
|
-
// 점수 바 생성 함수
|
|
826
|
-
const scoreBar = (score) => {
|
|
827
|
-
const filled = Math.round(score / 10);
|
|
828
|
-
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
829
|
-
};
|
|
830
|
-
const checkMark = (score) => score >= 70 ? "✓" : "✗";
|
|
831
|
-
const iterationHeader = iteration ? `반복 ${iteration}/${maxIterations}` : "";
|
|
832
|
-
return {
|
|
833
|
-
content: [
|
|
834
|
-
{
|
|
835
|
-
type: "text",
|
|
836
|
-
text: `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
837
|
-
📊 **디자인 비교 결과** ${iterationHeader}
|
|
838
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
839
|
-
|
|
840
|
-
┌────────────┬────────────┬──────┬──────┐
|
|
841
|
-
│ 항목 │ 점수바 │ 점수 │ 상태 │
|
|
842
|
-
├────────────┼────────────┼──────┼──────┤
|
|
843
|
-
│ 레이아웃 │ ${scoreBar(scores.layout)} │ ${String(scores.layout).padStart(3)} │ ${checkMark(scores.layout)} │
|
|
844
|
-
│ 색상 │ ${scoreBar(scores.colors)} │ ${String(scores.colors).padStart(3)} │ ${checkMark(scores.colors)} │
|
|
845
|
-
│ 타이포 │ ${scoreBar(scores.typography)} │ ${String(scores.typography).padStart(3)} │ ${checkMark(scores.typography)} │
|
|
846
|
-
│ 간격 │ ${scoreBar(scores.spacing)} │ ${String(scores.spacing).padStart(3)} │ ${checkMark(scores.spacing)} │
|
|
847
|
-
├────────────┼────────────┼──────┼──────┤
|
|
848
|
-
│ **종합** │ ${scoreBar(avgScore)} │ **${String(avgScore).padStart(3)}** │ ${checkMark(avgScore)} │
|
|
849
|
-
└────────────┴────────────┴──────┴──────┘
|
|
850
|
-
|
|
851
|
-
${input.differences?.length ? `
|
|
852
|
-
## 발견된 차이점
|
|
853
|
-
${input.differences.map((d) => `- ${d}`).join("\n")}
|
|
854
|
-
` : ""}
|
|
855
|
-
## 다음 단계
|
|
856
|
-
→ \`d2c_iteration_check\` 호출하여 계속 여부 판단`,
|
|
857
|
-
},
|
|
858
|
-
],
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
869
|
// ============ 3단계 PHASE 핸들러 ============
|
|
862
870
|
case "d2c_phase1_compare": {
|
|
863
871
|
const input = z
|
|
@@ -1157,6 +1165,277 @@ ${input.currentPhase === 1 ? " ↑ 현재" : input.currentPhase === 2 ? "
|
|
|
1157
1165
|
],
|
|
1158
1166
|
};
|
|
1159
1167
|
}
|
|
1168
|
+
// ============ OpenSpec 통합 핸들러 ============
|
|
1169
|
+
case "d2c_load_openspec_rules": {
|
|
1170
|
+
const input = z
|
|
1171
|
+
.object({
|
|
1172
|
+
forceReload: z.boolean().optional().default(false),
|
|
1173
|
+
specNames: z.array(z.string()).optional(),
|
|
1174
|
+
})
|
|
1175
|
+
.parse(args);
|
|
1176
|
+
const rules = await loadOpenSpecRules(input.forceReload);
|
|
1177
|
+
let filteredRules = rules;
|
|
1178
|
+
if (input.specNames?.length) {
|
|
1179
|
+
filteredRules = rules.filter(r => input.specNames.includes(r.specName));
|
|
1180
|
+
}
|
|
1181
|
+
if (filteredRules.length === 0) {
|
|
1182
|
+
return {
|
|
1183
|
+
content: [
|
|
1184
|
+
{
|
|
1185
|
+
type: "text",
|
|
1186
|
+
text: `📋 **OpenSpec 규칙 로드 결과**
|
|
1187
|
+
|
|
1188
|
+
## 발견된 규칙
|
|
1189
|
+
없음
|
|
1190
|
+
|
|
1191
|
+
## 탐지 경로
|
|
1192
|
+
${OPENSPEC_SEARCH_PATHS.map(p => `- ${path.join(PROJECT_ROOT, p)}`).join("\n")}
|
|
1193
|
+
|
|
1194
|
+
## 대안
|
|
1195
|
+
- 환경변수 RULES_PATHS로 규칙 파일 지정
|
|
1196
|
+
- \`d2c_get_design_rules\`로 기본 규칙 사용
|
|
1197
|
+
|
|
1198
|
+
💡 프로젝트에 OpenSpec 규칙을 추가하려면:
|
|
1199
|
+
\`\`\`
|
|
1200
|
+
mkdir -p openspec/specs/figma-standard
|
|
1201
|
+
touch openspec/specs/figma-standard/spec.md
|
|
1202
|
+
\`\`\``,
|
|
1203
|
+
},
|
|
1204
|
+
],
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const rulesText = filteredRules.map(rule => {
|
|
1208
|
+
const reqList = rule.requirements.map(req => {
|
|
1209
|
+
const scenarioCount = req.scenarios.length;
|
|
1210
|
+
return ` - ${req.name} (${scenarioCount}개 시나리오)`;
|
|
1211
|
+
}).join("\n");
|
|
1212
|
+
return `### ${rule.specName}
|
|
1213
|
+
- 경로: \`${rule.filePath}\`
|
|
1214
|
+
- Requirements (${rule.requirements.length}개):
|
|
1215
|
+
${reqList}`;
|
|
1216
|
+
}).join("\n\n");
|
|
1217
|
+
return {
|
|
1218
|
+
content: [
|
|
1219
|
+
{
|
|
1220
|
+
type: "text",
|
|
1221
|
+
text: `📋 **OpenSpec 규칙 로드 결과**
|
|
1222
|
+
|
|
1223
|
+
## 발견된 규칙 (${filteredRules.length}개)
|
|
1224
|
+
|
|
1225
|
+
${rulesText}
|
|
1226
|
+
|
|
1227
|
+
## 사용법
|
|
1228
|
+
1. \`d2c_get_workflow_tasks\`로 체크리스트에서 규칙 확인
|
|
1229
|
+
2. \`d2c_validate_against_spec\`로 코드 검증
|
|
1230
|
+
3. 각 Phase에서 규칙 준수 여부 자동 확인`,
|
|
1231
|
+
},
|
|
1232
|
+
],
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
case "d2c_get_workflow_tasks": {
|
|
1236
|
+
const input = z
|
|
1237
|
+
.object({
|
|
1238
|
+
phase: z.number(),
|
|
1239
|
+
completedTasks: z.array(z.string()).optional().default([]),
|
|
1240
|
+
includeRules: z.boolean().optional().default(true),
|
|
1241
|
+
})
|
|
1242
|
+
.parse(args);
|
|
1243
|
+
const phaseInfo = PHASE_TASKS[input.phase];
|
|
1244
|
+
if (!phaseInfo) {
|
|
1245
|
+
throw new Error(`Invalid phase: ${input.phase}. Must be 1, 2, or 3.`);
|
|
1246
|
+
}
|
|
1247
|
+
// 체크리스트 생성
|
|
1248
|
+
const taskList = phaseInfo.tasks.map(task => {
|
|
1249
|
+
const isCompleted = input.completedTasks.includes(task.id);
|
|
1250
|
+
return `- [${isCompleted ? "x" : " "}] ${task.id} ${task.content}`;
|
|
1251
|
+
}).join("\n");
|
|
1252
|
+
// 완료율 계산
|
|
1253
|
+
const completedCount = phaseInfo.tasks.filter(t => input.completedTasks.includes(t.id)).length;
|
|
1254
|
+
const totalCount = phaseInfo.tasks.length;
|
|
1255
|
+
const progressPercent = Math.round((completedCount / totalCount) * 100);
|
|
1256
|
+
// OpenSpec 규칙 섹션
|
|
1257
|
+
let rulesSection = "";
|
|
1258
|
+
if (input.includeRules) {
|
|
1259
|
+
const rules = await loadOpenSpecRules();
|
|
1260
|
+
if (rules.length > 0) {
|
|
1261
|
+
const rulesList = rules.map(rule => {
|
|
1262
|
+
const keyReqs = rule.requirements.slice(0, 3).map(r => r.name).join(", ");
|
|
1263
|
+
return `- **${rule.specName}**: ${keyReqs}${rule.requirements.length > 3 ? " 외 " + (rule.requirements.length - 3) + "개" : ""}`;
|
|
1264
|
+
}).join("\n");
|
|
1265
|
+
rulesSection = `\n### 적용 규칙\n${rulesList}\n`;
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
rulesSection = `\n### 적용 규칙\n- (없음) 기본 규칙 사용\n`;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return {
|
|
1272
|
+
content: [
|
|
1273
|
+
{
|
|
1274
|
+
type: "text",
|
|
1275
|
+
text: `## ${phaseInfo.name} (목표 ${phaseInfo.target}%)
|
|
1276
|
+
|
|
1277
|
+
### 진행률: ${progressPercent}% (${completedCount}/${totalCount})
|
|
1278
|
+
${"█".repeat(Math.round(progressPercent / 10))}${"░".repeat(10 - Math.round(progressPercent / 10))}
|
|
1279
|
+
|
|
1280
|
+
### Tasks
|
|
1281
|
+
${taskList}
|
|
1282
|
+
${rulesSection}
|
|
1283
|
+
### 다음 단계
|
|
1284
|
+
${completedCount === totalCount
|
|
1285
|
+
? `✅ Phase ${input.phase} 완료! ${input.phase < 3 ? `Phase ${input.phase + 1}로 진행하세요.` : "워크플로우 완료!"}`
|
|
1286
|
+
: `➡️ ${phaseInfo.tasks.find(t => !input.completedTasks.includes(t.id))?.id} ${phaseInfo.tasks.find(t => !input.completedTasks.includes(t.id))?.content} 진행`}`,
|
|
1287
|
+
},
|
|
1288
|
+
],
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
case "d2c_validate_against_spec": {
|
|
1292
|
+
const input = z
|
|
1293
|
+
.object({
|
|
1294
|
+
code: z.string(),
|
|
1295
|
+
specName: z.string().optional(),
|
|
1296
|
+
componentName: z.string().optional(),
|
|
1297
|
+
})
|
|
1298
|
+
.parse(args);
|
|
1299
|
+
const rules = await loadOpenSpecRules();
|
|
1300
|
+
let targetRules = rules;
|
|
1301
|
+
if (input.specName) {
|
|
1302
|
+
targetRules = rules.filter(r => r.specName === input.specName);
|
|
1303
|
+
}
|
|
1304
|
+
const results = [];
|
|
1305
|
+
// 기본 검증 규칙 (항상 적용)
|
|
1306
|
+
const code = input.code;
|
|
1307
|
+
const componentName = input.componentName || "Component";
|
|
1308
|
+
// 1. PascalCase 컴포넌트 네이밍
|
|
1309
|
+
if (componentName && /^[A-Z][a-zA-Z0-9]*$/.test(componentName)) {
|
|
1310
|
+
results.push({
|
|
1311
|
+
specName: "default",
|
|
1312
|
+
requirement: "컴포넌트 네이밍 규칙",
|
|
1313
|
+
status: "pass",
|
|
1314
|
+
message: `${componentName}은(는) PascalCase 준수`,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
else if (componentName) {
|
|
1318
|
+
results.push({
|
|
1319
|
+
specName: "default",
|
|
1320
|
+
requirement: "컴포넌트 네이밍 규칙",
|
|
1321
|
+
status: "fail",
|
|
1322
|
+
message: `${componentName}은(는) PascalCase가 아님. 권장: ${componentName.split(/[-_]/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("")}`,
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
// 2. Props 인터페이스
|
|
1326
|
+
if (code.includes("interface") && code.includes("Props")) {
|
|
1327
|
+
results.push({
|
|
1328
|
+
specName: "default",
|
|
1329
|
+
requirement: "Props 인터페이스 정의",
|
|
1330
|
+
status: "pass",
|
|
1331
|
+
message: "TypeScript Props 인터페이스 정의됨",
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
else if (code.includes(": {") || code.includes("Props")) {
|
|
1335
|
+
results.push({
|
|
1336
|
+
specName: "default",
|
|
1337
|
+
requirement: "Props 인터페이스 정의",
|
|
1338
|
+
status: "warn",
|
|
1339
|
+
message: "Props 타입이 있으나 명시적 인터페이스 권장",
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
results.push({
|
|
1344
|
+
specName: "default",
|
|
1345
|
+
requirement: "Props 인터페이스 정의",
|
|
1346
|
+
status: "fail",
|
|
1347
|
+
message: "Props 인터페이스가 없음. interface ComponentProps {} 추가 권장",
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
// 3. 접근성
|
|
1351
|
+
const a11yPatterns = ["aria-", "role=", "tabIndex", "alt="];
|
|
1352
|
+
const hasA11y = a11yPatterns.some(p => code.includes(p));
|
|
1353
|
+
results.push({
|
|
1354
|
+
specName: "default",
|
|
1355
|
+
requirement: "접근성 속성",
|
|
1356
|
+
status: hasA11y ? "pass" : "warn",
|
|
1357
|
+
message: hasA11y ? "접근성 속성 포함됨" : "aria-*, role 속성 추가 권장",
|
|
1358
|
+
});
|
|
1359
|
+
// OpenSpec 규칙 기반 검증
|
|
1360
|
+
for (const rule of targetRules) {
|
|
1361
|
+
for (const req of rule.requirements) {
|
|
1362
|
+
// 키워드 기반 간단한 검증
|
|
1363
|
+
const keywords = req.name.toLowerCase().split(/\s+/);
|
|
1364
|
+
let matched = false;
|
|
1365
|
+
let status = "warn";
|
|
1366
|
+
// 네이밍 관련
|
|
1367
|
+
if (keywords.some(k => ["naming", "네이밍", "이름"].includes(k))) {
|
|
1368
|
+
if (/^[A-Z][a-zA-Z0-9]*$/.test(componentName || "")) {
|
|
1369
|
+
matched = true;
|
|
1370
|
+
status = "pass";
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
// Props 관련
|
|
1374
|
+
if (keywords.some(k => ["props", "인터페이스", "interface"].includes(k))) {
|
|
1375
|
+
if (code.includes("interface") && code.includes("Props")) {
|
|
1376
|
+
matched = true;
|
|
1377
|
+
status = "pass";
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
// 접근성 관련
|
|
1381
|
+
if (keywords.some(k => ["접근성", "a11y", "accessibility", "aria"].includes(k))) {
|
|
1382
|
+
if (hasA11y) {
|
|
1383
|
+
matched = true;
|
|
1384
|
+
status = "pass";
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (!matched) {
|
|
1388
|
+
results.push({
|
|
1389
|
+
specName: rule.specName,
|
|
1390
|
+
requirement: req.name,
|
|
1391
|
+
status: "warn",
|
|
1392
|
+
message: `검증 필요: ${req.description || req.name}`,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
results.push({
|
|
1397
|
+
specName: rule.specName,
|
|
1398
|
+
requirement: req.name,
|
|
1399
|
+
status,
|
|
1400
|
+
message: status === "pass" ? "규칙 준수" : "검토 필요",
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// 결과 집계
|
|
1406
|
+
const passCount = results.filter(r => r.status === "pass").length;
|
|
1407
|
+
const failCount = results.filter(r => r.status === "fail").length;
|
|
1408
|
+
const warnCount = results.filter(r => r.status === "warn").length;
|
|
1409
|
+
const totalCount = results.length;
|
|
1410
|
+
const passRate = Math.round((passCount / totalCount) * 100);
|
|
1411
|
+
const statusIcon = (s) => s === "pass" ? "✅" : s === "fail" ? "❌" : "⚠️";
|
|
1412
|
+
const resultText = results.map(r => `${statusIcon(r.status)} **${r.requirement}** (${r.specName})\n ${r.message}`).join("\n\n");
|
|
1413
|
+
return {
|
|
1414
|
+
content: [
|
|
1415
|
+
{
|
|
1416
|
+
type: "text",
|
|
1417
|
+
text: `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1418
|
+
📋 **OpenSpec 규칙 검증 결과**
|
|
1419
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1420
|
+
|
|
1421
|
+
## 요약
|
|
1422
|
+
- 통과: ${passCount}개 ✅
|
|
1423
|
+
- 실패: ${failCount}개 ❌
|
|
1424
|
+
- 경고: ${warnCount}개 ⚠️
|
|
1425
|
+
- **준수율: ${passRate}%**
|
|
1426
|
+
|
|
1427
|
+
${"█".repeat(Math.round(passRate / 10))}${"░".repeat(10 - Math.round(passRate / 10))} ${passRate}%
|
|
1428
|
+
|
|
1429
|
+
## 상세 결과
|
|
1430
|
+
|
|
1431
|
+
${resultText}
|
|
1432
|
+
|
|
1433
|
+
${failCount > 0 ? `\n## 수정 필요 항목\n${results.filter(r => r.status === "fail").map(r => `- ${r.requirement}: ${r.message}`).join("\n")}` : ""}
|
|
1434
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
1435
|
+
},
|
|
1436
|
+
],
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1160
1439
|
case "d2c_get_component_template": {
|
|
1161
1440
|
const input = z
|
|
1162
1441
|
.object({
|
|
@@ -1347,6 +1626,13 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
1347
1626
|
|
|
1348
1627
|
---
|
|
1349
1628
|
|
|
1629
|
+
### 🔰 Step 0: OpenSpec 규칙 로드
|
|
1630
|
+
1. **\`d2c_load_openspec_rules\`** 호출하여 프로젝트 규칙 확인
|
|
1631
|
+
2. 발견된 규칙(예: figma-standard, design-rules)을 워크플로우에 적용
|
|
1632
|
+
3. 규칙이 없으면 기본 규칙 사용
|
|
1633
|
+
|
|
1634
|
+
---
|
|
1635
|
+
|
|
1350
1636
|
### Step 1: 사전 검사
|
|
1351
1637
|
1. \`d2c_log_step(step:1, stepName:"사전 검사", status:"start")\`
|
|
1352
1638
|
2. \`d2c_preflight_check\` 호출
|
|
@@ -1362,46 +1648,52 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
1362
1648
|
---
|
|
1363
1649
|
|
|
1364
1650
|
### 🔄 Phase 1: Figma MCP 추출 (목표 60%)
|
|
1365
|
-
1.
|
|
1366
|
-
2. \`
|
|
1367
|
-
3.
|
|
1368
|
-
4.
|
|
1369
|
-
5. \`playwright-mcp.
|
|
1370
|
-
6.
|
|
1371
|
-
7.
|
|
1372
|
-
8.
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1651
|
+
1. **\`d2c_get_workflow_tasks(phase:1)\`**로 체크리스트 확인
|
|
1652
|
+
2. \`d2c_log_step(step:3, stepName:"Phase 1", status:"start", iteration:1)\`
|
|
1653
|
+
3. \`d2c_get_component_template\`로 템플릿 생성
|
|
1654
|
+
4. **Figma MCP로 코드 추출/수정**
|
|
1655
|
+
5. \`playwright-mcp.browser_navigate\`로 렌더링
|
|
1656
|
+
6. \`playwright-mcp.browser_screenshot\`으로 스크린샷
|
|
1657
|
+
7. **Playwright toHaveScreenshot()으로 비교하여 성공률 계산**
|
|
1658
|
+
8. **\`d2c_phase1_compare\`** 호출 (successRate, iteration 필수!)
|
|
1659
|
+
9. **\`d2c_validate_against_spec\`**로 OpenSpec 규칙 검증
|
|
1660
|
+
10. **HITL 확인**: 사용자 응답에 따라:
|
|
1661
|
+
- [Y] → 60% 미달이면 반복, 달성이면 Phase 2로
|
|
1662
|
+
- [M] → 수동 수정 후 재비교
|
|
1663
|
+
- [N] → 현재 상태로 다음 단계
|
|
1664
|
+
11. \`d2c_log_step(step:3, stepName:"Phase 1", status:"done")\`
|
|
1377
1665
|
|
|
1378
1666
|
---
|
|
1379
1667
|
|
|
1380
1668
|
### 🔄 Phase 2: LLM 이미지 Diff (목표 70%)
|
|
1381
|
-
1.
|
|
1382
|
-
2.
|
|
1383
|
-
3.
|
|
1384
|
-
4.
|
|
1385
|
-
5.
|
|
1386
|
-
6.
|
|
1669
|
+
1. **\`d2c_get_workflow_tasks(phase:2)\`**로 체크리스트 확인
|
|
1670
|
+
2. \`d2c_log_step(step:4, stepName:"Phase 2", status:"start", iteration:1)\`
|
|
1671
|
+
3. **Playwright 이미지 diff 분석**
|
|
1672
|
+
4. diff 결과 기반으로 **LLM이 코드 수정**
|
|
1673
|
+
5. 렌더링 후 스크린샷 비교
|
|
1674
|
+
6. **\`d2c_phase2_image_diff\`** 호출 (successRate, diffAreas 포함!)
|
|
1675
|
+
7. **\`d2c_validate_against_spec\`**로 OpenSpec 규칙 검증
|
|
1676
|
+
8. **HITL 확인**: 사용자 응답에 따라:
|
|
1387
1677
|
- [Y] → 70% 미달이면 LLM 수정 반복, 달성이면 Phase 3로
|
|
1388
1678
|
- [M] → 수동 수정 후 재비교
|
|
1389
1679
|
- [N] → 현재 상태로 다음 단계
|
|
1390
|
-
|
|
1680
|
+
9. \`d2c_log_step(step:4, stepName:"Phase 2", status:"done")\`
|
|
1391
1681
|
|
|
1392
1682
|
---
|
|
1393
1683
|
|
|
1394
1684
|
### 🔄 Phase 3: LLM DOM 비교 (목표 90%)
|
|
1395
|
-
1.
|
|
1396
|
-
2.
|
|
1397
|
-
3. DOM
|
|
1398
|
-
4.
|
|
1399
|
-
5.
|
|
1400
|
-
6.
|
|
1685
|
+
1. **\`d2c_get_workflow_tasks(phase:3)\`**로 체크리스트 확인
|
|
1686
|
+
2. \`d2c_log_step(step:5, stepName:"Phase 3", status:"start", iteration:1)\`
|
|
1687
|
+
3. **Playwright DOM 스냅샷 비교**
|
|
1688
|
+
4. DOM 차이 기반으로 **LLM이 코드 수정**
|
|
1689
|
+
5. 렌더링 후 DOM 비교
|
|
1690
|
+
6. **\`d2c_phase3_dom_compare\`** 호출 (successRate, domDiffs 포함!)
|
|
1691
|
+
7. **\`d2c_validate_against_spec\`**로 OpenSpec 규칙 최종 검증
|
|
1692
|
+
8. **HITL 확인**: 사용자 응답에 따라:
|
|
1401
1693
|
- [Y] → 90% 미달이면 LLM 수정 반복, 달성이면 완료
|
|
1402
1694
|
- [M] → 수동 수정 후 재비교
|
|
1403
1695
|
- [N] → 현재 상태로 완료
|
|
1404
|
-
|
|
1696
|
+
9. \`d2c_log_step(step:5, stepName:"Phase 3", status:"done")\`
|
|
1405
1697
|
|
|
1406
1698
|
---
|
|
1407
1699
|
|
|
@@ -1414,10 +1706,22 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
1414
1706
|
---
|
|
1415
1707
|
|
|
1416
1708
|
**⚠️ 중요 규칙**:
|
|
1709
|
+
- **워크플로우 시작 시 \`d2c_load_openspec_rules\`로 규칙 로드**
|
|
1710
|
+
- **각 Phase에서 \`d2c_get_workflow_tasks\`로 체크리스트 확인**
|
|
1711
|
+
- **코드 수정 후 \`d2c_validate_against_spec\`로 규칙 검증**
|
|
1417
1712
|
- 매 Phase마다 **반드시 HITL 확인** (사용자에게 계속 여부 질문)
|
|
1418
1713
|
- 모든 Phase에서 사용자가 수동 수정 가능 ([M] 옵션)
|
|
1419
1714
|
- 성공률은 Playwright 비교 결과를 기반으로 객관적으로 측정
|
|
1420
|
-
- \`d2c_workflow_status\`로 언제든 전체 진행 상황 확인
|
|
1715
|
+
- \`d2c_workflow_status\`로 언제든 전체 진행 상황 확인 가능
|
|
1716
|
+
|
|
1717
|
+
---
|
|
1718
|
+
|
|
1719
|
+
### 📋 OpenSpec 도구 사용법
|
|
1720
|
+
| 도구 | 용도 | 호출 시점 |
|
|
1721
|
+
|------|------|----------|
|
|
1722
|
+
| \`d2c_load_openspec_rules\` | 프로젝트 규칙 로드 | 워크플로우 시작 시 |
|
|
1723
|
+
| \`d2c_get_workflow_tasks\` | Phase별 체크리스트 | 각 Phase 시작 시 |
|
|
1724
|
+
| \`d2c_validate_against_spec\` | 규칙 준수 검증 | 코드 수정 후 |`,
|
|
1421
1725
|
},
|
|
1422
1726
|
},
|
|
1423
1727
|
],
|
|
@@ -1489,9 +1793,10 @@ export default Component;
|
|
|
1489
1793
|
async function main() {
|
|
1490
1794
|
const transport = new StdioServerTransport();
|
|
1491
1795
|
await server.connect(transport);
|
|
1492
|
-
console.error("SYR D2C Workflow MCP server running on stdio (v0.
|
|
1796
|
+
console.error("SYR D2C Workflow MCP server running on stdio (v0.4.0)");
|
|
1493
1797
|
console.error(` Rules paths: ${RULES_PATHS.join(", ") || "(none)"}`);
|
|
1494
1798
|
console.error(` Rules glob: ${RULES_GLOB || "(none)"}`);
|
|
1799
|
+
console.error(` OpenSpec paths: ${OPENSPEC_SEARCH_PATHS.map(p => path.join(PROJECT_ROOT, p)).join(", ")}`);
|
|
1495
1800
|
}
|
|
1496
1801
|
main().catch((error) => {
|
|
1497
1802
|
console.error("Fatal error:", error);
|