fix(lab): preserve replay view and restore SLAM semantics
This commit is contained in:
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user