Files
document-haness/scripts/build-tech-log-tree.py
T

114 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""tech-log-tree.json 을 다시 만든다.
노드 필드는 document-detail 의 root-tree 계약을 따른다 — readiness, source, code,
evidence, classification, missing-verification, relations. 제목만 보고 기록을 만들지
못하게 하려는 것이다.
SSOT(final/document.md)에서 뽑은 글감과 이미 쓴 기록을 한 파일에 모은다. 기록 파일이
정본이므로 이 스크립트는 그것을 읽어 채우고, 아직 글이 없는 글감은 사람이 적은 항목을
그대로 둔다.
python3 scripts/build-tech-log-tree.py [프로젝트 ...]
"""
from __future__ import annotations
import json, re, sys, glob, os, datetime, hashlib
KINDS = ["case", "concept", "reference", "question", "decision"]
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def front_matter(path: str) -> dict:
text = open(path, encoding="utf-8").read()
if not text.startswith("---"):
return {}
end = text.find("\n---", 3)
out = {}
for line in text[3:end].splitlines():
m = re.match(r"^([a-zA-Z_]+):\s*(.*)$", line)
if m:
out[m.group(1)] = m.group(2).strip().strip('"')
return out
def build(project: str) -> dict:
base = os.path.join(ROOT, "docs", project)
studio = os.path.join(base, "tech-log-studio")
tree_path = os.path.join(studio, "tech-log-tree.json")
previous = {}
if os.path.exists(tree_path):
previous = json.load(open(tree_path, encoding="utf-8"))
ssot = "final/document.md" if os.path.exists(os.path.join(base, "final/document.md")) else None
topics = {}
for topic_dir in sorted(d for d in glob.glob(os.path.join(studio, "*")) if os.path.isdir(d)):
topic = os.path.basename(topic_dir)
entry = {"topic": topic, "kinds": {}}
for kind in KINDS:
items = []
for f in sorted(glob.glob(os.path.join(topic_dir, kind, "*.md"))):
fm = front_matter(f)
text = open(f, encoding="utf-8").read()
node = {
"title": fm.get("title", os.path.basename(f)),
"slug": fm.get("slug", ""),
"file": os.path.relpath(f, studio),
"readiness": "READY" if fm.get("id") else "NEEDS_EVIDENCE",
"status": fm.get("status", "미작성"),
"studioId": fm.get("id", ""),
"assets": re.findall(r"^ - key: (\S+)", text, re.M),
"evidence": re.findall(r"^ - (\.\./\S+)", text, re.M),
"relations": re.findall(r"^- \*\*(.+?)\*\*$", text, re.M),
}
# 이미 쓴 글감은 지난 트리의 사람이 적은 칸을 잃지 않는다
for old in previous.get("topics", {}).get(topic, {}).get("kinds", {}).get(kind, []):
if old.get("slug") == node["slug"]:
for key in ("classification", "missing-verification", "source", "code"):
if old.get(key):
node[key] = old[key]
items.append(node)
# 아직 글이 없는 글감은 지난 tree 에서 가져와 유지한다
written = {i["slug"] for i in items if i["slug"]}
for old in previous.get("topics", {}).get(topic, {}).get("kinds", {}).get(kind, []):
if not old.get("file") and old.get("slug") not in written:
items.append(old)
entry["kinds"][kind] = items
topics[topic] = entry
ssot_path = os.path.join(base, ssot) if ssot else None
digest = None
if ssot_path and os.path.exists(ssot_path):
digest = hashlib.sha256(open(ssot_path, "rb").read()).hexdigest()
return {
"schemaVersion": 2,
"project": project,
"ssot": ssot,
"ssotSha256": digest,
"generatedAt": datetime.date.today().isoformat(),
"note": "글감 목록이다. file 이 있으면 이미 쓴 기록이고, 없으면 아직 쓰지 않은 글감이다. "
"ssotSha256 이 지금 final/document.md 와 다르면 SSOT 가 바뀐 뒤 트리를 다시 보지 않은 것이다.",
"readinessValues": ["READY", "NEEDS_EVIDENCE", "BLOCKED", "REJECTED"],
"topics": topics,
}
def main(argv: list[str]) -> int:
projects = argv[1:] or [
os.path.basename(os.path.dirname(p))
for p in glob.glob(os.path.join(ROOT, "docs/*/tech-log-studio"))
]
for project in sorted(projects):
tree = build(project)
out = os.path.join(ROOT, "docs", project, "tech-log-studio", "tech-log-tree.json")
with open(out, "w", encoding="utf-8") as fh:
json.dump(tree, fh, ensure_ascii=False, indent=2)
fh.write("\n")
n = sum(len(v) for t in tree["topics"].values() for v in t["kinds"].values())
print(f"{os.path.relpath(out, ROOT)} — 주제 {len(tree['topics'])} · 글감 {n}")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))