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>
This commit is contained in:
DongHyeonka
2026-09-17 11:02:02 +09:00
co-authored by Claude Opus 5
parent 2109f726fe
commit ab59130196
1524 changed files with 3160026 additions and 8369 deletions
+97 -7
View File
@@ -1,21 +1,93 @@
"""종류가 요구하는 내용이 채워졌는지 보는 검사기.
고정 사례는 다섯 종류마다 둘이다 — 채운 것과 하나를 뺀 것.
고정 사례는 **여섯** 종류마다 둘이다 — 채운 것과 하나를 뺀 것.
「무조건 통과」도 「무조건 거절」도 아닌 것을 이 짝이 확인한다.
"""
import contextlib
import importlib.util
import json
import os
import shutil
import tempfile
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
FIXTURES = os.path.join(ROOT, "scripts", "tests", "fixtures", "required-content")
# `docs/` 안에 만드는 픽스처의 접두사. 실재 프로젝트와 섞이지 않게 `zz-` 로 시작한다
FIXTURE_PREFIX = "zz-no-contract-"
def _sweep_stale_fixtures(docs: str) -> None:
"""지난 실행이 남긴 픽스처를 치운다.
`finally` 는 SIGKILL 을 못 막는다. 한 번 남으면 그 폴더가 **진짜 프로젝트로
세어져서** `verify-pipeline.py` 가 프로젝트 수를 하나 더 세고 계약이 없다고
error 를 낸다 — 시험이 저장소를 고장 낸 것처럼 보인다. 지우는 것은 이 접두사로
시작하는 것뿐이라 실재 프로젝트를 건드리지 않는다.
"""
for name in os.listdir(docs):
if name.startswith(FIXTURE_PREFIX):
shutil.rmtree(os.path.join(docs, name), ignore_errors=True)
_spec = importlib.util.spec_from_file_location(
"check_required_content", os.path.join(ROOT, "scripts", "check-required-content.py"))
crc = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(crc)
KINDS = ("case", "concept", "reference", "question", "decision")
# 계약의 `RecordKind` 와 같은 여섯이다. 손으로 세지 않는다 — 이 목록에 종류를 빠뜨리면
# 그 종류의 고정 사례가 있어도 **아무도 열어 보지 않는다**
KINDS = tuple(crc.techlog.KINDS)
@contextlib.contextmanager
def _a_contract_with_no_records():
"""계약은 있고 기록이 0건인 프로젝트를 만들어 준다.
이 상태를 살아 있는 프로젝트 이름으로 가리키면, 누가 그 프로젝트에 기록 한 편을
쓰는 순간 시험이 깨진다. 실제로 그렇게 깨졌다. 이름이 아니라 상태가 필요한
시험이므로 여기서 그 상태를 만든다.
"""
docs = os.path.join(ROOT, "docs")
path = tempfile.mkdtemp(prefix="zz-no-records-", dir=docs)
try:
studio = os.path.join(path, "tech-log-studio")
os.makedirs(studio)
name = os.path.basename(path)
with open(os.path.join(studio, "tech-log-tree.json"), "w", encoding="utf-8") as fh:
json.dump({"schemaVersion": 4, "project": name, "ssot": "final/document.md",
"topics": {}, "candidates": []}, fh)
yield name
finally:
shutil.rmtree(path, ignore_errors=True)
@contextlib.contextmanager
def _a_project_without_a_contract():
"""SSOT 는 있고 분해 계약이 없는 프로젝트를 만들어 준다.
`_sweep_stale_fixtures` 를 먼저 부르는 이유는 `finally` 가 못 도는 경우가 있어서다.
이 시험이 SIGKILL 을 맞으면(메모리 부족 등) 픽스처가 `docs/` 에 남고, 그러면
**그것이 진짜 프로젝트로 세어진다** — `verify-pipeline.py` 가 「프로젝트 7」로
찍고 계약이 없으니 error 를 낸다. 실제로 그렇게 됐다. 저장소를 어지럽히지 않는
자리에 두는 것이 낫지만, `check_evidence.mjs <프로젝트>` 가 `docs/` 아래에서만
프로젝트를 찾으므로 여기 두어야 한다. 그래서 다음 실행이 스스로 치운다.
이 상태를 실재 프로젝트 이름으로 가리키면 그 프로젝트가 저장소에서 빠지는 순간
시험이 엉뚱한 이유로 통과한다 — 「계약이 없다」가 「그런 프로젝트가 없다」로 바뀐다.
둘 다 exit 2 라 종료 코드만 보면 구분되지 않는다. 그래서 상태를 여기서 만든다.
"""
docs = os.path.join(ROOT, "docs")
_sweep_stale_fixtures(docs)
path = tempfile.mkdtemp(prefix=FIXTURE_PREFIX, dir=docs)
try:
final = os.path.join(path, "final")
os.makedirs(final)
with open(os.path.join(final, "document.md"), "w", encoding="utf-8") as fh:
fh.write("# 계약 없는 프로젝트\n\n## §1 아무것도 아니다\n\n본문.\n")
yield os.path.basename(path)
finally:
shutil.rmtree(path, ignore_errors=True)
def _report(sub, name):
@@ -47,6 +119,7 @@ class RequiredContentTest(unittest.TestCase):
"reference": "적용 조건",
"question": "다음 검증",
"decision": "판단 이유",
"setup": "pinnedVersions",
}
for kind, part in expected.items():
with self.subTest(kind=kind):
@@ -64,8 +137,22 @@ class RequiredContentTest(unittest.TestCase):
rep = _report("ok", "question")
self.assertEqual(0, rep.error_count)
def test_body_markers_belong_only_to_case_and_concept(self):
self.assertEqual({"case", "concept"}, crc.BODY_KINDS)
def test_body_markers_belong_to_case_concept_and_setup(self):
"""**본문이 있는 종류는 셋이다.** 환경 구성의 본문도 Case 와 같은 파서를 탄다."""
self.assertEqual({"case", "concept", "setup"}, crc.BODY_KINDS)
def test_every_kind_has_a_fixture_pair(self):
"""종류를 더하면서 고정 사례를 안 만들면 위 시험들이 조용히 그 종류를 건너뛴다.
`_report` 가 없는 파일을 열면 터지지만, 그 전에 **어느 종류가 비었는지**를 말해 준다.
「볼 것이 없어서 통과」를 「문제 없음」으로 쓰지 않는 것과 같은 자리다.
"""
for sub in ("ok", "missing"):
for kind in KINDS:
with self.subTest(sub=sub, kind=kind):
self.assertTrue(
os.path.isfile(os.path.join(FIXTURES, sub, f"{kind}.md")),
f"{sub}/{kind}.md 이 없다 — 종류를 더했으면 고정 사례도 둘 만든다")
def _cli(self, *args):
import subprocess
@@ -81,13 +168,15 @@ class RequiredContentTest(unittest.TestCase):
def test_a_project_without_a_contract_does_not_come_back_green(self):
"""계약이 없으면 「볼 것이 없어서 통과」다. 그것을 초록으로 내지 않는다."""
p = self._cli("ca-tmpl")
with _a_project_without_a_contract() as project:
p = self._cli(project)
self.assertEqual(2, p.returncode)
self.assertIn("tech-log-tree.json 이 없다", p.stderr)
def test_a_contract_with_no_records_yet_is_not_an_error(self):
"""아직 안 쓴 것은 결함이 아니다. 다만 초록으로 보이면 안 된다."""
p = self._cli("keycloak-session-store")
with _a_contract_with_no_records() as project:
p = self._cli(project)
self.assertEqual(0, p.returncode)
self.assertIn("아직 안 쓰였다", p.stdout)
@@ -98,7 +187,8 @@ class RequiredContentTest(unittest.TestCase):
저장소 체크아웃 이름을 적는 것을 금지해서(`FORBIDDEN_LITERAL`), 그 이름과 같은
프로젝트를 테스트에 적으면 계약 검사가 깨진다.
"""
p = self._cli("keycloak", "ca-tmpl")
with _a_project_without_a_contract() as project:
p = self._cli("keycloak", project)
self.assertEqual(2, p.returncode)