@simplysm/sd-claude 13.0.77 → 13.0.80
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/claude/rules/sd-claude-rules.md +4 -63
- package/claude/rules/sd-simplysm-usage.md +7 -0
- package/claude/sd-session-start.sh +10 -0
- package/claude/skills/sd-api-review/SKILL.md +89 -0
- package/claude/skills/sd-check/SKILL.md +55 -57
- package/claude/skills/sd-commit/SKILL.md +37 -42
- package/claude/skills/sd-debug/SKILL.md +75 -265
- package/claude/skills/sd-document/SKILL.md +63 -53
- package/claude/skills/sd-document/_common.py +94 -0
- package/claude/skills/sd-document/extract_docx.py +19 -48
- package/claude/skills/sd-document/extract_pdf.py +22 -50
- package/claude/skills/sd-document/extract_pptx.py +17 -40
- package/claude/skills/sd-document/extract_xlsx.py +19 -40
- package/claude/skills/sd-email-analyze/SKILL.md +23 -31
- package/claude/skills/sd-email-analyze/email-analyzer.py +79 -65
- package/claude/skills/sd-init/SKILL.md +133 -0
- package/claude/skills/sd-plan/SKILL.md +69 -120
- package/claude/skills/sd-readme/SKILL.md +106 -131
- package/claude/skills/sd-review/SKILL.md +38 -155
- package/claude/skills/sd-simplify/SKILL.md +59 -0
- package/package.json +3 -2
- package/README.md +0 -297
- package/claude/refs/sd-angular.md +0 -127
- package/claude/refs/sd-code-conventions.md +0 -155
- package/claude/refs/sd-directories.md +0 -7
- package/claude/refs/sd-library-issue.md +0 -7
- package/claude/refs/sd-migration.md +0 -7
- package/claude/refs/sd-orm-v12.md +0 -81
- package/claude/refs/sd-orm.md +0 -23
- package/claude/refs/sd-service.md +0 -5
- package/claude/refs/sd-simplysm-docs.md +0 -52
- package/claude/refs/sd-solid.md +0 -68
- package/claude/refs/sd-workflow.md +0 -25
- package/claude/rules/sd-refs-linker.md +0 -52
- package/claude/sd-statusline.js +0 -296
- package/claude/skills/sd-api-name-review/SKILL.md +0 -154
- package/claude/skills/sd-brainstorm/SKILL.md +0 -215
- package/claude/skills/sd-debug/condition-based-waiting-example.ts +0 -158
- package/claude/skills/sd-debug/condition-based-waiting.md +0 -114
- package/claude/skills/sd-debug/defense-in-depth.md +0 -128
- package/claude/skills/sd-debug/find-polluter.sh +0 -64
- package/claude/skills/sd-debug/root-cause-tracing.md +0 -168
- package/claude/skills/sd-discuss/SKILL.md +0 -91
- package/claude/skills/sd-explore/SKILL.md +0 -118
- package/claude/skills/sd-plan-dev/SKILL.md +0 -294
- package/claude/skills/sd-plan-dev/code-quality-reviewer-prompt.md +0 -49
- package/claude/skills/sd-plan-dev/final-review-prompt.md +0 -50
- package/claude/skills/sd-plan-dev/implementer-prompt.md +0 -60
- package/claude/skills/sd-plan-dev/spec-reviewer-prompt.md +0 -45
- package/claude/skills/sd-review/api-reviewer-prompt.md +0 -75
- package/claude/skills/sd-review/code-reviewer-prompt.md +0 -82
- package/claude/skills/sd-review/convention-checker-prompt.md +0 -61
- package/claude/skills/sd-review/refactoring-analyzer-prompt.md +0 -92
- package/claude/skills/sd-skill/SKILL.md +0 -417
- package/claude/skills/sd-skill/anthropic-best-practices.md +0 -156
- package/claude/skills/sd-skill/cso-guide.md +0 -161
- package/claude/skills/sd-skill/examples/CLAUDE_MD_TESTING.md +0 -200
- package/claude/skills/sd-skill/persuasion-principles.md +0 -220
- package/claude/skills/sd-skill/testing-skills-with-subagents.md +0 -408
- package/claude/skills/sd-skill/writing-guide.md +0 -159
- package/claude/skills/sd-tdd/SKILL.md +0 -385
- package/claude/skills/sd-tdd/testing-anti-patterns.md +0 -317
- package/claude/skills/sd-use/SKILL.md +0 -67
- package/claude/skills/sd-worktree/SKILL.md +0 -78
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import sys
|
|
5
5
|
import os
|
|
6
6
|
import io
|
|
7
|
-
import subprocess
|
|
8
7
|
import email
|
|
9
8
|
import html
|
|
10
9
|
import re
|
|
@@ -19,6 +18,7 @@ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="repla
|
|
|
19
18
|
|
|
20
19
|
def ensure_packages():
|
|
21
20
|
"""Auto-install required packages."""
|
|
21
|
+
import subprocess
|
|
22
22
|
packages = {"extract-msg": "extract_msg"}
|
|
23
23
|
missing = []
|
|
24
24
|
for pip_name, import_name in packages.items():
|
|
@@ -35,11 +35,6 @@ def ensure_packages():
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
ensure_packages()
|
|
39
|
-
|
|
40
|
-
import extract_msg # noqa: E402
|
|
41
|
-
|
|
42
|
-
|
|
43
38
|
# ── Korean charset helpers ──────────────────────────────────────────
|
|
44
39
|
|
|
45
40
|
KOREAN_CHARSET_MAP = {
|
|
@@ -81,39 +76,38 @@ def _parse_eml(filepath):
|
|
|
81
76
|
|
|
82
77
|
body_plain = ""
|
|
83
78
|
body_html = ""
|
|
79
|
+
attachments = []
|
|
80
|
+
inline_images = []
|
|
81
|
+
seen_cids = set()
|
|
84
82
|
|
|
85
|
-
if msg.is_multipart():
|
|
86
|
-
for part in msg.walk():
|
|
87
|
-
ctype = part.get_content_type()
|
|
88
|
-
cdisp = part.get_content_disposition()
|
|
89
|
-
if cdisp == "attachment":
|
|
90
|
-
continue
|
|
91
|
-
if ctype == "text/plain" and not body_plain:
|
|
92
|
-
body_plain = _get_text_eml(part)
|
|
93
|
-
elif ctype == "text/html" and not body_html:
|
|
94
|
-
body_html = _get_text_eml(part)
|
|
95
|
-
else:
|
|
83
|
+
if not msg.is_multipart():
|
|
96
84
|
ctype = msg.get_content_type()
|
|
97
85
|
if ctype == "text/html":
|
|
98
86
|
body_html = _get_text_eml(msg)
|
|
99
87
|
else:
|
|
100
88
|
body_plain = _get_text_eml(msg)
|
|
101
|
-
|
|
102
|
-
attachments = []
|
|
103
|
-
inline_images = []
|
|
104
|
-
seen_cids = set()
|
|
89
|
+
return headers, body_plain, body_html, attachments, inline_images
|
|
105
90
|
|
|
106
91
|
for part in msg.walk():
|
|
92
|
+
ctype = part.get_content_type()
|
|
93
|
+
cdisp = part.get_content_disposition()
|
|
107
94
|
payload = part.get_payload(decode=True)
|
|
95
|
+
|
|
96
|
+
# Body extraction (non-attachment text parts)
|
|
97
|
+
if cdisp != "attachment" and payload is not None:
|
|
98
|
+
if ctype == "text/plain" and not body_plain:
|
|
99
|
+
body_plain = _get_text_eml(part)
|
|
100
|
+
elif ctype == "text/html" and not body_html:
|
|
101
|
+
body_html = _get_text_eml(part)
|
|
102
|
+
|
|
108
103
|
if payload is None:
|
|
109
104
|
continue
|
|
110
105
|
|
|
111
106
|
content_id = (part.get("Content-ID") or "").strip("<> ")
|
|
112
|
-
ctype = part.get_content_type()
|
|
113
107
|
filename = part.get_filename()
|
|
114
108
|
|
|
115
|
-
# Inline image
|
|
116
|
-
if content_id
|
|
109
|
+
# Inline image
|
|
110
|
+
if _is_inline_image(content_id, ctype):
|
|
117
111
|
if content_id not in seen_cids:
|
|
118
112
|
seen_cids.add(content_id)
|
|
119
113
|
ext = _guess_image_ext(ctype, filename)
|
|
@@ -126,10 +120,9 @@ def _parse_eml(filepath):
|
|
|
126
120
|
})
|
|
127
121
|
continue
|
|
128
122
|
|
|
129
|
-
# Regular attachment
|
|
123
|
+
# Regular attachment
|
|
130
124
|
if not filename:
|
|
131
125
|
continue
|
|
132
|
-
cdisp = part.get_content_disposition()
|
|
133
126
|
if cdisp not in ("attachment", "inline", None):
|
|
134
127
|
continue
|
|
135
128
|
attachments.append({
|
|
@@ -143,6 +136,9 @@ def _parse_eml(filepath):
|
|
|
143
136
|
|
|
144
137
|
|
|
145
138
|
def _parse_msg(filepath):
|
|
139
|
+
ensure_packages()
|
|
140
|
+
import extract_msg
|
|
141
|
+
|
|
146
142
|
msg = extract_msg.openMsg(filepath)
|
|
147
143
|
try:
|
|
148
144
|
headers = {
|
|
@@ -186,7 +182,7 @@ def _parse_msg(filepath):
|
|
|
186
182
|
"data": data,
|
|
187
183
|
}
|
|
188
184
|
|
|
189
|
-
if cid
|
|
185
|
+
if _is_inline_image(cid, mimetype):
|
|
190
186
|
entry["content_id"] = cid.strip("<> ")
|
|
191
187
|
inline_images.append(entry)
|
|
192
188
|
else:
|
|
@@ -224,6 +220,25 @@ def _guess_image_ext(content_type, filename=None):
|
|
|
224
220
|
return mapping.get(content_type, ".bin")
|
|
225
221
|
|
|
226
222
|
|
|
223
|
+
def _is_inline_image(content_id, content_type):
|
|
224
|
+
"""Check if a MIME part is an inline image (has Content-ID + image MIME type)."""
|
|
225
|
+
return bool(content_id) and content_type.startswith("image/")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ── Pre-compiled regexes ───────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
_RE_DATA_URI = re.compile(
|
|
231
|
+
r'<img[^>]+src=["\']data:image/([^;]+);base64,([^"\']+)["\']',
|
|
232
|
+
re.IGNORECASE,
|
|
233
|
+
)
|
|
234
|
+
_RE_STYLE = re.compile(r"<style[^>]*>.*?</style>", re.DOTALL | re.I)
|
|
235
|
+
_RE_SCRIPT = re.compile(r"<script[^>]*>.*?</script>", re.DOTALL | re.I)
|
|
236
|
+
_RE_BR = re.compile(r"<br\s*/?>", re.I)
|
|
237
|
+
_RE_BLOCK_CLOSE = re.compile(r"</(?:p|div|tr|li)>", re.I)
|
|
238
|
+
_RE_TAG = re.compile(r"<[^>]+>")
|
|
239
|
+
_RE_MULTI_NEWLINE = re.compile(r"\n{3,}")
|
|
240
|
+
|
|
241
|
+
|
|
227
242
|
# ── File saving ────────────────────────────────────────────────────
|
|
228
243
|
|
|
229
244
|
|
|
@@ -232,10 +247,10 @@ def save_files(files, output_dir):
|
|
|
232
247
|
os.makedirs(output_dir, exist_ok=True)
|
|
233
248
|
result = []
|
|
234
249
|
for f in files:
|
|
250
|
+
stem = Path(f["filename"]).stem
|
|
251
|
+
ext = Path(f["filename"]).suffix
|
|
235
252
|
filepath = os.path.join(output_dir, f["filename"])
|
|
236
253
|
if os.path.exists(filepath):
|
|
237
|
-
stem = Path(filepath).stem
|
|
238
|
-
ext = Path(filepath).suffix
|
|
239
254
|
n = 1
|
|
240
255
|
while os.path.exists(filepath):
|
|
241
256
|
filepath = os.path.join(output_dir, f"{stem}_{n}{ext}")
|
|
@@ -248,44 +263,41 @@ def save_files(files, output_dir):
|
|
|
248
263
|
|
|
249
264
|
def extract_data_uri_images(html_body, output_dir):
|
|
250
265
|
"""Extract base64 data URI images embedded in HTML body."""
|
|
251
|
-
|
|
252
|
-
matches = re.findall(pattern, html_body, re.IGNORECASE)
|
|
266
|
+
matches = _RE_DATA_URI.findall(html_body)
|
|
253
267
|
if not matches:
|
|
254
268
|
return []
|
|
255
269
|
|
|
256
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
257
270
|
images = []
|
|
258
271
|
for i, (img_type, b64data) in enumerate(matches, 1):
|
|
259
272
|
try:
|
|
260
273
|
data = base64.b64decode(b64data)
|
|
261
|
-
|
|
262
|
-
ext =
|
|
263
|
-
filename = f"datauri_{i}{ext}"
|
|
264
|
-
filepath = os.path.join(output_dir, filename)
|
|
265
|
-
with open(filepath, "wb") as f:
|
|
266
|
-
f.write(data)
|
|
274
|
+
content_type = f"image/{img_type}"
|
|
275
|
+
ext = _guess_image_ext(content_type)
|
|
267
276
|
images.append({
|
|
268
|
-
"filename":
|
|
269
|
-
"content_type":
|
|
277
|
+
"filename": f"datauri_{i}{ext}",
|
|
278
|
+
"content_type": content_type,
|
|
270
279
|
"size": len(data),
|
|
271
|
-
"
|
|
280
|
+
"data": data,
|
|
272
281
|
})
|
|
273
282
|
except Exception:
|
|
274
283
|
continue
|
|
275
|
-
|
|
284
|
+
|
|
285
|
+
if not images:
|
|
286
|
+
return []
|
|
287
|
+
return save_files(images, output_dir)
|
|
276
288
|
|
|
277
289
|
|
|
278
290
|
# ── HTML stripping ──────────────────────────────────────────────────
|
|
279
291
|
|
|
280
292
|
|
|
281
293
|
def strip_html(text):
|
|
282
|
-
text =
|
|
283
|
-
text =
|
|
284
|
-
text =
|
|
285
|
-
text =
|
|
286
|
-
text =
|
|
294
|
+
text = _RE_STYLE.sub("", text)
|
|
295
|
+
text = _RE_SCRIPT.sub("", text)
|
|
296
|
+
text = _RE_BR.sub("\n", text)
|
|
297
|
+
text = _RE_BLOCK_CLOSE.sub("\n", text)
|
|
298
|
+
text = _RE_TAG.sub("", text)
|
|
287
299
|
text = html.unescape(text)
|
|
288
|
-
text =
|
|
300
|
+
text = _RE_MULTI_NEWLINE.sub("\n\n", text)
|
|
289
301
|
return text.strip()
|
|
290
302
|
|
|
291
303
|
|
|
@@ -303,6 +315,23 @@ def fmt_size(n):
|
|
|
303
315
|
# ── Markdown report ─────────────────────────────────────────────────
|
|
304
316
|
|
|
305
317
|
|
|
318
|
+
def _render_file_table(title, items):
|
|
319
|
+
"""Render a Markdown table of files with #, Filename, Size, Saved path columns."""
|
|
320
|
+
if not items:
|
|
321
|
+
return []
|
|
322
|
+
lines = [
|
|
323
|
+
f"## {title}\n",
|
|
324
|
+
"| # | Filename | Size | Saved path |",
|
|
325
|
+
"|---|--------|------|-----------|",
|
|
326
|
+
]
|
|
327
|
+
for i, item in enumerate(items, 1):
|
|
328
|
+
lines.append(
|
|
329
|
+
f"| {i} | {item['filename']} | {fmt_size(item['size'])} | `{item['saved_path']}` |"
|
|
330
|
+
)
|
|
331
|
+
lines.append("")
|
|
332
|
+
return lines
|
|
333
|
+
|
|
334
|
+
|
|
306
335
|
def build_report(filepath):
|
|
307
336
|
headers, body_plain, body_html, attachments, inline_images = parse_email(filepath)
|
|
308
337
|
|
|
@@ -352,23 +381,8 @@ def build_report(filepath):
|
|
|
352
381
|
out.append(body.strip() if body else "_(No body)_")
|
|
353
382
|
out.append("")
|
|
354
383
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
out.append("## Inline Images\n")
|
|
358
|
-
out.append("| # | Filename | Size | Saved path |")
|
|
359
|
-
out.append("|---|--------|------|-----------|")
|
|
360
|
-
for i, img in enumerate(all_inline, 1):
|
|
361
|
-
out.append(f"| {i} | {img['filename']} | {fmt_size(img['size'])} | `{img['saved_path']}` |")
|
|
362
|
-
out.append("")
|
|
363
|
-
|
|
364
|
-
# ── Attachments
|
|
365
|
-
if saved_attachments:
|
|
366
|
-
out.append("## Attachments\n")
|
|
367
|
-
out.append("| # | Filename | Size | Saved path |")
|
|
368
|
-
out.append("|---|--------|------|-----------|")
|
|
369
|
-
for i, a in enumerate(saved_attachments, 1):
|
|
370
|
-
out.append(f"| {i} | {a['filename']} | {fmt_size(a['size'])} | `{a['saved_path']}` |")
|
|
371
|
-
out.append("")
|
|
384
|
+
out.extend(_render_file_table("Inline Images", all_inline))
|
|
385
|
+
out.extend(_render_file_table("Attachments", saved_attachments))
|
|
372
386
|
|
|
373
387
|
return "\n".join(out)
|
|
374
388
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sd-init
|
|
3
|
+
description: "초기화", "init", "sd-init", "CLAUDE.md 생성" 등을 요청할 때 사용.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SD Init — CLAUDE.md 자동 생성
|
|
7
|
+
|
|
8
|
+
프로젝트를 분석하여 CLAUDE.md를 자동 생성한다. 기존 파일이 있으면 덮어쓴다.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Step 1: 패키지 매니저 감지
|
|
13
|
+
|
|
14
|
+
프로젝트 루트의 lock 파일로 PM을 판별하라:
|
|
15
|
+
|
|
16
|
+
1. `pnpm-lock.yaml` → pnpm
|
|
17
|
+
2. `yarn.lock` → yarn
|
|
18
|
+
3. 그외 → npm
|
|
19
|
+
|
|
20
|
+
## Step 2: 스크립트 분석
|
|
21
|
+
|
|
22
|
+
`package.json`의 `scripts`를 읽고, 각 스크립트가 호출하는 CLI 도구에 대해 Bash로 `--help`등의 도움말 보기를 실행하여 사용 가능한 인자와 플래그를 파악하라.
|
|
23
|
+
|
|
24
|
+
파악한 정보를 바탕으로 스크립트를 카테고리별(개발, 빌드, 테스트, 린트 등)로 그룹화하고, 각 스크립트의 기본 사용법과 주요 플래그 예시를 정리하라.
|
|
25
|
+
|
|
26
|
+
## Step 3: 코딩 규칙 분석
|
|
27
|
+
|
|
28
|
+
프로젝트 루트에서 아래 설정 파일들을 찾아 읽어라 (존재하는 것만):
|
|
29
|
+
|
|
30
|
+
- ESLint: `eslint.config.*`, `.eslintrc.*`, `packages/*/eslint.*` 등
|
|
31
|
+
- Prettier: `.prettierrc*`, `prettier.config.*`
|
|
32
|
+
- EditorConfig: `.editorconfig`
|
|
33
|
+
- TypeScript: `tsconfig.json` (루트)의 `compilerOptions` 중 `strict`, `noImplicitAny` 등 코드 스타일에 영향을 주는 옵션
|
|
34
|
+
- Stylelint: `.stylelintrc*`, `stylelint.config.*`
|
|
35
|
+
|
|
36
|
+
Claude가 규칙과 반대로 수정하기를 제안할 정도의 자주 실수할 내용들만 대폭 간결하게 정리하라.
|
|
37
|
+
|
|
38
|
+
## Step 4: CLAUDE.md 생성
|
|
39
|
+
|
|
40
|
+
아래 정보를 종합하여 프로젝트 루트에 `CLAUDE.md`를 작성하라:
|
|
41
|
+
|
|
42
|
+
- **프로젝트 정보**: `package.json`의 `name`, `description`
|
|
43
|
+
- **PM**: Step 1에서 감지한 패키지 매니저
|
|
44
|
+
- **모노레포 구조**: `workspaces` 필드 또는 `pnpm-workspace.yaml`이 있으면 워크스페이스 경로를 간단히 기술
|
|
45
|
+
- **기술스택**: `dependencies`/`devDependencies`에서 주요 기술(프레임워크, 번들러, 테스트 도구 등)을 파악하여 아주 간단히 기술
|
|
46
|
+
- **명령어**: Step 2에서 정리한 스크립트 사용법
|
|
47
|
+
- **코딩 규칙**: Step 3에서 분석한 규칙 중 Claude가 지켜야 할 것들. `## 코딩 규칙` 섹션으로 작성
|
|
48
|
+
|
|
49
|
+
### 참고 예시
|
|
50
|
+
|
|
51
|
+
아래는 잘 작성된 CLAUDE.md의 예시다. 형식을 그대로 복사하지 말고, 프로젝트 특성에 맞게 유연하게 작성하라.
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
# Simplysm
|
|
55
|
+
|
|
56
|
+
pnpm 모노레포. 패키지 경로: `packages/*`, 테스트: `tests/*`
|
|
57
|
+
|
|
58
|
+
## 명령어
|
|
59
|
+
|
|
60
|
+
모든 명령어는 내부적으로 `pnpm sd-cli <명령>`을 실행한다. 모든 명령에 `--debug` 플래그 사용 가능.
|
|
61
|
+
`[targets..]`를 지정하지 않으면 `sd.config.ts`에 정의된 전체 패키지 대상으로 실행된다.
|
|
62
|
+
타겟은 패키지 경로로 지정한다 (예: `packages/core-common`, `tests/orm`).
|
|
63
|
+
|
|
64
|
+
### 개발
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pnpm dev [targets..] # client+server 패키지 개발 모드 실행
|
|
68
|
+
pnpm dev packages/solid-demo # 특정 패키지만 dev 모드
|
|
69
|
+
pnpm dev -o key=value # sd.config.ts에 옵션 전달
|
|
70
|
+
|
|
71
|
+
pnpm watch [targets..] # 라이브러리 패키지 빌드 워치 모드
|
|
72
|
+
pnpm watch packages/core-common # 특정 패키지만 워치
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 빌드 & 배포
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpm build [targets..] # 프로덕션 빌드
|
|
79
|
+
pnpm build packages/solid # 특정 패키지만 빌드
|
|
80
|
+
|
|
81
|
+
pnpm pub [targets..] # 빌드 후 배포 (npm/sftp)
|
|
82
|
+
pnpm pub --no-build # 빌드 생략하고 배포만
|
|
83
|
+
pnpm pub --dry-run # 실제 배포 없이 시뮬레이션
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 코드 품질 검사
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pnpm typecheck [targets..] # TypeScript 타입 체크
|
|
90
|
+
pnpm lint [targets..] # ESLint + Stylelint 실행
|
|
91
|
+
pnpm lint:fix [targets..] # 린트 자동 수정 (--fix)
|
|
92
|
+
pnpm check [targets..] # 전체 검사 (typecheck + lint + test 병렬)
|
|
93
|
+
pnpm vitest [targets..] # vitest 워치 모드
|
|
94
|
+
pnpm vitest run [targets..] # 테스트 1회 실행
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 아키텍처
|
|
98
|
+
|
|
99
|
+
의존 방향: 위 → 아래. `core-common`은 내부 의존 없는 leaf 패키지.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Apps: solid-demo (client) / solid-demo-server (server)
|
|
103
|
+
UI: solid (SolidJS + Tailwind)
|
|
104
|
+
Service: service-server (Fastify) / service-client / service-common
|
|
105
|
+
ORM: orm-node (MySQL/PostgreSQL/MSSQL) / orm-common
|
|
106
|
+
Core: core-common (neutral) / core-browser / core-node
|
|
107
|
+
Tools: sd-cli, lint, excel, storage, sd-claude, mcp-playwright
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## 통합 테스트
|
|
112
|
+
|
|
113
|
+
`tests/` 폴더에 위치. `pnpm vitest run tests/orm` 등으로 실행.
|
|
114
|
+
|
|
115
|
+
- `tests/orm` — DB 커넥션, DbContext, 이스케이프 테스트 (MySQL, PostgreSQL, MSSQL). Docker 필요.
|
|
116
|
+
- `tests/service` — 서비스 클라이언트-서버 통신 테스트.
|
|
117
|
+
|
|
118
|
+
## 코딩 규칙
|
|
119
|
+
|
|
120
|
+
- `import type` 필수 (`verbatimModuleSyntax`), `#private` 금지 → `private` 사용
|
|
121
|
+
- `console.*` 금지, `if (str)` 금지 → `str !== ""` 명시 비교 (nullable boolean/object는 허용)
|
|
122
|
+
- `Buffer` 금지 → `Uint8Array`, `events` 금지 → `@simplysm/core-common`의 `EventEmitter`
|
|
123
|
+
- SolidJS: props 구조 분해 금지, `.map()` 대신 `<For>`, `className` 대신 `class`
|
|
124
|
+
- Prettier: 100자, 2칸 스페이스, 세미콜론, trailing comma, LF
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Step 5: 완료 안내
|
|
128
|
+
|
|
129
|
+
생성이 완료되면 아래를 출력하라:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
CLAUDE.md가 생성되었습니다. 커밋하려면 /sd-commit 을 실행하세요.
|
|
133
|
+
```
|
|
@@ -1,154 +1,103 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sd-plan
|
|
3
|
-
description: "
|
|
3
|
+
description: 이 스킬은 사용자가 "계획 세워줘", "plan 만들어", "sd-plan", "구현 계획", "작업 계획" 등을 요청할 때 사용한다.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# SD Plan — 명확한 계획서 생성
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
**MANDATORY:** Before proceeding, verify that `sd-brainstorm` has already been completed in this conversation.
|
|
11
|
-
|
|
12
|
-
- If a brainstorm design document exists (discussed or saved in this session) → proceed.
|
|
13
|
-
- If NOT → **STOP** and tell the user (in the system's configured language) that sd-plan requires sd-brainstorm to be completed first, then invoke sd-brainstorm instead.
|
|
14
|
-
|
|
15
|
-
## Overview
|
|
16
|
-
|
|
17
|
-
Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
|
18
|
-
|
|
19
|
-
Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know good test design very well.
|
|
20
|
-
|
|
21
|
-
When a task uses a codebase-specific utility (hook, helper, style token) or test pattern, add a one-line explanation of what it does and the source file path. Example: "`createMountTransition(open)` — manages mount/unmount with CSS transitions (`packages/solid/src/hooks/createMountTransition.ts`)". This applies to test utilities and patterns too — if a test uses a framework-specific pattern (e.g., SolidJS `createRoot` for reactive context), explain why that pattern is needed.
|
|
22
|
-
|
|
23
|
-
**Announce at start:** "I'm using the sd-plan skill to create the implementation plan."
|
|
24
|
-
|
|
25
|
-
**Save plans to:** `docs/plans/YYYY-MM-DD-<feature-name>.md`
|
|
26
|
-
|
|
27
|
-
## Bite-Sized Task Granularity
|
|
28
|
-
|
|
29
|
-
**Each step is one action (2-5 minutes):**
|
|
30
|
-
- "Write the failing test" - step
|
|
31
|
-
- "Run it to make sure it fails" - step
|
|
32
|
-
- "Implement the minimal code to make the test pass" - step
|
|
33
|
-
- "Run the tests and make sure they pass" - step
|
|
34
|
-
- "Commit" - step
|
|
35
|
-
|
|
36
|
-
**Step size limit:** If a single step produces more than ~30 lines of code, it is too large. Split it into multiple steps (e.g., "Define types and interfaces" → "Create context and hook" → "Implement provider component").
|
|
37
|
-
|
|
38
|
-
**TDD means YAGNI per step:** Step 3 ("Write minimal implementation") must implement ONLY what's needed to pass Step 1's test — nothing more. If the component needs additional behavior (e.g., FIFO eviction, remove), that behavior goes in a SUBSEQUENT task with its own failing test first. Do NOT implement the full component in one task and then test it after the fact.
|
|
39
|
-
|
|
40
|
-
## Task Ordering
|
|
41
|
-
|
|
42
|
-
**Shared resources BEFORE consumers.** Tasks must be ordered so that every file a task imports already exists from a prior task.
|
|
43
|
-
|
|
44
|
-
- Types, config, i18n entries → before components that use them
|
|
45
|
-
- Provider → before components that call useX() hooks
|
|
46
|
-
- If Task B imports from Task A's file → Task A must come first
|
|
47
|
-
|
|
48
|
-
## Plan Document Header
|
|
49
|
-
|
|
50
|
-
**Every plan MUST start with this header:**
|
|
51
|
-
|
|
52
|
-
```markdown
|
|
53
|
-
# [Feature Name] Implementation Plan
|
|
54
|
-
|
|
55
|
-
> **For Claude:** REQUIRED SUB-SKILL: Use sd-plan-dev to implement this plan task-by-task.
|
|
56
|
-
|
|
57
|
-
**Goal:** [One sentence describing what this builds]
|
|
58
|
-
|
|
59
|
-
**Architecture:** [2-3 sentences about approach]
|
|
60
|
-
|
|
61
|
-
**Tech Stack:** [Key technologies/libraries]
|
|
8
|
+
사용자의 작업 요청을 받아 초기 계획서를 생성한 뒤, 불명확한 부분을 반복적으로 검토하고 질문하여 완벽히 명확한 계획서를 만든다.
|
|
62
9
|
|
|
63
10
|
---
|
|
64
|
-
```
|
|
65
11
|
|
|
66
|
-
##
|
|
12
|
+
## Step 1: 입력 확인
|
|
67
13
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
14
|
+
- 작업 설명을 아래 우선순위로 확보하라:
|
|
15
|
+
1. **작업 요청**: 사용자가 스킬 호출 시 함께 전달한 작업 설명
|
|
16
|
+
2. **현재 대화**: 작업 요청이 없으면 현재 대화 컨텍스트에서 작업 내용을 파악
|
|
17
|
+
3. **AskUserQuestion**: 위 둘로도 파악이 안 되면 "어떤 작업에 대한 계획서를 만들까요? 작업 내용을 설명해 주세요."라고 질문
|
|
18
|
+
- 충분한 작업 설명을 확보한 후 Step 2로 진행하라.
|
|
72
19
|
|
|
73
|
-
|
|
20
|
+
---
|
|
74
21
|
|
|
75
|
-
##
|
|
22
|
+
## Step 2: 계획서 생성 + 명확화 반복
|
|
76
23
|
|
|
77
|
-
|
|
78
|
-
### Task N: [Component Name]
|
|
24
|
+
### 2-1. 초안 작성
|
|
79
25
|
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
26
|
+
계획서 초안을 작성하라. 각 구현 단계는 **검증 → 구현 → 확인** 순서로 배치하라:
|
|
27
|
+
- 테스트 프레임워크가 있으면 → 테스트 코드 먼저 작성
|
|
28
|
+
- 테스트 프레임워크가 없으면 → CLI 실행, dry-run 등 검증 방법 명시
|
|
29
|
+
- 비코드 작업이면 → 자체 검증 체크리스트 먼저 정의
|
|
84
30
|
|
|
85
|
-
|
|
31
|
+
### 2-2. 명확화 사이클
|
|
86
32
|
|
|
87
|
-
|
|
88
|
-
test("specific behavior", () => {
|
|
89
|
-
const result = functionUnderTest(input);
|
|
90
|
-
expect(result).toBe(expected);
|
|
91
|
-
});
|
|
92
|
-
```
|
|
33
|
+
아래를 **불명확 항목이 0개가 될 때까지** 반복하라:
|
|
93
34
|
|
|
94
|
-
|
|
35
|
+
1. **추출**: 계획서를 아래 "불명확 판단 기준" 12개 항목에 전부 대조하여 불명확 항목을 나열하라.
|
|
36
|
+
2. **의존성 분석**: 항목 간 의존 관계를 파악하라. ("A가 정해져야 B를 질문 가능" → B는 A에 의존)
|
|
37
|
+
3. **질문**: 의존 대상이 없는 항목들을 AskUserQuestion 도구 **하나당 단 하나의 질문**만 하라. 각 질문에 2~5개 선택지를 제시하라.
|
|
38
|
+
4. **반영**: 답변을 모두 반영하여 계획서를 업데이트하고, 1번으로 돌아가라.
|
|
95
39
|
|
|
96
|
-
|
|
97
|
-
Expected: FAIL with "functionUnderTest is not defined"
|
|
40
|
+
불명확 항목 0개 → **Step 2.5 최종 검증**으로 이동.
|
|
98
41
|
|
|
99
|
-
|
|
42
|
+
### 불명확 판단 기준
|
|
100
43
|
|
|
101
|
-
|
|
102
|
-
function functionUnderTest(input: InputType): OutputType {
|
|
103
|
-
return expected;
|
|
104
|
-
}
|
|
105
|
-
```
|
|
44
|
+
> **핵심 원칙**: 사용자가 명시하지 않았고, 코드베이스에서 확인되지 않은 것은 **모두 추측/가정으로 간주**하여 불명확 항목으로 취급하라. Claude가 자신있게 작성했더라도 출처가 불분명하면 불명확이다.
|
|
106
45
|
|
|
107
|
-
|
|
46
|
+
아래 12개 항목을 **매 검토 시 전부** 대조하라. "해당 없음"이라고 넘기려면 구체적 근거(사용자 발언 또는 코드베이스 확인)가 있어야 한다.
|
|
108
47
|
|
|
109
|
-
|
|
110
|
-
|
|
48
|
+
1. **사용자 미명시 가정**: 사용자가 말하지 않았는데 Claude가 채워넣은 결정사항
|
|
49
|
+
2. **구체성 부족**: HOW 없이 "적절히 처리", "필요에 따라" 등의 표현
|
|
50
|
+
3. **범위 모호**: IN/OUT 스코프가 정의되지 않음
|
|
51
|
+
4. **미지정 동작**: 에러, 유효하지 않은 입력, 기본값 등이 지정되지 않음
|
|
52
|
+
5. **알 수 없는 제약조건**: 성능, 호환성, 플랫폼 요구사항이 불분명
|
|
53
|
+
6. **누락된 엣지케이스**: 경계 조건, 동시성, 빈 상태 등
|
|
54
|
+
7. **모호한 파일/함수 참조**: 구체적 경로 없이 "관련 파일을 수정" 등
|
|
55
|
+
8. **불명확한 순서/의존성**: 단계 간 선후 관계 미명시
|
|
56
|
+
9. **추측 표현**: "아마", "~일 수 있음", "TBD", "???" 등
|
|
57
|
+
10. **통합 세부사항 누락**: API 계약, 데이터 형식, 인터페이스 미정의
|
|
58
|
+
11. **실패/롤백 전략 부재**: 실패 시 대응 방안 없음
|
|
59
|
+
12. **검증 방법 미정의**: 구현 단계에 대응하는 검증 방법이 없음
|
|
111
60
|
|
|
112
|
-
|
|
61
|
+
---
|
|
113
62
|
|
|
114
|
-
|
|
115
|
-
git add exact/path/to/tests/file.spec.ts exact/path/to/file.ts
|
|
116
|
-
git commit -m "feat: add specific feature"
|
|
117
|
-
```
|
|
118
|
-
```
|
|
63
|
+
## Step 2.5: 최종 검증 (불명확 없음 선언 전 필수)
|
|
119
64
|
|
|
120
|
-
|
|
65
|
+
"불명확 없음"을 선언하기 **직전에 반드시** 아래를 수행하라:
|
|
121
66
|
|
|
122
|
-
|
|
67
|
+
1. 계획서의 **모든 단계를 처음부터 끝까지** 다시 읽으며, 위 12개 기준을 한 번 더 전수 대조하라.
|
|
68
|
+
2. 특히 다음을 집중 확인하라:
|
|
69
|
+
- Claude가 스스로 결정한 부분 중 사용자 확인을 받지 않은 것이 있는가?
|
|
70
|
+
- "~한다", "~로 처리한다" 등 단정적으로 쓴 부분의 근거가 사용자 발언 또는 코드베이스에 있는가?
|
|
71
|
+
- 구체적 파일 경로, 함수명, 데이터 구조가 빠진 곳은 없는가?
|
|
72
|
+
3. 이 검증에서 **하나라도** 불명확 항목이 발견되면 Step 2의 질문 사이클로 돌아가라.
|
|
73
|
+
4. 진짜로 없으면 → Step 3으로 이동.
|
|
123
74
|
|
|
124
|
-
|
|
125
|
-
- If the logic is UI-only (visual rendering, Portal placement, CSS animation) → include a manual verification step with exact instructions ("Open the browser, click X, expect Y")
|
|
126
|
-
- The **Files:** section must list the test file: `Test: exact/path/to/tests/file.spec.ts`
|
|
127
|
-
- If you find yourself writing a task with no test step → **STOP and add one**
|
|
75
|
+
---
|
|
128
76
|
|
|
129
|
-
##
|
|
130
|
-
- Exact file paths always
|
|
131
|
-
- Cross-check the design document's file structure — every file listed in the design MUST appear in the plan (create or modify)
|
|
132
|
-
- Complete code in plan (not "add validation")
|
|
133
|
-
- When modifying an existing file, show ALL necessary import additions/changes — not just the appended code
|
|
134
|
-
- Code must compile cleanly — no unused imports or variables
|
|
135
|
-
- Exact commands with expected output
|
|
136
|
-
- DRY, YAGNI, TDD, frequent commits
|
|
77
|
+
## Step 3: 최종 출력
|
|
137
78
|
|
|
138
|
-
|
|
79
|
+
모든 불명확 항목이 해소되었으면 완성된 계획서를 사용자에게 제시하고, AskUserQuestion으로 구현 승인을 요청하라.
|
|
139
80
|
|
|
140
|
-
|
|
141
|
-
-
|
|
81
|
+
사용자가 승인하면 `.tmp/plans/{yyMMddHHmmss}_{topic}.md`에 계획서를 Write하라.
|
|
82
|
+
- 파일명 예시: `260311143052_progress-컴포넌트-추가.md`
|
|
83
|
+
- `yyMMddHHmmss`: 연월일시분초 (예: 260311143052)
|
|
84
|
+
- `{topic}`: 작업 내용 기반 짧은 케밥케이스 (예: progress-컴포넌트-추가)
|
|
142
85
|
|
|
143
|
-
|
|
86
|
+
---
|
|
144
87
|
|
|
145
|
-
|
|
88
|
+
## Step 4: 구현 완료 후 안내
|
|
146
89
|
|
|
147
|
-
|
|
90
|
+
사용자가 구현을 승인하면, 계획서에 따라 구현하라. 구현이 완료되면 아래 안내를 출력하라:
|
|
148
91
|
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
|
|
92
|
+
- **코드 수정이 포함된 경우**:
|
|
93
|
+
```
|
|
94
|
+
구현이 완료되었습니다. 다음 단계를 순서대로 실행하는 것을 권장합니다:
|
|
95
|
+
1. /sd-check — 타입체크 + 린트 + 테스트 검사 및 자동 수정
|
|
96
|
+
2. /sd-simplify — 변경된 코드 단순화 리뷰
|
|
97
|
+
3. /sd-commit — 변경사항 커밋
|
|
98
|
+
```
|
|
152
99
|
|
|
153
|
-
-
|
|
154
|
-
|
|
100
|
+
- **코드 수정이 없는 경우** (설정, 문서 등):
|
|
101
|
+
```
|
|
102
|
+
구현이 완료되었습니다. 커밋하려면 /sd-commit 을 실행하세요.
|
|
103
|
+
```
|