글감 1,001개 중 제1부(§3~§11) 앵커를 하나라도 가진 것은 112개뿐이었다. 나머지 889개는
제2부 모듈 분석 65편의 절 제목에서 나온 것이고, 그것이 재판정이 필요했던 이유다.
주제 44 → 16 (43개가 독자 질문 없이 있었다. 지금은 전부 있다)
글감 1,001 → 123 (제1부 앵커 112 + 제1부가 채택했는데 비어 있던 자리 11)
후보 965 → 1,088 · PENDING 905 → 0
error 3,042 → 0
내려온 889개는 후보 대장에 KEEP_IN_SSOT 로 남는다 — 버린 것이 아니라 분석에 남기고 독립
기록으로 만들지 않기로 한 것이다. 그 글감을 받치던 기록 파일 828개는 지웠다. 계약이 정본이고,
파일이 남아 있다는 이유로 계약에서 뺀 주제가 되살아나면 안 된다. 이력에는 그대로 있다 —
git checkout a0ca2bb -- <경로>.
제1부가 채택했는데 글감이 없던 자리 열하나를 채웠다: mongo high-water mark 가 재전달 이벤트를
삼킨 P1, admin plane 이 가드만 켜고 서비스는 켜지 않은 것과 그 짝인 결정, 실패 어휘 세 층과
SQLState 매트릭스 병합 규칙, 부하 아래에서만 새는 admission 경계, 발행 증거와 완료 판정의
분리, keyset·JSONB 결정 둘.
Concept 17개에 basis-version 을 채우고, 계약 제목과 기록 제목이 갈라져 있던 23건을 기록 쪽에
맞췄다. candidateScope 에 excludedAnchorPattern 을 적어 제2부 앵커만 가진 글감이 다시 올라올
수 없게 한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
5.6 KiB
Python
Executable File
149 lines
5.6 KiB
Python
Executable File
#!/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))
|