From c30d77572ecd84c6172918bdf736ff44128c8616 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 29 Aug 2026 20:33:52 +0300 Subject: [PATCH] fix(lab): restore canonical vegetation evidence layers --- .../laboratory/M49TgsFullShadowEvidence.tsx | 12 + .../laboratory/M4ReplayThreatVisual.tsx | 51 +- .../laboratory/VegetationShadowResult.tsx | 527 +----------------- .../test/semanticEvidencePrimitives.test.mjs | 3 + .../test/vegetationShadow.test.mjs | 25 +- src/k1link/web/advanced_laboratory_api.py | 27 + tests/test_advanced_laboratory_api.py | 67 +++ 7 files changed, 191 insertions(+), 521 deletions(-) diff --git a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx index 4cd1525..235f93d 100644 --- a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx +++ b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx @@ -215,12 +215,24 @@ export function M49TgsFullShadowEvidence({ controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet", }] : []), ], [semantic, semanticOverride]); + const spatialSemantic = useMemo(() => ( + semantic ? { + id: "spatial-urban", + controlLabel: "SEMANTICS", + resultId: semantic.resultId, + spatialResultId: semantic.resultId, + taxonomy: semantic.taxonomy, + label: "EoMT Cityscapes semantic · point-aligned E47", + maskAriaLabel: "EoMT urban semantic prediction", + } : undefined + ), [semantic]); return ( <> (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId, ) ?? availableSemanticLayers[0]; + const activeSpatialSemantic = spatialSemantic ?? activeSemantic; const evidenceDemand = useMemo(() => laboratoryRecordedEvidenceDemand({ mediaMode, spatialMode, showMediaSemantic: Boolean(activeSemantic) && showMediaSemantic, - showSpatialSemantic: Boolean(activeSemantic) && showSpatialSemantic, + showSpatialSemantic: Boolean(activeSpatialSemantic) && showSpatialSemantic, showMediaPoints, classifiedSpatialMode: !classifiedSpatialLayer ? "none" @@ -248,6 +251,7 @@ export function M4ReplayThreatVisual({ : "overlay", }), [ activeSemantic, + activeSpatialSemantic, classifiedSpatialLayer, mediaMode, showMediaPoints, @@ -362,21 +366,21 @@ export function M4ReplayThreatVisual({ sequence: frame?.sequence ?? null, endpointRoot: timelineEndpointRoot, }); - const semanticSpatialResultId = activeSemantic - ? activeSemantic.spatialResultId === undefined - ? activeSemantic.resultId - : activeSemantic.spatialResultId + const semanticSpatialResultId = activeSpatialSemantic + ? activeSpatialSemantic.spatialResultId === undefined + ? activeSpatialSemantic.resultId + : activeSpatialSemantic.spatialResultId : null; const spatialSemanticTaxonomy = useMemo( - () => semanticSpatialResultId && activeSemantic - ? activeSemantic.taxonomy.map((item) => ({ + () => semanticSpatialResultId && activeSpatialSemantic + ? activeSpatialSemantic.taxonomy.map((item) => ({ classId: item.classId, label: item.label, disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled", colorRgb: item.colorRgb, })) : [], - [activeSemantic, semanticSpatialResultId], + [activeSpatialSemantic, semanticSpatialResultId], ); const semanticTimeline = useE47SemanticTimelineFrame({ resultId: semanticSpatialResultId, @@ -473,6 +477,27 @@ export function M4ReplayThreatVisual({ })) ?? [], [activeSemantic?.taxonomy], ); + const spatialSemanticClasses = useMemo( + () => activeSpatialSemantic?.taxonomy.map((item) => ({ + id: item.classId, + label: `semantic: ${item.label}`, + })) ?? [], + [activeSpatialSemantic?.taxonomy], + ); + const spatialSemanticPalette = useMemo( + () => activeSpatialSemantic?.taxonomy.map((item) => ({ + classId: item.classId, + color: item.disposition === "undefined" + ? { kind: "transparent" as const } + : item.disposition === "ambiguous" + ? { kind: "token" as const, token: "--nodedc-warning-rgb" as const } + : { kind: "diagnostic" as const, rgb: item.colorRgb }, + opacity: item.disposition === "undefined" + ? 0 + : item.disposition === "ambiguous" ? 0.52 : 0.92, + })) ?? [], + [activeSpatialSemantic?.taxonomy], + ); const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence ? semanticTimeline.activeFrame : null; @@ -489,7 +514,7 @@ export function M4ReplayThreatVisual({ && lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence ? lastSpatialSemanticFrameRef.current.frame : null; - const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && ( + const semanticIntegrityError = activeSpatialSemantic && spatialFrame && spatialSemanticFrame && ( spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount || spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount || spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount @@ -498,7 +523,7 @@ export function M4ReplayThreatVisual({ : null; const alignedSemanticPointIds = useMemo(() => { if ( - !activeSemantic + !activeSpatialSemantic || !showSpatialSemantic || !spatialFrame || !spatialSemanticFrame @@ -508,7 +533,7 @@ export function M4ReplayThreatVisual({ const status = spatialSemanticFrame.statusCodes[index]; return status === 2 || status === 3 ? classId : null; }); - }, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]); + }, [activeSpatialSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]); const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence ? spatialFrame : null; @@ -1200,10 +1225,10 @@ export function M4ReplayThreatVisual({ : alignedSemanticPointIds} semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud ? displayedClassifiedSpatialFrame.classes - : semanticClasses} + : spatialSemanticClasses} semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud ? displayedClassifiedSpatialFrame.palette - : semanticPalette} + : spatialSemanticPalette} classifiedCells={classifiedCellsBody} classifiedPackedCells={classifiedPackedCellsBody} classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM} diff --git a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx index 73128de..14d8441 100644 --- a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx @@ -1,9 +1,5 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react"; +import { useEffect, useState } from "react"; -import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer"; -import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer"; -import { RerunViewport } from "../../components/RerunViewport"; import { LaboratoryEvidence, LaboratoryResultSummary, @@ -11,457 +7,22 @@ import { LaboratoryWorkTemplate, } from "../../components/laboratory/LaboratoryPresentation"; import { - RecordedEvidenceSemanticMaskOverlay, - type RecordedEvidenceSemanticClass, - type RecordedEvidenceSemanticPaletteEntry, -} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay"; -import { - vegetationFullRouteMaskUrl, vegetationVideoMaskUrl, - type VegetationFullRouteLayer, - type VegetationFullRouteReview, - type VegetationMixedRouteReview, type VegetationShadowResult, } from "../../core/laboratory/vegetationShadow"; -import { recordedObservationSources } from "../../core/observation/recordedObservationSources"; -import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions"; -import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive"; -import { - recordedSessionRerunProfile, - type RerunPlaybackController, -} from "../../core/observation/viewerProfile"; -import type { ObservationSourceDescriptor } from "../../core/runtime/contracts"; import { fetchM49TgsFullShadowResult, type M49TgsFullShadowResult, } from "../../core/laboratory/m49TgsFullShadow"; -import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual"; import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence"; -import { - M48EvidenceModeRail, - type M48BlindEvidenceMode, -} from "./annotation/M48EvidenceModeControls"; function decimal(value: number, digits = 1): string { return value.toLocaleString("ru-RU", { maximumFractionDigits: digits }); } -const MIXED_ROUTE_MODES = [ - { value: "source", label: "SOURCE" }, - { value: "city", label: "ГОРОД · EoMT" }, - { value: "vegetation", label: "ПРИРОДА · DDRNet" }, - { value: "tgs", label: "TGS" }, -] as const; - -const FULL_ROUTE_MODES = [ - { value: "source", label: "SOURCE" }, - { 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 FullRouteReviewEvidence({ - resultId, - review, -}: { - resultId: string; - review: VegetationFullRouteReview; -}) { - const [sequence, setSequence] = useState(1); - const [playing, setPlaying] = useState(false); - const [playbackRate, setPlaybackRate] = useState(1); - const [mode, setMode] = useState("vegetation"); - const [expanded, setExpanded] = useState(false); - const [evidenceMode, setEvidenceMode] = useState("3d"); - const [cameraVisible, setCameraVisible] = useState(true); - const [videoSource, setVideoSource] = useState(null); - const [replayLaunch, setReplayLaunch] = useState(null); - const [videoError, setVideoError] = useState(null); - const spatialControllerRef = useRef(null); - const frames = useMemo( - () => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({ - sequence: index + 1, - sourceTimeNs, - })), - [review.frameSourceTimesNs], - ); - const layer = mode === "source" ? null : review[mode]; - const semantic = useMemo(() => layer ? semanticPresentation(layer) : null, [layer]); - const maskSequence = sequence - 1; - const prefetchSrcs = useMemo(() => layer - ? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1) - .filter((candidate) => candidate < review.frameCount) - .map((candidate) => vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", candidate)) - : [], [layer, maskSequence, mode, resultId, review.frameCount]); - - 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, - ]); - - 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 cameraPresentation = evidenceMode === "camera" - ? "primary" - : cameraVisible ? "companion" : "hidden"; - - return ( - -
- {videoSource ? ( - - ) : ( -
- Открываем sealed RRD и point-cloud evidence… -
- )} - cameraOverlay={( - <> -
- {mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`} -
- {layer && semantic ? ( -
- -
- ) : null} - - )} - /> - ) : ( -
- {videoError ?? "Открываем автономный recorded source…"} -
- )} - {videoSource ? ( - - ) : null} -
-
- ); -} - -function FullRouteReviewResult({ - rigLabel, - resultId, - review, -}: { - rigLabel: string; - resultId: string; - review: VegetationFullRouteReview; -}) { - return ( - - )} - evidence={( - - - - )} - result={( - - )} - /> - ); -} - -function MixedRouteReviewEvidence({ review }: { review: VegetationMixedRouteReview }) { - const [index, setIndex] = useState(0); - const [mode, setMode] = useState("vegetation"); - const [expanded, setExpanded] = useState(false); - const item = review.cases[index]!; - return ( - - setIndex((index - 1 + review.cases.length) % review.cases.length)}> - - - setIndex((index + 1) % review.cases.length)}> - - - - )} - overlay={( -
- - {item.phase.toUpperCase()} · {index + 1}/{review.cases.length} - - sequence {item.sourceSequence} · +{decimal(item.sessionSeconds, 2)} s - - TGS: {item.tgs.groundCells} ground · {item.tgs.occupiedCells} occupied · {item.tgs.unobservedCells} unobserved - -
- )} - > -
- -
-
- ); -} - -function MixedRouteReviewResult({ - rigLabel, - review, -}: { - rigLabel: string; - review: VegetationMixedRouteReview; -}) { - return ( - - )} - evidence={( - - - - )} - result={( - - )} - /> - ); -} - function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) { const route = result.routeVideo!; + const linkedTgsResultId = route.linkedTgsResultId; const [tgs, setTgs] = useState(null); const [tgsError, setTgsError] = useState(null); @@ -469,8 +30,8 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) const controller = new AbortController(); setTgs(null); setTgsError(null); - if (!route.linkedTgsResultId) return () => controller.abort(); - void fetchM49TgsFullShadowResult(route.linkedTgsResultId, { + if (!linkedTgsResultId) return () => controller.abort(); + void fetchM49TgsFullShadowResult(linkedTgsResultId, { signal: controller.signal, }).then((next) => { if (controller.signal.aborted) return; @@ -484,7 +45,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) } }); return () => controller.abort(); - }, [route.baseM4ResultId, route.linkedTgsResultId]); + }, [linkedTgsResultId, route.baseM4ResultId]); + + if (!linkedTgsResultId) { + throw new Error("Vegetation LAB result has no linked canonical M4.9 TGS evidence."); + } const semantic = { id: "vegetation", @@ -497,16 +62,14 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) maskAriaLabel: "DDRNet vegetation material prediction", } as const; - if (route.linkedTgsResultId && tgs) { + if (tgsError) { return ( - +
+ Канонический M4.9 TGS слой недоступен: {tgsError} +
); } - if (route.linkedTgsResultId && !tgsError) { + if (!tgs) { return (
Открываем sealed EoMT, TGS и coarse vegetation timeline… @@ -514,20 +77,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) ); } return ( - <> - - {tgsError ? ( -
- TGS слой недоступен: {tgsError} -
- ) : null} - + ); } @@ -538,18 +92,11 @@ export function VegetationShadowResultView({ rigLabel: string; result: VegetationShadowResult; }) { - if (result.routeFullReview) { - return ( - + if (!result.routeVideo?.linkedTgsResultId) { + throw new Error( + "Vegetation LAB result has no canonical M4 source timeline and linked M4.9 TGS evidence.", ); } - if (result.routeReview) { - return ; - } const route = result.routeVideo; const selected = result.candidates.find( (candidate) => candidate.candidate === result.selectedCandidate, @@ -561,9 +108,7 @@ export function VegetationShadowResultView({ )} - evidence={route ? ( + evidence={( - ) : ( - -
- Для этой immutable identity нет полного route video evidence. -
-
)} result={( index \+ 1\)/); assert.match(source, /\|\| !showSpatialSemantic/); diff --git a/apps/control-station/test/vegetationShadow.test.mjs b/apps/control-station/test/vegetationShadow.test.mjs index 16c952f..44ed3d5 100644 --- a/apps/control-station/test/vegetationShadow.test.mjs +++ b/apps/control-station/test/vegetationShadow.test.mjs @@ -365,7 +365,7 @@ test("vegetation GOOSE benchmark opens through its separate archival endpoint", }); test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => { - const [resultSource, benchmarkSource] = await Promise.all([ + const [resultSource, benchmarkSource, m49Source] = await Promise.all([ readFile( new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url), "utf8", @@ -374,21 +374,24 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr new URL("../src/workspaces/laboratory/VegetationBenchmarkResult.tsx", import.meta.url), "utf8", ), + readFile( + new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url), + "utf8", + ), ]); assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/); - assert.match(resultSource, /M4ReplayThreatVisual/); assert.match(resultSource, /M49TgsFullShadowEvidence/); assert.match(resultSource, /semanticOverride/); assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/); - assert.equal(resultSource.match(/ None: + if work_id != "lab-v1-vegetation-shadow": + return + route = document.get("route_video") + fusion = route.get("fusion") if isinstance(route, dict) else None + if ( + not isinstance(route, dict) + or route.get("view_kind") != "coarse-material-policy-review" + or _M4_RESULT_ID.fullmatch(str(route.get("base_m4_result_id", ""))) is None + or _M49_TGS_RESULT_ID.fullmatch(str(route.get("linked_tgs_result_id", ""))) + is None + or not isinstance(fusion, dict) + or fusion.get("mode") != "synchronised-multilayer-review" + or fusion.get("pixel_raster_fusion") is not False + or document.get("route_review") is not None + or document.get("route_full_review") is not None + ): + raise ValueError("vegetation LAB has no canonical M4/M4.9 publication shape") + + def _advanced_index( specs: tuple[_AdvancedIndexSpec, ...], ) -> dict[str, object]: diff --git a/tests/test_advanced_laboratory_api.py b/tests/test_advanced_laboratory_api.py index a2a7b48..87686c3 100644 --- a/tests/test_advanced_laboratory_api.py +++ b/tests/test_advanced_laboratory_api.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os from pathlib import Path, PurePosixPath from types import SimpleNamespace @@ -151,6 +152,72 @@ 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: + root = tmp_path / "vegetation" + + def publish(digest: str, *, canonical: bool) -> Path: + result_id = f"lab-v1-vegetation-shadow-{digest}" + candidate = root / result_id + candidate.mkdir(parents=True) + route_video = { + "view_kind": "coarse-material-policy-review", + "base_m4_result_id": f"m4-threat-replay-{'1' * 64}", + "linked_tgs_result_id": f"m49-tgs-full-shadow-{'2' * 64}", + "fusion": { + "mode": "synchronised-multilayer-review", + "pixel_raster_fusion": False, + }, + } if canonical else None + (candidate / "manifest.json").write_text( + json.dumps( + { + "schema_version": "missioncore.lab-v1-vegetation-shadow/v1", + "result_id": result_id, + "identity_sha256": digest, + "identity": { + "authority": { + "commands_enabled": False, + "navigation_or_safety_accepted": False, + } + }, + "created_at_utc": "2026-08-29T10:00:00Z", + "ground_truth": False, + "route_video": route_video, + "route_review": None, + "route_full_review": None if canonical else {"frame_count": 6830}, + } + ), + encoding="utf-8", + ) + return candidate + + canonical = publish("a" * 64, canonical=True) + incomplete = publish("b" * 64, canonical=False) + os.utime(canonical, ns=(10_000_000_000, 10_000_000_000)) + os.utime(incomplete, ns=(20_000_000_000, 20_000_000_000)) + registry = _evidence_registry( + root, + work_id="lab-v1-vegetation-shadow", + result_id_prefix="lab-v1-vegetation-shadow", + schema_version="missioncore.lab-v1-vegetation-shadow/v1", + ) + router = build_advanced_laboratory_router( + evidence_registry=registry, + evidence_runtime_root_provider=lambda: root.parent, + ) + + index = _endpoint(router, "/api/v1/laboratory/advanced-index")() + + assert index["items"] == [ # type: ignore[index] + { + "work_id": "lab-v1-vegetation-shadow", + "result_id": canonical.name, + "created_at_utc": "2026-08-29T10:00:00Z", + "access": "read-only", + } + ] + + def test_advanced_index_includes_valid_l31_identity( tmp_path: Path, monkeypatch: MonkeyPatch,