From 7657d4aa0261b3552b40c28f9adf57475a82ede0 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:43:31 +0100 Subject: [PATCH 01/25] docs: name where psxonpsp660.bin is not world --- tests/test_doc_pages.py | 27 +++++++++++++++++++++++++++ wiki/advanced-usage.md | 9 ++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index cdd6c3ed1..e4b85bfea 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -2004,3 +2004,30 @@ class TheCatalogRowsStateFacts(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class RegionFreeRuleNamesWhereItDoesNotHold(unittest.TestCase): + """advanced-usage.md said psxonpsp660.bin is declared [world] and always + survives --region. PCSX ReARMed declares it [japan], and on the platforms + where it is the only PlayStation core the filter drops it.""" + + def test_every_platform_that_drops_a_named_file_is_named(self): + from common import (list_registered_platforms, load_emulator_profiles, + load_platform_config) + from region import WORLD, platform_region_index + + section = _section(_page("advanced-usage.md"), "Region filtering") + bullet = next(p for p in section.split("\n- ") if "Region-free" in p) + files = re.findall(r"`([^`]+\.bin)`", bullet) + self.assertTrue(files) + profiles = load_emulator_profiles(str(ROOT / "emulators")) + for platform in list_registered_platforms(str(ROOT / "platforms")): + config = load_platform_config(platform, str(ROOT / "platforms")) + index = platform_region_index(config, profiles) + for name in files: + entry = index.get(name) + drops = bool(entry and entry["regions"] and WORLD not in entry["regions"] + and not entry["has_untagged"]) + if drops: + with self.subTest(platform=platform, file=name): + self.assertRegex(bullet, rf"(?i)\b{re.escape(platform)}\b") diff --git a/wiki/advanced-usage.md b/wiki/advanced-usage.md index aff9b10b6..8ee0f0c17 100644 --- a/wiki/advanced-usage.md +++ b/wiki/advanced-usage.md @@ -134,9 +134,12 @@ Three rules keep a filtered pack usable: - **No system is ever emptied.** If nothing matches, the whole group is kept. `--region us` still ships `disksys.rom`, because the Famicom Disk System has no American BIOS. -- **Region-free files always survive.** `psxonpsp660.bin`, `ps1_rom.bin` and - `openbios.bin` are declared `[world]`: they add a capability rather than - competing regionally. +- **Region-free files survive.** A file one of the platform's cores declares + `[world]` is kept. SwanStation declares `psxonpsp660.bin` and `ps1_rom.bin` + that way, DuckStation `openbios.bin`. The tag comes from each core: + PCSX ReARMed reads the region inside `psxonpsp660.bin` and files it as + `[japan]`, so on MinUI, NextUI and MyMinUI, where it is the only PlayStation + core, `--region us` drops it. - **Untagged files always survive.** Most BIOS are not regional and carry no `region:` at all. From 747d40e98877c05bbac76f8dabbb4c31d80741cb Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:47:47 +0100 Subject: [PATCH 02/25] docs: give the clone command and size for packs --- README.md | 2 +- scripts/generate_readme.py | 26 +++++++++++++++++++++++++- tests/test_doc_pages.py | 19 +++++++++++++++++++ wiki/advanced-usage.md | 3 ++- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e554dfb12..aa2a0781c 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Checked by is the test your platform runs on its own, replicated here from its s ## Build your own pack -Clone the repo and generate packs for any platform, emulator, or system: +Packs are built from a clone, and the clone carries the collection: its checkout alone is over 7.7 GB. `git clone --depth 1 https://github.com/Abdess/retrobios.git` leaves the history out. Files kept as release assets are downloaded when a pack needs them. Then generate packs for any platform, emulator, or system: ```bash # The pack of a platform, as released diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index e845201a0..2dd324168 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -40,6 +40,7 @@ from nativemode import check_label import release_record from siteguides import install_command from validate_pr import LARGE_FILE_ISSUE_URL +from largefiles import load_registered_paths from verify import verify_platform @@ -447,6 +448,29 @@ REPO = "Abdess/retrobios" UPLOAD_URL = f"https://github.com/{REPO}/upload/main/bios" +def clone_line(db: dict) -> str: + """How to get the clone the commands below run in, and its size. + + Said bare, "clone the repo" started a multi-gigabyte transfer for a user + after a 20 MB --required-only pack. The size counts the collection files + the repository commits, a floor: release assets are fetched only when a + pack needs them. + """ + assets = set(load_registered_paths()) + committed = sum( + entry.get("size", 0) for entry in db.get("files", {}).values() + if entry.get("path") not in assets + ) + return ( + "Packs are built from a clone, and the clone carries the collection:" + f" its checkout alone is over {format_size(committed)}." + f" `git clone --depth 1 https://github.com/{REPO}.git` leaves the" + " history out. Files kept as release assets are downloaded when a" + " pack needs them. Then generate packs for any platform, emulator," + " or system:" + ) + + def _existing_contributor_block(readme_path: str = "README.md") -> list[str]: """The contributors already published, read back from the file. @@ -767,7 +791,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json" "", "## Build your own pack", "", - "Clone the repo and generate packs for any platform, emulator, or system:", + clone_line(db), "", "```bash", "# The pack of a platform, as released", diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index e4b85bfea..bc7a6a5a7 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -2031,3 +2031,22 @@ class RegionFreeRuleNamesWhereItDoesNotHold(unittest.TestCase): if drops: with self.subTest(platform=platform, file=name): self.assertRegex(bullet, rf"(?i)\b{re.escape(platform)}\b") + + +class BuildYourOwnPackSaysWhatTheCloneCosts(unittest.TestCase): + """The README sent a user after a minimal pack to "Clone the repo", with no + command and no size, and the clone runs to several gigabytes.""" + + def test_readme_gives_the_clone_command_and_its_size(self): + import generate_readme + + db = json.loads((ROOT / "database.json").read_text(encoding="utf-8")) + line = generate_readme.clone_line(db) + self.assertIn("git clone --depth 1", line) + self.assertRegex(line, r"over \d+(\.\d)? GB") + section = _section((ROOT / "README.md").read_text(encoding="utf-8"), + "Build your own pack") + self.assertIn("git clone --depth 1", section) + + def test_advanced_usage_names_the_shallow_clone(self): + self.assertIn("--depth 1", _page("advanced-usage.md").split("## Custom packs")[0]) diff --git a/wiki/advanced-usage.md b/wiki/advanced-usage.md index 8ee0f0c17..ecc7a801d 100644 --- a/wiki/advanced-usage.md +++ b/wiki/advanced-usage.md @@ -19,7 +19,8 @@ boot, with no message saying why. It is not a guarantee either: source profiles can document files nobody has dumped or that only the user can provide, all of them visible in the [gap analysis](../gaps.md). -From a clone of the repository, `python scripts/generate_pack.py --platform +A clone carries the whole collection, several gigabytes, and `git clone +--depth 1` leaves its history out. From that clone, `python scripts/generate_pack.py --platform retroarch --region us` builds a narrower pack, `--required-only` the bare minimum each core needs to start, and `--no-game-data` keeps the firmware and leaves out the files a game reads. `--help` lists every way to build your own, From bd50b08133ce314ce0bdcaa74f06075a2805bc10 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:49:06 +0100 Subject: [PATCH 03/25] docs: fetch platform files before the PR tests --- CONTRIBUTING.md | 1 + scripts/generate_readme.py | 1 + tests/test_doc_surfaces.py | 25 +++++++++++++++++++++++++ wiki/testing-guide.md | 4 ++++ 4 files changed, 31 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 965d1a80a..3073b4eb2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,7 @@ pip install pyyaml "jsonschema[format-nongpl]" markdown pyte hypothesis vermin s python scripts/validate_pr.py --changed # the BIOS files the branch adds python scripts/validate_schemas.py --source-only # profiles and platform files python scripts/profile_sync.py --added --strict # each profile the branch adds +python scripts/export_native.py --all --fetch --output-dir tmp/native # files the exporter tests compare with python scripts/refresh_data_dirs.py # data directories the pack-building tests read python scripts/generate_site.py # the pages the site tests read npm ci --prefix website && npm run build --prefix website # the built site, Node 24 diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 2dd324168..462b3d093 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -1002,6 +1002,7 @@ pip install pyyaml "jsonschema[format-nongpl]" markdown pyte hypothesis vermin s python scripts/validate_pr.py --changed # the BIOS files the branch adds python scripts/validate_schemas.py --source-only # profiles and platform files python scripts/profile_sync.py --added --strict # each profile the branch adds +python scripts/export_native.py --all --fetch --output-dir tmp/native # files the exporter tests compare with python scripts/refresh_data_dirs.py # data directories the pack-building tests read python scripts/generate_site.py # the pages the site tests read npm ci --prefix website && npm run build --prefix website # the built site, Node 24 diff --git a/tests/test_doc_surfaces.py b/tests/test_doc_surfaces.py index b7a53ee06..08a2702ba 100644 --- a/tests/test_doc_surfaces.py +++ b/tests/test_doc_surfaces.py @@ -611,3 +611,28 @@ class EveryOptionExists(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class ContributorChecksFetchThePlatformsFiles(unittest.TestCase): + """validate.yml fetches each platform's own file before the suite. The + checklist left it out, so the exporter fidelity cases skipped on a + contributor's clone and first ran, red, in CI.""" + + def test_the_pr_checklist_fetches_them_before_the_tests(self): + workflow = _read(".github/workflows/validate.yml") + self.assertLess( + workflow.index("python scripts/export_native.py --all --fetch"), + workflow.index("python -m unittest discover"), + ) + for text in ( + contributing_text(lambda page: page, "# C", "here"), + _read("CONTRIBUTING.md"), + ): + block = _section(text, "## Before opening a PR") + self.assertIn("python scripts/export_native.py --all --fetch", block) + self.assertLess( + block.index("export_native.py"), block.index("unittest discover") + ) + + def test_the_testing_guide_names_the_command(self): + self.assertIn("export_native.py --all --fetch", _read("wiki/testing-guide.md")) diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 3345889d1..351ab614d 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -493,6 +493,10 @@ of main passes, not every commit under it. Modules that need real artifacts skip themselves when those artifacts are absent, so `test_pack_integrity` is a no-op in CI (no `dist/`) and a real check locally. +The exporter fidelity cases go the other way: they compare with each platform's +own file, which `python scripts/export_native.py --all --fetch --output-dir +tmp/native` downloads into `.cache/upstream-native/`. CI runs it before the +suite, and a clone without that cache skips them. Run `python -m unittest discover -s tests -t .` before pushing. Tests must pass before merge. If a test fails in CI, reproduce locally with: From af6936055bc8438304e9298e596eac5eeafd2dad Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:51:00 +0100 Subject: [PATCH 04/25] docs: quote the pip lines CI pins for PR checks --- .github/workflows/deploy-site.yml | 2 ++ CONTRIBUTING.md | 8 +++++- scripts/generate_readme.py | 23 +++++++++++++++- tests/test_contributing_text.py | 45 ++++++++++++++++++++++++++++++- wiki/testing-guide.md | 4 ++- 5 files changed, 78 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 67130fe8f..4ea290a9b 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -89,6 +89,8 @@ on: # generated folders are gitignored and never trigger anything. - "website/**" - ".github/workflows/deploy-site.yml" + # The contributing guide quotes the pip lines of its test job. + - ".github/workflows/validate.yml" workflow_dispatch: permissions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3073b4eb2..f7b2d05f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ validation above is its check. For code, profiles or platform files, these are the checks CI runs on a pull request (Python 3.12 or newer): ```bash -pip install pyyaml "jsonschema[format-nongpl]" markdown pyte hypothesis vermin setuptools +pip install pyyaml "jsonschema[format-nongpl]==4.26.0" "markdown==3.10.2" "pyte==0.8.2" "hypothesis==6.170.8" "vermin==1.9.2" "setuptools==80.9.0" python scripts/validate_pr.py --changed # the BIOS files the branch adds python scripts/validate_schemas.py --source-only # profiles and platform files python scripts/profile_sync.py --added --strict # each profile the branch adds @@ -83,8 +83,14 @@ npm ci --prefix website && npm run build --prefix website # the built site, Nod python -m unittest discover -s tests -t . python scripts/validate_site.py --site-dir website/dist # links and page budgets vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8 +python3.8 -m unittest tests.test_install_tui # the full screen on the installer's floor ``` +The versions are the ones CI pins. Three tools come from the system, not from +that line, and the tests that need them skip without them: `shellcheck` for the +handheld launchers, `bwrap` (bubblewrap) for the probe sandbox, and a Python 3.8 +with pyte (`python3.8 -m pip install "pyte==0.8.2"`) for the full screen. CI has all three. + `profile_sync.py` compares the branch with this repository's `main`, and fetches it in a fork's clone. A profile you only edit is not held to `--strict`: its older refs are reviewed by hand. diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 462b3d093..51588abb4 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -13,6 +13,7 @@ from __future__ import annotations import argparse import json import os +import re import sys from collections.abc import Callable from datetime import datetime, timezone @@ -915,6 +916,19 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json" return "\n".join(lines) + "\n" +def ci_install_lines() -> tuple[str, str]: + """The pip lines the test job of validate.yml runs, verbatim. + + The guide listed the packages without their versions, so a local run + could pass on releases CI never installs. Read from the workflow, the + two cannot disagree. The second line is the Python 3.8 one. + """ + text = (_REPO / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") + main = re.search(r'^\s*run: (pip install pyyaml "jsonschema.*markdown.*)$', text, re.M) + floor = re.search(r"^\s*run: (python3\.8 -m pip install .*)$", text, re.M) + return main.group(1), floor.group(1) + + def contributing_text( link: Callable[[str], str], title: str, site_words: str ) -> str: @@ -924,6 +938,7 @@ def contributing_text( site. *link* renders a site page (absolute URL for the README, relative .md for the site); *site_words* names the site from where the text is read. """ + pip_line, floor_line = ci_install_lines() return f"""{title} ## Add a BIOS file @@ -998,7 +1013,7 @@ validation above is its check. For code, profiles or platform files, these are the checks CI runs on a pull request (Python 3.12 or newer): ```bash -pip install pyyaml "jsonschema[format-nongpl]" markdown pyte hypothesis vermin setuptools +{pip_line} python scripts/validate_pr.py --changed # the BIOS files the branch adds python scripts/validate_schemas.py --source-only # profiles and platform files python scripts/profile_sync.py --added --strict # each profile the branch adds @@ -1009,8 +1024,14 @@ npm ci --prefix website && npm run build --prefix website # the built site, Nod python -m unittest discover -s tests -t . python scripts/validate_site.py --site-dir website/dist # links and page budgets vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8 +python3.8 -m unittest tests.test_install_tui # the full screen on the installer's floor ``` +The versions are the ones CI pins. Three tools come from the system, not from +that line, and the tests that need them skip without them: `shellcheck` for the +handheld launchers, `bwrap` (bubblewrap) for the probe sandbox, and a Python 3.8 +with pyte (`{floor_line}`) for the full screen. CI has all three. + `profile_sync.py` compares the branch with this repository's `main`, and fetches it in a fork's clone. A profile you only edit is not held to `--strict`: its older refs are reviewed by hand. diff --git a/tests/test_contributing_text.py b/tests/test_contributing_text.py index 593ad04fa..579f8a617 100644 --- a/tests/test_contributing_text.py +++ b/tests/test_contributing_text.py @@ -114,11 +114,41 @@ class BeforeOpeningAPullRequest(unittest.TestCase): line naming the old theme installed packages nothing reads.""" text = generate_readme.generate_contributing() section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0] - self.assertIn('pip install pyyaml "jsonschema[format-nongpl]" markdown', section) + self.assertIn('pip install pyyaml "jsonschema[format-nongpl]==', section) self.assertNotIn("mkdocs", section) self.assertTrue((REPO_ROOT / "website" / "package-lock.json").is_file()) +class TheChecklistInstallsWhatCiInstalls(unittest.TestCase): + """The pip line named the packages without versions, and nothing said + that shellcheck, bubblewrap and a Python 3.8 with pyte come from the + system: their tests skipped locally and failed first in CI.""" + + def test_the_pip_lines_are_the_workflow_lines(self): + workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") + text = generate_readme.generate_contributing() + section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0] + pinned = [line.split("run: ", 1)[1] for line in workflow.splitlines() + if re.match(r"\s*run: (python3\.8 -m )?pip install .*==", line) + and "pyte" in line] + self.assertEqual(len(pinned), 2) + for line in pinned: + with self.subTest(line=line): + self.assertIn(line, section) + + def test_it_names_the_system_tools_ci_installs(self): + workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") + text = generate_readme.generate_contributing() + section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0] + for tool, step in (("shellcheck", "shellcheck --version"), + ("bwrap", "bwrap --ro-bind"), + ("python3.8 -m unittest tests.test_install_tui", + "python3.8 -m unittest tests.test_install_tui")): + with self.subTest(tool=tool): + self.assertIn(step, workflow) + self.assertIn(tool, section) + + class OneFileThroughTheBrowser(unittest.TestCase): """Adding one BIOS asked for a fork, a 5.5 GB clone, five pip packages, the data directories and the whole suite, and promised a PR comment that @@ -174,3 +204,16 @@ class OneFileThroughTheBrowser(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class TheGuideRebuildsWhenItsPinsMove(unittest.TestCase): + """The checklist quotes validate.yml, so a pin bump there must redeploy + the guide and meet the README/CONTRIBUTING freshness check.""" + + def test_deploy_site_watches_the_validation_workflow(self): + import yaml + + deploy = yaml.safe_load( + (REPO_ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(encoding="utf-8")) + paths = deploy[True]["push"]["paths"] + self.assertIn(".github/workflows/validate.yml", paths) diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 351ab614d..a9f94702d 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -42,7 +42,9 @@ tests are reported as skipped, so install them before a pull request full-screen tests drive `install.py --tui` on a pseudo-terminal and read the screen back with `pyte`, and fuzz the key decoder with `hypothesis`: both skip without them (`pip install pyte hypothesis`). `pip install -e ".[dev]"` from the -checkout installs all of these at once, from the `dev` extra of `pyproject.toml`. +checkout installs all of these at once, from the `dev` extra of `pyproject.toml`, +which sets lower bounds only. The contributing guide's checklist reads the exact +versions CI pins from `validate.yml`. No test framework beyond the standard library `unittest` module. ## Modules at a glance From f4b45c641d7c7ab19354d80fbfcd4acee91d2df8 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:55:03 +0100 Subject: [PATCH 05/25] feat: run the PR checks with one command --- .github/workflows/deploy-site.yml | 1 + CONTRIBUTING.md | 28 +++++---- scripts/generate_readme.py | 22 +++---- scripts/pr_checks.py | 99 +++++++++++++++++++++++++++++++ tests/test_contributing_text.py | 67 ++++++++++++++++++++- wiki/tools.md | 1 + 6 files changed, 196 insertions(+), 22 deletions(-) create mode 100644 scripts/pr_checks.py diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 4ea290a9b..04b329d26 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -38,6 +38,7 @@ on: - "scripts/packreadme.py" - "scripts/packresolve.py" - "scripts/packverify.py" + - "scripts/pr_checks.py" - "scripts/provenance_report.py" - "scripts/refresh_data_dirs.py" - "scripts/region.py" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7b2d05f5..ff2a2baec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,17 +73,25 @@ the checks CI runs on a pull request (Python 3.12 or newer): ```bash pip install pyyaml "jsonschema[format-nongpl]==4.26.0" "markdown==3.10.2" "pyte==0.8.2" "hypothesis==6.170.8" "vermin==1.9.2" "setuptools==80.9.0" -python scripts/validate_pr.py --changed # the BIOS files the branch adds -python scripts/validate_schemas.py --source-only # profiles and platform files -python scripts/profile_sync.py --added --strict # each profile the branch adds +python scripts/pr_checks.py +``` + +`pr_checks.py` runs these, in CI's order, carries on past a failure as CI +does, and names at the end each check that failed or could not run: + +```bash +python scripts/validate_pr.py --changed # the BIOS files the branch adds +python scripts/validate_schemas.py --source-only # profiles and platform files +python scripts/profile_sync.py --added --strict # each profile the branch adds +vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8 python scripts/export_native.py --all --fetch --output-dir tmp/native # files the exporter tests compare with -python scripts/refresh_data_dirs.py # data directories the pack-building tests read -python scripts/generate_site.py # the pages the site tests read -npm ci --prefix website && npm run build --prefix website # the built site, Node 24 -python -m unittest discover -s tests -t . -python scripts/validate_site.py --site-dir website/dist # links and page budgets -vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8 -python3.8 -m unittest tests.test_install_tui # the full screen on the installer's floor +python scripts/refresh_data_dirs.py # data directories the pack-building tests read +python scripts/generate_site.py # the pages the site tests read +npm ci --prefix website # the site's packages, Node 24 +npm run build --prefix website # the built site +python -m unittest discover -s tests -t . # the suite +python3.8 -m unittest tests.test_install_tui # the full screen on the installer's floor +python scripts/validate_site.py --site-dir website/dist # links and page budgets ``` The versions are the ones CI pins. Three tools come from the system, not from diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 51588abb4..572f3d1eb 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -42,6 +42,7 @@ import release_record from siteguides import install_command from validate_pr import LARGE_FILE_ISSUE_URL from largefiles import load_registered_paths +import pr_checks from verify import verify_platform @@ -939,6 +940,8 @@ def contributing_text( .md for the site); *site_words* names the site from where the text is read. """ pip_line, floor_line = ci_install_lines() + width = max(len(step.command) for step in pr_checks.STEPS) + steps = "\n".join(f"{step.command:<{width}} # {step.why}" for step in pr_checks.STEPS) return f"""{title} ## Add a BIOS file @@ -1014,17 +1017,14 @@ the checks CI runs on a pull request (Python 3.12 or newer): ```bash {pip_line} -python scripts/validate_pr.py --changed # the BIOS files the branch adds -python scripts/validate_schemas.py --source-only # profiles and platform files -python scripts/profile_sync.py --added --strict # each profile the branch adds -python scripts/export_native.py --all --fetch --output-dir tmp/native # files the exporter tests compare with -python scripts/refresh_data_dirs.py # data directories the pack-building tests read -python scripts/generate_site.py # the pages the site tests read -npm ci --prefix website && npm run build --prefix website # the built site, Node 24 -python -m unittest discover -s tests -t . -python scripts/validate_site.py --site-dir website/dist # links and page budgets -vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8 -python3.8 -m unittest tests.test_install_tui # the full screen on the installer's floor +python scripts/pr_checks.py +``` + +`pr_checks.py` runs these, in CI's order, carries on past a failure as CI +does, and names at the end each check that failed or could not run: + +```bash +{steps} ``` The versions are the ones CI pins. Three tools come from the system, not from diff --git a/scripts/pr_checks.py b/scripts/pr_checks.py new file mode 100644 index 000000000..cd7741295 --- /dev/null +++ b/scripts/pr_checks.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Run the checks validate.yml runs on a pull request, in its order. + +Usage: + python scripts/pr_checks.py + python scripts/pr_checks.py --list + +Each step runs even when an earlier one failed, as CI runs them, and the +summary names every step that failed or could not run. A step whose tool is +absent (npm, python3.8, vermin) is reported as not run, with what provides it. +""" + +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +@dataclass(frozen=True) +class Step: + command: str + why: str + needs: str = "" + provider: str = "" + env: dict[str, str] = field(default_factory=dict) + + +# The order of validate.yml: data fetched, site generated and built, then the +# suite, its 3.8 run and the built-site check. +STEPS: tuple[Step, ...] = ( + Step("python scripts/validate_pr.py --changed", "the BIOS files the branch adds"), + Step("python scripts/validate_schemas.py --source-only", "profiles and platform files"), + Step("python scripts/profile_sync.py --added --strict", "each profile the branch adds"), + Step("vermin --no-tips -t=3.8- --violations install.py", "install.py stays on Python 3.8", + needs="vermin", provider="the pip line of CONTRIBUTING.md"), + Step("python scripts/export_native.py --all --fetch --output-dir tmp/native", + "files the exporter tests compare with"), + Step("python scripts/refresh_data_dirs.py", "data directories the pack-building tests read"), + Step("python scripts/generate_site.py", "the pages the site tests read"), + Step("npm ci --prefix website", "the site's packages, Node 24", + needs="npm", provider="Node 24"), + Step("npm run build --prefix website", "the built site", + needs="npm", provider="Node 24"), + Step("python -m unittest discover -s tests -t .", "the suite", + env={"RETROBIOS_OFFLINE": "1"}), + Step("python3.8 -m unittest tests.test_install_tui", "the full screen on the installer's floor", + needs="python3.8", provider="a Python 3.8 with pyte", + env={"RETROBIOS_OFFLINE": "1"}), + Step("python scripts/validate_site.py --site-dir website/dist", "links and page budgets"), +) + + +def argv(step: Step) -> list[str]: + """The command, with `python` run by the interpreter running this script.""" + words = step.command.split() + return [sys.executable, *words[1:]] if words[0] == "python" else words + + +def run(steps: tuple[Step, ...] = STEPS) -> int: + failed: list[str] = [] + not_run: list[str] = [] + for step in steps: + print(f"\n== {step.command}", flush=True) + if step.needs and shutil.which(step.needs) is None: + print(f"not run: {step.needs} is not installed ({step.provider})") + not_run.append(f"{step.command} ({step.needs}: {step.provider})") + continue + code = subprocess.run(argv(step), cwd=ROOT, env={**os.environ, **step.env}).returncode + if code: + failed.append(f"{step.command} (exit {code})") + print() + for line in failed: + print(f"FAILED {line}") + for line in not_run: + print(f"NOT RUN {line}") + print(f"{len(steps) - len(failed) - len(not_run)}/{len(steps)} checks passed") + return 1 if failed else 0 + + +def main() -> None: + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0]) + parser.add_argument("--list", action="store_true", help="print the steps and exit") + args = parser.parse_args() + if args.list: + for step in STEPS: + print(step.command) + return + sys.exit(run()) + + +if __name__ == "__main__": + main() diff --git a/tests/test_contributing_text.py b/tests/test_contributing_text.py index 579f8a617..10f9a3d9b 100644 --- a/tests/test_contributing_text.py +++ b/tests/test_contributing_text.py @@ -65,7 +65,7 @@ class BeforeOpeningAPullRequest(unittest.TestCase): text = (REPO_ROOT / "CONTRIBUTING.md").read_text(encoding="utf-8") section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0] - block = _re.search(r"```bash\n(.*?)```", section, _re.S).group(1) + block = "".join(_re.findall(r"```bash\n(.*?)```", section, _re.S)) for command in ("validate_pr.py --changed", "validate_schemas.py --source-only", "unittest discover"): self.assertIn(command, block) @@ -217,3 +217,68 @@ class TheGuideRebuildsWhenItsPinsMove(unittest.TestCase): (REPO_ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(encoding="utf-8")) paths = deploy[True]["push"]["paths"] self.assertIn(".github/workflows/validate.yml", paths) + + +class OneCommandRunsTheChecks(unittest.TestCase): + """A contributor copied ten commands by hand before a pull request, and + skipping one (refresh_data_dirs) failed the pack tests with no hint.""" + + CHECK = re.compile(r"(?:xargs -0 |run: |^\s*)((?:python3?\.?8? -m unittest|python scripts/\w+\.py" + r"|vermin|npm (?:ci|run build)))") + + def test_every_check_ci_runs_is_a_step(self): + import pr_checks + + workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") + ran = {m.group(1) for line in workflow.splitlines() + if not line.strip().startswith("#") for m in [self.CHECK.search(line)] if m} + self.assertGreaterEqual(len(ran), 10) + steps = [step.command for step in pr_checks.STEPS] + for check in sorted(ran): + with self.subTest(check=check): + self.assertTrue(any(command.startswith(check) for command in steps)) + + def test_steps_keep_ci_order(self): + import pr_checks + + workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") + order = [workflow.index(key) for key in ( + "refresh_data_dirs.py", "run: python scripts/generate_site.py", + "npm run build --prefix website", "python -m unittest discover", + "validate_site.py")] + self.assertEqual(order, sorted(order)) + commands = [step.command for step in pr_checks.STEPS] + positions = [next(i for i, c in enumerate(commands) if key in c) for key in ( + "refresh_data_dirs.py", "generate_site.py", "npm run build", + "unittest discover", "validate_site.py")] + self.assertEqual(positions, sorted(positions)) + + def test_the_guide_gives_the_runner_and_what_it_runs(self): + import pr_checks + + text = generate_readme.generate_contributing() + section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0] + self.assertIn("python scripts/pr_checks.py\n", section) + for step in pr_checks.STEPS: + with self.subTest(step=step.command): + self.assertIn(step.command, section) + + def test_a_missing_tool_is_reported_and_a_failure_does_not_stop_the_run(self): + import contextlib + import io + + import pr_checks + + steps = ( + pr_checks.Step("python -c raise(SystemExit(3))", "fails"), + pr_checks.Step("no-such-tool-here --x", "absent", needs="no-such-tool-here", + provider="nothing"), + pr_checks.Step("python -c pass", "passes"), + ) + out = io.StringIO() + with contextlib.redirect_stdout(out): + code = pr_checks.run(steps) + self.assertEqual(code, 1) + self.assertIn("FAILED python -c raise(SystemExit(3)) (exit 3)", out.getvalue()) + self.assertIn("NOT RUN no-such-tool-here --x", out.getvalue()) + self.assertIn("1/3 checks passed", out.getvalue()) diff --git a/wiki/tools.md b/wiki/tools.md index 608f60318..a8ad76c26 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -1353,6 +1353,7 @@ before a release (wiki/release-process.md). Its lines stay in | `slots.py` | Destinations the platform and a core both claim, and how the pack settles each (`--platform`, `--target`, `--json`, `--strict`, and pipeline step 3b passes the run's own `--target`) | | `region_audit.py` | Confront every profile `region:` with the regions No-Intro names for the same dump (`--json`, `--db`, `--emulators-dir`). An opinion, never an authority: the code decides, and a disagreement is a line to read in the emulator's source | | `validate_pr.py` | Validate BIOS files in pull requests, post markdown report | +| `pr_checks.py` | Run the checks `validate.yml` runs on a pull request, in its order, past a failure as CI does, and name each check that failed or whose tool is absent (`--list` prints them) | | `validate_schemas.py` | Validate the data contracts: schemas and semantic invariants. `--source-only` checks `emulators/` and `platforms/` alone, which is what PR validation runs | | `generate_event_schema.py` | Write `schemas/install-events.schema.json` from `EVENT_KEYS` in `install.py` (`--check` exits 1 when the file is stale, which `validate_schemas.py` also reports) | | `generate_installer_doc.py` | Rewrite the generated blocks of `wiki/installer.md` from `install.py`: the tables (options, events, exit codes, keys per route, card markers), the line menu, and the run samples `install.py` prints in a sandbox against the committed `install/*.json`, so a new list rewrites the page (`--check` exits 1 when a block is stale, `--page FILE` writes another copy of the page). Pipeline step 7 | From f2ddcc235f5649ef500816f3c4e9a0f27bee1882 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:57:30 +0100 Subject: [PATCH 06/25] docs: drop double hyphen dashes from profile notes --- emulators/ardens.yml | 6 +++--- emulators/atari800.yml | 9 ++++----- emulators/cruzes.yml | 2 +- emulators/emux_chip8.yml | 1 - emulators/fake08.yml | 4 ++-- emulators/freechaf.yml | 2 +- emulators/galaksija.yml | 4 ++-- emulators/mesen.yml | 2 +- emulators/mgba.yml | 2 +- emulators/pokemini.yml | 4 ++-- emulators/px68k.yml | 8 ++++---- emulators/retrodream.yml | 2 +- tests/test_prose_style.py | 11 +++++++++++ 13 files changed, 33 insertions(+), 24 deletions(-) diff --git a/emulators/ardens.yml b/emulators/ardens.yml index 3fb3eb951..4471e18b5 100644 --- a/emulators/ardens.yml +++ b/emulators/ardens.yml @@ -67,6 +67,6 @@ platform_details: size: 1536 note: | Empty flashcart header written to the first 1536 bytes of the - W25Q128 flash image during initialization. This is not a BIOS - file -- it is the default flash content the Arduboy FX expects - at the start of external storage. + W25Q128 flash image during initialization. It is the default flash + content the Arduboy FX expects at the start of external storage, + not a BIOS file. diff --git a/emulators/atari800.yml b/emulators/atari800.yml index cc18f11af..85a78c4c4 100644 --- a/emulators/atari800.yml +++ b/emulators/atari800.yml @@ -16,7 +16,7 @@ cores: [atari800] systems: [atari-400-800, atari-5200, xegs] # Atari800 emulates the Atari 8-bit family (400/800/XL/XE) and the 5200 console. -# All BIOS files are optional -- the core ships built-in Altirra OS replacements +# All BIOS files are optional. The core ships built-in Altirra OS replacements # (altirraos_800, altirraos_xl, altirra_5200_os, altirra_basic) compiled into the # binary. If real ROM files are found in the system directory they take priority. # @@ -121,9 +121,8 @@ files: note: > Atari 400/800 OS Rev B (NTSC). CRC32 0x0e86d61d matches sysrom.c SYSROM_B_NTSC. Preferred over Rev A for 400/800 mode. - Note: .info lists md5 a3e8d617c95d08031fe1b20d541434b2 which is a - different dump (crc32 3e28a1fe) -- both in repo, the CRC32-matched - version is the one sysrom.c auto-detects. + The .info lists md5 a3e8d617c95d08031fe1b20d541434b2, a different + dump (crc32 3e28a1fe). sysrom.c detects the CRC32-matched one. source_ref: "source atari800/src/sysrom.c:91, source atari800/src/sysrom.c:286" # -- Atari XL/XE OS -- @@ -165,7 +164,7 @@ files: # -- XEGS Missile Command built-in game -- # The XEGS had Missile Command in ROM (8 KB), CRC32: 0xbdca01fb - # No filename auto-match -- must be set via config or detected by CRC. + # No filename auto-match. It must be set via config or detected by CRC. - name: "XEGAME.ROM" path: "XEGAME.ROM" diff --git a/emulators/cruzes.yml b/emulators/cruzes.yml index eea7783c2..2c70bae54 100644 --- a/emulators/cruzes.yml +++ b/emulators/cruzes.yml @@ -17,7 +17,7 @@ systems: notes: | Unfinished Picross puzzle game, part of the libretro-samples repository. - No separate upstream -- written directly for the libretro API. + The libretro API is its only target, with no separate upstream. supports_no_game true (tests/cruzes/cruzes.c:772), valid_extensions "" (tests/cruzes/cruzes.c:749). Puzzle grid hardcoded in retro_init (tests/cruzes/cruzes.c:706-717) via load_challenge. diff --git a/emulators/emux_chip8.yml b/emulators/emux_chip8.yml index 34027feb2..1ef84b7fa 100644 --- a/emulators/emux_chip8.yml +++ b/emulators/emux_chip8.yml @@ -51,4 +51,3 @@ platform_details: note: | No external BIOS. The 80-byte character font is compiled into the binary as char_mem[] and memcpy'd into RAM[0x000] on every reset. - This is standard CHIP-8 behavior -- all interpreters embed the font. diff --git a/emulators/fake08.yml b/emulators/fake08.yml index a6a62586a..40f4c102c 100644 --- a/emulators/fake08.yml +++ b/emulators/fake08.yml @@ -30,8 +30,8 @@ notes: | binary as string constants (p8GlobalLuaFunctions.h, DefaultCart.h). When no ROM is passed (supports_no_game = true), the core queues its - built-in BIOS cart "__FAKE08-BIOS.p8" (platform/libretro/libretro.cpp:751). This is not - an external file -- it is generated internally by the VM. + built-in BIOS cart "__FAKE08-BIOS.p8" (platform/libretro/libretro.cpp:751). The VM + generates it internally, so no external file is read. The core accepts .p8 (text) and .png (cartridge-as-image) formats (platform/libretro/libretro.cpp:258). Content is loaded either via the libretro data buffer diff --git a/emulators/freechaf.yml b/emulators/freechaf.yml index a45926c2b..527674410 100644 --- a/emulators/freechaf.yml +++ b/emulators/freechaf.yml @@ -35,7 +35,7 @@ notes: | hle_state.psu1_hle and hle_state.psu2_hle (src/channelf_hle.c). The .info file declares all three firmware files as required (opt=false), - but the core does not abort on missing BIOS -- it falls back to HLE. + but the core does not abort on a missing BIOS. It falls back to HLE. files: - name: "sl31253.bin" diff --git a/emulators/galaksija.yml b/emulators/galaksija.yml index 746d1f918..12fe25ba0 100644 --- a/emulators/galaksija.yml +++ b/emulators/galaksija.yml @@ -20,7 +20,7 @@ systems: # Z80-based at 3.072 MHz, PAL 50fps, character-mode display (32x16 chars, 256x208 px). # # The core embeds all three firmware files compressed in rom.c (LoadCHRGENBIN, -# LoadROM1BIN, LoadROM2BIN). External files are optional overrides -- if ANY of +# LoadROM1BIN, LoadROM2BIN). External files are optional overrides. If ANY of # CHRGEN.BIN or ROM1.BIN is missing from /galaksija/, the core sets # firmware_ignore=true and loads all ROMs from the embedded copies instead. # @@ -73,7 +73,7 @@ files: note: > Math extension ROM. Mapped at 0x1000-0x1FFF. Adds floating-point math functions (SIN, COS, TG, SQR, LN, EXP, POW, etc). Optional even when - loading from disk -- missing ROM2 only triggers a warning. Embedded in + loading from disk, where a missing ROM2 only triggers a warning. Embedded in rom.c as fallback. source_ref: "source src/rom.h:11, source src/libretro.c:107-110, source src/external/galaksija-emulator/src/init.c:83-92" diff --git a/emulators/mesen.yml b/emulators/mesen.yml index 54eb463e8..2114a0332 100644 --- a/emulators/mesen.yml +++ b/emulators/mesen.yml @@ -42,7 +42,7 @@ notes: | File path construction: retro_load_game() sets FolderUtilities::SetHomeFolder(systemFolder). All BIOS lookups use - CombinePath(GetHomeFolder(), filename) -- flat, no subdirectories. + CombinePath(GetHomeFolder(), filename), flat, with no subdirectories. No functional divergence between upstream and libretro for BIOS loading. diff --git a/emulators/mgba.yml b/emulators/mgba.yml index 30663a7fd..2f8be12e7 100644 --- a/emulators/mgba.yml +++ b/emulators/mgba.yml @@ -30,7 +30,7 @@ notes: | All BIOS files are optional. mGBA ships a built-in HLE BIOS for GBA (src/gba/hle-bios.c) that covers most software. The real BIOS improves accuracy (startup logo, some edge-case SWI behavior). There is no HLE - BIOS for GB/GBC/SGB -- those rely on GBSkipBIOS() if no file is found. + BIOS for GB/GBC/SGB, which rely on GBSkipBIOS() if no file is found. BIOS loading is controlled by two core options: mgba_use_bios (default ON) - whether to look for BIOS files at all diff --git a/emulators/pokemini.yml b/emulators/pokemini.yml index 76efaa28c..12900eb5e 100644 --- a/emulators/pokemini.yml +++ b/emulators/pokemini.yml @@ -21,8 +21,8 @@ notes: | The core has an embedded FreeBIOS (open-source replacement) compiled directly into the binary from freebios/freebios.c. This means the - real BIOS is never strictly required -- games will run with FreeBIOS - alone, though compatibility may differ from the original firmware. + real BIOS is never strictly required. Games run with FreeBIOS alone, + though compatibility may differ from the original firmware. BIOS loading sequence in the libretro port: diff --git a/emulators/px68k.yml b/emulators/px68k.yml index 479d774b6..5d73c2efe 100644 --- a/emulators/px68k.yml +++ b/emulators/px68k.yml @@ -23,7 +23,7 @@ systems: # 1. Base path set to /keropi/ via file_setcd(winx68k_dir) at line 969 # 2. IPL ROM: iterates BIOSFILE[] = {iplrom.dat, iplrom30.dat, iplromco.dat, iplromxv.dat} # and opens the first file found. Reads 0x20000 bytes (128 KB) into IPL buffer. -# Returns 0 (failure) if none found -- core will not start. +# Returns 0 (failure) if none found, and the core does not start. # 3. SCSI check (WinX68k_SCSICheck): if IPL contains SCSI vectors at offset 0x30600-0x30c00, # patches a fake SCSI BIOS stub at $fc0000 with "Human68k" signature. # 4. Byte-swaps IPL in 16-bit pairs (big-endian ROM on little-endian host). @@ -32,7 +32,7 @@ systems: # # SRAM (x68k/sram.c:50-73): # sram.dat is 16 KB of battery-backed SRAM, loaded on init and saved on cleanup. -# Not a BIOS -- runtime state. Created automatically if missing. +# Not a BIOS but runtime state. Created automatically if missing. # # Directory layout expected: # /keropi/iplrom.dat (or iplrom30.dat, iplromco.dat, iplromxv.dat) @@ -41,7 +41,7 @@ systems: # /keropi/config (auto-created, core settings) files: - # IPL ROM -- the main system BIOS. The core tries four filenames in order and + # IPL ROM, the main system BIOS. The core tries four filenames in order and # uses the first one found. Only one is needed. iplrom.dat is the original X68000, # iplrom30.dat is X68030, iplromco.dat is X68000 Compact, iplromxv.dat is X68000 XVI. @@ -87,7 +87,7 @@ files: Only used if the three other IPL variants are not found. source_ref: "source libretro.c:985" - # Font ROM -- character generator ROM for text rendering. + # Font ROM, the character generator ROM for text rendering. - name: "cgrom.dat" installed_on: [recalbox] diff --git a/emulators/retrodream.yml b/emulators/retrodream.yml index 0d0c76fba..a99dd0bdf 100644 --- a/emulators/retrodream.yml +++ b/emulators/retrodream.yml @@ -18,7 +18,7 @@ systems: notes: | Fork of the last GPLv3 release of redream (inolen) before it went closed-source. - The core has HLE bootstrap -- when boot.bin is missing, bios_invalid_instr() + The core has an HLE bootstrap. When boot.bin is missing, bios_invalid_instr() traps at PC=0x0 and calls bios_boot(), which loads IP.BIN and 1ST_READ.BIN directly from disc, sets up syscall vectors, and starts execution at 0xac008300. diff --git a/tests/test_prose_style.py b/tests/test_prose_style.py index d089e9f45..1d6e1c584 100644 --- a/tests/test_prose_style.py +++ b/tests/test_prose_style.py @@ -155,6 +155,17 @@ class NoGeneratedTypography(unittest.TestCase): offenders.append(f"{path.relative_to(ROOT)}:{number}: {line.strip()[:100]}") self.assertEqual(offenders, [], "\n".join(offenders[:40])) + def test_no_double_hyphen_dash_in_profiles(self): + """GLYPHS sees U+2014, not its ASCII spelling: "No separate upstream -- + written directly for the libretro API" reached the cruzes page as is.""" + dash = re.compile(r"[A-Za-z0-9)] -- [A-Za-z0-9(]") + offenders = [] + for path in sorted((ROOT / "emulators").glob("*.yml")): + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if dash.search(BACKTICKS.sub("", line)): + offenders.append(f"{path.relative_to(ROOT)}:{number}: {line.strip()[:100]}") + self.assertEqual(offenders, [], "\n".join(offenders)) + class NoMarketingVocabulary(unittest.TestCase): """The project bans these words in published text, and nothing checked From 0abe83da33ab33cbe5711d64a931651c15f99179 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:58:30 +0100 Subject: [PATCH 07/25] fix: give full hashes in the auto_fetch issue body --- scripts/auto_fetch.py | 47 +++++++++++++++------------------ tests/test_audit_regressions.py | 31 ++++++++++++++++++++++ wiki/tools.md | 2 +- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/scripts/auto_fetch.py b/scripts/auto_fetch.py index e7272524a..08d7b3afe 100644 --- a/scripts/auto_fetch.py +++ b/scripts/auto_fetch.py @@ -6,7 +6,8 @@ Pipeline: 2. Scan old branches (git show origin/branch:path) 3. Search public BIOS repos on GitHub 4. Search archive.org collections -5. Create GitHub Issue for community help +5. Print an issue body for the files still missing (--create-issues). + It posts nothing. Usage: python scripts/auto_fetch.py --platform retroarch [--dry-run] @@ -459,43 +460,37 @@ def fetch_missing( ) stats["errors"] += 1 continue - print(" [5] Not found - needs community contribution") + print(" [5] Not found, open for contributions") still_missing.append(entry) stats["not_found"] += 1 return {"stats": stats, "still_missing": still_missing} +CONTRIBUTING_URL = "https://github.com/Abdess/retrobios/blob/main/CONTRIBUTING.md" + + def generate_issue_body(missing: list[dict], platform: str) -> str: - """Generate a GitHub Issue body for missing BIOS files.""" + """An issue body for the files a platform still needs. + + The hashes are written in full: a contributor matches a file against + them, and a 12-character prefix confirms nothing. + """ lines = [ - f"## Missing BIOS Files for {platform}", - "", - "The following BIOS files are required but not available in the repository.", - "If you have any of these files, please submit a Pull Request!", + f"## Files {platform} still needs", "", "| File | System | SHA1 | MD5 |", "|------|--------|------|-----|", ] - for entry in missing: - sha1 = ", ".join(sorted(_accepted(entry, "sha1"))) or "N/A" - md5 = ", ".join(sorted(_accepted(entry, "md5"))) or "N/A" - lines.append( - f"| `{entry['name']}` | {entry['system']} | `{sha1[:12]}...` | `{md5[:12]}...` |" - ) - - lines.extend( - [ - "", - "### How to Contribute", - "", - "1. Fork this repository", - "2. Add the BIOS file to `bios/Manufacturer/Console/`", - "3. Create a Pull Request - checksums are verified automatically", - ] - ) - + sha1 = ", ".join(f"`{h}`" for h in sorted(_accepted(entry, "sha1"))) or "none declared" + md5 = ", ".join(f"`{h}`" for h in sorted(_accepted(entry, "md5"))) or "none declared" + lines.append(f"| `{entry['name']}` | {entry['system']} | {sha1} | {md5} |") + lines += [ + "", + "A file with one of these hashes can be added through the upload link in" + f" [CONTRIBUTING.md]({CONTRIBUTING_URL}). CI checks it on arrival.", + ] return "\n".join(lines) @@ -563,7 +558,7 @@ def main() -> None: result = fetch_missing(missing, db, args.bios_dir, args.dry_run) if result["still_missing"]: - all_still_missing[platform] = result["still_missing"] + all_still_missing[config.get("platform") or platform] = result["still_missing"] stats = result["stats"] print( diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index a93c0a8c8..34744187d 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -7414,5 +7414,36 @@ class TupleAnnotationsMatchTheirReturns(unittest.TestCase): self.assertEqual(mismatched, []) +class AutoFetchIssueBodyReadsAsWritten(unittest.TestCase): + """The --create-issues body was a template: Title Case headings, "The + following...", an exclamation mark, numbered orders to the reader, a + spaced dash, and hashes cut to 12 characters a contributor cannot + match a file against.""" + + ENTRY = {"name": "scph5501.bin", "system": "sony-playstation", + "sha1": "b056ee5a4d65937e1a3a17e1e78f3258ea49c38e", + "md5": "490f666e1afb15b7362b406ed1cea246"} + + def test_full_hashes_and_no_template_phrasing(self): + from scripts import auto_fetch + + body = auto_fetch.generate_issue_body([self.ENTRY], "RetroArch") + self.assertIn(f"`{self.ENTRY['sha1']}`", body) + self.assertIn(f"`{self.ENTRY['md5']}`", body) + self.assertNotIn("...", body) + self.assertNotIn("!", body) + self.assertNotRegex(body, r"(?i)the following|please| - |\bfork\b") + self.assertNotRegex(body, r"(?m)^\d+\. ") + for heading in re.findall(r"(?m)^#+ (.*)$", body): + later = [w for w in heading.split()[1:] if w != "RetroArch"] + self.assertEqual(later, [w.lower() for w in later], heading) + + def test_the_docstring_says_nothing_is_posted(self): + from scripts import auto_fetch + + self.assertNotIn("Create GitHub Issue", auto_fetch.__doc__) + self.assertIn("posts nothing", auto_fetch.__doc__) + + if __name__ == "__main__": unittest.main() diff --git a/wiki/tools.md b/wiki/tools.md index a8ad76c26..ce8c14359 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -1389,7 +1389,7 @@ Further options, by script: | Script | Options | |--------|---------| -| `auto_fetch.py` | `--create-issues` prints a GitHub issue body per file still missing. It posts nothing | +| `auto_fetch.py` | `--create-issues` prints a GitHub issue body per platform for the files still missing, with their full hashes. It posts nothing | | `check_buildbot_system.py` | `--update` refreshes the entries found stale, `--json`, `--registry` | | `check_freshness.py` | `--cache-dir`, `--native-cache-dir` (originals of the native export) and `--truth-dir` move the caches it reads | | `check_release.py` | `--tag`, `--sums dist/SHA256SUMS.txt`, `--record` (default `release.json`), `--json` | From 3d420145c334b2e762293c4f49b0d5e7ca39c2c5 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:59:47 +0100 Subject: [PATCH 08/25] refactor: name export report changes as updates --- exports/amberelec.json | 6 +- exports/batocera.json | 12 +- exports/bizhawk.json | 6 +- exports/emudeck.json | 6 +- exports/emuelec.json | 6 +- exports/knulli.json | 14 +- exports/lakka.json | 212 ++++++++++++------------- exports/misterfpga.json | 6 +- exports/recalbox.json | 266 ++++++++++++++++---------------- exports/retroarch.json | 212 ++++++++++++------------- exports/retrobat.json | 10 +- exports/retrodeck.json | 112 +++++++------- exports/retropie.json | 6 +- exports/rocknix.json | 6 +- exports/romm.json | 8 +- scripts/export_native.py | 22 +-- scripts/siteexports.py | 24 +-- tests/test_batocera_md5_list.py | 4 +- tests/test_export_counts.py | 2 +- tests/test_export_report.py | 40 +++-- tests/test_exporters.py | 2 +- 21 files changed, 499 insertions(+), 483 deletions(-) diff --git a/exports/amberelec.json b/exports/amberelec.json index 494490274..f409a92a7 100644 --- a/exports/amberelec.json +++ b/exports/amberelec.json @@ -864,7 +864,7 @@ } } ], - "hashes_corrected": [], + "hashes_updated": [], "hashes_filled": [ "msx/MSXDOS2.ROM md5", "msx/FMPAC.rom md5", @@ -877,10 +877,10 @@ "neogeo/aes.zip md5", "neogeocd/neocdz.zip md5" ], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 107, "removal_candidates": [ diff --git a/exports/batocera.json b/exports/batocera.json index cf6d3eaf0..5429fd3b0 100644 --- a/exports/batocera.json +++ b/exports/batocera.json @@ -213,7 +213,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "nds", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -222,7 +222,7 @@ "new": "322ceeafb77b0cc96a1fcc6271e0e0ae" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "nds", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -231,7 +231,7 @@ "new": "23031f25602a52c2df32c39554b0c55e" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "msx", "name": "MSX.ROM", "destination": "MSX.ROM", @@ -1560,16 +1560,16 @@ } } ], - "hashes_corrected": [ + "hashes_updated": [ "nds/dsi_bios7.bin md5", "nds/dsi_bios9.bin md5", "msx/MSX.ROM md5" ], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 114, "removal_candidates": [ diff --git a/exports/bizhawk.json b/exports/bizhawk.json index 272ca6115..50029774d 100644 --- a/exports/bizhawk.json +++ b/exports/bizhawk.json @@ -14,12 +14,12 @@ "not_kept": 0 }, "changes": [], - "hashes_corrected": [], + "hashes_updated": [], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 0, "removal_candidates": [], diff --git a/exports/emudeck.json b/exports/emudeck.json index a970a3962..9b3c88d81 100644 --- a/exports/emudeck.json +++ b/exports/emudeck.json @@ -14,12 +14,12 @@ "not_kept": 0 }, "changes": [], - "hashes_corrected": [], + "hashes_updated": [], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 107, "removal_candidates": [ diff --git a/exports/emuelec.json b/exports/emuelec.json index 36da198f3..a6c82fbbd 100644 --- a/exports/emuelec.json +++ b/exports/emuelec.json @@ -955,7 +955,7 @@ } } ], - "hashes_corrected": [], + "hashes_updated": [], "hashes_filled": [ "msx/MSXDOS2.ROM md5", "msx/FMPAC.rom md5", @@ -1005,10 +1005,10 @@ "Cannonball/opr-10268.99 md5", "RickDangerous/data.zip md5" ], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 312, "unattributed": [], diff --git a/exports/knulli.json b/exports/knulli.json index 026f24351..f8b9fe592 100644 --- a/exports/knulli.json +++ b/exports/knulli.json @@ -48,7 +48,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "neogeo", "name": "neogeo.zip", "destination": "neogeo.zip", @@ -233,7 +233,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "nds", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -242,7 +242,7 @@ "new": "322ceeafb77b0cc96a1fcc6271e0e0ae" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "nds", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -284,7 +284,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "msx", "name": "MSX.ROM", "destination": "MSX.ROM", @@ -1393,17 +1393,17 @@ } } ], - "hashes_corrected": [ + "hashes_updated": [ "neogeo/neogeo.zip md5", "nds/dsi_bios7.bin md5", "nds/dsi_bios9.bin md5", "msx/MSX.ROM md5" ], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 114, "removal_candidates": [ diff --git a/exports/lakka.json b/exports/lakka.json index a1e732fa6..f8e8f00e6 100644 --- a/exports/lakka.json +++ b/exports/lakka.json @@ -29,7 +29,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -38,7 +38,7 @@ "new": "23c66caa80b93199914fae246a5ea6e84db1be29" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -47,7 +47,7 @@ "new": "ad9585c72130c56f04ae26aae87c289d" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -56,7 +56,7 @@ "new": "12c66621" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -889,7 +889,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -898,7 +898,7 @@ "new": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -907,7 +907,7 @@ "new": "322ceeafb77b0cc96a1fcc6271e0e0ae" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -916,7 +916,7 @@ "new": "4316cc42" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -925,7 +925,7 @@ "new": "719b9eef33692406d7170ff526069615759c4dfc" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -934,7 +934,7 @@ "new": "23031f25602a52c2df32c39554b0c55e" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -957,7 +957,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "SNK - NeoGeo CD", "name": "neocdz.zip", "destination": "neocdz.zip", @@ -1965,7 +1965,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -1974,7 +1974,7 @@ "new": "775b12b7e787bbaaeb73aa45b1ad3bb383e041ef" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -1983,7 +1983,7 @@ "new": "4f9dd40b455876d5c54b5ebd93aec88c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -1992,7 +1992,7 @@ "new": "f63ddcc5" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -2001,7 +2001,7 @@ "new": 520352 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2010,7 +2010,7 @@ "new": "3971847e26be0abad8196caf03e5852a81ddbdea" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2019,7 +2019,7 @@ "new": "7224c7a94074addeb69015b1346695d2" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2028,7 +2028,7 @@ "new": "5956e0e9" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2037,7 +2037,7 @@ "new": 70416 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2046,7 +2046,7 @@ "new": "05c97e92439b67aea8f6eb7fc96db023edf62124" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2055,7 +2055,7 @@ "new": "b304979ed642a6dcaf26f43f8a37a1de" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2064,7 +2064,7 @@ "new": "b36f7b0e" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2073,7 +2073,7 @@ "new": 259625 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2082,7 +2082,7 @@ "new": "1eaef39122559500fbbc705c9425500311d23e03" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2091,7 +2091,7 @@ "new": "6df29d218962e155d0a2114006af45c0" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2100,7 +2100,7 @@ "new": "f59313fa" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2109,7 +2109,7 @@ "new": 6449829 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2118,7 +2118,7 @@ "new": "c6b3d3152e4e2ff4c9bf55e103bb39ed7faca2bd" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2127,7 +2127,7 @@ "new": "71697f90fb0c7747616dbb0799ce966b" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2136,7 +2136,7 @@ "new": "54f0350e" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2145,7 +2145,7 @@ "new": 30124 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2154,7 +2154,7 @@ "new": "74a5fa172f5327fedc145cb9eccc8fd050b9cfb9" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2163,7 +2163,7 @@ "new": "a865e70f1b0998a500f522dab441c8fc" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2172,7 +2172,7 @@ "new": "fc0dd1cb" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2181,7 +2181,7 @@ "new": 186866 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2190,7 +2190,7 @@ "new": "31b0b9418af2f9dd4e4094aada05d48eb355975b" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2199,7 +2199,7 @@ "new": "ebddd7bbd97135a427083db5daf6f9fc" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2208,7 +2208,7 @@ "new": "6b7229ce" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2217,7 +2217,7 @@ "new": 2028520 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2226,7 +2226,7 @@ "new": "516347ec0ec2a397265d1b2fb053aa3758382e09" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2235,7 +2235,7 @@ "new": "6f90cf73f36453d332d5c9cee311f81c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2244,7 +2244,7 @@ "new": "d616bea5" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2253,7 +2253,7 @@ "new": 768832 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2262,7 +2262,7 @@ "new": "e431e718e3c784532422ea2654d5fc6042a95c09" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2271,7 +2271,7 @@ "new": "3b1faf40e685b955372e02f54ed70263" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2280,7 +2280,7 @@ "new": "24a09690" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2289,7 +2289,7 @@ "new": 4146 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2298,7 +2298,7 @@ "new": "0816fe29b57911b28cbf774348f5492062326a85" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2307,7 +2307,7 @@ "new": "01f074a6ac34dd86fcc6d1e8090614f0" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2316,7 +2316,7 @@ "new": "f007671e" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2325,7 +2325,7 @@ "new": 280803 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2334,7 +2334,7 @@ "new": "7c8907d33da59afd4e4b62eee67541910e0b78d9" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2343,7 +2343,7 @@ "new": "eb7cfdb3f74972beb8f43196e4e44001" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2352,7 +2352,7 @@ "new": "d7976af8" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2361,7 +2361,7 @@ "new": 255042 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2370,7 +2370,7 @@ "new": "7bafa28dfd9b3be17430d467d8787d8204437ba0" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2379,7 +2379,7 @@ "new": "93c6d6379eb0290ce93854a6888ec3f4" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2388,7 +2388,7 @@ "new": "d713b1ab" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2397,7 +2397,7 @@ "new": 1100505 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2406,7 +2406,7 @@ "new": "8a571cfc431b5fe794f0262f0e55f72927ab27b4" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2415,7 +2415,7 @@ "new": "89664136c503a30894d5c713be5bfe90" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2424,7 +2424,7 @@ "new": "9643ad1a" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2433,7 +2433,7 @@ "new": 523211 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2442,7 +2442,7 @@ "new": "0d488164930332bae3aa1ad38cef44abbe3d00a6" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2451,7 +2451,7 @@ "new": "4594620989a0bfd8b120da2de7ac30de" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2460,7 +2460,7 @@ "new": "90b9e752" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2469,7 +2469,7 @@ "new": 403315 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2478,7 +2478,7 @@ "new": "85bb688125d66f2797a41f153ce1fa177027a83f" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2487,7 +2487,7 @@ "new": "2b71a5d9aa19219d46711e8a77451f0f" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2496,7 +2496,7 @@ "new": "7731190d" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2505,7 +2505,7 @@ "new": 20996 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2514,7 +2514,7 @@ "new": "e049932518390257f4d49b1691cad229e6060079" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2523,7 +2523,7 @@ "new": "e359fb7f14c4cb072b6e9300a23a6d62" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2532,7 +2532,7 @@ "new": "c15f9059" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2541,7 +2541,7 @@ "new": 15981464 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2550,7 +2550,7 @@ "new": "5302f543384b21fb55466118a6966e5ef46b85f6" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2559,7 +2559,7 @@ "new": "9783248899f9cb531f0a3e0a711ab8b3" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2568,7 +2568,7 @@ "new": "f7dc9bc4" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2577,7 +2577,7 @@ "new": 4520 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2586,7 +2586,7 @@ "new": "38e74e6994924781487a0d39cc53dc4708a90c1c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2595,7 +2595,7 @@ "new": "9c59e0e9875434456ec1b7eb8e51666f" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2604,7 +2604,7 @@ "new": "0aa0c87d" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2613,7 +2613,7 @@ "new": 3315855 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2622,7 +2622,7 @@ "new": "589b622f55c9944c4883e4163d4d49508e22e056" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2631,7 +2631,7 @@ "new": "b2811fa9b209733b61cd49ddb21e1e95" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2640,7 +2640,7 @@ "new": "93296840" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2649,7 +2649,7 @@ "new": 94555 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2658,7 +2658,7 @@ "new": "69836fb392710f2182df07bab8960e8a2d92c1a9" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2667,7 +2667,7 @@ "new": "8f37bd19dd51be456ac8f005fd5669a8" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2676,7 +2676,7 @@ "new": "103168ea" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2685,7 +2685,7 @@ "new": 16488 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2694,7 +2694,7 @@ "new": "aa2dfd12c11c6e6f3baaf7bf199bd6bcd71a520c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2703,7 +2703,7 @@ "new": "ee71d6cd464c618c632e6fd62b430d2c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2712,7 +2712,7 @@ "new": "00194297" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2721,7 +2721,7 @@ "new": 68087 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2730,7 +2730,7 @@ "new": "03e53349eefc39ce25220bfcb1ae4a580704d29d" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2739,7 +2739,7 @@ "new": "0d990484d0dfc1680b20cfabcae373cd" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2748,7 +2748,7 @@ "new": "6fa86fae" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2757,7 +2757,7 @@ "new": 93901 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2766,7 +2766,7 @@ "new": "e5b6f6d6473e2d6aa0a250fd9983b573ebf6fbe4" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2775,7 +2775,7 @@ "new": "a206e54bac1af13559ac8b2d9e550577" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2784,7 +2784,7 @@ "new": "14689714" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2793,7 +2793,7 @@ "new": 3693914 } ], - "hashes_corrected": [ + "hashes_updated": [ "Arcade/aes.zip sha1", "Arcade/aes.zip md5", "Arcade/aes.zip crc32", @@ -2875,7 +2875,7 @@ "ScummVM/translations.dat crc32" ], "hashes_filled": [], - "sizes_corrected": [ + "sizes_updated": [ "Arcade/aes.zip", "ScummVM/access.dat", "ScummVM/cryomni3d.dat", @@ -2908,7 +2908,7 @@ "SNK - NeoGeo CD/neocdz.zip sha1", "SNK - NeoGeo CD/neocdz.zip crc32" ], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 97, "removal_candidates": [ diff --git a/exports/misterfpga.json b/exports/misterfpga.json index 6b9bf906c..85cbbd0ac 100644 --- a/exports/misterfpga.json +++ b/exports/misterfpga.json @@ -15,12 +15,12 @@ "not_kept": 0 }, "changes": [], - "hashes_corrected": [], + "hashes_updated": [], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 0, "removal_candidates": [], diff --git a/exports/recalbox.json b/exports/recalbox.json index 3297c413d..c5d016716 100644 --- a/exports/recalbox.json +++ b/exports/recalbox.json @@ -16,7 +16,7 @@ }, "changes": [ { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga600", "name": "kick34005.A500", "destination": "amiga/bios/kick34005.A500", @@ -25,7 +25,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga600", "name": "kick37175.A500", "destination": "amiga/bios/kick37175.A500", @@ -298,7 +298,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga1200", "name": "kick40068.A4000", "destination": "amiga/bios/kick40068.A4000", @@ -307,7 +307,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amigacd32", "name": "kick40060.CD32.ext", "destination": "amiga/bios/kick40060.CD32.ext", @@ -316,7 +316,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "macintosh", "name": "MinivMacBootv2.dsk", "destination": "macintosh/MinivMacBootv2.dsk", @@ -325,7 +325,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atari800", "name": "ATARIBAS.ROM", "destination": "atari800/ATARIBAS.ROM", @@ -334,7 +334,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atari800", "name": "ATARIOSA.ROM", "destination": "atari800/ATARIOSA.ROM", @@ -343,7 +343,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atari800", "name": "ATARIOSB.ROM", "destination": "atari800/ATARIOSB.ROM", @@ -352,7 +352,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atari800", "name": "ATARIXL.ROM", "destination": "atari800/ATARIXL.ROM", @@ -361,7 +361,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "bk", "name": "B11M_BOS.ROM", "destination": "bk/B11M_BOS.ROM", @@ -370,7 +370,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "bk", "name": "B11M_EXT.ROM", "destination": "bk/B11M_EXT.ROM", @@ -379,7 +379,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "bk", "name": "BAS11M_0.ROM", "destination": "bk/BAS11M_0.ROM", @@ -388,7 +388,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "bk", "name": "BAS11M_1.ROM", "destination": "bk/BAS11M_1.ROM", @@ -397,7 +397,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "bk", "name": "DISK_327.ROM", "destination": "bk/DISK_327.ROM", @@ -406,7 +406,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "bk", "name": "FOCAL10.ROM", "destination": "bk/FOCAL10.ROM", @@ -643,7 +643,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "dreamcast", "name": "dc_boot.bin", "destination": "dc/dc_boot.bin", @@ -664,7 +664,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pc88", "name": "n88_1.rom", "destination": "quasi88/n88_1.rom", @@ -673,7 +673,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pc88", "name": "n88_2.rom", "destination": "quasi88/n88_2.rom", @@ -682,7 +682,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pc88", "name": "n88_3.rom", "destination": "quasi88/n88_3.rom", @@ -691,7 +691,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pc88", "name": "n88n.rom", "destination": "quasi88/n88n.rom", @@ -700,7 +700,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pc98", "name": "bios.rom", "destination": "np2kai/bios.rom", @@ -709,7 +709,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pc98", "name": "font.bmp", "destination": "np2kai/font.bmp", @@ -718,7 +718,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pc98", "name": "sound.rom", "destination": "np2kai/sound.rom", @@ -739,7 +739,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "sufami", "name": "STBIOS.bin", "destination": "sufami/STBIOS.bin", @@ -748,7 +748,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "CARTS.SHA", "destination": "msx/CARTS.SHA", @@ -757,7 +757,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "CYRILLIC.FNT", "destination": "msx/CYRILLIC.FNT", @@ -766,7 +766,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "DISK.ROM", "destination": "msx/DISK.ROM", @@ -775,7 +775,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "FMPAC.ROM", "destination": "msx/FMPAC.ROM", @@ -784,7 +784,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "ITALIC.FNT", "destination": "msx/ITALIC.FNT", @@ -793,7 +793,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "KANJI.ROM", "destination": "msx/KANJI.ROM", @@ -802,7 +802,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "MSXDOS2.ROM", "destination": "msx/MSXDOS2.ROM", @@ -811,7 +811,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "PAINTER.ROM", "destination": "msx/PAINTER.ROM", @@ -820,7 +820,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2", "name": "RS232.ROM", "destination": "msx/RS232.ROM", @@ -829,7 +829,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "pesadelo.rom", "destination": "Machines/Forte II Games - Pesadelo/pesadelo.rom", @@ -838,7 +838,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_logo_msx1.rom", "destination": "Machines/MSX - C-BIOS/cbios_logo_msx1.rom", @@ -847,7 +847,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_main_msx1.rom", "destination": "Machines/MSX - C-BIOS/cbios_main_msx1.rom", @@ -856,7 +856,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_logo_msx2.rom", "destination": "Machines/MSX2 - C-BIOS/cbios_logo_msx2.rom", @@ -865,7 +865,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_main_msx2.rom", "destination": "Machines/MSX2 - C-BIOS/cbios_main_msx2.rom", @@ -874,7 +874,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_sub.rom", "destination": "Machines/MSX2 - C-BIOS/cbios_sub.rom", @@ -883,7 +883,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_logo_msx2+.rom", "destination": "Machines/MSX2+ - C-BIOS/cbios_logo_msx2+.rom", @@ -892,7 +892,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_main_msx2+.rom", "destination": "Machines/MSX2+ - C-BIOS/cbios_main_msx2+.rom", @@ -901,7 +901,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "cbios_music.rom", "destination": "Machines/MSX2+ - C-BIOS/cbios_music.rom", @@ -910,7 +910,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "Msx2pe.rom", "destination": "Machines/MSX2+ - European/Msx2pe.rom", @@ -919,7 +919,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "Msx2pexte.rom", "destination": "Machines/MSX2+ - European/Msx2pexte.rom", @@ -928,7 +928,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "ARAB1.ROM", "destination": "Machines/Shared Roms/ARAB1.ROM", @@ -937,7 +937,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "ARABIC.rom", "destination": "Machines/Shared Roms/ARABIC.rom", @@ -946,7 +946,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "BEERIDE.ROM", "destination": "Machines/Shared Roms/BEERIDE.ROM", @@ -955,7 +955,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "FMPAC.rom", "destination": "Machines/Shared Roms/FMPAC.rom", @@ -964,7 +964,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "GCVMX80.ROM", "destination": "Machines/Shared Roms/GCVMX80.ROM", @@ -973,7 +973,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "HANGUL.rom", "destination": "Machines/Shared Roms/HANGUL.rom", @@ -982,7 +982,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "KANJI.rom", "destination": "Machines/Shared Roms/KANJI.rom", @@ -991,7 +991,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MICROSOLDISK.ROM", "destination": "Machines/Shared Roms/MICROSOLDISK.ROM", @@ -1000,7 +1000,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MOONSOUND.rom", "destination": "Machines/Shared Roms/MOONSOUND.rom", @@ -1009,7 +1009,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2AR.ROM", "destination": "Machines/Shared Roms/MSX2AR.ROM", @@ -1018,7 +1018,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2AREXT.ROM", "destination": "Machines/Shared Roms/MSX2AREXT.ROM", @@ -1027,7 +1027,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2BR.rom", "destination": "Machines/Shared Roms/MSX2BR.rom", @@ -1036,7 +1036,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2BREXT.rom", "destination": "Machines/Shared Roms/MSX2BREXT.rom", @@ -1045,7 +1045,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2FR.rom", "destination": "Machines/Shared Roms/MSX2FR.rom", @@ -1054,7 +1054,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2FREXT.rom", "destination": "Machines/Shared Roms/MSX2FREXT.rom", @@ -1063,7 +1063,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2G.rom", "destination": "Machines/Shared Roms/MSX2G.rom", @@ -1072,7 +1072,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2GEXT.rom", "destination": "Machines/Shared Roms/MSX2GEXT.rom", @@ -1081,7 +1081,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2HAN.rom", "destination": "Machines/Shared Roms/MSX2HAN.rom", @@ -1090,7 +1090,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2J.rom", "destination": "Machines/Shared Roms/MSX2J.rom", @@ -1099,7 +1099,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2JEXT.rom", "destination": "Machines/Shared Roms/MSX2JEXT.rom", @@ -1108,7 +1108,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2KR.rom", "destination": "Machines/Shared Roms/MSX2KR.rom", @@ -1117,7 +1117,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2KREXT.rom", "destination": "Machines/Shared Roms/MSX2KREXT.rom", @@ -1126,7 +1126,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2R.rom", "destination": "Machines/Shared Roms/MSX2R.rom", @@ -1135,7 +1135,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2R2.ROM", "destination": "Machines/Shared Roms/MSX2R2.ROM", @@ -1144,7 +1144,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2REXT.rom", "destination": "Machines/Shared Roms/MSX2REXT.rom", @@ -1153,7 +1153,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2SE.rom", "destination": "Machines/Shared Roms/MSX2SE.rom", @@ -1162,7 +1162,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2SP.rom", "destination": "Machines/Shared Roms/MSX2SP.rom", @@ -1171,7 +1171,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSX2SPEXT.rom", "destination": "Machines/Shared Roms/MSX2SPEXT.rom", @@ -1180,7 +1180,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXAR.ROM", "destination": "Machines/Shared Roms/MSXAR.ROM", @@ -1189,7 +1189,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXBR.rom", "destination": "Machines/Shared Roms/MSXBR.rom", @@ -1198,7 +1198,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXFR.rom", "destination": "Machines/Shared Roms/MSXFR.rom", @@ -1207,7 +1207,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXG.rom", "destination": "Machines/Shared Roms/MSXG.rom", @@ -1216,7 +1216,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXHAN.rom", "destination": "Machines/Shared Roms/MSXHAN.rom", @@ -1225,7 +1225,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXJ.rom", "destination": "Machines/Shared Roms/MSXJ.rom", @@ -1234,7 +1234,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXKANJI.rom", "destination": "Machines/Shared Roms/MSXKANJI.rom", @@ -1243,7 +1243,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXKR.rom", "destination": "Machines/Shared Roms/MSXKR.rom", @@ -1252,7 +1252,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXR.rom", "destination": "Machines/Shared Roms/MSXR.rom", @@ -1261,7 +1261,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXR2.ROM", "destination": "Machines/Shared Roms/MSXR2.ROM", @@ -1270,7 +1270,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXSE.ROM", "destination": "Machines/Shared Roms/MSXSE.ROM", @@ -1279,7 +1279,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "MSXSP.rom", "destination": "Machines/Shared Roms/MSXSP.rom", @@ -1288,7 +1288,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "NATIONALDISK.rom", "destination": "Machines/Shared Roms/NATIONALDISK.rom", @@ -1297,7 +1297,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "NOVAXIS.rom", "destination": "Machines/Shared Roms/NOVAXIS.rom", @@ -1306,7 +1306,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "PAINT.rom", "destination": "Machines/Shared Roms/PAINT.rom", @@ -1315,7 +1315,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "PANASONICDISK.rom", "destination": "Machines/Shared Roms/PANASONICDISK.rom", @@ -1324,7 +1324,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "PHILIPSDISK.rom", "destination": "Machines/Shared Roms/PHILIPSDISK.rom", @@ -1333,7 +1333,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "RS232.ROM", "destination": "Machines/Shared Roms/RS232.ROM", @@ -1342,7 +1342,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "SUNRISEIDE.rom", "destination": "Machines/Shared Roms/SUNRISEIDE.rom", @@ -1351,7 +1351,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "SWP.rom", "destination": "Machines/Shared Roms/SWP.rom", @@ -1360,7 +1360,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "XBASIC2.rom", "destination": "Machines/Shared Roms/XBASIC2.rom", @@ -1369,7 +1369,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "nowindDos1.rom", "destination": "Machines/Shared Roms/nowindDos1.rom", @@ -1378,7 +1378,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx1,msx2,msxturbor", "name": "nowindDos2.rom", "destination": "Machines/Shared Roms/nowindDos2.rom", @@ -1387,7 +1387,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "lynx", "name": "lynxboot.img", "destination": "lynx/lynxboot.img", @@ -1396,7 +1396,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atarist", "name": "tos.img", "destination": "atarist/tos.img", @@ -1441,7 +1441,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "o2em", "name": "c52.bin", "destination": "o2em/c52.bin", @@ -1450,7 +1450,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "videopacplus", "name": "g7400.bin", "destination": "o2em/g7400.bin", @@ -1459,7 +1459,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "videopacplus", "name": "jopac.bin", "destination": "o2em/jopac.bin", @@ -1468,7 +1468,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "3do", "name": "panafz1.bin", "destination": "3do/panafz1.bin", @@ -1477,7 +1477,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "3do", "name": "panafz1j.bin", "destination": "3do/panafz1j.bin", @@ -1486,7 +1486,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "3do", "name": "panafz1j-norsa.bin", "destination": "3do/panafz1j-norsa.bin", @@ -1507,7 +1507,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "x1", "name": "IPLROM.X1", "destination": "xmil/IPLROM.X1", @@ -1516,7 +1516,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "x1", "name": "IPLROM.X1T", "destination": "xmil/IPLROM.X1T", @@ -1525,7 +1525,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "psx", "name": "scph101.bin", "destination": "scph101.bin", @@ -1534,7 +1534,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "psx", "name": "scph7001.bin", "destination": "scph7001.bin", @@ -1543,7 +1543,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "psx", "name": "scph1001.bin", "destination": "scph1001.bin", @@ -1576,7 +1576,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "nds", "name": "bios7.bin", "destination": "nds/bios7.bin", @@ -1585,7 +1585,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "nds", "name": "bios9.bin", "destination": "nds/bios9.bin", @@ -1594,7 +1594,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "nds", "name": "firmware.bin", "destination": "nds/firmware.bin", @@ -1603,7 +1603,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "oricatmos", "name": "jasmin.rom", "destination": "oricutron/jasmin.rom", @@ -1612,7 +1612,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "oricatmos", "name": "microdis.rom", "destination": "oricutron/microdis.rom", @@ -1621,7 +1621,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "gb,gbc", "name": "SGB1.sfc", "destination": "sgb/SGB1.sfc", @@ -1630,7 +1630,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "gb,gbc", "name": "SGB2.sfc", "destination": "sgb/SGB2.sfc", @@ -1903,7 +1903,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "64dd", "name": "IPL.n64", "destination": "Mupen64plus/IPL.n64", @@ -1912,7 +1912,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "saturn", "name": "mpr-18811-mx.ic1", "destination": "saturn/mpr-18811-mx.ic1", @@ -1921,7 +1921,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "saturn", "name": "mpr-19367-mx.ic1", "destination": "saturn/mpr-19367-mx.ic1", @@ -1930,7 +1930,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "neogeocd", "name": "neocd.bin", "destination": "neocd/neocd.bin", @@ -1939,7 +1939,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "neogeocd", "name": "neocd.zip", "destination": "geolith/neocd.zip", @@ -1948,7 +1948,7 @@ "new": false }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ti994a", "name": "TI-994A.ctg", "destination": "ti994a/TI-994A.ctg", @@ -1957,7 +1957,7 @@ "new": "fb1ca5c58357896a0be5d6725894845d" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ti994a", "name": "ti-disk.ctg", "destination": "ti994a/ti-disk.ctg", @@ -2002,7 +2002,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "dragon", "name": "d64rom2.rom", "destination": "dragon/d64rom2.rom", @@ -2011,7 +2011,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "dragon", "name": "d200rom1.rom", "destination": "dragon/d200rom1.rom", @@ -2080,7 +2080,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "trs80coco", "name": "bas11.rom", "destination": "trs80coco/bas11.rom", @@ -2089,7 +2089,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "trs80coco", "name": "extbas10.rom", "destination": "trs80coco/extbas10.rom", @@ -2098,7 +2098,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "trs80coco", "name": "mx1600bas.rom", "destination": "trs80coco/mx1600bas.rom", @@ -2107,7 +2107,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "trs80coco", "name": "mc10.rom", "destination": "trs80coco/mc10.rom", @@ -2116,7 +2116,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "trs80coco", "name": "coco3.rom", "destination": "trs80coco/coco3.rom", @@ -2125,7 +2125,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "trs80coco", "name": "coco3p.rom", "destination": "trs80coco/coco3p.rom", @@ -2218,15 +2218,15 @@ } } ], - "hashes_corrected": [ + "hashes_updated": [ "ti994a/TI-994A.ctg md5", "ti994a/ti-disk.ctg md5" ], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 128, + "required_aligned": 128, "withheld": [], "found_by_content": 103, "removal_candidates": [ diff --git a/exports/retroarch.json b/exports/retroarch.json index 29776aee0..4b00637be 100644 --- a/exports/retroarch.json +++ b/exports/retroarch.json @@ -29,7 +29,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -38,7 +38,7 @@ "new": "23c66caa80b93199914fae246a5ea6e84db1be29" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -47,7 +47,7 @@ "new": "ad9585c72130c56f04ae26aae87c289d" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -56,7 +56,7 @@ "new": "12c66621" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "Arcade", "name": "aes.zip", "destination": "aes.zip", @@ -903,7 +903,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -912,7 +912,7 @@ "new": "c7c7570bfe51c3c7c5da3b01331b94e7e7cb4f53" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -921,7 +921,7 @@ "new": "322ceeafb77b0cc96a1fcc6271e0e0ae" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios7.bin", "destination": "dsi_bios7.bin", @@ -930,7 +930,7 @@ "new": "4316cc42" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -939,7 +939,7 @@ "new": "719b9eef33692406d7170ff526069615759c4dfc" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -948,7 +948,7 @@ "new": "23031f25602a52c2df32c39554b0c55e" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "Nintendo - Nintendo DSi", "name": "dsi_bios9.bin", "destination": "dsi_bios9.bin", @@ -971,7 +971,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "SNK - NeoGeo CD", "name": "neocdz.zip", "destination": "neocdz.zip", @@ -1979,7 +1979,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -1988,7 +1988,7 @@ "new": "775b12b7e787bbaaeb73aa45b1ad3bb383e041ef" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -1997,7 +1997,7 @@ "new": "4f9dd40b455876d5c54b5ebd93aec88c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -2006,7 +2006,7 @@ "new": "f63ddcc5" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "access.dat", "destination": "scummvm/extra/access.dat", @@ -2015,7 +2015,7 @@ "new": 520352 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2024,7 +2024,7 @@ "new": "3971847e26be0abad8196caf03e5852a81ddbdea" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2033,7 +2033,7 @@ "new": "7224c7a94074addeb69015b1346695d2" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2042,7 +2042,7 @@ "new": "5956e0e9" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "cryomni3d.dat", "destination": "scummvm/extra/cryomni3d.dat", @@ -2051,7 +2051,7 @@ "new": 70416 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2060,7 +2060,7 @@ "new": "05c97e92439b67aea8f6eb7fc96db023edf62124" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2069,7 +2069,7 @@ "new": "b304979ed642a6dcaf26f43f8a37a1de" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2078,7 +2078,7 @@ "new": "b36f7b0e" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "drascula.dat", "destination": "scummvm/extra/drascula.dat", @@ -2087,7 +2087,7 @@ "new": 259625 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2096,7 +2096,7 @@ "new": "1eaef39122559500fbbc705c9425500311d23e03" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2105,7 +2105,7 @@ "new": "6df29d218962e155d0a2114006af45c0" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2114,7 +2114,7 @@ "new": "f59313fa" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "fonts.dat", "destination": "scummvm/extra/fonts.dat", @@ -2123,7 +2123,7 @@ "new": 6449829 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2132,7 +2132,7 @@ "new": "c6b3d3152e4e2ff4c9bf55e103bb39ed7faca2bd" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2141,7 +2141,7 @@ "new": "71697f90fb0c7747616dbb0799ce966b" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2150,7 +2150,7 @@ "new": "54f0350e" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "freescape.dat", "destination": "scummvm/extra/freescape.dat", @@ -2159,7 +2159,7 @@ "new": 30124 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2168,7 +2168,7 @@ "new": "74a5fa172f5327fedc145cb9eccc8fd050b9cfb9" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2177,7 +2177,7 @@ "new": "a865e70f1b0998a500f522dab441c8fc" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2186,7 +2186,7 @@ "new": "fc0dd1cb" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "hugo.dat", "destination": "scummvm/extra/hugo.dat", @@ -2195,7 +2195,7 @@ "new": 186866 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2204,7 +2204,7 @@ "new": "31b0b9418af2f9dd4e4094aada05d48eb355975b" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2213,7 +2213,7 @@ "new": "ebddd7bbd97135a427083db5daf6f9fc" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2222,7 +2222,7 @@ "new": "6b7229ce" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "kyra.dat", "destination": "scummvm/extra/kyra.dat", @@ -2231,7 +2231,7 @@ "new": 2028520 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2240,7 +2240,7 @@ "new": "516347ec0ec2a397265d1b2fb053aa3758382e09" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2249,7 +2249,7 @@ "new": "6f90cf73f36453d332d5c9cee311f81c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2258,7 +2258,7 @@ "new": "d616bea5" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "lure.dat", "destination": "scummvm/extra/lure.dat", @@ -2267,7 +2267,7 @@ "new": 768832 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2276,7 +2276,7 @@ "new": "e431e718e3c784532422ea2654d5fc6042a95c09" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2285,7 +2285,7 @@ "new": "3b1faf40e685b955372e02f54ed70263" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2294,7 +2294,7 @@ "new": "24a09690" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "macventure.dat", "destination": "scummvm/extra/macventure.dat", @@ -2303,7 +2303,7 @@ "new": 4146 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2312,7 +2312,7 @@ "new": "0816fe29b57911b28cbf774348f5492062326a85" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2321,7 +2321,7 @@ "new": "01f074a6ac34dd86fcc6d1e8090614f0" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2330,7 +2330,7 @@ "new": "f007671e" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "mm.dat", "destination": "scummvm/extra/mm.dat", @@ -2339,7 +2339,7 @@ "new": 280803 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2348,7 +2348,7 @@ "new": "7c8907d33da59afd4e4b62eee67541910e0b78d9" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2357,7 +2357,7 @@ "new": "eb7cfdb3f74972beb8f43196e4e44001" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2366,7 +2366,7 @@ "new": "d7976af8" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "nancy.dat", "destination": "scummvm/extra/nancy.dat", @@ -2375,7 +2375,7 @@ "new": 255042 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2384,7 +2384,7 @@ "new": "7bafa28dfd9b3be17430d467d8787d8204437ba0" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2393,7 +2393,7 @@ "new": "93c6d6379eb0290ce93854a6888ec3f4" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2402,7 +2402,7 @@ "new": "d713b1ab" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "queen.tbl", "destination": "scummvm/extra/queen.tbl", @@ -2411,7 +2411,7 @@ "new": 1100505 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2420,7 +2420,7 @@ "new": "8a571cfc431b5fe794f0262f0e55f72927ab27b4" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2429,7 +2429,7 @@ "new": "89664136c503a30894d5c713be5bfe90" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2438,7 +2438,7 @@ "new": "9643ad1a" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "supernova.dat", "destination": "scummvm/extra/supernova.dat", @@ -2447,7 +2447,7 @@ "new": 523211 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2456,7 +2456,7 @@ "new": "0d488164930332bae3aa1ad38cef44abbe3d00a6" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2465,7 +2465,7 @@ "new": "4594620989a0bfd8b120da2de7ac30de" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2474,7 +2474,7 @@ "new": "90b9e752" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "teenagent.dat", "destination": "scummvm/extra/teenagent.dat", @@ -2483,7 +2483,7 @@ "new": 403315 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2492,7 +2492,7 @@ "new": "85bb688125d66f2797a41f153ce1fa177027a83f" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2501,7 +2501,7 @@ "new": "2b71a5d9aa19219d46711e8a77451f0f" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2510,7 +2510,7 @@ "new": "7731190d" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "toon.dat", "destination": "scummvm/extra/toon.dat", @@ -2519,7 +2519,7 @@ "new": 20996 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2528,7 +2528,7 @@ "new": "e049932518390257f4d49b1691cad229e6060079" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2537,7 +2537,7 @@ "new": "e359fb7f14c4cb072b6e9300a23a6d62" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2546,7 +2546,7 @@ "new": "c15f9059" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "ultima.dat", "destination": "scummvm/extra/ultima.dat", @@ -2555,7 +2555,7 @@ "new": 15981464 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2564,7 +2564,7 @@ "new": "5302f543384b21fb55466118a6966e5ef46b85f6" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2573,7 +2573,7 @@ "new": "9783248899f9cb531f0a3e0a711ab8b3" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2582,7 +2582,7 @@ "new": "f7dc9bc4" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "wintermute.zip", "destination": "scummvm/extra/wintermute.zip", @@ -2591,7 +2591,7 @@ "new": 4520 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2600,7 +2600,7 @@ "new": "38e74e6994924781487a0d39cc53dc4708a90c1c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2609,7 +2609,7 @@ "new": "9c59e0e9875434456ec1b7eb8e51666f" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2618,7 +2618,7 @@ "new": "0aa0c87d" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "gui-icons.dat", "destination": "scummvm/theme/gui-icons.dat", @@ -2627,7 +2627,7 @@ "new": 3315855 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2636,7 +2636,7 @@ "new": "589b622f55c9944c4883e4163d4d49508e22e056" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2645,7 +2645,7 @@ "new": "b2811fa9b209733b61cd49ddb21e1e95" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2654,7 +2654,7 @@ "new": "93296840" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "residualvm.zip", "destination": "scummvm/theme/residualvm.zip", @@ -2663,7 +2663,7 @@ "new": 94555 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2672,7 +2672,7 @@ "new": "69836fb392710f2182df07bab8960e8a2d92c1a9" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2681,7 +2681,7 @@ "new": "8f37bd19dd51be456ac8f005fd5669a8" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2690,7 +2690,7 @@ "new": "103168ea" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "scummclassic.zip", "destination": "scummvm/theme/scummclassic.zip", @@ -2699,7 +2699,7 @@ "new": 16488 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2708,7 +2708,7 @@ "new": "aa2dfd12c11c6e6f3baaf7bf199bd6bcd71a520c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2717,7 +2717,7 @@ "new": "ee71d6cd464c618c632e6fd62b430d2c" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2726,7 +2726,7 @@ "new": "00194297" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "scummmodern.zip", "destination": "scummvm/theme/scummmodern.zip", @@ -2735,7 +2735,7 @@ "new": 68087 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2744,7 +2744,7 @@ "new": "03e53349eefc39ce25220bfcb1ae4a580704d29d" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2753,7 +2753,7 @@ "new": "0d990484d0dfc1680b20cfabcae373cd" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2762,7 +2762,7 @@ "new": "6fa86fae" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "scummremastered.zip", "destination": "scummvm/theme/scummremastered.zip", @@ -2771,7 +2771,7 @@ "new": 93901 }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2780,7 +2780,7 @@ "new": "e5b6f6d6473e2d6aa0a250fd9983b573ebf6fbe4" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2789,7 +2789,7 @@ "new": "a206e54bac1af13559ac8b2d9e550577" }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2798,7 +2798,7 @@ "new": "14689714" }, { - "kind": "size_corrected", + "kind": "size_updated", "system": "ScummVM", "name": "translations.dat", "destination": "scummvm/theme/translations.dat", @@ -2947,7 +2947,7 @@ } } ], - "hashes_corrected": [ + "hashes_updated": [ "Arcade/aes.zip sha1", "Arcade/aes.zip md5", "Arcade/aes.zip crc32", @@ -3029,7 +3029,7 @@ "ScummVM/translations.dat crc32" ], "hashes_filled": [], - "sizes_corrected": [ + "sizes_updated": [ "Arcade/aes.zip", "ScummVM/access.dat", "ScummVM/cryomni3d.dat", @@ -3062,7 +3062,7 @@ "SNK - NeoGeo CD/neocdz.zip sha1", "SNK - NeoGeo CD/neocdz.zip crc32" ], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 97, "removal_candidates": [ diff --git a/exports/retrobat.json b/exports/retrobat.json index f39297df9..fbdad54a2 100644 --- a/exports/retrobat.json +++ b/exports/retrobat.json @@ -662,7 +662,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "dragon32", "name": "d64_1.rom", "destination": "dragon/d64_1.rom", @@ -766,7 +766,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "neogeocd", "name": "neocdz.zip", "destination": "neocdz.zip", @@ -885,7 +885,7 @@ } } ], - "hashes_corrected": [ + "hashes_updated": [ "dragon32/d64_1.rom md5", "neogeocd/neocdz.zip md5" ], @@ -894,10 +894,10 @@ "nds/dsi_bios9.bin md5", "nds/dsi_bios7.bin md5" ], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 420, "removal_candidates": [ diff --git a/exports/retrodeck.json b/exports/retrodeck.json index bdf93693b..685d3911b 100644 --- a/exports/retrodeck.json +++ b/exports/retrodeck.json @@ -379,7 +379,7 @@ "new": "bf05cc848e403f8ae457bfe26faa8135cf4725cfff956fa030f5c0a00a26adcc" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "mame", "name": "p1_rom.zip", "destination": "bios/p1_rom.zip", @@ -388,7 +388,7 @@ "new": false }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "fbneo", "name": "neocdz.zip", "destination": "bios/neocdz.zip", @@ -408,7 +408,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "nds", "name": "bios7.bin", "destination": "bios/bios7.bin", @@ -417,7 +417,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "nds", "name": "bios9.bin", "destination": "bios/bios9.bin", @@ -503,7 +503,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "pico8", "name": "pico8_dyn", "destination": "bios/pico-8/pico8_dyn", @@ -649,7 +649,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga", "name": "kick34005.A500", "destination": "bios/kick34005.A500", @@ -658,7 +658,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga", "name": "kick40068.A1200", "destination": "bios/kick40068.A1200", @@ -667,7 +667,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga", "name": "kick34005.CDTV", "destination": "bios/kick34005.CDTV", @@ -676,7 +676,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga", "name": "kick40060.CD32", "destination": "bios/kick40060.CD32", @@ -685,7 +685,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "amiga", "name": "kick40060.CD32.ext", "destination": "bios/kick40060.CD32.ext", @@ -730,7 +730,7 @@ "new": "4b5ed8cb2472013c19021fea99352fee6a50a116295cc38d4c040514c2eb39f1" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atarixe", "name": "xegs.zip", "destination": "bios/xegs.zip", @@ -739,7 +739,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atari5200", "name": "ATARIXL.ROM", "destination": "bios/ATARIXL.ROM", @@ -748,7 +748,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atari5200", "name": "ATARIBAS.ROM", "destination": "bios/ATARIBAS.ROM", @@ -757,7 +757,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "atari5200", "name": "ATARIOSA.ROM", "destination": "bios/ATARIOSA.ROM", @@ -953,7 +953,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "apple2", "name": "a2diskiing.zip", "destination": "bios/a2diskiing.zip", @@ -962,7 +962,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "apple2", "name": "apple2e.zip", "destination": "bios/apple2e.zip", @@ -971,7 +971,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "apple2", "name": "d2fdc.zip", "destination": "bios/d2fdc.zip", @@ -980,7 +980,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "apple2", "name": "apple2gs.zip", "destination": "bios/apple2gs.zip", @@ -989,7 +989,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "cdimono1", "name": "cdibios.zip", "destination": "bios/cdibios.zip", @@ -1186,7 +1186,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "gamecom", "name": "gamecom.zip", "destination": "bios/gamecom.zip", @@ -1206,7 +1206,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "gmaster", "name": "gmaster.zip", "destination": "bios/gmaster.zip", @@ -1226,7 +1226,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "neogeo", "name": "aes.zip", "destination": "bios/aes.zip", @@ -1253,7 +1253,7 @@ "new": "1d39368f8dd5c2c65be1b872e749f7e20fe5707f9897deefe54a82d11d4748c7" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "ngp", "name": "ngp.zip", "destination": "bios/ngp.zip", @@ -1262,7 +1262,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "odyssey2", "name": "c52.bin", "destination": "bios/c52.bin", @@ -1271,7 +1271,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx", "name": "DISK.ROM", "destination": "bios/DISK.ROM", @@ -1289,7 +1289,7 @@ "new": "31740a761447d7590c304338d389b0ae114546a8b9f79c9ffd5e374f80b7d354" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx", "name": "MSXDOS2.ROM", "destination": "bios/MSXDOS2.ROM", @@ -1325,7 +1325,7 @@ "new": "56785703b14f252ce77be586716dbf5d94125e93b9e6069ff26f7686566cd91a" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx", "name": "CARTS.SHA", "destination": "bios/CARTS.SHA", @@ -1343,7 +1343,7 @@ "new": "0a09940701f40647a2715271b43c9ab0d82bd488871427ad627b7e0f462f5dda" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "msx", "name": "RS232.ROM", "destination": "bios/RS232.ROM", @@ -1415,7 +1415,7 @@ "new": "728fbc6dd5f7532b526d1fa6e55bade10060954cf5412d23e61ceafaa1e6bf85" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "nes", "name": "disksys.rom", "destination": "bios/disksys.rom", @@ -1424,7 +1424,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "sega32x", "name": "32X_M_BIOS.BIN", "destination": "bios/32X_M_BIOS.BIN", @@ -1433,7 +1433,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "sega32x", "name": "32X_S_BIOS.BIN", "destination": "bios/32X_S_BIOS.BIN", @@ -1442,7 +1442,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "dreamcast", "name": "dc_boot.bin", "destination": "bios/dc/dc_boot.bin", @@ -1451,7 +1451,7 @@ "new": false }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "dreamcast", "name": "dc_flash.bin", "destination": "bios/dc/dc_flash.bin", @@ -1478,7 +1478,7 @@ "new": "8c8a21335038285cfa03dc076100c1f0bfadf3e4ff70796f11f3dfaaab60eee2" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "colecovision", "name": "colecovision.rom", "destination": "bios/colecovision.rom", @@ -1487,7 +1487,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "colecovision", "name": "coleco.rom", "destination": "bios/coleco.rom", @@ -1677,7 +1677,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "snes", "name": "BS-X.bin", "destination": "bios/BS-X.bin", @@ -1686,7 +1686,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "snes", "name": "STBIOS.bin", "destination": "bios/STBIOS.bin", @@ -1731,7 +1731,7 @@ "new": "ea65e0fb44ee93ede4b6c507509b7e5ddf497fb7155023bea91ef229469fa04d" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "scv", "name": "upd7801g.s01", "destination": "bios/upd7801g.s01", @@ -1749,7 +1749,7 @@ "new": "248ec2f28294316558475ba0e416da19cfff0a5dd8ad3525a484a9be67b8a290" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "scv", "name": "scv.zip", "destination": "bios/scv.zip", @@ -1769,7 +1769,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "supracan", "name": "umc6650.zip", "destination": "bios/umc6650.zip", @@ -1778,7 +1778,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "supracan", "name": "supracan.zip", "destination": "bios/supracan.zip", @@ -1809,7 +1809,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "vsmile", "name": "vsmile.zip", "destination": "bios/vsmile.zip", @@ -1840,7 +1840,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "xbox", "name": "mcpx_1.0.bin", "destination": "bios/mcpx_1.0.bin", @@ -1849,7 +1849,7 @@ "new": true }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "xbox", "name": "Complex.bin", "destination": "bios/Complex.bin", @@ -1878,7 +1878,7 @@ "new": "fc0e900bfec6b52f0f80ba1e65a4712808d2a411b5b00496639ef1a2152351f1" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "tanodragon", "name": "d64tano.rom", "destination": "bios/d64tano.rom", @@ -1905,7 +1905,7 @@ "new": "c3d0b8fd92020c939ec97388402d10b3890e6e26455963d50cc00b5f9cd68fa5" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "tanodragon", "name": "d200rom1.rom", "destination": "bios/d200rom1.rom", @@ -1923,7 +1923,7 @@ "new": "0486c51d3b6f7b6b4bcdde661d54f076278fd6a4ee92cbf9d1bc791554e16bd7" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "tanodragon", "name": "ddos10.rom", "destination": "bios/ddos10.rom", @@ -1986,7 +1986,7 @@ "new": "09e8573e31effc16e9f1a177f46514ed9f332a6c5e4e46a550574bd969319c11" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "tanodragon", "name": "cp400bas.rom", "destination": "bios/cp400bas.rom", @@ -2069,7 +2069,7 @@ } }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "coco", "name": "cp400extbas.rom", "destination": "bios/cp400extbas.rom", @@ -2123,7 +2123,7 @@ "new": "5375b1caef9df815b781d9cd6df81ecdaddd2642b06a01adac521e40f1329751" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "coco", "name": "coco3.rom", "destination": "bios/coco3.rom", @@ -2150,7 +2150,7 @@ "new": "ccd347508f177cb3eeb2e17fd60d3cccb64312d069c5084771467cfbbc04a27c" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "coco", "name": "extbas11.rom", "destination": "bios/extbas11.rom", @@ -2177,7 +2177,7 @@ "new": "8c86c83720043525029797dd3defd0fd285651549a373117304cc470e8e61f91" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "coco", "name": "disk11.rom", "destination": "bios/disk11.rom", @@ -2195,7 +2195,7 @@ "new": "1c40c5a74e72edaeecc2511fee1aab347eae2c3d5fc6778cb622e3df14d63157" }, { - "kind": "requirement_corrected", + "kind": "required_aligned", "system": "coco", "name": "coco3p.rom", "destination": "bios/coco3p.rom", @@ -2265,7 +2265,7 @@ } } ], - "hashes_corrected": [ + "hashes_updated": [ "fbneo/neocdz.zip md5" ], "hashes_filled": [ @@ -2364,10 +2364,10 @@ "coco/disk11.rom sha256", "coco/coco3p.rom sha256" ], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 52, + "required_aligned": 52, "withheld": [], "found_by_content": 279, "removal_candidates": [ diff --git a/exports/retropie.json b/exports/retropie.json index 74bae1e10..1e7c61d32 100644 --- a/exports/retropie.json +++ b/exports/retropie.json @@ -40,12 +40,12 @@ "new": {} } ], - "hashes_corrected": [], + "hashes_updated": [], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 98, "removal_candidates": [ diff --git a/exports/rocknix.json b/exports/rocknix.json index b3b07add2..b367714ff 100644 --- a/exports/rocknix.json +++ b/exports/rocknix.json @@ -27,12 +27,12 @@ } } ], - "hashes_corrected": [], + "hashes_updated": [], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 132, "removal_candidates": [ diff --git a/exports/romm.json b/exports/romm.json index a0695b141..760b00df3 100644 --- a/exports/romm.json +++ b/exports/romm.json @@ -1316,7 +1316,7 @@ } }, { - "kind": "hash_corrected", + "kind": "hash_updated", "system": "doom", "name": "PLUTONIA.WAD", "destination": "doom/PLUTONIA.WAD", @@ -1325,14 +1325,14 @@ "new": "816c7c6b0098f66c299c9253f62bd908456efb63" } ], - "hashes_corrected": [ + "hashes_updated": [ "doom/PLUTONIA.WAD sha1" ], "hashes_filled": [], - "sizes_corrected": [], + "sizes_updated": [], "sizes_removed": [], "hashes_cleared": [], - "requirements_corrected": 0, + "required_aligned": 0, "withheld": [], "found_by_content": 0, "removal_candidates": [ diff --git a/scripts/export_native.py b/scripts/export_native.py index 449b4c055..032ceb345 100644 --- a/scripts/export_native.py +++ b/scripts/export_native.py @@ -515,7 +515,7 @@ class LandedChanges: @property def applied(self) -> list[str]: - return self._labels("hash_corrected") + return self._labels("hash_updated") @property def filled(self) -> list[str]: @@ -527,7 +527,7 @@ class LandedChanges: @property def resized(self) -> list[str]: - return [label.removesuffix(" size") for label in self._labels("size_corrected")] + return [label.removesuffix(" size") for label in self._labels("size_updated")] @property def unsized(self) -> list[str]: @@ -535,7 +535,7 @@ class LandedChanges: @property def requirements(self) -> int: - return sum(r["kind"] == "requirement_corrected" for r in self.records) + return sum(r["kind"] == "required_aligned" for r in self.records) @property def lists(self) -> int: @@ -624,14 +624,14 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges: and exporter.required_value(entry) != bool(entry.platform["required"]) ): records.append(_change( - "requirement_corrected", entry, "required", + "required_aligned", entry, "required", bool(entry.platform["required"]), exporter.required_value(entry), )) for field_name in entry.corrections: if field_name == "required" or not exporter.states(entry, field_name): continue records.append(_change( - "hash_corrected", entry, field_name, + "hash_updated", entry, field_name, _platform_value(entry, field_name), _written_value(exporter, entry, field_name), )) @@ -648,7 +648,7 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges: and exporter.states(entry, field_name) ): records.append(_change( - "hash_corrected", entry, field_name, + "hash_updated", entry, field_name, _platform_value(entry, field_name), listed, )) records.extend( @@ -666,7 +666,7 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges: and exporter.states(entry, "size") ): records.append(_change( - "size_corrected", entry, "size", _single_size(theirs), ours + "size_updated", entry, "size", _single_size(theirs), ours )) elif ( theirs @@ -690,7 +690,7 @@ def landed_changes(exporter: object, systems: dict) -> LandedChanges: # A value the render rewrote though the model reads it as the # platform's own (RomM's sha1 with a leading space). records.extend( - _change("hash_corrected", entry, field_name, old, new) + _change("hash_updated", entry, field_name, old, new) for entry, field_name, old, new in getattr(exporter, "normalized_values", []) ) return changes @@ -863,12 +863,12 @@ def export_platform( "not_kept": lost, }, "changes": changes.records, - "hashes_corrected": applied, + "hashes_updated": applied, "hashes_filled": filled, - "sizes_corrected": resized, + "sizes_updated": resized, "sizes_removed": unsized, "hashes_cleared": cleared, - "requirements_corrected": requirements, + "required_aligned": requirements, "withheld": list(withheld), "found_by_content": report.labels, "unattributed": report.unattributed, diff --git a/scripts/siteexports.py b/scripts/siteexports.py index 870cbe43d..300ce9758 100644 --- a/scripts/siteexports.py +++ b/scripts/siteexports.py @@ -22,9 +22,9 @@ API_EXPORTS = "api/v1/exports" # Report key, then how a line of it reads on the page. CHANGE_KINDS = ( - ("hashes_corrected", "Hashes updated to the emulator source"), + ("hashes_updated", "Hashes updated to the emulator source"), ("hashes_filled", "Hashes added where the original lists none"), - ("sizes_corrected", "Sizes updated to the emulator source"), + ("sizes_updated", "Sizes updated to the emulator source"), ("sizes_removed", "Sizes no longer stated"), ("hashes_cleared", "Hashes no longer listed"), ("withheld", "Changes not applied"), @@ -33,9 +33,9 @@ CHANGE_KINDS = ( # Report key, then the kind each line of `changes` carries. CHANGE_RECORDS = { - "hashes_corrected": "hash_corrected", + "hashes_updated": "hash_updated", "hashes_filled": "hash_filled", - "sizes_corrected": "size_corrected", + "sizes_updated": "size_updated", "sizes_removed": "size_removed", "hashes_cleared": "hash_cleared", } @@ -52,11 +52,11 @@ def describe_change(change: dict) -> str: """One change in words, with the value before and after it.""" label = f"`{change['system']}/{change['name']}` {change['field']}" kind, old, new = change["kind"], change["old"], change["new"] - if kind in ("hash_corrected", "size_corrected"): + if kind in ("hash_updated", "size_updated"): return f"{label} {_value(old)} becomes {_value(new)}" if kind == "hash_filled": return f"{label} {_value(new)} added" - if kind == "requirement_corrected": + if kind == "required_aligned": return f"`{change['system']}/{change['name']}` becomes {'required' if new else 'optional'}" return f"{label} {_value(old)} no longer listed" @@ -76,7 +76,7 @@ def requirement_lines(report: dict) -> list[str]: """Each required flag aligned with the code, and what it is now.""" items = [ describe_change(c) for c in report.get("changes") or [] - if c["kind"] == "requirement_corrected" + if c["kind"] == "required_aligned" ] if not items: return [] @@ -108,11 +108,11 @@ def change_text(report: dict) -> str: parts = [f"{_plural(counts['kept'], 'entry', 'entries')} kept"] extra = [ (counts["added"], "file", "files", "added"), - (len(report.get("hashes_corrected") or []), "hash", "hashes", "updated"), - (report.get("requirements_corrected") or 0, "required flag", "required flags", + (len(report.get("hashes_updated") or []), "hash", "hashes", "updated"), + (report.get("required_aligned") or 0, "required flag", "required flags", "aligned with the code"), (len(report.get("hashes_filled") or []), "hash", "hashes", "added"), - (len(report.get("sizes_corrected") or []), "size", "sizes", "updated"), + (len(report.get("sizes_updated") or []), "size", "sizes", "updated"), ] parts += [f"{_plural(n, one, many)} {verb}" for n, one, many, verb in extra if n] return ", ".join(parts) @@ -216,7 +216,7 @@ def _display(platform: str, names: dict[str, str]) -> str: return names.get(platform) or platform -def _corrected_link(report: dict) -> str: +def _updated_link(report: dict) -> str: platform, files = report["platform"], report["files"] if len(files) == 1: path = f"{EXPORTS_DIR}/{platform}/{files[0]}" @@ -286,7 +286,7 @@ def generate_exports_page(reports: list[dict], names: dict[str, str]) -> str: platform = report["platform"] lines.append( f"| [{_display(platform, names)}](#{platform}) | " - f"{_original_link(report)} | {_corrected_link(report)} | " + f"{_original_link(report)} | {_updated_link(report)} | " f"{change_text(report)} |" ) lines.append("") diff --git a/tests/test_batocera_md5_list.py b/tests/test_batocera_md5_list.py index 223d2add5..1facf7028 100644 --- a/tests/test_batocera_md5_list.py +++ b/tests/test_batocera_md5_list.py @@ -343,7 +343,7 @@ class SeveralAcceptedMd5BecomeAList(unittest.TestCase): self.assertEqual( produced, LISTING.replace(f'"md5": "{C}"', f'"md5": [ "{C}", "{D}" ]') ) - record = next(r for r in changes.records if r["kind"] == "hash_corrected") + record = next(r for r in changes.records if r["kind"] == "hash_updated") self.assertEqual((record["old"], record["new"]), (C, [C, D])) self.assertEqual(changes.lists, 1) @@ -431,7 +431,7 @@ class SeveralAcceptedMd5BecomeAList(unittest.TestCase): class SitePageShowsAList(unittest.TestCase): def test_a_list_reads_as_any_of_its_values(self): line = describe_change({ - "kind": "hash_corrected", "system": "psx", "name": "scph5500.bin", + "kind": "hash_updated", "system": "psx", "name": "scph5500.bin", "field": "md5", "old": C, "new": [C, D], }) self.assertEqual(line, f"`psx/scph5500.bin` md5 `{C}` becomes `{C}` or `{D}`") diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index f0abfc261..308156bb5 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -1081,7 +1081,7 @@ class AnExtendedListIsReported(unittest.TestCase): self.assertEqual(fe.hashes("md5"), [A, B]) self.assertEqual( [(r["kind"], r["field"], r["old"], r["new"]) for r in records], - [("hash_corrected", "md5", A, [A, B])], + [("hash_updated", "md5", A, [A, B])], ) def test_an_unchanged_value_reports_nothing(self): diff --git a/tests/test_export_report.py b/tests/test_export_report.py index 1b25ab45e..5e3173ae4 100644 --- a/tests/test_export_report.py +++ b/tests/test_export_report.py @@ -27,8 +27,8 @@ def _report(platform: str, files: list[str], **changes: object) -> dict: "originals": {f: f"https://example.org/{platform}/{f}" for f in files}, "files": files, "summary": "3 kept, 1 file added, 7 hashes updated to the emulator source", - "hashes_corrected": [], "hashes_filled": [], "sizes_corrected": [], - "sizes_removed": [], "hashes_cleared": [], "requirements_corrected": 0, + "hashes_updated": [], "hashes_filled": [], "sizes_updated": [], + "sizes_removed": [], "hashes_cleared": [], "required_aligned": 0, "withheld": [], "invalid": [], } report.update(changes) @@ -56,8 +56,8 @@ class ExportReportIsWrittenAndRead(unittest.TestCase): body = source[source.index("def export_platform("):source.index("def write_report(")] call = body[body.index("write_report("):] for key, value in ( - ("hashes_corrected", "applied"), ("hashes_filled", "filled"), - ("sizes_corrected", "resized"), ("sizes_removed", "unsized"), + ("hashes_updated", "applied"), ("hashes_filled", "filled"), + ("sizes_updated", "resized"), ("sizes_removed", "unsized"), ("hashes_cleared", "cleared"), ("invalid", "issues"), ): self.assertIn(f'"{key}": {value},', call) @@ -71,7 +71,7 @@ class CorrectedFilesPage(unittest.TestCase): def setUp(self): corrected = [f"sys/file{i}.bin md5" for i in range(12)] self.reports = [ - _report("recalbox", ["es_bios.xml"], hashes_corrected=corrected), + _report("recalbox", ["es_bios.xml"], hashes_updated=corrected), _report("retropie", ["scriptmodules/a.sh", "scriptmodules/b.sh"], invalid=["bash -n failed on a.sh"], withheld=["a size the format has no field for"]), @@ -152,7 +152,7 @@ class ThePageAddressesMaintainersAsPeers(unittest.TestCase): from siteexports import change_text report = _report("batocera", ["batocera-systems"], - hashes_corrected=["a md5", "b md5"], requirements_corrected=3) + hashes_updated=["a md5", "b md5"], required_aligned=3) report["counts"] = {"kept": 692, "added": 253, "not_added": 10, "not_kept": 0} self.assertEqual( change_text(report), @@ -161,7 +161,7 @@ class ThePageAddressesMaintainersAsPeers(unittest.TestCase): ) def test_no_wording_that_judges_the_original(self): - report = _report("batocera", ["batocera-systems"], hashes_corrected=["a md5"]) + report = _report("batocera", ["batocera-systems"], hashes_updated=["a md5"]) report["counts"] = {"kept": 1, "added": 0, "not_added": 4, "not_kept": 1} page = generate_exports_page([report], {}).lower() for word in ("their file", "theirs", "corrected", "wrong", "drift", "left out"): @@ -241,13 +241,29 @@ class TheRunReportStatesWhatTheFileCarries(unittest.TestCase): for text in [report["summary"], *messages]: for word in self.JUDGING: self.assertNotIn(word, text.lower()) + # The report is served as api/v1/exports/.json, keys and + # change kinds included: "hash_corrected" sat on 130 Recalbox lines. + names = [*report, *(c["kind"] for c in report["changes"])] + self.assertIn("hash_updated", names) + for name in names: + for word in self.JUDGING: + self.assertNotIn(word, name) + + def test_the_committed_reports_use_the_same_names(self): + import json # noqa: PLC0415 + + for path in sorted((ROOT / "exports").glob("*.json")): + report = json.loads(path.read_text(encoding="utf-8")) + names = [*report, *(c["kind"] for c in report.get("changes") or [])] + with self.subTest(report=path.name): + self.assertFalse([n for n in names if "corrected" in n]) UPSTREAM_CACHE = ROOT / ".cache" / "upstream-native" _DAT_ROM = re.compile(r"^\trom \( name (\"[^\"]+\"|\S+) (.*) \)$") _DAT_FIELDS = {"size": "size", "crc": "crc32", "md5": "md5", "sha1": "sha1"} _HASH_KINDS = { - "hash_corrected", "hash_filled", "hash_cleared", "size_corrected", "size_removed", + "hash_updated", "hash_filled", "hash_cleared", "size_updated", "size_removed", } @@ -364,7 +380,7 @@ class ReportedChangesAreTheDiff(unittest.TestCase): ((c["system"], path_of[(c["system"], c["name"], c["destination"])]), c["field"], _norm(c["old"]), _norm(c["new"])) for c in changes - if c["kind"] in _HASH_KINDS or c["kind"] == "requirement_corrected" + if c["kind"] in _HASH_KINDS or c["kind"] == "required_aligned" } self.assertTrue(changed, "nothing changed, the comparison proves nothing") self.assertEqual(reported, changed) @@ -418,10 +434,10 @@ class ThePageShowsEachValueBeforeAndAfter(unittest.TestCase): self.report = _report( "recalbox", ["es_bios.xml"], - hashes_corrected=["psx/scph5501.bin md5"], requirements_corrected=1, + hashes_updated=["psx/scph5501.bin md5"], required_aligned=1, changes=[ - change("hash_corrected", "md5", "5626" + "0" * 28, "c523" + "1" * 28), - change("requirement_corrected", "required", True, False), + change("hash_updated", "md5", "5626" + "0" * 28, "c523" + "1" * 28), + change("required_aligned", "required", True, False), change("hash_filled", "sha1", None, "f" * 40), ], ) diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 8dc24b4ff..215f72f37 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1379,7 +1379,7 @@ class RecalboxExport(unittest.TestCase): self.assertNotEqual(element.get("mandatory"), "false") self.assertEqual( [c for c in landed_changes(exporter, systems).records - if c["kind"] == "requirement_corrected"], + if c["kind"] == "required_aligned"], [], ) From 093b1b21cf6ccd928dbec37f02ca3a722e4431e1 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:00:49 +0100 Subject: [PATCH 09/25] docs: document the commit rules and their hook --- tests/test_doc_pages.py | 49 +++++++++++++++++++++++++++++++++++++++++ wiki/testing-guide.md | 17 ++++++++++++-- wiki/tools.md | 5 +++-- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index bc7a6a5a7..aa691ec21 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -2050,3 +2050,52 @@ class BuildYourOwnPackSaysWhatTheCloneCosts(unittest.TestCase): def test_advanced_usage_names_the_shallow_clone(self): self.assertIn("--depth 1", _page("advanced-usage.md").split("## Custom packs")[0]) + + +class LandingAChangeNamesTheCommitRules(unittest.TestCase): + """The page left out the review-tag rule scripts/commit_rules.py + enforces, and never said where the rules live or how a clone runs them: + the hook is not versioned.""" + + def _landing(self) -> str: + return _section(_page("testing-guide.md"), "Landing a change") + + def test_every_refusal_is_on_the_page(self): + import commit_rules + + section = self._landing() + words = re.search(r"\\b\((.*?)\)", commit_rules.REFUSED_WORDS.pattern).group(1) + from tests.test_prose_style import NoMarketingVocabulary + + for word in words.replace("|", " ").split(): + if NoMarketingVocabulary.WORDS.search(word): + # Named by "the marketing vocabulary above": spelled out, + # the page would fail that test. + continue + with self.subTest(word=word): + self.assertIn(word, section.lower()) + self.assertIn("which review asked for it", section) + self.assertIn("scripts/commit_rules.py", section) + + def test_the_hook_command_on_the_page_refuses_a_review_tag(self): + import subprocess + import tempfile + + block = re.search(r"```bash\n(.*?)```", self._landing(), re.S).group(1) + with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as scratch: + clone = Path(scratch) + (clone / ".git" / "hooks").mkdir(parents=True) + (clone / "scripts").mkdir() + (clone / "scripts" / "commit_rules.py").write_bytes( + (ROOT / "scripts" / "commit_rules.py").read_bytes()) + subprocess.run(["sh", "-c", block], cwd=clone, check=True) + message = clone / "MSG" + message.write_text("fix: address review 3 notes\n", encoding="utf-8") + refused = subprocess.run([str(clone / ".git" / "hooks" / "commit-msg"), "MSG"], + cwd=clone, capture_output=True, text=True) + message.write_text("fix: say what the change does\n", encoding="utf-8") + accepted = subprocess.run([str(clone / ".git" / "hooks" / "commit-msg"), "MSG"], + cwd=clone, capture_output=True, text=True) + self.assertEqual(refused.returncode, 1, refused.stderr) + self.assertIn("which review asked for it", refused.stderr) + self.assertEqual(accepted.returncode, 0, accepted.stderr) diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index a9f94702d..ef18126b2 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -373,6 +373,9 @@ to the way the project writes: line naming a .info, core-info or System.dat no "is correct", "is accurate", "still declares", "omits" or "missing". The note states what that file declares and what the code reads. +- No pointer at a review in a comment, a docstring or a test name: a review + number in parentheses, a note file named after one, a test module or class + named after a review. The comment says what the code does. Formatting stays: bold labels, lists, tables and admonitions make a page easier to use. When markup shows raw on the site, the rendering is fixed and @@ -456,8 +459,10 @@ A commit subject is English, 50 characters at most, `type: description` with a lowercase word after the colon. The types are `feat`, `refactor`, `chore`, `docs` and `fix`. The message does not use bug, regression, revert, dead code, broken, wrong, hack, workaround or implement (derived forms included), nor the -marketing vocabulary above, and carries no Co-Authored-By trailer. A body -explains the why when the subject cannot: the scenario that failed, the +marketing vocabulary above, and carries no Co-Authored-By trailer. It says +what the change does, never which review asked for it: "review 3", "audit 12" +or "round 5" with its number points at notes no clone holds and is refused. A +body explains the why when the subject cannot: the scenario that failed, the upstream `file:line` that decides it, what users will see. ``` @@ -465,6 +470,14 @@ feat: add retrobat platform support refactor: extract shared utils to common.py ``` +`scripts/commit_rules.py` holds these rules. Git does not copy hooks with a +clone, so the commit-msg hook that runs them is set up once per clone: + +```bash +printf '#!/bin/sh\nexec python3 scripts/commit_rules.py "$1"\n' > .git/hooks/commit-msg +chmod +x .git/hooks/commit-msg +``` + Files are staged by name, never with `git add -A` or `git add .`, which sweep in deletions and work that is not yours. Generated artifacts (`database.json`, `retired.json`, `derivable.json`, `install/*.json`, `README.md`, `CONTRIBUTING.md`, `exports/`, diff --git a/wiki/tools.md b/wiki/tools.md index ce8c14359..6e0024392 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -814,8 +814,9 @@ hard-links `.cache/large/`, `data/` and the gitignored `bios/` files from the main checkout. Before each job it is fast-forwarded, or rebased when it holds unpushed commits, onto `origin/main`. A job that finds anything uncommitted there stops as `blocked` and cleans nothing. Commits name their files -(`git commit -- `), and their subjects are checked against the -commit-msg hook when the registry loads. A job that leaves a change no commit +(`git commit -- `), and their subjects are checked against +`scripts/commit_rules.py`, the rules the commit-msg hook runs, when the +registry loads. A job that leaves a change no commit rule takes fails. `--push` sends the batch: rebase on `origin/main` (a conflict keeps the batch From 7a34c2ddecb66128652968638a8a24c1c62a9e98 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:02:00 +0100 Subject: [PATCH 10/25] fix: align the privacy page with what is counted --- tests/test_site_privacy.py | 32 ++++++++++++++++++++++++++++++++ website/src/scripts/count.ts | 26 ++++++++++++++++---------- wiki/privacy.md | 9 +++++++-- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/tests/test_site_privacy.py b/tests/test_site_privacy.py index 906d39ba7..c37038da0 100644 --- a/tests/test_site_privacy.py +++ b/tests/test_site_privacy.py @@ -49,6 +49,25 @@ class TheCounterAsksNothingOfTheDevice(unittest.TestCase): self.assertIn("countEvent(`search-none/${searchWords(query)}`)", self.code) self.assertIn("message.startsWith('No results for')", self.code) + def _shape(self, name: str): + """A search-shape predicate of count.ts, its regexes run by Python + (the counter's patterns use no syntax the two read differently).""" + body = re.search(rf"const {name} = \(query: string\): boolean =>(.*?);\n", self.code, re.S).group(1) + patterns = [re.compile(p) for p in re.findall(r"/(\^.*?\$|\[[^/]*?\]|\\d)/\.test\(query\)", body)] + digits = "(query.match(/\\d/g) ?? []).length <= 4" in body + return lambda q: all(p.search(q) for p in patterns) and ( + not digits or len(re.findall(r"\d", q)) <= 4) + + def test_a_number_alone_is_never_sent(self) -> None: + """An 8-digit phone number is valid hex and went out as a hash.""" + shapes = [self._shape(n) for n in ("isHash", "isFileName", "isFileStem")] + for query in ("55512345", "91234567", "0612345678", "+33612345678"): + with self.subTest(query=query): + self.assertFalse(any(shape(query) for shape in shapes)) + for query in ("b4430a3b", "scph5501", "scph5501.bin", "a" * 40): + with self.subTest(query=query): + self.assertTrue(any(shape(query) for shape in shapes)) + def test_one_endpoint(self) -> None: self.assertEqual(set(re.findall(r"https://[\w.-]+", self.code)), {"https://retrobios.goatcounter.com"}) @@ -70,6 +89,19 @@ class ThePageSaysWhatIsCounted(unittest.TestCase): with self.subTest(said=said): self.assertIn(said, page) + def test_the_page_names_what_the_counter_cannot_tell_apart(self) -> None: + """The page left out outbound clicks, which count.ts sends, said the + referrer went by domain when the origin goes, and promised a search + word goes out only as a file name while a nickname with digits does.""" + page = PRIVACY.read_text(encoding="utf-8") + code = COUNTER.read_text(encoding="utf-8") + self.assertIn("`out/${url.hostname}`", code) + self.assertIn("link to another site", page) + self.assertIn("from.origin", code) + self.assertIn("`https://www.google.com`", page) + self.assertIn("`dupont1985`", page) + self.assertIn("phone number, never is", page) + def test_the_installer_has_no_counter(self) -> None: installer = (REPO_ROOT / "install.py").read_text(encoding="utf-8") self.assertNotIn("goatcounter", installer.lower()) diff --git a/website/src/scripts/count.ts b/website/src/scripts/count.ts index 31c5473e7..1a798738c 100644 --- a/website/src/scripts/count.ts +++ b/website/src/scripts/count.ts @@ -4,11 +4,12 @@ // not to be tracked (Do Not Track, Global Privacy Control), from a local // build, or from a page not served at the site's address. // -// Sent: the page path (never its query or #anchor), the domain of the site -// the reader came from, and the name of an action. An action is a click on +// Sent: the page path (never its query or #anchor), the origin of the site +// the reader came from (scheme and host, no path), and the name of an +// action. An action is a click on // a copy button, a pack or file download, a link that leaves the site, a -// site search that found nothing (its words only when they are a file -// name or a hash), and the answer to "Did this page help?". +// site search that found nothing (its words only in the shapes below), +// and the answer to "Did this page help?". const ENDPOINT = 'https://retrobios.goatcounter.com/count'; const HOST = 'abdess.github.io'; @@ -37,7 +38,7 @@ export const countEvent = (name: string): void => { send({ p: name.slice(0, 120), t: pagePath(), e: 'true' }); }; -// The site the reader came from, by domain alone: a path can carry a +// The site the reader came from, by its origin alone: a path can carry a // search or a private thread. const referrer = (): string => { try { @@ -104,11 +105,16 @@ const onClick = (event: MouseEvent): void => { // Search // A search that found nothing names a missing page. Its words are sent -// only in a shape no person's details take: a hash, a file name -// ("scph5501.bin"), or one word of letters and at most four digits -// ("scph5501", "ps3updat"). Any other search is counted with no words, -// as "other": a name, a phone number or an address is never sent. -const isHash = (query: string): boolean => /^(?:[0-9a-f]{8}|[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/.test(query); +// only in three shapes: a hash with at least one letter, a file name +// ("scph5501.bin"), or one word that starts with a letter and holds at most +// four digits ("scph5501", "ps3updat"). Any other search is counted with no +// words, as "other". A number alone, a phone number among them, is never +// sent. A nickname of the third shape ("dupont1985") is, as wiki/privacy.md +// says: nothing tells it from a file name. +// An 8-digit phone number is valid hex: a hash needs a letter, and a CRC32 +// written in digits only is counted as "other". +const isHash = (query: string): boolean => + /^(?:[0-9a-f]{8}|[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/.test(query) && /[a-f]/.test(query); // The extensions of the files the collection holds: "john.smith" is no file. const isFileName = (query: string): boolean => /^[\w.+-]{1,60}\.(?:bin|rom|zip|7z|pup|keys|img|dat|bios|ic\d+|u\d+|sys|nvm|mec|rom1|j64|pce|col|sms|gb|gbc|gba|nds|cfg|chd|iso|txt|fw|xml|yaml|pak|pk3|grp)$/.test(query); diff --git a/wiki/privacy.md b/wiki/privacy.md index 16ccd5c07..70f7972dc 100644 --- a/wiki/privacy.md +++ b/wiki/privacy.md @@ -16,7 +16,8 @@ When a page opens, your browser sends one request to carries: - the page address, without the part after `?` or `#`; -- the site you came from, by its domain only (`google.com`, `reddit.com`). +- the site you came from, by its address without a path + (`https://www.google.com`). GoatCounter reads the browser name and the country from that request. It does not keep your IP address. To tell a reload from a second visitor it keeps a @@ -25,9 +26,13 @@ code made from the request for 8 hours, then drops it. These actions are counted the same way, with the page they happened on: - a click on a copy button, a pack download or a file link; +- a click on a link to another site, by that site's domain (`reddit.com`); - a site search that found nothing, so a missing page shows up. Its words are sent only when they read as a file name (`scph5501.bin`, `scph5501`) or a - hash. Any other search is counted as "other", without its words; + hash with at least one letter. A word of letters with a few digits is sent + as typed, so a nickname written that way (`dupont1985`) would be too. A + number alone, such as a phone number, never is. Any other search is counted + as "other", without its words; - the answer to "Did this page help?". Not sent: your screen size, your language, a search that found results. From 9930f46f03ec1bf7b05a5f429ac173fc646bf771 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:02:43 +0100 Subject: [PATCH 11/25] docs: drop a link claim the emulator pages lack --- scripts/sitemeasurements.py | 4 ++-- tests/test_measurements.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/sitemeasurements.py b/scripts/sitemeasurements.py index 7514d1253..e7a71a05c 100644 --- a/scripts/sitemeasurements.py +++ b/scripts/sitemeasurements.py @@ -733,8 +733,8 @@ def data_page_section(path: Path) -> list[str]: "", "[`measurements.sqlite`](downloads/measurements.sqlite) holds every core probe " f"record kept in [`{mr.RECORDS_DIR}/`]({REPO}/tree/main/{mr.RECORDS_DIR}), and " - "`downloads/measurements/.sqlite` the records of one emulator, linked " - f"from its page. The [Measurements]({EXPLORER_PAGE}) page reads " + "`downloads/measurements/.sqlite` the records of one emulator. The " + f"[Measurements]({EXPLORER_PAGE}) page reads " "[`data/measurements/index.json`](data/measurements/index.json). A record line is " "valid against [`core-probe-record.schema.json`](api/v1/schemas/core-probe-record.schema.json).", "", diff --git a/tests/test_measurements.py b/tests/test_measurements.py index 692b409f3..d33742c0c 100644 --- a/tests/test_measurements.py +++ b/tests/test_measurements.py @@ -468,6 +468,22 @@ class SitePages(unittest.TestCase): def item(self, **values: object) -> dict: return {**self.ITEM, **values} + def test_the_data_page_claims_no_link_an_emulator_page_lacks(self) -> None: + """The Data page said each per-emulator database is "linked from its + page". An emulator page shows the proof lines, which link the record + lines and never the database.""" + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as scratch: + path = Path(scratch) / "m.sqlite" + connection = sqlite3.connect(path) + connection.execute("CREATE TABLE meta (key TEXT)") + connection.commit() + connection.close() + text = "\n".join(sm.data_page_section(path)) + proofs = "\n".join(sm.proof_section([self.item()])) + self.assertNotIn("downloads/measurements/", proofs) + self.assertNotIn("linked from its page", text) + self.assertIn("downloads/measurements/.sqlite", text) + URL = f"{REPO_URL}/blob/{'e' * 40}/measurements/beetle_psx.jsonl" def record(self, line: int, proves=(), shown=None, observed=None) -> dict: From 6846bb4e48a8b165be1e22b641a415c096eb36c2 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:03:00 +0100 Subject: [PATCH 12/25] fix: strip links with bracketed text from meta --- scripts/generate_site.py | 10 ++++++++-- tests/test_site_emulator_page.py | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 22ac647b6..9f2c3ee95 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2485,10 +2485,16 @@ def _browser_title(relative: Path, title: str) -> str: return title +_LINK_TEXT = r"(?:`[^`]*`|\[[^\]\[]*\]|[^\]\[])" + + def _plain_markdown(text: str) -> str: text = re.sub(r"<[^>]+>", " ", text) - text = re.sub(r"!\[([^]]*)\]\([^)]*\)", r"\1", text) - text = re.sub(r"\[([^]]+)\]\([^)]*\)", r"\1", text) + # Link text may hold brackets: a code span or a No-Intro name such as + # `[BIOS] LaserActive PAC-N1 (Japan) (v1.02).bin` left the whole link, + # anchor included, in the Pioneer page's meta description. + text = re.sub(rf"!\[({_LINK_TEXT}*)\]\([^)]*\)", r"\1", text) + text = re.sub(rf"\[({_LINK_TEXT}+)\]\([^)]*\)", r"\1", text) # Code spans keep their text whole: validate_schemas.py read # validateschemas.py once every underscore was taken for emphasis. pieces = re.split(r"(`[^`]*`)", text) diff --git a/tests/test_site_emulator_page.py b/tests/test_site_emulator_page.py index be1165ee1..0973ed36c 100644 --- a/tests/test_site_emulator_page.py +++ b/tests/test_site_emulator_page.py @@ -148,6 +148,15 @@ class WhatASearchSnippetSays(unittest.TestCase): self.assertTrue(plain.startswith(text[:-3])) self.assertIn(plain[len(text) - 3], " ,:") + def test_a_link_whose_text_holds_brackets_keeps_its_text_only(self): + """The Pioneer meta description published the whole link, anchor + included: the text `[BIOS] ...bin` stopped the pattern at its "]".""" + self.assertEqual( + gs._plain_markdown("**Start with** [`[BIOS] LaserActive PAC-N1 (Japan) (v1.02).bin`]" + "(#sha1-6973e2593e66). ares reads it."), + "Start with [BIOS] LaserActive PAC-N1 (Japan) (v1.02).bin. ares reads it.") + self.assertEqual(gs._plain_markdown("[a [b] c](x) and [d](y)"), "a [b] c and d") + def test_code_keeps_its_underscores(self): self.assertEqual(gs._plain_markdown("only `validate_schemas.py` and _this_"), "only validate_schemas.py and this") From d7019a0ca495a60814573409587659d976ff7b9f Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:04:25 +0100 Subject: [PATCH 13/25] fix: give prboom-plus citations their full paths --- emulators/prboom-plus.yml | 28 ++++++++++++------------ tests/test_audit_regressions.py | 34 ++++++++++++++++++++++++++++++ website/src/data/source-paths.json | 8 +++++++ 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/emulators/prboom-plus.yml b/emulators/prboom-plus.yml index 912ff69bd..d12e1fca9 100644 --- a/emulators/prboom-plus.yml +++ b/emulators/prboom-plus.yml @@ -120,7 +120,7 @@ files: analysis: engine_wad_contents: - source_ref: "data/CMakeLists.txt:24-340 (rdatawad input lists), data/CMakeLists.txt:342-362 (WAD_SRC and WAD_CMDLINE)" + source_ref: "prboom2/data/CMakeLists.txt:24-340 (rdatawad input lists), prboom2/data/CMakeLists.txt:342-362 (WAD_SRC and WAD_CMDLINE)" note: > What rdatawad packs into prboom-plus.wad, in the order the command line builds it. @@ -135,8 +135,8 @@ analysis: - "SINETABL, TANGTABL, TANTOANG, GAMMATBL" - "CHEXDEH, BFGBEX, NERVEBEX" - "GLSHADOW, GLFP, GLVP" - - "-PRBHUD-, read by hu_stuff.c:1144" - - "the M_* OpenGL automap icons listed in gl_map.c:48-68" + - "-PRBHUD-, read by prboom2/src/hu_stuff.c:1144" + - "the M_* OpenGL automap icons listed in prboom2/src/gl_map.c:48-68" sounds: "DSDGSIT, DSDGATK, DSDGACT, DSDGDTH, DSDGPAIN" graphics: "the DIG* status bar digits, STBR*, the BOX* frame pieces, STKEYS6-8, STCFN096, M_BUTT1/2, M_COLORS, M_PALNO, M_PALSEL, M_VBOX, CROSS1-3" flats: "-N0_TEX-, between FF_START and FF_END" @@ -146,7 +146,7 @@ analysis: files: - "shaders/glvp.txt" - "shaders/glfp.txt" - source_ref: "gl_shader.c:144-152 (path built from I_DoomExeDir), gl_shader.c:84-129 (ReadLump)" + source_ref: "prboom2/src/gl_shader.c:144-152 (path built from I_DoomExeDir), prboom2/src/gl_shader.c:84-129 (ReadLump)" note: > Read from the config directory when present. ReadLump falls back to the GLVP and GLFP lumps in the ns_prboom namespace when the file is missing, @@ -160,18 +160,18 @@ analysis: - filename: "prboom-plus.cfg" source_ref: "prboom2/src/m_misc.c:1586-1600 (BOOM_CFG, path from I_DoomExeDir), prboom2/src/m_misc.c:1606 (read)" - filename: "RGB2PAL.dat" - source_ref: "gl_hires.c:938-953 (read), gl_hires.c:962-967 (opened wb and written)" + source_ref: "prboom2/src/gl_hires.c:938-953 (read), prboom2/src/gl_hires.c:962-967 (opened wb and written)" note: > A 24-bit RGB to palette lookup table built from V_GetPlaypal, so its - contents follow whichever IWAD is loaded. gl_hires.c:920-936 prefers + contents follow whichever IWAD is loaded. prboom2/src/gl_hires.c:920-936 prefers an RGB2PAL lump of the right length when a WAD supplies one. - filename: ".cache" - source_ref: "gl_hires.c:1144-1147 (read), gl_hires.c:1179 (gld_HiRes_WriteCache)" + source_ref: "prboom2/src/gl_hires.c:1144-1147 (read), prboom2/src/gl_hires.c:1179 (gld_HiRes_WriteCache)" note: "Keyed on the source image mtime and discarded when it differs." - filename: "levelstat.txt" - source_ref: "e6y.c:968" + source_ref: "prboom2/src/e6y.c:968" - filename: "levelinfo.txt" - source_ref: "prboom2/src/d_main.c:1049-1060, behind GL_DOOM and PRBOOM_DEBUG" + source_ref: "prboom2/src/d_main.c:1049-1060 (behind GL_DOOM and PRBOOM_DEBUG)" - filename: "temp_a.nut, temp_v.nut" source_ref: "prboom2/src/m_misc.c:1065-1066 (cap_tempfile1, cap_tempfile2)" @@ -189,11 +189,11 @@ analysis: - what: "autoload directories, /autoload/doom-all and one per IWAD" source_ref: "prboom2/src/d_main.c:1400-1414 (GetAutoloadBaseDir), prboom2/src/d_main.c:1500-1519 (D_AutoloadIWadDir)" - what: "demos, -playdemo, -fastdemo, -record" - source_ref: "r_demo.c:89-119, g_game.c:2570, g_game.c:3150" + source_ref: "prboom2/src/r_demo.c:89-119, prboom2/src/g_game.c:2570, prboom2/src/g_game.c:3150" - what: "external high resolution texture packs, under textures/, flats/ and patches/" - source_ref: "gl_hires.c:467-631 (hires_paths), gl_hires.c:703-719 (gl_texture_hires_dir or I_DoomExeDir)" + source_ref: "prboom2/src/gl_hires.c:467-631 (hires_paths), prboom2/src/gl_hires.c:703-719 (gl_texture_hires_dir or I_DoomExeDir)" - what: "savegames, DOOMSAVEDIR or the config directory" - source_ref: "prboom2/src/d_main.c:1019-1032, m_menu.c:957" + source_ref: "prboom2/src/d_main.c:1019-1032, prboom2/src/m_menu.c:957" - what: "response files, @name" source_ref: "prboom2/src/d_main.c:1094-1112" @@ -203,6 +203,6 @@ analysis: - what: "ffmpeg, video capture and muxing" source_ref: "prboom2/src/m_misc.c:1062-1064 (cap_soundcommand, cap_videocommand, cap_muxcommand)" - what: "wget then unzip, fetching a WAD a game server asks for" - source_ref: "d_client.c:236-257 (execlp), doc/prboom-plus.6:200-206" + source_ref: "prboom2/src/d_client.c:236-257 (execlp), prboom2/doc/prboom-plus.6:200-206" - what: "getwad_cmdline, run through system() when a needed WAD is missing" - source_ref: "r_demo.c:1641-1704 (D_TryGetWad), prboom2/src/m_misc.c:1029 (default empty, so the path is inert until configured)" + source_ref: "prboom2/src/r_demo.c:1641-1704 (D_TryGetWad), prboom2/src/m_misc.c:1029 (default empty, so the path is inert until configured)" diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 34744187d..65ba55419 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -7414,6 +7414,40 @@ class TupleAnnotationsMatchTheirReturns(unittest.TestCase): self.assertEqual(mismatched, []) +class PrboomPlusCitesFullPaths(unittest.TestCase): + """prboom-plus cited gl_hires.c, e6y.c, d_client.c and g_game.c bare: + none resolves against the repository, so the page showed them as plain + code with no link, and "behind GL_DOOM and PRBOOM_DEBUG" was read as a + seventeenth citation. Each name has one path in the tree at the pin.""" + + def test_every_cited_file_has_its_directory(self): + import yaml + + from scripts.sourceref import source_ref_values, split_source_ref + + profile = yaml.safe_load((ROOT / "emulators" / "prboom-plus.yml").read_text(encoding="utf-8")) + refs: list[str] = [] + + def walk(node): + if isinstance(node, dict): + for key, value in node.items(): + if key == "source_ref": + refs.extend(r for _, r in source_ref_values(value)) + else: + walk(value) + elif isinstance(node, list): + for item in node: + walk(item) + + walk(profile) + parts = [p for ref in refs for p in split_source_ref(ref)] + self.assertGreater(len(parts), 40) + for part in parts: + with self.subTest(path=part.path): + self.assertIsNotNone(part.start) + self.assertTrue(part.path.startswith("prboom2/"), part.path) + + class AutoFetchIssueBodyReadsAsWritten(unittest.TestCase): """The --create-issues body was a template: Title Case headings, "The following...", an exclamation mark, numbered orders to the reader, a diff --git a/website/src/data/source-paths.json b/website/src/data/source-paths.json index d0f00e7a2..b5e05212f 100644 --- a/website/src/data/source-paths.json +++ b/website/src/data/source-paths.json @@ -2878,11 +2878,19 @@ "github.com/coelckers/prboom-plus@969515162c5aebea4ad7a125ee178dcad3d576ad": [ "prboom2/CMakeLists.txt", "prboom2/data/CMakeLists.txt", + "prboom2/doc/prboom-plus.6", "prboom2/src/CMakeLists.txt", "prboom2/src/MUSIC/flplayer.c", "prboom2/src/SDL/i_system.c", + "prboom2/src/d_client.c", "prboom2/src/d_main.c", + "prboom2/src/e6y.c", + "prboom2/src/g_game.c", + "prboom2/src/gl_hires.c", + "prboom2/src/gl_shader.c", + "prboom2/src/m_menu.c", "prboom2/src/m_misc.c", + "prboom2/src/r_demo.c", "prboom2/src/w_wad.c" ], "github.com/crawl/crawl@a5ed93406571b9682050cfc318b15b9f637d4e7e": [ From cf4afccf047c0b10fccfe2ffabc7662b45a6a3d0 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:10:40 +0100 Subject: [PATCH 14/25] fix: show source_ref annotations on the cards --- scripts/sitesources.py | 23 +++++++++++++++++----- scripts/sourceref.py | 19 ++++++++++++++++++ tests/test_site_render.py | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/scripts/sitesources.py b/scripts/sitesources.py index c28b7c5f1..a5cdbbccc 100644 --- a/scripts/sitesources.py +++ b/scripts/sitesources.py @@ -16,9 +16,10 @@ from dataclasses import dataclass from pathlib import Path import upstream -from siterender import _by_mode +from siterender import _by_mode, _escape_tags from sourceref import ( - _HEX_ADDRESS_RE, _prose_runs, is_binary_citation, ref_role, source_ref_values, + _HEX_ADDRESS_RE, _prose_runs, is_binary_citation, ref_annotation, ref_role, + source_ref_values, split_source_ref, ) @@ -512,7 +513,7 @@ def source_ref_items(profile: dict, value: str | dict | None) -> list[tuple[str, for part in split_source_ref(refs): if is_binary_citation(part.path): text, binary = _binary_citation(part.path, binary) - rendered.append((text, "")) + rendered.append((text + _annotation_markdown(ref_annotation(part.raw)), "")) continue binary = "" role, bare = ref_role(part.path) @@ -544,19 +545,31 @@ def source_ref_items(profile: dict, value: str | dict | None) -> list[tuple[str, role = (role_label(profile, current_role, part.path) if current_role and _two_repositories(profile) else "") + note = _annotation_markdown(ref_annotation(part.raw)) if selected is None: - rendered.append((f"`{display}`", role)) + rendered.append((f"`{display}`{note}", role)) else: repo, pin = selected url = _source_permalink( repo, pin, real_path, part.start, part.end ) - rendered.append((f"[`{display}`]({url})", role)) + rendered.append((f"[`{display}`]({url}){note}", role)) groups.append((label, rendered or [(f"`{refs}`", "")])) return groups +def _annotation_markdown(text: str) -> str: + """A part's annotation after its citation, as written: `(lsf default)`. + + Profile prose, so placeholders keep their brackets and nothing in it + opens emphasis or a table cell. + """ + if not text: + return "" + return " " + _escape_tags(re.sub(r"([*|\\])", r"\\\1", text)) + + def _binary_citation(path: str, previous: str) -> tuple[str, str]: """A part citing code inside a shipped binary, and the binary it names. diff --git a/scripts/sourceref.py b/scripts/sourceref.py index ec980b6e2..a7436f977 100644 --- a/scripts/sourceref.py +++ b/scripts/sourceref.py @@ -123,6 +123,25 @@ def split_source_ref(ref: str) -> list[RefPart]: return parts +_LOCATION_RE = re.compile(r":\d+(?:-\d+)?|^\s*\d+(?:-\d+)?(?=\s|$)") + + +def ref_annotation(raw: str) -> str: + """The words a part writes beside its location, for a reader. + + split_source_ref drops them because a comparison has no use for them, + but `src/loader2.cpp:284 (lsf default), src/netcfg.cpp:204 (lsf + override)` says which line wins only through them, and 1,103 of the + profiles' annotations were on no page. + """ + paren = raw.find("(") + head = raw if paren < 0 else raw[:paren] + located = _LOCATION_RE.search(head) + if located: + return raw[located.end():].strip() + return raw[paren:].strip() if paren >= 0 else "" + + def ref_prose_text(ref: str) -> str: """The text of a source_ref its parts do not read, positions kept. diff --git a/tests/test_site_render.py b/tests/test_site_render.py index 9628bd5ed..7fe9c52dc 100644 --- a/tests/test_site_render.py +++ b/tests/test_site_render.py @@ -255,6 +255,47 @@ class AnAliasTreeIsListedOnItsParentsPage(unittest.TestCase): self.assertEqual([t["ref"] for t in trees], ["freeintv-overlays"]) +class CitationsKeepTheirAnnotations(unittest.TestCase): + """split_source_ref drops annotations for profile_sync, and the cards + reused it: abuse's two lsf lines lost "(lsf default)" and "(lsf + override)", the only words saying which one wins.""" + + @mock.patch("sitesources.tree_holds", lambda *_args: True) + def test_each_part_shows_what_the_profile_wrote_beside_it(self): + import sitesources + + profile = {"source": "https://github.com/Xenoveritas/abuse", + "source_commit": "a" * 40} + ref = ("src/loader2.cpp:284 (lsf default), src/netcfg.cpp:204 (lsf override), " + "libretro.cpp:1520-1521 candidates_a1200, 30-31 (see ), src/x.c:1") + (label, items), = sitesources.source_ref_items(profile, ref) + texts = [text for text, _role in items] + self.assertTrue(texts[0].endswith(") (lsf default)"), texts[0]) + self.assertTrue(texts[1].endswith(") (lsf override)"), texts[1]) + self.assertTrue(texts[2].endswith(") candidates_a1200"), texts[2]) + self.assertTrue(texts[3].endswith(") (see <system_dir>)"), texts[3]) + self.assertTrue(texts[4].endswith(")"), texts[4]) + + def test_the_annotation_is_what_follows_the_location(self): + from sourceref import ref_annotation + + self.assertEqual(ref_annotation("munt ROMInfo.cpp:206-213"), "") + self.assertEqual(ref_annotation("source atari800/src/sysrom.c:91"), "") + self.assertEqual(ref_annotation("doc/notes.txt (read at boot)"), "(read at boot)") + self.assertEqual(ref_annotation("a.c (see b.c:12)"), "(see b.c:12)") + self.assertEqual(ref_annotation("0x53c338-0x53c36f (dreamBios row)"), "(dreamBios row)") + + def test_a_binary_citation_keeps_its_annotation(self): + import sitesources + + (_label, items), = sitesources.source_ref_items( + {}, "demul.exe 0x6b4d80-0x6b4ea3 (dc table), 0x53ca4d-0x53ca7e (load, fatal)") + self.assertEqual([text for text, _ in items], [ + "offset `0x6b4d80-0x6b4ea3` in `demul.exe` (dc table)", + "offset `0x53ca4d-0x53ca7e` in `demul.exe` (load, fatal)", + ]) + + class ForgesUpstreamDoesNotRead(unittest.TestCase): """Profiles pinned on SourceForge, voidpoint.io, Bitbucket or Software Heritage showed no revision, and their citations were plain code.""" From b6b26392e1f24ea5e739fa307894fd7762b67420 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:12:36 +0100 Subject: [PATCH 15/25] refactor: move the collection model to its module --- .github/workflows/deploy-site.yml | 1 + scripts/fetch_site_images.py | 3 +- scripts/generate_site.py | 174 +----------------------------- scripts/make_og_cards.py | 2 +- scripts/sitecollection.py | 169 +++++++++++++++++++++++++++++ scripts/siteinputs.py | 11 ++ scripts/siterender.py | 2 +- tests/test_site_inputs.py | 39 ++++++- 8 files changed, 227 insertions(+), 174 deletions(-) create mode 100644 scripts/sitecollection.py diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 04b329d26..cdc72f0eb 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -47,6 +47,7 @@ on: - "scripts/safeparse.py" - "scripts/sect233r1.py" - "scripts/sitebootstrap.py" + - "scripts/sitecollection.py" - "scripts/sitediagrams.py" - "scripts/siteentries.py" - "scripts/siteguides.py" diff --git a/scripts/fetch_site_images.py b/scripts/fetch_site_images.py index 8da5e1c9a..f698289e9 100644 --- a/scripts/fetch_site_images.py +++ b/scripts/fetch_site_images.py @@ -172,7 +172,8 @@ def fetch_logos() -> None: def fetch_icons() -> None: - from generate_site import _group_by_manufacturer, load_database + from common import load_database + from sitecollection import _group_by_manufacturer from siterender import SYSTEM_ICON_BASE, _icon_name db = load_database(str(ROOT / "database.json")) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 9f2c3ee95..7ea555b9c 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -39,6 +39,10 @@ from pathlib import Path from typing import TypeVar sys.path.insert(0, os.path.dirname(__file__)) +from sitecollection import ( # noqa: E402 + _copy_places, _group_by_manufacturer, _profiles_in, + _profiles_on_disk, _slug, collection_paths, +) from common import ( BUCKET_FOLDERS, names_manufacturer, @@ -70,10 +74,6 @@ from cross_reference import FileConsumers, entry_target, held_copy from nativemode import check_label, digest_algorithm, reads_file_contents - - - - yaml = require_yaml() from generate_readme import ( compute_coverage, @@ -138,8 +138,6 @@ GENERATED_DIRS = ["platforms", "systems", "emulators", "wiki", "api", "downloads PUBLIC_DIRS = ("api", "downloads") - - def _content_check_ceiling(profiles: dict) -> str: """How far content checking can reach across every profiled entry.""" hashed = sized = neither = 0 @@ -169,9 +167,6 @@ def _content_check_ceiling(profiles: dict) -> str: ) - - - CLS_LABELS = { "official_port": "Official ports", "community_fork": "Community forks", @@ -201,33 +196,6 @@ CLS_LABEL_ONE = { } -def _copy_places(db: dict) -> dict[str, set[tuple[str, str, str]]]: - """sha1 -> every (manufacturer, console, file name) holding a copy. - - The database keeps one path per content, deduplication having left one - copy. by_path_suffix still records the other folders a copy is filed in, - under the name that folder gives it. - """ - files = db.get("files", {}) - bios_dir = Path(str(db.get("bios_dir") or "bios")) - places: dict[str, set[tuple[str, str, str]]] = {} - for sha1, entry in files.items(): - parts = str(entry.get("path", "")).split("/") - if len(parts) >= 3 and parts[0] == "bios": - places.setdefault(sha1, set()).add((parts[1], parts[2], parts[-1])) - for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items(): - parts = tail.split("/") - if len(parts) < 3 or parts[1].startswith(".") or len(sha1s) != 1: - continue - if sha1s[0] in files and (bios_dir / tail).is_file(): - places.setdefault(sha1s[0], set()).add((parts[0], parts[1], parts[-1])) - return places - - -def _slug(manufacturer: str) -> str: - return manufacturer.lower().replace(" ", "-") - - def _copies_by_path(db: dict) -> dict[str, set[tuple[str, str, str]]]: """The copy places of each content, keyed by the path the database keeps.""" files = db.get("files", {}) @@ -413,11 +381,6 @@ def _system_pages_from_data( return pages - - - - - def _timestamp() -> str: """The build time for data files (stats, API records), ISO 8601.""" return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") @@ -436,8 +399,6 @@ def _date_line() -> str: return f'
{data_as_of()}.
' - - # Home page @@ -5271,122 +5232,6 @@ def write_lookup_shards( return len(shards) -def collection_paths(db: dict) -> dict[str, set[str]]: - """Every path under bios/ that holds each file, by sha1. - - The database keeps one path per content. The others are the full tails - by_path_suffix records that name a file on disk: RPG Maker, engine - trees and the copies filed under several consoles keep 2,502 of them. - """ - files = db.get("files", {}) - bios_dir = Path(str(db.get("bios_dir") or "bios")) - paths: dict[str, set[str]] = {} - for sha1, entry in files.items(): - path = str(entry.get("path", "")) - if path.startswith("bios/") and path.count("/") >= 2: - paths.setdefault(sha1, set()).add(path) - for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items(): - if tail.count("/") < 2 or len(sha1s) != 1 or sha1s[0] not in files: - continue - if (bios_dir / tail).is_file(): - paths.setdefault(sha1s[0], set()).add(f"bios/{tail}") - return paths - - -def _group_by_manufacturer(db: dict) -> dict[str, dict[str, list]]: - """Group files by manufacturer -> console -> files. - - The database keeps one path per content, and a copy filed elsewhere was - on no page of its own: the Channel F BIOS sat under Arcade > FBNeo only, - and Fairchild had no system page. Every folder holding a copy lists it, - read from the full paths by_path_suffix records. The game engines' - files are grouped by engine (_engine_sections). - """ - files = db.get("files", {}) - manufacturers: dict[str, dict[str, list]] = {} - for sha1, where in _copy_places(db).items(): - for manufacturer, console in sorted({(m, c) for m, c, _ in where}): - manufacturers.setdefault(manufacturer, {}).setdefault(console, []).append(files[sha1]) - return _engine_sections(manufacturers) - - -def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dict[str, list]]: - """A game engine's files in one section headed with the engine's name. - - The Systems index listed ".variants", "scummvm.zip" and "wildmidi.cfg" - as games, and ScummVM's files sat on three pages. A folder of variants - joins the section of its primary, a file kept at a top's root joins - the section of the one engine that reads it (else of the engine the - top is named after), the folder under Other named after a game-engine - top joins that top, and a folder named by a profile key is headed with - the engine's name (SECTION_LABELS), its address unchanged. - """ - from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415 - - profiles = {key: profile for key, profile in _profiles_on_disk().items() - if profile.get("type") not in ("alias", "test")} - readers: dict[str, set[str]] = {} - for key, profile in profiles.items(): - for entry in profile.get("files") or []: - if isinstance(entry, dict): - for name in [entry.get("name"), *(entry.get("aliases") or [])]: - if name: - readers.setdefault(str(name).rsplit("/", 1)[-1], set()).add(key) - - def engine(key: str) -> str: - return str(profiles[key].get("emulator") or key) - - def profile_of(name: str) -> str | None: - return next((k for k in sorted(profiles) - if name.casefold() in (k.casefold(), engine(k).casefold())), None) - - out: dict[str, dict[str, list]] = {} - for mfr, consoles in manufacturers.items(): - if mfr not in GAME_DATA_TOPS: - out[mfr] = dict(consoles) - continue - own = profile_of(mfr) - regrouped: dict[str, list] = {} - for console, files in sorted(consoles.items(), key=lambda item: item[0].startswith(".")): - for f in files: - parts = str(f.get("path", "")).split("/") - target = console - if console.startswith(".") or (len(parts) == 3 and console == parts[-1]): - # A variant answers its primary's name: scummvm.zip.a93f1c4b. - base = parts[-1].rsplit(".", 1)[0] if console.startswith(".") else parts[-1] - keys = sorted(readers.get(base, ())) - key = keys[0] if len(keys) == 1 else own - found = next((c for c, held in regrouped.items() - if any(h.get("name") == base for h in held)), None) - target = found or (next((c for c in consoles if c == key), None) - or (engine(key) if key else base)) - if target != console: - record_card_move(mfr, console, str(f.get("sha1", "")), mfr, target) - regrouped.setdefault(target, []).append(f) - out[mfr] = regrouped - # The folder under Other an engine top is named after: ScummVM's - # engine data under Other/ScummVM, its themes under ScummVM/. - others = out.get("Other", {}) - for folder in sorted(others): - top = next((t for t in out if t in GAME_DATA_TOPS and t.casefold() == folder.casefold()), - None) - if top is None or top in _PROGRAM_TOPS: - continue - sections = out[top] - target = next((c for c in sections if c.casefold() == folder.casefold()), folder) - held = {str(f.get("sha1", "")) for f in sections.get(target, [])} - for f in others[folder]: - record_card_move("Other", folder, str(f.get("sha1", "")), top, target) - sections[target] = [*sections.get(target, []), - *(f for f in others.pop(folder) if str(f.get("sha1", "")) not in held)] - for mfr in GAME_DATA_TOPS: - for console in out.get(mfr, {}): - key = profiles.get(console) and console - if key and engine(key) != console: - SECTION_LABELS[(mfr, console)] = engine(key) - return {mfr: consoles for mfr, consoles in out.items() if consoles} - - def _console_display(manufacturer: str, console: str) -> str: """A console section's name as the reader looks for it: the heading system_name gives its id, or the folder alone under a program's top @@ -6701,15 +6546,6 @@ def _reading_pairs(sha1: str, consumers: FileConsumers) -> list[tuple[str, dict] return pairs -def _profiles_on_disk() -> dict[str, dict]: - return _profiles_in(str(siteinputs.current().emulators), skip_aliases=False) - - -@functools.lru_cache(maxsize=None) -def _profiles_in(emulators_dir: str, skip_aliases: bool) -> dict[str, dict]: - return load_emulator_profiles(emulators_dir, skip_aliases=skip_aliases) - - def _entry_systems(emulator: str, entry: dict) -> list[str]: """The systems an entry is read for: its own `system:` or `systems:` (a MAME device set several systems load), else its profile's only one.""" @@ -11361,8 +11197,6 @@ def generate_gap_analysis( return "\n".join(lines) + "\n" - - def _core_scope_note(config: dict, profiles: dict) -> str: """" (all libretro)" when the platform resolves every libretro core. diff --git a/scripts/make_og_cards.py b/scripts/make_og_cards.py index ca0a2832d..e35bf3441 100644 --- a/scripts/make_og_cards.py +++ b/scripts/make_og_cards.py @@ -70,7 +70,7 @@ def system_cards() -> dict[str, tuple[str, str]]: manufacturer and listing its consoles. Read from the same grouping the system pages are written from.""" from common import GAME_DATA_TOPS, load_database - from generate_site import _group_by_manufacturer, _slug + from sitecollection import _group_by_manufacturer, _slug def lead(manufacturer: str) -> str: # Other holds one folder per emulator, a game publisher's page its diff --git a/scripts/sitecollection.py b/scripts/sitecollection.py new file mode 100644 index 000000000..937bebe77 --- /dev/null +++ b/scripts/sitecollection.py @@ -0,0 +1,169 @@ +"""The collection as the site files it: every folder holding each file. + +The database keeps one path per content. The folders a copy is also filed +in, the system sections and the game-engine sections are read here, by the +page generator and by the tools that draw or fetch per-system images. The +bios folder comes from siteinputs, never the working directory. +""" + +from __future__ import annotations + +import functools +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +import siteinputs # noqa: E402 +from common import GAME_DATA_TOPS, load_emulator_profiles # noqa: E402 + + +def _copy_places(db: dict) -> dict[str, set[tuple[str, str, str]]]: + """sha1 -> every (manufacturer, console, file name) holding a copy. + + The database keeps one path per content, deduplication having left one + copy. by_path_suffix still records the other folders a copy is filed in, + under the name that folder gives it. + """ + files = db.get("files", {}) + bios_dir = siteinputs.bios_dir(db) + places: dict[str, set[tuple[str, str, str]]] = {} + for sha1, entry in files.items(): + parts = str(entry.get("path", "")).split("/") + if len(parts) >= 3 and parts[0] == "bios": + places.setdefault(sha1, set()).add((parts[1], parts[2], parts[-1])) + for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items(): + parts = tail.split("/") + if len(parts) < 3 or parts[1].startswith(".") or len(sha1s) != 1: + continue + if sha1s[0] in files and (bios_dir / tail).is_file(): + places.setdefault(sha1s[0], set()).add((parts[0], parts[1], parts[-1])) + return places + + +def _slug(manufacturer: str) -> str: + return manufacturer.lower().replace(" ", "-") + + +def collection_paths(db: dict) -> dict[str, set[str]]: + """Every path under bios/ that holds each file, by sha1. + + The database keeps one path per content. The others are the full tails + by_path_suffix records that name a file on disk: RPG Maker, engine + trees and the copies filed under several consoles keep 2,502 of them. + """ + files = db.get("files", {}) + bios_dir = siteinputs.bios_dir(db) + paths: dict[str, set[str]] = {} + for sha1, entry in files.items(): + path = str(entry.get("path", "")) + if path.startswith("bios/") and path.count("/") >= 2: + paths.setdefault(sha1, set()).add(path) + for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items(): + if tail.count("/") < 2 or len(sha1s) != 1 or sha1s[0] not in files: + continue + if (bios_dir / tail).is_file(): + paths.setdefault(sha1s[0], set()).add(f"bios/{tail}") + return paths + + +def _group_by_manufacturer(db: dict) -> dict[str, dict[str, list]]: + """Group files by manufacturer -> console -> files. + + The database keeps one path per content, and a copy filed elsewhere was + on no page of its own: the Channel F BIOS sat under Arcade > FBNeo only, + and Fairchild had no system page. Every folder holding a copy lists it, + read from the full paths by_path_suffix records. The game engines' + files are grouped by engine (_engine_sections). + """ + files = db.get("files", {}) + manufacturers: dict[str, dict[str, list]] = {} + for sha1, where in _copy_places(db).items(): + for manufacturer, console in sorted({(m, c) for m, c, _ in where}): + manufacturers.setdefault(manufacturer, {}).setdefault(console, []).append(files[sha1]) + return _engine_sections(manufacturers) + + +def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dict[str, list]]: + """A game engine's files in one section headed with the engine's name. + + The Systems index listed ".variants", "scummvm.zip" and "wildmidi.cfg" + as games, and ScummVM's files sat on three pages. A folder of variants + joins the section of its primary, a file kept at a top's root joins + the section of the one engine that reads it (else of the engine the + top is named after), the folder under Other named after a game-engine + top joins that top, and a folder named by a profile key is headed with + the engine's name (SECTION_LABELS), its address unchanged. + """ + from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415 + + profiles = {key: profile for key, profile in _profiles_on_disk().items() + if profile.get("type") not in ("alias", "test")} + readers: dict[str, set[str]] = {} + for key, profile in profiles.items(): + for entry in profile.get("files") or []: + if isinstance(entry, dict): + for name in [entry.get("name"), *(entry.get("aliases") or [])]: + if name: + readers.setdefault(str(name).rsplit("/", 1)[-1], set()).add(key) + + def engine(key: str) -> str: + return str(profiles[key].get("emulator") or key) + + def profile_of(name: str) -> str | None: + return next((k for k in sorted(profiles) + if name.casefold() in (k.casefold(), engine(k).casefold())), None) + + out: dict[str, dict[str, list]] = {} + for mfr, consoles in manufacturers.items(): + if mfr not in GAME_DATA_TOPS: + out[mfr] = dict(consoles) + continue + own = profile_of(mfr) + regrouped: dict[str, list] = {} + for console, files in sorted(consoles.items(), key=lambda item: item[0].startswith(".")): + for f in files: + parts = str(f.get("path", "")).split("/") + target = console + if console.startswith(".") or (len(parts) == 3 and console == parts[-1]): + # A variant answers its primary's name: scummvm.zip.a93f1c4b. + base = parts[-1].rsplit(".", 1)[0] if console.startswith(".") else parts[-1] + keys = sorted(readers.get(base, ())) + key = keys[0] if len(keys) == 1 else own + found = next((c for c, held in regrouped.items() + if any(h.get("name") == base for h in held)), None) + target = found or (next((c for c in consoles if c == key), None) + or (engine(key) if key else base)) + if target != console: + record_card_move(mfr, console, str(f.get("sha1", "")), mfr, target) + regrouped.setdefault(target, []).append(f) + out[mfr] = regrouped + # The folder under Other an engine top is named after: ScummVM's + # engine data under Other/ScummVM, its themes under ScummVM/. + others = out.get("Other", {}) + for folder in sorted(others): + top = next((t for t in out if t in GAME_DATA_TOPS and t.casefold() == folder.casefold()), + None) + if top is None or top in _PROGRAM_TOPS: + continue + sections = out[top] + target = next((c for c in sections if c.casefold() == folder.casefold()), folder) + held = {str(f.get("sha1", "")) for f in sections.get(target, [])} + for f in others[folder]: + record_card_move("Other", folder, str(f.get("sha1", "")), top, target) + sections[target] = [*sections.get(target, []), + *(f for f in others.pop(folder) if str(f.get("sha1", "")) not in held)] + for mfr in GAME_DATA_TOPS: + for console in out.get(mfr, {}): + key = profiles.get(console) and console + if key and engine(key) != console: + SECTION_LABELS[(mfr, console)] = engine(key) + return {mfr: consoles for mfr, consoles in out.items() if consoles} + + +def _profiles_on_disk() -> dict[str, dict]: + return _profiles_in(str(siteinputs.current().emulators), skip_aliases=False) + + +@functools.lru_cache(maxsize=None) +def _profiles_in(emulators_dir: str, skip_aliases: bool) -> dict[str, dict]: + return load_emulator_profiles(emulators_dir, skip_aliases=skip_aliases) diff --git a/scripts/siteinputs.py b/scripts/siteinputs.py index 7aa3326ff..1df5389c3 100644 --- a/scripts/siteinputs.py +++ b/scripts/siteinputs.py @@ -30,3 +30,14 @@ def current() -> SiteInputs: def use(inputs: SiteInputs) -> None: _current[0] = inputs + + +def bios_dir(db: dict) -> Path: + """The database's bios folder, as the inputs place it. + + database.json names it relative to its own folder ("bios"). Read from the + working directory instead, a tool started elsewhere found no copy on disk + and dropped five system sections and 2,500 collection paths in silence. + """ + named = Path(str(db.get("bios_dir") or "bios")) + return named if named.is_absolute() else current().db.parent / named diff --git a/scripts/siterender.py b/scripts/siterender.py index 0e4fda9b2..c71d2db10 100644 --- a/scripts/siterender.py +++ b/scripts/siterender.py @@ -780,7 +780,7 @@ def _ensure_console_sections() -> None: import json from common import load_emulator_profiles - from generate_site import _group_by_manufacturer + from sitecollection import _group_by_manufacturer inputs = siteinputs.current() with open(inputs.db, encoding="utf-8") as handle: diff --git a/tests/test_site_inputs.py b/tests/test_site_inputs.py index 57ee16fa4..6e695263d 100644 --- a/tests/test_site_inputs.py +++ b/tests/test_site_inputs.py @@ -85,11 +85,48 @@ class TheGivenProfilesAndPlatforms(_Inputs): self.assertEqual(sitediagrams.registry()["retroarch"].get("release_pack"), False) +class TheCollectionDoesNotDependOnTheWorkingDirectory(unittest.TestCase): + """database.json names its bios folder "bios", and the collection model + read it from the working directory: started from tmp/, it found no copy + on disk and dropped five system sections and 2,500 collection paths.""" + + def test_same_sections_and_paths_from_another_directory(self) -> None: + import json + + import sitecollection + + db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8")) + here = sitecollection.collection_paths(db) + places = sitecollection._copy_places(db) + elsewhere = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp" if (REPO_ROOT / "tmp").is_dir() else None)) + self.addCleanup(shutil.rmtree, elsewhere) + cwd = os.getcwd() + os.chdir(elsewhere) + self.addCleanup(os.chdir, cwd) + self.assertEqual(sitecollection.collection_paths(db), here) + self.assertEqual(sitecollection._copy_places(db), places) + self.assertGreater(sum(map(len, here.values())), len(here)) + + def test_the_bios_folder_sits_beside_the_given_database(self) -> None: + import siteinputs + + siteinputs.use(siteinputs.SiteInputs(db=Path("/elsewhere/database.json"))) + self.addCleanup(siteinputs.use, siteinputs.SiteInputs()) + self.assertEqual(siteinputs.bios_dir({"bios_dir": "bios"}), Path("/elsewhere/bios")) + self.assertEqual(siteinputs.bios_dir({"bios_dir": "/abs/bios"}), Path("/abs/bios")) + + def test_the_image_tools_do_not_load_the_page_generator(self) -> None: + for module in ("make_og_cards.py", "fetch_site_images.py", "siterender.py"): + with self.subTest(module=module): + source = (REPO_ROOT / "scripts" / module).read_text(encoding="utf-8") + self.assertNotIn("from generate_site import", source) + + class NoPageModuleReadsTheRepositoryInputs(unittest.TestCase): """A helper that opens the repository's database, profiles or platform files itself bypasses the flags. siteinputs.current() is the one source.""" - MODULES = ("generate_site.py", "siterender.py", "sitediagrams.py") + MODULES = ("generate_site.py", "siterender.py", "sitediagrams.py", "sitecollection.py") INPUTS = {"database.json", "emulators", "platforms"} def test_no_literal_input_path(self) -> None: From a195023d3dcce80e1d0a5c1d8801e9ce01a1afa9 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:01:27 +0100 Subject: [PATCH 16/25] chore: run every test class when a file runs alone --- tests/test_contributing_text.py | 9 ++++----- tests/test_doc_pages.py | 10 ++++------ tests/test_doc_surfaces.py | 9 ++++----- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/test_contributing_text.py b/tests/test_contributing_text.py index 10f9a3d9b..6999d8e35 100644 --- a/tests/test_contributing_text.py +++ b/tests/test_contributing_text.py @@ -35,7 +35,6 @@ class OneContributingText(unittest.TestCase): ) - class CoverageReadsTheGivenProfiles(unittest.TestCase): """generate_site --emulators-dir changed the emulator pages but not the platform coverage, which read ./emulators whatever was given.""" @@ -202,10 +201,6 @@ class OneFileThroughTheBrowser(unittest.TestCase): )) -if __name__ == "__main__": - unittest.main() - - class TheGuideRebuildsWhenItsPinsMove(unittest.TestCase): """The checklist quotes validate.yml, so a pin bump there must redeploy the guide and meet the README/CONTRIBUTING freshness check.""" @@ -282,3 +277,7 @@ class OneCommandRunsTheChecks(unittest.TestCase): self.assertIn("FAILED python -c raise(SystemExit(3)) (exit 3)", out.getvalue()) self.assertIn("NOT RUN no-such-tool-here --x", out.getvalue()) self.assertIn("1/3 checks passed", out.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index aa691ec21..aa106e1e9 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -1983,7 +1983,6 @@ class NoTextClaimsAScheduledScrape(unittest.TestCase): self.assertNotIn("list_platforms", workflow.read_text(encoding="utf-8")) - class TheCatalogRowsStateFacts(unittest.TestCase): """tools.md said "redump.org serves stale DATs" and "Dat-o-Matic blocks automation", verdicts on two preservation projects, and gave Redump's @@ -2001,11 +2000,6 @@ class TheCatalogRowsStateFacts(unittest.TestCase): self.assertIn("`/bios/`", rows[0]) -if __name__ == "__main__": - unittest.main() - - - class RegionFreeRuleNamesWhereItDoesNotHold(unittest.TestCase): """advanced-usage.md said psxonpsp660.bin is declared [world] and always survives --region. PCSX ReARMed declares it [japan], and on the platforms @@ -2099,3 +2093,7 @@ class LandingAChangeNamesTheCommitRules(unittest.TestCase): self.assertEqual(refused.returncode, 1, refused.stderr) self.assertIn("which review asked for it", refused.stderr) self.assertEqual(accepted.returncode, 0, accepted.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_doc_surfaces.py b/tests/test_doc_surfaces.py index 08a2702ba..e2c598931 100644 --- a/tests/test_doc_surfaces.py +++ b/tests/test_doc_surfaces.py @@ -434,8 +434,6 @@ class PagesOpenOnTheirContent(unittest.TestCase): self.assertEqual(announcing, []) - - class CoreCountsReadFromTheData(unittest.TestCase): """The cores page said "623 cores across 18 platforms": that was every profile, and 91 of them no platform runs.""" @@ -609,9 +607,6 @@ class EveryOptionExists(unittest.TestCase): if (bad := unknown_options(text))} self.assertEqual(wrong, {}) -if __name__ == "__main__": - unittest.main() - class ContributorChecksFetchThePlatformsFiles(unittest.TestCase): """validate.yml fetches each platform's own file before the suite. The @@ -636,3 +631,7 @@ class ContributorChecksFetchThePlatformsFiles(unittest.TestCase): def test_the_testing_guide_names_the_command(self): self.assertIn("export_native.py --all --fetch", _read("wiki/testing-guide.md")) + + +if __name__ == "__main__": + unittest.main() From 3755729beb56929cd01b82f79d6d5b3c92f442f5 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:01:27 +0100 Subject: [PATCH 17/25] fix: end pr_checks through run_cli --- scripts/pr_checks.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/pr_checks.py b/scripts/pr_checks.py index cd7741295..0acd4082c 100644 --- a/scripts/pr_checks.py +++ b/scripts/pr_checks.py @@ -84,16 +84,18 @@ def run(steps: tuple[Step, ...] = STEPS) -> int: return 1 if failed else 0 -def main() -> None: +def main() -> int: parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0]) parser.add_argument("--list", action="store_true", help="print the steps and exit") args = parser.parse_args() if args.list: for step in STEPS: print(step.command) - return - sys.exit(run()) + return 0 + return run() if __name__ == "__main__": - main() + from common import run_cli + + run_cli(main) From fd62b21abd6bfab1fd47d8fdc5121e6289df00d3 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:01:27 +0100 Subject: [PATCH 18/25] chore: expect binary citation annotations --- tests/test_site_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_site_reference.py b/tests/test_site_reference.py index 36f130fde..e2b92f96b 100644 --- a/tests/test_site_reference.py +++ b/tests/test_site_reference.py @@ -1102,7 +1102,7 @@ class SourceLinksOnlyWhereTheFileIs(unittest.TestCase): profile = {"source": "https://github.com/o/r", "source_commit": "e" * 40} text = sitesources._source_ref_markdown( profile, "Nesoid 2.5.0 libnes.so 0x455a4-0x45694 (setter), 0x8094-0x80ac") - self.assertEqual(text, "offset `0x455a4-0x45694` in `Nesoid 2.5.0 libnes.so`, " + self.assertEqual(text, "offset `0x455a4-0x45694` in `Nesoid 2.5.0 libnes.so` (setter), " "offset `0x8094-0x80ac` in `Nesoid 2.5.0 libnes.so`") def test_only_a_recorded_path_links(self): From 7cbfa538511e7b4052deba83fafaf2bd45aa94e2 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:01:27 +0100 Subject: [PATCH 19/25] fix: give commit_rules.py a --help --- scripts/commit_rules.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/commit_rules.py b/scripts/commit_rules.py index 356b3a871..1da6d7b1b 100644 --- a/scripts/commit_rules.py +++ b/scripts/commit_rules.py @@ -10,6 +10,7 @@ Usage as a hook: python scripts/commit_rules.py MESSAGE_FILE from __future__ import annotations +import argparse import re import sys from pathlib import Path @@ -51,10 +52,11 @@ def problems(message: str) -> list[str]: def main(argv: list[str]) -> int: - if len(argv) != 2: - print("usage: commit_rules.py MESSAGE_FILE", file=sys.stderr) - return 2 - found = problems(Path(argv[1]).read_text(encoding="utf-8", errors="replace")) + parser = argparse.ArgumentParser( + allow_abbrev=False, description="Check a commit message against the rules above") + parser.add_argument("message_file", type=Path, help="the message file a commit-msg hook receives") + args = parser.parse_args(argv[1:]) + found = problems(args.message_file.read_text(encoding="utf-8", errors="replace")) for problem in found: print(f"commit-msg: {problem}", file=sys.stderr) return 1 if found else 0 From 0c074f84585f35c79e001d49915539cab6e46d06 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:58:44 +0100 Subject: [PATCH 20/25] fix: run mypy and shellcheck in pr_checks validate.yml runs mypy against its baseline and checks that shellcheck is installed, and pr_checks ran neither. The guard test only knew the python, vermin and npm commands, so it missed both. A step can now pipe its output into a second command, as mypy into mypy-baseline filter. --- CONTRIBUTING.md | 2 ++ scripts/generate_readme.py | 4 ++-- scripts/pr_checks.py | 33 ++++++++++++++++++++++++++------- tests/test_contributing_text.py | 4 ++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff2a2baec..e85902a47 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,8 +82,10 @@ does, and names at the end each check that failed or could not run: ```bash python scripts/validate_pr.py --changed # the BIOS files the branch adds python scripts/validate_schemas.py --source-only # profiles and platform files +mypy | mypy-baseline filter --no-colors # no type error beyond mypy-baseline.txt python scripts/profile_sync.py --added --strict # each profile the branch adds vermin --no-tips -t=3.8- --violations install.py # install.py stays on Python 3.8 +shellcheck --version # the launcher test runs shellcheck python scripts/export_native.py --all --fetch --output-dir tmp/native # files the exporter tests compare with python scripts/refresh_data_dirs.py # data directories the pack-building tests read python scripts/generate_site.py # the pages the site tests read diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 572f3d1eb..4489173df 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -940,8 +940,8 @@ def contributing_text( .md for the site); *site_words* names the site from where the text is read. """ pip_line, floor_line = ci_install_lines() - width = max(len(step.command) for step in pr_checks.STEPS) - steps = "\n".join(f"{step.command:<{width}} # {step.why}" for step in pr_checks.STEPS) + width = max(len(pr_checks.shown(step)) for step in pr_checks.STEPS) + steps = "\n".join(f"{pr_checks.shown(step):<{width}} # {step.why}" for step in pr_checks.STEPS) return f"""{title} ## Add a BIOS file diff --git a/scripts/pr_checks.py b/scripts/pr_checks.py index 0acd4082c..a5eba4f9e 100644 --- a/scripts/pr_checks.py +++ b/scripts/pr_checks.py @@ -30,6 +30,9 @@ class Step: needs: str = "" provider: str = "" env: dict[str, str] = field(default_factory=dict) + # A second command that reads the first one's output, as a shell pipe + # would: mypy's report goes through mypy-baseline filter. + then: str = "" # The order of validate.yml: data fetched, site generated and built, then the @@ -37,9 +40,13 @@ class Step: STEPS: tuple[Step, ...] = ( Step("python scripts/validate_pr.py --changed", "the BIOS files the branch adds"), Step("python scripts/validate_schemas.py --source-only", "profiles and platform files"), + Step("mypy", "no type error beyond mypy-baseline.txt", needs="mypy", + provider="the pip line of CONTRIBUTING.md", then="mypy-baseline filter --no-colors"), Step("python scripts/profile_sync.py --added --strict", "each profile the branch adds"), Step("vermin --no-tips -t=3.8- --violations install.py", "install.py stays on Python 3.8", needs="vermin", provider="the pip line of CONTRIBUTING.md"), + Step("shellcheck --version", "the launcher test runs shellcheck", needs="shellcheck", + provider="the shellcheck package"), Step("python scripts/export_native.py --all --fetch --output-dir tmp/native", "files the exporter tests compare with"), Step("python scripts/refresh_data_dirs.py", "data directories the pack-building tests read"), @@ -57,24 +64,36 @@ STEPS: tuple[Step, ...] = ( ) -def argv(step: Step) -> list[str]: +def argv(command: str) -> list[str]: """The command, with `python` run by the interpreter running this script.""" - words = step.command.split() + words = command.split() return [sys.executable, *words[1:]] if words[0] == "python" else words +def shown(step: Step) -> str: + return f"{step.command} | {step.then}" if step.then else step.command + + +def execute(step: Step) -> int: + env = {**os.environ, **step.env} + if not step.then: + return subprocess.run(argv(step.command), cwd=ROOT, env=env).returncode + first = subprocess.run(argv(step.command), cwd=ROOT, env=env, stdout=subprocess.PIPE) + return subprocess.run(argv(step.then), cwd=ROOT, env=env, input=first.stdout).returncode + + def run(steps: tuple[Step, ...] = STEPS) -> int: failed: list[str] = [] not_run: list[str] = [] for step in steps: - print(f"\n== {step.command}", flush=True) + print(f"\n== {shown(step)}", flush=True) if step.needs and shutil.which(step.needs) is None: print(f"not run: {step.needs} is not installed ({step.provider})") - not_run.append(f"{step.command} ({step.needs}: {step.provider})") + not_run.append(f"{shown(step)} ({step.needs}: {step.provider})") continue - code = subprocess.run(argv(step), cwd=ROOT, env={**os.environ, **step.env}).returncode + code = execute(step) if code: - failed.append(f"{step.command} (exit {code})") + failed.append(f"{shown(step)} (exit {code})") print() for line in failed: print(f"FAILED {line}") @@ -90,7 +109,7 @@ def main() -> int: args = parser.parse_args() if args.list: for step in STEPS: - print(step.command) + print(shown(step)) return 0 return run() diff --git a/tests/test_contributing_text.py b/tests/test_contributing_text.py index c79ea1413..6c08e2f60 100644 --- a/tests/test_contributing_text.py +++ b/tests/test_contributing_text.py @@ -221,7 +221,7 @@ class OneCommandRunsTheChecks(unittest.TestCase): skipping one (refresh_data_dirs) failed the pack tests with no hint.""" CHECK = re.compile(r"(?:xargs -0 |run: |^\s*)((?:python3?\.?8? -m unittest|python scripts/\w+\.py" - r"|vermin|npm (?:ci|run build)))") + r"|vermin|npm (?:ci|run build)|mypy-baseline filter|mypy|shellcheck))") def test_every_check_ci_runs_is_a_step(self): import pr_checks @@ -230,7 +230,7 @@ class OneCommandRunsTheChecks(unittest.TestCase): ran = {m.group(1) for line in workflow.splitlines() if not line.strip().startswith("#") for m in [self.CHECK.search(line)] if m} self.assertGreaterEqual(len(ran), 10) - steps = [step.command for step in pr_checks.STEPS] + steps = [c for step in pr_checks.STEPS for c in (step.command, step.then) if c] for check in sorted(ran): with self.subTest(check=check): self.assertTrue(any(command.startswith(check) for command in steps)) From 4af681c754cbb89b1e10f3c7ace2b30a0282400e Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:43:05 +0100 Subject: [PATCH 21/25] fix: clear the XDG folders for the test suite The CI runner sets XDG_CONFIG_HOME to its own home, and the installer now reads it as RetroArch does. Tests that fake HOME then found the runner's RetroArch folders. The suite clears the three XDG variables it reads, and a test that exercises one sets it itself. --- tests/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index 1540b4305..bffe7a05f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,6 +17,12 @@ import tempfile from pathlib import Path os.environ.setdefault("RETROBIOS_OFFLINE", "1") +# The installer reads the XDG base directories as RetroArch and the +# emulators do. The CI runner sets XDG_CONFIG_HOME to its own home, so tests +# that fake HOME found the runner's folders instead of theirs. A test that +# exercises one of them sets it for itself. +for _name in ("XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME"): + os.environ.pop(_name, None) # A test never waits on, nor blocks, the measurements running on the host. os.environ.setdefault( "RETROBIOS_PROBE_LOCK", From 3e2587025968ef9489af9286f5ef2e937fecbb92 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:43:05 +0100 Subject: [PATCH 22/25] fix: type errors outside the mypy baseline Code moved to sitecollection.py and the triage progress added errors the baseline did not hold. A reused loop name, two lists without a type, returns without a value, and a regex match read without a test for None. Two baseline entries no longer occur and are removed. --- mypy-baseline.txt | 2 -- scripts/generate_readme.py | 2 ++ scripts/profile_sync.py | 11 ++++++----- scripts/sitecollection.py | 12 ++++++------ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 6aed090c2..7a2310dd3 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -405,8 +405,6 @@ scripts/generate_site.py:0: error: Expected iterable as variadic argument [misc scripts/generate_site.py:0: error: Generator has incompatible item type "int"; expected "bool" [misc] scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", target has type "str") [assignment] scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] -scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Optional[str]", variable has type "str") [assignment] -scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "Union[dict[Any, Any], str, None]", variable has type "str") [assignment] scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[Any, Any]", variable has type "int") [assignment] scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[str, set[str]]", variable has type "set[str]") [assignment] scripts/generate_site.py:0: error: Incompatible types in assignment (expression has type "dict[str, str]", variable has type "dict[str, tuple[str, dict[str, list[Any]]]]") [assignment] diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 4489173df..2d33474d9 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -927,6 +927,8 @@ def ci_install_lines() -> tuple[str, str]: text = (_REPO / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") main = re.search(r'^\s*run: (pip install pyyaml "jsonschema.*markdown.*)$', text, re.M) floor = re.search(r"^\s*run: (python3\.8 -m pip install .*)$", text, re.M) + if main is None or floor is None: + raise RuntimeError("validate.yml no longer installs the packages CONTRIBUTING quotes") return main.group(1), floor.group(1) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 16e47ffba..a4a34e3e7 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -5018,7 +5018,7 @@ def _print_triage( ) -> bool: """Profiles ranked by drift, worst first. False when Ctrl-C cut the scoring short: the ranking then covers the profiles scored.""" - ranked = [] + ranked: list[tuple] = [] sampled = 0 progress = _Progress(len(reports), "scored", args.offline or len(reports) < 2) complete = True @@ -5299,7 +5299,7 @@ def main() -> None: raise SystemExit(1) return - reports = [] + reports: list[ProfileReport] = [] quiet = args.offline or len(selected) < 2 if not quiet: print( @@ -5474,18 +5474,18 @@ def _emit( triage was cut short.""" if args.as_json: print(json.dumps([report_to_dict(r) for r in shown], indent=2)) - return + return None if args.markdown: target = Path(args.report_dir) / f"profile-sync-{date.today().isoformat()}.md" target.parent.mkdir(parents=True, exist_ok=True) target.write_text(format_markdown(shown), encoding="utf-8") print(f"written: {target}") - return + return None if args.fetch_plan: for report in shown: for url in fetch_plan(report): print(url) - return + return None if args.triage: return _print_triage(args, selected, shown) for report in shown: @@ -5494,6 +5494,7 @@ def _emit( print() if args.changed_only: _print_elided(reports, len(shown), frozenset(report.name for report in shown)) + return None def _print_elided( diff --git a/scripts/sitecollection.py b/scripts/sitecollection.py index 937bebe77..08ad817ba 100644 --- a/scripts/sitecollection.py +++ b/scripts/sitecollection.py @@ -128,11 +128,11 @@ def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dic # A variant answers its primary's name: scummvm.zip.a93f1c4b. base = parts[-1].rsplit(".", 1)[0] if console.startswith(".") else parts[-1] keys = sorted(readers.get(base, ())) - key = keys[0] if len(keys) == 1 else own + reader = keys[0] if len(keys) == 1 else own found = next((c for c, held in regrouped.items() if any(h.get("name") == base for h in held)), None) - target = found or (next((c for c in consoles if c == key), None) - or (engine(key) if key else base)) + target = found or (next((c for c in consoles if c == reader), None) + or (engine(reader) if reader else base)) if target != console: record_card_move(mfr, console, str(f.get("sha1", "")), mfr, target) regrouped.setdefault(target, []).append(f) @@ -154,9 +154,9 @@ def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dic *(f for f in others.pop(folder) if str(f.get("sha1", "")) not in held)] for mfr in GAME_DATA_TOPS: for console in out.get(mfr, {}): - key = profiles.get(console) and console - if key and engine(key) != console: - SECTION_LABELS[(mfr, console)] = engine(key) + named = console if console in profiles else None + if named and engine(named) != console: + SECTION_LABELS[(mfr, console)] = engine(named) return {mfr: consoles for mfr, consoles in out.items() if consoles} From 7db9fe14e19cb52cf7cf9d97d0bbc43c7719c70f Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:43:05 +0100 Subject: [PATCH 23/25] fix: let the export step fail as CI does validate.yml runs export_native with continue-on-error, and pr_checks failed the whole run on it. The step is now shown and does not fail the run, and a test keeps that list equal to the workflow's. --- scripts/pr_checks.py | 12 +++++++++--- tests/test_contributing_text.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/pr_checks.py b/scripts/pr_checks.py index a5eba4f9e..518cb158d 100644 --- a/scripts/pr_checks.py +++ b/scripts/pr_checks.py @@ -33,6 +33,9 @@ class Step: # A second command that reads the first one's output, as a shell pipe # would: mypy's report goes through mypy-baseline filter. then: str = "" + # validate.yml marks the step continue-on-error: its failure is shown + # and does not fail the run. + may_fail: bool = False # The order of validate.yml: data fetched, site generated and built, then the @@ -48,7 +51,7 @@ STEPS: tuple[Step, ...] = ( Step("shellcheck --version", "the launcher test runs shellcheck", needs="shellcheck", provider="the shellcheck package"), Step("python scripts/export_native.py --all --fetch --output-dir tmp/native", - "files the exporter tests compare with"), + "files the exporter tests compare with", may_fail=True), Step("python scripts/refresh_data_dirs.py", "data directories the pack-building tests read"), Step("python scripts/generate_site.py", "the pages the site tests read"), Step("npm ci --prefix website", "the site's packages, Node 24", @@ -84,6 +87,7 @@ def execute(step: Step) -> int: def run(steps: tuple[Step, ...] = STEPS) -> int: failed: list[str] = [] + allowed: list[str] = [] not_run: list[str] = [] for step in steps: print(f"\n== {shown(step)}", flush=True) @@ -93,13 +97,15 @@ def run(steps: tuple[Step, ...] = STEPS) -> int: continue code = execute(step) if code: - failed.append(f"{shown(step)} (exit {code})") + (allowed if step.may_fail else failed).append(f"{shown(step)} (exit {code})") print() for line in failed: print(f"FAILED {line}") + for line in allowed: + print(f"FAILED {line}, continue-on-error in CI as well") for line in not_run: print(f"NOT RUN {line}") - print(f"{len(steps) - len(failed) - len(not_run)}/{len(steps)} checks passed") + print(f"{len(steps) - len(failed) - len(allowed) - len(not_run)}/{len(steps)} checks passed") return 1 if failed else 0 diff --git a/tests/test_contributing_text.py b/tests/test_contributing_text.py index 6c08e2f60..12431f02d 100644 --- a/tests/test_contributing_text.py +++ b/tests/test_contributing_text.py @@ -235,6 +235,16 @@ class OneCommandRunsTheChecks(unittest.TestCase): with self.subTest(check=check): self.assertTrue(any(command.startswith(check) for command in steps)) + def test_a_step_ci_lets_fail_does_not_fail_the_run(self): + import pr_checks + + workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8") + tolerated = set(re.findall(r"continue-on-error: true\n\s*run: (\S+ \S+)", workflow)) + self.assertTrue(tolerated) + for step in pr_checks.STEPS: + with self.subTest(step=step.command): + self.assertEqual(step.may_fail, " ".join(step.command.split()[:2]) in tolerated) + def test_steps_keep_ci_order(self): import pr_checks From a9d4eecd13e16c74de9e587d72eedd31d12ae210 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:29:16 +0100 Subject: [PATCH 24/25] fix: keep main's site commits inside the gates Three site commits pushed to main fail checks CI runs: two prose semicolons, an untyped signature, and GoatCounter answers read as a dict without a test. GoatCounter's stats now go through one reader that refuses an answer that is not an object. --- mypy-baseline.txt | 3 --- scripts/generate_site.py | 6 +++--- scripts/site_insights.py | 16 ++++++++++++---- scripts/sitestarlight.py | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 7a2310dd3..71fc9d12f 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -899,9 +899,6 @@ scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "github_header scripts/scraper/targets/rocknix_targets_scraper.py:0: error: Name "standalone_emulators" already defined (possibly by an import) [no-redef] scripts/sect233r1.py:0: error: Incompatible types in assignment (expression has type "Optional[tuple[int, int]]", variable has type "tuple[int, int]") [assignment] scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] -scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] -scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] -scripts/site_insights.py:0: error: Item "list[Any]" of "Union[dict[Any, Any], list[Any]]" has no attribute "get" [union-attr] scripts/sitediagrams.py:0: error: "AST" has no attribute "value" [attr-defined] scripts/sitediagrams.py:0: error: Argument 1 to "lstrip" of "bytes" has incompatible type "str"; expected "Optional[Buffer]" [arg-type] scripts/sitediagrams.py:0: error: If x = b'abc' then f"{x}" or "{}".format(x) produces "b'abc'", not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). Otherwise, decode the bytes [str-bytes-safe] diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 851cefaf6..83b370da7 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -32,7 +32,7 @@ import urllib.parse import urllib.request import zipfile from collections import Counter -from collections.abc import Callable, Iterator +from collections.abc import Callable, Iterable, Iterator from datetime import datetime, timezone from pathlib import Path from typing import TypeVar @@ -7907,9 +7907,9 @@ def generate_emulators_index(profiles: dict) -> str: return "\n".join(lines) + "\n" -def _snippet_noun(names, firmware: int, fetched: int) -> str: +def _snippet_noun(names: Iterable[str], firmware: int, fetched: int) -> str: """What an emulator's files are called in its snippet. A Switch - emulator reads keys, and "eden keys" is what readers search for; Ship + emulator reads keys, and "eden keys" is what readers search for. Ship of Harkinian reads game ROMs, which are no BIOS.""" names = list(names) keys = sum("key" in name.casefold() for name in names) diff --git a/scripts/site_insights.py b/scripts/site_insights.py index f9edc0bfa..0bfb3701c 100644 --- a/scripts/site_insights.py +++ b/scripts/site_insights.py @@ -123,7 +123,15 @@ def fetch_queries(start: dt.date, end: dt.date) -> list[Query]: return rows -def fetch_goatcounter(start: dt.date, end: dt.date) -> dict[str, list[tuple[str, int]]]: +def _get_object(url: str, headers: dict[str, str] | None = None) -> dict: + """A JSON answer that must be an object: GoatCounter's stats endpoints.""" + answer = _get_json(url, headers) + if not isinstance(answer, dict): + raise ValueError(f"{url} answered {type(answer).__name__}, not an object") + return answer + + +def fetch_goatcounter(start: dt.date, end: dt.date) -> dict[str, list[tuple[str, int]] | int]: """Paths and events with their visit counts, and the referring domains.""" token = (CONFIG / "goatcounter-api-token.txt").read_text(encoding="utf-8").strip() auth = {"Authorization": f"Bearer {token}"} @@ -132,14 +140,14 @@ def fetch_goatcounter(start: dt.date, end: dt.date) -> dict[str, list[tuple[str, exclude: list[str] = [] while True: params = urllib.parse.urlencode({**span, "limit": 100, "exclude_paths": ",".join(exclude)}) - answer = _get_json(f"{GOATCOUNTER}/stats/hits?{params}", auth) + answer = _get_object(f"{GOATCOUNTER}/stats/hits?{params}", auth) for hit in answer.get("hits", []): hits.append((hit["path"] + (" [404]" if hit.get("title") == "404" else ""), int(hit["count"]))) exclude.append(str(hit["path_id"])) if not answer.get("more"): break - refs = _get_json(f"{GOATCOUNTER}/stats/toprefs?{urllib.parse.urlencode({**span, 'limit': 50})}", auth) - total = _get_json(f"{GOATCOUNTER}/stats/total?{urllib.parse.urlencode(span)}", auth) + refs = _get_object(f"{GOATCOUNTER}/stats/toprefs?{urllib.parse.urlencode({**span, 'limit': 50})}", auth) + total = _get_object(f"{GOATCOUNTER}/stats/total?{urllib.parse.urlencode(span)}", auth) # An action carries no referrer, and GoatCounter files it as a direct # visit: 592 of the first day's 1,145 "direct" were clicks. events = int(total.get("total_events", 0)) diff --git a/scripts/sitestarlight.py b/scripts/sitestarlight.py index 4f39e20ce..7bd717e95 100644 --- a/scripts/sitestarlight.py +++ b/scripts/sitestarlight.py @@ -330,7 +330,7 @@ OG_DIR = Path(__file__).resolve().parent.parent / "docs_assets" / "og" # An emulator page's card is that of the maker of the first system it runs, # by page path: "emulators/mgba.mdx" -> "nintendo". One card per emulator -# would be some 900 images in the repository; the maker's names the +# would be some 900 images in the repository. The maker's names the # consoles. Filled by generate_site.record_emulator_page. EMULATOR_CARDS: dict[str, str] = {} From 966ab201eacb0fa93cd98ffd0042cafebc04099f Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:48:46 +0100 Subject: [PATCH 25/25] fix: regenerate the launcher list after main Main changed install.py without the launcher index, so --add-app refused every launcher and nine tests failed on main itself. The demo script added a list without a type, and an archive-size test still built the data-tree refs without the needed flag the builder reads. --- install/launchers/index.json | 16 ++++++++-------- scripts/make_demo.py | 3 ++- tests/test_refresh_data_dirs.py | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/install/launchers/index.json b/install/launchers/index.json index f6dc104a6..117bcb065 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -27,8 +27,8 @@ { "dest": "roms/ports/retrobios/install.py", "repo_path": "install.py", - "size": 1112438, - "sha256": "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4", + "size": 1113711, + "sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1166065, - "sha256": "1c9e5737a0b32c85108f556c2799cf0ebb5ce52cbd2bf501bc24783fa3a04b9a" + "size": 1167338, + "sha256": "3ca24c5ba64437c1b2554238910b38463c160e74fcd59db675bac3fccd426d67" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1112438, - "sha256": "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4", + "size": 1113711, + "sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1112438, - "sha256": "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4", + "size": 1113711, + "sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184", "executable": false }, { diff --git a/scripts/make_demo.py b/scripts/make_demo.py index 570b1b1cf..2bc1dd289 100644 --- a/scripts/make_demo.py +++ b/scripts/make_demo.py @@ -89,7 +89,8 @@ VIDEO_FPS = 25 def game_bytes(name: str) -> bytes: """Bytes of a game no catalog names, the same on every run.""" seed = hashlib.sha256(name.encode("utf-8")).digest() - blocks, counter = [], 0 + blocks: list[bytes] = [] + counter = 0 while len(blocks) * 32 < GAME_BYTES: blocks.append(hashlib.sha256(seed + counter.to_bytes(4, "big")).digest()) counter += 1 diff --git a/tests/test_refresh_data_dirs.py b/tests/test_refresh_data_dirs.py index 604eba7ba..525c622fd 100644 --- a/tests/test_refresh_data_dirs.py +++ b/tests/test_refresh_data_dirs.py @@ -318,7 +318,7 @@ class TheManifestCarriesTheArchiveSize(unittest.TestCase): registry = {ref: {"source_type": "zip", "source_url": f"https://x/{ref}.zip", "local_cache": str(data / ref)} for ref in ("pak", "old")} members = [(str(data / ref / "a.bin"), f"{ref}/a.bin") for ref in ("pak", "old")] - refs = {f"{ref}/a.bin": (ref, ("s",), ref) for ref in ("pak", "old")} + refs = {f"{ref}/a.bin": (ref, ("s",), ref, True) for ref in ("pak", "old")} listed = {entry["ref"]: entry for entry in _manifest_data_directories(members, refs, registry, "")} self.assertEqual(listed["pak"]["archive_size"], 21_360_579)