diff --git a/scripts/check-preservation.py b/scripts/check-preservation.py index 624de8d..a238575 100644 --- a/scripts/check-preservation.py +++ b/scripts/check-preservation.py @@ -46,14 +46,28 @@ EXTRACTORS: dict[str, re.Pattern[str]] = { r"(?![0-9A-Za-z_.\-])"), } -# 유보 표현. 늘어난 것은 세지 않고 줄어든 것만 낸다 +# 유보 표현. 늘어난 것은 세지 않고 줄어든 것만 낸다. +# +# 두 갈래다. 앞쪽은 **확신을 낮추는 말**이고, 뒤쪽은 **「안 했다」로 한계를 밝히는 말**이다. +# 처음에는 앞쪽에만 몰려 있었는데, 그러면 「확인하지 못한 것」 절을 통째로 지워도 계수가 +# 안 움직인다 — 그 절의 문장이 대개 뒤쪽 모양이기 때문이다. HEDGES = ( + # 확신을 낮춘다 "추정", "가능성", "보인다", "보였다", "아마", "듯", "일 수 있다", "일지도", - "확인하지 못했다", "확인하지 않았다", "미확인", "안 봤다", "못 봤다", - "모른다", "정하지 않았다", "재지 않았다", "돌리지 않았다", "열지 않았다", - "로컬", "이 환경에서", "이번에는", "한정", "범위 안", + "모른다", "로컬", "이 환경에서", "이번에는", "한정", "범위 안", + # 「안 했다」로 한계를 밝힌다 + "확인하지 못했다", "확인하지 않았다", "확인 안 했다", "미확인", + "세지 않았다", "세 보지 않았다", "재지 않았다", "재 보지 않았다", + "안 쟀다", "못 쟀다", "돌리지 않았다", "돌려 보지 않았다", "안 돌렸다", "못 돌렸다", + "열지 않았다", "열어 보지 않았다", "안 봤다", "못 봤다", + "대조하지 않았다", "시험하지 않았다", "정하지 않았다", "만들지 않았다", + "하지 않았다", "않은 것", "못 한 것", ) +# `## 이름` 절. 통째로 사라진 절은 그 자체를 낸다 — 안에 보호 구간이 없으면 +# 다른 어떤 계수도 안 움직인다. 「확인하지 못한 것」 절을 지우는 편집이 그 모양이다 +SECTION = re.compile(r"^##\s+(.+?)\s*$", re.M) + def _counts(text: str) -> dict[str, collections.Counter]: out = {} @@ -98,6 +112,14 @@ def compare(before: str, after: str) -> dict: "sentence": _sentence_of(after, value), "note": "편집 전에 없던 값이다"}) + # 절이 통째로 사라졌나. 제목만으로 무엇이 사라졌는지 충분하다 + gone = [t for t in SECTION.findall(before) if t not in set(SECTION.findall(after))] + for title in gone: + warnings.append({"kind": "절", "change": "사라짐", "count": 1, "value": f"## {title}", + "sentence": _sentence_of(before, f"## {title}"), + "note": "절이 통째로 없어졌다. 덜어 낸 것인지 한계를 지운 것인지는 " + "근거를 읽어야 안다"}) + hb, ha = _hedges(before), _hedges(after) hedge = [] for w in sorted(hb - ha): diff --git a/scripts/tests/test_preservation.py b/scripts/tests/test_preservation.py index cff5572..ffb6dc3 100644 --- a/scripts/tests/test_preservation.py +++ b/scripts/tests/test_preservation.py @@ -65,6 +65,37 @@ class PreservationTest(unittest.TestCase): 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(