feat(lab): complete E30 evidence review gate

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 11:00:32 +03:00
parent a44d7627fd
commit 001d597a89
55 changed files with 15897 additions and 1548 deletions
+85
View File
@@ -33,6 +33,9 @@ from k1link.sessions import (
SessionStore,
)
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.environment_api import build_environment_router
from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.lidar_api import build_lidar_router
@@ -493,6 +496,88 @@ app.include_router(
),
)
)
app.include_router(
build_e30_review_router(
materialization_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "materializations"
),
review_pack_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "review-packs"
),
)
)
app.include_router(
build_e30_engineering_router(
generation_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "engineering-generations"
),
materialization_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "materializations"
),
review_pack_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "review-packs"
),
)
)
app.include_router(
build_e30_human_review_router(
materialization_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "materializations"
),
review_pack_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "review-packs"
),
engineering_generation_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "engineering-generations"
),
draft_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "human-review-drafts"
),
generation_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e30"
/ "human-review-generations"
),
)
)
frontend_dist = REPOSITORY_ROOT / "apps" / "control-station" / "dist"
+690
View File
@@ -0,0 +1,690 @@
from __future__ import annotations
import copy
import hashlib
import json
import math
import re
from collections import Counter
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query
from k1link.compute.e30_engineering_generation import (
E30_ENGINEERING_CAUSES_SCHEMA,
E30_ENGINEERING_DECISION_SCHEMA,
E30_ENGINEERING_DECISION_SCHEMAS,
E30_ENGINEERING_EXCEPTION_SCHEMA,
E30_ENGINEERING_EXCEPTION_SCHEMAS,
E30_ENGINEERING_GENERATION_SCHEMA,
E30_ENGINEERING_SUMMARY_SCHEMA,
EXCEPTION_DISPOSITIONS,
)
from k1link.web.e30_review_api import (
E30ReviewEvidenceError,
e30_review_item_summary,
load_verified_e30_review,
)
LABORATORY_E30_ENGINEERING_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-e30-engineering-generations/v1"
)
LABORATORY_E30_ENGINEERING_DECISION_SCHEMA: Final = (
"missioncore.laboratory-e30-engineering-decision/v1"
)
LABORATORY_E30_ENGINEERING_EXCEPTIONS_SCHEMA: Final = (
"missioncore.laboratory-e30-engineering-exceptions/v1"
)
_GENERATION_ID = re.compile(r"^e30-engineering-generation-[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}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_MAX_MANIFEST_BYTES: Final = 1024 * 1024
_MAX_DECISIONS_BYTES: Final = 8 * 1024 * 1024
_MAX_AUXILIARY_BYTES: Final = 2 * 1024 * 1024
RootProvider = Callable[[], Path | None]
class E30EngineeringEvidenceError(ValueError):
"""An engineering generation is incomplete, changed, or incompatible."""
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _read_json(path: Path, maximum_bytes: int) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise E30EngineeringEvidenceError("engineering JSON is unavailable")
size = path.stat().st_size
if not 0 < size <= maximum_bytes:
raise E30EngineeringEvidenceError("engineering JSON is out of bounds")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E30EngineeringEvidenceError("engineering JSON is invalid") from exc
if not isinstance(value, dict):
raise E30EngineeringEvidenceError("engineering JSON must be an object")
return value
def _safe_file(root: Path, value: object) -> Path:
if not isinstance(value, str) or not value or Path(value).is_absolute():
raise E30EngineeringEvidenceError("engineering artifact path is invalid")
if ".." in Path(value).parts:
raise E30EngineeringEvidenceError("engineering artifact escaped its root")
candidate = root / value
if candidate.is_symlink():
raise E30EngineeringEvidenceError("engineering artifact is a symlink")
candidate = candidate.resolve(strict=True)
try:
candidate.relative_to(root)
except ValueError as exc:
raise E30EngineeringEvidenceError(
"engineering artifact escaped its root"
) from exc
if not candidate.is_file():
raise E30EngineeringEvidenceError("engineering artifact is unavailable")
return candidate
def _artifact(
root: Path,
value: object,
*,
allow_empty: bool = False,
) -> tuple[Path, dict[str, Any]]:
if not isinstance(value, dict):
raise E30EngineeringEvidenceError("engineering artifact metadata is invalid")
byte_length = value.get("byte_length")
digest = value.get("sha256")
if (
not isinstance(byte_length, int)
or isinstance(byte_length, bool)
or byte_length < (0 if allow_empty else 1)
or not isinstance(digest, str)
or _SHA256.fullmatch(digest) is None
):
raise E30EngineeringEvidenceError("engineering artifact metadata is invalid")
path = _safe_file(root, value.get("path"))
if path.stat().st_size != byte_length or _sha256(path) != digest:
raise E30EngineeringEvidenceError("engineering artifact content changed")
return path, value
def _read_jsonl(path: Path, maximum_bytes: int) -> list[dict[str, Any]]:
if path.stat().st_size > maximum_bytes:
raise E30EngineeringEvidenceError("engineering JSONL is out of bounds")
values: list[dict[str, Any]] = []
with path.open("r", encoding="utf-8") as stream:
for line in stream:
try:
value = json.loads(line)
except json.JSONDecodeError as exc:
raise E30EngineeringEvidenceError(
"engineering JSONL is invalid"
) from exc
if not isinstance(value, dict):
raise E30EngineeringEvidenceError(
"engineering JSONL row must be an object"
)
values.append(value)
return values
def _signature(root: Path) -> tuple[int, ...]:
paths = [
root / "manifest.json",
root / "engineering-decisions.jsonl",
root / "human-exceptions.jsonl",
root / "summary.json",
root / "cause-distribution.json",
]
signature: list[int] = []
for path in paths:
stat = path.stat()
signature.extend((stat.st_size, stat.st_mtime_ns))
return tuple(signature)
def _authority(value: object) -> None:
if (
not isinstance(value, dict)
or value.get("commands_enabled") is not False
or value.get("navigation_or_safety_accepted") is not False
):
raise E30EngineeringEvidenceError(
"engineering generation must remain diagnostic-only"
)
def _verify_materialization_binding(
*,
materialization_root: Path,
source: dict[str, Any],
) -> None:
result_id = source.get("materialization_id")
if (
not isinstance(result_id, str)
or _MATERIALIZATION_ID.fullmatch(result_id) is None
):
raise E30EngineeringEvidenceError("materialization binding is invalid")
candidate = materialization_root / result_id
manifest_path = candidate / "manifest.json"
if candidate.is_symlink() or not candidate.is_dir():
raise E30EngineeringEvidenceError("materialization binding is unavailable")
manifest = _read_json(manifest_path, _MAX_MANIFEST_BYTES)
if (
manifest.get("result_id") != result_id
or manifest.get("identity_sha256")
!= source.get("materialization_identity_sha256")
or _sha256(manifest_path) != source.get("materialization_manifest_sha256")
or _sha256(candidate / "materialized-items.jsonl")
!= source.get("materialization_index_sha256")
):
raise E30EngineeringEvidenceError("materialization binding changed")
def _validate_decisions(
values: list[dict[str, Any]],
) -> tuple[dict[str, object], dict[str, object]]:
if (
len(values) != 486
or len({value.get("item_id") for value in values}) != len(values)
):
raise E30EngineeringEvidenceError("engineering coverage is incomplete")
verdicts: Counter[str] = Counter()
detector: Counter[str] = Counter()
projection: Counter[str] = Counter()
ownership: Counter[str] = Counter()
causes: Counter[str] = Counter()
exceptions = 0
confidence_total = 0.0
for sequence, value in enumerate(values):
confidence = value.get("confidence")
schema_version = value.get("schema_version")
exception_required = value.get("human_exception_required")
review_prompt = value.get("review_prompt")
if (
schema_version not in E30_ENGINEERING_DECISION_SCHEMAS
or value.get("sequence") != sequence
or _ITEM_ID.fullmatch(str(value.get("item_id"))) is None
or not isinstance(value.get("verdict"), str)
or not isinstance(value.get("detector_assessment"), str)
or not isinstance(value.get("projection_assessment"), str)
or not isinstance(value.get("point_ownership"), str)
or not isinstance(confidence, (int, float))
or isinstance(confidence, bool)
or not math.isfinite(float(confidence))
or not isinstance(exception_required, bool)
):
raise E30EngineeringEvidenceError("engineering decision is invalid")
if schema_version == E30_ENGINEERING_DECISION_SCHEMA and (
exception_required != _valid_review_prompt(review_prompt)
):
raise E30EngineeringEvidenceError(
"engineering exception prompt is invalid"
)
verdicts[value["verdict"]] += 1
detector[value["detector_assessment"]] += 1
projection[value["projection_assessment"]] += 1
ownership[value["point_ownership"]] += 1
if isinstance(value.get("cause_code"), str):
causes[value["cause_code"]] += 1
if value["human_exception_required"]:
exceptions += 1
confidence_total += float(confidence)
summary: dict[str, object] = {
"schema_version": E30_ENGINEERING_SUMMARY_SCHEMA,
"item_count": len(values),
"reviewed_item_count": len(values),
"verdict_distribution": dict(sorted(verdicts.items())),
"detector_distribution": dict(sorted(detector.items())),
"projection_distribution": dict(sorted(projection.items())),
"point_ownership_distribution": dict(sorted(ownership.items())),
"human_exception_count": exceptions,
"mean_confidence": round(confidence_total / len(values), 4),
}
cause_document: dict[str, object] = {
"schema_version": E30_ENGINEERING_CAUSES_SCHEMA,
"item_count_with_cause": sum(causes.values()),
"reasons": [
{"reason_code": reason, "count": count}
for reason, count in sorted(causes.items())
],
}
return summary, cause_document
def _valid_review_prompt(value: object) -> bool:
if not isinstance(value, dict) or set(value) != {"question", "focus", "effects"}:
return False
effects = value.get("effects")
return (
isinstance(value.get("question"), str)
and bool(value["question"].strip())
and isinstance(value.get("focus"), str)
and bool(value["focus"].strip())
and isinstance(effects, dict)
and set(effects) == set(EXCEPTION_DISPOSITIONS)
and all(
isinstance(effects.get(disposition), str)
and bool(effects[disposition].strip())
for disposition in EXCEPTION_DISPOSITIONS
)
)
@lru_cache(maxsize=8)
def _generation_cached(
root_text: str,
materialization_root_text: str,
signature: tuple[int, ...],
) -> tuple[dict[str, object], tuple[dict[str, Any], ...]]:
del signature
root = Path(root_text)
materialization_root = Path(materialization_root_text)
if (
root.is_symlink()
or not root.is_dir()
or _GENERATION_ID.fullmatch(root.name) is None
):
raise E30EngineeringEvidenceError("engineering generation id is invalid")
manifest = _read_json(root / "manifest.json", _MAX_MANIFEST_BYTES)
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != E30_ENGINEERING_GENERATION_SCHEMA
or manifest.get("result_id") != root.name
or not isinstance(identity, dict)
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or root.name != f"e30-engineering-generation-{identity_sha256}"
or manifest.get("ai_review_complete") is not True
or manifest.get("human_review_complete") is not False
or manifest.get("lab_published") is not False
):
raise E30EngineeringEvidenceError(
"engineering generation identity is invalid"
)
_authority(manifest.get("authority"))
source = identity.get("source")
producer = identity.get("producer")
if (
not isinstance(source, dict)
or not isinstance(producer, dict)
or producer.get("kind") != "ai-assisted-engineering-review"
or producer.get("claims_human_ground_truth") is not False
):
raise E30EngineeringEvidenceError("engineering provenance is invalid")
_verify_materialization_binding(
materialization_root=materialization_root,
source=source,
)
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list):
raise E30EngineeringEvidenceError("engineering artifacts are invalid")
by_role = {
str(value.get("role")): value
for value in artifacts
if isinstance(value, dict)
}
required_roles = {
"engineering-decisions",
"human-exceptions",
"engineering-summary",
"cause-distribution",
}
if set(by_role) != required_roles or len(artifacts) != len(required_roles):
raise E30EngineeringEvidenceError("engineering artifacts are incomplete")
decisions_path, decisions_artifact = _artifact(
root,
by_role["engineering-decisions"],
)
exceptions_path, _ = _artifact(
root,
by_role["human-exceptions"],
allow_empty=True,
)
summary_path, _ = _artifact(root, by_role["engineering-summary"])
causes_path, _ = _artifact(root, by_role["cause-distribution"])
decisions = _read_jsonl(decisions_path, _MAX_DECISIONS_BYTES)
exceptions = _read_jsonl(exceptions_path, _MAX_AUXILIARY_BYTES)
for exception in exceptions:
exception_schema = exception.get("schema_version")
if (
exception_schema not in E30_ENGINEERING_EXCEPTION_SCHEMAS
or (
exception_schema == E30_ENGINEERING_EXCEPTION_SCHEMA
and not _valid_review_prompt(exception.get("review_prompt"))
)
):
raise E30EngineeringEvidenceError(
"engineering exception contract is invalid"
)
calculated_summary, calculated_causes = _validate_decisions(decisions)
summary = _read_json(summary_path, _MAX_AUXILIARY_BYTES)
causes = _read_json(causes_path, _MAX_AUXILIARY_BYTES)
if (
summary != calculated_summary
or causes != calculated_causes
or manifest.get("summary") != summary
or manifest.get("cause_distribution") != causes
or len(exceptions) != summary["human_exception_count"]
or identity.get("decision_content_sha256")
!= decisions_artifact.get("sha256")
or source.get("item_count") != len(decisions)
):
raise E30EngineeringEvidenceError("engineering summary changed")
exception_ids = {value.get("item_id") for value in exceptions}
expected_exception_ids = {
value["item_id"]
for value in decisions
if value["human_exception_required"]
}
if exception_ids != expected_exception_ids:
raise E30EngineeringEvidenceError("engineering exception queue changed")
catalog_item: dict[str, object] = {
"generation_id": root.name,
"created_at_utc": manifest.get("created_at_utc"),
"materialization_id": source["materialization_id"],
"producer": copy.deepcopy(producer),
"summary": copy.deepcopy(summary),
"cause_distribution": copy.deepcopy(causes),
"human_exceptions": [
{
"item_id": value["item_id"],
"review_key": value["review_key"],
"source_stratum": value["source_stratum"],
"confidence": value["confidence"],
"review_prompt": copy.deepcopy(value.get("review_prompt")),
}
for value in exceptions
],
"ai_review_complete": True,
"human_exception_complete": (
manifest.get("human_exception_complete") is True
),
"human_review_complete": False,
"lab_published": False,
"access": "read-only",
"authority": copy.deepcopy(manifest["authority"]),
}
return catalog_item, tuple(decisions)
def load_verified_e30_engineering_generation(
*,
generation_root: Path,
materialization_root: Path,
result_id: str,
generation_id: str,
) -> tuple[dict[str, object], tuple[dict[str, Any], ...]]:
generation_root = generation_root.resolve()
materialization_root = materialization_root.resolve()
if (
not generation_root.is_dir()
or not materialization_root.is_dir()
or _MATERIALIZATION_ID.fullmatch(result_id) is None
or _GENERATION_ID.fullmatch(generation_id) is None
):
raise E30EngineeringEvidenceError(
"engineering generation is unavailable"
)
candidate = generation_root / generation_id
if candidate.is_symlink() or not candidate.is_dir():
raise E30EngineeringEvidenceError(
"engineering generation is unavailable"
)
catalog_item, decisions = _generation_cached(
str(candidate.resolve()),
str(materialization_root),
_signature(candidate),
)
if catalog_item["materialization_id"] != result_id:
raise E30EngineeringEvidenceError(
"engineering generation materialization differs"
)
return copy.deepcopy(catalog_item), decisions
def build_e30_engineering_router(
*,
generation_root_provider: RootProvider = lambda: None,
materialization_root_provider: RootProvider = lambda: None,
review_pack_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/e30", tags=["laboratory"])
def roots() -> tuple[Path, Path] | None:
generation_root = generation_root_provider()
materialization_root = materialization_root_provider()
if generation_root is None or materialization_root is None:
return None
generation_root = generation_root.resolve()
materialization_root = materialization_root.resolve()
if not generation_root.is_dir() or not materialization_root.is_dir():
return None
return generation_root, materialization_root
@router.get("/reviews/{result_id}/engineering-generations")
def list_engineering_generations(
result_id: str,
limit: int = Query(default=1, ge=1, le=10),
) -> dict[str, object]:
configured_roots = roots()
if (
configured_roots is None
or _MATERIALIZATION_ID.fullmatch(result_id) is None
):
return {
"schema_version": LABORATORY_E30_ENGINEERING_CATALOG_SCHEMA,
"configured": configured_roots is not None,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
generation_root, materialization_root = configured_roots
candidates = sorted(
(
candidate
for candidate in generation_root.iterdir()
if candidate.is_dir()
and _GENERATION_ID.fullmatch(candidate.name) is not None
),
key=lambda candidate: candidate.stat().st_mtime_ns,
reverse=True,
)
items: list[dict[str, object]] = []
invalid_total = 0
matching_total = 0
for candidate in candidates:
try:
item, _ = _generation_cached(
str(candidate.resolve()),
str(materialization_root),
_signature(candidate),
)
if item["materialization_id"] != result_id:
continue
matching_total += 1
if len(items) < limit:
items.append(copy.deepcopy(item))
except (E30EngineeringEvidenceError, OSError):
invalid_total += 1
return {
"schema_version": LABORATORY_E30_ENGINEERING_CATALOG_SCHEMA,
"configured": True,
"items": items,
"candidate_total": matching_total,
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get(
"/reviews/{result_id}/engineering-generations/"
"{generation_id}/exceptions"
)
def list_engineering_exceptions(
result_id: str,
generation_id: str,
limit: int = Query(default=48, ge=1, le=128),
cursor: int = Query(default=0, ge=0),
) -> dict[str, object]:
configured_roots = roots()
review_pack_root = review_pack_root_provider()
if (
configured_roots is None
or review_pack_root is None
or not review_pack_root.is_dir()
or _MATERIALIZATION_ID.fullmatch(result_id) is None
or _GENERATION_ID.fullmatch(generation_id) is None
):
raise HTTPException(
status_code=404,
detail="A3 engineering exception queue не найдена",
)
generation_root, materialization_root = configured_roots
candidate = generation_root / generation_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(
status_code=404,
detail="A3 engineering exception queue не найдена",
)
try:
catalog_item, _ = _generation_cached(
str(candidate.resolve()),
str(materialization_root),
_signature(candidate),
)
if catalog_item["materialization_id"] != result_id:
raise HTTPException(
status_code=404,
detail="A3 engineering exception queue не найдена",
)
_, rows, _ = load_verified_e30_review(
materialization_root=materialization_root,
review_pack_root=review_pack_root,
result_id=result_id,
)
rows_by_id = {row["item_id"]: row for row in rows}
exception_ids = [
value["item_id"]
for value in catalog_item["human_exceptions"]
]
if any(item_id not in rows_by_id for item_id in exception_ids):
raise E30EngineeringEvidenceError(
"engineering exception source item is unavailable"
)
page_ids = exception_ids[cursor : cursor + limit]
next_cursor = cursor + len(page_ids)
return {
"schema_version": LABORATORY_E30_ENGINEERING_EXCEPTIONS_SCHEMA,
"result_id": result_id,
"generation_id": generation_id,
"items": [
e30_review_item_summary(rows_by_id[item_id])
for item_id in page_ids
],
"total": len(exception_ids),
"next_cursor": (
next_cursor if next_cursor < len(exception_ids) else None
),
"access": "read-only",
}
except HTTPException:
raise
except (
E30EngineeringEvidenceError,
E30ReviewEvidenceError,
OSError,
) as exc:
raise HTTPException(
status_code=409,
detail="A3 engineering exception queue не прошла проверку целостности",
) from exc
@router.get(
"/reviews/{result_id}/engineering-generations/"
"{generation_id}/items/{item_id}"
)
def get_engineering_decision(
result_id: str,
generation_id: str,
item_id: str,
) -> dict[str, object]:
configured_roots = roots()
if (
configured_roots is None
or _MATERIALIZATION_ID.fullmatch(result_id) is None
or _GENERATION_ID.fullmatch(generation_id) is None
or _ITEM_ID.fullmatch(item_id) is None
):
raise HTTPException(
status_code=404,
detail="A3 engineering decision не найден",
)
generation_root, materialization_root = configured_roots
candidate = generation_root / generation_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(
status_code=404,
detail="A3 engineering decision не найден",
)
try:
catalog_item, decisions = _generation_cached(
str(candidate.resolve()),
str(materialization_root),
_signature(candidate),
)
if catalog_item["materialization_id"] != result_id:
raise HTTPException(
status_code=404,
detail="A3 engineering decision не найден",
)
decision = next(
(value for value in decisions if value["item_id"] == item_id),
None,
)
if decision is None:
raise HTTPException(
status_code=404,
detail="A3 engineering decision не найден",
)
return {
"schema_version": LABORATORY_E30_ENGINEERING_DECISION_SCHEMA,
"generation_id": generation_id,
"decision": copy.deepcopy(decision),
"access": "read-only",
}
except HTTPException:
raise
except (E30EngineeringEvidenceError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="A3 engineering generation не прошла проверку целостности",
) from exc
return router
+263
View File
@@ -0,0 +1,263 @@
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Annotated, Literal
from fastapi import APIRouter, HTTPException
from fastapi import Path as ApiPath
from pydantic import BaseModel, ConfigDict, Field
from k1link.compute.e30_human_review import (
E30HumanReviewConflictError,
E30HumanReviewIntegrityError,
E30HumanReviewNotFoundError,
E30HumanReviewStore,
E30HumanReviewValidationError,
E30ExceptionDisposition,
E30ReviewSubject,
E30ReviewSubstrate,
)
from k1link.web.e30_engineering_api import (
E30EngineeringEvidenceError,
load_verified_e30_engineering_generation,
)
from k1link.web.e30_review_api import (
E30ReviewEvidenceError,
load_verified_e30_review,
)
RootProvider = Callable[[], Path | None]
class E30HumanReviewCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
reviewer_id: str = Field(min_length=1, max_length=128)
engineering_generation_id: str = Field(
pattern=r"^e30-engineering-generation-[a-f0-9]{64}$"
)
class E30HumanReviewDecisionRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
expected_revision: int = Field(ge=0)
idempotency_key: str = Field(min_length=1, max_length=128)
disposition: E30ExceptionDisposition
notes: str | None = Field(default=None, max_length=2_000)
class E30HumanReviewFinalizeRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
expected_revision: int = Field(ge=0)
confirm_generation: Literal[True]
def build_e30_human_review_router(
*,
materialization_root_provider: RootProvider = lambda: None,
review_pack_root_provider: RootProvider = lambda: None,
engineering_generation_root_provider: RootProvider = lambda: None,
draft_root_provider: RootProvider = lambda: None,
generation_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/e30", tags=["laboratory"])
def source(
result_id: str,
engineering_generation_id: str,
) -> E30ReviewSubstrate:
materialization_root = materialization_root_provider()
review_pack_root = review_pack_root_provider()
engineering_generation_root = engineering_generation_root_provider()
if (
materialization_root is None
or review_pack_root is None
or engineering_generation_root is None
):
raise HTTPException(status_code=404, detail="E30 review не найден")
try:
_, _, source_substrate = load_verified_e30_review(
materialization_root=materialization_root,
review_pack_root=review_pack_root,
result_id=result_id,
)
generation, _ = load_verified_e30_engineering_generation(
generation_root=engineering_generation_root,
materialization_root=materialization_root,
result_id=result_id,
generation_id=engineering_generation_id,
)
subjects_by_id = {
subject.item_id: subject
for subject in source_substrate.subjects
}
exception_ids = [
value["item_id"]
for value in generation["human_exceptions"]
]
if (
not exception_ids
or any(item_id not in subjects_by_id for item_id in exception_ids)
):
raise E30HumanReviewValidationError(
"engineering exception substrate is invalid"
)
return E30ReviewSubstrate(
materialization_id=source_substrate.materialization_id,
materialization_identity_sha256=(
source_substrate.materialization_identity_sha256
),
review_pack_id=source_substrate.review_pack_id,
review_items_sha256=source_substrate.review_items_sha256,
reason_taxonomy=(),
subjects=tuple(
E30ReviewSubject(
item_id=item_id,
sequence=sequence,
source_stratum=subjects_by_id[item_id].source_stratum,
)
for sequence, item_id in enumerate(exception_ids)
),
engineering_generation_id=engineering_generation_id,
)
except (
E30EngineeringEvidenceError,
E30ReviewEvidenceError,
E30HumanReviewValidationError,
OSError,
) as exc:
raise HTTPException(
status_code=409,
detail="E30 source evidence не прошло проверку целостности",
) from exc
def store() -> E30HumanReviewStore:
draft_root = draft_root_provider()
generation_root = generation_root_provider()
if draft_root is None or generation_root is None:
raise HTTPException(
status_code=503,
detail="A3 human review не настроен",
)
try:
return E30HumanReviewStore(
draft_root=draft_root,
generation_root=generation_root,
)
except (E30HumanReviewIntegrityError, OSError) as exc:
raise HTTPException(
status_code=503,
detail="A3 human review storage недоступен",
) from exc
def invoke(
operation: Callable[[], dict[str, object]],
) -> dict[str, object]:
try:
return operation()
except E30HumanReviewNotFoundError as exc:
raise HTTPException(
status_code=404,
detail="A3 human review draft не найден",
) from exc
except E30HumanReviewValidationError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
except E30HumanReviewConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except (E30HumanReviewIntegrityError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="A3 human review не прошло проверку целостности",
) from exc
@router.post("/reviews/{result_id}/human-review")
def create_or_resume_human_review(
result_id: str,
request: E30HumanReviewCreateRequest,
) -> dict[str, object]:
substrate = source(result_id, request.engineering_generation_id)
review_store = store()
return invoke(
lambda: review_store.create_or_resume(
substrate=substrate,
reviewer_id=request.reviewer_id,
)
)
@router.get("/reviews/{result_id}/human-review/{draft_id}")
def get_human_review(
result_id: str,
draft_id: Annotated[
str,
ApiPath(pattern=r"^e30-human-draft-[a-f0-9]{64}$"),
],
engineering_generation_id: str,
) -> dict[str, object]:
substrate = source(result_id, engineering_generation_id)
review_store = store()
return invoke(
lambda: review_store.get(draft_id=draft_id, substrate=substrate)
)
@router.put(
"/reviews/{result_id}/human-review/{draft_id}/decisions/{item_id}"
)
def record_human_review_decision(
result_id: str,
draft_id: Annotated[
str,
ApiPath(pattern=r"^e30-human-draft-[a-f0-9]{64}$"),
],
item_id: Annotated[
str,
ApiPath(pattern=r"^e30-review-item-[a-f0-9]{64}$"),
],
request: E30HumanReviewDecisionRequest,
engineering_generation_id: str,
) -> dict[str, object]:
substrate = source(result_id, engineering_generation_id)
review_store = store()
return invoke(
lambda: review_store.record_decision(
draft_id=draft_id,
substrate=substrate,
item_id=item_id,
expected_revision=request.expected_revision,
idempotency_key=request.idempotency_key,
disposition=request.disposition,
notes=request.notes,
)
)
@router.post("/reviews/{result_id}/human-review/{draft_id}/finalize")
def finalize_human_review(
result_id: str,
draft_id: Annotated[
str,
ApiPath(pattern=r"^e30-human-draft-[a-f0-9]{64}$"),
],
request: E30HumanReviewFinalizeRequest,
engineering_generation_id: str,
) -> dict[str, object]:
substrate = source(result_id, engineering_generation_id)
review_store = store()
generation = invoke(
lambda: review_store.finalize(
draft_id=draft_id,
substrate=substrate,
expected_revision=request.expected_revision,
)
)
draft = invoke(
lambda: review_store.get(draft_id=draft_id, substrate=substrate)
)
return {
"schema_version": "missioncore.laboratory-e30-human-review-finalized/v2",
"draft": draft,
"generation": generation,
}
return router
+818
View File
@@ -0,0 +1,818 @@
from __future__ import annotations
import copy
import hashlib
import json
import re
from collections import Counter
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Annotated, Any, Final, Literal, cast
import numpy as np
import numpy.typing as npt
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import FileResponse
from k1link.compute.e30_human_review import (
E30ReviewSubject,
E30ReviewSubstrate,
E30Stratum,
)
from k1link.compute.e30_materialization import (
E30_MATERIALIZATION_INDEX_NAME,
E30_MATERIALIZATION_ITEM_SCHEMA,
E30_MATERIALIZATION_MANIFEST_NAME,
E30_MATERIALIZATION_SCHEMA,
)
LABORATORY_E30_CATALOG_SCHEMA: Final = "missioncore.laboratory-e30-catalog/v1"
LABORATORY_E30_ITEMS_SCHEMA: Final = "missioncore.laboratory-e30-items/v1"
LABORATORY_E30_ITEM_DETAIL_SCHEMA: Final = (
"missioncore.laboratory-e30-item-detail/v1"
)
_MATERIALIZATION_ID = re.compile(r"^e30-materialization-[a-f0-9]{64}$")
_REVIEW_PACK_ID = re.compile(r"^e30-review-pack-[a-f0-9]{64}$")
_REVIEW_ITEM_ID = re.compile(r"^e30-review-item-[a-f0-9]{64}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_STRATA: Final = ("conflict", "agree", "camera-only", "unknown", "geometry-only")
_MAX_MANIFEST_BYTES: Final = 512 * 1024
_MAX_INDEX_BYTES: Final = 8 * 1024 * 1024
_MAX_ITEM_BYTES: Final = 8 * 1024 * 1024
_MAX_CAMERA_FRAME_BYTES: Final = 8 * 1024 * 1024
RootProvider = Callable[[], Path | None]
Stratum = Literal["conflict", "agree", "camera-only", "unknown", "geometry-only"]
class E30ReviewEvidenceError(ValueError):
"""E30 review evidence is incomplete, changed or incompatible."""
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _read_json(path: Path, maximum_bytes: int) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise E30ReviewEvidenceError("E30 JSON artifact is unavailable")
size = path.stat().st_size
if not 0 < size <= maximum_bytes:
raise E30ReviewEvidenceError("E30 JSON artifact is out of bounds")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E30ReviewEvidenceError("E30 JSON artifact is invalid") from exc
if not isinstance(value, dict):
raise E30ReviewEvidenceError("E30 JSON artifact must be an object")
return value
def _safe_relative_file(root: Path, value: object) -> Path:
if not isinstance(value, str) or not value or Path(value).is_absolute():
raise E30ReviewEvidenceError("E30 artifact path is invalid")
if ".." in Path(value).parts:
raise E30ReviewEvidenceError("E30 artifact path escaped its root")
candidate = root / value
if candidate.is_symlink():
raise E30ReviewEvidenceError("E30 artifact must not be a symlink")
candidate = candidate.resolve(strict=True)
try:
candidate.relative_to(root)
except ValueError as exc:
raise E30ReviewEvidenceError("E30 artifact escaped its root") from exc
if not candidate.is_file():
raise E30ReviewEvidenceError("E30 artifact is unavailable")
return candidate
def _artifact(
root: Path,
value: object,
*,
expected_role: str | None = None,
) -> tuple[Path, dict[str, Any]]:
if not isinstance(value, dict):
raise E30ReviewEvidenceError("E30 artifact metadata is invalid")
role = value.get("role")
byte_length = value.get("byte_length")
digest = value.get("sha256")
if (
(expected_role is not None and role != expected_role)
or not isinstance(byte_length, int)
or byte_length <= 0
or not isinstance(digest, str)
or _SHA256.fullmatch(digest) is None
):
raise E30ReviewEvidenceError("E30 artifact metadata is invalid")
path = _safe_relative_file(root, value.get("path"))
if path.stat().st_size != byte_length or _sha256(path) != digest:
raise E30ReviewEvidenceError("E30 artifact content changed")
return path, value
def _authority(value: object) -> None:
if (
not isinstance(value, dict)
or value.get("commands_enabled") is not False
or value.get("navigation_or_safety_accepted") is not False
):
raise E30ReviewEvidenceError("E30 evidence must remain diagnostic-only")
def _signature(root: Path) -> tuple[int, ...]:
paths = [
root / E30_MATERIALIZATION_MANIFEST_NAME,
root / E30_MATERIALIZATION_INDEX_NAME,
*sorted((root / "items").glob("*.npz")),
*sorted((root / "frames").glob("*.jpg")),
]
signature: list[int] = []
for path in paths:
stat = path.stat()
signature.extend((stat.st_size, stat.st_mtime_ns))
return tuple(signature)
def _review_pack(
root: Path,
*,
expected_id: str,
expected_items_sha256: str,
) -> dict[str, Any]:
candidate = root / expected_id
if (
_REVIEW_PACK_ID.fullmatch(expected_id) is None
or candidate.is_symlink()
or not candidate.is_dir()
):
raise E30ReviewEvidenceError("linked E30 review pack is unavailable")
manifest = _read_json(candidate / "manifest.json", _MAX_MANIFEST_BYTES)
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != "missioncore.e30-evidence-review-pack/v1"
or manifest.get("result_id") != expected_id
or not isinstance(identity, dict)
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or expected_id != f"e30-review-pack-{identity_sha256}"
or manifest.get("human_review_complete") is not False
or manifest.get("lab_published") is not False
):
raise E30ReviewEvidenceError("linked E30 review pack identity is invalid")
_authority(manifest.get("authority"))
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or len(artifacts) != 1:
raise E30ReviewEvidenceError("linked E30 review artifacts are invalid")
_, artifact = _artifact(
candidate,
artifacts[0],
expected_role="review-items",
)
if artifact.get("sha256") != expected_items_sha256:
raise E30ReviewEvidenceError("linked E30 review item digest differs")
return manifest
@lru_cache(maxsize=8)
def _read_materialization_cached(
root_text: str,
review_root_text: str,
signature: tuple[int, ...],
) -> tuple[dict[str, Any], tuple[dict[str, Any], ...]]:
del signature
root = Path(root_text)
review_root = Path(review_root_text)
if (
root.is_symlink()
or not root.is_dir()
or _MATERIALIZATION_ID.fullmatch(root.name) is None
):
raise E30ReviewEvidenceError("E30 materialization id is invalid")
manifest = _read_json(
root / E30_MATERIALIZATION_MANIFEST_NAME,
_MAX_MANIFEST_BYTES,
)
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != E30_MATERIALIZATION_SCHEMA
or manifest.get("result_id") != root.name
or not isinstance(identity, dict)
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or root.name != f"e30-materialization-{identity_sha256}"
or manifest.get("human_review_complete") is not False
or manifest.get("lab_published") is not False
):
raise E30ReviewEvidenceError("E30 materialization identity is invalid")
_authority(manifest.get("authority"))
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or len(artifacts) != 1:
raise E30ReviewEvidenceError("E30 materialization artifacts are invalid")
index_path, _ = _artifact(
root,
artifacts[0],
expected_role="materialized-items",
)
if index_path.stat().st_size > _MAX_INDEX_BYTES:
raise E30ReviewEvidenceError("E30 materialization index is out of bounds")
review_binding = identity.get("review_pack")
if not isinstance(review_binding, dict):
raise E30ReviewEvidenceError("E30 review binding is invalid")
review_manifest = _review_pack(
review_root,
expected_id=_string(review_binding, "result_id"),
expected_items_sha256=_sha_value(review_binding, "items_sha256"),
)
rows: list[dict[str, Any]] = []
counts: Counter[str] = Counter()
with index_path.open("r", encoding="utf-8") as stream:
for expected_sequence, line in enumerate(stream):
try:
value = json.loads(line)
except json.JSONDecodeError as exc:
raise E30ReviewEvidenceError(
"E30 materialization item is invalid"
) from exc
if not isinstance(value, dict):
raise E30ReviewEvidenceError(
"E30 materialization item must be an object"
)
item_id = value.get("item_id")
stratum = value.get("stratum")
if (
value.get("schema_version") != E30_MATERIALIZATION_ITEM_SCHEMA
or value.get("sequence") != expected_sequence
or not isinstance(item_id, str)
or _REVIEW_ITEM_ID.fullmatch(item_id) is None
or stratum not in _STRATA
or not isinstance(value.get("review"), dict)
or value["review"].get("state") != "unreviewed"
):
raise E30ReviewEvidenceError(
"E30 materialization item contract is invalid"
)
_authority(value.get("authority"))
_, artifact = _artifact(root, value.get("artifact"))
if artifact.get("path") != f"items/{item_id}.npz":
raise E30ReviewEvidenceError("E30 item artifact path differs")
camera_frame = value.get("camera_frame")
if camera_frame is not None:
camera_path, camera_artifact = _artifact(
root,
camera_frame,
expected_role="camera-frame",
)
expected_frame = value.get("evidence_binding", {}).get(
"source_frame_index"
)
if (
not isinstance(expected_frame, int)
or isinstance(expected_frame, bool)
or expected_frame < 0
or camera_path.stat().st_size > _MAX_CAMERA_FRAME_BYTES
or camera_artifact.get("media_type") != "image/jpeg"
or camera_artifact.get("source_frame_index")
!= expected_frame
or camera_artifact.get("exact_source_frame") is not True
or camera_artifact.get("path")
!= f"frames/frame-{expected_frame:06d}.jpg"
):
raise E30ReviewEvidenceError(
"E30 camera frame binding is invalid"
)
triage = value.get("engineering_triage")
if (
not isinstance(triage, dict)
or triage.get("schema_version")
!= "missioncore.e30-engineering-triage/v1"
or triage.get("semantic_verdict") is not None
or triage.get("human_exception_required") is not None
):
raise E30ReviewEvidenceError(
"E30 engineering triage contract is invalid"
)
rows.append(value)
counts[stratum] += 1
if (
len(rows) != manifest.get("item_count")
or len(rows) != review_binding.get("item_count")
or len({row["item_id"] for row in rows}) != len(rows)
):
raise E30ReviewEvidenceError("E30 materialization item count differs")
review_identity = review_manifest.get("identity")
reason_taxonomy = (
review_identity.get("reason_taxonomy")
if isinstance(review_identity, dict)
else None
)
source = identity.get("source")
projection = identity.get("projection")
if (
not isinstance(reason_taxonomy, list)
or not all(isinstance(reason, str) for reason in reason_taxonomy)
or not isinstance(source, dict)
or not isinstance(projection, dict)
):
raise E30ReviewEvidenceError("E30 catalog metadata is incomplete")
catalog_item = {
"result_id": root.name,
"created_at_utc": manifest.get("created_at_utc"),
"review_pack_id": review_binding["result_id"],
"e29_result_id": source.get("e29_result_id"),
"source_session_id": source.get("source_session_id"),
"item_count": len(rows),
"stratum_counts": {stratum: counts[stratum] for stratum in _STRATA},
"reason_taxonomy": reason_taxonomy,
"projection": projection,
"camera_evidence_available": (
manifest.get("camera_evidence_available") is True
),
"human_review_complete": False,
"lab_published": False,
"access": "read-only",
"authority": manifest.get("authority"),
}
return catalog_item, tuple(rows)
def _roots(
*,
materialization_root_provider: RootProvider,
review_pack_root_provider: RootProvider,
) -> tuple[Path, Path] | None:
materialization_root = materialization_root_provider()
review_root = review_pack_root_provider()
if materialization_root is None or review_root is None:
return None
materialization_root = materialization_root.resolve()
review_root = review_root.resolve()
if not materialization_root.is_dir() or not review_root.is_dir():
return None
return materialization_root, review_root
def _candidate(
materialization_root: Path,
result_id: str,
) -> Path:
if _MATERIALIZATION_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="E30 review не найден")
candidate = materialization_root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="E30 review не найден")
return candidate
def e30_review_item_summary(row: dict[str, Any]) -> dict[str, object]:
binding = row.get("evidence_binding")
locator = row.get("e29_locator")
snapshot = row.get("e29_snapshot")
materialization = row.get("materialization")
if (
not isinstance(binding, dict)
or not isinstance(locator, dict)
or not isinstance(snapshot, dict)
or not isinstance(materialization, dict)
):
raise E30ReviewEvidenceError("E30 item summary is incomplete")
return {
"item_id": row["item_id"],
"sequence": row["sequence"],
"review_key": row["review_key"],
"stratum": row["stratum"],
"range_bucket": row["range_bucket"],
"frame_index": binding.get("frame_index"),
"source_frame_index": binding.get("source_frame_index"),
"session_seconds": binding.get("session_seconds"),
"locator": locator,
"snapshot": snapshot,
"materialization": materialization,
"camera_frame_available": isinstance(row.get("camera_frame"), dict),
"engineering_triage": row.get("engineering_triage"),
"review": row["review"],
"access": "read-only",
}
def _logical_arrays_sha256(arrays: dict[str, npt.NDArray[Any]]) -> str:
digest = hashlib.sha256()
for name in sorted(arrays):
array = np.ascontiguousarray(arrays[name])
digest.update(name.encode())
digest.update(array.dtype.str.encode())
digest.update(_canonical_json(list(array.shape)))
digest.update(array.tobytes(order="C"))
return digest.hexdigest()
def _item_detail(root: Path, row: dict[str, Any]) -> dict[str, object]:
path, artifact = _artifact(root, row.get("artifact"))
if path.stat().st_size > _MAX_ITEM_BYTES:
raise E30ReviewEvidenceError("E30 item artifact is out of bounds")
expected_files = {
"selected_source_indices",
"selected_points_map_xyz_m",
"candidate_source_indices",
"candidate_points_map_xyz_m",
"projected_source_indices",
"projected_points_map_xyz_m",
"projected_pixels_xy",
"projected_depth_m",
"projected_point_class",
"projected_point_height_m",
"projected_candidate_mask",
"projected_selected_mask",
"sensor_position_map_xyz_m",
"sensor_orientation_map_from_lidar_xyzw",
}
with np.load(path, allow_pickle=False) as archive:
if set(archive.files) != expected_files:
raise E30ReviewEvidenceError("E30 item arrays are incompatible")
arrays = {
name: np.ascontiguousarray(archive[name])
for name in expected_files
}
if _logical_arrays_sha256(arrays) != artifact.get("logical_sha256"):
raise E30ReviewEvidenceError("E30 item logical content changed")
projected_count = arrays["projected_source_indices"].shape[0]
selected_count = arrays["selected_source_indices"].shape[0]
candidate_count = arrays["candidate_source_indices"].shape[0]
if (
arrays["selected_source_indices"].shape != (selected_count,)
or arrays["selected_points_map_xyz_m"].shape != (selected_count, 3)
or arrays["candidate_source_indices"].shape != (candidate_count,)
or arrays["candidate_points_map_xyz_m"].shape != (candidate_count, 3)
or arrays["projected_points_map_xyz_m"].shape != (projected_count, 3)
or arrays["projected_pixels_xy"].shape != (projected_count, 2)
or arrays["projected_depth_m"].shape != (projected_count,)
or arrays["projected_point_class"].shape != (projected_count,)
or arrays["projected_point_height_m"].shape != (projected_count,)
or arrays["projected_candidate_mask"].shape != (projected_count,)
or arrays["projected_selected_mask"].shape != (projected_count,)
or arrays["sensor_position_map_xyz_m"].shape != (3,)
or arrays["sensor_orientation_map_from_lidar_xyzw"].shape != (4,)
or not all(np.isfinite(value).all() for value in arrays.values())
):
raise E30ReviewEvidenceError("E30 item array shape is invalid")
camera_frame = row.get("camera_frame")
camera_document: dict[str, object] | None = None
if camera_frame is not None:
_, camera_artifact = _artifact(
root,
camera_frame,
expected_role="camera-frame",
)
camera_document = {
"available": True,
"url": (
f"/api/v1/laboratory/e30/reviews/{root.name}/items/"
f"{row['item_id']}/camera-frame"
f"?generation={camera_artifact['sha256']}"
),
"sha256": camera_artifact["sha256"],
"width": camera_artifact.get("width"),
"height": camera_artifact.get("height"),
"source_frame_index": camera_artifact.get("source_frame_index"),
"exact_source_frame": True,
}
return {
**e30_review_item_summary(row),
"camera_frame": camera_document,
"selected": {
"source_indices": arrays["selected_source_indices"].astype(
np.int64
).tolist(),
"points_map_xyz_m": arrays["selected_points_map_xyz_m"].astype(
np.float64
).tolist(),
},
"candidate": {
"source_indices": arrays["candidate_source_indices"].astype(
np.int64
).tolist(),
"points_map_xyz_m": arrays["candidate_points_map_xyz_m"].astype(
np.float64
).tolist(),
},
"projection": {
"source_indices": arrays["projected_source_indices"].astype(
np.int64
).tolist(),
"points_map_xyz_m": arrays["projected_points_map_xyz_m"].astype(
np.float64
).tolist(),
"pixels_xy": arrays["projected_pixels_xy"].astype(np.float64).tolist(),
"depth_m": arrays["projected_depth_m"].astype(np.float64).tolist(),
"point_class": arrays["projected_point_class"].astype(np.int64).tolist(),
"point_height_m": arrays["projected_point_height_m"].astype(
np.float64
).tolist(),
"candidate_mask": arrays["projected_candidate_mask"].astype(
np.int64
).tolist(),
"selected_mask": arrays["projected_selected_mask"].astype(
np.int64
).tolist(),
},
"pose": {
"position_map_xyz_m": arrays["sensor_position_map_xyz_m"].astype(
np.float64
).tolist(),
"orientation_map_from_lidar_xyzw": arrays[
"sensor_orientation_map_from_lidar_xyzw"
]
.astype(np.float64)
.tolist(),
},
}
def _materialization(
candidate: Path,
review_root: Path,
) -> tuple[dict[str, Any], tuple[dict[str, Any], ...]]:
return _read_materialization_cached(
str(candidate.resolve()),
str(review_root.resolve()),
_signature(candidate),
)
def load_verified_e30_review(
*,
materialization_root: Path,
review_pack_root: Path,
result_id: str,
) -> tuple[
dict[str, Any],
tuple[dict[str, Any], ...],
E30ReviewSubstrate,
]:
"""Load the verified A2 evidence and its exact A3 source binding."""
materialization_root = materialization_root.resolve()
review_pack_root = review_pack_root.resolve()
if (
not materialization_root.is_dir()
or not review_pack_root.is_dir()
or _MATERIALIZATION_ID.fullmatch(result_id) is None
):
raise E30ReviewEvidenceError("E30 materialization is unavailable")
candidate = materialization_root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise E30ReviewEvidenceError("E30 materialization is unavailable")
catalog, rows = _materialization(candidate, review_pack_root)
manifest = _read_json(
candidate / E30_MATERIALIZATION_MANIFEST_NAME,
_MAX_MANIFEST_BYTES,
)
identity = manifest.get("identity")
review_binding = identity.get("review_pack") if isinstance(identity, dict) else None
if (
not isinstance(identity, dict)
or not isinstance(review_binding, dict)
or not isinstance(manifest.get("identity_sha256"), str)
):
raise E30ReviewEvidenceError("E30 reviewer source binding is incomplete")
substrate = E30ReviewSubstrate(
materialization_id=result_id,
materialization_identity_sha256=cast(str, manifest["identity_sha256"]),
review_pack_id=_string(review_binding, "result_id"),
review_items_sha256=_sha_value(review_binding, "items_sha256"),
reason_taxonomy=tuple(cast(list[str], catalog["reason_taxonomy"])),
subjects=tuple(
E30ReviewSubject(
item_id=cast(str, row["item_id"]),
sequence=cast(int, row["sequence"]),
source_stratum=cast(E30Stratum, row["stratum"]),
)
for row in rows
),
)
return catalog, rows, substrate
def _string(document: dict[str, Any], key: str) -> str:
value = document.get(key)
if not isinstance(value, str) or not value:
raise E30ReviewEvidenceError(f"{key} must be a nonempty string")
return value
def _sha_value(document: dict[str, Any], key: str) -> str:
value = _string(document, key)
if _SHA256.fullmatch(value) is None:
raise E30ReviewEvidenceError(f"{key} must be a SHA-256 digest")
return value
def build_e30_review_router(
*,
materialization_root_provider: RootProvider = lambda: None,
review_pack_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/e30", tags=["laboratory"])
@router.get("/reviews")
def list_reviews(
limit: int = Query(default=1, ge=1, le=10),
) -> dict[str, object]:
roots = _roots(
materialization_root_provider=materialization_root_provider,
review_pack_root_provider=review_pack_root_provider,
)
if roots is None:
return {
"schema_version": LABORATORY_E30_CATALOG_SCHEMA,
"configured": False,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
materialization_root, review_root = roots
candidates = sorted(
(
candidate
for candidate in materialization_root.iterdir()
if candidate.is_dir()
and _MATERIALIZATION_ID.fullmatch(candidate.name) is not None
),
key=lambda candidate: candidate.stat().st_mtime_ns,
reverse=True,
)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in candidates:
try:
item, _ = _materialization(candidate, review_root)
if len(items) < limit:
items.append(copy.deepcopy(item))
except (E30ReviewEvidenceError, OSError):
invalid_total += 1
return {
"schema_version": LABORATORY_E30_CATALOG_SCHEMA,
"configured": True,
"items": items,
"candidate_total": len(candidates),
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/reviews/{result_id}/items")
def list_review_items(
result_id: str,
stratum: Annotated[Stratum, Query()] = "conflict",
limit: Annotated[int, Query(ge=1, le=128)] = 48,
cursor: Annotated[int, Query(ge=0)] = 0,
) -> dict[str, object]:
roots = _roots(
materialization_root_provider=materialization_root_provider,
review_pack_root_provider=review_pack_root_provider,
)
if roots is None:
raise HTTPException(status_code=404, detail="E30 review не найден")
materialization_root, review_root = roots
candidate = _candidate(materialization_root, result_id)
try:
catalog, rows = _materialization(candidate, review_root)
filtered = [row for row in rows if row.get("stratum") == stratum]
page = filtered[cursor : cursor + limit]
next_cursor = cursor + len(page)
return {
"schema_version": LABORATORY_E30_ITEMS_SCHEMA,
"result_id": result_id,
"stratum": stratum,
"items": [e30_review_item_summary(row) for row in page],
"total": len(filtered),
"next_cursor": next_cursor if next_cursor < len(filtered) else None,
"reason_taxonomy": catalog["reason_taxonomy"],
"access": "read-only",
}
except (E30ReviewEvidenceError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="E30 review evidence не прошло проверку целостности",
) from exc
@router.get("/reviews/{result_id}/items/{item_id}")
def get_review_item(
result_id: str,
item_id: str,
) -> dict[str, object]:
roots = _roots(
materialization_root_provider=materialization_root_provider,
review_pack_root_provider=review_pack_root_provider,
)
if roots is None or _REVIEW_ITEM_ID.fullmatch(item_id) is None:
raise HTTPException(status_code=404, detail="E30 review item не найден")
materialization_root, review_root = roots
candidate = _candidate(materialization_root, result_id)
try:
_, rows = _materialization(candidate, review_root)
row = next((value for value in rows if value.get("item_id") == item_id), None)
if row is None:
raise HTTPException(
status_code=404,
detail="E30 review item не найден",
)
return {
"schema_version": LABORATORY_E30_ITEM_DETAIL_SCHEMA,
"result_id": result_id,
"item": _item_detail(candidate, row),
"access": "read-only",
}
except HTTPException:
raise
except (E30ReviewEvidenceError, OSError, ValueError) as exc:
raise HTTPException(
status_code=409,
detail="E30 review item не прошёл проверку целостности",
) from exc
@router.get("/reviews/{result_id}/items/{item_id}/camera-frame")
def get_review_camera_frame(
result_id: str,
item_id: str,
generation: Annotated[str, Query(min_length=64, max_length=64)],
) -> FileResponse:
roots = _roots(
materialization_root_provider=materialization_root_provider,
review_pack_root_provider=review_pack_root_provider,
)
if (
roots is None
or _REVIEW_ITEM_ID.fullmatch(item_id) is None
or _SHA256.fullmatch(generation) is None
):
raise HTTPException(
status_code=404,
detail="E30 camera frame не найден",
)
materialization_root, review_root = roots
candidate = _candidate(materialization_root, result_id)
try:
_, rows = _materialization(candidate, review_root)
row = next(
(value for value in rows if value.get("item_id") == item_id),
None,
)
if row is None or row.get("camera_frame") is None:
raise HTTPException(
status_code=404,
detail="E30 camera frame не найден",
)
path, artifact = _artifact(
candidate,
row["camera_frame"],
expected_role="camera-frame",
)
if artifact.get("sha256") != generation:
raise HTTPException(
status_code=409,
detail="E30 camera generation изменилась",
)
return FileResponse(
path,
media_type="image/jpeg",
filename=f"e30-frame-{artifact['source_frame_index']:06d}.jpg",
headers={
"ETag": f'"{generation}"',
"Cache-Control": "private, max-age=31536000, immutable",
"X-Content-Type-Options": "nosniff",
},
)
except HTTPException:
raise
except (E30ReviewEvidenceError, OSError, ValueError) as exc:
raise HTTPException(
status_code=409,
detail="E30 camera frame не прошёл проверку целостности",
) from exc
return router