feat(lab): add lazy E40 evidence review

This commit is contained in:
DCCONSTRUCTIONS
2026-07-30 21:53:04 +03:00
parent e9bbfb9a41
commit fdcaf57ae7
24 changed files with 3570 additions and 165 deletions
+436
View File
@@ -0,0 +1,436 @@
from __future__ import annotations
import hashlib
import json
import os
import re
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Literal
from k1link.artifacts import utc_now_iso, write_json_atomic
E40_OPERATOR_REVIEW_SCHEMA: Final = "missioncore.e40-operator-review/v1"
E40_OPERATOR_REVIEW_PROTOCOL: Final = "sealed-error-adjudication/v1"
E40OperatorVerdict = Literal["confirmed-error", "rejected-error"]
_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
_MATERIALIZATION_ID = re.compile(r"^e30-materialization-[a-f0-9]{64}$")
_ITEM_ID = re.compile(r"^e30-review-item-[a-f0-9]{64}$")
_REVIEW_ID = re.compile(r"^e40-operator-review-[a-f0-9]{64}$")
_EVENT_ID = re.compile(r"^e40-operator-verdict-[a-f0-9]{64}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_REVIEWER_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$")
_IDEMPOTENCY_KEY = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
_VERDICTS: Final = {"confirmed-error", "rejected-error"}
_MAX_JSON_BYTES: Final = 2 * 1024 * 1024
class E40OperatorReviewError(RuntimeError):
"""Base error for the E40 operator-review lifecycle."""
class E40OperatorReviewConflictError(E40OperatorReviewError):
"""The caller attempted to update a stale review revision."""
class E40OperatorReviewValidationError(E40OperatorReviewError):
"""The requested review operation violates the frozen protocol."""
class E40OperatorReviewIntegrityError(E40OperatorReviewError):
"""The stored review no longer matches its sealed E40 substrate."""
@dataclass(frozen=True)
class E40OperatorReviewSubject:
item_id: str
sequence: int
@dataclass(frozen=True)
class E40OperatorReviewSubstrate:
result_id: str
materialization_id: str
case_catalog_sha256: str
subjects: tuple[E40OperatorReviewSubject, ...]
def __post_init__(self) -> None:
if (
_RESULT_ID.fullmatch(self.result_id) is None
or _MATERIALIZATION_ID.fullmatch(self.materialization_id) is None
or _SHA256.fullmatch(self.case_catalog_sha256) is None
or not self.subjects
or len(self.subjects) > 64
):
raise E40OperatorReviewValidationError(
"E40 operator-review source binding is invalid"
)
if len({subject.item_id for subject in self.subjects}) != len(self.subjects):
raise E40OperatorReviewValidationError(
"E40 operator-review subjects are not unique"
)
if any(
_ITEM_ID.fullmatch(subject.item_id) is None or subject.sequence < 0
for subject in self.subjects
):
raise E40OperatorReviewValidationError(
"E40 operator-review subject is invalid"
)
@property
def item_set_sha256(self) -> str:
return hashlib.sha256(
_canonical_json(
[
{"item_id": subject.item_id, "sequence": subject.sequence}
for subject in self.subjects
]
)
).hexdigest()
def binding(self) -> dict[str, object]:
return {
"result_id": self.result_id,
"materialization_id": self.materialization_id,
"case_catalog_sha256": self.case_catalog_sha256,
"item_count": len(self.subjects),
"item_set_sha256": self.item_set_sha256,
}
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def _safe_root(root: Path) -> Path:
root.mkdir(parents=True, exist_ok=True)
if root.is_symlink() or not root.is_dir():
raise E40OperatorReviewIntegrityError(
"E40 operator-review root is invalid"
)
return root.resolve()
def _reviewer_id(value: str) -> str:
value = value.strip()
if _REVIEWER_ID.fullmatch(value) is None:
raise E40OperatorReviewValidationError("reviewer_id is invalid")
return value
def _review_id(
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> str:
identity = {
"protocol": E40_OPERATOR_REVIEW_PROTOCOL,
"source": substrate.binding(),
"reviewer_id": reviewer_id,
}
return f"e40-operator-review-{hashlib.sha256(_canonical_json(identity)).hexdigest()}"
@contextmanager
def _exclusive_lock(path: Path) -> Iterator[None]:
path.parent.mkdir(parents=True, exist_ok=True)
try:
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError as exc:
raise E40OperatorReviewConflictError(
"E40 operator review is being updated"
) from exc
try:
yield
finally:
os.close(descriptor)
path.unlink(missing_ok=True)
class E40OperatorReviewStore:
"""Mutable operator verdicts kept separate from immutable E40 evidence."""
def __init__(self, *, root: Path) -> None:
self.root = _safe_root(root)
def get(
self,
*,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> dict[str, object]:
reviewer_id = _reviewer_id(reviewer_id)
review_id = _review_id(substrate, reviewer_id)
path = self.root / f"{review_id}.json"
if path.is_symlink():
raise E40OperatorReviewIntegrityError(
"E40 operator review must not be a symlink"
)
if not path.exists():
return self._empty(
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
)
return self._read(
path=path,
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
)
def record_verdict(
self,
*,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
item_id: str,
expected_revision: int,
idempotency_key: str,
verdict: E40OperatorVerdict,
) -> dict[str, object]:
reviewer_id = _reviewer_id(reviewer_id)
if (
_ITEM_ID.fullmatch(item_id) is None
or item_id not in {subject.item_id for subject in substrate.subjects}
):
raise E40OperatorReviewValidationError(
"item_id is outside the E40 error catalog"
)
if (
_IDEMPOTENCY_KEY.fullmatch(idempotency_key) is None
or verdict not in _VERDICTS
):
raise E40OperatorReviewValidationError(
"E40 operator verdict request is invalid"
)
if expected_revision < 0:
raise E40OperatorReviewValidationError(
"expected_revision is invalid"
)
review_id = _review_id(substrate, reviewer_id)
path = self.root / f"{review_id}.json"
lock_path = self.root / ".locks" / f"{review_id}.lock"
with _exclusive_lock(lock_path):
current = self.get(
substrate=substrate,
reviewer_id=reviewer_id,
)
decisions = list(current["decisions"]) # type: ignore[arg-type]
replay = next(
(
decision
for decision in decisions
if decision["idempotency_key"] == idempotency_key
),
None,
)
if replay is not None:
if (
replay["item_id"] != item_id
or replay["verdict"] != verdict
):
raise E40OperatorReviewConflictError(
"idempotency key was already used for another verdict"
)
return current
if current["revision"] != expected_revision:
raise E40OperatorReviewConflictError(
"E40 operator-review revision changed"
)
next_revision = expected_revision + 1
decided_at = utc_now_iso()
event_identity = {
"review_id": review_id,
"item_id": item_id,
"verdict": verdict,
"revision": next_revision,
"idempotency_key": idempotency_key,
"decided_at_utc": decided_at,
}
decision = {
"item_id": item_id,
"verdict": verdict,
"revision": next_revision,
"idempotency_key": idempotency_key,
"decided_at_utc": decided_at,
"event_id": (
"e40-operator-verdict-"
f"{hashlib.sha256(_canonical_json(event_identity)).hexdigest()}"
),
}
decisions = [
previous
for previous in decisions
if previous["item_id"] != item_id
]
decisions.append(decision)
subject_order = {
subject.item_id: index
for index, subject in enumerate(substrate.subjects)
}
decisions.sort(key=lambda value: subject_order[str(value["item_id"])])
payload = self._payload(
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
revision=next_revision,
decisions=decisions,
updated_at_utc=decided_at,
)
write_json_atomic(path, payload)
path.chmod(0o600)
return payload
def _empty(
self,
*,
review_id: str,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> dict[str, object]:
return self._payload(
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
revision=0,
decisions=[],
updated_at_utc=None,
)
def _payload(
self,
*,
review_id: str,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
revision: int,
decisions: list[dict[str, object]],
updated_at_utc: str | None,
) -> dict[str, object]:
return {
"schema_version": E40_OPERATOR_REVIEW_SCHEMA,
"review_id": review_id,
"protocol": E40_OPERATOR_REVIEW_PROTOCOL,
"source": substrate.binding(),
"reviewer_id": reviewer_id,
"revision": revision,
"decisions": decisions,
"reviewed_item_count": len(decisions),
"remaining_item_count": len(substrate.subjects) - len(decisions),
"updated_at_utc": updated_at_utc,
"access": "review-write",
}
def _read(
self,
*,
path: Path,
review_id: str,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> dict[str, object]:
if (
path.is_symlink()
or not path.is_file()
or not 0 < path.stat().st_size <= _MAX_JSON_BYTES
):
raise E40OperatorReviewIntegrityError(
"E40 operator review is unavailable"
)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E40OperatorReviewIntegrityError(
"E40 operator review is invalid"
) from exc
if not isinstance(value, dict):
raise E40OperatorReviewIntegrityError(
"E40 operator review must be an object"
)
decisions = value.get("decisions")
revision = value.get("revision")
if (
value.get("schema_version") != E40_OPERATOR_REVIEW_SCHEMA
or value.get("review_id") != review_id
or value.get("protocol") != E40_OPERATOR_REVIEW_PROTOCOL
or value.get("source") != substrate.binding()
or value.get("reviewer_id") != reviewer_id
or value.get("access") != "review-write"
or isinstance(revision, bool)
or not isinstance(revision, int)
or revision < 0
or not isinstance(decisions, list)
or value.get("reviewed_item_count") != len(decisions)
or value.get("remaining_item_count")
!= len(substrate.subjects) - len(decisions)
):
raise E40OperatorReviewIntegrityError(
"E40 operator review identity is invalid"
)
known_items = {subject.item_id for subject in substrate.subjects}
item_ids: set[str] = set()
idempotency_keys: set[str] = set()
for decision in decisions:
decided_at = decision.get("decided_at_utc") if isinstance(
decision, dict
) else None
event_identity = {
"review_id": review_id,
"item_id": decision.get("item_id") if isinstance(
decision, dict
) else None,
"verdict": decision.get("verdict") if isinstance(
decision, dict
) else None,
"revision": decision.get("revision") if isinstance(
decision, dict
) else None,
"idempotency_key": decision.get("idempotency_key") if isinstance(
decision, dict
) else None,
"decided_at_utc": decided_at,
}
expected_event_id = (
"e40-operator-verdict-"
f"{hashlib.sha256(_canonical_json(event_identity)).hexdigest()}"
)
if (
not isinstance(decision, dict)
or decision.get("item_id") not in known_items
or decision.get("item_id") in item_ids
or decision.get("verdict") not in _VERDICTS
or not isinstance(decision.get("revision"), int)
or not 1 <= int(decision["revision"]) <= revision
or not isinstance(decision.get("decided_at_utc"), str)
or _EVENT_ID.fullmatch(str(decision.get("event_id"))) is None
or decision.get("event_id") != expected_event_id
or _IDEMPOTENCY_KEY.fullmatch(
str(decision.get("idempotency_key"))
)
is None
or decision.get("idempotency_key") in idempotency_keys
):
raise E40OperatorReviewIntegrityError(
"E40 operator verdict is invalid"
)
item_ids.add(str(decision["item_id"]))
idempotency_keys.add(str(decision["idempotency_key"]))
if decisions and max(
int(decision["revision"]) for decision in decisions
) != revision:
raise E40OperatorReviewIntegrityError(
"E40 operator-review revision is invalid"
)
return value
+159
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import copy
import json
import re
from collections.abc import Callable
from functools import lru_cache
@@ -58,6 +59,10 @@ from k1link.compute.e40_perception_product_gate import (
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-advanced-catalog/v1"
)
LABORATORY_ADVANCED_INDEX_SCHEMA: Final = (
"missioncore.laboratory-advanced-index/v1"
)
_INDEX_DOCUMENT_MAX_BYTES: Final = 64 * 1024
_E31_RESULT_ID = re.compile(r"^e31-source-qualification-[a-f0-9]{64}$")
_E32_RESULT_ID = re.compile(r"^e32-track-geometry-[a-f0-9]{64}$")
@@ -71,6 +76,14 @@ _E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
RootProvider = Callable[[], Path | None]
_AdvancedIndexSpec = tuple[
str,
RootProvider,
re.Pattern[str],
str,
str,
]
def _result_signature(root: Path) -> tuple[int, ...]:
signature: list[int] = []
@@ -193,6 +206,82 @@ def _candidates(root: Path, pattern: re.Pattern[str]) -> list[Path]:
)
def _advanced_index_item(
candidate: Path,
*,
work_id: str,
document_name: str,
schema_version: str,
) -> dict[str, object]:
document_path = candidate / document_name
if document_path.is_symlink() or not document_path.is_file():
raise ValueError("advanced LAB index document is missing")
if document_path.stat().st_size > _INDEX_DOCUMENT_MAX_BYTES:
raise ValueError("advanced LAB index document is too large")
payload = json.loads(document_path.read_text(encoding="utf-8"))
document = _object(payload, "advanced LAB index document")
if document.get("schema_version") != schema_version:
raise ValueError("advanced LAB index schema is invalid")
if document.get("result_id") != candidate.name:
raise ValueError("advanced LAB index result identity is invalid")
identity_sha256 = document.get("identity_sha256")
if (
not isinstance(identity_sha256, str)
or re.fullmatch(r"[a-f0-9]{64}", identity_sha256) is None
or not candidate.name.endswith(identity_sha256)
):
raise ValueError("advanced LAB index digest is invalid")
identity = _object(document.get("identity"), "advanced LAB identity")
authority = _object(
identity.get("authority"),
"advanced LAB authority",
)
if (
authority.get("commands_enabled") is not False
or authority.get("navigation_or_safety_accepted") is not False
):
raise ValueError("advanced LAB authority is invalid")
if document.get("ground_truth") not in (None, False):
raise ValueError("advanced LAB ground-truth claim is invalid")
created_at_utc = document.get("created_at_utc")
if not isinstance(created_at_utc, str) or not created_at_utc.strip():
raise ValueError("advanced LAB creation time is invalid")
return {
"work_id": work_id,
"result_id": candidate.name,
"created_at_utc": created_at_utc,
"access": "read-only",
}
def _advanced_index(
specs: tuple[_AdvancedIndexSpec, ...],
) -> dict[str, object]:
items: list[dict[str, object]] = []
for work_id, provider, pattern, document_name, schema_version in specs:
root = _configured_root(provider)
if root is None:
continue
for candidate in _candidates(root, pattern):
try:
items.append(
_advanced_index_item(
candidate,
work_id=work_id,
document_name=document_name,
schema_version=schema_version,
)
)
break
except (json.JSONDecodeError, OSError, TypeError, ValueError):
continue
return {
"schema_version": LABORATORY_ADVANCED_INDEX_SCHEMA,
"items": items,
"access": "read-only",
}
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ValueError(f"{label} is invalid")
@@ -739,6 +828,76 @@ def build_advanced_laboratory_router(
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@router.get("/advanced-index")
def list_advanced_results() -> dict[str, object]:
return _advanced_index(
(
(
"e31-source-binding",
e31_root_provider,
_E31_RESULT_ID,
"manifest.json",
"missioncore.e31-source-qualification/v1",
),
(
"e32-track-geometry",
e32_root_provider,
_E32_RESULT_ID,
"manifest.json",
"missioncore.e32-track-geometry-replay/v1",
),
(
"e33-worker-shadow",
e33_root_provider,
_E33_RESULT_ID,
"result.json",
"missioncore.e33-worker-shadow-result/v1",
),
(
"e34-temporal-layer",
e34_root_provider,
_E34_RESULT_ID,
"manifest.json",
"missioncore.e34-temporal-occupied-result/v1",
),
(
"e35-degradation-recovery",
e35_root_provider,
_E35_RESULT_ID,
"manifest.json",
"missioncore.e35-degradation-result/v1",
),
(
"e37-ravnoves-acceptance",
e37_root_provider,
_E37_RESULT_ID,
"manifest.json",
"missioncore.e37-acceptance-contract/v1",
),
(
"e38-perception-baseline",
e38_root_provider,
_E38_RESULT_ID,
"manifest.json",
"missioncore.e38-perception-baseline/v1",
),
(
"e39-perception-refinement",
e39_root_provider,
_E39_RESULT_ID,
"manifest.json",
"missioncore.e39-perception-refinement/v1",
),
(
"e40-perception-product-gate",
e40_root_provider,
_E40_RESULT_ID,
"manifest.json",
"missioncore.e40-perception-product-gate/v1",
),
)
)
@router.get("/e31/results")
def list_e31_results(
limit: int = Query(default=1, ge=1, le=10),
+19
View File
@@ -40,6 +40,7 @@ from k1link.web.device_plugin_composition import load_installed_device_plugins
from k1link.web.e30_engineering_api import build_e30_engineering_router
from k1link.web.e30_human_review_api import build_e30_human_review_router
from k1link.web.e30_review_api import build_e30_review_router
from k1link.web.e40_case_review_api import build_e40_case_review_router
from k1link.web.environment_api import build_environment_router
from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.lidar_api import build_lidar_router
@@ -614,6 +615,24 @@ app.include_router(
),
)
)
app.include_router(
build_e40_case_review_router(
e40_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e40"
/ "results"
),
operator_review_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e40"
/ "operator-reviews"
),
)
)
app.include_router(
build_e30_engineering_router(
generation_root_provider=lambda: (
+479
View File
@@ -0,0 +1,479 @@
from __future__ import annotations
import hashlib
import json
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Annotated, Any, Final, Literal
from fastapi import APIRouter, HTTPException, Query
from fastapi import Path as ApiPath
from pydantic import BaseModel, ConfigDict, Field
from k1link.compute.e40_operator_review import (
E40OperatorReviewConflictError,
E40OperatorReviewIntegrityError,
E40OperatorReviewStore,
E40OperatorReviewSubject,
E40OperatorReviewSubstrate,
E40OperatorReviewValidationError,
)
from k1link.compute.e40_perception_product_gate import (
E40_PREDICTION_SCHEMA,
E40_PREDICTIONS_NAME,
E40PerceptionProductGate,
E40PerceptionProductGateError,
read_e40_perception_product_gate,
)
LABORATORY_E40_CASE_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-e40-case-catalog/v1"
)
_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
_MATERIALIZATION_ID = re.compile(r"^e30-materialization-[a-f0-9]{64}$")
_ITEM_ID = re.compile(r"^e30-review-item-[a-f0-9]{64}$")
_MAX_PREDICTIONS_BYTES: Final = 8 * 1024 * 1024
_AUTHORITY: Final = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
_DIMENSIONS: Final = (
"presence",
"geometry_association",
"freshness",
)
_STRATA: Final = {
"agree",
"camera-only",
"conflict",
"geometry-only",
"unknown",
}
_SEVERITIES: Final = {"high", "medium", "standard"}
_SPLITS: Final = {"development", "validation"}
_VALUES: Final = {
"presence": {
"background-or-noise",
"object-present",
"occupied-environment",
},
"geometry_association": {
"independent-occupied",
"insufficient-support",
"object-associated",
"rejected-nonobject",
"unknown",
},
"freshness": {"current", "stale", "unavailable"},
}
_SEVERITY_ORDER: Final = {"high": 0, "medium": 1, "standard": 2}
RootProvider = Callable[[], Path | None]
class E40OperatorVerdictRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
reviewer_id: str = Field(min_length=1, max_length=128)
expected_revision: int = Field(ge=0)
idempotency_key: str = Field(min_length=1, max_length=128)
verdict: Literal["confirmed-error", "rejected-error"]
def _result_signature(root: Path) -> tuple[int, ...]:
signature: list[int] = []
for path in sorted(root.iterdir(), key=lambda item: item.name):
if not path.is_file() or path.is_symlink():
continue
stat = path.stat()
signature.extend((stat.st_size, stat.st_mtime_ns))
return tuple(signature)
@lru_cache(maxsize=8)
def _read_result_cached(
root_text: str,
signature: tuple[int, ...],
) -> E40PerceptionProductGate:
del signature
return read_e40_perception_product_gate(Path(root_text))
def _configured_root(provider: RootProvider) -> Path | None:
root = provider()
if root is None:
return None
resolved = root.resolve()
return resolved if resolved.is_dir() else None
def _candidate(root: Path, result_id: str) -> Path:
if _RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="E40 result не найден")
candidate = root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="E40 result не найден")
return candidate
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise E40PerceptionProductGateError(f"{label} must be an object")
return value
def _nonnegative_integer(value: object, label: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise E40PerceptionProductGateError(
f"{label} must be a nonnegative integer"
)
return value
def _bounded_number(value: object, label: str) -> float:
if (
isinstance(value, bool)
or not isinstance(value, (int, float))
or not 0 <= float(value) <= 1
):
raise E40PerceptionProductGateError(f"{label} must be within [0, 1]")
return float(value)
def _dimension_state(value: object, label: str) -> dict[str, str]:
source = _object(value, label)
if set(source) != set(_DIMENSIONS):
raise E40PerceptionProductGateError(
f"{label} has incompatible dimensions"
)
state: dict[str, str] = {}
for dimension in _DIMENSIONS:
item = source.get(dimension)
if not isinstance(item, str) or item not in _VALUES[dimension]:
raise E40PerceptionProductGateError(
f"{label}.{dimension} is invalid"
)
state[dimension] = item
return state
def _read_predictions(result: E40PerceptionProductGate) -> tuple[dict[str, Any], ...]:
path = result.result_root / E40_PREDICTIONS_NAME
if (
not path.is_file()
or path.is_symlink()
or path.stat().st_size > _MAX_PREDICTIONS_BYTES
):
raise E40PerceptionProductGateError(
"E40 sealed predictions are unavailable"
)
rows: list[dict[str, Any]] = []
sequences: set[int] = set()
item_ids: set[str] = set()
try:
with path.open("r", encoding="utf-8-sig") as stream:
for line in stream:
row = _object(json.loads(line), E40_PREDICTIONS_NAME)
sequence = _nonnegative_integer(
row.get("sequence"),
"E40 prediction.sequence",
)
item_id = row.get("item_id")
review_key = row.get("review_key")
source_frame_index = _nonnegative_integer(
row.get("source_frame_index"),
"E40 prediction.source_frame_index",
)
stratum = row.get("source_stratum")
severity = row.get("severity")
split = row.get("split")
if (
row.get("schema_version") != E40_PREDICTION_SCHEMA
or not isinstance(item_id, str)
or _ITEM_ID.fullmatch(item_id) is None
or not isinstance(review_key, str)
or not review_key
or stratum not in _STRATA
or severity not in _SEVERITIES
or split not in _SPLITS
or row.get("scored") is not (split == "validation")
or row.get("authority") != _AUTHORITY
or sequence in sequences
or item_id in item_ids
):
raise E40PerceptionProductGateError(
"E40 prediction identity is invalid"
)
prediction = _dimension_state(
row.get("prediction"),
"E40 prediction.prediction",
)
reference = _dimension_state(
row.get("reference"),
"E40 prediction.reference",
)
rows.append(
{
"sequence": sequence,
"item_id": item_id,
"review_key": review_key,
"source_frame_index": source_frame_index,
"source_stratum": stratum,
"severity": severity,
"split": split,
"prediction": prediction,
"reference": reference,
"presence_confidence": _bounded_number(
row.get("presence_confidence"),
"E40 prediction.presence_confidence",
),
}
)
sequences.add(sequence)
item_ids.add(item_id)
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E40PerceptionProductGateError(
"E40 sealed predictions are invalid"
) from exc
validation_total = _object(
result.report.get("metrics"),
"E40 metrics",
).get("validation_items")
if (
not rows
or _nonnegative_integer(validation_total, "E40 validation_items")
!= sum(row["split"] == "validation" for row in rows)
):
raise E40PerceptionProductGateError(
"E40 sealed validation denominator changed"
)
return tuple(rows)
def _materialization_id(result: E40PerceptionProductGate) -> str:
identity = _object(result.manifest.get("identity"), "E40 identity")
source = _object(identity.get("source"), "E40 source")
value = source.get("materialization_id")
if not isinstance(value, str) or _MATERIALIZATION_ID.fullmatch(value) is None:
raise E40PerceptionProductGateError(
"E40 materialization binding is invalid"
)
return value
def _case(row: dict[str, Any]) -> dict[str, object]:
reference = _object(row["reference"], "E40 reference")
prediction = _object(row["prediction"], "E40 prediction")
source_stratum = str(row["source_stratum"])
mismatched = [
dimension
for dimension in _DIMENSIONS
if reference[dimension] != prediction[dimension]
]
return {
"item_id": row["item_id"],
"sequence": row["sequence"],
"source_frame_index": row["source_frame_index"],
"source_stratum": source_stratum,
"severity": row["severity"],
"presence_confidence": row["presence_confidence"],
"prediction_basis": (
"camera-only-softmax"
if source_stratum == "camera-only"
else "fixed-stratum-policy"
),
"reference": dict(reference),
"prediction": dict(prediction),
"mismatched_dimensions": mismatched,
"access": "read-only",
}
def _load_catalog(
root_provider: RootProvider,
result_id: str,
) -> tuple[dict[str, object], E40OperatorReviewSubstrate]:
root = _configured_root(root_provider)
if root is None:
raise HTTPException(status_code=404, detail="E40 result не найден")
candidate = _candidate(root, result_id)
result = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
rows = _read_predictions(result)
errors = [
row
for row in rows
if row["split"] == "validation"
and any(
row["reference"][dimension] != row["prediction"][dimension]
for dimension in _DIMENSIONS
)
]
errors.sort(
key=lambda row: (
_SEVERITY_ORDER[str(row["severity"])],
int(row["sequence"]),
)
)
if not errors or len(errors) > 64:
raise E40PerceptionProductGateError(
"E40 error catalog is outside the review boundary"
)
materialization_id = _materialization_id(result)
cases = [_case(row) for row in errors]
case_catalog_sha256 = hashlib.sha256(
json.dumps(
{
"result_id": result.result_id,
"materialization_id": materialization_id,
"items": cases,
},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
).hexdigest()
catalog: dict[str, object] = {
"schema_version": LABORATORY_E40_CASE_CATALOG_SCHEMA,
"result_id": result.result_id,
"materialization_id": materialization_id,
"items": cases,
"total": len(errors),
"truncated": False,
"access": "read-only",
}
substrate = E40OperatorReviewSubstrate(
result_id=result.result_id,
materialization_id=materialization_id,
case_catalog_sha256=case_catalog_sha256,
subjects=tuple(
E40OperatorReviewSubject(
item_id=str(row["item_id"]),
sequence=int(row["sequence"]),
)
for row in errors
),
)
return catalog, substrate
def build_e40_case_review_router(
*,
e40_root_provider: RootProvider = lambda: None,
operator_review_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/e40", tags=["laboratory"])
def catalog_and_source(
result_id: str,
) -> tuple[dict[str, object], E40OperatorReviewSubstrate]:
try:
return _load_catalog(e40_root_provider, result_id)
except HTTPException:
raise
except (
E40PerceptionProductGateError,
E40OperatorReviewValidationError,
KeyError,
OSError,
TypeError,
ValueError,
) as exc:
raise HTTPException(
status_code=409,
detail="E40 case-review не прошёл проверку целостности",
) from exc
def store() -> E40OperatorReviewStore:
root = operator_review_root_provider()
if root is None:
raise HTTPException(
status_code=503,
detail="E40 operator review не настроен",
)
try:
return E40OperatorReviewStore(root=root)
except (E40OperatorReviewIntegrityError, OSError) as exc:
raise HTTPException(
status_code=503,
detail="E40 operator review storage недоступен",
) from exc
def invoke(
operation: Callable[[], dict[str, object]],
) -> dict[str, object]:
try:
return operation()
except E40OperatorReviewValidationError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
except E40OperatorReviewConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except (E40OperatorReviewIntegrityError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="E40 operator review не прошёл проверку целостности",
) from exc
@router.get("/results/{result_id}/cases")
def list_e40_error_cases(
result_id: str,
limit: int = Query(default=48, ge=1, le=64),
) -> dict[str, object]:
catalog, _ = catalog_and_source(result_id)
items = list(catalog["items"]) # type: ignore[arg-type]
return {
**catalog,
"items": items[:limit],
"truncated": len(items) > limit,
}
@router.get("/results/{result_id}/operator-review")
def get_operator_review(
result_id: str,
reviewer_id: str = Query(
default="DC",
min_length=1,
max_length=128,
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$",
),
) -> dict[str, object]:
_, substrate = catalog_and_source(result_id)
review_store = store()
return invoke(
lambda: review_store.get(
substrate=substrate,
reviewer_id=reviewer_id,
)
)
@router.put(
"/results/{result_id}/operator-review/decisions/{item_id}"
)
def record_operator_verdict(
result_id: str,
item_id: Annotated[
str,
ApiPath(pattern=r"^e30-review-item-[a-f0-9]{64}$"),
],
request: E40OperatorVerdictRequest,
) -> dict[str, object]:
_, substrate = catalog_and_source(result_id)
review_store = store()
return invoke(
lambda: review_store.record_verdict(
substrate=substrate,
reviewer_id=request.reviewer_id,
item_id=item_id,
expected_revision=request.expected_revision,
idempotency_key=request.idempotency_key,
verdict=request.verdict,
)
)
return router