Files
document-haness/scripts/tests/test_preservation.py
T
DongHyeonkaandClaude Opus 5 f1637ee532 fix(check-preservation): 절을 통째로 지우면 아무것도 안 움직이던 자리
「확인하지 못한 것」 절을 통째로 지운 편집이 경고를 비운 채 지나갔다. 배관 문제가 아니라
실을 것이 없었다 — 지운 절에 보호 구간이 없었고 그 절의 문장이 유보 목록에 없었다.
그래서 「경고가 비면 자동 통과」라는 읽기 계약으로도 그대로 통과한다.

유보 목록이 추정·가능성 쪽에만 몰려 있었다. 한계를 밝히는 말은 대개 「안 했다」 모양인데
그쪽이 얇았다. 두 갈래로 나누고 뒤쪽을 채웠다.

그리고 `##` 절이 통째로 사라지면 그 자체를 경고에 올린다. 무엇이 사라졌는지는 절 제목으로
충분하다.

error 를 늘리지 않았다. 판정하는 것이 아니라 보이게 하는 것이다. 절을 덜어 낸 것인지
한계를 지운 것인지는 근거를 읽어야 안다.

채택된 편집 100쌍을 다시 돌려 막은 쌍이 1 그대로인 것을 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:27:49 +09:00

110 lines
5.0 KiB
Python

"""윤문 전후에 보호 구간이 그대로인지 보는 검사기.
정상 윤문은 통과하고, 수치·코드·인용·URL 을 건드린 편집은 걸린다.
유보 표현이 줄어든 것은 걸러 내되 판정하지 않는다 — 판단은 근거를 읽는 검토가 한다.
"""
import importlib.util
import os
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
F = os.path.join(ROOT, "scripts", "tests", "fixtures", "preservation")
_spec = importlib.util.spec_from_file_location(
"check_preservation", os.path.join(ROOT, "scripts", "check-preservation.py"))
cp = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(cp)
def _read(name):
return open(os.path.join(F, name), encoding="utf-8").read()
class PreservationTest(unittest.TestCase):
def setUp(self):
self.before = _read("before.md")
def test_an_ordinary_rewrite_passes(self):
res = cp.compare(self.before, _read("after-ok.md"))
self.assertEqual([], res["findings"])
self.assertEqual([], res["hedgesDropped"])
def test_a_number_changed_in_prose_is_caught(self):
res = cp.compare(self.before, _read("after-tampered.md"))
values = {f["value"] for f in res["findings"] if f["kind"] == "수치"}
self.assertIn("14ms", values)
self.assertIn("4ms", values)
def test_a_number_changed_inside_a_code_block_is_caught(self):
res = cp.compare(self.before, _read("after-tampered.md"))
blocks = {f["value"] for f in res["findings"] if f["kind"] == "코드블록"}
self.assertTrue(any("exit=1" in b for b in blocks))
self.assertTrue(any("exit=0" in b for b in blocks))
def test_a_changed_direct_quotation_is_caught(self):
res = cp.compare(self.before, _read("after-tampered.md"))
kinds = {f["kind"] for f in res["findings"]}
self.assertIn("직접인용", kinds)
def test_a_changed_url_is_caught(self):
res = cp.compare(self.before, _read("after-tampered.md"))
kinds = {f["kind"] for f in res["findings"]}
self.assertIn("URL", kinds)
def test_dropped_hedges_are_surfaced_without_a_verdict(self):
"""유보가 줄면 낸다. 옳은지 그른지는 말하지 않는다."""
res = cp.compare(self.before, _read("after-tampered.md"))
dropped = {h["word"] for h in res["hedgesDropped"]}
self.assertIn("확인하지 못했다", dropped)
self.assertIn("로컬", dropped)
self.assertEqual(0, res["hedgeTotalAfter"])
def test_adding_a_hedge_is_not_reported(self):
"""유보를 더하는 것은 이 규범에서 안전한 쪽이다."""
after = self.before.replace("14ms 였다", "14ms 였다. 다만 한 번만 쟀다")
res = cp.compare(self.before, after)
self.assertEqual([], res["hedgesDropped"])
def test_a_whole_section_removed_is_surfaced(self):
"""절을 통째로 지우면 안에 보호 구간이 없어도 보여야 한다.
「확인하지 못한 것」 절을 지우는 편집이 그 모양이다. 낱말 계수만 보면
아무것도 안 움직여서 경고가 비고, 「경고가 비면 자동 통과」로 그대로 지나간다.
"""
before = ("# 제목\n\n요약이다.\n\n## 결론\n\n값은 14ms 였다.\n\n"
"## 확인하지 못한 것\n\n다른 환경에서는 세 보지 않았다.\n")
after = "# 제목\n\n요약이다.\n\n## 결론\n\n값은 14ms 였다.\n"
res = cp.compare(before, after)
self.assertEqual([], res["errors"], "절 삭제는 error 가 아니다")
gone = [w["value"] for w in res["warnings"] if w["kind"] == "절"]
self.assertIn("## 확인하지 못한 것", gone)
def test_a_section_that_stays_is_not_reported(self):
"""대조군. 절이 그대로면 아무것도 안 나온다."""
text = "# 제목\n\n요약이다.\n\n## 결론\n\n값은 14ms 였다.\n"
self.assertEqual([], cp.compare(text, text)["warnings"])
def test_limitation_hedges_are_counted(self):
"""「안 했다」로 한계를 밝히는 말도 유보다.
목록이 추정·가능성 쪽에만 몰려 있으면 한계를 적은 문장을 지워도 계수가 안 움직인다.
"""
for word in ("세 보지 않았다", "돌려 보지 않았다", "재 보지 않았다", "확인 안 했다"):
with self.subTest(word=word):
before = f"값은 14ms 였다. 다른 환경에서는 {word}."
after = "값은 14ms 였다."
dropped = {h["word"] for h in cp.compare(before, after)["hedgesDropped"]}
self.assertIn(word, dropped)
def test_a_missing_file_is_not_reported_as_clean(self):
import subprocess
p = subprocess.run(
["python3", os.path.join(ROOT, "scripts", "check-preservation.py"),
os.path.join(F, "before.md"), os.path.join(F, "nope.md")],
cwd=ROOT, capture_output=True, text=True)
self.assertEqual(2, p.returncode)
if __name__ == "__main__":
unittest.main()