From 9992490854e0590f7210fb0dec7967a1efef90a9 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:25:03 +0200 Subject: [PATCH] fix: track citations a source_ref part drops Annotations and text after a located head were discarded before anchoring, so a citation written there never moved with its pin: zesarux cited two macros four lines off. The leftover text of every source_ref is now scanned as prose, and the two citations are fixed. --- emulators/zesarux.yml | 4 +-- scripts/profile_sync.py | 51 +++++++++++++++++++++++++++++++++ tests/test_audit_regressions.py | 37 ++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/emulators/zesarux.yml b/emulators/zesarux.yml index 07995828..ef0031e6 100644 --- a/emulators/zesarux.yml +++ b/emulators/zesarux.yml @@ -202,7 +202,7 @@ files: required: true bundled: true system: sinclair-zx-spectrum - source_ref: "src/cpu.c:3381-3383, src/utils.c:4100 (MACHINE_IS_SPECTRUM_128_P2 covers Pentagon, src/cpu.h:545)" + source_ref: "src/cpu.c:3381-3383, src/utils.c:4100 (MACHINE_IS_SPECTRUM_128_P2 covers Pentagon, src/cpu.h:549)" - name: "se.rom" sha1: "14321f2092b8af21b52b35c1518d9a753d6618d4" @@ -346,7 +346,7 @@ files: required: true bundled: true system: timex-tc2048 - source_ref: "src/cpu.c:3418-3420, src/utils.c:4098 (TC2048 falls under MACHINE_IS_SPECTRUM_16_48, src/cpu.h:543)" + source_ref: "src/cpu.c:3418-3420, src/utils.c:4098 (TC2048 falls under MACHINE_IS_SPECTRUM_16_48, src/cpu.h:547)" - name: "ts2068.rom" sha1: "34cd8113bf75b65a4da6eea355f9d46ad635f7e4" diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 3053c914..19c522b1 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -158,6 +158,48 @@ def split_source_ref(ref: str) -> list[RefPart]: return parts +def ref_prose_text(ref: str) -> str: + """The text of a source_ref its parts do not read, positions kept. + + split_source_ref keeps the located head of each chunk and drops the + rest: annotations, and prose after the head. A citation written there + (`(MACHINE_IS_SPECTRUM_128_P2 covers Pentagon, src/cpu.h:545)`, or the + second half of `pce.cpp:477-483 + huc.cpp:284-304`) was never anchored. + Every character a part reads is blanked, so the prose scanner finds + the rest at the offsets it has in the scalar. + """ + masked = list(ref) + depth = 0 + outside: list[int] = [] + + def settle() -> None: + text = "".join(ref[i] for i in outside) + head = text.strip().split(" ", 1)[0] if text.strip() else "" + located = bool(head) and bool( + _LOCATED_RE.match(head) or _BARE_RANGE_RE.match(head) + ) + blank = outside + if located: + first = next(k for k, i in enumerate(outside) if not ref[i].isspace()) + blank = outside[first:first + len(head)] + for i in blank: + masked[i] = " " + outside.clear() + + for index, char in enumerate(ref): + if char == "(": + depth += 1 + if depth == 0: + if char in ",;": + settle() + else: + outside.append(index) + if char == ")": + depth = max(0, depth - 1) + settle() + return "".join(masked) + + def split_outside_parentheses(text: str) -> list[str]: """Split on commas and semicolons, ignoring those inside an annotation. @@ -676,6 +718,15 @@ def collect_citations(document: dict, text: str | None = None) -> list[Citation] field=field, kind="ref", ref=ref, entry=holder, holder=holder, key=key, label=label, )) + if isinstance(value, str): + for line, path_span, spans, parts, run, wrapped in _prose_runs( + ref_prose_text(value), known + ): + citations.append(Citation( + field=field, kind="prose", ref=run, holder=holder, + key=key, line=line, path_span=path_span, spans=spans, + parts=parts, wrapped=wrapped, + )) continue for line, path_span, spans, parts, run, wrapped in _prose_runs( value, known diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 26a757c3..d0a7f10e 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -3003,3 +3003,40 @@ class ProseCitationsUseTheTrackedForm(unittest.TestCase): for match in self.UNTRACKED.finditer(line) ] self.assertEqual(found, []) + + +class ACitationInsideASourceRefIsTracked(unittest.TestCase): + """split_source_ref reads each chunk's located head and drops the rest. + zesarux cited src/cpu.h:545 inside an annotation, four lines off at its + pin, and pce-emu joined two citations with ` + `: neither was anchored.""" + + def citations(self, ref: str) -> list[str]: + from scripts.profile_sync import collect_citations + + document = {"files": [{"name": "x.rom", "source_ref": ref}]} + return [(c.kind, c.ref) for c in collect_citations(document)] + + def test_an_annotation_citation_is_read_as_prose(self): + found = self.citations( + "src/cpu.c:3381-3383, src/utils.c:4100 " + "(MACHINE_IS_SPECTRUM_128_P2 covers Pentagon, src/cpu.h:549)" + ) + self.assertIn(("prose", "src/cpu.h:549"), found) + self.assertNotIn(("prose", "src/cpu.c:3381-3383"), found) + + def test_a_joined_citation_is_read_as_prose(self): + found = self.citations( + "src/pce_fast/pce.cpp:477-483 + src/pce_fast/huc.cpp:284-304" + ) + self.assertIn(("prose", "src/pce_fast/huc.cpp:284-304"), found) + self.assertEqual([c for c in found if c[0] == "prose"], + [("prose", "src/pce_fast/huc.cpp:284-304")]) + + def test_offsets_match_the_scalar(self): + from scripts.profile_sync import ref_prose_text + + ref = "a.c:1-2, 5 (see b.c:7)" + masked = ref_prose_text(ref) + self.assertEqual(len(masked), len(ref)) + self.assertEqual(masked.index("b.c:7"), ref.index("b.c:7")) + self.assertNotIn("a.c", masked)