perf(M01): 요소·표 저장도 흔들리는 로직만 검사 — 「어느 로직이 무엇을 보는지」 지도 (일감 33)

- 인력 한 줄 저장 7.0초 → 0.9초 · 자재품목 7.2초 → 2.0초 · 소요량 표 1.7초 ·
  자재품목 저장 뒤 목록 2.5초 → 0.45초 · 켠 뒤 첫 목록 3.4초 → 1.6초
- 로직 줄을 셀 때 `Master.get` 을 덧옷(`_Watch`)으로 감싸 그 줄이 본 자리를 모음 —
  키 · 「ID:원문번호」 · 없는 키를 물은 것까지 · 재료 고르기 줄은 자재품목 묶음 통째
- 저장은 그 지도로 흔들리는 줄만 검사(고친 키 · 고치기 전후 원문번호 · 지운 줄) ·
  요소를 고친 저장은 연결·준용 검사를 그대로 돌림
- 저장 뒤 목록은 흔들린 자리를 알려 받아(`stale(names, marks)`) 그 줄만 다시 셈 ·
  다른 창이 고친 파일이면 예전처럼 통째로 다시 셈
- 자재품목을 「구분」 으로 미리 갈라 둠(`master_material.by_class`) — 고르기 후보를
  3만 줄에서 훑지 않음 · 가른 차례는 파일 차례 그대로라 고르는 줄은 안 바뀜
- 전수 확인 — 요소·표 파일 90 개마다 줄을 통째로 비우고 로직 1,354 줄을 다시 검사,
  달라진 줄이 모두 지도 안(지도 밖 0)
- 시험 `test_m01_cache.py` 에 셋 더함(열둘) — 쓰이는 요소를 지우면 저장이 막힘 ·
  요소를 고쳐 저장한 뒤 막힘 표시가 줄마다 검사한 것과 같음 · 인력 줄을 비운 전수 대조

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-22 02:59:52 +09:00
co-authored by Claude Opus 5
parent 589403f536
commit 3a4823b6da
6 changed files with 193 additions and 34 deletions
+58
View File
@@ -189,3 +189,61 @@ def test_미리_읽어_두면_첫_목록이_기다리지_않는다(client: TestC
start = time.perf_counter()
assert _logics(client)
assert time.perf_counter() - start < 1.0 # 미리 안 읽으면 2~3초
def _plain(file: str) -> dict:
"""정본 파일 그대로(수는 글이 아닌 그대로) — 저장에 실어 보낼 줄을 꺼내려고."""
return json.loads((store.FOLDER / file).read_text(encoding="utf-8"))
def _save_file(client: TestClient, file: str, changes: list[dict]):
"""정본 파일을 저장 길로 고침 — 요소·표 줄도 같은 길."""
return client.post(
"/api/m01/save",
json={"files": [{"file": file, "version": store.read(file)[1], "changes": changes}]},
)
def test_쓰이는_요소를_지우면_저장이_막힌다(client: TestClient) -> None:
"""요소 저장도 좁혀 검사하지만 그 요소를 쓰던 로직은 그대로 걸림."""
_logics(client) # 미리 세어 둔 값이 선 평소 모양
res = _save_file(client, "인력.json", [{"op": "delete", "key": "LB000014"}])
assert any("LB000014" in x for x in _errors(res))
def test_요소를_고쳐_저장해도_막힘_표시가_전수와_같다(client: TestClient) -> None:
"""요소를 고친 뒤 다시 센 막힘이 줄마다 검사한 것과 같은지 — 좁혀 세는 길."""
was = _logics(client)
raw = _plain("인력.json")
row = {**next(r for r in raw["줄"] if str(r.get("키")) == "LB000014"), "비고": "저장 검사 시험"}
res = _save_file(client, "인력.json", [{"op": "edit", "key": "LB000014", "row": row}])
assert res.status_code == 200, res.text
listed = _logics(client)
files, whole = store.loaded()
rows = {
str(r.get("키")): r for _, d in files.items() if d.get("그룹") == "로직" for r in d["줄"]
}
assert [x["키"] for x in listed] == [x["키"] for x in was]
for one in listed:
assert one["reasons"] == store.cm.mf.check_logic(whole, rows[one["키"]])[0]
def test_요소_지도가_전수_검사와_같다(client: TestClient) -> None:
"""「어느 로직이 어느 요소를 보는지」 지도가 놓치는 자리가 없는지 —
인력 줄을 통째로 비우고 1,354 줄을 다 검사해, 달라진 줄이 지도 안에 드는지 봄."""
counts = store.cache.counted(store.FOLDER)
files, _ = store.loaded()
marks = set()
for row in files["인력.json"]["줄"]:
key = str(row.get("키"))
marks |= {key, "#" + key[:2]}
if row.get("원문번호"):
marks.add(f"{key[:2]}:{row['원문번호']}")
marked = {key for key, got in counts.items() if got[2] & marks}
empty = {**files, "인력.json": {**files["인력.json"], "줄": []}}
other = store.cm.mf.Master(empty)
rows = {
str(r.get("키")): r for _, d in files.items() if d.get("그룹") == "로직" for r in d["줄"]
}
changed = {k for k, r in rows.items() if store.cm.mf.check_logic(other, r)[0] != counts[k][0]}
assert changed and changed <= marked