Files
document-haness/scripts/studio-body.py
T
DongHyeonkaandClaude Opus 5 ab59130196 chore: 이전 세션이 남긴 변경을 커밋한다
이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다.
사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다.

대부분은 clean-architecture-backend-template 의 그림 정본 재배치다 —
final/assets/diagrams/<이름>/ 에 있던 것이 CLAUDE.md 가 적은 배치인
final/assets/<이름>/ 로 옮겨졌고 .techviz/<이름>/ 이 함께 들어왔다.
삽입 줄의 대부분(3.15M)이 그 .techviz context.json 이다.

그 밖에 ca-tmpl·document-haness 의 정리, .claude/agents/ 열한 개,
writing-practitioner-guides 스킬, .playwright-mcp 세션 산출물,
scripts/check-ssot-facts.py 와 그 시험이 들어 있다.

이 커밋의 내용은 내가 만든 것이 아니라 이전 세션이 남긴 것이고 검증하지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:02:02 +09:00

110 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""기록의 본문을 Studio 가 받는 형태로 바꿔 낸다.
저장소의 `.md` 는 사람이 읽는 파일이라 그림을 마크다운 이미지로 적는다. 그래야 편집기에서
그대로 보인다. Studio 는 그 자리에 `:::evidence key="…"` 를 요구하고 상대 경로 이미지는
`unsafe image URL` 로 거절한다. 두 형태를 한 파일에 담을 수 없으므로 **저장소는 읽는 형태로
두고 Studio 로 보낼 때 이 스크립트가 바꾼다.**
python3 scripts/studio-body.py <기록.md> # 바꾼 전문을 표준출력으로
python3 scripts/studio-body.py <기록.md> -o /tmp/x.md # 파일로 (check_body 용)
python3 scripts/studio-body.py <기록.md> --body-only # 본문 구간만 (Studio 붙여넣기용)
python3 scripts/studio-body.py <기록.md> --key a=a-1234 # 서버가 준 키로 바꿔서
`--key` 를 주지 않으면 frontmatter 의 `assets` key 를 그대로 쓴다. Studio 에 올린 뒤에는
서버가 `<이름>-<해시8>` 을 주므로 그때 `--key` 로 짝지어 준다.
"""
from __future__ import annotations
import argparse
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "scripts"))
import techlog # noqa: E402
BODY = re.compile(r"(<!-- body:start -->)(.*?)(<!-- body:end -->)", re.S)
IMAGE = re.compile(r"^!\[([^\]]*)\]\(([^)]+)\)$", re.M)
def asset_keys(text: str) -> dict[str, str]:
"""frontmatter 의 assets 를 상대경로 → key 로 읽는다."""
head = text.split("\n---\n", 1)[0]
keys = re.findall(r"^ - key: (\S+)$", head, re.M)
files = re.findall(r"^ file: (\S+)$", head, re.M)
return {f: k for k, f in zip(keys, files)}
# 본문이 있는 종류는 **셋**이다 — CASE·CONCEPT·SETUP. 나머지 셋은 본문 구간이 없는 것이
# 정상이다. 목록을 여기 다시 적지 않는다 (techlog.BODY_KIND_CODES 가 정본)
BODY_KINDS = techlog.BODY_KIND_CODES
def record_kind(text: str) -> str:
"""frontmatter 의 kind. 없으면 빈 문자열."""
head = text.split("\n---\n", 1)[0]
m = re.search(r"^kind:\s*(\S+)", head, re.M)
return m.group(1).upper() if m else ""
def convert(text: str, overrides: dict[str, str]) -> tuple[str, list[str]]:
by_path = asset_keys(text)
missing: list[str] = []
def one(m: re.Match) -> str:
alt, path = m.group(1), m.group(2)
key = by_path.get(path)
if key is None:
missing.append(path)
return m.group(0)
key = overrides.get(key, key)
return f':::evidence key="{key}" alt="{alt}" caption=" " zoom="true"\n:::'
m = BODY.search(text)
if not m:
if record_kind(text) in BODY_KINDS:
return text, ["본문 구간(body:start ~ body:end)이 없다"]
# Reference·Question·Decision 은 본문을 둘 자리가 없다. 결함이 아니다
return text, []
body = IMAGE.sub(one, m.group(2))
return text[:m.start(2)] + body + text[m.end(2):], missing
def main() -> int:
ap = argparse.ArgumentParser(description="기록 본문을 Studio 형태로 바꾼다.")
ap.add_argument("record")
ap.add_argument("-o", "--out")
ap.add_argument("--body-only", action="store_true")
ap.add_argument("--key", action="append", default=[],
help="frontmatter key=서버가 준 key. 여러 번 줄 수 있다")
args = ap.parse_args()
overrides = {}
for pair in args.key:
if "=" not in pair:
sys.exit(f"--key 는 a=b 형태다: {pair}")
a, b = pair.split("=", 1)
overrides[a] = b
text = open(args.record, encoding="utf-8").read()
out, missing = convert(text, overrides)
for path in missing:
print(f"경고: frontmatter 의 assets 에 없다 — {path}", file=sys.stderr)
if args.body_only:
m = BODY.search(out)
out = m.group(2).strip("\n") if m else out
if args.out:
open(args.out, "w", encoding="utf-8").write(out)
print(args.out)
else:
sys.stdout.write(out)
return 1 if missing else 0
if __name__ == "__main__":
raise SystemExit(main())