Source: src/mcss/mechanism_pilot/support_redesign_contracts.py · Plain text

src/mcss/mechanism_pilot/support_redesign_contracts.py

"""Fail-closed support-redesign roles, immutable methods and one-shot holdout access.

These guards enforce their loader APIs, not arbitrary filesystem reads. Oracle geometry
must remain a diagnostic and can never obtain a final-holdout session.
"""

from __future__ import annotations

import hashlib
import json
import os
from collections.abc import Callable
from dataclasses import asdict, dataclass
from pathlib import Path

from mcss.dynamic.types import SealedScene, hash_scene_state, hash_value
from mcss.evaluation.sealed_queries import _validate_sealed

DEPLOYABLE_METHODS = ("R0", "R1", "R2", "R3", "R12", "R123")
ORACLE_METHODS = ("ORACLE_VOLUME", "ORACLE_SUPPORT", "ORACLE_VOLUME_SUPPORT")
EXPOSED_SCENES = (
    "ai_004_001",
    "ai_006_001",
    "ai_007_002",
    "ai_008_001",
    "ai_009_001",
    "ai_010_001",
    "ai_011_001",
)
DEPLOYMENT_CHANNELS = (
    "arrived_rgb",
    "context_intrinsics",
    "context_poses",
    "frozen_image_features",
    "train_global_constants",
)
ORACLE_CHANNELS = DEPLOYMENT_CHANNELS + ("context_gt_depth", "query_gt_geometry")


def _sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def _digest(value):
    return hashlib.sha256(
        json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
    ).hexdigest()


@dataclass(frozen=True)
class SupportProtocolConfig:
    method: str
    scope: str = "DEPLOYABLE"
    input_channels: tuple[str, ...] = DEPLOYMENT_CHANNELS
    candidate_count: int = 128
    grid_size: tuple[int, int, int] = (8, 8, 8)
    renderer_samples: int = 64
    metric: str = "scene_macro_depth_absrel"
    hyperparameters: tuple[tuple[str, str | int | float | bool], ...] = ()

    def __post_init__(self):
        if self.method not in DEPLOYABLE_METHODS + ORACLE_METHODS:
            raise ValueError("Unknown support method")
        if self.scope not in ("DEPLOYABLE", "DIAGNOSTIC_ORACLE"):
            raise ValueError("Unknown method scope")
        if self.method in ORACLE_METHODS and self.scope != "DIAGNOSTIC_ORACLE":
            raise PermissionError("Oracle support cannot enter deployable configuration")
        if self.method in DEPLOYABLE_METHODS and self.scope != "DEPLOYABLE":
            raise ValueError("GT-free method must remain explicitly deployable")
        for name in ("input_channels", "grid_size", "hyperparameters"):
            if not isinstance(getattr(self, name), tuple):
                raise TypeError("Immutable tuple configuration required")
        allowed = ORACLE_CHANNELS if self.scope == "DIAGNOSTIC_ORACLE" else DEPLOYMENT_CHANNELS
        if set(self.input_channels) - set(allowed):
            raise PermissionError("Forbidden input channel for support method")
        if self.candidate_count != 128 or self.grid_size != (8, 8, 8):
            raise ValueError("Locked state and candidate budgets required")
        if self.renderer_samples != 64 or self.metric != "scene_macro_depth_absrel":
            raise ValueError("Locked renderer and primary metric required")
        for pair in self.hyperparameters:
            if not isinstance(pair, tuple) or len(pair) != 2 or not isinstance(pair[0], str):
                raise TypeError("Immutable named hyperparameter pairs required")
            if type(pair[1]) not in (str, int, float, bool):
                raise TypeError("Nested mutable hyperparameters prohibited")
        if len(dict(self.hyperparameters)) != len(self.hyperparameters):
            raise ValueError("Duplicate hyperparameter names")
        _digest(asdict(self))

    def assert_allowed_scene_role(self, role):
        if role == "FINAL_QUALIFICATION_HOLDOUT" and self.scope != "DEPLOYABLE":
            raise PermissionError("Oracle inference forbidden on final holdout")
        if role not in (
            "TRAIN",
            "REDESIGN_DEV",
            "REDESIGN_DEV_EXPOSED",
            "FINAL_QUALIFICATION_HOLDOUT",
        ):
            raise ValueError("Unknown scene role")


@dataclass(frozen=True)
class SceneRoleLock:
    train_scenes: tuple[str, ...]
    redesign_dev_scenes: tuple[str, ...]
    final_holdout_scenes: tuple[str, ...]
    exposed_scenes: tuple[str, ...] = EXPOSED_SCENES
    physical_identities: tuple[tuple[str, str], ...] = ()

    def __post_init__(self):
        groups = (
            self.train_scenes,
            self.redesign_dev_scenes,
            self.final_holdout_scenes,
            self.exposed_scenes,
        )
        if any(not isinstance(group, tuple) or not group for group in groups):
            raise TypeError("Nonempty immutable scene tuples required")
        if self.exposed_scenes != EXPOSED_SCENES:
            raise PermissionError("Old seven scenes must stay REDESIGN_DEV_EXPOSED")
        flat = tuple(s for group in groups for s in group)
        if len(set(flat)) != len(flat) or any(not isinstance(s, str) or not s for s in flat):
            raise PermissionError("Scene identity overlap or duplicate across locked roles")
        if not isinstance(self.physical_identities, tuple) or any(
            not isinstance(p, tuple) or len(p) != 2 for p in self.physical_identities
        ):
            raise TypeError("Immutable physical identity pairs required")
        ids = dict(self.physical_identities)
        if self.physical_identities:
            if set(ids) != set(flat) or len(ids) != len(self.physical_identities):
                raise PermissionError("Physical identity audit must cover each scene exactly once")
            if any(not isinstance(p, str) or not p for p in ids.values()):
                raise PermissionError("Unverified physical identity")
            other = {
                ids[s] for s in self.train_scenes + self.redesign_dev_scenes + self.exposed_scenes
            }
            held = [ids[s] for s in self.final_holdout_scenes]
            if set(held) & other or len(set(held)) != len(held):
                raise PermissionError("Physical environment leaks into final holdout")

    def assert_final_ready(self):
        if len(self.redesign_dev_scenes) < 8 or len(self.final_holdout_scenes) < 8:
            raise PermissionError("Final qualification requires >=8 dev and >=8 holdout scenes")
        if not self.physical_identities:
            raise PermissionError("Final qualification requires verified physical identity mapping")


@dataclass(frozen=True)
class MatchedTrainingConfig:
    support_method: str
    scene_ids: tuple[str, ...]
    seeds: tuple[int, ...]
    optimizer: str
    total_steps: int
    loss: str
    supervision: str
    parameter_count: int
    checkpoint_selection: str
    data_sha256: str
    optimizer_hyperparameters: tuple[tuple[str, float], ...] = ()

    def __post_init__(self):
        if self.support_method not in DEPLOYABLE_METHODS:
            raise PermissionError("Matched retraining requires deployable methods")
        if not isinstance(self.scene_ids, tuple) or not isinstance(self.seeds, tuple):
            raise TypeError("Immutable training scenes and seeds required")
        if not isinstance(self.optimizer_hyperparameters, tuple) or any(
            not isinstance(p, tuple) or len(p) != 2 or type(p[1]) not in (float, int)
            for p in self.optimizer_hyperparameters
        ):
            raise TypeError("Immutable optimizer hyperparameters required")
        if (
            not self.scene_ids
            or not self.seeds
            or self.total_steps <= 0
            or self.parameter_count <= 0
        ):
            raise ValueError("Nonempty matched training configuration required")


def assert_matched_training(current, redesigned):
    if not isinstance(current, MatchedTrainingConfig) or not isinstance(
        redesigned, MatchedTrainingConfig
    ):
        raise TypeError("MatchedTrainingConfig required")
    a, b = asdict(current), asdict(redesigned)
    a.pop("support_method")
    b.pop("support_method")
    differences = sorted(k for k in a if a[k] != b[k])
    if differences:
        raise PermissionError(f"Matched training differs beyond support_method: {differences}")
    return True


@dataclass(frozen=True)
class MethodLock:
    config: SupportProtocolConfig
    scene_roles: SceneRoleLock
    artifact_hashes: tuple[tuple[str, str, str], ...]
    dev_gate_path: str
    dev_gate_sha256: str
    digest: str


def seal_method_lock(config, scene_roles, artifact_paths, dev_gate_path):
    config.assert_allowed_scene_role("FINAL_QUALIFICATION_HOLDOUT")
    scene_roles.assert_final_ready()
    required = {"carrier_checkpoint", "support_config", "scene_role_lock", "inference_code"}
    if not required.issubset(artifact_paths):
        raise PermissionError("Incomplete method freeze artifacts")
    gate_path = str(Path(dev_gate_path).resolve())
    gate = json.loads(Path(gate_path).read_text())
    if gate.get("status") != "PASS":
        raise PermissionError("Development gate has not passed")
    artifacts = tuple(
        (k, str(Path(p).resolve()), _sha(p)) for k, p in sorted(artifact_paths.items())
    )
    if gate.get("method") != config.method or gate.get("config_digest") != _digest(asdict(config)):
        raise PermissionError("Development gate does not certify this frozen method configuration")
    if gate.get("artifact_hashes") != {name: digest for name, _, digest in artifacts}:
        raise PermissionError("Development gate artifacts differ from frozen method artifacts")
    if tuple(gate.get("dev_scene_ids", ())) != scene_roles.redesign_dev_scenes:
        raise PermissionError("Development gate scene identities differ from locked development")
    gate_hash = _sha(gate_path)
    digest = _digest(
        {
            "config": asdict(config),
            "roles": asdict(scene_roles),
            "artifacts": artifacts,
            "dev_gate": (gate_path, gate_hash),
        }
    )
    return MethodLock(config, scene_roles, artifacts, gate_path, gate_hash, digest)


class FinalHoldoutGate:
    """One atomic claim per experiment; interruptions do not silently reopen holdout."""

    def __init__(self, claim_path, method_lock=None):
        self.claim_path = Path(claim_path)
        self.method_lock = method_lock

    def _verify(self, config):
        lock = self.method_lock
        if not isinstance(lock, MethodLock):
            raise PermissionError("Final holdout cannot load before sealed method lock")
        if config != lock.config:
            raise PermissionError("Holdout inference cannot modify frozen configuration")
        config.assert_allowed_scene_role("FINAL_QUALIFICATION_HOLDOUT")
        rebuilt = seal_method_lock(
            config,
            lock.scene_roles,
            {name: path for name, path, _ in lock.artifact_hashes},
            lock.dev_gate_path,
        )
        if rebuilt != lock:
            raise PermissionError("Locked method artifact or development gate mutated")

    def claim(self, config, scene_ids):
        self._verify(config)
        lock = self.method_lock
        if tuple(scene_ids) != lock.scene_roles.final_holdout_scenes:
            raise PermissionError("Holdout scene identity allowlist changed")
        self.claim_path.parent.mkdir(parents=True, exist_ok=True)
        flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
        try:
            descriptor = os.open(self.claim_path, flags, 0o600)
        except FileExistsError as error:
            raise PermissionError("Final holdout has already been claimed; no reopening") from error
        with os.fdopen(descriptor, "w") as handle:
            json.dump(
                {
                    "method_lock_digest": lock.digest,
                    "scene_ids": list(scene_ids),
                    "status": "CLAIMED_ONCE",
                },
                handle,
                sort_keys=True,
            )
            handle.flush()
            os.fsync(handle.fileno())
        return _HoldoutSession(self, config, _sha(self.claim_path))


class _HoldoutSession:
    def __init__(self, gate, config, claim_hash):
        self.gate, self.config, self.claim_hash = gate, config, claim_hash
        self.events = []
        self._model_scenes = set()
        self._queries = set()

    def _check(self, scene_id):
        self.gate._verify(self.config)
        if _sha(self.gate.claim_path) != self.claim_hash:
            raise PermissionError("Atomic holdout claim mutated")
        if scene_id not in self.gate.method_lock.scene_roles.final_holdout_scenes:
            raise PermissionError("Scene not in final holdout allowlist")

    def load_model_scene(self, scene_id, loader: Callable):
        self._check(scene_id)
        if scene_id in self._model_scenes:
            raise PermissionError("Holdout model scene cannot be opened twice")
        self._model_scenes.add(scene_id)
        self.events.append({"kind": "model_scene", "scene_id": scene_id})
        value = loader()
        self._check(scene_id)
        return value

    def read_query_ground_truth(self, scene_id, query_id, sealed_states, loader: Callable):
        self._check(scene_id)
        if scene_id not in self._model_scenes or not sealed_states:
            raise PermissionError("Query GT requires constructed and sealed states")
        fingerprints = []
        for state in sealed_states:
            if not isinstance(state, SealedScene) or state.scene_id != scene_id:
                raise PermissionError("Expected sealed states for target holdout scene")
            _validate_sealed(state)
            if hash_scene_state(state.scene_state) != state.state_hash:
                raise PermissionError("Sealed holdout state hash mismatch")
            fingerprints.append(hash_value(state))
        key = (scene_id, query_id)
        if key in self._queries:
            raise PermissionError("Holdout query GT cannot be reopened")
        self._queries.add(key)
        self.events.append({"kind": "query_GT", "scene_id": scene_id, "query_id": query_id})
        value = loader()
        self._check(scene_id)
        if fingerprints != [hash_value(s) for s in sealed_states]:
            raise PermissionError("Evaluator mutated sealed holdout states")
        return value


def old_exposed_manifest():
    return {
        "scene_ids": list(EXPOSED_SCENES),
        "role": "REDESIGN_DEV_EXPOSED",
        "final_qualification_allowed": False,
        "independent_confirmation_allowed": False,
    }


def validate_dev_scene_ids(roles, scene_ids):
    """Run before media loading; diagnostic query geometry exception never includes holdout."""
    if isinstance(roles, SceneRoleLock):
        train = set(roles.train_scenes)
        dev = set(roles.redesign_dev_scenes)
        holdout = set(roles.final_holdout_scenes)
    else:
        train = set(roles["TRAIN_SCENES"])
        dev = set(roles["REDESIGN_DEV_SCENES"])
        holdout = set(roles["FINAL_HOLDOUT_SCENES"])
    exposed = set(EXPOSED_SCENES)
    if holdout & (train | dev | exposed) or train & (dev | exposed) or dev & exposed:
        raise PermissionError("Scene roles overlap")
    ids = tuple(scene_ids)
    if not ids or len(ids) != len(set(ids)) or set(ids) - (dev | exposed):
        raise PermissionError(
            "Diagnostic/dev inference rejects holdout, training and unknown identities"
        )
    return True