@simplysm/sd-claude 14.0.75 → 14.0.77
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/output-styles/sd-tone.md +128 -0
- package/claude/references/sd-simplysm14/apis/angular/README.md +28 -89
- package/claude/references/sd-simplysm14/apis/angular/app-structure.md +75 -32
- package/claude/references/sd-simplysm14/apis/angular/buttons.md +65 -29
- package/claude/references/sd-simplysm14/apis/angular/crud.md +86 -21
- package/claude/references/sd-simplysm14/apis/angular/forms.md +168 -42
- package/claude/references/sd-simplysm14/apis/angular/infrastructure.md +200 -49
- package/claude/references/sd-simplysm14/apis/angular/kanban.md +64 -20
- package/claude/references/sd-simplysm14/apis/angular/layout.md +75 -30
- package/claude/references/sd-simplysm14/apis/angular/modal.md +92 -40
- package/claude/references/sd-simplysm14/apis/angular/routing.md +86 -25
- package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +72 -41
- package/claude/references/sd-simplysm14/apis/angular/shared-data.md +113 -21
- package/claude/references/sd-simplysm14/apis/angular/sheet.md +108 -33
- package/claude/references/sd-simplysm14/apis/angular/toast.md +81 -30
- package/claude/references/sd-simplysm14/apis/angular/visual.md +140 -32
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +46 -43
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +59 -48
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +17 -7
- package/claude/references/sd-simplysm14/apis/core-common/README.md +43 -116
- package/claude/references/sd-simplysm14/apis/core-common/extensions.md +74 -109
- package/claude/references/sd-simplysm14/apis/core-common/features.md +40 -35
- package/claude/references/sd-simplysm14/apis/core-common/types.md +80 -106
- package/claude/references/sd-simplysm14/apis/core-common/utils.md +142 -111
- package/claude/references/sd-simplysm14/apis/core-node/README.md +7 -16
- package/claude/references/sd-simplysm14/apis/core-node/consola.md +33 -38
- package/claude/references/sd-simplysm14/apis/core-node/cpx.md +25 -33
- package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +27 -38
- package/claude/references/sd-simplysm14/apis/core-node/fsx.md +32 -60
- package/claude/references/sd-simplysm14/apis/core-node/pathx.md +14 -45
- package/claude/references/sd-simplysm14/apis/core-node/worker.md +35 -81
- package/claude/references/sd-simplysm14/apis/excel/README.md +178 -80
- package/claude/references/sd-simplysm14/apis/lint/README.md +5 -0
- package/claude/references/sd-simplysm14/apis/orm-node/README.md +1 -1
- package/claude/references/sd-simplysm14/apis/sd-claude/README.md +28 -5
- package/claude/references/sd-simplysm14/apis/sd-cli/README.md +1 -1
- package/claude/references/sd-simplysm14/apis/service-client/README.md +57 -50
- package/claude/references/sd-simplysm14/apis/service-server/README.md +8 -15
- package/claude/references/sd-simplysm14/apis/service-server/auth.md +24 -16
- package/claude/references/sd-simplysm14/apis/service-server/builtin-services.md +55 -31
- package/claude/references/sd-simplysm14/apis/service-server/define-service.md +28 -44
- package/claude/references/sd-simplysm14/apis/service-server/internals.md +59 -18
- package/claude/references/sd-simplysm14/apis/service-server/server.md +37 -46
- package/claude/references/sd-simplysm14/manuals/client-component.md +3 -1
- package/claude/references/sd-simplysm14/manuals/logging.md +9 -8
- package/claude/rules/sd-base-rules.md +380 -217
- package/claude/settings.json +1 -0
- package/claude/skills/sd-commit/SKILL.md +31 -8
- package/claude/skills/sd-docs/SKILL.md +15 -10
- package/claude/skills/sd-docs/references/subagent-prompt.md +26 -8
- package/claude/skills/sd-impl/SKILL.md +1 -1
- package/claude/skills/sd-skill/references/skill-authoring.md +1 -1
- package/claude/skills/sd-spec/SKILL.md +22 -13
- package/claude/skills/sd-spec/references/spec-authoring.md +1 -1
- package/claude/skills/sd-unpack/SKILL.md +150 -26
- package/claude/skills/sd-unpack/scripts/handlers/__pycache__/_common.cpython-314.pyc +0 -0
- package/claude/skills/sd-unpack/scripts/handlers/__pycache__/eml_handler.cpython-314.pyc +0 -0
- package/claude/skills/sd-unpack/scripts/handlers/__pycache__/office_com.cpython-314.pyc +0 -0
- package/claude/skills/sd-unpack/scripts/handlers/__pycache__/pdf_handler.cpython-314.pyc +0 -0
- package/claude/skills/sd-unpack/scripts/handlers/_common.py +17 -2
- package/claude/skills/sd-unpack/scripts/handlers/eml_handler.py +100 -24
- package/claude/skills/sd-unpack/scripts/handlers/msg_handler.py +140 -27
- package/claude/skills/sd-unpack/scripts/handlers/office_com.py +698 -107
- package/claude/skills/sd-unpack/scripts/handlers/office_worker.py +34 -26
- package/claude/skills/sd-unpack/scripts/handlers/pdf_handler.py +130 -8
- package/package.json +1 -1
|
@@ -122,7 +122,11 @@ def cmd_ppt_png(args) -> None:
|
|
|
122
122
|
# ====================================================================
|
|
123
123
|
|
|
124
124
|
def cmd_excel_sheets(args) -> None:
|
|
125
|
-
"""input 은 이미 sheetProtection strip 된 사본 (호출자가 처리).
|
|
125
|
+
"""input 은 이미 sheetProtection strip 된 사본 (호출자가 처리).
|
|
126
|
+
|
|
127
|
+
stdout JSON 형식: {"sheet_ranges": {raw_name: [last_row, last_col]},
|
|
128
|
+
"skipped": {raw_name: "PNG skip 사유"}}
|
|
129
|
+
"""
|
|
126
130
|
import pythoncom
|
|
127
131
|
import win32com.client
|
|
128
132
|
|
|
@@ -131,6 +135,7 @@ def cmd_excel_sheets(args) -> None:
|
|
|
131
135
|
sheet_names = json.loads(args.sheet_names) # [[idx, safe_name, raw_name], ...]
|
|
132
136
|
|
|
133
137
|
sheet_ranges: dict[str, tuple[int, int]] = {}
|
|
138
|
+
skipped: dict[str, str] = {} # raw_name -> PNG skip 사유 (silent skip 금지)
|
|
134
139
|
tmp_dir = Path(tempfile.mkdtemp(prefix="sd-unpack-worker-"))
|
|
135
140
|
|
|
136
141
|
pythoncom.CoInitialize()
|
|
@@ -148,7 +153,7 @@ def cmd_excel_sheets(args) -> None:
|
|
|
148
153
|
excel.Calculation = -4135 # xlCalculationManual
|
|
149
154
|
wb.RemovePersonalInformation = False
|
|
150
155
|
for idx, safe_name, raw_name in sheet_names:
|
|
151
|
-
_export_one_sheet(wb, tmp_dir, sheets_dir, idx, safe_name, raw_name, sheet_ranges)
|
|
156
|
+
_export_one_sheet(wb, tmp_dir, sheets_dir, idx, safe_name, raw_name, sheet_ranges, skipped)
|
|
152
157
|
finally:
|
|
153
158
|
wb.Close(SaveChanges=False)
|
|
154
159
|
finally:
|
|
@@ -157,13 +162,16 @@ def cmd_excel_sheets(args) -> None:
|
|
|
157
162
|
pythoncom.CoUninitialize()
|
|
158
163
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
159
164
|
|
|
160
|
-
sys.stdout.write(json.dumps(sheet_ranges))
|
|
165
|
+
sys.stdout.write(json.dumps({"sheet_ranges": sheet_ranges, "skipped": skipped}))
|
|
161
166
|
|
|
162
167
|
|
|
163
168
|
def _export_one_sheet(wb, tmp: Path, sheets_dir: Path,
|
|
164
169
|
idx: str, safe_name: str, raw_name: str,
|
|
165
|
-
sheet_ranges: dict) -> None:
|
|
166
|
-
"""한 시트 데이터 영역 → PNG. sheet_ranges 에 (last_row, last_col) 기록.
|
|
170
|
+
sheet_ranges: dict, skipped: dict) -> None:
|
|
171
|
+
"""한 시트 데이터 영역 → PNG. sheet_ranges 에 (last_row, last_col) 기록.
|
|
172
|
+
|
|
173
|
+
PNG export 실패 시 skipped[raw_name] 에 사유 기록 후 return (silent 금지, 호출자가 README 명시).
|
|
174
|
+
"""
|
|
167
175
|
ws = wb.Worksheets(raw_name)
|
|
168
176
|
# xlSheetVisible=-1, xlSheetHidden=0, xlSheetVeryHidden=2
|
|
169
177
|
# Hidden/VeryHidden 시트도 export 하려면 임시로 보이게.
|
|
@@ -196,10 +204,20 @@ def _export_one_sheet(wb, tmp: Path, sheets_dir: Path,
|
|
|
196
204
|
# xlScreen=1, xlPicture=-4147
|
|
197
205
|
# Format=xlBitmap(2) 는 화면 픽셀 버퍼 캡처라 Excel.Visible=False headless 환경에서 빈/부분 비트맵 생성됨 → xlPicture(EMF) 사용.
|
|
198
206
|
# EMF 는 메타파일 명령으로 시트 콘텐츠 직접 직렬화 → 화면 렌더 의존 없음.
|
|
199
|
-
# ChartObject pt 그대로. PNG 는 시각/레이아웃용, 정확한 텍스트는 .
|
|
200
|
-
# 사이즈 키우면 큰 시트가 Chart.Export PNG dimension 16-bit cap (65535px) 에 걸림.
|
|
207
|
+
# ChartObject pt 그대로. PNG 는 시각/레이아웃용, 정확한 텍스트는 .jsonl 이 책임.
|
|
201
208
|
chart_w = data_range.Width
|
|
202
209
|
chart_h = data_range.Height
|
|
210
|
+
|
|
211
|
+
# Chart.Export PNG dimension 16-bit cap (65535px) 회피.
|
|
212
|
+
# ChartObject 의 pt → px 변환은 약 96 dpi. 안전 margin 으로 60000px cap.
|
|
213
|
+
est_w_px = chart_w * 96 / 72
|
|
214
|
+
est_h_px = chart_h * 96 / 72
|
|
215
|
+
if est_w_px > 60000 or est_h_px > 60000:
|
|
216
|
+
skipped[raw_name] = (
|
|
217
|
+
f"16-bit cap 초과 (chart_w_pt={chart_w:.0f}, chart_h_pt={chart_h:.0f}, "
|
|
218
|
+
f"est_w_px={est_w_px:.0f}, est_h_px={est_h_px:.0f})"
|
|
219
|
+
)
|
|
220
|
+
return
|
|
203
221
|
try:
|
|
204
222
|
data_range.CopyPicture(Appearance=1, Format=-4147)
|
|
205
223
|
chart_obj = ws.ChartObjects().Add(0, 0, chart_w, chart_h)
|
|
@@ -211,30 +229,20 @@ def _export_one_sheet(wb, tmp: Path, sheets_dir: Path,
|
|
|
211
229
|
if tmp_png.exists():
|
|
212
230
|
# long-path-safe copy
|
|
213
231
|
shutil.copy2(long_str(tmp_png), long_str(sheets_dir / f"{idx}_{safe_name}.png"))
|
|
232
|
+
else:
|
|
233
|
+
skipped[raw_name] = "Chart.Export 산출 PNG 미생성"
|
|
214
234
|
finally:
|
|
215
235
|
chart_obj.Delete()
|
|
216
236
|
except Exception as e:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
f"
|
|
237
|
+
# PNG export 실패 — 시트 jsonl 은 호출자에서 별도 생성됨. sheet_ranges 이미 기록됨.
|
|
238
|
+
# 분석 정확도: 데이터(jsonl) 보존 우선, PNG 만 skip + 사유 명시.
|
|
239
|
+
reason_parts = [
|
|
240
|
+
f"Excel COM PNG export 실패",
|
|
241
|
+
f"chart_w_pt={chart_w:.0f}, chart_h_pt={chart_h:.0f}",
|
|
221
242
|
f"last_row={last_row}, last_col={last_col}",
|
|
222
|
-
f"
|
|
243
|
+
f"error={str(e)[:200]}",
|
|
223
244
|
]
|
|
224
|
-
|
|
225
|
-
try:
|
|
226
|
-
diag.append(f"{attr}={getattr(ws, attr)}")
|
|
227
|
-
except Exception as ee:
|
|
228
|
-
diag.append(f"{attr}_FAIL={ee}")
|
|
229
|
-
try:
|
|
230
|
-
diag.append(f"chartobj_count={ws.ChartObjects().Count}")
|
|
231
|
-
except Exception as ee:
|
|
232
|
-
diag.append(f"chartobj_count_FAIL={ee}")
|
|
233
|
-
raise RuntimeError(
|
|
234
|
-
"Excel sheet PNG export failed: "
|
|
235
|
-
+ " | ".join(diag)
|
|
236
|
-
+ f" | original_error={e}"
|
|
237
|
-
) from e
|
|
245
|
+
skipped[raw_name] = " | ".join(reason_parts)
|
|
238
246
|
finally:
|
|
239
247
|
if original_visible != -1:
|
|
240
248
|
ws.Visible = original_visible
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
"""PDF 핸들러. PyMuPDF (fitz) 로 페이지별 PNG +
|
|
1
|
+
"""PDF 핸들러. PyMuPDF (fitz) 로 페이지별 PNG + 블록 단위 JSONL + 표 셀 단위 노드.
|
|
2
|
+
|
|
3
|
+
블록 단위 (text_block·image_block) 는 bbox 좌표 보존. 표는 find_tables() 로 셀 단위 노드.
|
|
4
|
+
"""
|
|
2
5
|
from __future__ import annotations
|
|
3
6
|
|
|
7
|
+
import json
|
|
4
8
|
import os
|
|
5
9
|
from pathlib import Path
|
|
6
10
|
|
|
@@ -12,6 +16,7 @@ def run(input_path: Path, out_dir: Path) -> None:
|
|
|
12
16
|
import fitz # PyMuPDF
|
|
13
17
|
|
|
14
18
|
pages_dir = out_dir / "pages"
|
|
19
|
+
images_dir = out_dir / "images"
|
|
15
20
|
_common.mkdir(pages_dir)
|
|
16
21
|
|
|
17
22
|
doc = fitz.open(_common.long_str(input_path))
|
|
@@ -19,11 +24,19 @@ def run(input_path: Path, out_dir: Path) -> None:
|
|
|
19
24
|
page_summaries: list[str] = []
|
|
20
25
|
for i, page in enumerate(doc, start=1):
|
|
21
26
|
idx = f"{i:03d}"
|
|
22
|
-
text = page.get_text("text") or ""
|
|
23
|
-
_common.write_text(pages_dir / f"{idx}.md", text)
|
|
24
27
|
pix = page.get_pixmap(dpi=300)
|
|
25
28
|
pix.save(_common.long_str(pages_dir / f"{idx}.png"))
|
|
26
|
-
|
|
29
|
+
|
|
30
|
+
jsonl_lines, counts = _pdf_page_to_jsonl(page, i, images_dir)
|
|
31
|
+
_common.write_text(pages_dir / f"{idx}.jsonl", "\n".join(jsonl_lines))
|
|
32
|
+
|
|
33
|
+
parts = [
|
|
34
|
+
f"`pages/{idx}.png` `.jsonl`",
|
|
35
|
+
f"blocks {counts['text_blocks'] + counts['image_blocks']}",
|
|
36
|
+
]
|
|
37
|
+
if counts["tables"]:
|
|
38
|
+
parts.append(f"tables {counts['tables']} (cells {counts['table_cells']})")
|
|
39
|
+
page_summaries.append(" — ".join([parts[0], ", ".join(parts[1:])]))
|
|
27
40
|
|
|
28
41
|
# 임베드된 첨부 (embedded files)
|
|
29
42
|
attachment_links: list[str] = []
|
|
@@ -36,12 +49,13 @@ def run(input_path: Path, out_dir: Path) -> None:
|
|
|
36
49
|
_common.mkdir(attachments_dir)
|
|
37
50
|
dst = _common.unique_path(attachments_dir, filename)
|
|
38
51
|
_common.write_bytes(dst, data)
|
|
52
|
+
size = dst.stat().st_size
|
|
39
53
|
recursed = maybe_recurse_attachment(dst, attachments_dir)
|
|
40
54
|
if recursed is not None:
|
|
41
55
|
os.unlink(_common.long_str(dst))
|
|
42
|
-
attachment_links.append(f"attachments/{recursed.name}/")
|
|
56
|
+
attachment_links.append(f"attachments/{recursed.name}/ ({_common.format_size(size)})")
|
|
43
57
|
else:
|
|
44
|
-
attachment_links.append(f"attachments/{dst.name}")
|
|
58
|
+
attachment_links.append(f"attachments/{dst.name} ({_common.format_size(size)})")
|
|
45
59
|
finally:
|
|
46
60
|
doc.close()
|
|
47
61
|
|
|
@@ -50,7 +64,115 @@ def run(input_path: Path, out_dir: Path) -> None:
|
|
|
50
64
|
source_name=input_path.name,
|
|
51
65
|
source_size=input_path.stat().st_size,
|
|
52
66
|
tool="PyMuPDF (fitz)",
|
|
53
|
-
loss_notes=
|
|
54
|
-
|
|
67
|
+
loss_notes=(
|
|
68
|
+
"PDF 양식 필드(form field)·서명·OCR 미적용(스캔 PDF). "
|
|
69
|
+
"구조는 pages/<NNN>.jsonl (블록 bbox + 표 셀 단위 노드), 시각은 .png. "
|
|
70
|
+
"이미지 블록은 images/ 로 별도 저장."
|
|
71
|
+
),
|
|
72
|
+
sections={f"페이지 (총 {len(page_summaries)}개)": page_summaries} if page_summaries else None,
|
|
55
73
|
attachments=attachment_links,
|
|
56
74
|
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _pdf_page_to_jsonl(
|
|
78
|
+
page, page_num: int, images_dir: Path,
|
|
79
|
+
) -> tuple[list[str], dict[str, int]]:
|
|
80
|
+
"""한 PDF 페이지 → jsonl 라인 list + counts.
|
|
81
|
+
|
|
82
|
+
추출:
|
|
83
|
+
1. get_text("dict") 의 모든 블록 (text_block·image_block) — 표 영역 겹쳐도 그대로 보존
|
|
84
|
+
2. find_tables() 로 표 셀 단위 노드 추가 (블록과 중복 가능, Claude 가 양쪽 비교 판단)
|
|
85
|
+
"""
|
|
86
|
+
counts = {"text_blocks": 0, "image_blocks": 0, "tables": 0, "table_cells": 0}
|
|
87
|
+
|
|
88
|
+
# 블록 추출 (모든 블록 보존)
|
|
89
|
+
page_dict = page.get_text("dict")
|
|
90
|
+
blocks = page_dict.get("blocks", [])
|
|
91
|
+
|
|
92
|
+
node_lines: list[dict] = []
|
|
93
|
+
block_idx = 0
|
|
94
|
+
for blk in blocks:
|
|
95
|
+
btype = blk.get("type", 0)
|
|
96
|
+
bbox = blk.get("bbox", [0, 0, 0, 0])
|
|
97
|
+
|
|
98
|
+
if btype == 0:
|
|
99
|
+
text_lines: list[str] = []
|
|
100
|
+
for line in blk.get("lines", []):
|
|
101
|
+
spans = line.get("spans", [])
|
|
102
|
+
line_text = "".join(span.get("text", "") for span in spans)
|
|
103
|
+
if line_text.strip():
|
|
104
|
+
text_lines.append(line_text)
|
|
105
|
+
text = "\n".join(text_lines).strip()
|
|
106
|
+
if not text:
|
|
107
|
+
continue
|
|
108
|
+
node_lines.append({
|
|
109
|
+
"page": page_num,
|
|
110
|
+
"block": block_idx,
|
|
111
|
+
"type": "text_block",
|
|
112
|
+
"bbox": [round(c, 2) for c in bbox],
|
|
113
|
+
"text": text,
|
|
114
|
+
})
|
|
115
|
+
counts["text_blocks"] += 1
|
|
116
|
+
block_idx += 1
|
|
117
|
+
elif btype == 1:
|
|
118
|
+
img_bytes = blk.get("image")
|
|
119
|
+
ext = (blk.get("ext") or "bin").lstrip(".")
|
|
120
|
+
ref = ""
|
|
121
|
+
if img_bytes:
|
|
122
|
+
_common.mkdir(images_dir)
|
|
123
|
+
img_filename = f"p{page_num:03d}_b{block_idx:03d}.{ext}"
|
|
124
|
+
_common.write_bytes(images_dir / img_filename, img_bytes)
|
|
125
|
+
ref = f"images/{img_filename}"
|
|
126
|
+
node_lines.append({
|
|
127
|
+
"page": page_num,
|
|
128
|
+
"block": block_idx,
|
|
129
|
+
"type": "image_block",
|
|
130
|
+
"bbox": [round(c, 2) for c in bbox],
|
|
131
|
+
"ref": ref,
|
|
132
|
+
})
|
|
133
|
+
counts["image_blocks"] += 1
|
|
134
|
+
block_idx += 1
|
|
135
|
+
|
|
136
|
+
# 표 셀 노드 (find_tables, 블록과 중복 가능 — 양쪽 다 보존)
|
|
137
|
+
tables: list = []
|
|
138
|
+
try:
|
|
139
|
+
finder = page.find_tables()
|
|
140
|
+
tables = list(finder.tables) if hasattr(finder, "tables") else list(finder)
|
|
141
|
+
except Exception:
|
|
142
|
+
tables = []
|
|
143
|
+
counts["tables"] = len(tables)
|
|
144
|
+
|
|
145
|
+
for t_idx, tab in enumerate(tables, start=1):
|
|
146
|
+
try:
|
|
147
|
+
rows = tab.extract()
|
|
148
|
+
t_bbox = [round(c, 2) for c in tab.bbox]
|
|
149
|
+
except Exception:
|
|
150
|
+
continue
|
|
151
|
+
for r_idx, row in enumerate(rows, start=1):
|
|
152
|
+
for c_idx, cell_text in enumerate(row, start=1):
|
|
153
|
+
if cell_text is None:
|
|
154
|
+
continue
|
|
155
|
+
node_lines.append({
|
|
156
|
+
"page": page_num,
|
|
157
|
+
"type": "table_cell",
|
|
158
|
+
"table_idx": t_idx,
|
|
159
|
+
"table_bbox": t_bbox,
|
|
160
|
+
"row": r_idx,
|
|
161
|
+
"col": c_idx,
|
|
162
|
+
"text": str(cell_text).strip(),
|
|
163
|
+
})
|
|
164
|
+
counts["table_cells"] += 1
|
|
165
|
+
|
|
166
|
+
meta = {
|
|
167
|
+
"_meta": {
|
|
168
|
+
"page": page_num,
|
|
169
|
+
"size": [round(page.rect.width, 2), round(page.rect.height, 2)],
|
|
170
|
+
"blocks": counts["text_blocks"] + counts["image_blocks"],
|
|
171
|
+
"tables": counts["tables"],
|
|
172
|
+
"table_cells": counts["table_cells"],
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
lines = [json.dumps(meta, ensure_ascii=False)]
|
|
176
|
+
for n in node_lines:
|
|
177
|
+
lines.append(json.dumps(n, ensure_ascii=False))
|
|
178
|
+
return lines, counts
|