diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index 222c172d..cfdfdca2 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -815,7 +815,8 @@ "required": ["url", "pattern"], "properties": { "url": {"type": "string", "format": "uri"}, - "pattern": {"type": "string", "description": "Regular expression whose first group is the latest version the page announces."} + "pattern": {"type": "string", "description": "Regular expression whose first group is the latest version the page announces."}, + "no_genuine_copy": {"type": "string", "description": "An announced version a search found only rebuilt or re-signed copies of. While the page announces it, the profile is not stale. A later version makes it stale again."} }, "description": "For a project published outside any forge, the page that announces its releases. profile_sync --check-version compares the version read there with core_version." }, diff --git a/scripts/check_freshness.py b/scripts/check_freshness.py index 217ca46c..ab765ab8 100644 --- a/scripts/check_freshness.py +++ b/scripts/check_freshness.py @@ -1110,11 +1110,16 @@ def summarize_profile_sync(report: object) -> list[Finding]: # Off any forge, the release page is the feed: a version there # beyond the declared one is a release the profile does not cover. declared, announced = str(row.get("declared_version") or ""), str(row["page_version"]) - findings.append(Finding( - "profiles", name, OK if announced == declared else STALE, - local=declared, remote=announced, - detail="" if announced == declared else "a newer release is announced", - )) + if announced == declared: + status, detail = OK, "" + elif announced == str(row.get("no_genuine_copy") or ""): + # Searched for and found only rebuilt: the profile covers the + # newest build that can be read, until the page moves on. + status, detail = OK, "no genuine copy of the announced release" + else: + status, detail = STALE, "a newer release is announced" + findings.append(Finding("profiles", name, status, local=declared, + remote=announced, detail=detail)) continue if row.get("skipped"): findings.append(Finding("profiles", name, UNKNOWN, detail=str(row["skipped"]))) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index e644dfd0..60db294e 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -1118,6 +1118,8 @@ class ProfileReport: # (version_source): the version read there, beside the one declared. declared_version: str | None = None page_version: str | None = None + # An announced version searched for and found only rebuilt or re-signed. + no_genuine_copy: str | None = None # Skips that say the profile cannot be checked as written: a repository # that no longer answers, no pin to read, a death the forge contradicts. @@ -2040,6 +2042,9 @@ def format_report(report: ProfileReport, changed_only: bool = False) -> str: lines.append( f" version page: declared {report.declared_version or '-'}, " f"announced {report.page_version or 'unreadable'}" + + (" (no genuine copy found)" + if report.page_version and report.page_version == report.no_genuine_copy + else "") ) if report.skipped: lines.append(f" skipped: {report.skipped}") @@ -3841,6 +3846,8 @@ def main() -> None: if args.check_version and profile.get("version_source"): report.declared_version = str(profile.get("core_version") or "") or None report.page_version = page_version(profile, args.cache_dir, args.offline) + report.no_genuine_copy = str( + profile["version_source"].get("no_genuine_copy") or "") or None reports.append(report) if writes: try: diff --git a/tests/test_check_freshness.py b/tests/test_check_freshness.py index 8d7d0892..08291331 100644 --- a/tests/test_check_freshness.py +++ b/tests/test_check_freshness.py @@ -455,6 +455,35 @@ class AReleasePageMakesAProfileStale(unittest.TestCase): self.assertEqual(found["altirra"], check_freshness.STALE) self.assertEqual(found["mednafen"], check_freshness.OK) + def test_a_release_with_no_genuine_copy_waits_for_the_next(self): + """FPse 13.0 exists only as rebuilt copies: the profile stays on 12.1 + and flagging it on every run said nothing new. 13.1 would.""" + import check_freshness + + rows = [ + {"name": "fpse", "skipped": "unsupported host: x", "declared_version": "12.1", + "page_version": "13.0", "no_genuine_copy": "13.0"}, + {"name": "later", "skipped": "unsupported host: x", "declared_version": "12.1", + "page_version": "13.1", "no_genuine_copy": "13.0"}, + ] + found = {f.subject: f for f in check_freshness.summarize_profile_sync(rows)} + self.assertEqual(found["fpse"].status, check_freshness.OK) + self.assertIn("no genuine copy", found["fpse"].detail) + self.assertEqual(found["later"].status, check_freshness.STALE) + + def test_no_genuine_copy_is_never_the_profiled_version(self): + """The field names a release the profile could not read.""" + import yaml + + for path in sorted((REPO_ROOT / "emulators").glob("*.yml")): + text = path.read_text(encoding="utf-8") + if "no_genuine_copy" not in text: + continue + profile = yaml.safe_load(text) + source = profile["version_source"] + with self.subTest(profile=path.stem): + self.assertNotEqual(source["no_genuine_copy"], str(profile["core_version"])) + if __name__ == "__main__": unittest.main() diff --git a/wiki/profiling.md b/wiki/profiling.md index 10106dbf..02eb8bc6 100644 --- a/wiki/profiling.md +++ b/wiki/profiling.md @@ -384,7 +384,7 @@ partitions), and `game_count`. | `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 | -| `version_source` | for a project published outside any forge, `{url, pattern}`: the page that announces releases and a regular expression whose first group is the latest version. `profile_sync --check-version` compares it with `core_version`, and `check_freshness --profiles` calls the profile stale when they differ | +| `version_source` | for a project published outside any forge, `{url, pattern}`: the page that announces releases and a regular expression whose first group is the latest version. `profile_sync --check-version` compares it with `core_version`, and `check_freshness --profiles` calls the profile stale when they differ. `no_genuine_copy` names an announced version found only rebuilt or re-signed: the profile stays on the newest unmodified build and is not stale until the page announces a later one | | `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 |