fix(lab): preserve replay view and restore SLAM semantics

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 23:37:05 +03:00
parent 46cef8df17
commit 296cf610cd
8 changed files with 230 additions and 45 deletions
@@ -1,6 +1,7 @@
import type { LaboratoryFetch } from "./advancedResults";
const RESULT_ID = /^m49-tgs-full-shadow-[a-f0-9]{64}$/;
const SEMANTIC_RESULT_ID = /^e47-semantic-slam-[a-f0-9]{64}$/;
export type M49TgsFullShadowStateCode = 0 | 1 | 2 | 3;
@@ -10,6 +11,7 @@ export interface M49TgsFullShadowResult {
source: {
sourcePackSha256: string;
linkedVisualResultId: string;
linkedSemanticResultId: string;
};
configuration: {
configSha256: string;
@@ -158,12 +160,20 @@ export async function fetchM49TgsFullShadowResult(
const performance = objectValue(payload.performance, "M49 full performance");
const acceptance = objectValue(payload.acceptance, "M49 full acceptance");
const decision = objectValue(payload.decision, "M49 full decision");
const linkedSemanticResultId = text(
source.linked_semantic_result_id,
"M49 full semantic result",
);
if (!SEMANTIC_RESULT_ID.test(linkedSemanticResultId)) {
throw new M49TgsFullShadowContractError("M49 full semantic result: нарушена идентичность.");
}
return {
resultId: text(payload.result_id, "M49 full result"),
createdAtUtc: text(payload.created_at_utc, "M49 full created"),
source: {
sourcePackSha256: text(source.source_pack_sha256, "M49 full source pack"),
linkedVisualResultId: text(source.linked_visual_result_id, "M49 full visual result"),
linkedSemanticResultId,
},
configuration: {
configSha256: text(configuration.config_sha256, "M49 full config"),
@@ -4,6 +4,10 @@ import type {
RecordedEvidenceSemanticClass,
RecordedEvidenceSemanticPaletteEntry,
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
import {
fetchE47SemanticSlamResult,
type E47SemanticSlamResult,
} from "../../core/laboratory/e47SemanticSlam";
import {
fetchM49TgsFullShadowSpatialChunk,
type M49TgsFullShadowResult,
@@ -46,6 +50,8 @@ function message(error: unknown): string {
export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowResult }) {
const [activeSequence, setActiveSequence] = useState<number | null>(null);
const [semantic, setSemantic] = useState<E47SemanticSlamResult | null>(null);
const [semanticError, setSemanticError] = useState<string | null>(null);
const [chunks, setChunks] = useState<ReadonlyMap<number, M49TgsFullShadowSpatialChunk>>(
() => new Map(),
);
@@ -57,6 +63,27 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
const activeChunkStartRef = useRef(activeChunkStart);
activeChunkStartRef.current = activeChunkStart;
useEffect(() => {
const controller = new AbortController();
setSemantic(null);
setSemanticError(null);
void fetchE47SemanticSlamResult({
resultId: result.source.linkedSemanticResultId,
signal: controller.signal,
})
.then((next) => {
if (controller.signal.aborted) return;
if (!next || next.baseM4ResultId !== result.source.linkedVisualResultId) {
throw new Error("Semantic archive не совпал с исходным M4 timeline.");
}
setSemantic(next);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) setSemanticError(message(caught));
});
return () => controller.abort();
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
useEffect(() => {
for (const controller of inFlightRef.current.values()) controller.abort();
inFlightRef.current.clear();
@@ -142,23 +169,34 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
}, []);
return (
<M4ReplayThreatVisual
resultId={result.source.linkedVisualResultId}
showReviewAnchorBoxes={false}
reviewLabel="4 489 source-paced TGS frames"
evidenceLabel="M49 · full TGS shadow"
initialSpatialMode="3d"
onActiveSequenceChange={handleSequenceChange}
classifiedSpatialLayer={{
label: "TGS full shadow · causal rolling 1 s",
pointLayerLabel: "SOURCE POINTS",
cellLayerLabel: "TGS COSTMAP",
expectedAtSequence: true,
frame: classifiedFrame,
loading,
error,
replacePointCloud: false,
}}
/>
<>
<M4ReplayThreatVisual
resultId={result.source.linkedVisualResultId}
semantic={semantic ? {
resultId: semantic.resultId,
taxonomy: semantic.taxonomy,
} : undefined}
showReviewAnchorBoxes={false}
reviewLabel="4 489 source-paced TGS frames"
evidenceLabel="M49 · full TGS shadow"
initialSpatialMode="3d"
onActiveSequenceChange={handleSequenceChange}
classifiedSpatialLayer={{
label: "TGS full shadow · causal rolling 1 s",
pointLayerLabel: "SOURCE POINTS",
cellLayerLabel: "TGS COSTMAP",
expectedAtSequence: true,
frame: classifiedFrame,
loading,
error,
replacePointCloud: false,
}}
/>
{semanticError ? (
<div className="m4-replay-threat-visual__pane-status" role="alert">
Semantic overlay недоступен: {semanticError}
</div>
) : null}
</>
);
}
@@ -409,6 +409,17 @@ export function M4ReplayThreatVisual({
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
? classifiedSpatialLayer?.frame ?? null
: null;
const lastClassifiedSpatialFrameRef = useRef<{
resultId: string;
frame: M4ReplayClassifiedSpatialFrame;
} | null>(null);
if (classifiedSpatialLayer?.frame) {
lastClassifiedSpatialFrameRef.current = { resultId, frame: classifiedSpatialLayer.frame };
}
const displayedClassifiedSpatialFrame = classifiedSpatialFrame
?? (lastClassifiedSpatialFrameRef.current?.resultId === resultId
? lastClassifiedSpatialFrameRef.current.frame
: null);
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
const mapGravityLocalSensorToBodyGround = useCallback((
@@ -425,13 +436,13 @@ export function M4ReplayThreatVisual({
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
const classifiedPointsBody = useMemo(
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
() => displayedClassifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
mapGravityLocalSensorToBodyGround,
) ?? [],
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
);
const classifiedCellsBody = useMemo<readonly LaboratoryMetricCellEvidence[]>(
() => classifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
() => displayedClassifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
const body = mapGravityLocalSensorToBodyGround([
cell.centerXyM[0],
cell.centerXyM[1],
@@ -451,7 +462,7 @@ export function M4ReplayThreatVisual({
state: cell.state,
};
}) ?? [],
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
);
const classifiedCellCounts = useMemo(() => ({
ground: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
@@ -641,6 +652,16 @@ export function M4ReplayThreatVisual({
>
{classifiedSpatialLayer.pointLayerLabel}
</Button>
<Button
size="compact"
shape="pill"
variant={showLocalSurface ? "primary" : "secondary"}
aria-pressed={showLocalSurface}
title="Bounded local SLAM surface · visual-derived"
onClick={() => setShowLocalSurface((visible) => !visible)}
>
LOCAL SLAM
</Button>
<Button
size="compact"
shape="pill"
@@ -650,6 +671,17 @@ export function M4ReplayThreatVisual({
>
{classifiedSpatialLayer.cellLayerLabel}
</Button>
{semantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
) : null}
</div>
) : (
<div
@@ -952,38 +984,38 @@ export function M4ReplayThreatVisual({
</div>
</div>
) : null}
{(!classifiedSpatialLayer ? spatialFrame : classifiedSpatialFrame) ? (
{(!classifiedSpatialLayer ? spatialFrame : displayedClassifiedSpatialFrame) ? (
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={classifiedSpatialFrame && replaceClassifiedPointCloud
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedPointsBody
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={displayedClassifiedSpatialFrame ? [] : sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={classifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
mode={spatialMode}
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
showLowStep={classifiedSpatialFrame ? false : showLowStep}
pointSemanticClassIds={classifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedSpatialFrame.pointClassIds
showLowStep={displayedClassifiedSpatialFrame ? false : showLowStep}
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.pointClassIds
: alignedSemanticPointIds}
semanticClasses={classifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedSpatialFrame.classes
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.classes
: semanticClasses}
semanticPalette={classifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedSpatialFrame.palette
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.palette
: semanticPalette}
classifiedCells={classifiedCellsBody}
classifiedCellSizeM={classifiedSpatialFrame?.cellSizeM}
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
showClassifiedCells={showRollingMap}
/>
) : null}
{classifiedSpatialLayer && !classifiedSpatialFrame ? (
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
{classifiedSpatialLayer.loading || displayingBufferedFrame
? <span className="busy-indicator" aria-hidden="true" />
@@ -74,12 +74,22 @@ test("M4.9T5 chunk keeps every missing-LiDAR cell explicitly UNOBSERVED", async
});
test("M4.9T5 viewer prefetches 24-frame immutable chunks", async () => {
const source = await readFile(
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
"utf8",
);
const [source, contract] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/core/laboratory/m49TgsFullShadow.ts", import.meta.url),
"utf8",
),
]);
assert.match(source, /const CHUNK_FRAMES = 24/);
assert.match(source, /activeChunkStart \+ CHUNK_FRAMES/);
assert.match(source, /fetchM49TgsFullShadowSpatialChunk/);
assert.match(source, /sampleAvailable: spatial\.sampleAvailable/);
assert.match(source, /fetchE47SemanticSlamResult/);
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/);
assert.match(source, /semantic=\{semantic \? \{/);
assert.match(contract, /linked_semantic_result_id/);
});
@@ -801,12 +801,15 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /showLocalSurface/);
assert.match(
visual,
/pointCloudBodyXyzM=\{classifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: activeSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: activeSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
);
assert.match(
visual,
/const activeSpatialFrame = spatialFrame\?\.sequence === timelineFrame\.activeSequence[\s\S]*const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence/,
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame[\s\S]*lastClassifiedSpatialFrameRef/,
);
assert.match(visual, /localSurfaceBodyXyzM=\{localSurface\.pointsBodyXyzM\}/);
assert.match(visual, /showLocalSurface=\{showLocalSurface\}/);
assert.match(visual, /\{semantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
assert.match(visual, /все 2 244 TGS-ячейки явно UNOBSERVED/);
assert.match(
visual,
@@ -20,13 +20,14 @@ correct for vegetation, terrain, gaps or the future vehicle envelope.
| Item | Identity |
| --- | --- |
| LAB result | `m49-tgs-full-shadow-0faeaaf3aba8dccae974eab51ff9cccf264a13ec285abb1920c1e5a09a7e87bc` |
| LAB result | `m49-tgs-full-shadow-ef98de7db7596d48e8c8c0549ce68e6704ee03e87c8c4bcf1e3e748b7ccb032e` |
| Worker run | `Worker 006 / ravnoves00-full-001` |
| Mission Core revision used by Worker | `40c850b167dda366d8aa45d828520168affaf9fd` |
| Deterministic Worker artifact | `5e0ea16c7a5cc760463836718b0cd8b0006ffc4b202e5f706a20a86ef2f912ab` |
| Source pack SHA-256 | `0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944` |
| TGS config SHA-256 | `c2e07010aaee78259d36c057962d6bfb885349251ff7356d867e5813e632881c` |
| Linked visual result | `m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324` |
| Linked semantic result | `e47-semantic-slam-f8c53ba12e9719856195890c7dcd2ba6c114434674159f948064368012e6b3bc` |
The profile is a `0.45 m`, `12 m` radius, `1 s` causal rolling
`map-gravity-local` costmap. The four states remain separate:
@@ -62,7 +63,9 @@ source points. This preserves chronology without inventing free space.
The immutable result is published on the canonical Mission Core service at
port `8000`. The LAB reuses the recorded camera timeline and exposes independent
`SOURCE POINTS`, `TGS COSTMAP`, `3D` and `PLAN` controls.
`SOURCE POINTS`, `LOCAL SLAM`, `TGS COSTMAP`, `SEMANTICS`, `3D` and `PLAN`
controls. The semantic mask is the exact E47 full-route archive bound to the
same M4 camera result; it remains diagnostic and does not change TGS states.
The first implementation fetched one large JSON frame at a time. Browser QA
showed that this was only intermittently exact at `1×`. The published viewer
@@ -73,6 +76,14 @@ showed the loading placeholder. A real missing-LiDAR frame was separately
accepted with `0 source points`, `2,244 unobserved`, `0 occupied` and no retained
previous cloud.
A follow-up playback acceptance fixed a UI lifecycle defect that remounted the
Three.js scene while the next TGS frame crossed the React buffer boundary. The
scene now retains the last sealed classified frame until the exact next frame
arrives, so orbit controls and the operator-selected view survive continuous
playback. Twelve consecutive `250 ms` playback samples kept one mounted 3D
canvas, a visible bounded `LOCAL SLAM` surface and the synchronized E47 video
mask. Pausing is no longer required to rotate, pan or zoom the 3D view.
## Sealed evidence
| File | Bytes | SHA-256 |
@@ -79,6 +79,7 @@ def seal_m49_tgs_full_shadow(
destination_root: Path,
profile_path: Path,
linked_visual_result_id: str,
linked_semantic_result_id: str,
created_at_utc: str | None = None,
) -> M49TgsFullShadowResult:
source = source_root.expanduser().resolve(strict=True)
@@ -115,6 +116,11 @@ def seal_m49_tgs_full_shadow(
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
):
raise M49TgsFullShadowError("linked visual result is invalid")
if (
not linked_semantic_result_id.startswith("e47-semantic-slam-")
or len(linked_semantic_result_id) != len("e47-semantic-slam-") + 64
):
raise M49TgsFullShadowError("linked semantic result is invalid")
for name in EVIDENCE_FILES:
path = source / name
proof = worker.get("files", {}).get(name, {})
@@ -131,6 +137,7 @@ def seal_m49_tgs_full_shadow(
"input_manifest_sha256": worker["input_manifest_sha256"],
"config_sha256": worker["config_sha256"],
"linked_visual_result_id": linked_visual_result_id,
"linked_semantic_result_id": linked_semantic_result_id,
"files": {name: worker["files"][name]["sha256"] for name in EVIDENCE_FILES},
"authority": {
"commands_enabled": False,
@@ -154,6 +161,7 @@ def seal_m49_tgs_full_shadow(
"source_session_id": "20260720T065719Z_viewer_live",
"source_pack_sha256": worker["source_pack_sha256"],
"linked_visual_result_id": linked_visual_result_id,
"linked_semantic_result_id": linked_semantic_result_id,
},
"configuration": {
"profile_id": profile["profile_id"],
@@ -211,6 +219,7 @@ def seal_m49_tgs_full_shadow(
"visual_review": {
"instrument": "m4-canonical-reference-graph",
"linked_visual_result_id": linked_visual_result_id,
"linked_semantic_result_id": linked_semantic_result_id,
"frame_count": 4489,
"state_codes": profile["state_codes"],
},
+72
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import hashlib
import importlib.util
import json
import sys
@@ -7,6 +8,7 @@ from pathlib import Path
import numpy as np
from k1link.laboratory.m49_tgs_full_shadow import seal_m49_tgs_full_shadow
from k1link.web import m49_tgs_full_shadow_api as full_shadow_api
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
@@ -59,6 +61,76 @@ def test_full_shadow_worker_artifact_is_deterministic_and_cpu_only(tmp_path: Pat
assert "gpu_requested = $false" in runner_text
def test_full_shadow_seal_binds_visual_and_semantic_timelines(tmp_path: Path) -> None:
source = tmp_path / "worker"
source.mkdir()
files: dict[str, dict[str, object]] = {}
for name in (
"costmap-cell-centers-xy-m.npy",
"costmap-cell-indices-xy.npy",
"costmap-states.npy",
"costmap-z-bounds-m.npy",
"frames.ndjson",
):
payload = f"sealed:{name}\n".encode()
(source / name).write_bytes(payload)
files[name] = {
"bytes": len(payload),
"sha256": hashlib.sha256(payload).hexdigest(),
}
worker = {
"schema_version": "missioncore.m49-tgs-full-shadow-result/v1",
"status": "passed",
"source_pack_sha256": "a" * 64,
"input_manifest_sha256": "b" * 64,
"config_sha256": "c" * 64,
"timeline": {
"frame_count": 4489,
"available_lidar_frame_count": 3928,
"missing_lidar_frame_count": 561,
},
"point_accounting": {"unaccounted": 0},
"costmap": {"cell_size_m": 0.45, "radius_m": 12.0},
"performance": {},
"acceptance": {},
"files": files,
}
(source / "result.json").write_text(json.dumps(worker), encoding="utf-8")
(source / "worker-summary.json").write_text(json.dumps({
"schema_version": "missioncore.m49-tgs-full-shadow-worker-summary/v1",
"gpu_requested": False,
"aos_used": False,
"all_timeline_frames_accounted": True,
"all_eligible_points_accounted": True,
"canonical_triton_health": "healthy",
"canonical_triton_id": "triton",
"wall_seconds": 1.0,
}), encoding="utf-8")
profile = tmp_path / "profile.json"
profile.write_text(json.dumps({
"schema_version": "missioncore.m49-tgs-full-shadow-profile/v1",
"profile_id": "test",
"profile": {"history_seconds": 1.0},
"costmap": {"state_priority": ["NONGROUND_OCCUPIED"]},
"state_codes": {"UNOBSERVED": 0},
}), encoding="utf-8")
visual = "m4-threat-replay-" + "d" * 64
semantic = "e47-semantic-slam-" + "e" * 64
sealed = seal_m49_tgs_full_shadow(
source_root=source,
destination_root=tmp_path / "results",
profile_path=profile,
linked_visual_result_id=visual,
linked_semantic_result_id=semantic,
created_at_utc="2026-08-26T20:27:19Z",
)
assert sealed.report["source"]["linked_visual_result_id"] == visual
assert sealed.report["source"]["linked_semantic_result_id"] == semantic
assert sealed.manifest["identity"]["linked_semantic_result_id"] == semantic
def test_full_shadow_chunk_contract_keeps_missing_lidar_unobserved(
monkeypatch,
tmp_path: Path,