feat(M01): 로직 지우기 검사 — 모든 층 구조물 일위대가 문서가 담은 로직까지 (39-1b B9)

- M01 저장 전 검사(check_saved)에 labor_errors — 시스템 층 · storage 회사 · 개인 층 labor/*.json 이 담은 로직이 없어지면 막음(프로젝트 층 빼고 · 실무문서는 조합 검사 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-04 17:57:07 +09:00
co-authored by Claude Opus 5.5
parent 5617f9e891
commit ecc4a651a4
2 changed files with 62 additions and 1 deletions
+35 -1
View File
@@ -16,12 +16,17 @@ import threading
from decimal import Decimal
from pathlib import Path
from config import config_system
SCRIPTS = Path(__file__).resolve().parents[1] / "resources/master_data/scripts"
sys.path.insert(0, str(SCRIPTS))
import check_master as cm # noqa: E402
import master_material as mm # noqa: E402 (자재품목 테이블ID — 재료 고르기는 그 묶음을 훑음)
# 구조물 일위대가 문서(M02 `labor/<열>.json` · 39-1b) 시스템 층 — storage 층은 따로 · 시험이 바꿈
LABOR_ROOTS: tuple[Path, ...] = (config_system.PROJECT_ROOT / "resources" / "master_template",)
_FRESH = threading.RLock()
_READ: dict[str, tuple[tuple, dict]] = {} # 파일 이름 → (자국, 읽은 내용)
_INDEX: dict = {} # 폴더 · 자국 · files · whole
@@ -217,7 +222,36 @@ def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> l
out += cm.check_logics(files, whole, book, narrowed and narrowed[:2])
if narrowed and narrowed[2]: # 요소·표를 고쳤으면 연결 · 준용도 봄(로직 좁힘과 따로)
out += cm.check_links(whole)
return out + cm.mcb.check_all(files, whole)
return out + cm.mcb.check_all(files, whole) + labor_errors(whole)
def _labor_files() -> list[Path]:
base = Path(config_system.STORAGE_BASE_DIR)
roots = [*LABOR_ROOTS]
if base.is_dir():
roots += [*base.glob("*/templates"), *base.glob("*/*/templates")] # 프로젝트 빼고
return [p for root in roots for p in sorted((root / "labor").glob("[!._]*.json"))]
def labor_errors(whole) -> list[str]:
"""모든 층 구조물 일위대가가 담은 로직이 마스터에 없으면 한 줄씩 — 쓰는 로직 지우기를 막음
(실무문서 = 마스터 조합은 `check_all` 몫)."""
out = []
for path in _labor_files():
try:
doc = json.loads(path.read_bytes())
except (OSError, ValueError):
continue
for one in doc.get("담은로직") or []:
code = str(one.get("로직") or "") if isinstance(one, dict) else ""
try:
whole.logic(code)
except cm.mf.FormulaError:
where = f"{path.parent.parent.name}/{path.stem}"
out.append(
f"구조물 일위대가 {where} 이 담은 로직 「{code}」 이 없음 — 쓰는 로직은 못 지움"
)
return out
def warm(folder: Path) -> threading.Thread:
+27
View File
@@ -211,3 +211,30 @@ def test_길_틀린_문서_400_없는_열_404_남의_개인_층_403(client):
assert client.post(url.format("system", "없는열"), json={}).status_code == 404
other = client.post(url.format("personal", "rv") + "?owner=7/99", json={})
assert other.status_code == 403
def test_로직_지우기_검사는_모든_층_일위대가를_봄(tmp_path, monkeypatch):
from config import config_system
from M01_MasterData import M01_MasterData_Store_Cache as cache
system, storage = tmp_path / "system", tmp_path / "storage"
for folder, code in (
(system / "labor", STONE),
(storage / "7" / "templates" / "labor", TEN), # 회사 층
(storage / "7" / "42" / "templates" / "labor", STONE), # 개인 층
(storage / "7" / "42" / "p1" / "templates" / "labor", STONE), # 프로젝트 — 안 봄
):
folder.mkdir(parents=True)
body = {"담은로직": [{"로직": code}]}
(folder / "a.json").write_text(json.dumps(body), encoding="utf-8")
monkeypatch.setattr(cache, "LABOR_ROOTS", (system,))
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
class Gone: # STONE 을 지운 마스터
def logic(self, code):
if code == STONE:
raise cache.cm.mf.FormulaError("없음")
got = cache.labor_errors(Gone())
assert len(got) == 2 and all(STONE in x for x in got)
assert any("system/a" in x for x in got) and any("templates/a" in x for x in got)