diff --git a/emulators/emucorex.yml b/emulators/emucorex.yml index 04bb9abc..b5012bfb 100644 --- a/emulators/emucorex.yml +++ b/emulators/emucorex.yml @@ -6,6 +6,7 @@ upstream: "https://github.com/PCSX2/pcsx2" author: "Oleksandr (sashkinbro)" profiled_date: "2026-10-04" source_commit: "9884c9f9a0c5d5df2f0c0c064ec41029a4528d72" +prose_written_at: "9884c9f9a0c5d5df2f0c0c064ec41029a4528d72" upstream_commit: "c10a5c9ad951c517dc48b722b5b84eb9bf7fdb42" core_version: "0.4.7" display_name: "Sony - PlayStation 2 (EmuCoreX)" diff --git a/emulators/fixnes.yml b/emulators/fixnes.yml index 1cf6804f..b9f61fe7 100644 --- a/emulators/fixnes.yml +++ b/emulators/fixnes.yml @@ -5,6 +5,7 @@ source: "https://github.com/libretro/fixNES" upstream: "https://github.com/FIX94/fixNES" profiled_date: "2026-03-23" source_commit: "72af6c6fd9d3b6c23d334ba7385b6a09225f2276" +prose_written_at: "72af6c6fd9d3b6c23d334ba7385b6a09225f2276" core_version: "Alpha v1.3.1" display_name: "Nintendo - NES / Famicom (fixNES)" cores: diff --git a/emulators/zesarux.yml b/emulators/zesarux.yml index ef0031e6..710e0088 100644 --- a/emulators/zesarux.yml +++ b/emulators/zesarux.yml @@ -4,6 +4,7 @@ core_classification: other source: "https://github.com/chernandezba/zesarux" upstream: "https://github.com/chernandezba/zesarux" source_commit: "8e1e8f3fcb5a8e677984df7aeaff30667682ab56" +prose_written_at: "8e1e8f3fcb5a8e677984df7aeaff30667682ab56" profiled_date: "2026-09-17" core_version: "13.1-SN" display_name: "Sinclair - ZX Spectrum and clones (ZEsarUX)" diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index 26a45828..f908a46b 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -800,6 +800,11 @@ "type": "string", "description": "Branch the port was read from, when it is not the default one." }, + "prose_written_at": { + "type": "string", + "pattern": "^[0-9a-f]{40}$", + "description": "The pin a reader confirmed every prose citation against. When the profile's history shows several pins under one text, profile_sync takes this one as the revision the text describes; it never overrides a single candidate." + }, "pin_frozen": { "type": "string", "description": "Why source_commit is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag: comparing it to HEAD says nothing and recaling would repoint its refs at code the build it documents never had." diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index ee663dad..a7e2e998 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -3016,14 +3016,23 @@ def _writing_pairs( Each reading lists (repo, writing pin, current pin) per declared repository. A repository whose text lived under several pins gives one reading per pin; the caller settles them when they all agree. + + `prose_written_at` names the pin a reader confirmed the prose against. + Among several candidate pins it is the answer: fixnes's doOpenFDSBIOS + citation lived under four pins, exact at the last, and with nothing to + record that reading the bump stayed refused for good. It never + overrides a history with a single candidate. """ options: list[list] = [] ambiguous: list[str] = [] + attested = str(document.get("prose_written_at") or "") for pin_field, mode, repo in repos: current = _pin_of(document, f"{pin_field}_commit", mode) if not current: continue written = _writing_pins(revisions, intro_sha, f"{pin_field}_commit", mode) + if attested in written: + written = [attested] if len(written) > 1: ambiguous.append(f"{pin_field}_commit {' -> '.join(written)}") # The current pin among the candidates reads as "no move" (None) for @@ -3336,7 +3345,7 @@ def _stale_prose_blocks(name: str, path: Path, args: argparse.Namespace) -> bool if stale: print( f"{name}: {len(stale)} prose citation(s) written under an older pin; " - "run --realign-prose or read them before the pin moves", + "run --realign-prose, or read them and set prose_written_at to the pin they match", file=sys.stderr, ) return bool(stale) diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 8ea6016b..13123f31 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -2379,6 +2379,39 @@ class TestWriteDryRun(unittest.TestCase): self.assertEqual(readings, [[(repo, "pin1", "pin2")], []]) self.assertTrue(ambiguous) + def test_a_confirmed_pin_settles_the_readings(self): + """fixnes: doOpenFDSBIOS lived under four pins and was exact at the + last; with nothing to record that reading, the bump was refused for + good. prose_written_at names the pin and the readings collapse.""" + revisions = [ + ("c2", {"source_commit": "pin2", "notes": "x"}), + ("c1", {"source_commit": "pin1", "notes": "x"}), + ] + repo = object() + readings, _ambiguous = profile_sync._writing_pairs( + {"source_commit": "pin2", "prose_written_at": "pin2"}, + [("source", "", repo)], revisions, "c1", + ) + self.assertEqual(readings, [[]]) + later, _ = profile_sync._writing_pairs( + {"source_commit": "pin3", "prose_written_at": "pin2"}, + [("source", "", repo)], revisions, "c1", + ) + self.assertEqual(later, [[(repo, "pin2", "pin3")]]) + + def test_a_confirmed_pin_outside_the_history_changes_nothing(self): + revisions = [ + ("c2", {"source_commit": "pin2", "notes": "x"}), + ("c1", {"source_commit": "pin1", "notes": "x"}), + ] + repo = object() + readings, ambiguous = profile_sync._writing_pairs( + {"source_commit": "pin2", "prose_written_at": "elsewhere"}, + [("source", "", repo)], revisions, "c1", + ) + self.assertEqual(readings, [[(repo, "pin1", "pin2")], []]) + self.assertTrue(ambiguous) + def test_a_pin_keyed_by_build_mode_is_read_for_its_mode(self): """eka2l1 pins a dict per build: read as a string, it gave no repository and its prose was never judged.""" diff --git a/wiki/profiling.md b/wiki/profiling.md index 0eeda1f6..d3fd67b9 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -355,6 +355,7 @@ which CI validates every profile against. | `known_hash_adler32` | expected Adler-32 hash (Dolphin's DSP ROMs); pair with `adler32_byteswap` when the code hashes 16-bit byte-swapped data | | `adler32_pairs_with`, `adler32_pairs` | the file hashed together with this one and the combinations the code accepts, this file's value first, in table order: Dolphin warns on a DSP ROM pair outside its `known_roms` even when each file passes alone | | `pin_frozen` | why `source_commit` is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag | +| `prose_written_at` | the pin a reader confirmed the prose citations against. When the profile's history shows a text living under several pins, profile_sync takes this one as the revision the text describes, so a bump is no longer refused for a citation already reread. It never overrides a history with a single candidate | | `upstream_gone` | why the declared upstream can no longer be reached, when that is settled rather than transient. Records the death instead of leaving the profile failing every pass; profile_sync reports it as declared, and reports the contradiction if the forge answers again | | `source_mirror` | a repository carrying the same tree, consulted after `source` and `upstream`. What keeps a profile checkable once its own forge stops answering | | `validation` | checks the code performs: `size`, `crc32`, `md5`, `sha1`, `adler32`, `signature`, `crypto`. Can be a list or dict `{core: [...], upstream: [...]}` for divergent checks |