# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-manifest-level-skill-shadowing-during-dynamic-tool-registry-reload.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-manifest-level-skill-shadowing-during-dynamic-tool-registry-reload.warrant.json
# Markdown SHA-256:   ed7dd49edc6c8fc3d3a3db5be564811445b75f1e417663fb944ac8b62182eb1a
# Code AST SHA-256:   06f5d2d85c2e4fbedcf5903669c8c1bc3f9c12b61e133e35403e8fb52a27168b
# Verified Classes:   SkillShadowingError, Binding
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass
from typing import Mapping, Optional


class SkillShadowingError(RuntimeError):
    """Raised when a registry reload would silently rebind a trusted skill."""
    pass


@dataclass(frozen=True)
class Binding:
    identifier: str
    digest: str            # implementation hash or signing identity
    version: tuple         # parsed semver, NOT a raw string
    promotion: Optional[str] = None   # signed promotion record, if any


def _normalize(identifier: str) -> str:
    return identifier.lower().replace("_", "").replace("-", "").replace(".", "")


def _digest(payload: bytes) -> str:
    return hashlib.sha256(payload).hexdigest()


def validate_reload(
    before: Mapping[str, Binding],
    after: Mapping[str, Binding],
) -> None:
    """
    Enforce binding monotonicity across a registry reload.
    Raises SkillShadowingError on the first violation; caller must not commit.
    """
    # 1. Namespace collision check across ALL identifiers in the new table.
    seen: dict[str, str] = {}
    for ident in after:
        norm = _normalize(ident)
        if norm in seen and seen[norm] != ident:
            raise SkillShadowingError(
                f"namespace collision: {seen[norm]!r} and {ident!r} "
                f"normalize to {norm!r}"
            )
        seen[norm] = ident

    # 2. Per-identifier monotonicity and downgrade checks.
    for ident, new_b in after.items():
        old_b = before.get(ident)
        if old_b is None:
            continue  # new skill: benign expansion
        if new_b.digest == old_b.digest:
            continue  # metadata refresh: benign
        if new_b.version < old_b.version:
            raise SkillShadowingError(
                f"version downgrade for {ident!r}: "
                f"{old_b.version} -> {new_b.version}"
            )
        if new_b.promotion is None:
            raise SkillShadowingError(
                f"unpromoted rebind of {ident!r}: "
                f"digest {old_b.digest[:12]} -> {new_b.digest[:12]} "
                f"without signed promotion record"
            )
        # promotion present: legitimate upgrade, allowed.
    return None


def commit_reload(before, after, commit_fn):
    try:
        validate_reload(before, after)
    except SkillShadowingError:
        # Retain pre-reload table; caller surfaces failure to agent loop.
        raise
    commit_fn(after)
