feat(lab): separate evidence catalogs and record methods

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 17:56:23 +03:00
parent e9ff29297b
commit 4db00b53f7
17 changed files with 670 additions and 24 deletions
+29 -2
View File
@@ -43,6 +43,24 @@ from k1link.web.session_api import (
)
def lab_method() -> dict[str, object]:
return {
"schema_version": "missioncore.laboratory-method/v1",
"completeness": "complete",
"execution_class": "deterministic",
"pipeline_id": "test-pipeline/v1",
"components": [
{
"kind": "algorithm",
"name": "test algorithm",
"version": "v1",
"role": "contract fixture",
"identity_sha256": "9" * 64,
}
],
}
def make_legacy_session(sessions_root: Path, session_id: str) -> Path:
session = sessions_root / session_id
capture = session / "captures" / "mqtt_live"
@@ -281,17 +299,26 @@ def test_session_router_exposes_immutable_lab_provenance(tmp_path: Path) -> None
source_result_id="e21-realtime-envelope-" + "b" * 64,
config_sha256="c" * 64,
run_created_at_utc="2026-07-23T15:55:15.548Z",
provenance={"source_payloads_mutated": False},
provenance={
"source_payloads_mutated": False,
"method": lab_method(),
},
)
router = build_session_router(store)
list_route = endpoint(router, "/api/v1/observation-sessions", "GET")
detail_route = endpoint(router, "/api/v1/observation-sessions/{session_id}", "GET")
listing = list_route(limit=20, cursor=None)
listing = list_route(limit=20, cursor=None, scope="all")
item = next(value for value in listing["items"] if value["id"] == binding.session_id)
source_listing = list_route(limit=20, cursor=None, scope="source")
laboratory_listing = list_route(limit=20, cursor=None, scope="laboratory")
detail = detail_route(session_id=binding.session_id)
assert item["lab"] == binding.as_dict()
assert [value["id"] for value in source_listing["items"]] == [source.name]
assert [value["id"] for value in laboratory_listing["items"]] == [
binding.session_id
]
assert detail["lab"] == binding.as_dict()
assert item["lab"]["source_session_id"] == source.name
assert item["lab"]["provenance"]["source_payloads_mutated"] is False
+53 -3
View File
@@ -23,6 +23,24 @@ from k1link.sessions import (
)
def lab_method() -> dict[str, object]:
return {
"schema_version": "missioncore.laboratory-method/v1",
"completeness": "complete",
"execution_class": "deterministic",
"pipeline_id": "test-pipeline/v1",
"components": [
{
"kind": "algorithm",
"name": "test algorithm",
"version": "v1",
"role": "contract fixture",
"identity_sha256": "9" * 64,
}
],
}
def make_legacy_session(
sessions_root: Path,
session_id: str,
@@ -798,7 +816,10 @@ def test_lab_instance_is_independent_and_never_deletes_source_evidence(
source_result_id="e21-realtime-envelope-" + "b" * 64,
config_sha256="c" * 64,
run_created_at_utc="2026-07-23T15:51:25.000Z",
provenance={"storage_mode": "hard-linked-immutable-payloads"},
provenance={
"storage_mode": "hard-linked-immutable-payloads",
"method": lab_method(),
},
)
detail = store.get_session(binding.session_id)
@@ -809,6 +830,13 @@ def test_lab_instance_is_independent_and_never_deletes_source_evidence(
assert lab_command.session_id == binding.session_id
assert source.is_dir()
assert [item.session_id for item in store.list_recent(scope="source").items] == [
source.name
]
assert [
item.session_id for item in store.list_recent(scope="laboratory").items
] == [binding.session_id]
with pytest.raises(SessionIntegrityError, match="has LAB instances"):
store.delete_session(source.name)
assert source.is_dir()
@@ -838,7 +866,7 @@ def test_lab_instance_publication_is_idempotent_but_provenance_is_immutable(
"source_result_id": "e10-integrated-perception-" + "e" * 64,
"config_sha256": "f" * 64,
"run_created_at_utc": "2026-07-23T05:19:43.138Z",
"provenance": {"source": "accepted"},
"provenance": {"source": "accepted", "method": lab_method()},
}
first = store.publish_lab_instance(**parameters)
@@ -849,6 +877,28 @@ def test_lab_instance_publication_is_idempotent_but_provenance_is_immutable(
store.publish_lab_instance(**{**parameters, "config_sha256": "0" * 64})
def test_lab_instance_rejects_publication_without_a_method_manifest(
tmp_path: Path,
) -> None:
repository = tmp_path / "repo"
sessions = repository / "sessions"
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
store = SessionStore(repository, data_dir=tmp_path / "data")
store.reconcile_archive(xgrids_k1_archive_source(sessions))
with pytest.raises(ValueError, match="method manifest"):
store.publish_lab_instance(
session_id="lab-without-method",
source_session_id=source.name,
display_name="LAB E30 · incomplete method",
lab_id="LAB E30",
result_kind="e30-test",
result_id="e30-test",
run_created_at_utc="2026-07-26T15:00:00Z",
provenance={"schema_version": "legacy"},
)
def test_bounded_lab_instance_excludes_unbounded_recorded_media(
tmp_path: Path,
) -> None:
@@ -872,7 +922,7 @@ def test_bounded_lab_instance_excludes_unbounded_recorded_media(
run_created_at_utc="2026-07-23T15:55:15.548Z",
duration_seconds=59.962,
include_recorded_media=False,
provenance={"timeline_scope": "bounded"},
provenance={"timeline_scope": "bounded", "method": lab_method()},
)
detail = store.get_session(binding.session_id)