feat(perception): establish object centric contracts
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
"""Executable Milestone 4 baseline binding.
|
||||
|
||||
The profile points at immutable local evidence without copying large or sensitive
|
||||
artifacts into Git. Verification is explicit: missing evidence is a failure, not
|
||||
an invitation to silently select a different source or experiment result.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
BASELINE_SCHEMA: Final = "missioncore.perception-m4-baseline/v1"
|
||||
REUSE_INVENTORY_SCHEMA: Final = "missioncore.perception-reuse-inventory/v1"
|
||||
BASELINE_PROFILE_ID: Final = "m4-ravnoves00-recorded-realtime/v1"
|
||||
BASELINE_SOURCE_ID: Final = "RAVNOVES00"
|
||||
BASELINE_SESSION_ID: Final = "20260720T065719Z_viewer_live"
|
||||
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_EXPECTED_EVIDENCE_ROLES: Final = {
|
||||
"source-fusion",
|
||||
"track-geometry",
|
||||
"source-paced-worker",
|
||||
"temporal-occupied",
|
||||
"degradation-recovery",
|
||||
"raw-fisheye-detector-capacity",
|
||||
}
|
||||
_BASELINE_KEYS: Final = {
|
||||
"schema_version",
|
||||
"profile_id",
|
||||
"source",
|
||||
"calibration",
|
||||
"detector",
|
||||
"evidence",
|
||||
"authority",
|
||||
"non_goals",
|
||||
"rollback",
|
||||
}
|
||||
_EVIDENCE_KEYS: Final = {
|
||||
"role",
|
||||
"relative_path",
|
||||
"schema_version",
|
||||
"result_id",
|
||||
"identity_sha256",
|
||||
"file_sha256",
|
||||
}
|
||||
|
||||
|
||||
class BaselineContractError(ValueError):
|
||||
"""The recorded-realtime baseline is ambiguous, mutated or incomplete."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BaselineEvidence:
|
||||
role: str
|
||||
relative_path: str
|
||||
schema_version: str
|
||||
result_id: str
|
||||
identity_sha256: str
|
||||
file_sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BaselineProfile:
|
||||
path: Path
|
||||
document: dict[str, object]
|
||||
evidence: tuple[BaselineEvidence, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BaselineVerification:
|
||||
profile_id: str
|
||||
source_id: str
|
||||
session_id: str
|
||||
verified_paths: tuple[str, ...]
|
||||
|
||||
|
||||
def load_m4_baseline(path: Path) -> BaselineProfile:
|
||||
"""Load and fail-closed validate the one admitted M4 baseline profile."""
|
||||
|
||||
document = _read_object(path)
|
||||
_exact_keys(document, _BASELINE_KEYS, "baseline")
|
||||
if document.get("schema_version") != BASELINE_SCHEMA:
|
||||
raise BaselineContractError("baseline schema is incompatible")
|
||||
if document.get("profile_id") != BASELINE_PROFILE_ID:
|
||||
raise BaselineContractError("baseline profile identity changed")
|
||||
|
||||
source = _object(document.get("source"), "source")
|
||||
if source.get("source_id") != BASELINE_SOURCE_ID:
|
||||
raise BaselineContractError("M4 source must remain RAVNOVES00")
|
||||
if source.get("session_id") != BASELINE_SESSION_ID:
|
||||
raise BaselineContractError("M4 source session identity changed")
|
||||
modalities = _string_array(source.get("modalities"), "source modalities")
|
||||
if set(modalities) != {"image", "registered-point-increment", "pose"}:
|
||||
raise BaselineContractError("baseline source must bind image, points and pose")
|
||||
if _integer(source.get("frame_count"), "source frame count") != 4489:
|
||||
raise BaselineContractError("baseline source frame count changed")
|
||||
|
||||
authority = _object(document.get("authority"), "authority")
|
||||
if authority.get("mode") != "replay-simulated":
|
||||
raise BaselineContractError("M4 authority must remain replay-simulated")
|
||||
for key in (
|
||||
"ground_truth",
|
||||
"physical_live",
|
||||
"physical_collision_accepted",
|
||||
"commands_enabled",
|
||||
"actuation_allowed",
|
||||
"navigation_or_safety_accepted",
|
||||
):
|
||||
if authority.get(key) is not False:
|
||||
raise BaselineContractError(f"baseline authority {key} must remain false")
|
||||
|
||||
evidence_items = document.get("evidence")
|
||||
if not isinstance(evidence_items, list):
|
||||
raise BaselineContractError("baseline evidence must be an array")
|
||||
evidence = tuple(_evidence(item) for item in evidence_items)
|
||||
roles = [item.role for item in evidence]
|
||||
if len(set(roles)) != len(roles) or set(roles) != _EXPECTED_EVIDENCE_ROLES:
|
||||
raise BaselineContractError("baseline evidence roles are incomplete or duplicated")
|
||||
paths = [item.relative_path for item in evidence]
|
||||
if len(set(paths)) != len(paths):
|
||||
raise BaselineContractError("baseline evidence paths must be unique")
|
||||
|
||||
rollback = _object(document.get("rollback"), "rollback")
|
||||
if rollback.get("worker_id") != "worker-006":
|
||||
raise BaselineContractError("rollback worker identity changed")
|
||||
if rollback.get("worker_node") != "DESKTOP-OPJ8J04":
|
||||
raise BaselineContractError("rollback worker node changed")
|
||||
entrypoint = rollback.get("entrypoint")
|
||||
if not isinstance(entrypoint, str) or "run_e15_shadow_inference.py serve" not in entrypoint:
|
||||
raise BaselineContractError("rollback E15 process identity is missing")
|
||||
_digest(rollback.get("runner_sha256"), "rollback runner digest")
|
||||
_digest(rollback.get("orchestrator_sha256"), "rollback orchestrator digest")
|
||||
|
||||
return BaselineProfile(path=path, document=document, evidence=evidence)
|
||||
|
||||
|
||||
def verify_m4_baseline(repository_root: Path, profile: BaselineProfile) -> BaselineVerification:
|
||||
"""Resolve every immutable evidence document and verify its exact digest."""
|
||||
|
||||
root = repository_root.resolve()
|
||||
verified: list[str] = []
|
||||
for item in profile.evidence:
|
||||
evidence_path = (root / item.relative_path).resolve()
|
||||
if root not in evidence_path.parents:
|
||||
raise BaselineContractError("baseline evidence escapes the repository root")
|
||||
if not evidence_path.is_file():
|
||||
raise BaselineContractError(f"baseline evidence is missing: {item.relative_path}")
|
||||
if _file_sha256(evidence_path) != item.file_sha256:
|
||||
raise BaselineContractError(f"baseline evidence digest changed: {item.role}")
|
||||
evidence_document = _read_object(evidence_path)
|
||||
if evidence_document.get("schema_version") != item.schema_version:
|
||||
raise BaselineContractError(f"baseline evidence schema changed: {item.role}")
|
||||
if evidence_document.get("identity_sha256") != item.identity_sha256:
|
||||
raise BaselineContractError(f"baseline evidence identity changed: {item.role}")
|
||||
result_id = evidence_document.get("result_id")
|
||||
if result_id is None and item.role == "source-fusion":
|
||||
result_id = evidence_path.parent.name
|
||||
if result_id != item.result_id:
|
||||
raise BaselineContractError(f"baseline evidence result changed: {item.role}")
|
||||
verified.append(item.relative_path)
|
||||
|
||||
source = _object(profile.document.get("source"), "source")
|
||||
return BaselineVerification(
|
||||
profile_id=BASELINE_PROFILE_ID,
|
||||
source_id=_string(source.get("source_id"), "source id"),
|
||||
session_id=_string(source.get("session_id"), "session id"),
|
||||
verified_paths=tuple(verified),
|
||||
)
|
||||
|
||||
|
||||
def validate_reuse_inventory(path: Path) -> dict[str, object]:
|
||||
"""Validate the M4 primitive/wrapper split used by architecture tests."""
|
||||
|
||||
document = _read_object(path)
|
||||
_exact_keys(
|
||||
document,
|
||||
{
|
||||
"schema_version",
|
||||
"profile_id",
|
||||
"reusable_primitives",
|
||||
"historical_wrappers",
|
||||
"rules",
|
||||
},
|
||||
"reuse inventory",
|
||||
)
|
||||
if document.get("schema_version") != REUSE_INVENTORY_SCHEMA:
|
||||
raise BaselineContractError("reuse inventory schema is incompatible")
|
||||
reusable = document.get("reusable_primitives")
|
||||
wrappers = document.get("historical_wrappers")
|
||||
if not isinstance(reusable, list) or not reusable:
|
||||
raise BaselineContractError("reuse inventory has no admitted primitives")
|
||||
if not isinstance(wrappers, list) or not wrappers:
|
||||
raise BaselineContractError("reuse inventory has no historical wrappers")
|
||||
reusable_modules = {_module(item, "reusable primitive") for item in reusable}
|
||||
wrapper_modules = {_module(item, "historical wrapper") for item in wrappers}
|
||||
if reusable_modules & wrapper_modules:
|
||||
raise BaselineContractError("a module cannot be reusable and historical")
|
||||
rules = _object(document.get("rules"), "reuse rules")
|
||||
expected_rules = {
|
||||
"historical_wrappers_are_product_dependencies": False,
|
||||
"contracts_may_import_compute": False,
|
||||
"graph_may_import_experiment_modules": False,
|
||||
"providers_may_import_admitted_primitives": True,
|
||||
"bulk_legacy_migration_required": False,
|
||||
}
|
||||
if rules != expected_rules:
|
||||
raise BaselineContractError("reuse dependency rules changed")
|
||||
return document
|
||||
|
||||
|
||||
def _evidence(value: object) -> BaselineEvidence:
|
||||
document = _object(value, "evidence item")
|
||||
_exact_keys(document, _EVIDENCE_KEYS, "evidence item")
|
||||
relative_path = _string(document.get("relative_path"), "evidence path")
|
||||
path = Path(relative_path)
|
||||
if path.is_absolute() or ".." in path.parts or path.suffix != ".json":
|
||||
raise BaselineContractError("evidence path must be a relative JSON path")
|
||||
return BaselineEvidence(
|
||||
role=_string(document.get("role"), "evidence role"),
|
||||
relative_path=relative_path,
|
||||
schema_version=_string(document.get("schema_version"), "evidence schema"),
|
||||
result_id=_string(document.get("result_id"), "evidence result id"),
|
||||
identity_sha256=_digest(document.get("identity_sha256"), "evidence identity"),
|
||||
file_sha256=_digest(document.get("file_sha256"), "evidence file digest"),
|
||||
)
|
||||
|
||||
|
||||
def _module(value: object, label: str) -> str:
|
||||
document = _object(value, label)
|
||||
return _string(document.get("module"), f"{label} module")
|
||||
|
||||
|
||||
def _read_object(path: Path) -> dict[str, object]:
|
||||
try:
|
||||
value = json.loads(path.read_text("utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise BaselineContractError(f"cannot read baseline document: {path}") from exc
|
||||
return _object(value, str(path))
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
||||
raise BaselineContractError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None:
|
||||
if set(document) != expected:
|
||||
raise BaselineContractError(f"{label} fields are incompatible")
|
||||
|
||||
|
||||
def _string(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise BaselineContractError(f"{label} must be a nonempty string")
|
||||
return value
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise BaselineContractError(f"{label} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def _string_array(value: object, label: str) -> tuple[str, ...]:
|
||||
if not isinstance(value, list):
|
||||
raise BaselineContractError(f"{label} must be an array")
|
||||
return tuple(_string(item, label) for item in value)
|
||||
|
||||
|
||||
def _digest(value: object, label: str) -> str:
|
||||
digest = _string(value, label)
|
||||
if _SHA256.fullmatch(digest) is None:
|
||||
raise BaselineContractError(f"{label} must be a SHA-256 digest")
|
||||
return digest
|
||||
Reference in New Issue
Block a user