feat: link system ids to their files' section

A reviewed SYSTEM_PARENTS table leads variants (WonderSwan Color, Neo Geo MVS, MSX2) to the section holding their files, an Arcade heading that reads the name exactly is accepted, and system_search_aliases() gives the platforms' native ids for search. 260 of 636 ids linked before, 338 after.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 09:06:55 +02:00
1 parent d83a086355
commit fad947e9d0
3 files changed
+484 -7

No files matched your search

+190 -6
View File
@@ -223,14 +223,20 @@ def emulator_card_page(emulator: str, anchor: str) -> str:
def system_section(sys_id: str) -> tuple[str, str] | None:
"""The console section a system id is named after, as (manufacturer
slug, console): its own section, else the one section whose heading
reads system_name(). The folder a system's files sit in is no answer:
Emerson's Arcadia 2001 led to the Saturn and Sega's NAOMI to the
Dreamcast."""
"""The console section a system id leads to, as (manufacturer slug,
console): its own section, else the parent SYSTEM_PARENTS names, else
the one section whose heading reads system_name(). The folder a
system's files sit in is no answer by itself: Emerson's Arcadia 2001
led to the Saturn and Sega's NAOMI to the Dreamcast.
A heading under Arcade counts when it reads the name exactly (Namco
System 246). One under Other never does, its headings name programs."""
own = _own_section(sys_id)
if own is not None:
return _manufacturer_slug(own[0]), own[1]
parent = SYSTEM_PARENTS.get(sys_id)
if parent is not None and parent in _section_pairs:
return _manufacturer_slug(parent[0]), parent[1]
try:
name = system_name(sys_id)
except KeyError:
@@ -239,7 +245,9 @@ def system_section(sys_id: str) -> tuple[str, str] | None:
if len(places) != 1:
return None
mfr, console = next(iter(places))
if mfr in _PROGRAM_TOPS:
if mfr in _NAMELESS_TOPS or (
mfr in _PROGRAM_TOPS and console.casefold() in _program_names
):
return None
return _manufacturer_slug(mfr), console
@@ -359,7 +367,12 @@ def _by_mode(value: str | dict, mode: str) -> str:
# Folders that file copies by the emulator or board that loads them. Their
# headings name a program, not a system.
_PROGRAM_TOPS = frozenset({"Other", "Arcade"})
# Of those, the top whose headings never name a system, even read whole.
# An Arcade heading can (Namco System 246), an Other one cannot.
_NAMELESS_TOPS = frozenset({"Other"})
# Every (manufacturer, console) section the system pages carry.
_section_pairs: set[tuple[str, str]] = set()
# Normalized id -> the console sections spelled that way.
_console_sections: dict[str, set[tuple[str, str]]] = {}
# A console section by the name its heading reads (Bandai WonderSwan).
@@ -402,6 +415,8 @@ def set_console_sections(
_console_sections.clear()
_sections_by_name.clear()
_console_anchors.clear()
_section_pairs.clear()
_section_pairs.update(pairs)
by_maker: dict[str, list[str]] = {}
for mfr, console in pairs:
by_maker.setdefault(mfr, []).append(console)
@@ -927,3 +942,172 @@ SYSTEM_NAMES: dict[str, str] = {
"zx-spectrum": "Sinclair ZX Spectrum",
"zx-spectrum-128": "Sinclair ZX Spectrum 128",
}
# A system id with no section of its own -> the (manufacturer, console)
# section holding its files: a model, a regional name or an add-on of the
# machine that section covers, or a game whose data that section files.
# Reviewed entry by entry against the files the profiles and platforms
# declare for the id, never decided by a vote of the folders those files
# sit in. A section that is not the machine stays out of this table even
# when it holds a copy: NAOMI's naomi2.zip filed with the Dreamcast, the
# PET's ROMs in VICE's tree under the C128, the Arcadia's under the Saturn.
SYSTEM_PARENTS: dict[str, tuple[str, str]] = {
# Nintendo. The twin folders (DS and Nintendo DS, 64DD and Nintendo
# 64DD) gave each name two headings and no link.
"64dd": ("Nintendo", "Nintendo 64DD"),
"nintendo-64dd": ("Nintendo", "Nintendo 64DD"),
"nintendo-ds": ("Nintendo", "Nintendo DS"),
"nintendo-dsi": ("Nintendo", "Nintendo DS"),
"famicom": ("Nintendo", "NES"),
"super-famicom": ("Nintendo", "SNES"),
# Bandai: the Color boot ROMs, and those of Benesse's WonderSwan-based
# Pocket Challenge V2, sit with the WonderSwan.
"wonderswan-color": ("Bandai", "WonderSwan"),
"bandai-wswanc": ("Bandai", "WonderSwan"),
"pocket-challenge-v2": ("Bandai", "WonderSwan"),
# SNK
"ngpc": ("SNK", "Neo Geo Pocket"),
"snk-ngpc": ("SNK", "Neo Geo Pocket"),
"neo-geo-pocket-color": ("SNK", "Neo Geo Pocket"),
"snk-neogeo-aes": ("SNK", "Neo Geo"),
"snk-neogeo-mvs": ("SNK", "Neo Geo"),
# Sega. The VMU is the Dreamcast's memory card, its images sit there.
# Sega's arcade section holds the OutRun board ROMs Cannonball reads.
"sega-genesis": ("Sega", "Mega Drive"),
"mega-cd-32x": ("Sega", "Mega CD"),
"sega-vmu": ("Sega", "Dreamcast"),
"sega-outrun": ("Sega", "Arcade"),
# NEC and Pioneer: both LaserActive packs (LD-ROM2 and Mega LD) file
# their BIOS under the LaserActive.
"nec-supergrafx": ("NEC", "PC Engine"),
"supergrafx": ("NEC", "PC Engine"),
"supergrafx-cd": ("NEC", "PC Engine CD"),
"pc-engine-ld": ("Pioneer", "LaserActive"),
"mega-ld": ("Pioneer", "LaserActive"),
# Microsoft MSX generations
"microsoft-msx2": ("Microsoft", "MSX"),
"msx2": ("Microsoft", "MSX"),
"msx2plus": ("Microsoft", "MSX"),
"msx2+": ("Microsoft", "MSX"),
"msxturboR": ("Microsoft", "MSX"),
"msxturbor": ("Microsoft", "MSX"),
# Sinclair: 128-0.rom and 128-1.rom, and Timex's tc2048.rom.
"zx-spectrum-128": ("Sinclair", "ZX Spectrum"),
"timex-tc2048": ("Sinclair", "ZX Spectrum"),
# Acorn: os12.rom and basic2.rom for the Model B and B+, and the
# Master Compact's bbcmc.zip.
"bbc-micro-b": ("Acorn", "BBC Micro"),
"bbc-micro-bplus": ("Acorn", "BBC Micro"),
"bbc-master-compact": ("Acorn", "BBC Micro"),
# Atari: falcon.img, ste.img and tt.img sit with the ST TOS images.
"atari-ste": ("Atari", "ST"),
"atari-tt": ("Atari", "ST"),
"atari-falcon": ("Atari", "ST"),
# Home computers filed under their family
"apple-iie": ("Apple", "Apple II"),
"apf-m1000": ("APF", "Imagination Machine"),
"apfm1000": ("APF", "Imagination Machine"),
"bbk-longman-4980": ("BBK", "A4980"),
"bbk-longman-4988": ("BBK", "A4988"),
"coco3": ("Tandy", "CoCo"),
"commodore-cdtv": ("Commodore", "Amiga"),
"dragon32": ("Dragon", "Dragon"),
"dragon64": ("Dragon", "Dragon"),
"enterprise-64": ("Enterprise", "64-128"),
"enterprise-128": ("Enterprise", "64-128"),
"fm77av": ("Fujitsu", "FM-7"),
"gamepock": ("Epoch", "Game Pocket"),
"ibm-pcjr": ("IBM", "PC"),
"ibm-xt": ("IBM", "PC"),
"marty": ("Fujitsu", "FM Towns"),
"mz700": ("Sharp", "MZ"),
"mz80k": ("Sharp", "MZ"),
"oricatmos": ("Oric", "Oric"),
"palm-os": ("Palm", "Palm"),
"philips-vg5k": ("Philips", "VG5000"),
"vg5k": ("Philips", "VG5000"),
"videobrain": ("VideoBrain", "Family Computer"),
# Files kept at the maker's root, so each file is a heading of its own
# on a page that is the machine's: the first one leads there.
"galaksija": ("Galaksija", "CHRGEN.BIN"),
"vircon32": ("Vircon", "Vircon32Bios.v32"),
# Games, filed with the game whose folder holds their data
"dinothawr": ("Dinothawr", "Dinothawr"),
"doom": ("Id Software", "Doom"),
"ctf": ("Id Software", "Quake II"),
"teamarena": ("Id Software", "Quake III Arena"),
"enemyterritory": ("Id Software", "Wolfenstein Enemy Territory"),
"gearbox": ("Valve", "Half-Life"),
"dmc": ("Valve", "Half-Life"),
"ricochet": ("Valve", "Half-Life"),
"tfc": ("Valve", "Half-Life"),
"mohaa": ("Electronic Arts", "Medal of Honor Allied Assault"),
"mohaa-breakthrough": ("Electronic Arts", "Medal of Honor Allied Assault"),
"mohaa-spearhead": ("Electronic Arts", "Medal of Honor Allied Assault"),
"ur-quan-masters": ("Toys for Bob", "The Ur-Quan Masters"),
"star-control-2": ("Toys for Bob", "The Ur-Quan Masters"),
# The collection's general arcade section, where naomi.zip, neogeo.zip
# and the other board sets sit.
"arcade": ("Arcade", "Arcade"),
}
def system_ids(profiles: dict[str, dict], platforms: list[dict]) -> set[str]:
"""Every system id a page can name: the platforms' and the profiles'."""
ids = {str(sys_id) for config in platforms for sys_id in config.get("systems", {})}
for profile in profiles.values():
ids.update(str(sys_id) for sys_id in profile.get("systems") or [])
for entry in profile.get("files") or []:
if not isinstance(entry, dict):
continue
if entry.get("system"):
ids.add(str(entry["system"]))
ids.update(str(sys_id) for sys_id in entry.get("systems") or [])
return ids
def link_coverage(ids: set[str]) -> tuple[int, int]:
"""How many of ``ids`` lead to a section, and how many there are."""
return sum(1 for sys_id in ids if system_section(sys_id)), len(ids)
# A system id -> the names the platforms' own files give it, built once.
_search_aliases: dict[str, list[str]] = {}
_search_aliases_built = [False]
# A native name as a person types it: no space, no comma. RetroArch's
# native names are No-Intro titles ("Sony - PlayStation") and stay out.
_ALIAS_SHAPE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.+-]*")
def system_search_aliases(sys_id: str) -> list[str]:
"""The native system ids the platform YAMLs file ``sys_id`` under
(psx, dreamcast, gba), sorted, for search. Read from each platform's
own entries by common.platform_system_names, never typed by hand."""
if not _search_aliases_built[0]:
from common import (
list_registered_platforms,
load_platform_config,
platform_system_names,
)
platforms_dir = str(_REPO_ROOT / "platforms")
index: dict[str, set[str]] = {}
for platform in list_registered_platforms(platforms_dir, include_archived=True):
config = load_platform_config(platform, platforms_dir)
names = platform_system_names(config, list(config.get("systems", {})))
for owner, spelled in names.items():
index.setdefault(owner, set()).update(
name for name in spelled if _ALIAS_SHAPE.fullmatch(name)
)
# Search ignores case: one spelling per word, the lower-case one
# when a platform writes it (psx, not PSX).
_search_aliases.clear()
for owner, names in index.items():
spellings: dict[str, str] = {}
for name in sorted(names, key=lambda n: (n != n.lower(), n)):
spellings.setdefault(name.casefold(), name)
if spellings:
_search_aliases[owner] = sorted(spellings.values(), key=str.casefold)
_search_aliases_built[0] = True
return list(_search_aliases.get(str(sys_id), []))
+293
View File
@@ -0,0 +1,293 @@
"""Every system id with a section leads to it (round 3, package P0).
260 of 636 system ids linked: a variant (WonderSwan Color, Neo Geo MVS)
never matched a heading, twin folders (DS and Nintendo DS) made the exact
name ambiguous, and every Arcade heading was refused. siterender now reads
a reviewed SYSTEM_PARENTS table after the id's own section, and accepts an
Arcade heading that reads the name exactly. An id whose files the
collection holds and that stays plain is named below, with the reason.
Reads the repository: skips when the database is absent.
"""
from __future__ import annotations
import json
import os
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import siterender # noqa: E402
# Ids whose files the collection holds and that lead to no section.
UNLINKED: dict[str, str] = {
# Files only under a program's folder (Other/<emulator>, Arcade/<core>,
# Game Engines/<engine>, a top named after the program). The heading
# names the program, not the system.
"atari-system1": "Other > fbneo",
"bbc-master-128": "Other > beebem",
"bbc-master-et": "Other > beebem",
"capcom-cps3": "Arcade > FBNeo",
"cave-story": "Other > NXEngine",
"cps3": "Arcade > Arcade holds one CPS-3 set among every board's",
"commanderx16": "Arcade > Arcade",
"dark-forces-ii": "Game Engines > OpenJKDF2",
"deco-cassette": "Arcade > FBNeo",
"dos": "DOSBox, Roland and Other folders",
"fpinball": "Other > future-pinball-fploader",
"fruitmachine": "Other > mfme",
"igs-pgm": "Arcade > FBNeo",
"konami-bubsys": "Arcade > FBNeo",
"microsoft-xbox-360": "Other > xenia-edge",
"midway-mcr": "Arcade > FBNeo",
"mysteries-of-the-sith": "Game Engines > OpenJKDF2",
"mz2000": "Arcade > Arcade, not the Sharp MZ section",
"mz2500": "Arcade > Arcade, not the Sharp MZ section",
"mz800": "Arcade > Arcade, not the Sharp MZ section",
"namco-na1": "Arcade > FBNeo",
"namco-nb1": "Arcade > FBNeo",
"nmk16": "Arcade > FBNeo",
"omicron": "Other > omicron",
"pegasus": "Arcade > MAME",
"rpgmaker": "RPG Maker > easyrpg",
"scummvm": "Other > ScummVM",
"sdlpal": "Other > sdlpal",
"sega-system16b": "Arcade > FBNeo",
"sinclair-zx-spectrum-next": "Other > cspect",
"smbx": "Game Engines > TheXTech",
"solarus": "Game Engines > Solarus",
"sunplus-spmp8000": "Other > spmp8000emu",
"super-kaneko-nova": "Arcade > FBNeo",
"taito-cchip": "Arcade > FBNeo",
"thomson-mo6": "Other > Clock Signal",
"tomb-raider-1": "Game Engines > TRX",
"vgmplay": "Arcade > Arcade and MAME",
"vm-labs-nuon": "Other > nuance",
"vpinball": "Arcade > PinMAME",
"xrick": "Other > xrick",
"zc210": "ZC > ZC210, the emulator's folder",
# Files filed with another machine: a link would name the wrong one.
"atom": "atom.zip is filed with the BBC Micro",
"commodore-c64-supercpu": "VICE's tree under the C128",
"commodore-cbm-ii": "VICE's tree under the C128",
"commodore-pet": "VICE's tree under the C128",
"emerson-arcadia-2001": "ar_bios.zip is filed with the Saturn",
"naomi2": "naomi2.zip is filed with the Dreamcast",
"neogeo64": "the platform declares the Neo Geo's neogeo.zip",
"sega-dreamcast-arcade": "naomi.zip and awbios.zip, Arcade and Dreamcast",
"spectravideo": "the SVI ROMs are filed with the MSX",
"vvvvvv": "SDL's controller database",
"ym2608": "an FBNeo sound ROM, also filed with the NES",
# Files of distinct machines or games, one section each.
"catacomb-adventure": "three games, three sections",
"sinclair-zx80-81": "ZX 80 and ZX 81 sections",
}
def _accepts(fe: dict, local: str | None, status: str) -> bool:
"""A resolution that proves the entry's content or its full path."""
from common import resolution_is_hash_exact
if not local:
return False
if resolution_is_hash_exact(status) or status == "path_exact":
return True
path = fe.get("path")
return bool(path) and local.endswith("/" + str(path))
@unittest.skipUnless((REPO_ROOT / "database.json").exists(), "database.json is not built")
class SystemLinks(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
previous = os.getcwd()
os.chdir(REPO_ROOT)
try:
from common import (
list_registered_platforms,
load_emulator_profiles,
load_platform_config,
resolve_local_file,
)
from generate_site import _copy_places, _group_by_manufacturer
with open("database.json", encoding="utf-8") as handle:
db = json.load(handle)
cls.manufacturers = _group_by_manufacturer(db)
profiles = load_emulator_profiles("emulators", skip_aliases=False)
siterender.set_console_sections(
cls.manufacturers, siterender.program_names(profiles))
platforms = [
load_platform_config(name, "platforms")
for name in list_registered_platforms("platforms", include_archived=True)
]
cls.ids = siterender.system_ids(profiles, platforms)
entries: dict[str, list[dict]] = {}
for config in platforms:
for sys_id, system in config.get("systems", {}).items():
entries.setdefault(sys_id, []).extend(system.get("files") or [])
for profile in profiles.values():
systems = [str(s) for s in profile.get("systems") or []]
for fe in profile.get("files") or []:
if not isinstance(fe, dict):
continue
owners = {str(fe["system"])} if fe.get("system") else set()
owners.update(str(s) for s in fe.get("systems") or [])
if not owners and len(systems) == 1:
owners = set(systems)
for owner in owners:
entries.setdefault(owner, []).append(fe)
places = _copy_places(db)
by_path = {entry.get("path"): sha1 for sha1, entry in db["files"].items()}
# Id -> the (manufacturer, console) sections holding its files.
cls.held: dict[str, set[tuple[str, str]]] = {}
for sys_id in cls.ids:
sections: set[tuple[str, str]] = set()
for fe in entries.get(sys_id, []):
local, status = resolve_local_file(fe, db)
if not _accepts(fe, local, status):
continue
sha1 = by_path.get(local)
if sha1 is not None:
sections.update((m, c) for m, c, _ in places.get(sha1, set()))
if sections:
cls.held[sys_id] = sections
finally:
os.chdir(previous)
def test_every_id_with_files_links(self) -> None:
linked, total = siterender.link_coverage(self.ids)
with_files = set(self.held)
print(f"\n {linked} of {total} system ids linked, "
f"{len(with_files - set(UNLINKED))} of {len(with_files)} with files")
plain = sorted(
sys_id for sys_id in with_files - set(UNLINKED)
if siterender.system_section(sys_id) is None
)
self.assertEqual(plain, [], "link these or name them in UNLINKED with the reason")
def test_unlinked_list_is_current(self) -> None:
stale = sorted(
sys_id for sys_id in UNLINKED
if sys_id not in self.held or siterender.system_section(sys_id) is not None
)
self.assertEqual(stale, [])
def test_a_matching_heading_is_linked(self) -> None:
"""An id whose name reads one heading, outside Other, leads there."""
by_heading: dict[str, set[tuple[str, str]]] = {}
for mfr, consoles in self.manufacturers.items():
for console in consoles:
for name in {siterender._heading_name(mfr, console), console}:
by_heading.setdefault(name.casefold(), set()).add((mfr, console))
missed = []
for sys_id in sorted(self.ids):
places = by_heading.get(siterender.system_name(sys_id).casefold(), set())
if len(places) != 1:
continue
mfr, console = next(iter(places))
if mfr == "Other" or (
mfr == "Arcade" and console.casefold() in siterender._program_names
):
continue
if siterender.system_section(sys_id) is None:
missed.append(sys_id)
self.assertEqual(missed, [])
def test_parents_are_sections(self) -> None:
sections = {
(mfr, console)
for mfr, consoles in self.manufacturers.items() for console in consoles
}
self.assertEqual(
sorted(k for k, v in siterender.SYSTEM_PARENTS.items() if v not in sections), [])
def test_parents_hold_the_files(self) -> None:
"""A parent holds a file of the id, or of an id of the same name.
An entry its own section shadows is never read and is not judged."""
by_name: dict[str, set[tuple[str, str]]] = {}
for sys_id, sections in self.held.items():
by_name.setdefault(siterender.system_name(sys_id), set()).update(sections)
wrong = []
for sys_id, parent in sorted(siterender.SYSTEM_PARENTS.items()):
if siterender._own_section(sys_id) is not None:
continue
held = by_name.get(siterender.system_name(sys_id))
if held is not None and parent not in held:
wrong.append((sys_id, parent))
self.assertEqual(wrong, [])
def test_variants_lead_to_their_family(self) -> None:
expect = {
"wonderswan-color": ("bandai", "WonderSwan"),
"bandai-wswanc": ("bandai", "WonderSwan"),
"ngpc": ("snk", "Neo Geo Pocket"),
"snk-neogeo-mvs": ("snk", "Neo Geo"),
"snk-neogeo-aes": ("snk", "Neo Geo"),
"nintendo-ds": ("nintendo", "Nintendo DS"),
"nintendo-64dd": ("nintendo", "Nintendo 64DD"),
"zx-spectrum-128": ("sinclair", "ZX Spectrum"),
"pc-engine-ld": ("pioneer", "LaserActive"),
}
for sys_id, section in expect.items():
with self.subTest(sys_id=sys_id):
self.assertEqual(siterender.system_section(sys_id), section)
def test_arcade_heading_by_exact_name(self) -> None:
self.assertEqual(
siterender.system_section("namco-system246"), ("arcade", "Namco System 246"))
# No heading reads NAOMI: its sets are cards of the general section.
self.assertIsNone(siterender.system_section("sega-naomi"))
# Never a folder of copies a core loads.
self.assertIsNone(siterender.system_section("igs-pgm"))
def test_folder_votes_stay_out(self) -> None:
for sys_id in ("emerson-arcadia-2001", "naomi2", "commodore-pet"):
with self.subTest(sys_id=sys_id):
self.assertIsNone(siterender.system_section(sys_id))
self.assertNotIn(sys_id, siterender.SYSTEM_PARENTS)
def test_emulator_rows_link_the_variant(self) -> None:
from generate_site import _named_system_link
self.assertEqual(
_named_system_link("wonderswan-color", "../"),
"[Bandai WonderSwan Color](../systems/bandai.md#wonderswan)")
class SearchAliases(unittest.TestCase):
def test_platform_names(self) -> None:
self.assertIn("psx", siterender.system_search_aliases("sony-playstation"))
self.assertIn("dc", siterender.system_search_aliases("sega-dreamcast"))
self.assertIn("gba", siterender.system_search_aliases("nintendo-gba"))
def test_one_typed_word_per_name(self) -> None:
siterender.system_search_aliases("sony-playstation")
for sys_id, names in siterender._search_aliases.items():
with self.subTest(sys_id=sys_id):
self.assertNotIn(sys_id, names)
self.assertEqual(len({n.casefold() for n in names}), len(names))
for name in names:
self.assertNotRegex(name, r"[\s,]")
def test_unknown_id(self) -> None:
self.assertEqual(siterender.system_search_aliases("no-such-system"), [])
class BuildCount(unittest.TestCase):
def test_generator_prints_the_count(self) -> None:
source = (REPO_ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8")
self.assertIn("link_coverage(", source)
self.assertIn("system ids linked", source)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -54,7 +54,7 @@ Every module in `tests/`, by the area it guards.
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_offline`, `test_install_selection` |
| Handheld port | stubbed PortMaster dialogs, fake installer | `test_port` |
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_p0`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
## Test architecture