"""Prove MuseHub still serves the exact paths the Scooling adapter requires. Why this exists --------------- Scooling's ``src/adapters/museHubRepoTransport.ts`` does not merely build request URLs — it *re-derives* the expected pathname and rejects any response whose path does not match, so a prefix change on the MuseHub side is a hard consumer failure rather than a silent fallback. The ``/api`` prefix is therefore part of a cross-repo contract, not an implementation detail of ``main.py``. This matters because Phase 9A-4 F7 was reviewed with a request to register ``overseer_provenance.router`` "near the other fixed-path routers" in ``main.py``. Those routers are mounted at the *root*, while the auto-discovery aggregate in ``musehub/api/routes/musehub/__init__.py`` is mounted with ``prefix="/api"``. Applying that request as written would expose ``/overseer-run-provenance/{run_ref}``, which Scooling refuses, and duplicate the already-working ``/api`` registration. Path templates are compared against ``app.openapi()``, which is public API and resolves include-prefixes correctly. Route-status probing is deliberately *not* used as the gate: MuseHub declares wildcard routes (``/{owner}/{repo_slug}`` and its ``/api`` twin), so almost any two-segment path returns a non-404 by shadowing. Only exact template equality distinguishes "endpoint exists" from "wildcard swallowed it". Contract status --------------- ``required`` Scooling depends on this path today. A missing template fails the check. ``pending`` The path belongs to an unmerged MuseHub branch and is tracked elsewhere. It is reported but does not fail, so this gate stays green on ``dev`` while still naming the outstanding consumer blocker. Flipping an entry to ``required`` after its branch merges is the mechanical proof that the Scooling-side blocker is genuinely cleared. Exit codes ---------- ``0`` Every ``required`` template is present. Emits ``ARTIFACT_SHA256`` over the resolved contract so L1 evidence changes whenever the contract or its satisfaction changes. ``1`` A ``required`` template is missing, or a ``pending`` entry lacks a tracking reference. ``2`` Harness failure — the app or its OpenAPI schema could not be produced. """ from __future__ import annotations import hashlib import json import logging import sys from dataclasses import dataclass REQUIRED = "required" PENDING = "pending" @dataclass(frozen=True) class ContractPath: """One path template a Scooling adapter depends on. Attributes: template: Exact OpenAPI path template, including the ``/api`` prefix. method: HTTP method Scooling issues. consumer: ``path:line`` in the Scooling repo that pins this template. status: ``required`` or ``pending``. tracking: Branch, proposal, or issue reference; mandatory when pending. """ template: str method: str consumer: str status: str tracking: str = "" # Derived from Scooling src/adapters/museHubRepoTransport.ts, which is the only # Scooling adapter targeting MuseHub (`baseUrl`); every `/api/v1/...` adapter # targets the Knowtation hub (`hubUrl`) and is out of scope here. CONTRACT: tuple[ContractPath, ...] = ( ContractPath( template="/api/repos/{repo_id}", method="get", consumer="src/adapters/museHubRepoTransport.ts:274,290", status=REQUIRED, ), ContractPath( template="/api/repos/{repo_id}/commits", method="get", consumer="src/adapters/museHubRepoTransport.ts:229,257", status=REQUIRED, ), ContractPath( template="/api/overseer-run-provenance/{run_ref}", method="get", consumer="src/adapters/museHubRepoTransport.ts:466,481", status=PENDING, tracking="branch feat/9a-4-f7-overseer-provenance (proposal #9) not yet merged to dev", ), ) def main() -> int: logging.disable(logging.CRITICAL) try: from musehub.main import app schema = app.openapi() except Exception as exc: # noqa: BLE001 — harness reports, never raises print(f"FAIL: cannot produce OpenAPI schema: {exc!r}", file=sys.stderr) print("Run tools/verify/check_app_imports.py for the traceback.", file=sys.stderr) return 2 paths: dict[str, dict[str, object]] = schema.get("paths", {}) findings: list[str] = [] resolved: list[str] = [] for entry in CONTRACT: if entry.status not in {REQUIRED, PENDING}: findings.append(f"{entry.template}: unknown status {entry.status!r}") continue if entry.status == PENDING and not entry.tracking.strip(): findings.append(f"{entry.template}: pending without a tracking reference") continue operations = paths.get(entry.template) present = operations is not None and entry.method in operations resolved.append(f"{entry.status} {entry.method.upper()} {entry.template} present={present}") if present: if entry.status == PENDING: print( f"NOTE: {entry.template} is now served but still marked pending " f"({entry.tracking}) — flip it to required once merged to dev" ) continue if entry.status == REQUIRED: near = sorted(p for p in paths if entry.template.split("{")[0] in p)[:4] findings.append( f"{entry.method.upper()} {entry.template} is not served — " f"Scooling {entry.consumer} rejects any other path. " f"Nearest served: {near or 'none'}" ) else: print(f"PENDING: {entry.method.upper()} {entry.template} — {entry.tracking}") if findings: print("FAIL: consumer contract violations", file=sys.stderr) for finding in findings: print(f" - {finding}", file=sys.stderr) return 1 digest = hashlib.sha256( json.dumps(sorted(resolved), sort_keys=True).encode("utf-8") ).hexdigest() required_count = sum(1 for entry in CONTRACT if entry.status == REQUIRED) print( f"OK: {required_count}/{len(CONTRACT)} contract path(s) required and served; " f"{len(paths)} OpenAPI paths total" ) print(f"ARTIFACT_SHA256={digest}") return 0 if __name__ == "__main__": sys.exit(main())