# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-of-a-tool-manifest-loader-enforce-schema-invariants-relative-to-skill-injection.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-of-a-tool-manifest-loader-enforce-schema-invariants-relative-to-skill-injection.warrant.json
# Markdown SHA-256:   13c3d58daa7600c6145176641c72764b4591d96ff2c39c6600fe9c97ba09b526
# Code AST SHA-256:   9ad4baa2c662ee70661044f3da07d61c7ff6bc767b2206083d3652c5b050a7ab
# Verified Classes:   ManifestMergeInvariantError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

import hashlib
import json
from typing import Any


class ManifestMergeInvariantError(RuntimeError):
    """Raised when a manifest merge would widen a tool's effective schema."""


def _schema_hash(schema: dict[str, Any]) -> str:
    return hashlib.sha256(
        json.dumps(schema, sort_keys=True, separators=(",", ":")).encode()
    ).hexdigest()


def _required_fields(schema: dict[str, Any]) -> frozenset[str]:
    return frozenset(schema.get("required", []))


def _is_narrowing(prior: dict[str, Any], new: dict[str, Any]) -> bool:
    # Narrowing means: every field required by prior is still required,
    # and no new optional field is introduced that the prior lacked.
    prior_req = _required_fields(prior)
    new_req = _required_fields(new)
    if not prior_req.issubset(new_req):
        return False
    prior_props = set(prior.get("properties", {}))
    new_props = set(new.get("properties", {}))
    if not new_props.issubset(prior_props):
        return False
    return True


def merge_manifests(
    active: dict[str, dict[str, Any]],
    incoming: dict[str, dict[str, Any]],
    supersedes: dict[str, str] | None = None,
) -> dict[str, dict[str, Any]]:
    supersedes = supersedes or {}
    merged = dict(active)
    for name, new_schema in incoming.items():
        if name in active:
            prior = active[name]
            if supersedes.get(name) != _schema_hash(prior):
                raise ManifestMergeInvariantError(
                    f"tool {name!r} redefined without matching supersedes hash; "
                    f"expected {_schema_hash(prior)}"
                )
            if not _is_narrowing(prior, new_schema):
                raise ManifestMergeInvariantError(
                    f"tool {name!r} merge widens schema: "
                    f"prior required={sorted(_required_fields(prior))} "
                    f"new required={sorted(_required_fields(new_schema))}"
                )
        merged[name] = new_schema
    return merged
