#!/usr/bin/env python3 """`tech-log-tree.json` 의 파생 칸을 다시 채운다. **이 파일이 정본이다.** 주제·글감·readiness·source·classification·relations 는 사람이 적고, 이 스크립트는 손대지 않는다. 기록 파일을 읽어 채우는 것은 넷뿐이다. file 그 글감의 기록이 디스크에 있으면 상대 경로 publication 게시됨 | 초안 | 미작성 status 기록 frontmatter 의 status studioId · assets · evidenceFiles **readiness 와 publication 은 다른 것이다.** 증거가 갖춰진 정도와 Studio 에 올렸는지를 섞지 않는다. 계약에 없는 기록이 디스크에 있으면 `unlisted` 에 적는다 — 지우지도, 몰래 주제로 만들지도 않는다. python3 scripts/build-tech-log-tree.py [프로젝트 ...] """ from __future__ import annotations import datetime import glob import json import os import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import techlog # noqa: E402 from techlog import KINDS # noqa: E402 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DERIVED = ("file", "publication", "status", "studioId", "assets", "assetFiles", "evidenceFiles") def front_matter(path: str) -> tuple[dict, str]: text = open(path, encoding="utf-8").read() if not text.startswith("---"): return {}, text 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, text def records_on_disk(studio: str) -> dict[tuple[str, str], str]: found = {} for path in sorted(glob.glob(os.path.join(studio, "*", "*", "*.md"))): parts = path.split(os.sep) topic_dir, kind_dir = parts[-3], parts[-2] if topic_dir.startswith("_") or kind_dir not in KINDS: continue fm, _ = front_matter(path) slug = fm.get("slug") or os.path.basename(path)[:-3] found[(kind_dir, slug)] = path return found def build(project: str) -> tuple[dict | None, list[str]]: base = os.path.join(ROOT, "docs", project) studio = os.path.join(base, "tech-log-studio") index_path = os.path.join(studio, "tech-log-tree.json") index = techlog.load_index(index_path) if index is None: return None, [f"{project}: tech-log-tree.json 이 없다. 글감을 먼저 적는다"] disk = records_on_disk(studio) used: set[tuple[str, str]] = set() for _, kind, node in techlog.nodes(index): for key in DERIVED: node.pop(key, None) node["publication"] = "미작성" key = (kind, node.get("slug", "")) if not node.get("slug") or key not in disk: continue path = disk[key] fm, text = front_matter(path) studio_id = fm.get("id", "") node.update({ "file": os.path.relpath(path, studio), "status": fm.get("status", ""), "studioId": studio_id, "publication": "게시됨" if studio_id else "초안", "assets": re.findall(r"^ - key: (\S+)", text, re.M), # 배정한 SSOT 그림을 기록이 실제로 쓰는지 대조하려면 key 가 아니라 파일이 필요하다 "assetFiles": [os.path.basename(f)[:-4] if f.endswith(".svg") else os.path.basename(f) for f in re.findall(r"^ file: (\S+)", text, re.M)], "evidenceFiles": re.findall(r"^ - (\.\./\S+)", text, re.M), }) used.add(key) unlisted = [os.path.relpath(p, studio) for k, p in sorted(disk.items()) if k not in used] total = sum(1 for _ in techlog.nodes(index)) written = sum(1 for _, _, n in techlog.nodes(index) if n.get("file")) ssot = index.get("ssot", "final/document.md") index["ssotSha256"] = techlog.sha256_of(os.path.join(base, ssot)) index["generatedAt"] = datetime.date.today().isoformat() index["counts"] = { "topics": len(index.get("topics", {})), "nodes": total, "written": written, "unwritten": total - written, "unlisted": len(unlisted), "candidates": len(index.get("candidates", [])), } index["unlisted"] = unlisted warnings = [] if unlisted: warnings.append(f"계약에 없는 기록 {len(unlisted)}건이 디스크에 있다 — " "글감으로 올리거나 지운다") return index, warnings 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")) if not os.path.basename(os.path.dirname(p)).startswith("_") ] failed = 0 for project in sorted(projects): index, messages = build(project) if index is None: failed += 1 for line in messages: print(line) continue 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(index, fh, ensure_ascii=False, indent=2) fh.write("\n") c = index["counts"] extra = f" · 계약 밖 기록 {c['unlisted']}" if c.get("unlisted") else "" print(f"{os.path.relpath(out, ROOT)} — 주제 {c['topics']} · 글감 {c['nodes']} " f"(쓴 것 {c['written']}){extra}") for line in messages: print(f" ! {line}") return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))