feat: arbitrate a contested slot by who the pack serves

This commit is contained in:
Abdessamad Derraz committed 2026-09-05 08:53:47 +02:00
1 parent 1245bb6eed
commit c3d9e96d61
2 files changed
+117 -4

No files matched your search

+77 -4
View File
@@ -180,6 +180,67 @@ def find_conflicts(
]
# A frontend that never reads the bytes cannot be made unhappy by better ones.
CONTENT_CHECKING_MODES = frozenset({"md5", "sha1"})
@dataclass
class Decision:
"""Which claim a pack should honour at a contested destination, and why."""
conflict: Conflict
winner: Claim
reason: str
@property
def serves_both(self) -> bool:
"""Whether honouring the winner still satisfies the other layer."""
return self.reason == "existence"
def arbitrate(conflict: Conflict, mode: str, addressee: str = "platform") -> Decision:
"""Decide a contested destination for the pack being built.
A pack answers to whoever asked for it. A platform pack must leave the
frontend's own check green, because a user reading red concludes the pack
is broken; a pack built for one emulator answers to that emulator. The
other layer is served as well whenever the destination allows it.
In ``existence`` mode the frontend only looks for a path, so the emulator's
file satisfies both sides at once and there is nothing to trade away. In a
content-checking mode the two answers cannot share one path, and the pack's
addressee decides; the loss is reported rather than absorbed, because the
cause is an upstream declaration that needs fixing at its source.
"""
profile = conflict.profile_claims[0]
if addressee == "emulator":
return Decision(conflict, profile, "addressee")
if mode not in CONTENT_CHECKING_MODES:
return Decision(conflict, profile, "existence")
return Decision(conflict, conflict.platform_claim, "frontend_checks_content")
def format_decision(decision: Decision) -> str:
"""One line naming the contested slot, the winner and the ground for it."""
conflict = decision.conflict
if decision.serves_both:
return (
f"{conflict.destination}: serve {decision.winner.local_path} "
f"({', '.join(conflict.emulators) or 'profile'}); the frontend only "
"checks the path, so both are satisfied"
)
if decision.reason == "addressee":
return (
f"{conflict.destination}: serve {decision.winner.local_path}, "
"the pack answers to the emulator"
)
return (
f"{conflict.destination}: keep {decision.winner.local_path}, the frontend "
f"verifies content and would reject "
f"{conflict.profile_claims[0].local_path}; upstream declaration is wrong"
)
def format_conflict(conflict: Conflict) -> str:
"""One line per conflict, naming both answers and who gave them."""
emus = ", ".join(conflict.emulators) or "profile"
@@ -207,7 +268,11 @@ def main() -> int:
import argparse
import json
from common import list_registered_platforms, load_emulator_profiles
from common import (
list_registered_platforms,
load_emulator_profiles,
load_platform_config,
)
parser = argparse.ArgumentParser(
description="Report destinations where an emulator profile contradicts "
@@ -256,12 +321,20 @@ def main() -> int:
)
)
else:
fixable = 0
for platform, conflicts in found.items():
print(f"{platform}: {len(conflicts)} contradicted")
config = load_platform_config(platform, args.platforms_dir)
mode = config.get("verification_mode", "existence")
print(f"{platform}: {len(conflicts)} contradicted [{mode}]")
for conflict in conflicts:
print(f" {format_conflict(conflict)}")
decision = arbitrate(conflict, mode)
fixable += decision.serves_both
print(f" {format_decision(decision)}")
total = sum(len(c) for c in found.values())
print(f"\n{total} destinations where a profile contradicts what ships.")
print(
f"\n{total} contested destinations. {fixable} the pack can settle on "
f"its own, {total - fixable} rest on an upstream declaration."
)
return 1 if found else 0
+40
View File
@@ -198,6 +198,46 @@ class TestConflicts(unittest.TestCase):
self.assertIn("dolphin", line)
class TestArbitration(unittest.TestCase):
"""A pack answers to whoever asked for it, and serves both when it can."""
def _conflict(self):
platform = slots.Claim(
"platform", "GC/JAP/IPL.bin", "IPL.bin",
local_path="bios/Console/GC/USA/IPL.bin", status="md5_exact",
)
profile = slots.Claim(
"profile", "GC/JAP/IPL.bin", "IPL.bin", emulator="dolphin",
local_path="bios/Console/GC/JAP/IPL.bin", status="path_exact",
)
return slots.Conflict("GC/JAP/IPL.bin", platform, [profile])
def test_existence_mode_serves_both(self):
decision = slots.arbitrate(self._conflict(), "existence")
self.assertEqual(decision.winner.origin, "profile")
self.assertTrue(decision.serves_both)
def test_md5_mode_keeps_the_frontend_green(self):
decision = slots.arbitrate(self._conflict(), "md5")
self.assertEqual(decision.winner.origin, "platform")
self.assertFalse(decision.serves_both)
def test_sha1_mode_is_content_checking_too(self):
decision = slots.arbitrate(self._conflict(), "sha1")
self.assertEqual(decision.winner.origin, "platform")
def test_an_emulator_pack_answers_to_the_emulator(self):
decision = slots.arbitrate(self._conflict(), "md5", addressee="emulator")
self.assertEqual(decision.winner.origin, "profile")
self.assertFalse(decision.serves_both)
def test_the_reported_line_gives_the_ground_for_the_decision(self):
kept = slots.format_decision(slots.arbitrate(self._conflict(), "md5"))
self.assertIn("verifies content", kept)
served = slots.format_decision(slots.arbitrate(self._conflict(), "existence"))
self.assertIn("both are satisfied", served)
class TestProvenEvidence(unittest.TestCase):
"""What counts as proof that a claim is about content, not about a name."""