feat(lab): separate evidence catalogs and record methods
This commit is contained in:
@@ -83,6 +83,53 @@ class PublishedCameraEgoMotionLabInstance:
|
||||
build: CameraEgoMotionBuild
|
||||
|
||||
|
||||
def _laboratory_method(
|
||||
*,
|
||||
pipeline_id: str,
|
||||
execution_class: str,
|
||||
algorithm: str,
|
||||
profile_sha256: str | None,
|
||||
source_result_id: str,
|
||||
) -> dict[str, object]:
|
||||
source_identity: str | None = source_result_id.rsplit("-", 1)[-1]
|
||||
if len(source_identity) != 64 or any(
|
||||
character not in "0123456789abcdef" for character in source_identity
|
||||
):
|
||||
source_identity = None
|
||||
return {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
# E19-E26 predate the method manifest. The publisher now records the
|
||||
# exact known identities, but does not invent historical model/runtime
|
||||
# versions that were absent from their original accepted evidence.
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": execution_class,
|
||||
"pipeline_id": pipeline_id,
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "immutable accepted upstream result",
|
||||
"version": "content-addressed",
|
||||
"role": "read-only input evidence",
|
||||
"identity_sha256": source_identity,
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": algorithm,
|
||||
"version": pipeline_id,
|
||||
"role": "laboratory derivative",
|
||||
"identity_sha256": profile_sha256,
|
||||
},
|
||||
{
|
||||
"kind": "tool",
|
||||
"name": "Mission Core LAB publisher",
|
||||
"version": "missioncore.lab-instance/v1",
|
||||
"role": "immutable catalog projection",
|
||||
"identity_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def publish_integrated_lab_instance(
|
||||
*,
|
||||
repository_root: Path,
|
||||
@@ -156,6 +203,13 @@ def publish_integrated_lab_instance(
|
||||
run_created_at_utc=source.created_at_utc,
|
||||
provenance={
|
||||
"schema_version": "missioncore.integrated-lab-publication/v1",
|
||||
"method": _laboratory_method(
|
||||
pipeline_id="integrated-perception/v1",
|
||||
execution_class="hybrid",
|
||||
algorithm="camera semantics + LiDAR metric fusion",
|
||||
profile_sha256=profile_sha256,
|
||||
source_result_id=source.result_id,
|
||||
),
|
||||
"storage_mode": "hard-linked-immutable-payloads",
|
||||
"source_job_id": source.job.job_id,
|
||||
"projected_job_id": lab_job.job_id,
|
||||
@@ -263,6 +317,13 @@ def publish_e21_lab_instance(
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e21-lab-publication/v1",
|
||||
"method": _laboratory_method(
|
||||
pipeline_id="realtime-envelope/v1",
|
||||
execution_class="hybrid",
|
||||
algorithm="bounded real-time perception replay",
|
||||
profile_sha256=str(e21_report["identity"]["profile_sha256"]),
|
||||
source_result_id=str(e21_document["result_id"]),
|
||||
),
|
||||
"storage_mode": "bounded-derived-replay-and-projection",
|
||||
"e21_result_id": e21_document["result_id"],
|
||||
"worker_result_id": worker_document["result_id"],
|
||||
@@ -361,6 +422,13 @@ def publish_e22_lab_instance(
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e22-lab-publication/v1",
|
||||
"method": _laboratory_method(
|
||||
pipeline_id="temporal-stability/v1",
|
||||
execution_class="hybrid",
|
||||
algorithm="bounded temporal 2D/3D/semantic stabilization",
|
||||
profile_sha256=build.profile_sha256,
|
||||
source_result_id=source.result_id,
|
||||
),
|
||||
"storage_mode": "bounded-derived-replay-and-temporal-projection",
|
||||
"source_result_id": source.result_id,
|
||||
"source_lab_session_id": (None if source_lab is None else source_lab.session_id),
|
||||
@@ -487,6 +555,13 @@ def publish_e23_lab_instance(
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e23-lab-publication/v1",
|
||||
"method": _laboratory_method(
|
||||
pipeline_id="inline-temporal-stability/v1",
|
||||
execution_class="hybrid",
|
||||
algorithm="warm-worker inline temporal stabilization",
|
||||
profile_sha256=profile_sha256,
|
||||
source_result_id=str(worker_document["result_id"]),
|
||||
),
|
||||
"storage_mode": "bounded-inline-worker-result-and-immutable-source-replay",
|
||||
"worker_result_id": worker_document["result_id"],
|
||||
"source_report_sha256": _sha256(source_path),
|
||||
@@ -589,6 +664,13 @@ def publish_e24_lab_instance(
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e24-lab-publication/v1",
|
||||
"method": _laboratory_method(
|
||||
pipeline_id="world-motion/v1",
|
||||
execution_class="hybrid",
|
||||
algorithm="world-frame motion tracking",
|
||||
profile_sha256=build.profile_sha256,
|
||||
source_result_id=source.result_id,
|
||||
),
|
||||
"storage_mode": "bounded-world-frame-tracking-and-immutable-source-replay",
|
||||
"source_result_id": source.result_id,
|
||||
"source_lab_session_id": (None if source_lab is None else source_lab.session_id),
|
||||
@@ -695,6 +777,13 @@ def publish_e25_lab_instance(
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e25-lab-publication/v1",
|
||||
"method": _laboratory_method(
|
||||
pipeline_id="persistent-support-motion/v1",
|
||||
execution_class="hybrid",
|
||||
algorithm="persistent occupied-support tracking",
|
||||
profile_sha256=build.profile_sha256,
|
||||
source_result_id=source.result_id,
|
||||
),
|
||||
"storage_mode": "bounded-persistent-support-and-immutable-source-replay",
|
||||
"source_result_id": source.result_id,
|
||||
"source_lab_session_id": (None if source_lab is None else source_lab.session_id),
|
||||
@@ -826,6 +915,13 @@ def publish_e26_lab_instance(
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e26-lab-publication/v1",
|
||||
"method": _laboratory_method(
|
||||
pipeline_id="camera-ego-motion-fusion/v1",
|
||||
execution_class="hybrid",
|
||||
algorithm="KB4 multiview ego-motion + persistent LiDAR support",
|
||||
profile_sha256=build.profile_sha256,
|
||||
source_result_id=lidar_source.result_id,
|
||||
),
|
||||
"storage_mode": (
|
||||
"bounded-camera-ego-motion-and-immutable-lidar-source-replay"
|
||||
),
|
||||
|
||||
@@ -1375,6 +1375,7 @@ def k1_local_surface_catalog_item(model: K1LocalSurfaceV1) -> dict[str, object]:
|
||||
"status": model.report["status"],
|
||||
"source": model.report["source"],
|
||||
"surface_model": model.report["surface_model"],
|
||||
"producer_sha256": model.identity["producer_sha256"],
|
||||
"occupancy_policy": model.report["occupancy_policy"],
|
||||
"metrics": model.report["metrics"],
|
||||
"anchors": model.report["anchors"],
|
||||
|
||||
@@ -11,7 +11,7 @@ import threading
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from typing import Any, Literal, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from k1link.artifacts import utc_now_iso
|
||||
@@ -45,6 +45,8 @@ LAB_ARCHIVE_ID = "missioncore.lab-instances"
|
||||
LAB_ORIGIN = "missioncore.lab-instance/v1"
|
||||
LAB_ID_PATTERN = re.compile(r"^LAB [A-Z][A-Z0-9._-]{0,31}$")
|
||||
SHA256_PATTERN = re.compile(r"^[a-f0-9]{64}$")
|
||||
LAB_METHOD_SCHEMA = "missioncore.laboratory-method/v1"
|
||||
SessionScope = Literal["all", "source", "laboratory"]
|
||||
|
||||
SCHEMA_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS observation_sessions (
|
||||
@@ -205,27 +207,51 @@ class SessionStore:
|
||||
connection.commit()
|
||||
return tuple(imported)
|
||||
|
||||
def list_recent(self, *, limit: int = 20, cursor: str | None = None) -> SessionPage:
|
||||
def list_recent(
|
||||
self,
|
||||
*,
|
||||
limit: int = 20,
|
||||
cursor: str | None = None,
|
||||
scope: SessionScope = "all",
|
||||
) -> SessionPage:
|
||||
if not 1 <= limit <= 100:
|
||||
raise ValueError("limit must be within 1..100")
|
||||
scope_clause = {
|
||||
"all": "1 = 1",
|
||||
"source": (
|
||||
"NOT EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
|
||||
"WHERE lab.session_id = sessions.session_id)"
|
||||
),
|
||||
"laboratory": (
|
||||
"EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
|
||||
"WHERE lab.session_id = sessions.session_id)"
|
||||
),
|
||||
}.get(scope)
|
||||
if scope_clause is None:
|
||||
raise ValueError("scope must be all, source, or laboratory")
|
||||
parameters: list[object] = []
|
||||
where = ""
|
||||
where = f"WHERE {scope_clause}" # noqa: S608 - closed static scope clauses
|
||||
with self._connect() as connection:
|
||||
if cursor is not None:
|
||||
_validate_identifier(cursor, "session cursor")
|
||||
cursor_row = connection.execute(
|
||||
"SELECT started_at_utc, session_id FROM observation_sessions "
|
||||
"WHERE session_id = ?",
|
||||
"SELECT sessions.started_at_utc, sessions.session_id "
|
||||
"FROM observation_sessions AS sessions "
|
||||
f"WHERE {scope_clause} AND sessions.session_id = ?", # noqa: S608
|
||||
(cursor,),
|
||||
).fetchone()
|
||||
if cursor_row is None:
|
||||
raise SessionNotFoundError("observation session cursor was not found")
|
||||
where = "WHERE (COALESCE(started_at_utc, ''), session_id) < (COALESCE(?, ''), ?)"
|
||||
where += (
|
||||
" AND (COALESCE(sessions.started_at_utc, ''), sessions.session_id) "
|
||||
"< (COALESCE(?, ''), ?)"
|
||||
)
|
||||
parameters.extend((cursor_row["started_at_utc"], cursor_row["session_id"]))
|
||||
parameters.append(limit + 1)
|
||||
rows = connection.execute(
|
||||
f"SELECT * FROM observation_sessions {where} " # noqa: S608 - static clause
|
||||
"ORDER BY COALESCE(started_at_utc, '') DESC, session_id DESC LIMIT ?",
|
||||
f"SELECT sessions.* FROM observation_sessions AS sessions {where} " # noqa: S608
|
||||
"ORDER BY COALESCE(sessions.started_at_utc, '') DESC, "
|
||||
"sessions.session_id DESC LIMIT ?",
|
||||
parameters,
|
||||
).fetchall()
|
||||
lab_rows = (
|
||||
@@ -366,7 +392,9 @@ class SessionStore:
|
||||
or duration_seconds <= 0
|
||||
):
|
||||
raise ValueError("LAB duration must be a positive finite value")
|
||||
serialized_provenance = _serialize_provenance(provenance or {})
|
||||
normalized_provenance = provenance or {}
|
||||
_validate_lab_method(normalized_provenance)
|
||||
serialized_provenance = _serialize_provenance(normalized_provenance)
|
||||
published_at = utc_now_iso()
|
||||
|
||||
with self._lock, self._connect() as connection:
|
||||
@@ -1078,6 +1106,55 @@ def _serialize_provenance(value: dict[str, Any]) -> str:
|
||||
return serialized
|
||||
|
||||
|
||||
def _validate_lab_method(provenance: dict[str, Any]) -> None:
|
||||
method = provenance.get("method")
|
||||
if not isinstance(method, dict):
|
||||
raise ValueError("LAB provenance must include a method manifest")
|
||||
if method.get("schema_version") != LAB_METHOD_SCHEMA:
|
||||
raise ValueError("LAB method schema is invalid")
|
||||
if method.get("completeness") not in {"complete", "legacy-partial"}:
|
||||
raise ValueError("LAB method completeness is invalid")
|
||||
if method.get("execution_class") not in {
|
||||
"deterministic",
|
||||
"ai-inference",
|
||||
"hybrid",
|
||||
}:
|
||||
raise ValueError("LAB method execution class is invalid")
|
||||
pipeline_id = method.get("pipeline_id")
|
||||
if (
|
||||
not isinstance(pipeline_id, str)
|
||||
or not pipeline_id.strip()
|
||||
or len(pipeline_id) > 160
|
||||
):
|
||||
raise ValueError("LAB method pipeline id is invalid")
|
||||
components = method.get("components")
|
||||
if not isinstance(components, list) or not 1 <= len(components) <= 32:
|
||||
raise ValueError("LAB method components are invalid")
|
||||
identities = 0
|
||||
for component in components:
|
||||
if not isinstance(component, dict):
|
||||
raise ValueError("LAB method component is invalid")
|
||||
if component.get("kind") not in {"source", "tool", "model", "algorithm", "runtime"}:
|
||||
raise ValueError("LAB method component kind is invalid")
|
||||
for field in ("name", "version", "role"):
|
||||
value = component.get(field)
|
||||
if not isinstance(value, str) or not value.strip() or len(value) > 240:
|
||||
raise ValueError(f"LAB method component {field} is invalid")
|
||||
identity = component.get("identity_sha256")
|
||||
if identity is not None:
|
||||
if not isinstance(identity, str) or SHA256_PATTERN.fullmatch(identity) is None:
|
||||
raise ValueError("LAB method component identity is invalid")
|
||||
identities += 1
|
||||
if identities == 0:
|
||||
raise ValueError("LAB method must bind at least one component identity")
|
||||
if method["completeness"] == "complete" and any(
|
||||
component.get("identity_sha256") is None
|
||||
for component in components
|
||||
if component.get("kind") in {"model", "algorithm"}
|
||||
):
|
||||
raise ValueError("complete LAB method must identify every model and algorithm")
|
||||
|
||||
|
||||
def _require_utc_timestamp(value: str, field: str) -> None:
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -312,10 +312,11 @@ def build_session_router(
|
||||
def list_observation_sessions(
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
cursor: str | None = Query(default=None, max_length=128),
|
||||
scope: Literal["all", "source", "laboratory"] = "all",
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
_refresh_catalog(catalog_refresher)
|
||||
page = store.list_recent(limit=limit, cursor=cursor)
|
||||
page = store.list_recent(limit=limit, cursor=cursor, scope=scope)
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user