From a55ef475d9b5847a6f3664044fe5005988e7dcee Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 23 Aug 2026 22:40:07 +0300 Subject: [PATCH] fix(lab): restore semantic overlay in M4.7 --- .../src/core/laboratory/e47SemanticSlam.ts | 62 +++++++++++--- .../src/core/laboratory/m47ReferenceGraph.ts | 43 +++++++++- .../laboratory/M47ReferenceGraphResult.tsx | 17 +++- .../test/e47SemanticSlam.test.mjs | 15 ++++ .../test/m47ReferenceGraph.test.mjs | 81 +++++++++++++++++-- .../m47-reference-graph-shadow.json | 2 +- scripts/publish_m47_reference_graph_lab.py | 2 + src/k1link/laboratory/m47_reference_graph.py | 44 +++++++++- src/k1link/web/e47_semantic_slam_api.py | 4 + tests/test_e47_semantic_slam_api.py | 11 ++- tests/test_m47_reference_graph_lab.py | 40 ++++++++- 11 files changed, 289 insertions(+), 32 deletions(-) diff --git a/apps/control-station/src/core/laboratory/e47SemanticSlam.ts b/apps/control-station/src/core/laboratory/e47SemanticSlam.ts index bc6c35c..ac14c71 100644 --- a/apps/control-station/src/core/laboratory/e47SemanticSlam.ts +++ b/apps/control-station/src/core/laboratory/e47SemanticSlam.ts @@ -360,27 +360,65 @@ function parseResult(value: unknown): E47SemanticSlamResult { } export async function fetchE47SemanticSlamResult({ + resultId: requestedResultId, fetcher = fetch, signal, }: { + resultId?: string; fetcher?: LaboratoryFetch; signal?: AbortSignal; } = {}): Promise { - const response = await fetcher("/api/v1/laboratory/e47-semantic-slam/results?limit=1", { - headers: { Accept: "application/json" }, - signal, - }); + if (requestedResultId !== undefined) resultId(requestedResultId); + let response = await fetcher( + requestedResultId === undefined + ? "/api/v1/laboratory/e47-semantic-slam/results?limit=1" + : `/api/v1/laboratory/e47-semantic-slam/results/${requestedResultId}`, + { headers: { Accept: "application/json" }, signal }, + ); + let catalogFallback = false; + if (requestedResultId !== undefined && response.status === 404) { + response = await fetcher("/api/v1/laboratory/e47-semantic-slam/results?limit=10", { + headers: { Accept: "application/json" }, + signal, + }); + catalogFallback = true; + } if (!response.ok) { throw new E47SemanticSlamContractError(`E47 LAB недоступен: HTTP ${response.status}.`); } - const payload = object(await response.json(), "E47 catalog"); - exact( - payload.schema_version, - "missioncore.e47-semantic-slam-catalog/v1", - "E47 catalog schema", - ); - const items = array(payload.items, "E47 catalog items"); - return items.length ? parseResult(items[0]) : null; + const payload = await response.json(); + if (requestedResultId === undefined || catalogFallback) { + const catalog = object(payload, "E47 catalog"); + exact( + catalog.schema_version, + "missioncore.e47-semantic-slam-catalog/v1", + "E47 catalog schema", + ); + const items = array(catalog.items, "E47 catalog items"); + if (!items.length) return null; + const selected = requestedResultId === undefined + ? items[0] + : items.find((candidate) => ( + typeof candidate === "object" + && candidate !== null + && (candidate as Record).result_id === requestedResultId + )); + if (selected === undefined) { + throw new E47SemanticSlamContractError( + "E47 requested result: точный semantic-result отсутствует в каталоге.", + ); + } + const parsed = parseResult(selected); + if (requestedResultId !== undefined && parsed.resultId !== requestedResultId) { + throw new E47SemanticSlamContractError("E47 requested result: нарушена идентичность."); + } + return parsed; + } + const parsed = parseResult(payload); + if (parsed.resultId !== requestedResultId) { + throw new E47SemanticSlamContractError("E47 requested result: нарушена идентичность."); + } + return parsed; } function parseFrame( diff --git a/apps/control-station/src/core/laboratory/m47ReferenceGraph.ts b/apps/control-station/src/core/laboratory/m47ReferenceGraph.ts index 4f43452..b858027 100644 --- a/apps/control-station/src/core/laboratory/m47ReferenceGraph.ts +++ b/apps/control-station/src/core/laboratory/m47ReferenceGraph.ts @@ -6,6 +6,10 @@ import { fetchM4ThreatReplayResult, type M4ThreatReplayResult, } from "./m4ReplayThreat"; +import { + fetchE47SemanticSlamResult, + type E47SemanticSlamResult, +} from "./e47SemanticSlam"; export interface M47ReferenceGraphLabResult { resultId: string; @@ -41,6 +45,7 @@ export interface M47ReferenceGraphLabResult { >>; limitations: readonly string[]; visual: M4ThreatReplayResult; + semantic: E47SemanticSlamResult; } type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; @@ -154,7 +159,7 @@ export async function fetchM47ReferenceGraphLab({ const report = evidence.rawReport; exact( report.schema_version, - "missioncore.reference-perception-graph-lab-report/v1", + "missioncore.reference-perception-graph-lab-report/v2", "M4.7 report schema", ); exact(report.result_id, resultId, "M4.7 report result"); @@ -176,6 +181,16 @@ export async function fetchM47ReferenceGraphLab({ true, "M4.7 visual evidence", ); + exact( + visualEvidence.camera_semantic_overlay_available, + true, + "M4.7 camera semantic overlay", + ); + exact( + visualEvidence.semantic_binding, + "exact-sequence-and-session-time", + "M4.7 semantic binding", + ); const linkedVisualResultId = text( visualEvidence.linked_result_id, "M4.7 linked visual result", @@ -193,6 +208,31 @@ export async function fetchM47ReferenceGraphLab({ "M4.7 visual binding: сервер вернул другой replay result.", ); } + const semanticResultId = text( + visualEvidence.semantic_result_id, + "M4.7 linked semantic result", + ); + if (!/^e47-semantic-slam-[a-f0-9]{64}$/.test(semanticResultId)) { + throw new M47ReferenceGraphContractError( + "M4.7 semantic binding: нарушена идентичность.", + ); + } + const semantic = await fetchE47SemanticSlamResult({ + resultId: semanticResultId, + fetcher, + signal, + }); + if ( + !semantic + || semantic.resultId !== semanticResultId + || semantic.baseM4ResultId !== linkedVisualResultId + || semantic.metrics.frames.total !== 4489 + || semantic.metrics.frames.maskAvailable !== 4489 + ) { + throw new M47ReferenceGraphContractError( + "M4.7 semantic binding: сервер вернул несвязанный semantic-result.", + ); + } const graphResultId = text(source.graph_result_id, "M4.7 graph result"); if (!/^m47-reference-graph-[a-f0-9]{64}$/.test(graphResultId)) { throw new M47ReferenceGraphContractError("M4.7 graph result: нарушена идентичность."); @@ -219,5 +259,6 @@ export async function fetchM47ReferenceGraphLab({ queueHighWatermarks: queueWatermarks(metrics.queue_high_watermarks), limitations: stringArray(report.limitations, "M4.7 limitations"), visual, + semantic, }; } diff --git a/apps/control-station/src/workspaces/laboratory/M47ReferenceGraphResult.tsx b/apps/control-station/src/workspaces/laboratory/M47ReferenceGraphResult.tsx index 4ca6ed8..1865497 100644 --- a/apps/control-station/src/workspaces/laboratory/M47ReferenceGraphResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/M47ReferenceGraphResult.tsx @@ -44,7 +44,7 @@ export function M47ReferenceGraphResultView({ }, { label: "Визуал", - value: "VIDEO/CAMERA/3D/PLAN · единый recorded clock · exact M4.6 ledger binding", + value: "VIDEO/CAMERA/3D/PLAN · единый recorded clock · exact M4.6 + E47 semantic binding", }, ]} brief={{ @@ -72,6 +72,13 @@ export function M47ReferenceGraphResultView({ role: "synchronized VIDEO/CAMERA/3D/PLAN evidence", identitySha256: result.linkedVisualResultId.split("-").at(-1) ?? null, }, + { + kind: "source", + name: result.semantic.resultId, + version: "4489 exact camera masks", + role: "synchronized diagnostic semantic overlay", + identitySha256: result.semantic.resultId.split("-").at(-1) ?? null, + }, { kind: "algorithm", name: "canonical reference perception graph", @@ -90,7 +97,13 @@ export function M47ReferenceGraphResultView({ kind="diagnostic-model" resizable > - + )} result={( diff --git a/apps/control-station/test/e47SemanticSlam.test.mjs b/apps/control-station/test/e47SemanticSlam.test.mjs index 4f73a6f..a7547d3 100644 --- a/apps/control-station/test/e47SemanticSlam.test.mjs +++ b/apps/control-station/test/e47SemanticSlam.test.mjs @@ -127,6 +127,21 @@ test("E47 accepts only a fully accounted diagnostic semantic/SLAM view", async ( ); }); +test("E47 fetches one exact content-addressed semantic result", async () => { + const requests = []; + const result = await fetchE47SemanticSlamResult({ + resultId, + fetcher: async (input) => { + requests.push(String(input)); + return new Response(JSON.stringify(resultView())); + }, + }); + assert.equal(result.resultId, resultId); + assert.deepEqual(requests, [ + `/api/v1/laboratory/e47-semantic-slam/results/${resultId}`, + ]); +}); + test("E47 rejects result-level point accounting drift", async () => { await assert.rejects( fetchE47SemanticSlamResult({ diff --git a/apps/control-station/test/m47ReferenceGraph.test.mjs b/apps/control-station/test/m47ReferenceGraph.test.mjs index 3868543..cbf3fcb 100644 --- a/apps/control-station/test/m47ReferenceGraph.test.mjs +++ b/apps/control-station/test/m47ReferenceGraph.test.mjs @@ -9,6 +9,7 @@ let fetchM47ReferenceGraphLab; const resultId = `m47-reference-graph-lab-${"a".repeat(64)}`; const graphResultId = `m47-reference-graph-${"b".repeat(64)}`; const visualResultId = `m4-threat-replay-${"c".repeat(64)}`; +const semanticResultId = `e47-semantic-slam-${"d".repeat(64)}`; before(async () => { server = await createServer({ @@ -81,6 +82,62 @@ function m4Result() { }; } +function semanticResult() { + return { + schema_version: "missioncore.e47-semantic-slam-view/v1", + result_id: semanticResultId, + created_at_utc: "2026-08-06T08:13:48.601Z", + status: "diagnostic-semantic-slam-shadow", + profile_id: "ravnoves00-eomt-kb4-slam-shadow/v1", + base_m4_result_id: visualResultId, + semantic_result_id: `result-${"e".repeat(64)}`, + geometry_result_id: `m4-geometry-replay-${"f".repeat(64)}`, + source_pack_id: `e10-lidar-pack-${"1".repeat(64)}`, + calibration_content_sha256: "2".repeat(64), + provider: { + provider_id: "eomt-cityscapes-semantic-control/v1", + model_id: "cityscapes-semantic-eomt", + model_revision: "3".repeat(40), + model_weights_sha256: "4".repeat(64), + preprocess_id: "raw-kb4-valid-fov-semantic/v1", + }, + temporal_binding: { + semantic_to_camera: "exact-sequence-and-session-time", + camera_to_lidar: "accepted-e6-nearest-host-arrival-best-effort", + clock_basis: "recorded-host-monotonic-arrival", + maximum_lidar_camera_delta_ms: 100, + maximum_pose_point_delta_ms: 100, + physical_synchronization_proven: false, + }, + taxonomy: [{ + class_id: 1, + label: "road", + disposition: "labeled", + color_rgb: [128, 64, 128], + }], + metrics: { + frames: { total: 4489, mask_available: 4489, source_available: 3928 }, + points: { total: 1, projected: 1, labeled: 1, ambiguous: 0, unprojected: 0, absent: 0 }, + observations: { total: 1, labeled: 1, ambiguous: 0, unprojected: 0, absent: 0 }, + runtime: { elapsed_ms: 1000, frames_per_second: 4.489 }, + }, + acceptance: { + artifact_contract_passed: true, + frame_accounting_passed: true, + point_accounting_passed: true, + observation_binding_passed: true, + temporal_binding_passed: true, + independent_semantic_truth_passed: false, + provider_promoted: false, + }, + limitations: ["diagnostic only"], + ground_truth: false, + semantic_authority: "diagnostic-only", + navigation_or_safety_accepted: false, + actuation_allowed: false, + }; +} + function evidenceReport() { const mismatch = { source_binding: 0, @@ -92,7 +149,7 @@ function evidenceReport() { threat_assessments: 0, }; const raw = { - schema_version: "missioncore.reference-perception-graph-lab-report/v1", + schema_version: "missioncore.reference-perception-graph-lab-report/v2", result_id: resultId, source: { graph_result_id: graphResultId }, method: { graph_id: "reference-perception-graph/v2" }, @@ -122,8 +179,11 @@ function evidenceReport() { }, visual_evidence: { linked_result_id: visualResultId, + semantic_result_id: semanticResultId, shared_recorded_clock: true, video_camera_3d_plan_available: true, + camera_semantic_overlay_available: true, + semantic_binding: "exact-sequence-and-session-time", }, limitations: ["recorded replay only"], }; @@ -134,13 +194,13 @@ function evidenceReport() { created_at_utc: "2026-08-23T18:00:16.061Z", access: "read-only", proof: { - document_schema_version: "missioncore.reference-perception-graph-lab/v1", + document_schema_version: "missioncore.reference-perception-graph-lab/v2", document_sha256: "3".repeat(64), identity_sha256: "a".repeat(64), report_schema_version: raw.schema_version, report_sha256: "4".repeat(64), - artifact_count: 6, - verified_artifact_count: 6, + artifact_count: 9, + verified_artifact_count: 9, }, completeness: { identity: "recorded" }, identity: { authority: raw.authority }, @@ -167,14 +227,16 @@ test("M4.7 binds exact Worker graph proof to the synchronized M4.6 visual", asyn fetcher: async (input) => { const path = String(input); requests.push(path); - return new Response(JSON.stringify( - path.includes("/evidence-reports/") ? evidenceReport() : m4Result(), - ), { status: 200 }); + const payload = path.includes("/evidence-reports/") + ? evidenceReport() + : path.includes("/e47-semantic-slam/") ? semanticResult() : m4Result(); + return new Response(JSON.stringify(payload), { status: 200 }); }, }); assert.equal(result.graphResultId, graphResultId); assert.equal(result.visual.resultId, visualResultId); + assert.equal(result.semantic.resultId, semanticResultId); assert.equal(result.worker.id, "worker-006"); assert.equal(result.frames.delivered, 4489); assert.deepEqual(Object.values(result.parityMismatchCounts), Array(7).fill(0)); @@ -182,6 +244,7 @@ test("M4.7 binds exact Worker graph proof to the synchronized M4.6 visual", asyn assert.deepEqual(requests, [ `/api/v1/laboratory/evidence-reports/m47-reference-graph-shadow/${resultId}`, `/api/v1/laboratory/m4-threat/results/${visualResultId}`, + `/api/v1/laboratory/e47-semantic-slam/results/${semanticResultId}`, ]); }); @@ -198,6 +261,9 @@ test("M4.7 keeps exact visual binding during a rolling API deployment", async () if (path.endsWith(`/${visualResultId}`)) { return new Response(null, { status: 404 }); } + if (path.includes("/e47-semantic-slam/")) { + return new Response(JSON.stringify(semanticResult()), { status: 200 }); + } return new Response(JSON.stringify({ schema_version: "missioncore.m4-threat-replay-catalog/v1", items: [m4Result()], @@ -210,5 +276,6 @@ test("M4.7 keeps exact visual binding during a rolling API deployment", async () `/api/v1/laboratory/evidence-reports/m47-reference-graph-shadow/${resultId}`, `/api/v1/laboratory/m4-threat/results/${visualResultId}`, "/api/v1/laboratory/m4-threat/results?limit=10", + `/api/v1/laboratory/e47-semantic-slam/results/${semanticResultId}`, ]); }); diff --git a/config/laboratories/m47-reference-graph-shadow.json b/config/laboratories/m47-reference-graph-shadow.json index 8da1bec..7bee3b0 100644 --- a/config/laboratories/m47-reference-graph-shadow.json +++ b/config/laboratories/m47-reference-graph-shadow.json @@ -5,6 +5,6 @@ "runtime_relative_root": "m47/reference-graph-labs", "result_id_prefix": "m47-reference-graph-lab", "document_name": "manifest.json", - "schema_version": "missioncore.reference-perception-graph-lab/v1" + "schema_version": "missioncore.reference-perception-graph-lab/v2" } } diff --git a/scripts/publish_m47_reference_graph_lab.py b/scripts/publish_m47_reference_graph_lab.py index c988936..5449c53 100644 --- a/scripts/publish_m47_reference_graph_lab.py +++ b/scripts/publish_m47_reference_graph_lab.py @@ -14,11 +14,13 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--graph-result-root", type=Path, required=True) parser.add_argument("--visual-result-root", type=Path, required=True) + parser.add_argument("--semantic-result-root", type=Path, required=True) parser.add_argument("--output-root", type=Path, required=True) args = parser.parse_args() result = publish_m47_reference_graph_lab( graph_result_root=args.graph_result_root, visual_result_root=args.visual_result_root, + semantic_result_root=args.semantic_result_root, output_root=args.output_root, ) print( diff --git a/src/k1link/laboratory/m47_reference_graph.py b/src/k1link/laboratory/m47_reference_graph.py index 68de7b2..3c95295 100644 --- a/src/k1link/laboratory/m47_reference_graph.py +++ b/src/k1link/laboratory/m47_reference_graph.py @@ -12,11 +12,17 @@ from pathlib import Path from typing import Final from k1link.perception.reference_graph_result import read_reference_graph_result +from k1link.perception.semantic_slam_replay import ( + SEMANTIC_SLAM_MANIFEST_NAME, + SEMANTIC_SLAM_REPORT_NAME, + SEMANTIC_SLAM_TAXONOMY_NAME, + read_semantic_slam_replay_result, +) from k1link.perception.threat_replay import read_threat_replay_result -M47_REFERENCE_GRAPH_LAB_SCHEMA: Final = "missioncore.reference-perception-graph-lab/v1" +M47_REFERENCE_GRAPH_LAB_SCHEMA: Final = "missioncore.reference-perception-graph-lab/v2" M47_REFERENCE_GRAPH_LAB_REPORT_SCHEMA: Final = ( - "missioncore.reference-perception-graph-lab-report/v1" + "missioncore.reference-perception-graph-lab-report/v2" ) M47_REFERENCE_GRAPH_LAB_PREFIX: Final = "m47-reference-graph-lab-" @@ -37,10 +43,12 @@ def publish_m47_reference_graph_lab( *, graph_result_root: Path, visual_result_root: Path, + semantic_result_root: Path, output_root: Path, ) -> M47ReferenceGraphLab: graph = read_reference_graph_result(graph_result_root) visual = read_threat_replay_result(visual_result_root) + semantic = read_semantic_slam_replay_result(semantic_result_root) if not graph.accepted or not visual.accepted: raise M47ReferenceGraphLabError("both graph and visual replay must be accepted") graph_report = graph.report @@ -49,6 +57,9 @@ def publish_m47_reference_graph_lab( parity = _object(graph_report.get("accepted_parity"), "graph parity") mismatches = _object(parity.get("mismatch_counts"), "graph parity mismatches") visual_identity = _object(visual.manifest.get("identity"), "visual identity") + semantic_identity = _object(semantic.manifest.get("identity"), "semantic identity") + semantic_frames = _object(semantic_identity.get("counts"), "semantic counts") + semantic_frame_counts = _object(semantic_frames.get("frames"), "semantic frame counts") if ( parity.get("accepted") is not True or parity.get("expected_frames") != 4489 @@ -68,6 +79,13 @@ def publish_m47_reference_graph_lab( or parity.get("temporal_frames_sha256") != visual_identity.get("temporal_frames_sha256") ): raise M47ReferenceGraphLabError("graph-to-visual parity proof changed") + if ( + semantic_identity.get("base_m4_result_id") != visual.result_id + or semantic_identity.get("source_session_id") != visual_identity.get("source_session_id") + or semantic_frame_counts.get("total") != 4489 + or semantic_frame_counts.get("mask_available") != 4489 + ): + raise M47ReferenceGraphLabError("semantic-to-visual binding proof changed") terminal = _object(graph_report.get("terminal_outcomes"), "terminal outcomes") queues = _object(graph_report.get("queue_high_watermarks"), "queue high watermarks") execution = _object(graph_report.get("execution"), "graph execution") @@ -81,23 +99,30 @@ def publish_m47_reference_graph_lab( } visual_evidence = { "linked_result_id": visual.result_id, + "semantic_result_id": semantic.result_id, "binding": "exact-threat-and-temporal-ledger-parity", + "semantic_binding": "exact-sequence-and-session-time", "timeline_frames": 4489, "shared_recorded_clock": True, "video_camera_3d_plan_available": True, + "camera_semantic_overlay_available": True, "regression_sequences": [138, 274, 1880, 2584], "independent_ground_truth": False, } identity: dict[str, object] = { "schema_version": M47_REFERENCE_GRAPH_LAB_SCHEMA, - "binding_id": "m47-reference-graph-visual-binding/v1", + "binding_id": "m47-reference-graph-visual-binding/v2", "source": { "source_id": "RAVNOVES00", "source_session_id": visual_identity.get("source_session_id"), "graph_result_id": graph.result_id, "visual_result_id": visual.result_id, + "semantic_result_id": semantic.result_id, "temporal_frames_sha256": parity.get("temporal_frames_sha256"), "threat_frames_sha256": parity.get("threat_frames_sha256"), + "semantic_frames_sha256": semantic_identity.get("semantic_frames_sha256"), + "semantic_masks_sha256": semantic_identity.get("semantic_masks_sha256"), + "semantic_taxonomy_sha256": semantic_identity.get("taxonomy_sha256"), }, "method": { "graph_id": graph_manifest.get("graph_id"), @@ -179,6 +204,9 @@ def publish_m47_reference_graph_lab( "graph-runtime.json": graph.result_root / "runtime.json", "visual-manifest.json": visual.result_root / "manifest.json", "visual-report.json": visual.result_root / "report.json", + "semantic-manifest.json": semantic.result_root / SEMANTIC_SLAM_MANIFEST_NAME, + "semantic-report.json": semantic.result_root / SEMANTIC_SLAM_REPORT_NAME, + "semantic-taxonomy.json": semantic.result_root / SEMANTIC_SLAM_TAXONOMY_NAME, } for name, source in sources.items(): shutil.copyfile(source, staging / name) @@ -190,6 +218,9 @@ def publish_m47_reference_graph_lab( "graph-runtime.json": "m47-runtime", "visual-manifest.json": "linked-visual-manifest", "visual-report.json": "linked-visual-report", + "semantic-manifest.json": "linked-semantic-manifest", + "semantic-report.json": "linked-semantic-report", + "semantic-taxonomy.json": "linked-semantic-taxonomy", } schemas = { "graph-manifest.json": graph_manifest.get("schema_version"), @@ -197,6 +228,11 @@ def publish_m47_reference_graph_lab( "graph-runtime.json": graph_runtime.get("schema_version"), "visual-manifest.json": visual.manifest.get("schema_version"), "visual-report.json": visual.report.get("schema_version"), + "semantic-manifest.json": semantic.manifest.get("schema_version"), + "semantic-report.json": semantic.report.get("schema_version"), + "semantic-taxonomy.json": _read_json( + semantic.result_root / SEMANTIC_SLAM_TAXONOMY_NAME + ).get("schema_version"), "report.json": M47_REFERENCE_GRAPH_LAB_REPORT_SCHEMA, } artifacts = [_artifact(staging / name, role, schemas[name]) for name, role in roles.items()] @@ -247,7 +283,7 @@ def read_m47_reference_graph_lab(result_root: Path) -> M47ReferenceGraphLab: ): raise M47ReferenceGraphLabError("M4.7 LAB identity changed") artifacts = manifest.get("artifacts") - if not isinstance(artifacts, list) or len(artifacts) != 6: + if not isinstance(artifacts, list) or len(artifacts) != 9: raise M47ReferenceGraphLabError("M4.7 LAB artifact inventory changed") for value in artifacts: descriptor = _object(value, "LAB artifact") diff --git a/src/k1link/web/e47_semantic_slam_api.py b/src/k1link/web/e47_semantic_slam_api.py index 459f107..49e7186 100644 --- a/src/k1link/web/e47_semantic_slam_api.py +++ b/src/k1link/web/e47_semantic_slam_api.py @@ -137,6 +137,10 @@ def build_e47_semantic_slam_router( "access": "read-only-diagnostic-shadow", } + @router.get("/results/{result_id}") + def get_result(result_id: str) -> dict[str, object]: + return _project_result(result(result_id)) + @router.get("/results/{result_id}/timeline/chunk") def get_timeline_chunk( result_id: str, diff --git a/tests/test_e47_semantic_slam_api.py b/tests/test_e47_semantic_slam_api.py index ba23165..253a866 100644 --- a/tests/test_e47_semantic_slam_api.py +++ b/tests/test_e47_semantic_slam_api.py @@ -173,9 +173,7 @@ def test_catalog_projects_exact_diagnostic_only_view( assert item["status"] == "diagnostic-semantic-slam-shadow" assert item["base_m4_result_id"] == M4_RESULT_ID assert item["provider"]["provider_id"] == "eomt-cityscapes-semantic-control/v1" - assert item["temporal_binding"]["semantic_to_camera"] == ( - "exact-sequence-and-session-time" - ) + assert item["temporal_binding"]["semantic_to_camera"] == ("exact-sequence-and-session-time") assert item["temporal_binding"]["physical_synchronization_proven"] is False assert [entry["label"] for entry in item["taxonomy"]] == ["ambiguous", "road"] assert item["acceptance"] == { @@ -191,6 +189,13 @@ def test_catalog_projects_exact_diagnostic_only_view( assert item["navigation_or_safety_accepted"] is False assert item["actuation_allowed"] is False + get_result = _endpoint( + "/api/v1/laboratory/e47-semantic-slam/results/{result_id}", + root, + ) + exact = get_result(RESULT_ID) + assert exact == item + def test_timeline_chunk_preserves_point_index_space_and_unavailable_sentinel( publication: tuple[Path, Path], diff --git a/tests/test_m47_reference_graph_lab.py b/tests/test_m47_reference_graph_lab.py index cfd5b62..3bffef6 100644 --- a/tests/test_m47_reference_graph_lab.py +++ b/tests/test_m47_reference_graph_lab.py @@ -25,8 +25,10 @@ def test_m47_lab_binds_worker_graph_to_exact_visual_replay( ) -> None: graph_root = tmp_path / "graph" visual_root = tmp_path / "visual" + semantic_root = tmp_path / "semantic" graph_root.mkdir() visual_root.mkdir() + semantic_root.mkdir() runtime = { "schema_version": "missioncore.reference-graph-runtime-identity/v3", "worker_id": "worker-006", @@ -99,6 +101,20 @@ def test_m47_lab_binds_worker_graph_to_exact_visual_replay( }, } visual_report = {"schema_version": "missioncore.perception-threat-replay-report/v2"} + semantic_identity = { + "base_m4_result_id": "m4-threat-replay-" + "6" * 64, + "source_session_id": "20260720T065719Z_viewer_live", + "semantic_frames_sha256": "7" * 64, + "semantic_masks_sha256": "8" * 64, + "taxonomy_sha256": "9" * 64, + "counts": {"frames": {"total": 4489, "mask_available": 4489}}, + } + semantic_manifest = { + "schema_version": "missioncore.e47-semantic-slam-result/v1", + "identity": semantic_identity, + } + semantic_report = {"schema_version": "missioncore.e47-semantic-slam-report/v1"} + semantic_taxonomy = {"schema_version": "missioncore.e47-semantic-taxonomy/v1"} for root, documents in ( ( graph_root, @@ -112,6 +128,14 @@ def test_m47_lab_binds_worker_graph_to_exact_visual_replay( visual_root, {"manifest.json": visual_manifest, "report.json": visual_report}, ), + ( + semantic_root, + { + "manifest.json": semantic_manifest, + "report.json": semantic_report, + "taxonomy.json": semantic_taxonomy, + }, + ), ): for name, document in documents.items(): _write_json(root / name, document) @@ -129,6 +153,12 @@ def test_m47_lab_binds_worker_graph_to_exact_visual_replay( manifest=visual_manifest, report=visual_report, ) + semantic = SimpleNamespace( + result_id="e47-semantic-slam-" + "a" * 64, + result_root=semantic_root, + manifest=semantic_manifest, + report=semantic_report, + ) monkeypatch.setattr( "k1link.laboratory.m47_reference_graph.read_reference_graph_result", lambda _: graph, @@ -137,16 +167,22 @@ def test_m47_lab_binds_worker_graph_to_exact_visual_replay( "k1link.laboratory.m47_reference_graph.read_threat_replay_result", lambda _: visual, ) + monkeypatch.setattr( + "k1link.laboratory.m47_reference_graph.read_semantic_slam_replay_result", + lambda _: semantic, + ) runtime_root = tmp_path / "runtime" result = publish_m47_reference_graph_lab( graph_result_root=graph_root, visual_result_root=visual_root, + semantic_result_root=semantic_root, output_root=runtime_root / "m47" / "reference-graph-labs", ) assert read_m47_reference_graph_lab(result.result_root) == result assert result.report["visual_evidence"]["linked_result_id"] == visual.result_id + assert result.report["visual_evidence"]["semantic_result_id"] == semantic.result_id assert result.report["execution"]["worker_id"] == "worker-006" assert result.report["metrics"]["parity_mismatch_counts"] == mismatch_counts repository_root = Path(__file__).resolve().parents[1] @@ -157,11 +193,11 @@ def test_m47_lab_binds_worker_graph_to_exact_visual_replay( item for item in registry.definitions if item.work_id == "m47-reference-graph-shadow" ) proof = verify_laboratory_evidence_result(definition, result.result_root) - assert proof["artifact_count"] == 6 + assert proof["artifact_count"] == 9 projected = LaboratoryEvidenceReportService(registry, lambda: runtime_root).read( definition.work_id, result.result_id, ) assert projected["raw_report"]["schema_version"] == ( - "missioncore.reference-perception-graph-lab-report/v1" + "missioncore.reference-perception-graph-lab-report/v2" )