From 44ac56b4d633efafea9373929ca885880252570d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 19 Oct 2026 05:31:57 +0200 Subject: [PATCH] fix: date prose by its citations, not its wording A note reworded around unchanged citations took the rewording commit as its writing revision. That commit's pin is current, so citations a bump had left behind stopped being reported. A sentence end now separates a project word from the citation before it, like a comma. --- scripts/profile_sync.py | 29 ++++++++++++++++++++++++++--- tests/test_profile_sync.py | 22 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 752ca7c2..c2908b0a 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -383,8 +383,9 @@ def _walk_document( # What separates one citation from the next in prose. lynx-emu writes # "...:112; mednafen src/lynx/system.cpp:86-93": without the semicolon the -# project word read as prose and the citation was charged to the fork. -_PROSE_LIST_DELIMITERS = ",;:(" +# project word read as prose and the citation was charged to the fork. A +# sentence end separates them the same way. +_PROSE_LIST_DELIMITERS = ",;:(." def _prose_external( @@ -2845,6 +2846,26 @@ def _pin_of(document: dict, field: str, mode: str = "") -> str | None: return value if isinstance(value, str) and value else None +_CITATION_RUN_RE = re.compile( + PROSE_CITE_RE.pattern + r"(?:, *\d+(?:-\d+)?)*" +) + + +def _scalar_citations(text: str | None) -> tuple[str, ...] | None: + """The citations a scalar carries, in order, without the prose around them. + + A note reworded around unchanged citations was still written under the + pin those citations describe: comparing whole texts moved the writing + revision to the rewording commit, whose pin is current, and the stale + citations were never offered for realignment again. + """ + if text is None: + return None + return tuple( + _citation_key(m.group(0)) for m in _CITATION_RUN_RE.finditer(text) + ) + + def _writing_pins( revisions: list[tuple[str, dict]], intro_sha: str, field: str, mode: str = "" ) -> list[str]: @@ -3098,7 +3119,9 @@ def realign_prose( for field in fields: if not alive[field]: continue - if values.get(field) == current_values.get(field): + if _scalar_citations(values.get(field)) == _scalar_citations( + current_values.get(field) + ): intro[field] = sha else: alive[field] = False diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 4cc3607f..9d319522 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -3141,6 +3141,7 @@ class TestCollectCitations(unittest.TestCase): for line in ( "see Lynx.emu/src/lynx/system.cpp:112; mednafen src/lynx/system.cpp:86-93", "see Lynx.emu/src/lynx/system.cpp:112, mednafen src/lynx/system.cpp:86-93", + "see Lynx.emu/src/lynx/system.cpp:112. mednafen src/lynx/system.cpp:86-93", ): with self.subTest(line=line): start = line.index("src/lynx/system.cpp:86") @@ -3718,6 +3719,27 @@ class TestRealignProse(unittest.TestCase): ) self.assertEqual(self.path.read_text(), before) + def test_rewording_around_citations_keeps_the_writing_pin(self): + """Punctuation edited in a note after a bump is not a new writing. + + Keyed on the whole text, the rewording commit became the writing + revision, its pin was current, and the bump that had left the + citations behind was no longer reported. + """ + self._repo_with_advanced_pin() + self._commit(PROSE_SAMPLE, "advance the pin") + self._commit( + PROSE_SAMPLE.replace("on boot.", "at boot."), "reword the note" + ) + self.files[("oldpin", "a.c")] = ["x"] * 9 + ["subject"] + self.files[("pin", "a.c")] = ["x"] * 13 + ["subject"] + self.files[("oldpin", "b.c")] = list("pqrstuvwx") + self.files[("pin", "b.c")] = list("pqrstuvwx") + messages = profile_sync.realign_prose(self.path, self.tmp.name, dry_run=True) + self.assertTrue( + any("pin moved under this text" in m for m in messages), messages + ) + def test_a_citation_in_an_unmoved_upstream_needs_no_reading(self): """nethersx2 cites AetherSX2 files that only its upstream carries, at an upstream pin that never moved. The port's pin did move under