From 9927d31e692411ec799c3d58574237e3fcaadd49 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Tue, 22 Sep 2026 20:19:05 +0900 Subject: [PATCH] fix(verify): preserve CRLF when promoting records --- app/verify/promote.py | 6 ++++-- tests/verify/test_promote_crossref.py | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/verify/promote.py b/app/verify/promote.py index 1466916..a6efa21 100644 --- a/app/verify/promote.py +++ b/app/verify/promote.py @@ -25,7 +25,9 @@ CROSSREF_CACHE_PATH = STATE_DIR / "crossref_cache.jsonl" # A top-level, one-key-per-line "verified": false entry (2-space indented). -_VERIFIED_FALSE_RE = re.compile(r'^( )"verified": false(,?)[ \t]*$', re.MULTILINE) +# Permit either common newline convention. With ``re.MULTILINE``, ``$`` sits +# before ``\n`` but after the preceding ``\r`` in CRLF files. +_VERIFIED_FALSE_RE = re.compile(r'^( )"verified": false(,?)[ \t]*(\r?)$', re.MULTILINE) class PromotionDecision(NamedTuple): @@ -86,7 +88,7 @@ def flip_verified_text(raw: str) -> str | None: Returns None (refuse) unless exactly one such token exists, so we never touch a record that isn't shaped the way we expect. """ - new, n = _VERIFIED_FALSE_RE.subn(r'\g<1>"verified": true\g<2>', raw) + new, n = _VERIFIED_FALSE_RE.subn(r'\g<1>"verified": true\g<2>\g<3>', raw) return new if n == 1 else None diff --git a/tests/verify/test_promote_crossref.py b/tests/verify/test_promote_crossref.py index fa155c7..07f3810 100644 --- a/tests/verify/test_promote_crossref.py +++ b/tests/verify/test_promote_crossref.py @@ -95,6 +95,14 @@ def test_flip_refuses_already_true(): assert promote.flip_verified_text(SEED.replace("false", "true")) is None +def test_flip_preserves_crlf(): + crlf_seed = SEED.replace("\n", "\r\n") + out = promote.flip_verified_text(crlf_seed) + assert out is not None + assert ' "verified": true,\r\n' in out + assert "\n" not in out.replace("\r\n", "") + + def test_write_back_atomic_lf_preserved(): path = Path(__file__).parent / "_scratch_seed.json" try: