형식 관문 여덟이 확신 승격·수치 조작·화살표 뒤집기·필수 칸 삭제를 하나도 못 막는 것이 재현됐다. 그 가운데 결정적으로 판정 가능한 것을 코드로 옮긴다. check-required-content.py — 종류가 요구하는 칸이 없거나 비었는지 본다. audit-records.py 는 평문 칸 안에 마크업이 있는지만 보고 칸이 있는지는 안 센다. 고정 목차·자료 개수·답은 강제하지 않는다. 답이 없는 QUESTION 은 정상이고 「다음 검증」이 빈 것만 결함이다. 대상이 성립하지 않으면(프로젝트 없음·계약 없음) exit 2 로 막고, 계약은 있고 기록이 0건이면 통과시키되 초록으로 두지 않는다. 「봤고 괜찮다」와 「볼 것이 없어서 통과」는 다르다. check-preservation.py — 윤문 전후를 견준다. 지금 관문 가운데 편집 전후를 보는 것이 하나도 없어 수치를 바꾸거나 유보를 지운 편집이 그대로 통과했다. 사라진 것과 새로 생긴 것을 따로 센다. 새로 생긴 수치는 지어낸 값일 수 있다. 유보 표현이 줄면 내되 옳은지는 판정하지 않는다. 늘어난 것은 세지 않는다. review-package.py — 아무것도 판정하지 않는다. 판정할 사람이 받을 것을 모은다. 해시·검사기 버전·여기서 실제로 돌린 관문·주장 후보·판정 기준. 종료 코드로 안 걸리는 것은 warnings 로 따로 올린다 — 확신 승격이 딱 그 모양이라 안 실으면 아무도 못 본다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
79 lines
3.2 KiB
Python
79 lines
3.2 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_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()
|