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:
co-authored by
Claude Opus 5
parent
2109f726fe
commit
ab59130196
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user