From 76b447e1699d449ac688dc17772142ef3591ef77 Mon Sep 17 00:00:00 2001 From: Pengyu Zhang Date: Sun, 23 Aug 2026 11:18:14 -0700 Subject: [PATCH] fix(discovery): resolve declaration site paths at manifest load Thirteen of the twenty-nine MCP rows name a declaration site and a launch form but carry no path of their own: the M-PIN entries reuse the sites the fourteen M-SITE rows enumerate, and M-SP-02 deliberately spans two of them. Left unresolved, those rows have no location at all, so a runner cannot write them and a scorer cannot match them -- for reasons that have nothing to do with the collector under test. On Linux that is the difference between 51 and 64 executable entries. Resolution happens once at load rather than in each consumer, so the runner, the scorer and anything else reading the manifest cannot disagree about where a declaration lives. It fills gaps only: a row stating its own path keeps it. --- Discovery/tests/manifest.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Discovery/tests/manifest.py b/Discovery/tests/manifest.py index b909f8d..dfa7652 100644 --- a/Discovery/tests/manifest.py +++ b/Discovery/tests/manifest.py @@ -161,11 +161,39 @@ def load(directory: str = MANIFEST_DIR) -> "Manifest": entries.append(_entry(row, source=name)) canaries = _load_side_file(directory, "canaries.toml", "canaries") sources = _load_side_file(directory, "sources.toml", "sources") + _resolve_sites(entries) manifest = Manifest(entries=entries, canaries=canaries, sources=sources) manifest.validate() return manifest +def _resolve_sites(entries: List[Entry]) -> None: + """Give every declaration a path, using the sites that already name one. + + The fourteen M-SITE rows enumerate the declaration sites and where each one + lives per OS. The pinned and special-case rows reuse those sites by name and + carry no path of their own, so without this they would be unwritable by the + runner and unmatchable by the scorer for reasons that have nothing to do + with the collector. + """ + known: Dict[str, Dict[str, str]] = {} + for entry in entries: + site = entry.declare.get("site") + path = entry.declare.get("path") + if site and isinstance(path, dict): + known.setdefault(str(site), {}).update({k: v for k, v in path.items() if v}) + + for entry in entries: + if not entry.declare or entry.declare.get("path"): + continue + names = entry.declare.get("sites") or [entry.declare.get("site")] + for name in names: + resolved = known.get(str(name)) + if resolved: + entry.declare["path"] = dict(resolved) + break + + def _load_side_file(directory: str, name: str, key: str) -> Any: with open(os.path.join(directory, name), "rb") as handle: return (tomllib.load(handle) or {}).get(key) or {}