fix(lab): restore canonical RAV004 replay
This commit is contained in:
@@ -81,7 +81,9 @@ export function LaboratoryRecordedClipPlayer({
|
|||||||
sourceCount,
|
sourceCount,
|
||||||
cameraRef,
|
cameraRef,
|
||||||
cameraOverlay,
|
cameraOverlay,
|
||||||
|
cameraControls,
|
||||||
alternativeScene,
|
alternativeScene,
|
||||||
|
spatialControls,
|
||||||
onSequenceChange,
|
onSequenceChange,
|
||||||
onPlayingChange,
|
onPlayingChange,
|
||||||
onPlaybackRateChange,
|
onPlaybackRateChange,
|
||||||
@@ -97,7 +99,9 @@ export function LaboratoryRecordedClipPlayer({
|
|||||||
sourceCount: number;
|
sourceCount: number;
|
||||||
cameraRef?: RefObject<HTMLDivElement | null>;
|
cameraRef?: RefObject<HTMLDivElement | null>;
|
||||||
cameraOverlay?: ReactNode;
|
cameraOverlay?: ReactNode;
|
||||||
|
cameraControls?: ReactNode;
|
||||||
alternativeScene?: ReactNode;
|
alternativeScene?: ReactNode;
|
||||||
|
spatialControls?: ReactNode;
|
||||||
onSequenceChange: (sequence: number) => void;
|
onSequenceChange: (sequence: number) => void;
|
||||||
onPlayingChange: (playing: boolean) => void;
|
onPlayingChange: (playing: boolean) => void;
|
||||||
onPlaybackRateChange: (rate: number) => void;
|
onPlaybackRateChange: (rate: number) => void;
|
||||||
@@ -173,6 +177,14 @@ export function LaboratoryRecordedClipPlayer({
|
|||||||
aria-hidden={cameraPresentation === "primary"}
|
aria-hidden={cameraPresentation === "primary"}
|
||||||
>
|
>
|
||||||
{alternativeScene}
|
{alternativeScene}
|
||||||
|
{spatialControls ? (
|
||||||
|
<div
|
||||||
|
className="laboratory-recorded-clip-player__pane-controls"
|
||||||
|
data-pane="spatial"
|
||||||
|
>
|
||||||
|
{spatialControls}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
const cameraPane = (
|
const cameraPane = (
|
||||||
@@ -195,6 +207,14 @@ export function LaboratoryRecordedClipPlayer({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{cameraPresentation !== "hidden" ? cameraOverlay : null}
|
{cameraPresentation !== "hidden" ? cameraOverlay : null}
|
||||||
|
{cameraPresentation !== "hidden" && cameraControls ? (
|
||||||
|
<div
|
||||||
|
className="laboratory-recorded-clip-player__pane-controls"
|
||||||
|
data-pane="camera"
|
||||||
|
>
|
||||||
|
{cameraControls}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export interface VegetationFullRouteLayer {
|
|||||||
export interface VegetationFullRouteReview {
|
export interface VegetationFullRouteReview {
|
||||||
sourceId: "RAVNOVES004TREE";
|
sourceId: "RAVNOVES004TREE";
|
||||||
sessionId: "20260828T130511Z_viewer_live";
|
sessionId: "20260828T130511Z_viewer_live";
|
||||||
|
linkedRouteReviewResultId: string;
|
||||||
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0";
|
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0";
|
||||||
sourceJobInputSha256: string;
|
sourceJobInputSha256: string;
|
||||||
sourceStreamSha256: string;
|
sourceStreamSha256: string;
|
||||||
@@ -692,6 +693,15 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
|
|||||||
"recorded-camera-eb2783c5480d56bda07c8af0",
|
"recorded-camera-eb2783c5480d56bda07c8af0",
|
||||||
"vegetation.route_full_review.source_job_id",
|
"vegetation.route_full_review.source_job_id",
|
||||||
);
|
);
|
||||||
|
const linkedRouteReviewResultId = textValue(
|
||||||
|
row.linked_route_review_result_id,
|
||||||
|
"vegetation.route_full_review.linked_route_review_result_id",
|
||||||
|
);
|
||||||
|
if (!RESULT_ID.test(linkedRouteReviewResultId)) {
|
||||||
|
throw new VegetationShadowContractError(
|
||||||
|
"vegetation.route_full_review: linked route review identity invalid.",
|
||||||
|
);
|
||||||
|
}
|
||||||
exact(row.frame_count, 6830, "vegetation.route_full_review.frame_count");
|
exact(row.frame_count, 6830, "vegetation.route_full_review.frame_count");
|
||||||
exact(row.width, 800, "vegetation.route_full_review.width");
|
exact(row.width, 800, "vegetation.route_full_review.width");
|
||||||
exact(row.height, 600, "vegetation.route_full_review.height");
|
exact(row.height, 600, "vegetation.route_full_review.height");
|
||||||
@@ -810,6 +820,7 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
|
|||||||
return {
|
return {
|
||||||
sourceId: "RAVNOVES004TREE",
|
sourceId: "RAVNOVES004TREE",
|
||||||
sessionId: "20260828T130511Z_viewer_live",
|
sessionId: "20260828T130511Z_viewer_live",
|
||||||
|
linkedRouteReviewResultId,
|
||||||
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0",
|
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||||
sourceJobInputSha256,
|
sourceJobInputSha256,
|
||||||
sourceStreamSha256,
|
sourceStreamSha256,
|
||||||
|
|||||||
@@ -33,6 +33,29 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.laboratory-recorded-clip-player__pane-controls {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
top: 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
max-width: calc(100% - 1.2rem);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
border-radius: var(--nodedc-radius-control-pill);
|
||||||
|
background: var(--nodedc-floating-surface);
|
||||||
|
padding: 0.28rem;
|
||||||
|
backdrop-filter: blur(var(--nodedc-blur-control));
|
||||||
|
}
|
||||||
|
|
||||||
|
.laboratory-recorded-clip-player__pane-controls[data-pane="spatial"] {
|
||||||
|
right: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laboratory-recorded-clip-player__pane-controls[data-pane="camera"] {
|
||||||
|
left: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.laboratory-recorded-clip-player__split
|
.laboratory-recorded-clip-player__split
|
||||||
> .nodedc-split-pane__separator::before {
|
> .nodedc-split-pane__separator::before {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { Button, SegmentedControl } from "@nodedc/ui-react";
|
||||||
|
|
||||||
|
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||||
|
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
|
||||||
|
import { RerunViewport } from "../../components/RerunViewport";
|
||||||
import {
|
import {
|
||||||
LaboratoryEvidence,
|
LaboratoryEvidence,
|
||||||
LaboratoryResultSummary,
|
LaboratoryResultSummary,
|
||||||
@@ -7,7 +11,18 @@ import {
|
|||||||
LaboratoryWorkTemplate,
|
LaboratoryWorkTemplate,
|
||||||
} from "../../components/laboratory/LaboratoryPresentation";
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
import {
|
import {
|
||||||
|
RecordedEvidenceSemanticMaskOverlay,
|
||||||
|
type RecordedEvidenceSemanticClass,
|
||||||
|
type RecordedEvidenceSemanticPaletteEntry,
|
||||||
|
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||||
|
import {
|
||||||
|
fetchVegetationShadowResult,
|
||||||
|
vegetationFullRouteMaskUrl,
|
||||||
vegetationVideoMaskUrl,
|
vegetationVideoMaskUrl,
|
||||||
|
type VegetationFullRouteLayer,
|
||||||
|
type VegetationFullRouteReview,
|
||||||
|
type VegetationMixedRouteCase,
|
||||||
|
type VegetationMixedRouteReview,
|
||||||
type VegetationShadowResult,
|
type VegetationShadowResult,
|
||||||
} from "../../core/laboratory/vegetationShadow";
|
} from "../../core/laboratory/vegetationShadow";
|
||||||
import {
|
import {
|
||||||
@@ -15,11 +30,480 @@ import {
|
|||||||
type M49TgsFullShadowResult,
|
type M49TgsFullShadowResult,
|
||||||
} from "../../core/laboratory/m49TgsFullShadow";
|
} from "../../core/laboratory/m49TgsFullShadow";
|
||||||
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||||
|
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
|
||||||
|
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
|
||||||
|
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||||
|
import {
|
||||||
|
recordedSessionRerunProfile,
|
||||||
|
type RerunPlaybackController,
|
||||||
|
} from "../../core/observation/viewerProfile";
|
||||||
|
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
||||||
|
import { defaultSceneSettings, type SceneSettings } from "../../sceneSettings";
|
||||||
|
import {
|
||||||
|
M48EvidenceModeRail,
|
||||||
|
type M48BlindEvidenceMode,
|
||||||
|
} from "./annotation/M48EvidenceModeControls";
|
||||||
|
|
||||||
function decimal(value: number, digits = 1): string {
|
function decimal(value: number, digits = 1): string {
|
||||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FULL_ROUTE_SEMANTIC_MODES = [
|
||||||
|
{ value: "city", label: "ГОРОД · EoMT" },
|
||||||
|
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function semanticPresentation(layer: VegetationFullRouteLayer): {
|
||||||
|
classes: readonly RecordedEvidenceSemanticClass[];
|
||||||
|
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
classes: layer.taxonomy.map((item) => ({ id: item.classId, label: item.label })),
|
||||||
|
palette: layer.taxonomy.map((item) => ({
|
||||||
|
classId: item.classId,
|
||||||
|
color: item.classId === 0
|
||||||
|
? { kind: "transparent" as const }
|
||||||
|
: { kind: "diagnostic" as const, rgb: item.colorRgb },
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestTgsCase(
|
||||||
|
cases: readonly VegetationMixedRouteCase[],
|
||||||
|
sequence: number,
|
||||||
|
): VegetationMixedRouteCase | null {
|
||||||
|
return cases.reduce<VegetationMixedRouteCase | null>((nearest, candidate) => (
|
||||||
|
!nearest
|
||||||
|
|| Math.abs(candidate.sourceSequence - sequence)
|
||||||
|
< Math.abs(nearest.sourceSequence - sequence)
|
||||||
|
? candidate
|
||||||
|
: nearest
|
||||||
|
), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FullRouteReviewEvidence({
|
||||||
|
resultId,
|
||||||
|
review,
|
||||||
|
}: {
|
||||||
|
resultId: string;
|
||||||
|
review: VegetationFullRouteReview;
|
||||||
|
}) {
|
||||||
|
const [sequence, setSequence] = useState(1);
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
|
const [playbackRate, setPlaybackRate] = useState(1);
|
||||||
|
const [semanticLayer, setSemanticLayer] = useState<"city" | "vegetation">("vegetation");
|
||||||
|
const [showCameraSemantic, setShowCameraSemantic] = useState(true);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [evidenceMode, setEvidenceMode] = useState<M48BlindEvidenceMode>("3d");
|
||||||
|
const [cameraVisible, setCameraVisible] = useState(true);
|
||||||
|
const [sceneSettings, setSceneSettings] = useState<SceneSettings>(() => ({
|
||||||
|
...defaultSceneSettings,
|
||||||
|
accumulationSeconds: 12,
|
||||||
|
showPoints: true,
|
||||||
|
showTrajectory: true,
|
||||||
|
}));
|
||||||
|
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
|
||||||
|
const [replayLaunch, setReplayLaunch] = useState<ObservationSessionReplayLaunch | null>(null);
|
||||||
|
const [videoError, setVideoError] = useState<string | null>(null);
|
||||||
|
const [linkedReview, setLinkedReview] = useState<VegetationMixedRouteReview | null>(null);
|
||||||
|
const [linkedReviewError, setLinkedReviewError] = useState<string | null>(null);
|
||||||
|
const spatialControllerRef = useRef<RerunPlaybackController | null>(null);
|
||||||
|
const frames = useMemo(
|
||||||
|
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
|
||||||
|
sequence: index + 1,
|
||||||
|
sourceTimeNs,
|
||||||
|
})),
|
||||||
|
[review.frameSourceTimesNs],
|
||||||
|
);
|
||||||
|
const layer = review[semanticLayer];
|
||||||
|
const semantic = useMemo(() => semanticPresentation(layer), [layer]);
|
||||||
|
const maskSequence = sequence - 1;
|
||||||
|
const prefetchSrcs = useMemo(() => showCameraSemantic
|
||||||
|
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
|
||||||
|
.filter((candidate) => candidate < review.frameCount)
|
||||||
|
.map((candidate) => vegetationFullRouteMaskUrl(resultId, semanticLayer, candidate))
|
||||||
|
: [], [maskSequence, resultId, review.frameCount, semanticLayer, showCameraSemantic]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setVideoSource(null);
|
||||||
|
setReplayLaunch(null);
|
||||||
|
setVideoError(null);
|
||||||
|
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
|
||||||
|
.then((launch) => {
|
||||||
|
const source = recordedObservationSources(launch).find((candidate) => (
|
||||||
|
candidate.id === review.recordedMediaSourceId
|
||||||
|
&& candidate.modality === "video"
|
||||||
|
&& candidate.semanticChannelId === "camera.video.recorded"
|
||||||
|
&& candidate.delivery?.kind === "recorded-fmp4-manifest"
|
||||||
|
&& candidate.delivery.manifestGenerationSha256 === review.recordedMediaGenerationSha256
|
||||||
|
&& candidate.delivery.timelineStartSeconds === review.timelineStartSeconds
|
||||||
|
&& candidate.delivery.timelineEndSeconds >= review.timelineEndSeconds
|
||||||
|
));
|
||||||
|
if (!source) {
|
||||||
|
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
|
||||||
|
}
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
setVideoSource(source);
|
||||||
|
setReplayLaunch(launch);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
setVideoError(caught instanceof Error ? caught.message : "Записанное видео недоступно.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [
|
||||||
|
review.recordedMediaGenerationSha256,
|
||||||
|
review.recordedMediaSourceId,
|
||||||
|
review.sessionId,
|
||||||
|
review.timelineEndSeconds,
|
||||||
|
review.timelineStartSeconds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setLinkedReview(null);
|
||||||
|
setLinkedReviewError(null);
|
||||||
|
void fetchVegetationShadowResult(review.linkedRouteReviewResultId, {
|
||||||
|
signal: controller.signal,
|
||||||
|
}).then((result) => {
|
||||||
|
if (
|
||||||
|
!result.routeReview
|
||||||
|
|| result.routeReview.sourceId !== review.sourceId
|
||||||
|
|| result.routeReview.sessionId !== review.sessionId
|
||||||
|
) {
|
||||||
|
throw new Error("TGS anchors имеют другую source identity.");
|
||||||
|
}
|
||||||
|
if (!controller.signal.aborted) setLinkedReview(result.routeReview);
|
||||||
|
}).catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
setLinkedReviewError(caught instanceof Error ? caught.message : "TGS anchors недоступны.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [review.linkedRouteReviewResultId, review.sessionId, review.sourceId]);
|
||||||
|
|
||||||
|
const spatialProfile = useMemo(() => replayLaunch ? recordedSessionRerunProfile({
|
||||||
|
sourceUrl: replayLaunch.viewerSourceUrl,
|
||||||
|
artifact: {
|
||||||
|
sourceUrl: replayLaunch.sourceUrl,
|
||||||
|
viewerSourceUrl: replayLaunch.viewerSourceUrl,
|
||||||
|
byteLength: replayLaunch.byteLength,
|
||||||
|
sha256: replayLaunch.sha256,
|
||||||
|
},
|
||||||
|
autoplayWhenReady: false,
|
||||||
|
presentationGate: "ready",
|
||||||
|
expectedTimelineStartSeconds: replayLaunch.timelineStartSeconds,
|
||||||
|
expectedTimelineEndSeconds: replayLaunch.timelineEndSeconds,
|
||||||
|
initialPlaybackStartSeconds: review.timelineStartSeconds,
|
||||||
|
view: "spatial",
|
||||||
|
viewResetGeneration: 0,
|
||||||
|
followTrajectory: false,
|
||||||
|
perceptionLayers: {
|
||||||
|
enabled: false,
|
||||||
|
detections2d: false,
|
||||||
|
segmentation: false,
|
||||||
|
cuboids3d: false,
|
||||||
|
},
|
||||||
|
perceptionRetryGeneration: 0,
|
||||||
|
lockPerceptionCameraInteraction: false,
|
||||||
|
}) : null, [replayLaunch, review.timelineStartSeconds]);
|
||||||
|
const activeFrame = frames.find((candidate) => candidate.sequence === sequence)
|
||||||
|
?? frames[0]
|
||||||
|
?? null;
|
||||||
|
const activeSourceTimeNsRef = useRef(activeFrame?.sourceTimeNs ?? null);
|
||||||
|
activeSourceTimeNsRef.current = activeFrame?.sourceTimeNs ?? null;
|
||||||
|
const handleSpatialControllerChange = useCallback((controller: RerunPlaybackController | null) => {
|
||||||
|
spatialControllerRef.current = controller;
|
||||||
|
const sourceTimeNs = activeSourceTimeNsRef.current;
|
||||||
|
if (!controller || sourceTimeNs === null) return;
|
||||||
|
controller.setPlaying(false);
|
||||||
|
controller.seek(sourceTimeNs);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = spatialControllerRef.current;
|
||||||
|
if (!controller || !activeFrame) return;
|
||||||
|
controller.setPlaying(false);
|
||||||
|
controller.seek(activeFrame.sourceTimeNs);
|
||||||
|
}, [activeFrame]);
|
||||||
|
|
||||||
|
const selectedTgsCase = linkedReview
|
||||||
|
? nearestTgsCase(linkedReview.cases, sequence)
|
||||||
|
: null;
|
||||||
|
const selectTgsPlan = useCallback(() => {
|
||||||
|
if (!linkedReview) return;
|
||||||
|
const item = nearestTgsCase(linkedReview.cases, sequence);
|
||||||
|
if (!item) return;
|
||||||
|
setPlaying(false);
|
||||||
|
setSequence(item.sourceSequence);
|
||||||
|
setEvidenceMode("plan");
|
||||||
|
}, [linkedReview, sequence]);
|
||||||
|
const handleEvidenceModeChange = useCallback((nextMode: M48BlindEvidenceMode) => {
|
||||||
|
if (nextMode === "plan") {
|
||||||
|
selectTgsPlan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEvidenceMode(nextMode);
|
||||||
|
}, [selectTgsPlan]);
|
||||||
|
const cameraPresentation = evidenceMode === "camera"
|
||||||
|
? "primary"
|
||||||
|
: cameraVisible ? "companion" : "hidden";
|
||||||
|
const spatialScene = evidenceMode === "plan" ? (
|
||||||
|
selectedTgsCase ? (
|
||||||
|
<div className="recorded-evidence-image-scene">
|
||||||
|
<img
|
||||||
|
src={selectedTgsCase.assets.tgs}
|
||||||
|
alt={`TGS costmap · sequence ${selectedTgsCase.sourceSequence}`}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
<div className="m48-clip-player__pane-label" data-pane="spatial">
|
||||||
|
TGS COSTMAP · ЯКОРЬ {selectedTgsCase.sourceSequence} · {selectedTgsCase.tgs.occupiedCells} OCCUPIED
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
|
{linkedReviewError ?? "Открываем sealed TGS anchors…"}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : spatialProfile ? (
|
||||||
|
<RerunViewport
|
||||||
|
profile={spatialProfile}
|
||||||
|
sceneSettings={sceneSettings}
|
||||||
|
onPlaybackControllerChange={handleSpatialControllerChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
|
Открываем sealed RRD, source points и SLAM trajectory…
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LaboratoryEvidenceViewer
|
||||||
|
label="RAVNOVES004TREE full recorded review"
|
||||||
|
className="m48-atlas-visual"
|
||||||
|
mode={semanticLayer}
|
||||||
|
modes={FULL_ROUTE_SEMANTIC_MODES}
|
||||||
|
expanded={expanded}
|
||||||
|
onModeChange={setSemanticLayer}
|
||||||
|
onExpandedChange={setExpanded}
|
||||||
|
modeControlsVisible={false}
|
||||||
|
chromeLayout="stacked"
|
||||||
|
>
|
||||||
|
<div className="m48-evidence-stage">
|
||||||
|
{videoSource ? (
|
||||||
|
<LaboratoryRecordedClipPlayer
|
||||||
|
source={videoSource}
|
||||||
|
segmentCount={review.frameCount}
|
||||||
|
frames={frames}
|
||||||
|
sequence={sequence}
|
||||||
|
playing={playing}
|
||||||
|
playbackRate={playbackRate}
|
||||||
|
cameraPresentation={cameraPresentation}
|
||||||
|
continuousPlayback
|
||||||
|
sourceCount={4}
|
||||||
|
onSequenceChange={setSequence}
|
||||||
|
onPlayingChange={setPlaying}
|
||||||
|
onPlaybackRateChange={setPlaybackRate}
|
||||||
|
alternativeScene={spatialScene}
|
||||||
|
cameraControls={(
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={showCameraSemantic ? "primary" : "secondary"}
|
||||||
|
aria-pressed={showCameraSemantic}
|
||||||
|
onClick={() => setShowCameraSemantic((visible) => !visible)}
|
||||||
|
>
|
||||||
|
SEMANTICS
|
||||||
|
</Button>
|
||||||
|
<SegmentedControl
|
||||||
|
value={semanticLayer}
|
||||||
|
items={[...FULL_ROUTE_SEMANTIC_MODES]}
|
||||||
|
label="Источник семантики"
|
||||||
|
onChange={(value) => {
|
||||||
|
setSemanticLayer(value);
|
||||||
|
setShowCameraSemantic(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
spatialControls={(
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={sceneSettings.showPoints ? "primary" : "secondary"}
|
||||||
|
aria-pressed={sceneSettings.showPoints}
|
||||||
|
onClick={() => {
|
||||||
|
setEvidenceMode("3d");
|
||||||
|
setSceneSettings((settings) => ({
|
||||||
|
...settings,
|
||||||
|
showPoints: !settings.showPoints,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
SOURCE POINTS
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={sceneSettings.showTrajectory ? "primary" : "secondary"}
|
||||||
|
aria-pressed={sceneSettings.showTrajectory}
|
||||||
|
onClick={() => {
|
||||||
|
setEvidenceMode("3d");
|
||||||
|
setSceneSettings((settings) => ({
|
||||||
|
...settings,
|
||||||
|
showTrajectory: !settings.showTrajectory,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
LOCAL SLAM
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={evidenceMode === "plan" ? "primary" : "secondary"}
|
||||||
|
aria-pressed={evidenceMode === "plan"}
|
||||||
|
disabled={!linkedReview}
|
||||||
|
title={linkedReviewError ?? "10 sealed causal TGS anchors; continuous TGS отсутствует"}
|
||||||
|
onClick={selectTgsPlan}
|
||||||
|
>
|
||||||
|
TGS COSTMAP
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant="secondary"
|
||||||
|
disabled
|
||||||
|
title="Для RAV004 не опубликован point-aligned 3D semantic archive"
|
||||||
|
>
|
||||||
|
SEMANTICS
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
cameraOverlay={(
|
||||||
|
<>
|
||||||
|
<div className="m48-clip-player__pane-label" data-pane="camera">
|
||||||
|
{showCameraSemantic
|
||||||
|
? `${semanticLayer === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`
|
||||||
|
: `SOURCE · КАДР ${sequence}/${review.frameCount}`}
|
||||||
|
</div>
|
||||||
|
{showCameraSemantic ? (
|
||||||
|
<div className="m48-clip-player__overlay">
|
||||||
|
<RecordedEvidenceSemanticMaskOverlay
|
||||||
|
src={vegetationFullRouteMaskUrl(resultId, semanticLayer, maskSequence)}
|
||||||
|
prefetchSrcs={prefetchSrcs}
|
||||||
|
imageWidth={review.width}
|
||||||
|
imageHeight={review.height}
|
||||||
|
classes={semantic.classes}
|
||||||
|
palette={semantic.palette}
|
||||||
|
opacity={0.76}
|
||||||
|
ariaLabel={`${layer.name} semantic prediction`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
|
||||||
|
{videoError ?? "Открываем автономный RAVNOVES004TREE source…"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{videoSource ? (
|
||||||
|
<M48EvidenceModeRail
|
||||||
|
mode={evidenceMode}
|
||||||
|
cameraVisible={cameraVisible}
|
||||||
|
spatialAvailable={Boolean(spatialProfile)}
|
||||||
|
planAvailable={Boolean(linkedReview)}
|
||||||
|
onModeChange={handleEvidenceModeChange}
|
||||||
|
onCameraVisibleChange={setCameraVisible}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</LaboratoryEvidenceViewer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FullRouteReviewResult({
|
||||||
|
rigLabel,
|
||||||
|
resultId,
|
||||||
|
review,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
resultId: string;
|
||||||
|
review: VegetationFullRouteReview;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<LaboratoryWorkTemplate
|
||||||
|
summary={(
|
||||||
|
<LaboratorySummary
|
||||||
|
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
||||||
|
description="Общий recorded-LAB шаблон воспроизводит запись с травой и оврагами: RIGHT camera, исходное облако, SLAM trajectory, два независимых semantic-слоя и десять реально просчитанных TGS-якорей."
|
||||||
|
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||||
|
statusTone="warning"
|
||||||
|
facts={[
|
||||||
|
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} frames` },
|
||||||
|
{ label: "3D", value: "1437 source point chunks · 2825 SLAM poses · recorded RRD" },
|
||||||
|
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} fps` },
|
||||||
|
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} fps` },
|
||||||
|
{ label: "TGS", value: "10 sealed causal anchors · continuous costmap отсутствует" },
|
||||||
|
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||||
|
]}
|
||||||
|
brief={{
|
||||||
|
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
|
||||||
|
approach: "Одна recorded timeline открывается общим LAB viewer. Camera и RRD синхронизированы; EoMT/DDRNet переключаются на камере, source points и SLAM trajectory — в 3D, TGS — только на десяти запечатанных якорях.",
|
||||||
|
principalResult: "RAV004 больше не подменяется RAV00: доступна полная исходная запись и её реальные пространственные слои.",
|
||||||
|
limitation: "Ручной truth, continuous TGS и point-aligned 3D semantics отсутствуют. Один повреждённый H.264-пакет на позиции 6092 заменён предыдущим декодированным кадром и отражён в proof.",
|
||||||
|
}}
|
||||||
|
method={{
|
||||||
|
completeness: "complete",
|
||||||
|
executionClass: "ai-inference",
|
||||||
|
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||||
|
components: [
|
||||||
|
{ kind: "algorithm", name: "Recorded source points + SLAM trajectory", version: "sealed RRD", role: "spatial source evidence", identitySha256: null },
|
||||||
|
{ kind: "model", name: review.city.name, version: "sealed Worker 006 run", role: "urban semantic review", identitySha256: null },
|
||||||
|
{ kind: "model", name: review.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
|
||||||
|
{ kind: "algorithm", name: "Causal TGS", version: "10 linked route anchors", role: "bounded geometric evidence", identitySha256: null },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="CANONICAL RECORDED LAB · RAVNOVES004TREE"
|
||||||
|
title="CAMERA + SOURCE POINTS + LOCAL SLAM + TGS COSTMAP + SEMANTICS · 6830/6830"
|
||||||
|
kind="recorded-replay"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<FullRouteReviewEvidence resultId={resultId} review={review} />
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
|
<LaboratoryResultSummary
|
||||||
|
title="Полный RAV004 visual review восстановлен; управление не авторизовано"
|
||||||
|
status="Recorded evidence ready · navigation/actuation OFF"
|
||||||
|
statusTone="warning"
|
||||||
|
metrics={[
|
||||||
|
{ label: "Route masks", value: "6830/6830 × 2", hint: "sealed local archives · Worker не требуется" },
|
||||||
|
{ label: "EoMT throughput", value: `${decimal(review.city.inferenceFps, 2)} fps`, hint: "изолированный полный прогон" },
|
||||||
|
{ label: "DDRNet throughput", value: `${decimal(review.vegetation.inferenceFps, 2)} fps`, hint: "изолированный полный прогон" },
|
||||||
|
{ label: "Spatial evidence", value: "RRD + 10 TGS anchors", hint: "continuous TGS и 3D semantics отсутствуют" },
|
||||||
|
]}
|
||||||
|
conclusion={{
|
||||||
|
proved: "Полный RAV004 открывается в каноническом recorded viewer с camera, source points, SLAM trajectory, EoMT, DDRNet и связанными TGS-якорями.",
|
||||||
|
notProved: "Не доказаны truth accuracy, временная стабильность DDRNet, continuous negative-obstacle detection и безопасное управление ровером.",
|
||||||
|
decision: "Использовать как visual audit. Следующий gate — truth-набор овраг/трава/дерево/яма и motion-aware temporal evaluation при целевых ≥10 FPS; navigation/actuation оставить OFF.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
|
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
|
||||||
const route = result.routeVideo!;
|
const route = result.routeVideo!;
|
||||||
const linkedTgsResultId = route.linkedTgsResultId;
|
const linkedTgsResultId = route.linkedTgsResultId;
|
||||||
@@ -92,6 +576,15 @@ export function VegetationShadowResultView({
|
|||||||
rigLabel: string;
|
rigLabel: string;
|
||||||
result: VegetationShadowResult;
|
result: VegetationShadowResult;
|
||||||
}) {
|
}) {
|
||||||
|
if (result.routeFullReview) {
|
||||||
|
return (
|
||||||
|
<FullRouteReviewResult
|
||||||
|
rigLabel={rigLabel}
|
||||||
|
resultId={result.resultId}
|
||||||
|
review={result.routeFullReview}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!result.routeVideo?.linkedTgsResultId) {
|
if (!result.routeVideo?.linkedTgsResultId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Vegetation LAB result has no canonical M4 source timeline and linked M4.9 TGS evidence.",
|
"Vegetation LAB result has no canonical M4 source timeline and linked M4.9 TGS evidence.",
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ function fullRouteReview() {
|
|||||||
return {
|
return {
|
||||||
source_id: "RAVNOVES004TREE",
|
source_id: "RAVNOVES004TREE",
|
||||||
session_id: "20260828T130511Z_viewer_live",
|
session_id: "20260828T130511Z_viewer_live",
|
||||||
|
linked_route_review_result_id: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
|
||||||
source_job_id: "recorded-camera-eb2783c5480d56bda07c8af0",
|
source_job_id: "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||||
source_job_input_sha256: "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
source_job_input_sha256: "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
||||||
source_stream_sha256: "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
source_stream_sha256: "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||||
@@ -385,12 +386,15 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
|
|||||||
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
|
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
|
||||||
assert.match(m49Source, /spatialSemantic=\{spatialSemantic\}/);
|
assert.match(m49Source, /spatialSemantic=\{spatialSemantic\}/);
|
||||||
assert.match(m49Source, /controlLabel: "SEMANTICS"/);
|
assert.match(m49Source, /controlLabel: "SEMANTICS"/);
|
||||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
|
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
|
||||||
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
|
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
|
||||||
assert.doesNotMatch(resultSource, /RAVNOVES004TREE full recorded review/);
|
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
|
||||||
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer/);
|
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
|
||||||
assert.doesNotMatch(resultSource, /RerunViewport/);
|
assert.match(resultSource, /RerunViewport/);
|
||||||
assert.doesNotMatch(resultSource, /M48EvidenceModeRail/);
|
assert.match(resultSource, /M48EvidenceModeRail/);
|
||||||
|
assert.match(resultSource, /SOURCE POINTS/);
|
||||||
|
assert.match(resultSource, /LOCAL SLAM/);
|
||||||
|
assert.match(resultSource, /TGS COSTMAP/);
|
||||||
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
|
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
|
||||||
assert.match(resultSource, /linkedTgsResultId/);
|
assert.match(resultSource, /linkedTgsResultId/);
|
||||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||||
|
|||||||
@@ -742,6 +742,7 @@ def seal_mixed_route_full_video_review(
|
|||||||
full_route = {
|
full_route = {
|
||||||
"source_id": FULL_ROUTE_SOURCE_ID,
|
"source_id": FULL_ROUTE_SOURCE_ID,
|
||||||
"session_id": job.session_id,
|
"session_id": job.session_id,
|
||||||
|
"linked_route_review_result_id": base["result_id"],
|
||||||
"source_job_id": job.job_id,
|
"source_job_id": job.job_id,
|
||||||
"source_job_input_sha256": job.input_sha256,
|
"source_job_input_sha256": job.input_sha256,
|
||||||
"source_stream_sha256": FULL_ROUTE_STREAM_SHA256,
|
"source_stream_sha256": FULL_ROUTE_STREAM_SHA256,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ _E39_RESULT_ID = re.compile(r"^e39-perception-refinement-[a-f0-9]{64}$")
|
|||||||
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
|
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
|
||||||
_M4_RESULT_ID = re.compile(r"^m4-threat-replay-[a-f0-9]{64}$")
|
_M4_RESULT_ID = re.compile(r"^m4-threat-replay-[a-f0-9]{64}$")
|
||||||
_M49_TGS_RESULT_ID = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
|
_M49_TGS_RESULT_ID = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
|
||||||
|
_VEGETATION_RESULT_ID = re.compile(r"^lab-v1-vegetation-shadow-[a-f0-9]{64}$")
|
||||||
|
|
||||||
RootProvider = Callable[[], Path | None]
|
RootProvider = Callable[[], Path | None]
|
||||||
|
|
||||||
@@ -266,6 +267,21 @@ def _validate_product_publication_shape(
|
|||||||
) -> None:
|
) -> None:
|
||||||
if work_id != "lab-v1-vegetation-shadow":
|
if work_id != "lab-v1-vegetation-shadow":
|
||||||
return
|
return
|
||||||
|
full_route = document.get("route_full_review")
|
||||||
|
if isinstance(full_route, dict):
|
||||||
|
if (
|
||||||
|
full_route.get("source_id") != "RAVNOVES004TREE"
|
||||||
|
or full_route.get("session_id") != "20260828T130511Z_viewer_live"
|
||||||
|
or full_route.get("frame_count") != 6830
|
||||||
|
or _VEGETATION_RESULT_ID.fullmatch(
|
||||||
|
str(full_route.get("linked_route_review_result_id", ""))
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
or document.get("route_video") is not None
|
||||||
|
or document.get("route_review") is not None
|
||||||
|
):
|
||||||
|
raise ValueError("vegetation LAB has no canonical RAVNOVES004TREE publication shape")
|
||||||
|
return
|
||||||
route = document.get("route_video")
|
route = document.get("route_video")
|
||||||
fusion = route.get("fusion") if isinstance(route, dict) else None
|
fusion = route.get("fusion") if isinstance(route, dict) else None
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ def test_advanced_index_projects_one_most_mature_lifecycle_phase(tmp_path: Path)
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_advanced_index_skips_noncanonical_vegetation_viewers(tmp_path: Path) -> None:
|
def test_advanced_index_prefers_canonical_rav004_full_review(tmp_path: Path) -> None:
|
||||||
root = tmp_path / "vegetation"
|
root = tmp_path / "vegetation"
|
||||||
|
|
||||||
def publish(digest: str, *, canonical: bool) -> Path:
|
def publish(digest: str, *, publication: str) -> Path:
|
||||||
result_id = f"lab-v1-vegetation-shadow-{digest}"
|
result_id = f"lab-v1-vegetation-shadow-{digest}"
|
||||||
candidate = root / result_id
|
candidate = root / result_id
|
||||||
candidate.mkdir(parents=True)
|
candidate.mkdir(parents=True)
|
||||||
@@ -167,7 +167,17 @@ def test_advanced_index_skips_noncanonical_vegetation_viewers(tmp_path: Path) ->
|
|||||||
"mode": "synchronised-multilayer-review",
|
"mode": "synchronised-multilayer-review",
|
||||||
"pixel_raster_fusion": False,
|
"pixel_raster_fusion": False,
|
||||||
},
|
},
|
||||||
} if canonical else None
|
} if publication == "rav00" else None
|
||||||
|
route_full_review = {
|
||||||
|
"source_id": "RAVNOVES004TREE",
|
||||||
|
"session_id": "20260828T130511Z_viewer_live",
|
||||||
|
"frame_count": 6830,
|
||||||
|
"linked_route_review_result_id": (
|
||||||
|
f"lab-v1-vegetation-shadow-{'4' * 64}"
|
||||||
|
if publication == "rav004"
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
} if publication != "rav00" else None
|
||||||
(candidate / "manifest.json").write_text(
|
(candidate / "manifest.json").write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
@@ -184,17 +194,19 @@ def test_advanced_index_skips_noncanonical_vegetation_viewers(tmp_path: Path) ->
|
|||||||
"ground_truth": False,
|
"ground_truth": False,
|
||||||
"route_video": route_video,
|
"route_video": route_video,
|
||||||
"route_review": None,
|
"route_review": None,
|
||||||
"route_full_review": None if canonical else {"frame_count": 6830},
|
"route_full_review": route_full_review,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
canonical = publish("a" * 64, canonical=True)
|
rav00 = publish("a" * 64, publication="rav00")
|
||||||
incomplete = publish("b" * 64, canonical=False)
|
incomplete = publish("b" * 64, publication="incomplete")
|
||||||
os.utime(canonical, ns=(10_000_000_000, 10_000_000_000))
|
rav004 = publish("c" * 64, publication="rav004")
|
||||||
|
os.utime(rav00, ns=(10_000_000_000, 10_000_000_000))
|
||||||
os.utime(incomplete, ns=(20_000_000_000, 20_000_000_000))
|
os.utime(incomplete, ns=(20_000_000_000, 20_000_000_000))
|
||||||
|
os.utime(rav004, ns=(30_000_000_000, 30_000_000_000))
|
||||||
registry = _evidence_registry(
|
registry = _evidence_registry(
|
||||||
root,
|
root,
|
||||||
work_id="lab-v1-vegetation-shadow",
|
work_id="lab-v1-vegetation-shadow",
|
||||||
@@ -211,7 +223,7 @@ def test_advanced_index_skips_noncanonical_vegetation_viewers(tmp_path: Path) ->
|
|||||||
assert index["items"] == [ # type: ignore[index]
|
assert index["items"] == [ # type: ignore[index]
|
||||||
{
|
{
|
||||||
"work_id": "lab-v1-vegetation-shadow",
|
"work_id": "lab-v1-vegetation-shadow",
|
||||||
"result_id": canonical.name,
|
"result_id": rav004.name,
|
||||||
"created_at_utc": "2026-08-29T10:00:00Z",
|
"created_at_utc": "2026-08-29T10:00:00Z",
|
||||||
"access": "read-only",
|
"access": "read-only",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user