feat(observatory): admit canonical recorded replay
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
|
||||
|
||||
import { ObservationTimeline } from "../ObservationTimeline";
|
||||
import {
|
||||
RerunViewport,
|
||||
type RerunPlaybackController,
|
||||
type RerunPlaybackState,
|
||||
} from "../RerunViewport";
|
||||
import {
|
||||
CanonicalRecordedLabReplay,
|
||||
useCanonicalRecordedLabReplayState,
|
||||
} from "../laboratory/CanonicalRecordedLabReplay";
|
||||
import {
|
||||
resolveCanonicalLabReplay,
|
||||
type CanonicalLabReplayDescriptor,
|
||||
} from "../../core/laboratory/canonicalLabReplay";
|
||||
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
|
||||
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
|
||||
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import { defaultSceneSettings } from "../../sceneSettings";
|
||||
|
||||
type MediaMode = "video" | "camera";
|
||||
type SpatialMode = "3d" | "plan";
|
||||
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
|
||||
type SemanticLayer = "city" | "vegetation";
|
||||
|
||||
interface CanonicalReplayLaunch {
|
||||
base: ObservationSessionReplayLaunch;
|
||||
replay: CanonicalLabReplayDescriptor;
|
||||
}
|
||||
|
||||
export function CanonicalVegetationRerunReplay({
|
||||
resultId,
|
||||
review,
|
||||
}: {
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
const {
|
||||
mediaMode,
|
||||
spatialMode,
|
||||
splitPrimarySize,
|
||||
splitOrientation,
|
||||
expanded,
|
||||
onMediaModeChange,
|
||||
onSpatialModeChange,
|
||||
onSplitPrimarySizeChange,
|
||||
onExpandedChange,
|
||||
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
|
||||
initialMediaMode: "video",
|
||||
initialSpatialMode: "3d",
|
||||
});
|
||||
const [semanticLayer, setSemanticLayer] = useState<SemanticLayer>("vegetation");
|
||||
const [showSemantics, setShowSemantics] = useState(true);
|
||||
const [spatialLayer, setSpatialLayer] = useState<SpatialLayer>("source");
|
||||
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(0);
|
||||
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
|
||||
const [playbackController, setPlaybackController] =
|
||||
useState<RerunPlaybackController | null>(null);
|
||||
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
|
||||
const [launchError, setLaunchError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLaunch(null);
|
||||
setLaunchError(null);
|
||||
void resolveObservationSessionReplay(review.sessionId, {
|
||||
signal: controller.signal,
|
||||
maximumWaitMs: 30 * 60 * 1000,
|
||||
onUpdate: () => undefined,
|
||||
}).then(async (value) => ({
|
||||
base: value,
|
||||
replay: await resolveCanonicalLabReplay(resultId, value, {
|
||||
signal: controller.signal,
|
||||
}),
|
||||
})).then((value) => {
|
||||
if (!controller.signal.aborted) setLaunch(value);
|
||||
}).catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setLaunchError(
|
||||
caught instanceof Error ? caught.message : "Каноническая запись RAV004 недоступна.",
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [resultId, review.sessionId]);
|
||||
|
||||
const splitView = mediaMode !== null && spatialMode !== null;
|
||||
const sceneSettings = useMemo(() => ({
|
||||
...defaultSceneSettings,
|
||||
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
|
||||
showPoints: spatialMode !== null,
|
||||
showTrajectory: spatialMode !== null,
|
||||
showGrid: spatialMode !== null,
|
||||
pointSize: 3.8,
|
||||
}), [spatialLayer, spatialMode]);
|
||||
const profile = launch ? recordedSessionRerunProfile({
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
artifact: {
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
viewerSourceUrl: launch.replay.viewerSourceUrl,
|
||||
byteLength: launch.replay.byteLength,
|
||||
sha256: launch.replay.sha256,
|
||||
},
|
||||
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
|
||||
autoplayWhenReady: false,
|
||||
presentationGate: "ready",
|
||||
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
|
||||
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
|
||||
initialPlaybackStartSeconds: review.timelineStartSeconds,
|
||||
view: mediaMode !== null ? "perception" : "spatial",
|
||||
viewResetGeneration,
|
||||
followTrajectory: true,
|
||||
semanticLayer,
|
||||
unifiedPerception: splitView,
|
||||
planView: spatialMode === "plan",
|
||||
perceptionLayers: {
|
||||
enabled: mediaMode !== null,
|
||||
detections2d: mediaMode === "video",
|
||||
segmentation: mediaMode === "video" && showSemantics,
|
||||
cuboids3d: false,
|
||||
},
|
||||
perceptionRetryGeneration: 0,
|
||||
lockPerceptionCameraInteraction: false,
|
||||
}) : null;
|
||||
|
||||
const mediaLayerControls = (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Слои камеры и видео"
|
||||
>
|
||||
<Button
|
||||
size="dense"
|
||||
shape="pill"
|
||||
variant={showSemantics ? "primary" : "secondary"}
|
||||
aria-pressed={showSemantics}
|
||||
onClick={() => setShowSemantics((visible) => !visible)}
|
||||
>
|
||||
СЕМАНТИКА
|
||||
</Button>
|
||||
<SegmentedControl
|
||||
value={semanticLayer}
|
||||
items={[
|
||||
{ value: "city", label: "ГОРОД · EoMT" },
|
||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||
]}
|
||||
label="Источник семантики"
|
||||
size="dense"
|
||||
onChange={(value) => {
|
||||
setSemanticLayer(value);
|
||||
setShowSemantics(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const spatialLayerControls = (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Пространственные слои RAV004"
|
||||
>
|
||||
<SegmentedControl
|
||||
value={spatialLayer}
|
||||
items={[
|
||||
{ value: "source", label: "ИСХ. ТОЧКИ" },
|
||||
{ value: "local", label: "ЛОК. SLAM" },
|
||||
{ value: "tgs", label: "TGS", disabled: true },
|
||||
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
|
||||
]}
|
||||
label="Пространственные слои"
|
||||
size="dense"
|
||||
onChange={setSpatialLayer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const resetSpatialView = (
|
||||
<Button
|
||||
size="dense"
|
||||
variant="ghost"
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
aria-label="Сбросить положение 3D камеры"
|
||||
title="Сбросить положение 3D камеры"
|
||||
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
|
||||
>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const transport = playback && playbackController ? (
|
||||
<ObservationTimeline
|
||||
className="m4-replay-threat-visual__timeline"
|
||||
active
|
||||
sourceCount={3}
|
||||
mode="recorded"
|
||||
seekable
|
||||
synchronization="shared-clock"
|
||||
rangeNs={playback.rangeNs}
|
||||
currentNs={playback.currentNs}
|
||||
playing={playback.playing}
|
||||
onSeek={playbackController.seek}
|
||||
onPlayingChange={playbackController.setPlaying}
|
||||
showJumpToEnd={false}
|
||||
/>
|
||||
) : undefined;
|
||||
return (
|
||||
<CanonicalRecordedLabReplay
|
||||
label="RAVNOVES004TREE · канонический повтор Rerun"
|
||||
mediaMode={mediaMode ?? "none"}
|
||||
mediaModes={[
|
||||
{ value: "video", label: "ВИДЕО" },
|
||||
{ value: "camera", label: "КАМЕРА" },
|
||||
]}
|
||||
spatialMode={spatialMode ?? "none"}
|
||||
spatialModes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "plan", label: "ПЛАН" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
splitPrimarySize={splitPrimarySize}
|
||||
splitOrientation={splitOrientation}
|
||||
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
|
||||
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
|
||||
mediaLayerControls={mediaLayerControls}
|
||||
spatialLayerControls={spatialLayerControls}
|
||||
spatialLeadingControl={resetSpatialView}
|
||||
mediaMultiLayer
|
||||
unifiedContent={profile ? (
|
||||
<RerunViewport
|
||||
profile={profile}
|
||||
sceneSettings={sceneSettings}
|
||||
onPlaybackChange={setPlayback}
|
||||
onPlaybackControllerChange={setPlaybackController}
|
||||
/>
|
||||
) : (
|
||||
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
|
||||
{launchError ?? "Готовим единый кэш канонического повтора RAV004…"}
|
||||
</div>
|
||||
)}
|
||||
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
|
||||
transport={transport}
|
||||
onMediaModeChange={onMediaModeChange}
|
||||
onSpatialModeChange={onSpatialModeChange}
|
||||
onExpandedChange={onExpandedChange}
|
||||
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1026,7 +1026,7 @@ export async function fetchVegetationRouteTgsAnchor(
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchVegetationShadowResult(
|
||||
export async function fetchVegetationShadowResultMetadata(
|
||||
resultId: string,
|
||||
{
|
||||
fetcher = fetch,
|
||||
@@ -1048,6 +1048,17 @@ export async function fetchVegetationShadowResult(
|
||||
resultId,
|
||||
"/api/v1/laboratory/vegetation-shadow",
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function fetchVegetationShadowResult(
|
||||
resultId: string,
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
): Promise<VegetationShadowResult> {
|
||||
const result = await fetchVegetationShadowResultMetadata(resultId, { fetcher, signal });
|
||||
if (!result.routeFullReview) return result;
|
||||
const timelineResponse = await fetcher(
|
||||
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-timeline`,
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
export interface ObservationLabReplayCapability {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v1";
|
||||
kind: "canonical-recorded-rerun";
|
||||
viewerProfile: "recorded-session";
|
||||
timeline: "session_time";
|
||||
activation: "explicit";
|
||||
commandsEnabled: false;
|
||||
}
|
||||
|
||||
export class ObservationLabReplayCapabilityContractError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ObservationLabReplayCapabilityContractError";
|
||||
}
|
||||
}
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const CAPABILITY_KEYS = new Set([
|
||||
"schema_version",
|
||||
"kind",
|
||||
"viewer_profile",
|
||||
"timeline",
|
||||
"activation",
|
||||
"commands_enabled",
|
||||
]);
|
||||
const CANONICAL_PROVENANCE_KEYS = new Set([
|
||||
"schema_version",
|
||||
"evidence_identity_sha256",
|
||||
"result_document_sha256",
|
||||
"replay_capability",
|
||||
"authority",
|
||||
"method",
|
||||
]);
|
||||
const AUTHORITY_KEYS = new Set([
|
||||
"commands_enabled",
|
||||
"navigation_or_safety_accepted",
|
||||
"actuation_accepted",
|
||||
]);
|
||||
const METHOD_KEYS = new Set([
|
||||
"schema_version",
|
||||
"completeness",
|
||||
"execution_class",
|
||||
"pipeline_id",
|
||||
"components",
|
||||
]);
|
||||
const METHOD_COMPONENT_KEYS = new Set([
|
||||
"kind",
|
||||
"name",
|
||||
"version",
|
||||
"role",
|
||||
"identity_sha256",
|
||||
]);
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function assertExactKeys(
|
||||
value: Record<string, unknown>,
|
||||
expected: ReadonlySet<string>,
|
||||
label: string,
|
||||
): void {
|
||||
const keys = Object.keys(value);
|
||||
if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`${label} содержит неизвестные или пропущенные поля.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeLabReplayCapability(
|
||||
value: unknown,
|
||||
sessionId: string,
|
||||
): ObservationLabReplayCapability | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (!isRecord(value)) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Replay-возможность LAB-сессии ${sessionId} должна быть объектом или null.`,
|
||||
);
|
||||
}
|
||||
assertExactKeys(value, CAPABILITY_KEYS, `Replay-возможность LAB-сессии ${sessionId}`);
|
||||
if (
|
||||
value.schema_version !== "missioncore.observation-lab-replay-capability/v1"
|
||||
|| value.kind !== "canonical-recorded-rerun"
|
||||
|| value.viewer_profile !== "recorded-session"
|
||||
|| value.timeline !== "session_time"
|
||||
|| value.activation !== "explicit"
|
||||
|| value.commands_enabled !== false
|
||||
) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Replay-возможность LAB-сессии ${sessionId} нарушает observation-only контракт.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commandsEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeCanonicalLabReplayCapabilityProvenance(
|
||||
provenance: Readonly<Record<string, unknown>>,
|
||||
sessionId: string,
|
||||
resultId: string,
|
||||
): ObservationLabReplayCapability {
|
||||
assertExactKeys(provenance, CANONICAL_PROVENANCE_KEYS, `Canonical provenance LAB-сессии ${sessionId}`);
|
||||
if (
|
||||
provenance.schema_version !== "missioncore.canonical-recorded-lab-projection/v1"
|
||||
|| typeof provenance.evidence_identity_sha256 !== "string"
|
||||
|| !SHA256.test(provenance.evidence_identity_sha256)
|
||||
|| resultId !== `lab-v1-vegetation-shadow-${provenance.evidence_identity_sha256}`
|
||||
|| typeof provenance.result_document_sha256 !== "string"
|
||||
|| !SHA256.test(provenance.result_document_sha256)
|
||||
) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical provenance LAB-сессии ${sessionId} потеряла immutable identity.`,
|
||||
);
|
||||
}
|
||||
if (!isRecord(provenance.authority)) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical provenance LAB-сессии ${sessionId} не содержит authority.`,
|
||||
);
|
||||
}
|
||||
assertExactKeys(provenance.authority, AUTHORITY_KEYS, `Canonical authority LAB-сессии ${sessionId}`);
|
||||
if (
|
||||
provenance.authority.commands_enabled !== false
|
||||
|| provenance.authority.navigation_or_safety_accepted !== false
|
||||
|| provenance.authority.actuation_accepted !== false
|
||||
) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical provenance LAB-сессии ${sessionId} нарушает observation-only authority.`,
|
||||
);
|
||||
}
|
||||
if (!isRecord(provenance.method)) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical provenance LAB-сессии ${sessionId} не содержит method manifest.`,
|
||||
);
|
||||
}
|
||||
assertExactKeys(provenance.method, METHOD_KEYS, `Canonical method LAB-сессии ${sessionId}`);
|
||||
if (
|
||||
provenance.method.schema_version !== "missioncore.laboratory-method/v1"
|
||||
|| provenance.method.completeness !== "legacy-partial"
|
||||
|| provenance.method.execution_class !== "ai-inference"
|
||||
|| provenance.method.pipeline_id
|
||||
!== "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1"
|
||||
|| !Array.isArray(provenance.method.components)
|
||||
|| provenance.method.components.length !== 1
|
||||
) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical method LAB-сессии ${sessionId} не соответствует записанному прогону.`,
|
||||
);
|
||||
}
|
||||
const component = provenance.method.components[0];
|
||||
if (!isRecord(component)) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical method LAB-сессии ${sessionId} не содержит source component.`,
|
||||
);
|
||||
}
|
||||
assertExactKeys(
|
||||
component,
|
||||
METHOD_COMPONENT_KEYS,
|
||||
`Canonical source component LAB-сессии ${sessionId}`,
|
||||
);
|
||||
if (
|
||||
component.kind !== "source"
|
||||
|| component.name !== "sealed full-route LAB result"
|
||||
|| component.version !== "missioncore.lab-v1-vegetation-shadow/v1"
|
||||
|| component.role !== "immutable Session catalog projection"
|
||||
|| component.identity_sha256 !== provenance.evidence_identity_sha256
|
||||
) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical source component LAB-сессии ${sessionId} потерял immutable identity.`,
|
||||
);
|
||||
}
|
||||
const capability = decodeLabReplayCapability(provenance.replay_capability, sessionId);
|
||||
if (capability === null) {
|
||||
throw new ObservationLabReplayCapabilityContractError(
|
||||
`Canonical provenance LAB-сессии ${sessionId} не содержит replay capability.`,
|
||||
);
|
||||
}
|
||||
return capability;
|
||||
}
|
||||
|
||||
/** Rolling bridge for a new frontend against the pre-v2 catalog endpoint. */
|
||||
export function decodeRollingCanonicalLabReplayCapability(
|
||||
provenance: Readonly<Record<string, unknown>>,
|
||||
sessionId: string,
|
||||
resultId: string,
|
||||
): ObservationLabReplayCapability | null {
|
||||
if (!Object.prototype.hasOwnProperty.call(provenance, "replay_capability")) return null;
|
||||
return decodeCanonicalLabReplayCapabilityProvenance(provenance, sessionId, resultId);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export interface ObservationReplayAttempt {
|
||||
readonly signal: AbortSignal;
|
||||
isCurrent: () => boolean;
|
||||
finish: () => boolean;
|
||||
}
|
||||
|
||||
export interface ObservationReplayCoordinator {
|
||||
begin: () => ObservationReplayAttempt;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
/** Latest explicit admission owns the viewer, even if an older promise settles later. */
|
||||
export function createObservationReplayCoordinator(): ObservationReplayCoordinator {
|
||||
let sequence = 0;
|
||||
let active: AbortController | null = null;
|
||||
|
||||
return {
|
||||
begin() {
|
||||
active?.abort();
|
||||
const controller = new AbortController();
|
||||
const attemptSequence = ++sequence;
|
||||
active = controller;
|
||||
return {
|
||||
signal: controller.signal,
|
||||
isCurrent: () => (
|
||||
!controller.signal.aborted
|
||||
&& active === controller
|
||||
&& sequence === attemptSequence
|
||||
),
|
||||
finish: () => {
|
||||
if (active !== controller || sequence !== attemptSequence) return false;
|
||||
active = null;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
cancel() {
|
||||
active?.abort();
|
||||
// Keep settlement ownership until finish(). isCurrent() already fails,
|
||||
// while begin() can replace this attempt immediately.
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import {
|
||||
MAX_RECORDED_CAMERA_SOURCES,
|
||||
} from "./recordedSessionAdmission";
|
||||
import {
|
||||
decodeLabReplayCapability,
|
||||
decodeRollingCanonicalLabReplayCapability,
|
||||
ObservationLabReplayCapabilityContractError,
|
||||
type ObservationLabReplayCapability,
|
||||
} from "./labReplayCapability";
|
||||
|
||||
export type { ObservationLabReplayCapability } from "./labReplayCapability";
|
||||
|
||||
export type ObservationSessionStatus =
|
||||
| "recording"
|
||||
@@ -20,6 +28,7 @@ export interface ObservationLabInstance {
|
||||
configSha256: string | null;
|
||||
runCreatedAtUtc: string;
|
||||
publishedAtUtc: string;
|
||||
replayCapability: ObservationLabReplayCapability | null;
|
||||
provenance: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
@@ -155,7 +164,7 @@ const ITEM_KEYS = new Set([
|
||||
"preparation",
|
||||
"lab",
|
||||
]);
|
||||
const LAB_KEYS = new Set([
|
||||
const LEGACY_LAB_KEYS = new Set([
|
||||
"lab_id",
|
||||
"source_session_id",
|
||||
"result_kind",
|
||||
@@ -166,6 +175,7 @@ const LAB_KEYS = new Set([
|
||||
"published_at_utc",
|
||||
"provenance",
|
||||
]);
|
||||
const LAB_KEYS = new Set([...LEGACY_LAB_KEYS, "replay_capability"]);
|
||||
const CATALOG_PREPARATION_KEYS = new Set([
|
||||
"preparation_id",
|
||||
"state",
|
||||
@@ -394,7 +404,15 @@ function decodeLabInstance(
|
||||
`LAB-привязка сессии ${sessionId} должна быть объектом или null.`,
|
||||
);
|
||||
}
|
||||
assertExactKeys(value, LAB_KEYS, `LAB-привязка сессии ${sessionId}`);
|
||||
const hasTypedCapability = Object.prototype.hasOwnProperty.call(
|
||||
value,
|
||||
"replay_capability",
|
||||
);
|
||||
assertExactKeys(
|
||||
value,
|
||||
hasTypedCapability ? LAB_KEYS : LEGACY_LAB_KEYS,
|
||||
`LAB-привязка сессии ${sessionId}`,
|
||||
);
|
||||
const labId = requireString(value.lab_id, `lab(${sessionId}).lab_id`, 36);
|
||||
if (!/^LAB [A-Z][A-Z0-9._-]{0,31}$/.test(labId)) {
|
||||
throw new ObservationSessionContractError("Каталог содержит некорректный LAB-маркер.");
|
||||
@@ -430,6 +448,17 @@ function decodeLabInstance(
|
||||
if (!isRecord(value.provenance)) {
|
||||
throw new ObservationSessionContractError("LAB provenance должен быть JSON-объектом.");
|
||||
}
|
||||
let replayCapability: ObservationLabReplayCapability | null;
|
||||
try {
|
||||
replayCapability = hasTypedCapability
|
||||
? decodeLabReplayCapability(value.replay_capability, sessionId)
|
||||
: decodeRollingCanonicalLabReplayCapability(value.provenance, sessionId, resultId);
|
||||
} catch (error) {
|
||||
if (error instanceof ObservationLabReplayCapabilityContractError) {
|
||||
throw new ObservationSessionContractError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
labId,
|
||||
sourceSessionId,
|
||||
@@ -445,6 +474,7 @@ function decodeLabInstance(
|
||||
value.published_at_utc,
|
||||
`lab(${sessionId}).published_at_utc`,
|
||||
),
|
||||
replayCapability,
|
||||
provenance: value.provenance,
|
||||
};
|
||||
}
|
||||
@@ -1181,6 +1211,7 @@ export async function fetchObservationSessionCatalog({
|
||||
queryParameters.set("limit", String(Number(limit)));
|
||||
}
|
||||
if (scope !== "all") queryParameters.set("scope", scope);
|
||||
if (scope === "laboratory") queryParameters.set("lab_contract", "v2");
|
||||
const serializedQuery = queryParameters.toString();
|
||||
const query = serializedQuery ? `?${serializedQuery}` : "";
|
||||
let response: Response;
|
||||
|
||||
@@ -12,6 +12,16 @@ import {
|
||||
type ObservationSessionScope,
|
||||
type ObservationSessionSummary,
|
||||
} from "./sessionArchive";
|
||||
import {
|
||||
createObservationReplayCoordinator,
|
||||
type ObservationReplayCoordinator,
|
||||
} from "./replayCoordinator";
|
||||
|
||||
export {
|
||||
createObservationReplayCoordinator,
|
||||
type ObservationReplayAttempt,
|
||||
type ObservationReplayCoordinator,
|
||||
} from "./replayCoordinator";
|
||||
|
||||
export type ObservationSessionsLoadState = "idle" | "loading" | "ready" | "error";
|
||||
export type ObservationReplayOutcome = "accepted" | "error" | "cancelled";
|
||||
@@ -41,17 +51,6 @@ export interface ObservationSessionsController {
|
||||
remove: (sessionId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ObservationReplayAttempt {
|
||||
readonly signal: AbortSignal;
|
||||
isCurrent: () => boolean;
|
||||
finish: () => boolean;
|
||||
}
|
||||
|
||||
export interface ObservationReplayCoordinator {
|
||||
begin: () => ObservationReplayAttempt;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export async function deleteObservationSessionAfterTeardown(
|
||||
sessionId: string,
|
||||
{
|
||||
@@ -93,41 +92,6 @@ export class ObservationPreparationStalledError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Latest selection wins, even if an obsolete server job finishes later. */
|
||||
export function createObservationReplayCoordinator(): ObservationReplayCoordinator {
|
||||
let sequence = 0;
|
||||
let active: AbortController | null = null;
|
||||
|
||||
return {
|
||||
begin() {
|
||||
active?.abort();
|
||||
const controller = new AbortController();
|
||||
const attemptSequence = ++sequence;
|
||||
active = controller;
|
||||
return {
|
||||
signal: controller.signal,
|
||||
isCurrent: () => (
|
||||
!controller.signal.aborted &&
|
||||
active === controller &&
|
||||
sequence === attemptSequence
|
||||
),
|
||||
finish: () => {
|
||||
if (active !== controller || sequence !== attemptSequence) return false;
|
||||
active = null;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
cancel() {
|
||||
active?.abort();
|
||||
// Keep ownership until the cancelled attempt reaches `finish()`. This
|
||||
// lets its finally block settle a replacement that already passed
|
||||
// onReplayBegin, while `isCurrent()` still fails immediately because the
|
||||
// signal is aborted. A later `begin()` replaces and invalidates it.
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
type ObservationSessionFetch,
|
||||
type ObservationSessionSummary,
|
||||
} from "../observation/sessionArchive";
|
||||
import {
|
||||
observatoryRecordedRunBinding,
|
||||
type ObservatoryRecordedRunBinding,
|
||||
} from "./recordedRun";
|
||||
|
||||
export interface ObservatoryEvidence {
|
||||
readonly sessionId: string;
|
||||
@@ -11,6 +15,7 @@ export interface ObservatoryEvidence {
|
||||
readonly status: ObservationSessionSummary["status"];
|
||||
readonly publishedAtUtc: string;
|
||||
readonly lab: ObservationLabInstance;
|
||||
readonly recordedRun: ObservatoryRecordedRunBinding | null;
|
||||
}
|
||||
|
||||
export interface ObservatorySession {
|
||||
@@ -61,6 +66,7 @@ function evidenceFromSession(
|
||||
status: session.status,
|
||||
publishedAtUtc: session.lab.publishedAtUtc,
|
||||
lab: session.lab,
|
||||
recordedRun: observatoryRecordedRunBinding(session.id, session.lab),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { LaboratoryFetch } from "../laboratory/advancedResults";
|
||||
import {
|
||||
fetchVegetationShadowResultMetadata,
|
||||
type VegetationFullRouteReview,
|
||||
type VegetationShadowResult,
|
||||
} from "../laboratory/vegetationShadow";
|
||||
import type { ObservationLabInstance } from "../observation/sessionArchive";
|
||||
import {
|
||||
decodeCanonicalLabReplayCapabilityProvenance,
|
||||
ObservationLabReplayCapabilityContractError,
|
||||
} from "../observation/labReplayCapability";
|
||||
|
||||
const CANONICAL_RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
|
||||
const CANONICAL_SOURCE_SESSION_ID = "20260828T130511Z_viewer_live";
|
||||
|
||||
export interface ObservatoryRecordedRunBinding {
|
||||
readonly kind: "canonical-recorded-rerun";
|
||||
readonly evidenceSessionId: string;
|
||||
readonly sourceSessionId: string;
|
||||
readonly resultId: string;
|
||||
readonly viewerProfile: "recorded-session";
|
||||
readonly timeline: "session_time";
|
||||
readonly activation: "explicit";
|
||||
}
|
||||
|
||||
export class ObservatoryRecordedRunContractError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ObservatoryRecordedRunContractError";
|
||||
}
|
||||
}
|
||||
|
||||
export function observatoryRecordedRunBinding(
|
||||
evidenceSessionId: string,
|
||||
lab: ObservationLabInstance,
|
||||
): ObservatoryRecordedRunBinding | null {
|
||||
const capability = lab.replayCapability;
|
||||
if (!capability) return null;
|
||||
try {
|
||||
decodeCanonicalLabReplayCapabilityProvenance(
|
||||
lab.provenance,
|
||||
evidenceSessionId,
|
||||
lab.resultId,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof ObservationLabReplayCapabilityContractError)) throw error;
|
||||
throw new ObservatoryRecordedRunContractError(error.message);
|
||||
}
|
||||
if (
|
||||
evidenceSessionId !== lab.resultId
|
||||
|| lab.sourceSessionId !== CANONICAL_SOURCE_SESSION_ID
|
||||
|| lab.labId !== "LAB V1"
|
||||
|| lab.resultKind !== "recorded-perception-qualification"
|
||||
|| !CANONICAL_RESULT_ID.test(lab.resultId)
|
||||
|| lab.configSha256 !== null
|
||||
|| lab.sourceResultId === null
|
||||
|| !CANONICAL_RESULT_ID.test(lab.sourceResultId)
|
||||
|| capability.kind !== "canonical-recorded-rerun"
|
||||
|| capability.viewerProfile !== "recorded-session"
|
||||
|| capability.timeline !== "session_time"
|
||||
|| capability.activation !== "explicit"
|
||||
|| capability.commandsEnabled !== false
|
||||
) {
|
||||
throw new ObservatoryRecordedRunContractError(
|
||||
`LAB-результат ${lab.resultId} не допущен к каноническому recorded replay.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "canonical-recorded-rerun",
|
||||
evidenceSessionId,
|
||||
sourceSessionId: lab.sourceSessionId,
|
||||
resultId: lab.resultId,
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchObservatoryRecordedRunReview(
|
||||
binding: ObservatoryRecordedRunBinding,
|
||||
{
|
||||
selectedSourceSessionId,
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: {
|
||||
selectedSourceSessionId: string;
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Promise<VegetationFullRouteReview> {
|
||||
if (binding.sourceSessionId !== selectedSourceSessionId) {
|
||||
throw new ObservatoryRecordedRunContractError(
|
||||
"Запуск не связан с выбранной исходной сессией.",
|
||||
);
|
||||
}
|
||||
const result = await fetchVegetationShadowResultMetadata(binding.resultId, {
|
||||
fetcher,
|
||||
signal,
|
||||
});
|
||||
return admitObservatoryRecordedRunReview(
|
||||
binding,
|
||||
selectedSourceSessionId,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
export function admitObservatoryRecordedRunReview(
|
||||
binding: ObservatoryRecordedRunBinding,
|
||||
selectedSourceSessionId: string,
|
||||
result: VegetationShadowResult,
|
||||
): VegetationFullRouteReview {
|
||||
if (binding.sourceSessionId !== selectedSourceSessionId) {
|
||||
throw new ObservatoryRecordedRunContractError(
|
||||
"Запуск не связан с выбранной исходной сессией.",
|
||||
);
|
||||
}
|
||||
if (result.resultId !== binding.resultId) {
|
||||
throw new ObservatoryRecordedRunContractError(
|
||||
"Запечатанный результат не совпадает с выбранным запуском.",
|
||||
);
|
||||
}
|
||||
const review = result.routeFullReview;
|
||||
if (!review || review.sessionId !== binding.sourceSessionId) {
|
||||
throw new ObservatoryRecordedRunContractError(
|
||||
"Запечатанный результат потерял точную связь с исходной сессией.",
|
||||
);
|
||||
}
|
||||
return review;
|
||||
}
|
||||
@@ -145,6 +145,66 @@
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.observatory-evidence-card__heading,
|
||||
.observatory-evidence-card__action,
|
||||
.observatory-replay__header,
|
||||
.observatory-replay-state,
|
||||
.observatory-replay-state__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.observatory-evidence-card__heading,
|
||||
.observatory-evidence-card__action,
|
||||
.observatory-replay__header,
|
||||
.observatory-replay-state {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.observatory-evidence-card__heading > div,
|
||||
.observatory-evidence-card__action > span,
|
||||
.observatory-replay__header > div,
|
||||
.observatory-replay-state > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.observatory-evidence-card__action {
|
||||
padding-top: 0.85rem;
|
||||
border-top: 1px solid rgba(var(--nodedc-accent-rgb), 0.18);
|
||||
}
|
||||
|
||||
.observatory-evidence-card__action > span {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: var(--nodedc-font-size-xs);
|
||||
}
|
||||
|
||||
.observatory-replay,
|
||||
.observatory-replay-state {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.observatory-replay {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.observatory-replay__header {
|
||||
padding: 0.25rem 0.25rem 0;
|
||||
}
|
||||
|
||||
.observatory-replay__header h3,
|
||||
.observatory-replay-state h3 {
|
||||
margin: 0.3rem 0 0;
|
||||
}
|
||||
|
||||
.observatory-replay__header p,
|
||||
.observatory-replay-state p {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--nodedc-text-muted);
|
||||
}
|
||||
|
||||
.observatory-evidence-list strong,
|
||||
.observatory-evidence-list span {
|
||||
display: block;
|
||||
@@ -191,6 +251,13 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.observatory-evidence-card__action,
|
||||
.observatory-replay__header,
|
||||
.observatory-replay-state {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.observatory-catalog-bar__controls {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
@@ -1,249 +1 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
|
||||
|
||||
import { ObservationTimeline } from "../../components/ObservationTimeline";
|
||||
import {
|
||||
RerunViewport,
|
||||
type RerunPlaybackController,
|
||||
type RerunPlaybackState,
|
||||
} from "../../components/RerunViewport";
|
||||
import {
|
||||
CanonicalRecordedLabReplay,
|
||||
useCanonicalRecordedLabReplayState,
|
||||
} from "../../components/laboratory/CanonicalRecordedLabReplay";
|
||||
import {
|
||||
resolveCanonicalLabReplay,
|
||||
type CanonicalLabReplayDescriptor,
|
||||
} from "../../core/laboratory/canonicalLabReplay";
|
||||
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
|
||||
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
|
||||
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import { defaultSceneSettings } from "../../sceneSettings";
|
||||
|
||||
type MediaMode = "video" | "camera";
|
||||
type SpatialMode = "3d" | "plan";
|
||||
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
|
||||
type SemanticLayer = "city" | "vegetation";
|
||||
|
||||
interface CanonicalReplayLaunch {
|
||||
base: ObservationSessionReplayLaunch;
|
||||
replay: CanonicalLabReplayDescriptor;
|
||||
}
|
||||
|
||||
export function CanonicalVegetationRerunReplay({
|
||||
resultId,
|
||||
review,
|
||||
}: {
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
const {
|
||||
mediaMode,
|
||||
spatialMode,
|
||||
splitPrimarySize,
|
||||
splitOrientation,
|
||||
expanded,
|
||||
onMediaModeChange,
|
||||
onSpatialModeChange,
|
||||
onSplitPrimarySizeChange,
|
||||
onExpandedChange,
|
||||
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
|
||||
initialMediaMode: "video",
|
||||
initialSpatialMode: "3d",
|
||||
});
|
||||
const [semanticLayer, setSemanticLayer] = useState<SemanticLayer>("vegetation");
|
||||
const [showSemantics, setShowSemantics] = useState(true);
|
||||
const [spatialLayer, setSpatialLayer] = useState<SpatialLayer>("source");
|
||||
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(0);
|
||||
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
|
||||
const [playbackController, setPlaybackController] =
|
||||
useState<RerunPlaybackController | null>(null);
|
||||
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
|
||||
const [launchError, setLaunchError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLaunch(null);
|
||||
setLaunchError(null);
|
||||
void resolveObservationSessionReplay(review.sessionId, {
|
||||
signal: controller.signal,
|
||||
maximumWaitMs: 30 * 60 * 1000,
|
||||
onUpdate: () => undefined,
|
||||
}).then(async (value) => ({
|
||||
base: value,
|
||||
replay: await resolveCanonicalLabReplay(resultId, value, {
|
||||
signal: controller.signal,
|
||||
}),
|
||||
})).then((value) => {
|
||||
if (!controller.signal.aborted) setLaunch(value);
|
||||
}).catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setLaunchError(
|
||||
caught instanceof Error ? caught.message : "Каноническая запись RAV004 недоступна.",
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [resultId, review.sessionId]);
|
||||
|
||||
const splitView = mediaMode !== null && spatialMode !== null;
|
||||
const sceneSettings = useMemo(() => ({
|
||||
...defaultSceneSettings,
|
||||
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
|
||||
showPoints: spatialMode !== null,
|
||||
showTrajectory: spatialMode !== null,
|
||||
showGrid: spatialMode !== null,
|
||||
pointSize: 3.8,
|
||||
}), [spatialLayer, spatialMode]);
|
||||
const profile = launch ? recordedSessionRerunProfile({
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
artifact: {
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
viewerSourceUrl: launch.replay.viewerSourceUrl,
|
||||
byteLength: launch.replay.byteLength,
|
||||
sha256: launch.replay.sha256,
|
||||
},
|
||||
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
|
||||
autoplayWhenReady: false,
|
||||
presentationGate: "ready",
|
||||
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
|
||||
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
|
||||
initialPlaybackStartSeconds: review.timelineStartSeconds,
|
||||
view: mediaMode !== null ? "perception" : "spatial",
|
||||
viewResetGeneration,
|
||||
followTrajectory: true,
|
||||
semanticLayer,
|
||||
unifiedPerception: splitView,
|
||||
planView: spatialMode === "plan",
|
||||
perceptionLayers: {
|
||||
enabled: mediaMode !== null,
|
||||
detections2d: mediaMode === "video",
|
||||
segmentation: mediaMode === "video" && showSemantics,
|
||||
cuboids3d: false,
|
||||
},
|
||||
perceptionRetryGeneration: 0,
|
||||
lockPerceptionCameraInteraction: false,
|
||||
}) : null;
|
||||
|
||||
const mediaLayerControls = (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Слои камеры и видео"
|
||||
>
|
||||
<Button
|
||||
size="dense"
|
||||
shape="pill"
|
||||
variant={showSemantics ? "primary" : "secondary"}
|
||||
aria-pressed={showSemantics}
|
||||
onClick={() => setShowSemantics((visible) => !visible)}
|
||||
>
|
||||
СЕМАНТИКА
|
||||
</Button>
|
||||
<SegmentedControl
|
||||
value={semanticLayer}
|
||||
items={[
|
||||
{ value: "city", label: "ГОРОД · EoMT" },
|
||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||
]}
|
||||
label="Источник семантики"
|
||||
size="dense"
|
||||
onChange={(value) => {
|
||||
setSemanticLayer(value);
|
||||
setShowSemantics(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const spatialLayerControls = (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Пространственные слои RAV004"
|
||||
>
|
||||
<SegmentedControl
|
||||
value={spatialLayer}
|
||||
items={[
|
||||
{ value: "source", label: "ИСХ. ТОЧКИ" },
|
||||
{ value: "local", label: "ЛОК. SLAM" },
|
||||
{ value: "tgs", label: "TGS", disabled: true },
|
||||
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
|
||||
]}
|
||||
label="Пространственные слои"
|
||||
size="dense"
|
||||
onChange={setSpatialLayer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const resetSpatialView = (
|
||||
<Button
|
||||
size="dense"
|
||||
variant="ghost"
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
aria-label="Сбросить положение 3D камеры"
|
||||
title="Сбросить положение 3D камеры"
|
||||
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
|
||||
>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const transport = playback && playbackController ? (
|
||||
<ObservationTimeline
|
||||
className="m4-replay-threat-visual__timeline"
|
||||
active
|
||||
sourceCount={3}
|
||||
mode="recorded"
|
||||
seekable
|
||||
synchronization="shared-clock"
|
||||
rangeNs={playback.rangeNs}
|
||||
currentNs={playback.currentNs}
|
||||
playing={playback.playing}
|
||||
onSeek={playbackController.seek}
|
||||
onPlayingChange={playbackController.setPlaying}
|
||||
showJumpToEnd={false}
|
||||
/>
|
||||
) : undefined;
|
||||
return (
|
||||
<CanonicalRecordedLabReplay
|
||||
label="RAVNOVES004TREE · канонический повтор Rerun"
|
||||
mediaMode={mediaMode ?? "none"}
|
||||
mediaModes={[
|
||||
{ value: "video", label: "ВИДЕО" },
|
||||
{ value: "camera", label: "КАМЕРА" },
|
||||
]}
|
||||
spatialMode={spatialMode ?? "none"}
|
||||
spatialModes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "plan", label: "ПЛАН" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
splitPrimarySize={splitPrimarySize}
|
||||
splitOrientation={splitOrientation}
|
||||
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
|
||||
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
|
||||
mediaLayerControls={mediaLayerControls}
|
||||
spatialLayerControls={spatialLayerControls}
|
||||
spatialLeadingControl={resetSpatialView}
|
||||
mediaMultiLayer
|
||||
unifiedContent={profile ? (
|
||||
<RerunViewport
|
||||
profile={profile}
|
||||
sceneSettings={sceneSettings}
|
||||
onPlaybackChange={setPlayback}
|
||||
onPlaybackControllerChange={setPlaybackController}
|
||||
/>
|
||||
) : (
|
||||
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
|
||||
{launchError ?? "Готовим единый кэш канонического повтора RAV004…"}
|
||||
</div>
|
||||
)}
|
||||
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
|
||||
transport={transport}
|
||||
onMediaModeChange={onMediaModeChange}
|
||||
onSpatialModeChange={onSpatialModeChange}
|
||||
onExpandedChange={onExpandedChange}
|
||||
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export { CanonicalVegetationRerunReplay } from "../../components/laboratory/CanonicalVegetationRerunReplay";
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
buildLaboratoryProfiles,
|
||||
experimentOptionsForProfile,
|
||||
freshestLaboratorySelection,
|
||||
isLegacyPublishedLaboratoryWork,
|
||||
workOptionsForExperiment,
|
||||
} from "./laboratoryArchiveProfiles";
|
||||
import {
|
||||
@@ -499,12 +500,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
onDeleteBegin: props.sessionArchive.onDeleteBegin,
|
||||
});
|
||||
const publishedWorks = useMemo(
|
||||
() => sessions.items.filter((session) => (
|
||||
session.lab !== null
|
||||
&& session.status === "ready"
|
||||
&& session.replayable
|
||||
&& session.modalities.includes("point-cloud")
|
||||
)).sort((left, right) => (
|
||||
() => sessions.items.filter(isLegacyPublishedLaboratoryWork).sort((left, right) => (
|
||||
Date.parse(right.lab?.runCreatedAtUtc ?? right.startedAtUtc)
|
||||
- Date.parse(left.lab?.runCreatedAtUtc ?? left.startedAtUtc)
|
||||
)),
|
||||
|
||||
@@ -459,6 +459,17 @@ function pipelineIdForSession(session: ObservationSessionSummary): string {
|
||||
return session.lab?.resultKind ?? "legacy-perception";
|
||||
}
|
||||
|
||||
/** Keep capability projections owned by Observatory out of the legacy LAB surface. */
|
||||
export function isLegacyPublishedLaboratoryWork(
|
||||
session: ObservationSessionSummary,
|
||||
): boolean {
|
||||
return session.lab !== null
|
||||
&& session.lab.replayCapability === null
|
||||
&& session.status === "ready"
|
||||
&& session.replayable
|
||||
&& session.modalities.includes("point-cloud");
|
||||
}
|
||||
|
||||
export function buildLaboratoryCatalog({
|
||||
rigLabel,
|
||||
knownWorks,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Button,
|
||||
@@ -8,11 +8,38 @@ import {
|
||||
StatusBadge,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import { CanonicalVegetationRerunReplay } from "../../components/laboratory/CanonicalVegetationRerunReplay";
|
||||
import type { ObservationSessionStatus } from "../../core/observation/sessionArchive";
|
||||
import { createObservationReplayCoordinator } from "../../core/observation/replayCoordinator";
|
||||
import {
|
||||
fetchObservatoryRecordedRunReview,
|
||||
type ObservatoryRecordedRunBinding,
|
||||
} from "../../core/observatory/recordedRun";
|
||||
import { useObservatoryCatalog } from "../../core/observatory/useObservatoryCatalog";
|
||||
import type { WorkspaceDefinition } from "../../productModel";
|
||||
|
||||
const MAX_PRESENTED_EVIDENCE = 6;
|
||||
const EMPTY_OBSERVATORY_ITEMS = [] as const;
|
||||
type ObservatoryRecordedRunReview = Awaited<
|
||||
ReturnType<typeof fetchObservatoryRecordedRunReview>
|
||||
>;
|
||||
|
||||
type ObservatoryReplayState =
|
||||
| { readonly kind: "closed" }
|
||||
| {
|
||||
readonly kind: "loading";
|
||||
readonly binding: ObservatoryRecordedRunBinding;
|
||||
}
|
||||
| {
|
||||
readonly kind: "ready";
|
||||
readonly binding: ObservatoryRecordedRunBinding;
|
||||
readonly review: ObservatoryRecordedRunReview;
|
||||
}
|
||||
| {
|
||||
readonly kind: "error";
|
||||
readonly binding: ObservatoryRecordedRunBinding;
|
||||
readonly message: string;
|
||||
};
|
||||
|
||||
const statusLabel: Record<ObservationSessionStatus, string> = {
|
||||
recording: "Запись идёт",
|
||||
@@ -74,12 +101,22 @@ export function ObservatoryWorkspace({
|
||||
}) {
|
||||
const controller = useObservatoryCatalog();
|
||||
const [selectedSessionId, setSelectedSessionId] = useState("");
|
||||
const items = controller.catalog?.items ?? [];
|
||||
const [replay, setReplay] = useState<ObservatoryReplayState>({ kind: "closed" });
|
||||
const replayCoordinatorRef = useRef(createObservationReplayCoordinator());
|
||||
const items = controller.catalog?.items ?? EMPTY_OBSERVATORY_ITEMS;
|
||||
|
||||
const closeReplay = useCallback(() => {
|
||||
replayCoordinatorRef.current.cancel();
|
||||
setReplay((current) => current.kind === "closed" ? current : { kind: "closed" });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (items.some((item) => item.source.id === selectedSessionId)) return;
|
||||
closeReplay();
|
||||
setSelectedSessionId(items[0]?.source.id ?? "");
|
||||
}, [items, selectedSessionId]);
|
||||
}, [closeReplay, items, selectedSessionId]);
|
||||
|
||||
useEffect(() => () => replayCoordinatorRef.current.cancel(), []);
|
||||
|
||||
const selectedSession = items.find(
|
||||
(item) => item.source.id === selectedSessionId,
|
||||
@@ -96,12 +133,51 @@ export function ObservatoryWorkspace({
|
||||
const initialLoading = !controller.catalog
|
||||
&& ["idle", "loading"].includes(controller.state);
|
||||
const unavailable = !controller.catalog && controller.state === "error";
|
||||
const replayEvidenceId = replay.kind === "closed"
|
||||
? null
|
||||
: replay.binding.evidenceSessionId;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
replayEvidenceId === null
|
||||
|| selectedSession?.evidence.some(
|
||||
(evidence) => evidence.sessionId === replayEvidenceId,
|
||||
)
|
||||
) return;
|
||||
closeReplay();
|
||||
}, [closeReplay, replayEvidenceId, selectedSession]);
|
||||
|
||||
const openReplay = useCallback((binding: ObservatoryRecordedRunBinding) => {
|
||||
const attempt = replayCoordinatorRef.current.begin();
|
||||
setReplay({ kind: "loading", binding });
|
||||
void fetchObservatoryRecordedRunReview(binding, {
|
||||
selectedSourceSessionId: selectedSessionId,
|
||||
signal: attempt.signal,
|
||||
}).then((review) => {
|
||||
if (!attempt.isCurrent() || !attempt.finish()) return;
|
||||
setReplay({ kind: "ready", binding, review });
|
||||
}).catch((caught: unknown) => {
|
||||
if (!attempt.isCurrent() || !attempt.finish()) return;
|
||||
setReplay({
|
||||
kind: "error",
|
||||
binding,
|
||||
message: caught instanceof Error && caught.message.trim()
|
||||
? caught.message
|
||||
: "Канонический визуальный разбор недоступен.",
|
||||
});
|
||||
});
|
||||
}, [selectedSessionId]);
|
||||
|
||||
const selectSession = useCallback((sessionId: string) => {
|
||||
closeReplay();
|
||||
setSelectedSessionId(sessionId);
|
||||
}, [closeReplay]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="observatory-workspace"
|
||||
data-observatory-authority="observation-only"
|
||||
data-observatory-viewer="detached"
|
||||
data-observatory-viewer={replay.kind === "ready" ? "attached" : "detached"}
|
||||
>
|
||||
<section className="observatory-lead">
|
||||
<div>
|
||||
@@ -123,7 +199,7 @@ export function ObservatoryWorkspace({
|
||||
<div className="observatory-catalog-bar__copy">
|
||||
<span className="section-eyebrow">ИСТОЧНИК ДОКАЗАТЕЛЬСТВ</span>
|
||||
<h3>Сохранённая сессия</h3>
|
||||
<p>Выбор меняет только читаемую карточку и не готовит Rerun-запись в фоне.</p>
|
||||
<p>Выбор меняет только читаемую карточку и не готовит визуальный разбор в фоне.</p>
|
||||
</div>
|
||||
<div className="observatory-catalog-bar__controls">
|
||||
<Select
|
||||
@@ -136,7 +212,7 @@ export function ObservatoryWorkspace({
|
||||
emptyLabel="Сессия не найдена"
|
||||
minMenuWidth={360}
|
||||
menuWidth={460}
|
||||
onChange={setSelectedSessionId}
|
||||
onChange={selectSession}
|
||||
/>
|
||||
<Button
|
||||
size="compact"
|
||||
@@ -241,9 +317,14 @@ export function ObservatoryWorkspace({
|
||||
{presentedEvidence.map((evidence) => (
|
||||
<li key={evidence.sessionId}>
|
||||
<GlassSurface className="observatory-evidence-card" padding="md" tone="soft">
|
||||
<div>
|
||||
<strong>{evidence.lab.labId}</strong>
|
||||
<span>{evidence.label}</span>
|
||||
<div className="observatory-evidence-card__heading">
|
||||
<div>
|
||||
<strong>{evidence.lab.labId}</strong>
|
||||
<span>{evidence.label}</span>
|
||||
</div>
|
||||
{evidence.recordedRun ? (
|
||||
<StatusBadge tone="accent">Записанный разбор</StatusBadge>
|
||||
) : null}
|
||||
</div>
|
||||
<dl>
|
||||
<div><dt>Тип результата</dt><dd>{evidence.lab.resultKind}</dd></div>
|
||||
@@ -253,6 +334,34 @@ export function ObservatoryWorkspace({
|
||||
</div>
|
||||
<div><dt>Опубликован</dt><dd>{formatTimestamp(evidence.publishedAtUtc)}</dd></div>
|
||||
</dl>
|
||||
{evidence.recordedRun ? (
|
||||
<div className="observatory-evidence-card__action">
|
||||
<span>
|
||||
Записанный маршрут · единая временная шкала · только наблюдение
|
||||
</span>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="primary"
|
||||
icon={<Icon name="play" size={14} />}
|
||||
disabled={
|
||||
replay.kind === "loading"
|
||||
&& replay.binding.evidenceSessionId === evidence.sessionId
|
||||
}
|
||||
onClick={() => openReplay(evidence.recordedRun!)}
|
||||
>
|
||||
{replay.kind === "loading"
|
||||
&& replay.binding.evidenceSessionId === evidence.sessionId
|
||||
? "Проверяем результат"
|
||||
: replay.kind === "ready"
|
||||
&& replay.binding.evidenceSessionId === evidence.sessionId
|
||||
? "Открыть заново"
|
||||
: replay.kind === "error"
|
||||
&& replay.binding.evidenceSessionId === evidence.sessionId
|
||||
? "Повторить открытие"
|
||||
: "Открыть визуальный разбор"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</GlassSurface>
|
||||
</li>
|
||||
))}
|
||||
@@ -277,6 +386,57 @@ export function ObservatoryWorkspace({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{replay.kind === "loading" ? (
|
||||
<GlassSurface className="observatory-replay-state" padding="lg" role="status">
|
||||
<ActivityIndicator label="Проверяем запечатанный результат" />
|
||||
<div>
|
||||
<h3>Проверяем точную связь результата с исходной сессией</h3>
|
||||
<p>Визуализатор и данные маршрута ещё не запущены.</p>
|
||||
</div>
|
||||
<Button size="compact" variant="ghost" onClick={closeReplay}>Отменить</Button>
|
||||
</GlassSurface>
|
||||
) : replay.kind === "error" ? (
|
||||
<GlassSurface className="observatory-replay-state" padding="lg" role="alert">
|
||||
<Icon name="alert" size={20} />
|
||||
<div>
|
||||
<StatusBadge tone="danger">Просмотр недоступен</StatusBadge>
|
||||
<h3>{replay.message}</h3>
|
||||
</div>
|
||||
<div className="observatory-replay-state__actions">
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
onClick={() => openReplay(replay.binding)}
|
||||
>
|
||||
Повторить
|
||||
</Button>
|
||||
<Button size="compact" variant="ghost" onClick={closeReplay}>Закрыть</Button>
|
||||
</div>
|
||||
</GlassSurface>
|
||||
) : replay.kind === "ready" ? (
|
||||
<section className="observatory-replay" aria-label="Канонический визуальный разбор">
|
||||
<header className="observatory-replay__header">
|
||||
<div>
|
||||
<span className="section-eyebrow">ВИЗУАЛЬНЫЙ РАЗБОР / ЗАПИСАННАЯ СЕССИЯ</span>
|
||||
<h3>RAVNOVES004TREE · полный маршрут восприятия</h3>
|
||||
<p>Записанный маршрут синхронизирован по общей временной шкале.</p>
|
||||
</div>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
icon={<Icon name="close" size={14} />}
|
||||
onClick={closeReplay}
|
||||
>
|
||||
Закрыть разбор
|
||||
</Button>
|
||||
</header>
|
||||
<CanonicalVegetationRerunReplay
|
||||
resultId={replay.binding.resultId}
|
||||
review={replay.review}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(controller.catalog?.unresolvedEvidence.length ?? 0) > 0 ? (
|
||||
<GlassSurface className="observatory-notice" padding="md" tone="soft" role="status">
|
||||
<StatusBadge tone="warning">Вне среза</StatusBadge>
|
||||
|
||||
@@ -11,6 +11,7 @@ let fetchM49TgsAnchorSpatial;
|
||||
let AdvancedLaboratoryContractError;
|
||||
let buildLaboratoryCatalog;
|
||||
let buildLaboratoryProfiles;
|
||||
let isLegacyPublishedLaboratoryWork;
|
||||
let experimentOptionsForProfile;
|
||||
let freshestLaboratorySelection;
|
||||
let workOptionsForExperiment;
|
||||
@@ -928,6 +929,7 @@ before(async () => {
|
||||
({
|
||||
buildLaboratoryCatalog,
|
||||
buildLaboratoryProfiles,
|
||||
isLegacyPublishedLaboratoryWork,
|
||||
experimentOptionsForProfile,
|
||||
freshestLaboratorySelection,
|
||||
workOptionsForExperiment,
|
||||
@@ -962,6 +964,31 @@ after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
test("capability-owned projections never enter the legacy LAB catalog", () => {
|
||||
const legacy = {
|
||||
status: "ready",
|
||||
replayable: true,
|
||||
modalities: ["point-cloud", "trajectory"],
|
||||
lab: { replayCapability: null },
|
||||
};
|
||||
const capabilityProjection = {
|
||||
...legacy,
|
||||
lab: {
|
||||
replayCapability: {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commandsEnabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(isLegacyPublishedLaboratoryWork(legacy), true);
|
||||
assert.equal(isLegacyPublishedLaboratoryWork(capabilityProjection), false);
|
||||
});
|
||||
|
||||
test("LAB catalog is pipeline-scoped and ordered by real run time", () => {
|
||||
const catalog = buildLaboratoryCatalog({
|
||||
rigLabel: "K1",
|
||||
|
||||
@@ -105,10 +105,14 @@ test("laboratory UI is a bounded feature slice, not a central workspace branch",
|
||||
assert.match(e30HumanReviewCss, /\.e30-human-review/);
|
||||
});
|
||||
|
||||
test("Observatory is a bounded read-only slice outside legacy LAB and viewer lifecycles", async () => {
|
||||
test("Observatory owns a bounded explicit-open lifecycle around the shared recorded viewer", async () => {
|
||||
const workspaceHub = await read("workspaces/Workspaces.tsx");
|
||||
const observatory = await read("workspaces/observatory/ObservatoryWorkspace.tsx");
|
||||
const observatoryCore = await read("core/observatory/catalog.ts");
|
||||
const recordedRun = await read("core/observatory/recordedRun.ts");
|
||||
const sharedReplay = await read(
|
||||
"components/laboratory/CanonicalVegetationRerunReplay.tsx",
|
||||
);
|
||||
const workspaceCss = await read("styles/workspaces.css");
|
||||
const observatoryCss = await read("styles/observatory.css");
|
||||
|
||||
@@ -119,12 +123,27 @@ test("Observatory is a bounded read-only slice outside legacy LAB and viewer lif
|
||||
assert.match(observatory, /export function ObservatoryWorkspace/);
|
||||
assert.match(observatory, /useObservatoryCatalog/);
|
||||
assert.match(observatory, /data-observatory-authority="observation-only"/);
|
||||
assert.match(observatory, /data-observatory-viewer="detached"/);
|
||||
assert.match(
|
||||
observatory,
|
||||
/data-observatory-viewer={replay\.kind === "ready" \? "attached" : "detached"}/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
`${observatory}\n${observatoryCore}`,
|
||||
/(?:core|components|workspaces)\/laboratory|\/api\/v1\/laboratory|RerunViewport|ObservationSessionSelect/,
|
||||
/(?:core|workspaces)\/laboratory|\/api\/v1\/laboratory|RerunViewport|ObservationSessionSelect/,
|
||||
);
|
||||
assert.doesNotMatch(observatory, /replayObservationSession|deleteObservationSession/);
|
||||
assert.match(observatory, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
|
||||
assert.match(observatory, /closeReplay\(\);[\s\S]*setSelectedSessionId/);
|
||||
assert.doesNotMatch(
|
||||
observatory,
|
||||
/replayObservationSession|deleteObservationSession|useObservationSessions|useAdvancedLaboratoryCatalog/,
|
||||
);
|
||||
assert.match(recordedRun, /fetchVegetationShadowResultMetadata/);
|
||||
assert.doesNotMatch(
|
||||
recordedRun,
|
||||
/advanced-index|resolveObservationSessionReplay|resolveCanonicalLabReplay|RerunViewport/,
|
||||
);
|
||||
assert.equal(sharedReplay.match(/<RerunViewport\b/g)?.length, 1);
|
||||
assert.match(sharedReplay, /recordedSessionRerunProfile/);
|
||||
assert.doesNotMatch(workspaceCss, /\.observatory-/);
|
||||
assert.match(observatoryCss, /\.observatory-workspace/);
|
||||
});
|
||||
|
||||
@@ -77,6 +77,57 @@ function session(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalLab(overrides = {}) {
|
||||
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
|
||||
return {
|
||||
lab_id: "LAB V1",
|
||||
source_session_id: "20260828T130511Z_viewer_live",
|
||||
result_kind: "recorded-perception-qualification",
|
||||
result_id: resultId,
|
||||
source_result_id: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
|
||||
config_sha256: null,
|
||||
run_created_at_utc: "2026-08-29T18:05:11.329061+00:00",
|
||||
published_at_utc: "2026-08-30T12:00:00.000Z",
|
||||
replay_capability: {
|
||||
schema_version: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewer_profile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commands_enabled: false,
|
||||
},
|
||||
provenance: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalProjectionProvenance(resultId, replayCapability) {
|
||||
return {
|
||||
schema_version: "missioncore.canonical-recorded-lab-projection/v1",
|
||||
evidence_identity_sha256: resultId.slice("lab-v1-vegetation-shadow-".length),
|
||||
result_document_sha256: "a".repeat(64),
|
||||
replay_capability: replayCapability,
|
||||
authority: {
|
||||
commands_enabled: false,
|
||||
navigation_or_safety_accepted: false,
|
||||
actuation_accepted: false,
|
||||
},
|
||||
method: {
|
||||
schema_version: "missioncore.laboratory-method/v1",
|
||||
completeness: "legacy-partial",
|
||||
execution_class: "ai-inference",
|
||||
pipeline_id: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
components: [{
|
||||
kind: "source",
|
||||
name: "sealed full-route LAB result",
|
||||
version: "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
role: "immutable Session catalog projection",
|
||||
identity_sha256: resultId.slice("lab-v1-vegetation-shadow-".length),
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function replay(overrides = {}) {
|
||||
const sessionId = overrides.session_id ?? "session-20260716T205632Z";
|
||||
const sourceUrl = overrides.source_url ??
|
||||
@@ -141,6 +192,74 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
|
||||
assert.equal("path" in catalog.items[0], false);
|
||||
});
|
||||
|
||||
test("session catalog strictly decodes the explicit recorded LAB replay capability", () => {
|
||||
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
|
||||
const decoded = decodeObservationSessionCatalog({
|
||||
items: [session({ id: resultId, lab: canonicalLab() })],
|
||||
}).items[0].lab;
|
||||
|
||||
assert.deepEqual(decoded.replayCapability, {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commandsEnabled: false,
|
||||
});
|
||||
const rollingUpgradeLab = canonicalLab();
|
||||
rollingUpgradeLab.provenance = canonicalProjectionProvenance(
|
||||
resultId,
|
||||
rollingUpgradeLab.replay_capability,
|
||||
);
|
||||
delete rollingUpgradeLab.replay_capability;
|
||||
assert.deepEqual(
|
||||
decodeObservationSessionCatalog({
|
||||
items: [session({ id: resultId, lab: rollingUpgradeLab })],
|
||||
}).items[0].lab.replayCapability,
|
||||
decoded.replayCapability,
|
||||
);
|
||||
const invalidComponents = [
|
||||
{ kind: "tool" },
|
||||
{ name: "unsealed result" },
|
||||
{ version: "missioncore.lab-v1-vegetation-shadow/v0" },
|
||||
{ role: "mutable projection" },
|
||||
{ identity_sha256: "0".repeat(64) },
|
||||
{ unexpected: true },
|
||||
];
|
||||
for (const componentOverride of invalidComponents) {
|
||||
const invalidRollingUpgradeLab = canonicalLab();
|
||||
invalidRollingUpgradeLab.provenance = canonicalProjectionProvenance(
|
||||
resultId,
|
||||
invalidRollingUpgradeLab.replay_capability,
|
||||
);
|
||||
invalidRollingUpgradeLab.provenance.method.components = [{
|
||||
...invalidRollingUpgradeLab.provenance.method.components[0],
|
||||
...componentOverride,
|
||||
}];
|
||||
delete invalidRollingUpgradeLab.replay_capability;
|
||||
assert.throws(
|
||||
() => decodeObservationSessionCatalog({
|
||||
items: [session({ id: resultId, lab: invalidRollingUpgradeLab })],
|
||||
}),
|
||||
ObservationSessionContractError,
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => decodeObservationSessionCatalog({
|
||||
items: [session({
|
||||
id: resultId,
|
||||
lab: canonicalLab({
|
||||
replay_capability: {
|
||||
...canonicalLab().replay_capability,
|
||||
commands_enabled: true,
|
||||
},
|
||||
}),
|
||||
})],
|
||||
}),
|
||||
ObservationSessionContractError,
|
||||
);
|
||||
});
|
||||
|
||||
test("opened archive is named in the scene header and trash hover has no pill", async () => {
|
||||
const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
|
||||
const styles = await readFile(
|
||||
@@ -246,7 +365,7 @@ test("source and laboratory catalogs are requested as disjoint backend projectio
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"/api/v1/observation-sessions?limit=100&scope=source",
|
||||
"/api/v1/observation-sessions?limit=100&scope=laboratory",
|
||||
"/api/v1/observation-sessions?limit=100&scope=laboratory&lab_contract=v2",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -60,11 +60,47 @@ function evidence(id, sourceSessionId, publishedAtUtc) {
|
||||
configSha256: "a".repeat(64),
|
||||
runCreatedAtUtc: publishedAtUtc,
|
||||
publishedAtUtc,
|
||||
replayCapability: null,
|
||||
provenance: { verdict: "must-not-be-inferred" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalProvenance(resultId) {
|
||||
const identitySha256 = resultId.slice("lab-v1-vegetation-shadow-".length);
|
||||
return {
|
||||
schema_version: "missioncore.canonical-recorded-lab-projection/v1",
|
||||
evidence_identity_sha256: identitySha256,
|
||||
result_document_sha256: "a".repeat(64),
|
||||
replay_capability: {
|
||||
schema_version: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewer_profile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commands_enabled: false,
|
||||
},
|
||||
authority: {
|
||||
commands_enabled: false,
|
||||
navigation_or_safety_accepted: false,
|
||||
actuation_accepted: false,
|
||||
},
|
||||
method: {
|
||||
schema_version: "missioncore.laboratory-method/v1",
|
||||
completeness: "legacy-partial",
|
||||
execution_class: "ai-inference",
|
||||
pipeline_id: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
components: [{
|
||||
kind: "source",
|
||||
name: "sealed full-route LAB result",
|
||||
version: "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
role: "immutable Session catalog projection",
|
||||
identity_sha256: identitySha256,
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("Observatory joins evidence only by sourceSessionId and keeps deterministic order", () => {
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[
|
||||
@@ -130,7 +166,7 @@ test("Observatory fetches disjoint read-only source and laboratory projections",
|
||||
assert.deepEqual(
|
||||
calls.map(({ input }) => input).sort(),
|
||||
[
|
||||
"/api/v1/observation-sessions?limit=50&scope=laboratory",
|
||||
"/api/v1/observation-sessions?limit=50&scope=laboratory&lab_contract=v2",
|
||||
"/api/v1/observation-sessions?limit=50&scope=source",
|
||||
],
|
||||
);
|
||||
@@ -157,3 +193,43 @@ test("Observatory exposes bounded-window uncertainty without inventing a broken
|
||||
["outside-window"],
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory projects a typed canonical run only through its exact sourceSessionId", () => {
|
||||
const canonicalResultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
|
||||
const canonical = evidence(
|
||||
canonicalResultId,
|
||||
"20260828T130511Z_viewer_live",
|
||||
"2026-08-29T18:05:11Z",
|
||||
);
|
||||
canonical.lab = {
|
||||
...canonical.lab,
|
||||
labId: "LAB V1",
|
||||
resultKind: "recorded-perception-qualification",
|
||||
resultId: canonicalResultId,
|
||||
sourceResultId: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
|
||||
configSha256: null,
|
||||
provenance: canonicalProvenance(canonicalResultId),
|
||||
replayCapability: {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commandsEnabled: false,
|
||||
},
|
||||
};
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[source("20260828T130511Z_viewer_live", "2026-08-28T13:05:11Z")],
|
||||
[canonical],
|
||||
);
|
||||
|
||||
assert.deepEqual(catalog.items[0].evidence[0].recordedRun, {
|
||||
kind: "canonical-recorded-rerun",
|
||||
evidenceSessionId: canonicalResultId,
|
||||
sourceSessionId: "20260828T130511Z_viewer_live",
|
||||
resultId: canonicalResultId,
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let admitObservatoryRecordedRunReview;
|
||||
let observatoryRecordedRunBinding;
|
||||
let ObservatoryRecordedRunContractError;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
admitObservatoryRecordedRunReview,
|
||||
observatoryRecordedRunBinding,
|
||||
ObservatoryRecordedRunContractError,
|
||||
} = await server.ssrLoadModule("/src/core/observatory/recordedRun.ts"));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
const sourceSessionId = "20260828T130511Z_viewer_live";
|
||||
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
|
||||
|
||||
function canonicalProvenance() {
|
||||
const identitySha256 = resultId.slice("lab-v1-vegetation-shadow-".length);
|
||||
return {
|
||||
schema_version: "missioncore.canonical-recorded-lab-projection/v1",
|
||||
evidence_identity_sha256: identitySha256,
|
||||
result_document_sha256: "a".repeat(64),
|
||||
replay_capability: {
|
||||
schema_version: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewer_profile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commands_enabled: false,
|
||||
},
|
||||
authority: {
|
||||
commands_enabled: false,
|
||||
navigation_or_safety_accepted: false,
|
||||
actuation_accepted: false,
|
||||
},
|
||||
method: {
|
||||
schema_version: "missioncore.laboratory-method/v1",
|
||||
completeness: "legacy-partial",
|
||||
execution_class: "ai-inference",
|
||||
pipeline_id: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
components: [{
|
||||
kind: "source",
|
||||
name: "sealed full-route LAB result",
|
||||
version: "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
role: "immutable Session catalog projection",
|
||||
identity_sha256: identitySha256,
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function lab(overrides = {}) {
|
||||
return {
|
||||
labId: "LAB V1",
|
||||
sourceSessionId,
|
||||
resultKind: "recorded-perception-qualification",
|
||||
resultId,
|
||||
sourceResultId: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
|
||||
configSha256: null,
|
||||
runCreatedAtUtc: "2026-08-29T18:05:11.329061+00:00",
|
||||
publishedAtUtc: "2026-08-30T12:00:00.000Z",
|
||||
replayCapability: {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
|
||||
kind: "canonical-recorded-rerun",
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
commandsEnabled: false,
|
||||
},
|
||||
provenance: canonicalProvenance(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("Observatory admits only the exact typed canonical recorded run", () => {
|
||||
const binding = observatoryRecordedRunBinding(resultId, lab());
|
||||
assert.deepEqual(binding, {
|
||||
kind: "canonical-recorded-rerun",
|
||||
evidenceSessionId: resultId,
|
||||
sourceSessionId,
|
||||
resultId,
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
});
|
||||
assert.equal(
|
||||
observatoryRecordedRunBinding(resultId, lab({ replayCapability: null })),
|
||||
null,
|
||||
);
|
||||
assert.throws(
|
||||
() => observatoryRecordedRunBinding("different-session", lab()),
|
||||
ObservatoryRecordedRunContractError,
|
||||
);
|
||||
assert.throws(
|
||||
() => observatoryRecordedRunBinding(
|
||||
resultId,
|
||||
lab({ sourceSessionId: "RAVNOVES004TREE" }),
|
||||
),
|
||||
ObservatoryRecordedRunContractError,
|
||||
);
|
||||
assert.throws(
|
||||
() => observatoryRecordedRunBinding(
|
||||
resultId,
|
||||
lab({ configSha256: "a".repeat(64) }),
|
||||
),
|
||||
ObservatoryRecordedRunContractError,
|
||||
);
|
||||
assert.throws(
|
||||
() => observatoryRecordedRunBinding(
|
||||
resultId,
|
||||
lab({ sourceResultId: "legacy-result" }),
|
||||
),
|
||||
ObservatoryRecordedRunContractError,
|
||||
);
|
||||
const invalidProvenance = canonicalProvenance();
|
||||
invalidProvenance.method.components[0].identity_sha256 = "0".repeat(64);
|
||||
assert.throws(
|
||||
() => observatoryRecordedRunBinding(
|
||||
resultId,
|
||||
lab({ provenance: invalidProvenance }),
|
||||
),
|
||||
ObservatoryRecordedRunContractError,
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory rechecks the selected source and sealed review before mounting replay", () => {
|
||||
const binding = observatoryRecordedRunBinding(resultId, lab());
|
||||
const review = { sessionId: sourceSessionId, frameCount: 6830 };
|
||||
assert.equal(
|
||||
admitObservatoryRecordedRunReview(
|
||||
binding,
|
||||
sourceSessionId,
|
||||
{ resultId, routeFullReview: review },
|
||||
),
|
||||
review,
|
||||
);
|
||||
assert.throws(
|
||||
() => admitObservatoryRecordedRunReview(
|
||||
binding,
|
||||
"another-session",
|
||||
{ resultId, routeFullReview: review },
|
||||
),
|
||||
ObservatoryRecordedRunContractError,
|
||||
);
|
||||
assert.throws(
|
||||
() => admitObservatoryRecordedRunReview(
|
||||
binding,
|
||||
sourceSessionId,
|
||||
{ resultId, routeFullReview: { ...review, sessionId: "another-session" } },
|
||||
),
|
||||
ObservatoryRecordedRunContractError,
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory run admission remains metadata-only until explicit UI activation", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/core/observatory/recordedRun.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(source, /fetchVegetationShadowResultMetadata/);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/fetchVegetationShadowResult\(|advanced-index|resolveObservationSessionReplay|resolveCanonicalLabReplay|RerunViewport|recording\.rrd/,
|
||||
);
|
||||
});
|
||||
@@ -46,12 +46,25 @@ test("Observatory is the third independent Polygon workspace", () => {
|
||||
assert.equal(productModel.workspaceById("lab-archive").kind, "lab-archive");
|
||||
});
|
||||
|
||||
test("Observatory reads catalog evidence without inheriting replay or LAB composition", async () => {
|
||||
const [app, workspaceHub, workspace, hook, viewerProfiles, styles] = await Promise.all([
|
||||
test("Observatory mounts the one shared canonical replay only after explicit admission", async () => {
|
||||
const [
|
||||
app,
|
||||
workspaceHub,
|
||||
workspace,
|
||||
hook,
|
||||
recordedRun,
|
||||
sharedReplay,
|
||||
legacyReplayWrapper,
|
||||
viewerProfiles,
|
||||
styles,
|
||||
] = await Promise.all([
|
||||
read("App.tsx"),
|
||||
read("workspaces/Workspaces.tsx"),
|
||||
read("workspaces/observatory/ObservatoryWorkspace.tsx"),
|
||||
read("core/observatory/useObservatoryCatalog.ts"),
|
||||
read("core/observatory/recordedRun.ts"),
|
||||
read("components/laboratory/CanonicalVegetationRerunReplay.tsx"),
|
||||
read("workspaces/laboratory/CanonicalVegetationRerunReplay.tsx"),
|
||||
read("core/observation/viewerProfile.ts"),
|
||||
read("styles/observatory.css"),
|
||||
]);
|
||||
@@ -68,11 +81,30 @@ test("Observatory reads catalog evidence without inheriting replay or LAB compos
|
||||
assert.match(workspace, /вне текущего загруженного среза/);
|
||||
assert.match(workspace, /observatory-notice__copy/);
|
||||
assert.match(workspace, /observatory-evidence-card/);
|
||||
assert.match(workspace, /Открыть визуальный разбор/);
|
||||
assert.match(workspace, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
|
||||
assert.match(workspace, /Проверяем точную связь результата/);
|
||||
assert.match(workspace, /role="alert"/);
|
||||
assert.match(workspace, /Повторить/);
|
||||
assert.match(workspace, /Закрыть разбор/);
|
||||
assert.match(workspace, /const selectSession[\s\S]*closeReplay\(\);[\s\S]*setSelectedSessionId/);
|
||||
assert.match(workspace, /data-observatory-authority="observation-only"/);
|
||||
assert.doesNotMatch(workspace, /Нарушена связь|compactIdentity/);
|
||||
assert.doesNotMatch(
|
||||
`${workspace}\n${hook}`,
|
||||
/RerunViewport|ObservationSessionSelect|resolveObservationSessionReplay|deleteObservationSession|setInterval|setTimeout/,
|
||||
/RerunViewport|ObservationSessionSelect|resolveObservationSessionReplay|resolveCanonicalLabReplay|deleteObservationSession|setInterval|setTimeout|useObservationSessions|useAdvancedLaboratoryCatalog|advanced-index|prefetch|preload/,
|
||||
);
|
||||
assert.equal(sharedReplay.match(/<RerunViewport\b/g)?.length, 1);
|
||||
assert.match(sharedReplay, /recordedSessionRerunProfile/);
|
||||
assert.match(sharedReplay, /timelineStartSeconds/);
|
||||
assert.doesNotMatch(sharedReplay, /live-acquisition|lab-recorded-evidence/);
|
||||
assert.match(
|
||||
legacyReplayWrapper,
|
||||
/export \{ CanonicalVegetationRerunReplay \} from "\.\.\/\.\.\/components\/laboratory\/CanonicalVegetationRerunReplay"/,
|
||||
);
|
||||
assert.match(recordedRun, /fetchVegetationShadowResultMetadata/);
|
||||
assert.doesNotMatch(recordedRun, /advanced-index|recording\.rrd|canonical-replay\.rrd/);
|
||||
assert.doesNotMatch(workspace, /workspaces\/laboratory|LaboratoryRecordedClipPlayer|SimulationViewport/);
|
||||
assert.match(viewerProfiles, /kind: "live-acquisition"/);
|
||||
assert.match(viewerProfiles, /kind: "recorded-session"/);
|
||||
assert.match(viewerProfiles, /kind: "lab-recorded-evidence"/);
|
||||
|
||||
@@ -470,7 +470,7 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
|
||||
"utf8",
|
||||
),
|
||||
readFile(
|
||||
new URL("../src/workspaces/laboratory/CanonicalVegetationRerunReplay.tsx", import.meta.url),
|
||||
new URL("../src/components/laboratory/CanonicalVegetationRerunReplay.tsx", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -51,9 +51,48 @@ The existing source-specific viewer contracts remain separate and unchanged:
|
||||
|
||||
Canonical LAB compositions may configure the recorded Rerun engine inside the bounded LAB slice,
|
||||
but that does not make Observatory a LAB or live consumer. The first Observatory slice mounts no
|
||||
viewer at all. A later viewer slice may activate only the historical `recorded-session` profile
|
||||
after an explicit Session selection and a separate acceptance proof. It must not add a fourth
|
||||
profile or silently collapse the three existing lifecycles.
|
||||
viewer while its catalog is opened, refreshed or navigated. Its bounded canary may activate only
|
||||
the historical `recorded-session` profile after a separate explicit action and acceptance proof.
|
||||
It does not add a fourth profile or silently collapse the three existing lifecycles.
|
||||
|
||||
## M5.1 canary: canonical recorded replay
|
||||
|
||||
The accepted viewer canary reuses the stabilized full-route RAV004 composition; it does not create
|
||||
a second viewer or a second recorded-data pipeline. The sealed vegetation result is first projected
|
||||
into the generic Session/LAB catalog through `SessionStore.publish_lab_instance`. Its exact
|
||||
`route_full_review.session_id` is the only source relationship. Labels, `latest`, result prefixes and
|
||||
the advanced LAB index are not valid joins.
|
||||
|
||||
The immutable LAB projection carries a versioned replay capability:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "missioncore.observation-lab-replay-capability/v1",
|
||||
"kind": "canonical-recorded-rerun",
|
||||
"viewer_profile": "recorded-session",
|
||||
"timeline": "session_time",
|
||||
"activation": "explicit",
|
||||
"commands_enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
Catalog entry, Session selection and result selection remain lightweight. Only the explicit
|
||||
`Открыть визуальный разбор` action reads and revalidates the sealed result metadata. Only after that
|
||||
admission does the application mount the shared canonical replay component, which follows the
|
||||
existing `resolveObservationSessionReplay → resolveCanonicalLabReplay → recordedSessionRerunProfile
|
||||
→ RerunViewport` path. Closing the review or changing Session unmounts that component and aborts
|
||||
pending admission. At most one viewer and one `session_time` clock exist.
|
||||
|
||||
The publication command verifies the registered result root, schema, content-addressed identity,
|
||||
artifact hashes, exact RAV004 source Session and all false authority flags before adding an
|
||||
idempotent SQLite projection. The validated source snapshot is compared again inside the same
|
||||
SQLite write transaction, so reconciliation cannot swap the Session between admission and copy.
|
||||
The default v1 catalog continues to hide capability-owned projections from legacy consumers; the
|
||||
explicit v2 LAB catalog returns the typed capability. A narrowly matched rolling migration types
|
||||
the already-published canonical projection and leaves ordinary legacy LAB rows untouched. The
|
||||
command does not run inference, ffmpeg, RRD merge or replay
|
||||
materialization; source/result evidence is not copied or rewritten. Legacy LAB continues to import
|
||||
the same shared replay through a compatibility wrapper.
|
||||
|
||||
## Information hierarchy and states
|
||||
|
||||
@@ -89,5 +128,19 @@ workspace.
|
||||
the historical tail remains in legacy LAB and is never mounted into a long Observatory DOM.
|
||||
- Loading, empty, refreshing, error/retry and ready states contain no synthetic data.
|
||||
- Linked evidence is never presented as a CV or safety pass.
|
||||
- Existing LAB, Simulation, K1 and Data/Sessions code paths remain unchanged.
|
||||
- Existing LAB, Simulation, K1 and Data/Sessions operator behavior remains unchanged.
|
||||
- Live, historical Session and legacy LAB viewer-profile contracts remain distinct.
|
||||
|
||||
## Canary acceptance
|
||||
|
||||
- RAV004 appears as one exact LAB result linked to `20260828T130511Z_viewer_live` in the Session
|
||||
catalog; Observatory never reads `/api/v1/laboratory/advanced-index`.
|
||||
- Entering Observatory and changing Session perform no replay POST, RRD HEAD/GET, viewer mount or
|
||||
canvas creation.
|
||||
- Explicit open validates the result/source binding, then uses the existing canonical RRD,
|
||||
blueprint, cache, `recorded-session` profile and `session_time` clock.
|
||||
- One `RerunViewport` is mounted; legacy clip/Three renderers, a second clock and a second RRD
|
||||
builder are absent.
|
||||
- Close, Session change and run replacement abort pending work and fully unmount the viewer.
|
||||
- K1, Worker 006/add-worker, Simulation/Gaussian, raw Sessions and legacy LAB behavior remain
|
||||
unchanged.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish one exact sealed RAV004 result into the Session/LAB catalog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.laboratory.canonical_recorded_catalog import (
|
||||
publish_canonical_recorded_vegetation_result,
|
||||
)
|
||||
from k1link.sessions import SessionStore
|
||||
from k1link.sessions.store import resolve_missioncore_data_dir
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Verify and publish an immutable canonical recorded LAB result. "
|
||||
"This command does not build RRD, run inference or alter source evidence."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--repository-root", required=True, type=Path)
|
||||
parser.add_argument("--result-id", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = _arguments()
|
||||
repository_root = arguments.repository_root.expanduser().absolute()
|
||||
if repository_root.is_symlink():
|
||||
raise SystemExit("repository root must not be a symlink")
|
||||
try:
|
||||
repository_root = repository_root.resolve(strict=True)
|
||||
runtime_root = (
|
||||
repository_root / ".runtime" / "compute-experiments"
|
||||
).resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise SystemExit("canonical repository runtime is unavailable") from exc
|
||||
if not repository_root.is_dir() or not runtime_root.is_dir():
|
||||
raise SystemExit("canonical repository runtime is unavailable")
|
||||
if not runtime_root.is_relative_to(repository_root):
|
||||
raise SystemExit("canonical repository runtime escaped its repository root")
|
||||
data_dir = resolve_missioncore_data_dir(repository_root)
|
||||
database_path = data_dir / "mission-core.sqlite3"
|
||||
if database_path.is_symlink() or not database_path.is_file():
|
||||
raise SystemExit("existing Mission Core catalog is unavailable")
|
||||
result_root = (
|
||||
runtime_root
|
||||
/ "lab-v1-vegetation"
|
||||
/ "results"
|
||||
/ arguments.result_id
|
||||
)
|
||||
binding = publish_canonical_recorded_vegetation_result(
|
||||
store=SessionStore(repository_root, data_dir=data_dir),
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
print(json.dumps(binding.as_dict(), ensure_ascii=False, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,670 @@
|
||||
"""Publish an immutable canonical recorded LAB result into the Session catalog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import struct
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
||||
from k1link.laboratory.evidence_report import (
|
||||
LaboratoryEvidenceReportError,
|
||||
verify_laboratory_evidence_result,
|
||||
)
|
||||
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
|
||||
from k1link.sessions import LabReplayCapability, LabSessionBinding, SessionStore
|
||||
from k1link.sessions.models import SessionDetail, SessionStoreError
|
||||
|
||||
CANONICAL_RECORDED_PROJECTION_SCHEMA: Final = (
|
||||
"missioncore.canonical-recorded-lab-projection/v1"
|
||||
)
|
||||
CANONICAL_REPLAY_CAPABILITY_SCHEMA: Final = (
|
||||
"missioncore.observation-lab-replay-capability/v1"
|
||||
)
|
||||
CANONICAL_SOURCE_SESSION_ID: Final = "20260828T130511Z_viewer_live"
|
||||
CANONICAL_SOURCE_LABEL: Final = "RAVNOVES004TREE"
|
||||
CANONICAL_WORK_ID: Final = "lab-v1-vegetation-shadow"
|
||||
CANONICAL_RESULT_PREFIX: Final = "lab-v1-vegetation-shadow"
|
||||
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
|
||||
_RESULT_ID = re.compile(r"^lab-v1-vegetation-shadow-[a-f0-9]{64}$")
|
||||
_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
work_id=CANONICAL_WORK_ID,
|
||||
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
|
||||
result_id_prefix=CANONICAL_RESULT_PREFIX,
|
||||
document_name="result.json",
|
||||
result_schema_version=LAB_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class CanonicalRecordedCatalogError(ValueError):
|
||||
"""The sealed result cannot be admitted to the recorded Session catalog."""
|
||||
|
||||
|
||||
def publish_canonical_recorded_vegetation_result(
|
||||
*,
|
||||
store: SessionStore,
|
||||
runtime_root: Path,
|
||||
result_root: Path,
|
||||
) -> LabSessionBinding:
|
||||
"""Project one exact, verified full-route RAV004 result without computing replay data."""
|
||||
|
||||
runtime = _real_directory(runtime_root, "LAB runtime root")
|
||||
candidate = _real_directory(result_root, "canonical LAB result")
|
||||
if _RESULT_ID.fullmatch(candidate.name) is None:
|
||||
raise CanonicalRecordedCatalogError("canonical LAB result identity is invalid")
|
||||
expected_parent = _real_directory(
|
||||
_DEFINITION.result_root(runtime),
|
||||
"canonical LAB result collection",
|
||||
)
|
||||
if not expected_parent.is_relative_to(runtime):
|
||||
raise CanonicalRecordedCatalogError(
|
||||
"canonical LAB result collection escaped its runtime root"
|
||||
)
|
||||
if candidate.parent != expected_parent:
|
||||
raise CanonicalRecordedCatalogError("canonical LAB result escaped its registered root")
|
||||
|
||||
try:
|
||||
proof = verify_laboratory_evidence_result(_DEFINITION, candidate)
|
||||
except LaboratoryEvidenceReportError as exc:
|
||||
raise CanonicalRecordedCatalogError(str(exc)) from exc
|
||||
document = _read_document(
|
||||
candidate / _DEFINITION.document_name,
|
||||
expected_sha256=str(proof["document_sha256"]),
|
||||
)
|
||||
result_id = _exact_text(document.get("result_id"), candidate.name, "result id")
|
||||
_exact_text(document.get("schema_version"), LAB_SCHEMA, "result schema")
|
||||
_exact_text(
|
||||
document.get("status"),
|
||||
"visual-shadow-ready-policy-not-authorized",
|
||||
"result status",
|
||||
)
|
||||
identity_sha256 = _exact_text(
|
||||
document.get("identity_sha256"),
|
||||
str(proof["identity_sha256"]),
|
||||
"identity digest",
|
||||
)
|
||||
authority = _object(document.get("authority"), "result authority")
|
||||
_require_observation_only(authority, "result authority")
|
||||
identity = _object(document.get("identity"), "result identity")
|
||||
identity_authority = _object(identity.get("authority"), "identity authority")
|
||||
_require_observation_only(identity_authority, "identity authority")
|
||||
if identity_authority != authority:
|
||||
raise CanonicalRecordedCatalogError("top-level authority is not identity-bound")
|
||||
|
||||
if document.get("route_video") is not None or document.get("route_review") is not None:
|
||||
raise CanonicalRecordedCatalogError("canonical full-route result shape is invalid")
|
||||
review = _object(document.get("route_full_review"), "full-route review")
|
||||
identity_review = _object(
|
||||
identity.get("route_full_review"),
|
||||
"identity full-route review",
|
||||
)
|
||||
if identity_review != review:
|
||||
raise CanonicalRecordedCatalogError("full-route review is not identity-bound")
|
||||
_exact_text(review.get("source_id"), CANONICAL_SOURCE_LABEL, "source label")
|
||||
source_session_id = _exact_text(
|
||||
review.get("session_id"),
|
||||
CANONICAL_SOURCE_SESSION_ID,
|
||||
"source session id",
|
||||
)
|
||||
if review.get("frame_count") != 6830:
|
||||
raise CanonicalRecordedCatalogError("canonical frame count is invalid")
|
||||
source_result_id = _text(
|
||||
review.get("linked_route_review_result_id"),
|
||||
"linked route review result id",
|
||||
)
|
||||
if _RESULT_ID.fullmatch(source_result_id) is None:
|
||||
raise CanonicalRecordedCatalogError("linked route review identity is invalid")
|
||||
if identity.get("base_result_id") != source_result_id:
|
||||
raise CanonicalRecordedCatalogError("base result is not identity-bound")
|
||||
result_source = _object(document.get("source"), "result source")
|
||||
if result_source != _object(
|
||||
identity.get("source"),
|
||||
"identity source",
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("result source is not identity-bound")
|
||||
if result_source != {
|
||||
"shadow_session": CANONICAL_SOURCE_LABEL,
|
||||
"shadow_camera": "sensor.camera.right",
|
||||
"shadow_frame_count": 6830,
|
||||
"video_shadow_frame_count": 6830,
|
||||
}:
|
||||
raise CanonicalRecordedCatalogError("canonical source identity changed")
|
||||
timeline_start = _finite_number(
|
||||
review.get("timeline_start_seconds"),
|
||||
"timeline start",
|
||||
)
|
||||
timeline_end = _finite_number(
|
||||
review.get("timeline_end_seconds"),
|
||||
"timeline end",
|
||||
)
|
||||
if timeline_end <= timeline_start:
|
||||
raise CanonicalRecordedCatalogError("canonical timeline range is invalid")
|
||||
_validate_viewer_contract(candidate, document, review)
|
||||
try:
|
||||
source_detail, source_catalog_sha256 = (
|
||||
store.get_session_with_catalog_snapshot(source_session_id)
|
||||
)
|
||||
except SessionStoreError as exc:
|
||||
raise CanonicalRecordedCatalogError(
|
||||
"canonical source session is unavailable"
|
||||
) from exc
|
||||
_validate_source_catalog_binding(source_detail)
|
||||
|
||||
sealed_method = _object(document.get("method"), "sealed method")
|
||||
pipeline_id = _exact_text(
|
||||
sealed_method.get("pipeline_id"),
|
||||
"ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"sealed pipeline id",
|
||||
)
|
||||
execution_class = _exact_text(
|
||||
sealed_method.get("execution_class"),
|
||||
"ai-inference",
|
||||
"sealed execution class",
|
||||
)
|
||||
capability = LabReplayCapability(
|
||||
schema_version=CANONICAL_REPLAY_CAPABILITY_SCHEMA,
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
provenance = {
|
||||
"schema_version": CANONICAL_RECORDED_PROJECTION_SCHEMA,
|
||||
"evidence_identity_sha256": identity_sha256,
|
||||
"result_document_sha256": proof["document_sha256"],
|
||||
"replay_capability": capability.as_dict(),
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
},
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": execution_class,
|
||||
"pipeline_id": pipeline_id,
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": LAB_SCHEMA,
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": identity_sha256,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
return store.publish_lab_instance(
|
||||
session_id=result_id,
|
||||
source_session_id=source_session_id,
|
||||
display_name="RAVNOVES004TREE · полный маршрут восприятия",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id=result_id,
|
||||
source_result_id=source_result_id,
|
||||
config_sha256=None,
|
||||
run_created_at_utc=_text(document.get("created_at_utc"), "creation time"),
|
||||
replay_capability=capability,
|
||||
provenance=provenance,
|
||||
duration_seconds=timeline_end - timeline_start,
|
||||
include_recorded_media=False,
|
||||
expected_source_catalog_sha256=source_catalog_sha256,
|
||||
)
|
||||
|
||||
|
||||
def _real_directory(path: Path, label: str) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise CanonicalRecordedCatalogError(f"{label} must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise CanonicalRecordedCatalogError(f"{label} is unavailable") from exc
|
||||
if not resolved.is_dir():
|
||||
raise CanonicalRecordedCatalogError(f"{label} must be a directory")
|
||||
return resolved
|
||||
|
||||
|
||||
def _read_document(path: Path, *, expected_sha256: str) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_DOCUMENT_BYTES:
|
||||
raise CanonicalRecordedCatalogError("canonical LAB document is unavailable")
|
||||
try:
|
||||
payload = path.read_bytes()
|
||||
if hashlib.sha256(payload).hexdigest() != expected_sha256:
|
||||
raise CanonicalRecordedCatalogError(
|
||||
"canonical LAB document changed after verification"
|
||||
)
|
||||
value = json.loads(payload)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise CanonicalRecordedCatalogError("canonical LAB document is invalid") from exc
|
||||
return _object(value, "canonical LAB document")
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
||||
raise CanonicalRecordedCatalogError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _text(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip() or value != value.strip():
|
||||
raise CanonicalRecordedCatalogError(f"{label} must be a non-empty string")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_text(value: object, expected: str, label: str) -> str:
|
||||
text = _text(value, label)
|
||||
if text != expected:
|
||||
raise CanonicalRecordedCatalogError(f"{label} changed")
|
||||
return text
|
||||
|
||||
|
||||
def _finite_number(value: object, label: str) -> float:
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise CanonicalRecordedCatalogError(f"{label} must be numeric")
|
||||
number = float(value)
|
||||
if not math.isfinite(number):
|
||||
raise CanonicalRecordedCatalogError(f"{label} must be finite")
|
||||
return number
|
||||
|
||||
|
||||
def _require_observation_only(value: dict[str, Any], label: str) -> None:
|
||||
expected = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
"camera_semantics_can_clear_rigid_geometry": False,
|
||||
}
|
||||
if set(value) != set(expected) or any(value[key] is not False for key in expected):
|
||||
raise CanonicalRecordedCatalogError(f"{label} permits control authority")
|
||||
|
||||
|
||||
def _validate_viewer_contract(
|
||||
result_root: Path,
|
||||
document: dict[str, Any],
|
||||
review: dict[str, Any],
|
||||
) -> None:
|
||||
"""Require the metadata and artifacts consumed before the shared viewer mounts."""
|
||||
|
||||
if document.get("ground_truth") is not False:
|
||||
raise CanonicalRecordedCatalogError("canonical result ground-truth marker changed")
|
||||
identity = _object(document.get("identity"), "result identity")
|
||||
selected_candidate = _text(
|
||||
identity.get("selected_candidate"),
|
||||
"selected candidate",
|
||||
)
|
||||
if selected_candidate not in {"ddrnet", "ppliteseg"}:
|
||||
raise CanonicalRecordedCatalogError("canonical selected candidate changed")
|
||||
|
||||
metrics = _object(document.get("metrics"), "result metrics")
|
||||
candidates = _object(metrics.get("candidates"), "candidate metrics")
|
||||
identity_candidates = _object(
|
||||
identity.get("candidate_metrics"),
|
||||
"identity candidate metrics",
|
||||
)
|
||||
if candidates != identity_candidates or set(candidates) != {"ddrnet", "ppliteseg"}:
|
||||
raise CanonicalRecordedCatalogError("candidate metrics are not identity-bound")
|
||||
for candidate in ("ddrnet", "ppliteseg"):
|
||||
_validate_candidate_metrics(
|
||||
_object(candidates.get(candidate), f"{candidate} metrics"),
|
||||
candidate,
|
||||
)
|
||||
|
||||
decision = _object(document.get("decision"), "result decision")
|
||||
if (
|
||||
decision.get("selected_candidate") != selected_candidate
|
||||
or decision.get("visual_shadow_ready") is not True
|
||||
or decision.get("mission_policy_ready_for_configuration") is not True
|
||||
or decision.get("navigation_accepted") is not False
|
||||
or decision.get("production_accepted") is not False
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical result decision changed")
|
||||
catalogs = _object(document.get("catalogs"), "result catalogs")
|
||||
if catalogs.get("goose") != [] or catalogs.get("ravnoves") != []:
|
||||
raise CanonicalRecordedCatalogError("canonical result catalogs changed")
|
||||
limitations = document.get("limitations")
|
||||
if not isinstance(limitations, list) or not limitations or not all(
|
||||
isinstance(item, str) and item.strip() for item in limitations
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical result limitations are invalid")
|
||||
|
||||
expected_route_values: dict[str, object] = {
|
||||
"source_job_id": "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
"source_job_input_sha256": (
|
||||
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715"
|
||||
),
|
||||
"source_stream_sha256": (
|
||||
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac"
|
||||
),
|
||||
"recorded_media_source_id": "recorded.camera.6a3945242828a038",
|
||||
"recorded_media_generation_sha256": (
|
||||
"b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded"
|
||||
),
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
}
|
||||
if any(review.get(key) != expected for key, expected in expected_route_values.items()):
|
||||
raise CanonicalRecordedCatalogError("canonical full-route source binding changed")
|
||||
if review.get("ground_truth") is not False:
|
||||
raise CanonicalRecordedCatalogError("canonical full-route ground-truth marker changed")
|
||||
|
||||
artifacts = _artifact_catalog(document.get("artifacts"))
|
||||
timeline = _object(review.get("timeline"), "full-route timeline")
|
||||
if (
|
||||
timeline.get("path") != "video/frame-source-times-ns.bin"
|
||||
or timeline.get("encoding") != "uint64-le-nanoseconds"
|
||||
or timeline.get("frame_count") != 6830
|
||||
or timeline.get("byte_length") != 6830 * 8
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical full-route timeline changed")
|
||||
timeline_sha256 = _sha256_text(timeline.get("sha256"), "timeline digest")
|
||||
_require_artifact(
|
||||
artifacts,
|
||||
path="video/frame-source-times-ns.bin",
|
||||
sha256=timeline_sha256,
|
||||
byte_length=6830 * 8,
|
||||
media_type="application/octet-stream",
|
||||
)
|
||||
_validate_timeline_payload(
|
||||
result_root / "video" / "frame-source-times-ns.bin",
|
||||
expected_sha256=timeline_sha256,
|
||||
start_seconds=_finite_number(review.get("timeline_start_seconds"), "timeline start"),
|
||||
)
|
||||
|
||||
decode_repair = _object(review.get("decode_repair"), "decode repair")
|
||||
if (
|
||||
decode_repair.get("repaired_frame_count") != 1
|
||||
or decode_repair.get("sequence") != 6092
|
||||
or decode_repair.get("method") != "duplicate-previous-decoded-frame"
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical decode repair changed")
|
||||
repair_proofs = _object(decode_repair.get("proofs"), "decode repair proofs")
|
||||
for key, path in {
|
||||
"eomt": "proofs/decode_repair.json",
|
||||
"ddrnet": "proofs/ddrnet_decode_repair.json",
|
||||
}.items():
|
||||
proof = _object(repair_proofs.get(key), f"{key} decode proof")
|
||||
digest = _sha256_text(proof.get("sha256"), f"{key} decode proof digest")
|
||||
if proof.get("path") != path:
|
||||
raise CanonicalRecordedCatalogError("canonical decode proof changed")
|
||||
_require_artifact(artifacts, path=path, sha256=digest)
|
||||
|
||||
route_proofs = _object(review.get("proofs"), "full-route proofs")
|
||||
job_proof = _object(route_proofs.get("job"), "full-route job proof")
|
||||
job_digest = _sha256_text(job_proof.get("sha256"), "full-route job digest")
|
||||
if job_proof.get("path") != "proofs/job.json":
|
||||
raise CanonicalRecordedCatalogError("canonical job proof changed")
|
||||
_require_artifact(artifacts, path="proofs/job.json", sha256=job_digest)
|
||||
|
||||
layers = _object(review.get("layers"), "full-route layers")
|
||||
if set(layers) != {"city", "vegetation"}:
|
||||
raise CanonicalRecordedCatalogError("canonical full-route layers changed")
|
||||
_validate_full_route_layer(
|
||||
_object(layers.get("city"), "city layer"),
|
||||
layer="city",
|
||||
artifacts=artifacts,
|
||||
)
|
||||
_validate_full_route_layer(
|
||||
_object(layers.get("vegetation"), "vegetation layer"),
|
||||
layer="vegetation",
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def _validate_source_catalog_binding(detail: SessionDetail) -> None:
|
||||
summary = detail.summary
|
||||
if (
|
||||
detail.plugin_id != "nodedc.device.xgrids-lixelkity-k1"
|
||||
or detail.archive_id != "xgrids-k1.viewer-live.evidence"
|
||||
or summary.session_id != CANONICAL_SOURCE_SESSION_ID
|
||||
or summary.display_name != CANONICAL_SOURCE_LABEL
|
||||
or summary.status != "ready"
|
||||
or summary.started_at_utc != "2026-08-28T13:05:16.249Z"
|
||||
or summary.completed_at_utc != "2026-08-28T13:18:45.030Z"
|
||||
or summary.duration_seconds is None
|
||||
or not math.isclose(summary.duration_seconds, 808.779495667, abs_tol=1e-9)
|
||||
or summary.modalities != ("point-cloud", "trajectory", "video")
|
||||
or summary.source_count != 3
|
||||
or summary.total_bytes != 799_020_963
|
||||
or summary.replayable is not True
|
||||
or summary.lab is not None
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical source catalog identity changed")
|
||||
sources = {
|
||||
source.source_id: (
|
||||
source.semantic_channel_id,
|
||||
source.modality,
|
||||
source.status,
|
||||
source.seekable,
|
||||
source.artifact_id,
|
||||
)
|
||||
for source in detail.sources
|
||||
}
|
||||
if sources != {
|
||||
"sensor.camera.right": (
|
||||
"camera.video.recorded",
|
||||
"video",
|
||||
"recorded",
|
||||
True,
|
||||
"recorded-video-6a3945242828a038",
|
||||
),
|
||||
"sensor.lidar.primary": (
|
||||
"spatial.point-cloud.recorded",
|
||||
"point-cloud",
|
||||
"recorded",
|
||||
True,
|
||||
"raw-transport-primary",
|
||||
),
|
||||
"spatial.trajectory": (
|
||||
"spatial.pose.recorded",
|
||||
"trajectory",
|
||||
"recorded",
|
||||
True,
|
||||
"raw-transport-primary",
|
||||
),
|
||||
}:
|
||||
raise CanonicalRecordedCatalogError("canonical source channels changed")
|
||||
artifacts = {artifact.artifact_id: artifact for artifact in detail.artifacts}
|
||||
if set(artifacts) != {
|
||||
"raw-transport-clock",
|
||||
"raw-transport-clock-origin",
|
||||
"raw-transport-index",
|
||||
"raw-transport-primary",
|
||||
"recorded-video-6a3945242828a038",
|
||||
}:
|
||||
raise CanonicalRecordedCatalogError("canonical source artifacts changed")
|
||||
raw = artifacts["raw-transport-primary"]
|
||||
video = artifacts["recorded-video-6a3945242828a038"]
|
||||
if (
|
||||
raw.kind != "raw-transport"
|
||||
or raw.media_type != "application/x-nodedc-k1mqtt"
|
||||
or raw.byte_length != 245_183_013
|
||||
or raw.sha256 != "20c789eff922a6bbb53592f86614abc0729a30544df29e740e7a378d12af85c2"
|
||||
or raw.integrity_status != "verified"
|
||||
or video.kind != "recorded-video"
|
||||
or video.media_type != "video/mp4"
|
||||
or video.byte_length != 553_837_950
|
||||
or video.sha256 is not None
|
||||
or video.integrity_status != "validated-structure"
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical source artifact proof changed")
|
||||
def _validate_candidate_metrics(value: dict[str, Any], candidate: str) -> None:
|
||||
_text(value.get("loaded_model_name"), f"{candidate} model name")
|
||||
_sha256_text(value.get("checkpoint_sha256"), f"{candidate} checkpoint")
|
||||
validation = _object(value.get("validation_metrics"), f"{candidate} validation")
|
||||
validation_timing = _object(
|
||||
value.get("validation_timing"),
|
||||
f"{candidate} validation timing",
|
||||
)
|
||||
shadow_timing = _object(value.get("shadow_timing"), f"{candidate} shadow timing")
|
||||
resource = _object(value.get("resource"), f"{candidate} resource")
|
||||
for key in ("mean_iou_percent", "published_mean_iou_percent", "vegetation_mean_iou"):
|
||||
_finite_number(validation.get(key), f"{candidate} {key}")
|
||||
for key in ("latency_ms_p95", "throughput_fps_from_mean_inference"):
|
||||
_finite_number(validation_timing.get(key), f"{candidate} validation {key}")
|
||||
_finite_number(shadow_timing.get(key), f"{candidate} shadow {key}")
|
||||
_finite_number(shadow_timing.get("prewarm_latency_ms"), f"{candidate} prewarm")
|
||||
_nonnegative_integer(resource.get("peak_reserved_vram_bytes"), f"{candidate} VRAM")
|
||||
_text(resource.get("gpu_name"), f"{candidate} GPU")
|
||||
|
||||
|
||||
def _validate_full_route_layer(
|
||||
value: dict[str, Any],
|
||||
*,
|
||||
layer: str,
|
||||
artifacts: dict[str, dict[str, Any]],
|
||||
) -> None:
|
||||
expected = {
|
||||
"city": (
|
||||
re.compile(r"^result-[a-f0-9]{64}$"),
|
||||
"missioncore.recorded-eomt-taxonomy/v1",
|
||||
16,
|
||||
"video/eomt-semantic-masks.zip",
|
||||
),
|
||||
"vegetation": (
|
||||
re.compile(r"^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$"),
|
||||
"missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
64,
|
||||
"video/ddrnet-semantic-masks.zip",
|
||||
),
|
||||
}[layer]
|
||||
result_id = _text(value.get("result_id"), f"{layer} result id")
|
||||
if expected[0].fullmatch(result_id) is None or value.get("frame_count") != 6830:
|
||||
raise CanonicalRecordedCatalogError(f"canonical {layer} layer identity changed")
|
||||
_text(value.get("name"), f"{layer} layer name")
|
||||
_finite_number(value.get("inference_fps"), f"{layer} inference FPS")
|
||||
_finite_number(value.get("latency_p95_ms"), f"{layer} latency")
|
||||
_nonnegative_integer(value.get("peak_reserved_vram_bytes"), f"{layer} VRAM")
|
||||
_validate_taxonomy(
|
||||
_object(value.get("taxonomy"), f"{layer} taxonomy"),
|
||||
schema=expected[1],
|
||||
class_count=expected[2],
|
||||
label=layer,
|
||||
)
|
||||
archive = _object(value.get("mask_archive"), f"{layer} mask archive")
|
||||
digest = _sha256_text(archive.get("sha256"), f"{layer} archive digest")
|
||||
byte_length = _positive_integer(archive.get("byte_length"), f"{layer} archive bytes")
|
||||
if archive.get("path") != expected[3]:
|
||||
raise CanonicalRecordedCatalogError(f"canonical {layer} archive changed")
|
||||
_require_artifact(
|
||||
artifacts,
|
||||
path=expected[3],
|
||||
sha256=digest,
|
||||
byte_length=byte_length,
|
||||
media_type="application/zip",
|
||||
)
|
||||
|
||||
|
||||
def _validate_taxonomy(
|
||||
value: dict[str, Any],
|
||||
*,
|
||||
schema: str,
|
||||
class_count: int,
|
||||
label: str,
|
||||
) -> None:
|
||||
classes = value.get("classes")
|
||||
if value.get("schema_version") != schema or not isinstance(classes, list):
|
||||
raise CanonicalRecordedCatalogError(f"canonical {label} taxonomy changed")
|
||||
if len(classes) != class_count:
|
||||
raise CanonicalRecordedCatalogError(f"canonical {label} taxonomy size changed")
|
||||
for expected_id, raw in enumerate(classes):
|
||||
item = _object(raw, f"{label} taxonomy class")
|
||||
color = item.get("color_rgb")
|
||||
if (
|
||||
item.get("class_id") != expected_id
|
||||
or isinstance(item.get("class_id"), bool)
|
||||
or not isinstance(color, list)
|
||||
or len(color) != 3
|
||||
or any(
|
||||
isinstance(channel, bool)
|
||||
or not isinstance(channel, int)
|
||||
or not 0 <= channel <= 255
|
||||
for channel in color
|
||||
)
|
||||
or item.get("disposition")
|
||||
not in {"labeled", "ambiguous", "prediction", "undefined"}
|
||||
):
|
||||
raise CanonicalRecordedCatalogError(f"canonical {label} taxonomy class changed")
|
||||
_text(item.get("label"), f"{label} taxonomy label")
|
||||
for optional in ("material_class", "evidence_state"):
|
||||
if item.get(optional) is not None:
|
||||
_text(item.get(optional), f"{label} taxonomy {optional}")
|
||||
|
||||
|
||||
def _artifact_catalog(value: object) -> dict[str, dict[str, Any]]:
|
||||
if not isinstance(value, list) or not value:
|
||||
raise CanonicalRecordedCatalogError("canonical result artifacts are missing")
|
||||
catalog: dict[str, dict[str, Any]] = {}
|
||||
for raw in value:
|
||||
descriptor = _object(raw, "canonical result artifact")
|
||||
path = _text(descriptor.get("path"), "canonical artifact path")
|
||||
if path in catalog:
|
||||
raise CanonicalRecordedCatalogError("canonical result artifact is duplicated")
|
||||
catalog[path] = descriptor
|
||||
return catalog
|
||||
|
||||
|
||||
def _require_artifact(
|
||||
artifacts: dict[str, dict[str, Any]],
|
||||
*,
|
||||
path: str,
|
||||
sha256: str,
|
||||
byte_length: int | None = None,
|
||||
media_type: str | None = None,
|
||||
) -> None:
|
||||
descriptor = artifacts.get(path)
|
||||
if (
|
||||
descriptor is None
|
||||
or descriptor.get("sha256") != sha256
|
||||
or (byte_length is not None and descriptor.get("byte_length") != byte_length)
|
||||
or (media_type is not None and descriptor.get("media_type") != media_type)
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical replay artifact binding changed")
|
||||
|
||||
|
||||
def _validate_timeline_payload(
|
||||
path: Path,
|
||||
*,
|
||||
expected_sha256: str,
|
||||
start_seconds: float,
|
||||
) -> None:
|
||||
try:
|
||||
payload = path.read_bytes()
|
||||
values = struct.unpack("<6830Q", payload)
|
||||
except (OSError, struct.error) as exc:
|
||||
raise CanonicalRecordedCatalogError("canonical timeline payload is invalid") from exc
|
||||
if (
|
||||
hashlib.sha256(payload).hexdigest() != expected_sha256
|
||||
or values[0] != round(start_seconds * 1_000_000_000)
|
||||
or values[-1] > 9_007_199_254_740_991
|
||||
or any(
|
||||
current <= previous
|
||||
for previous, current in zip(values, values[1:], strict=False)
|
||||
)
|
||||
):
|
||||
raise CanonicalRecordedCatalogError("canonical timeline payload changed")
|
||||
|
||||
|
||||
def _sha256_text(value: object, label: str) -> str:
|
||||
digest = _text(value, label)
|
||||
if re.fullmatch(r"[a-f0-9]{64}", digest) is None:
|
||||
raise CanonicalRecordedCatalogError(f"{label} is invalid")
|
||||
return digest
|
||||
|
||||
|
||||
def _nonnegative_integer(value: object, label: str) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
||||
raise CanonicalRecordedCatalogError(f"{label} must be a non-negative integer")
|
||||
return value
|
||||
|
||||
|
||||
def _positive_integer(value: object, label: str) -> int:
|
||||
integer = _nonnegative_integer(value, label)
|
||||
if integer == 0:
|
||||
raise CanonicalRecordedCatalogError(f"{label} must be positive")
|
||||
return integer
|
||||
@@ -20,6 +20,7 @@ from .media import (
|
||||
validate_recorded_media_timeline,
|
||||
)
|
||||
from .models import (
|
||||
LabReplayCapability,
|
||||
LabSessionBinding,
|
||||
LayoutConflictError,
|
||||
ObservationArtifactCandidate,
|
||||
@@ -57,6 +58,7 @@ from .store import (
|
||||
|
||||
__all__ = [
|
||||
"LayoutConflictError",
|
||||
"LabReplayCapability",
|
||||
"LabSessionBinding",
|
||||
"ActiveSessionLease",
|
||||
"ActiveSessionLeaseError",
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any, Literal
|
||||
|
||||
SessionStatus = Literal["ready", "interrupted", "failed"]
|
||||
SessionModality = Literal["point-cloud", "trajectory", "video"]
|
||||
LAB_REPLAY_CAPABILITY_SCHEMA = "missioncore.observation-lab-replay-capability/v1"
|
||||
|
||||
|
||||
class SessionStoreError(RuntimeError):
|
||||
@@ -28,6 +29,45 @@ class LayoutConflictError(SessionStoreError):
|
||||
"""A workspace layout revision changed since the caller loaded it."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LabReplayCapability:
|
||||
"""Explicit, observation-only admission for a derived recorded replay."""
|
||||
|
||||
schema_version: Literal["missioncore.observation-lab-replay-capability/v1"]
|
||||
kind: Literal["canonical-recorded-rerun"]
|
||||
viewer_profile: Literal["recorded-session"]
|
||||
timeline: Literal["session_time"]
|
||||
activation: Literal["explicit"]
|
||||
commands_enabled: Literal[False]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
expected_text = (
|
||||
LAB_REPLAY_CAPABILITY_SCHEMA,
|
||||
"canonical-recorded-rerun",
|
||||
"recorded-session",
|
||||
"session_time",
|
||||
"explicit",
|
||||
)
|
||||
if (
|
||||
self.schema_version,
|
||||
self.kind,
|
||||
self.viewer_profile,
|
||||
self.timeline,
|
||||
self.activation,
|
||||
) != expected_text or self.commands_enabled is not False:
|
||||
raise ValueError("LAB replay capability is invalid")
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": self.schema_version,
|
||||
"kind": self.kind,
|
||||
"viewer_profile": self.viewer_profile,
|
||||
"timeline": self.timeline,
|
||||
"activation": self.activation,
|
||||
"commands_enabled": self.commands_enabled,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LabSessionBinding:
|
||||
"""Immutable provenance for one derived laboratory replay."""
|
||||
@@ -41,10 +81,15 @@ class LabSessionBinding:
|
||||
config_sha256: str | None
|
||||
run_created_at_utc: str
|
||||
published_at_utc: str
|
||||
replay_capability: LabReplayCapability | None
|
||||
provenance: dict[str, Any]
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
def as_dict(
|
||||
self,
|
||||
*,
|
||||
include_replay_capability: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
document: dict[str, Any] = {
|
||||
"lab_id": self.lab_id,
|
||||
"source_session_id": self.source_session_id,
|
||||
"result_kind": self.result_kind,
|
||||
@@ -55,6 +100,11 @@ class LabSessionBinding:
|
||||
"published_at_utc": self.published_at_utc,
|
||||
"provenance": self.provenance,
|
||||
}
|
||||
if include_replay_capability:
|
||||
document["replay_capability"] = (
|
||||
None if self.replay_capability is None else self.replay_capability.as_dict()
|
||||
)
|
||||
return document
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -137,6 +187,8 @@ class SessionDetail:
|
||||
summary: SessionSummary
|
||||
sources: tuple[SessionSource, ...]
|
||||
artifacts: tuple[SessionArtifact, ...]
|
||||
plugin_id: str
|
||||
archive_id: str
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
duration = self.summary.duration_seconds
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
@@ -17,6 +18,7 @@ from uuid import uuid4
|
||||
from k1link.artifacts import utc_now_iso
|
||||
|
||||
from .models import (
|
||||
LabReplayCapability,
|
||||
LabSessionBinding,
|
||||
LayoutConflictError,
|
||||
ObservationArtifactCandidate,
|
||||
@@ -114,6 +116,8 @@ CREATE TABLE IF NOT EXISTS observation_lab_instances (
|
||||
config_sha256 TEXT,
|
||||
run_created_at_utc TEXT NOT NULL,
|
||||
published_at_utc TEXT NOT NULL,
|
||||
include_recorded_media INTEGER CHECK (include_recorded_media IN (0, 1)),
|
||||
replay_capability_json TEXT,
|
||||
provenance_json TEXT NOT NULL
|
||||
);
|
||||
|
||||
@@ -213,10 +217,13 @@ class SessionStore:
|
||||
limit: int = 20,
|
||||
cursor: str | None = None,
|
||||
scope: SessionScope = "all",
|
||||
include_capability_projections: bool = True,
|
||||
) -> SessionPage:
|
||||
if not 1 <= limit <= 100:
|
||||
raise ValueError("limit must be within 1..100")
|
||||
scope_clause = {
|
||||
if not isinstance(include_capability_projections, bool):
|
||||
raise ValueError("capability projection policy must be boolean")
|
||||
scope_clause = ({
|
||||
"all": "1 = 1",
|
||||
"source": (
|
||||
"NOT EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
|
||||
@@ -226,7 +233,22 @@ class SessionStore:
|
||||
"EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
|
||||
"WHERE lab.session_id = sessions.session_id)"
|
||||
),
|
||||
}.get(scope)
|
||||
} if include_capability_projections else {
|
||||
"all": (
|
||||
"NOT EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
|
||||
"WHERE lab.session_id = sessions.session_id "
|
||||
"AND lab.replay_capability_json IS NOT NULL)"
|
||||
),
|
||||
"source": (
|
||||
"NOT EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
|
||||
"WHERE lab.session_id = sessions.session_id)"
|
||||
),
|
||||
"laboratory": (
|
||||
"EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
|
||||
"WHERE lab.session_id = sessions.session_id "
|
||||
"AND lab.replay_capability_json IS NULL)"
|
||||
),
|
||||
}).get(scope)
|
||||
if scope_clause is None:
|
||||
raise ValueError("scope must be all, source, or laboratory")
|
||||
parameters: list[object] = []
|
||||
@@ -283,8 +305,18 @@ class SessionStore:
|
||||
return SessionPage(items=items, next_cursor=next_cursor)
|
||||
|
||||
def get_session(self, session_id: str) -> SessionDetail:
|
||||
detail, _snapshot_sha256 = self.get_session_with_catalog_snapshot(session_id)
|
||||
return detail
|
||||
|
||||
def get_session_with_catalog_snapshot(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> tuple[SessionDetail, str]:
|
||||
"""Read one detail and its private catalog snapshot from one SQLite view."""
|
||||
|
||||
_validate_identifier(session_id, "session id")
|
||||
with self._connect() as connection:
|
||||
connection.execute("BEGIN")
|
||||
row = connection.execute(
|
||||
"SELECT * FROM observation_sessions WHERE session_id = ?",
|
||||
(session_id,),
|
||||
@@ -292,19 +324,20 @@ class SessionStore:
|
||||
if row is None:
|
||||
raise SessionNotFoundError("observation session was not found")
|
||||
source_rows = connection.execute(
|
||||
"SELECT source_id, semantic_channel_id, modality, status, seekable, artifact_id "
|
||||
"FROM observation_session_sources WHERE session_id = ? ORDER BY source_id",
|
||||
"SELECT * FROM observation_session_sources "
|
||||
"WHERE session_id = ? ORDER BY source_id",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
artifact_rows = connection.execute(
|
||||
"SELECT artifact_id, kind, media_type, byte_length, sha256, integrity_status "
|
||||
"FROM observation_session_artifacts WHERE session_id = ? ORDER BY artifact_id",
|
||||
"SELECT * FROM observation_session_artifacts "
|
||||
"WHERE session_id = ? ORDER BY artifact_id",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
lab_row = connection.execute(
|
||||
"SELECT * FROM observation_lab_instances WHERE session_id = ?",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
snapshot_sha256 = _catalog_snapshot_sha256(row, source_rows, artifact_rows)
|
||||
sources = tuple(
|
||||
SessionSource(
|
||||
source_id=source["source_id"],
|
||||
@@ -327,14 +360,17 @@ class SessionStore:
|
||||
)
|
||||
for artifact in artifact_rows
|
||||
)
|
||||
return SessionDetail(
|
||||
detail = SessionDetail(
|
||||
summary=_summary_from_row(
|
||||
row,
|
||||
lab=None if lab_row is None else _lab_binding_from_row(lab_row),
|
||||
),
|
||||
sources=sources,
|
||||
artifacts=artifacts,
|
||||
plugin_id=row["plugin_id"],
|
||||
archive_id=row["archive_id"],
|
||||
)
|
||||
return detail, snapshot_sha256
|
||||
|
||||
def get_lab_instance(self, session_id: str) -> LabSessionBinding | None:
|
||||
"""Return immutable LAB provenance without exposing filesystem locators."""
|
||||
@@ -359,9 +395,11 @@ class SessionStore:
|
||||
run_created_at_utc: str,
|
||||
source_result_id: str | None = None,
|
||||
config_sha256: str | None = None,
|
||||
replay_capability: LabReplayCapability | None = None,
|
||||
provenance: dict[str, Any] | None = None,
|
||||
duration_seconds: float | None = None,
|
||||
include_recorded_media: bool = True,
|
||||
expected_source_catalog_sha256: str | None = None,
|
||||
) -> LabSessionBinding:
|
||||
"""Append one immutable catalog projection over an existing source session.
|
||||
|
||||
@@ -392,9 +430,22 @@ class SessionStore:
|
||||
or duration_seconds <= 0
|
||||
):
|
||||
raise ValueError("LAB duration must be a positive finite value")
|
||||
if not isinstance(include_recorded_media, bool):
|
||||
raise ValueError("LAB recorded-media policy must be boolean")
|
||||
if (
|
||||
expected_source_catalog_sha256 is not None
|
||||
and SHA256_PATTERN.fullmatch(expected_source_catalog_sha256) is None
|
||||
):
|
||||
raise ValueError("LAB source catalog snapshot SHA-256 is invalid")
|
||||
normalized_provenance = provenance or {}
|
||||
_validate_lab_method(normalized_provenance)
|
||||
_validate_replay_capability_provenance(
|
||||
normalized_provenance,
|
||||
replay_capability,
|
||||
)
|
||||
serialized_provenance = _serialize_provenance(normalized_provenance)
|
||||
serialized_replay_capability = _serialize_replay_capability(replay_capability)
|
||||
serialized_recorded_media_policy = int(include_recorded_media)
|
||||
published_at = utc_now_iso()
|
||||
|
||||
with self._lock, self._connect() as connection:
|
||||
@@ -406,6 +457,19 @@ class SessionStore:
|
||||
if source is None:
|
||||
connection.rollback()
|
||||
raise SessionNotFoundError("LAB source observation session was not found")
|
||||
if (
|
||||
expected_source_catalog_sha256 is not None
|
||||
and _catalog_snapshot_sha256_for_session(
|
||||
connection,
|
||||
source_session_id,
|
||||
session_row=source,
|
||||
)
|
||||
!= expected_source_catalog_sha256
|
||||
):
|
||||
connection.rollback()
|
||||
raise SessionIntegrityError(
|
||||
"LAB source catalog changed after admission"
|
||||
)
|
||||
if (
|
||||
connection.execute(
|
||||
"SELECT 1 FROM observation_lab_instances WHERE session_id = ?",
|
||||
@@ -428,6 +492,7 @@ class SessionStore:
|
||||
"source_result_id": source_result_id,
|
||||
"config_sha256": config_sha256,
|
||||
"run_created_at_utc": run_created_at_utc,
|
||||
"replay_capability_json": serialized_replay_capability,
|
||||
"provenance_json": serialized_provenance,
|
||||
}
|
||||
if existing is not None:
|
||||
@@ -436,6 +501,36 @@ class SessionStore:
|
||||
raise SessionIntegrityError(
|
||||
"LAB session id is already bound to different provenance"
|
||||
)
|
||||
if replay_capability is not None:
|
||||
stored_recorded_media_policy = existing["include_recorded_media"]
|
||||
if (
|
||||
stored_recorded_media_policy is not None
|
||||
and stored_recorded_media_policy != serialized_recorded_media_policy
|
||||
):
|
||||
connection.rollback()
|
||||
raise SessionIntegrityError(
|
||||
"LAB session id is already bound to a different recorded-media policy"
|
||||
)
|
||||
_validate_existing_lab_projection(
|
||||
connection,
|
||||
session_id=session_id,
|
||||
source_session_id=source_session_id,
|
||||
display_name=normalized_name,
|
||||
run_created_at_utc=run_created_at_utc,
|
||||
duration_seconds=(
|
||||
source["duration_seconds"]
|
||||
if duration_seconds is None
|
||||
else duration_seconds
|
||||
),
|
||||
include_recorded_media=include_recorded_media,
|
||||
)
|
||||
if stored_recorded_media_policy is None:
|
||||
connection.execute(
|
||||
"UPDATE observation_lab_instances SET include_recorded_media = ? "
|
||||
"WHERE session_id = ?",
|
||||
(serialized_recorded_media_policy, session_id),
|
||||
)
|
||||
_synchronize_lab_projection_summary(connection, session_id)
|
||||
connection.commit()
|
||||
return _lab_binding_from_row(existing)
|
||||
if (
|
||||
@@ -507,8 +602,8 @@ class SessionStore:
|
||||
"INSERT INTO observation_lab_instances "
|
||||
"(session_id, source_session_id, lab_id, result_kind, result_id, "
|
||||
"source_result_id, config_sha256, run_created_at_utc, "
|
||||
"published_at_utc, provenance_json) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"published_at_utc, include_recorded_media, replay_capability_json, "
|
||||
"provenance_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
session_id,
|
||||
source_session_id,
|
||||
@@ -519,9 +614,13 @@ class SessionStore:
|
||||
config_sha256,
|
||||
run_created_at_utc,
|
||||
published_at,
|
||||
serialized_recorded_media_policy,
|
||||
serialized_replay_capability,
|
||||
serialized_provenance,
|
||||
),
|
||||
)
|
||||
if replay_capability is not None:
|
||||
_synchronize_lab_projection_summary(connection, session_id)
|
||||
connection.commit()
|
||||
binding = self.get_lab_instance(session_id)
|
||||
if binding is None:
|
||||
@@ -817,6 +916,22 @@ class SessionStore:
|
||||
"ALTER TABLE observation_session_artifacts "
|
||||
"ADD COLUMN replay_byte_length INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
lab_columns = {
|
||||
row["name"]
|
||||
for row in connection.execute("PRAGMA table_info(observation_lab_instances)")
|
||||
}
|
||||
if "replay_capability_json" not in lab_columns:
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances "
|
||||
"ADD COLUMN replay_capability_json TEXT"
|
||||
)
|
||||
if "include_recorded_media" not in lab_columns:
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances "
|
||||
"ADD COLUMN include_recorded_media INTEGER "
|
||||
"CHECK (include_recorded_media IN (0, 1))"
|
||||
)
|
||||
_migrate_canonical_replay_capabilities(connection)
|
||||
connection.commit()
|
||||
with _ignore_os_error():
|
||||
self.database_path.chmod(0o600)
|
||||
@@ -1046,6 +1161,428 @@ def _validate_candidate_replay(
|
||||
raise SessionIntegrityError("non-replayable observation declares replay artifacts")
|
||||
|
||||
|
||||
def _validate_existing_lab_projection(
|
||||
connection: sqlite3.Connection,
|
||||
*,
|
||||
session_id: str,
|
||||
source_session_id: str,
|
||||
display_name: str,
|
||||
run_created_at_utc: str,
|
||||
duration_seconds: float | None,
|
||||
include_recorded_media: bool,
|
||||
) -> None:
|
||||
"""Validate the immutable companion snapshot without rereading a mutable source row."""
|
||||
|
||||
summary = connection.execute(
|
||||
"SELECT * FROM observation_sessions "
|
||||
"WHERE session_id = ?",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
source_summary = connection.execute(
|
||||
"SELECT * FROM observation_sessions WHERE session_id = ?",
|
||||
(source_session_id,),
|
||||
).fetchone()
|
||||
if (
|
||||
summary is None
|
||||
or source_summary is None
|
||||
or summary["display_name"] != display_name
|
||||
or summary["duration_seconds"] != duration_seconds
|
||||
or summary["plugin_id"] != source_summary["plugin_id"]
|
||||
or summary["archive_id"] != LAB_ARCHIVE_ID
|
||||
or summary["status"] != source_summary["status"]
|
||||
or summary["started_at_utc"] != run_created_at_utc
|
||||
or summary["completed_at_utc"] != run_created_at_utc
|
||||
or summary["replayable"] != source_summary["replayable"]
|
||||
or summary["origin"] != LAB_ORIGIN
|
||||
or summary["primary_replay_artifact_id"]
|
||||
!= source_summary["primary_replay_artifact_id"]
|
||||
or summary["timeline_origin_epoch_ns"]
|
||||
!= source_summary["timeline_origin_epoch_ns"]
|
||||
or summary["timeline_origin_monotonic_ns"]
|
||||
!= source_summary["timeline_origin_monotonic_ns"]
|
||||
or summary["allowed_root"] != source_summary["allowed_root"]
|
||||
or summary["session_root"] != source_summary["session_root"]
|
||||
):
|
||||
raise SessionIntegrityError(
|
||||
"LAB session id is already bound to different catalog metadata"
|
||||
)
|
||||
if (
|
||||
summary["status"] != "ready"
|
||||
or summary["replayable"] != 1
|
||||
or summary["primary_replay_artifact_id"] is None
|
||||
or summary["timeline_origin_epoch_ns"] is None
|
||||
or summary["timeline_origin_monotonic_ns"] is None
|
||||
or summary["timeline_origin_epoch_ns"] < 0
|
||||
or summary["timeline_origin_monotonic_ns"] < 0
|
||||
):
|
||||
raise SessionIntegrityError(
|
||||
"LAB replay projection is not a complete ready recording"
|
||||
)
|
||||
if not include_recorded_media and (
|
||||
connection.execute(
|
||||
"SELECT 1 FROM observation_session_sources "
|
||||
"WHERE session_id = ? AND modality = 'video' LIMIT 1",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
is not None
|
||||
or connection.execute(
|
||||
"SELECT 1 FROM observation_session_artifacts "
|
||||
"WHERE session_id = ? AND kind = 'recorded-video' LIMIT 1",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
is not None
|
||||
):
|
||||
raise SessionIntegrityError(
|
||||
"LAB session id is already bound to a different recorded-media policy"
|
||||
)
|
||||
_validate_lab_projection_rows_match_source(
|
||||
connection,
|
||||
session_id=session_id,
|
||||
source_session_id=source_session_id,
|
||||
include_recorded_media=include_recorded_media,
|
||||
)
|
||||
|
||||
|
||||
def _validate_lab_projection_rows_match_source(
|
||||
connection: sqlite3.Connection,
|
||||
*,
|
||||
session_id: str,
|
||||
source_session_id: str,
|
||||
include_recorded_media: bool,
|
||||
) -> None:
|
||||
source_filter = "" if include_recorded_media else " AND modality <> 'video'"
|
||||
artifact_filter = "" if include_recorded_media else " AND kind <> 'recorded-video'"
|
||||
source_columns = (
|
||||
"source_id",
|
||||
"semantic_channel_id",
|
||||
"modality",
|
||||
"status",
|
||||
"seekable",
|
||||
"artifact_id",
|
||||
)
|
||||
artifact_columns = (
|
||||
"artifact_id",
|
||||
"kind",
|
||||
"media_type",
|
||||
"byte_length",
|
||||
"sha256",
|
||||
"integrity_status",
|
||||
"locator",
|
||||
"replay_byte_length",
|
||||
)
|
||||
expected_sources = connection.execute(
|
||||
"SELECT * FROM observation_session_sources WHERE session_id = ?"
|
||||
f"{source_filter} ORDER BY source_id", # noqa: S608 - closed static fragment
|
||||
(source_session_id,),
|
||||
).fetchall()
|
||||
actual_sources = connection.execute(
|
||||
"SELECT * FROM observation_session_sources "
|
||||
"WHERE session_id = ? ORDER BY source_id",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
expected_artifacts = connection.execute(
|
||||
"SELECT * FROM observation_session_artifacts WHERE session_id = ?"
|
||||
f"{artifact_filter} ORDER BY artifact_id", # noqa: S608 - closed static fragment
|
||||
(source_session_id,),
|
||||
).fetchall()
|
||||
actual_artifacts = connection.execute(
|
||||
"SELECT * FROM observation_session_artifacts "
|
||||
"WHERE session_id = ? ORDER BY artifact_id",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
|
||||
def values(
|
||||
rows: list[sqlite3.Row],
|
||||
columns: tuple[str, ...],
|
||||
) -> tuple[tuple[object, ...], ...]:
|
||||
return tuple(tuple(row[column] for column in columns) for row in rows)
|
||||
|
||||
if (
|
||||
values(actual_sources, source_columns)
|
||||
!= values(expected_sources, source_columns)
|
||||
or values(actual_artifacts, artifact_columns)
|
||||
!= values(expected_artifacts, artifact_columns)
|
||||
):
|
||||
raise SessionIntegrityError(
|
||||
"LAB replay projection no longer matches its admitted source snapshot"
|
||||
)
|
||||
|
||||
|
||||
def _synchronize_lab_projection_summary(
|
||||
connection: sqlite3.Connection,
|
||||
session_id: str,
|
||||
) -> None:
|
||||
"""Make the synthetic summary describe exactly its copied rows."""
|
||||
|
||||
summary = connection.execute(
|
||||
"SELECT modalities_json, source_count, total_bytes, replayable, "
|
||||
"primary_replay_artifact_id "
|
||||
"FROM observation_sessions WHERE session_id = ?",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
if summary is None:
|
||||
raise SessionIntegrityError("LAB projection catalog row is missing")
|
||||
try:
|
||||
declared_modalities = json.loads(summary["modalities_json"])
|
||||
except (TypeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("LAB projection modalities are invalid") from exc
|
||||
if not isinstance(declared_modalities, list) or not all(
|
||||
isinstance(value, str) for value in declared_modalities
|
||||
):
|
||||
raise SessionIntegrityError("LAB projection modalities are invalid")
|
||||
|
||||
actual_modalities = {
|
||||
row["modality"]
|
||||
for row in connection.execute(
|
||||
"SELECT DISTINCT modality FROM observation_session_sources "
|
||||
"WHERE session_id = ?",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
}
|
||||
modalities: list[str] = []
|
||||
for value in declared_modalities:
|
||||
if value in actual_modalities and value not in modalities:
|
||||
modalities.append(value)
|
||||
modalities.extend(sorted(actual_modalities.difference(modalities)))
|
||||
source_count = connection.execute(
|
||||
"SELECT COUNT(*) AS value FROM observation_session_sources WHERE session_id = ?",
|
||||
(session_id,),
|
||||
).fetchone()["value"]
|
||||
total_bytes = connection.execute(
|
||||
"SELECT COALESCE(SUM(byte_length), 0) AS value "
|
||||
"FROM observation_session_artifacts WHERE session_id = ? "
|
||||
"AND artifact_id IN ("
|
||||
"SELECT DISTINCT artifact_id FROM observation_session_sources "
|
||||
"WHERE session_id = ?)",
|
||||
(session_id, session_id),
|
||||
).fetchone()["value"]
|
||||
primary_artifact_id = summary["primary_replay_artifact_id"]
|
||||
if summary["replayable"] and (
|
||||
primary_artifact_id is None
|
||||
or connection.execute(
|
||||
"SELECT 1 FROM observation_session_artifacts "
|
||||
"WHERE session_id = ? AND artifact_id = ?",
|
||||
(session_id, primary_artifact_id),
|
||||
).fetchone()
|
||||
is None
|
||||
):
|
||||
raise SessionIntegrityError(
|
||||
"LAB projection omitted its primary replay artifact"
|
||||
)
|
||||
modalities_json = json.dumps(modalities, separators=(",", ":"))
|
||||
if (
|
||||
summary["modalities_json"] != modalities_json
|
||||
or summary["source_count"] != source_count
|
||||
or summary["total_bytes"] != total_bytes
|
||||
):
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET modalities_json = ?, source_count = ?, "
|
||||
"total_bytes = ?, updated_at_utc = ? WHERE session_id = ?",
|
||||
(
|
||||
modalities_json,
|
||||
source_count,
|
||||
total_bytes,
|
||||
utc_now_iso(),
|
||||
session_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _catalog_snapshot_sha256_for_session(
|
||||
connection: sqlite3.Connection,
|
||||
session_id: str,
|
||||
*,
|
||||
session_row: sqlite3.Row | None = None,
|
||||
) -> str:
|
||||
row = (
|
||||
session_row
|
||||
if session_row is not None
|
||||
else connection.execute(
|
||||
"SELECT * FROM observation_sessions WHERE session_id = ?",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
)
|
||||
if row is None:
|
||||
raise SessionNotFoundError("observation session was not found")
|
||||
source_rows = connection.execute(
|
||||
"SELECT * FROM observation_session_sources "
|
||||
"WHERE session_id = ? ORDER BY source_id",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
artifact_rows = connection.execute(
|
||||
"SELECT * FROM observation_session_artifacts "
|
||||
"WHERE session_id = ? ORDER BY artifact_id",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
return _catalog_snapshot_sha256(row, source_rows, artifact_rows)
|
||||
|
||||
|
||||
def _catalog_snapshot_sha256(
|
||||
session_row: sqlite3.Row,
|
||||
source_rows: list[sqlite3.Row],
|
||||
artifact_rows: list[sqlite3.Row],
|
||||
) -> str:
|
||||
"""Bind every stored field copied or referenced by a LAB projection."""
|
||||
|
||||
payload = {
|
||||
"session": dict(session_row),
|
||||
"sources": [dict(row) for row in source_rows],
|
||||
"artifacts": [dict(row) for row in artifact_rows],
|
||||
}
|
||||
serialized = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
return hashlib.sha256(serialized).hexdigest()
|
||||
|
||||
|
||||
def _migrate_canonical_replay_capabilities(connection: sqlite3.Connection) -> None:
|
||||
"""Type the one rolling-upgrade projection that predates the v2 column."""
|
||||
|
||||
rows = connection.execute(
|
||||
"SELECT * FROM observation_lab_instances "
|
||||
"WHERE replay_capability_json IS NULL OR include_recorded_media IS NULL"
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
try:
|
||||
provenance = json.loads(row["provenance_json"])
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
continue
|
||||
capability = _canonical_rolling_capability(row, provenance)
|
||||
if capability is None:
|
||||
continue
|
||||
serialized = _serialize_replay_capability(capability)
|
||||
stored = row["replay_capability_json"]
|
||||
if stored is not None and stored != serialized:
|
||||
raise SessionIntegrityError(
|
||||
"stored canonical LAB replay capability conflicts with provenance"
|
||||
)
|
||||
summary = connection.execute(
|
||||
"SELECT * FROM observation_sessions WHERE session_id = ?",
|
||||
(row["session_id"],),
|
||||
).fetchone()
|
||||
if summary is None:
|
||||
raise SessionIntegrityError("canonical LAB catalog row is missing")
|
||||
source_summary = connection.execute(
|
||||
"SELECT * FROM observation_sessions WHERE session_id = ?",
|
||||
(row["source_session_id"],),
|
||||
).fetchone()
|
||||
if (
|
||||
summary["display_name"]
|
||||
!= "RAVNOVES004TREE · полный маршрут восприятия"
|
||||
or summary["duration_seconds"] != 718.0
|
||||
or row["run_created_at_utc"]
|
||||
!= "2026-08-29T18:05:11.329061+00:00"
|
||||
or source_summary is None
|
||||
or source_summary["plugin_id"]
|
||||
!= "nodedc.device.xgrids-lixelkity-k1"
|
||||
or source_summary["archive_id"] != "xgrids-k1.viewer-live.evidence"
|
||||
):
|
||||
raise SessionIntegrityError(
|
||||
"canonical rolling LAB projection metadata is invalid"
|
||||
)
|
||||
_validate_existing_lab_projection(
|
||||
connection,
|
||||
session_id=row["session_id"],
|
||||
source_session_id=row["source_session_id"],
|
||||
display_name=summary["display_name"],
|
||||
run_created_at_utc=row["run_created_at_utc"],
|
||||
duration_seconds=summary["duration_seconds"],
|
||||
include_recorded_media=False,
|
||||
)
|
||||
connection.execute(
|
||||
"UPDATE observation_lab_instances SET replay_capability_json = ?, "
|
||||
"include_recorded_media = 0 WHERE session_id = ?",
|
||||
(serialized, row["session_id"]),
|
||||
)
|
||||
_synchronize_lab_projection_summary(connection, row["session_id"])
|
||||
|
||||
|
||||
def _canonical_rolling_capability(
|
||||
row: sqlite3.Row,
|
||||
provenance: object,
|
||||
) -> LabReplayCapability | None:
|
||||
if not isinstance(provenance, dict) or set(provenance) != {
|
||||
"schema_version",
|
||||
"evidence_identity_sha256",
|
||||
"result_document_sha256",
|
||||
"replay_capability",
|
||||
"authority",
|
||||
"method",
|
||||
}:
|
||||
return None
|
||||
evidence_identity = provenance.get("evidence_identity_sha256")
|
||||
result_document = provenance.get("result_document_sha256")
|
||||
expected_result_id = (
|
||||
f"lab-v1-vegetation-shadow-{evidence_identity}"
|
||||
if isinstance(evidence_identity, str)
|
||||
else None
|
||||
)
|
||||
if (
|
||||
provenance.get("schema_version")
|
||||
!= "missioncore.canonical-recorded-lab-projection/v1"
|
||||
or not isinstance(evidence_identity, str)
|
||||
or SHA256_PATTERN.fullmatch(evidence_identity) is None
|
||||
or not isinstance(result_document, str)
|
||||
or SHA256_PATTERN.fullmatch(result_document) is None
|
||||
or row["session_id"] != expected_result_id
|
||||
or row["result_id"] != expected_result_id
|
||||
or row["source_session_id"] != "20260828T130511Z_viewer_live"
|
||||
or row["lab_id"] != "LAB V1"
|
||||
or row["result_kind"] != "recorded-perception-qualification"
|
||||
or row["config_sha256"] is not None
|
||||
or not isinstance(row["source_result_id"], str)
|
||||
or re.fullmatch(
|
||||
r"lab-v1-vegetation-shadow-[a-f0-9]{64}",
|
||||
row["source_result_id"],
|
||||
)
|
||||
is None
|
||||
):
|
||||
return None
|
||||
authority = provenance.get("authority")
|
||||
authority_keys = {
|
||||
"commands_enabled",
|
||||
"navigation_or_safety_accepted",
|
||||
"actuation_accepted",
|
||||
}
|
||||
if (
|
||||
not isinstance(authority, dict)
|
||||
or set(authority) != authority_keys
|
||||
or any(authority[key] is not False for key in authority_keys)
|
||||
):
|
||||
return None
|
||||
method = provenance.get("method")
|
||||
expected_component = {
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": evidence_identity,
|
||||
}
|
||||
if method != {
|
||||
"schema_version": LAB_METHOD_SCHEMA,
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"components": [expected_component],
|
||||
}:
|
||||
return None
|
||||
capability_document = provenance.get("replay_capability")
|
||||
try:
|
||||
serialized = json.dumps(
|
||||
capability_document,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
return _replay_capability_from_row(serialized)
|
||||
except (TypeError, ValueError, SessionIntegrityError):
|
||||
return None
|
||||
|
||||
|
||||
def _summary_from_row(
|
||||
row: sqlite3.Row,
|
||||
*,
|
||||
@@ -1076,6 +1613,13 @@ def _lab_binding_from_row(row: sqlite3.Row) -> LabSessionBinding:
|
||||
raise SessionIntegrityError("stored LAB provenance is invalid") from exc
|
||||
if not isinstance(provenance, dict):
|
||||
raise SessionIntegrityError("stored LAB provenance is not an object")
|
||||
replay_capability = _replay_capability_from_row(row["replay_capability_json"])
|
||||
try:
|
||||
_validate_replay_capability_provenance(provenance, replay_capability)
|
||||
except ValueError as exc:
|
||||
raise SessionIntegrityError(
|
||||
"stored LAB replay capability does not match provenance"
|
||||
) from exc
|
||||
return LabSessionBinding(
|
||||
session_id=row["session_id"],
|
||||
source_session_id=row["source_session_id"],
|
||||
@@ -1086,10 +1630,66 @@ def _lab_binding_from_row(row: sqlite3.Row) -> LabSessionBinding:
|
||||
config_sha256=row["config_sha256"],
|
||||
run_created_at_utc=row["run_created_at_utc"],
|
||||
published_at_utc=row["published_at_utc"],
|
||||
replay_capability=replay_capability,
|
||||
provenance=provenance,
|
||||
)
|
||||
|
||||
|
||||
def _serialize_replay_capability(value: LabReplayCapability | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, LabReplayCapability):
|
||||
raise ValueError("LAB replay capability must use the typed contract")
|
||||
return json.dumps(value.as_dict(), sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def _replay_capability_from_row(value: object) -> LabReplayCapability | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
document = json.loads(value)
|
||||
except (TypeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("stored LAB replay capability is invalid") from exc
|
||||
expected_keys = {
|
||||
"schema_version",
|
||||
"kind",
|
||||
"viewer_profile",
|
||||
"timeline",
|
||||
"activation",
|
||||
"commands_enabled",
|
||||
}
|
||||
if not isinstance(document, dict) or set(document) != expected_keys:
|
||||
raise SessionIntegrityError("stored LAB replay capability is invalid")
|
||||
try:
|
||||
return LabReplayCapability(
|
||||
schema_version=document["schema_version"],
|
||||
kind=document["kind"],
|
||||
viewer_profile=document["viewer_profile"],
|
||||
timeline=document["timeline"],
|
||||
activation=document["activation"],
|
||||
commands_enabled=document["commands_enabled"],
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise SessionIntegrityError("stored LAB replay capability is invalid") from exc
|
||||
|
||||
|
||||
def _validate_replay_capability_provenance(
|
||||
provenance: dict[str, Any],
|
||||
capability: LabReplayCapability | None,
|
||||
) -> None:
|
||||
declared = provenance.get("replay_capability")
|
||||
if capability is None:
|
||||
if declared is not None:
|
||||
raise ValueError("LAB provenance cannot grant an untyped replay capability")
|
||||
return
|
||||
if (
|
||||
not isinstance(declared, dict)
|
||||
or _serialize_provenance(declared)
|
||||
!= _serialize_provenance(capability.as_dict())
|
||||
):
|
||||
raise ValueError("LAB replay capability must exactly match provenance")
|
||||
|
||||
|
||||
def _serialize_provenance(value: dict[str, Any]) -> str:
|
||||
try:
|
||||
serialized = json.dumps(
|
||||
|
||||
@@ -386,7 +386,11 @@ def finalized_replayable_recording_ids() -> tuple[str, ...]:
|
||||
finalized: list[str] = []
|
||||
cursor: str | None = None
|
||||
while True:
|
||||
page = session_store.list_recent(limit=100, cursor=cursor)
|
||||
page = session_store.list_recent(
|
||||
limit=100,
|
||||
cursor=cursor,
|
||||
include_capability_projections=False,
|
||||
)
|
||||
finalized.extend(
|
||||
summary.session_id
|
||||
for summary in page.items
|
||||
|
||||
@@ -341,10 +341,16 @@ def build_session_router(
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
cursor: str | None = Query(default=None, max_length=128),
|
||||
scope: Literal["all", "source", "laboratory"] = "all",
|
||||
lab_contract: Literal["v1", "v2"] = "v1",
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
_refresh_catalog(catalog_refresher)
|
||||
page = store.list_recent(limit=limit, cursor=cursor, scope=scope)
|
||||
page = store.list_recent(
|
||||
limit=limit,
|
||||
cursor=cursor,
|
||||
scope=scope,
|
||||
include_capability_projections=lab_contract == "v2",
|
||||
)
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
@@ -356,7 +362,15 @@ def build_session_router(
|
||||
"modalities": list(item.modalities),
|
||||
"duration_seconds": item.duration_seconds or 0.0,
|
||||
"replayable": item.replayable,
|
||||
**({"lab": item.lab.as_dict()} if item.lab is not None else {}),
|
||||
**(
|
||||
{
|
||||
"lab": item.lab.as_dict(
|
||||
include_replay_capability=lab_contract == "v2"
|
||||
)
|
||||
}
|
||||
if item.lab is not None
|
||||
else {}
|
||||
),
|
||||
**(
|
||||
{
|
||||
"preparation": _catalog_preparation_document(
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import k1link.laboratory.canonical_recorded_catalog as canonical_catalog
|
||||
from k1link.laboratory.canonical_recorded_catalog import (
|
||||
CANONICAL_REPLAY_CAPABILITY_SCHEMA,
|
||||
CanonicalRecordedCatalogError,
|
||||
publish_canonical_recorded_vegetation_result,
|
||||
)
|
||||
|
||||
|
||||
class CapturingStore:
|
||||
def __init__(self, *, source_label: str = "RAVNOVES004TREE") -> None:
|
||||
self.parameters: dict[str, Any] | None = None
|
||||
self.source_label = source_label
|
||||
|
||||
def publish_lab_instance(self, **parameters: Any) -> dict[str, Any]:
|
||||
self.parameters = parameters
|
||||
return parameters
|
||||
|
||||
def get_session_with_catalog_snapshot(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> tuple[SimpleNamespace, str]:
|
||||
assert session_id == "20260828T130511Z_viewer_live"
|
||||
summary = SimpleNamespace(
|
||||
session_id=session_id,
|
||||
display_name=self.source_label,
|
||||
status="ready",
|
||||
started_at_utc="2026-08-28T13:05:16.249Z",
|
||||
completed_at_utc="2026-08-28T13:18:45.030Z",
|
||||
duration_seconds=808.779495667,
|
||||
modalities=("point-cloud", "trajectory", "video"),
|
||||
source_count=3,
|
||||
total_bytes=799_020_963,
|
||||
replayable=True,
|
||||
lab=None,
|
||||
)
|
||||
sources = tuple(
|
||||
SimpleNamespace(
|
||||
source_id=source_id,
|
||||
semantic_channel_id=semantic_channel_id,
|
||||
modality=modality,
|
||||
status="recorded",
|
||||
seekable=True,
|
||||
artifact_id=artifact_id,
|
||||
)
|
||||
for source_id, semantic_channel_id, modality, artifact_id in (
|
||||
(
|
||||
"sensor.camera.right",
|
||||
"camera.video.recorded",
|
||||
"video",
|
||||
"recorded-video-6a3945242828a038",
|
||||
),
|
||||
(
|
||||
"sensor.lidar.primary",
|
||||
"spatial.point-cloud.recorded",
|
||||
"point-cloud",
|
||||
"raw-transport-primary",
|
||||
),
|
||||
(
|
||||
"spatial.trajectory",
|
||||
"spatial.pose.recorded",
|
||||
"trajectory",
|
||||
"raw-transport-primary",
|
||||
),
|
||||
)
|
||||
)
|
||||
artifacts = (
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-clock",
|
||||
kind="raw-transport-clock",
|
||||
media_type="application/json",
|
||||
byte_length=189,
|
||||
sha256="1" * 64,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-clock-origin",
|
||||
kind="raw-transport-clock-origin",
|
||||
media_type="application/json",
|
||||
byte_length=103,
|
||||
sha256="2" * 64,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-index",
|
||||
kind="raw-transport-index",
|
||||
media_type="application/x-ndjson",
|
||||
byte_length=7_679_275,
|
||||
sha256=None,
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="raw-transport-primary",
|
||||
kind="raw-transport",
|
||||
media_type="application/x-nodedc-k1mqtt",
|
||||
byte_length=245_183_013,
|
||||
sha256="20c789eff922a6bbb53592f86614abc0729a30544df29e740e7a378d12af85c2",
|
||||
integrity_status="verified",
|
||||
),
|
||||
SimpleNamespace(
|
||||
artifact_id="recorded-video-6a3945242828a038",
|
||||
kind="recorded-video",
|
||||
media_type="video/mp4",
|
||||
byte_length=553_837_950,
|
||||
sha256=None,
|
||||
integrity_status="validated-structure",
|
||||
),
|
||||
)
|
||||
return (
|
||||
SimpleNamespace(
|
||||
summary=summary,
|
||||
sources=sources,
|
||||
artifacts=artifacts,
|
||||
plugin_id="nodedc.device.xgrids-lixelkity-k1",
|
||||
archive_id="xgrids-k1.viewer-live.evidence",
|
||||
),
|
||||
"a" * 64,
|
||||
)
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
encoded = json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def _sealed_result(runtime_root: Path) -> Path:
|
||||
authority = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
"camera_semantics_can_clear_rigid_geometry": False,
|
||||
}
|
||||
timeline_payload = struct.pack(
|
||||
"<6830Q",
|
||||
*(39_250_000_000 + index * 100_000_000 for index in range(6830)),
|
||||
)
|
||||
artifact_payloads = {
|
||||
"video/eomt-semantic-masks.zip": b"fixture-city-mask-archive",
|
||||
"video/ddrnet-semantic-masks.zip": b"fixture-vegetation-mask-archive",
|
||||
"video/frame-source-times-ns.bin": timeline_payload,
|
||||
"proofs/job.json": b'{"fixture":"job"}',
|
||||
"proofs/decode_repair.json": b'{"fixture":"decode"}',
|
||||
"proofs/ddrnet_decode_repair.json": b'{"fixture":"decode"}',
|
||||
}
|
||||
|
||||
def proof(path: str) -> dict[str, object]:
|
||||
payload = artifact_payloads[path]
|
||||
return {
|
||||
"path": path,
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"byte_length": len(payload),
|
||||
}
|
||||
|
||||
def taxonomy(schema: str, count: int) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": schema,
|
||||
"classes": [
|
||||
{
|
||||
"class_id": index,
|
||||
"label": f"class-{index}",
|
||||
"color_rgb": [index % 256, (index * 2) % 256, (index * 3) % 256],
|
||||
"disposition": "undefined" if index == 0 else "prediction",
|
||||
}
|
||||
for index in range(count)
|
||||
],
|
||||
}
|
||||
|
||||
review = {
|
||||
"source_id": "RAVNOVES004TREE",
|
||||
"session_id": "20260828T130511Z_viewer_live",
|
||||
"source_job_id": "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
"source_job_input_sha256": (
|
||||
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715"
|
||||
),
|
||||
"source_stream_sha256": (
|
||||
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac"
|
||||
),
|
||||
"recorded_media_source_id": "recorded.camera.6a3945242828a038",
|
||||
"recorded_media_generation_sha256": (
|
||||
"b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded"
|
||||
),
|
||||
"linked_route_review_result_id": (
|
||||
"lab-v1-vegetation-shadow-" + "9" * 64
|
||||
),
|
||||
"frame_count": 6830,
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"ground_truth": False,
|
||||
"timeline_start_seconds": 39.25,
|
||||
"timeline_end_seconds": 757.25,
|
||||
"timeline": {
|
||||
**proof("video/frame-source-times-ns.bin"),
|
||||
"encoding": "uint64-le-nanoseconds",
|
||||
"frame_count": 6830,
|
||||
},
|
||||
"decode_repair": {
|
||||
"repaired_frame_count": 1,
|
||||
"sequence": 6092,
|
||||
"method": "duplicate-previous-decoded-frame",
|
||||
"proofs": {
|
||||
"eomt": proof("proofs/decode_repair.json"),
|
||||
"ddrnet": proof("proofs/ddrnet_decode_repair.json"),
|
||||
},
|
||||
},
|
||||
"proofs": {"job": proof("proofs/job.json")},
|
||||
"layers": {
|
||||
"city": {
|
||||
"name": "EoMT Cityscapes",
|
||||
"result_id": "result-" + "1" * 64,
|
||||
"frame_count": 6830,
|
||||
"taxonomy": taxonomy(
|
||||
"missioncore.recorded-eomt-taxonomy/v1",
|
||||
16,
|
||||
),
|
||||
"mask_archive": proof("video/eomt-semantic-masks.zip"),
|
||||
"inference_fps": 3.0,
|
||||
"latency_p95_ms": 361.0,
|
||||
"peak_reserved_vram_bytes": 2_977_955_840,
|
||||
},
|
||||
"vegetation": {
|
||||
"name": "ddrnet_39",
|
||||
"result_id": "lab-v1-ravnoves-video-ddrnet-" + "2" * 64,
|
||||
"frame_count": 6830,
|
||||
"taxonomy": taxonomy(
|
||||
"missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
64,
|
||||
),
|
||||
"mask_archive": proof("video/ddrnet-semantic-masks.zip"),
|
||||
"inference_fps": 52.0,
|
||||
"latency_p95_ms": 27.0,
|
||||
"peak_reserved_vram_bytes": 331_350_016,
|
||||
},
|
||||
},
|
||||
}
|
||||
candidates = {
|
||||
candidate: {
|
||||
"loaded_model_name": candidate,
|
||||
"checkpoint_sha256": str(index) * 64,
|
||||
"validation_metrics": {
|
||||
"mean_iou_percent": 70.0,
|
||||
"published_mean_iou_percent": 69.0,
|
||||
"vegetation_mean_iou": 0.7,
|
||||
},
|
||||
"validation_timing": {
|
||||
"latency_ms_p95": 20.0,
|
||||
"throughput_fps_from_mean_inference": 50.0,
|
||||
},
|
||||
"shadow_timing": {
|
||||
"latency_ms_p95": 21.0,
|
||||
"throughput_fps_from_mean_inference": 49.0,
|
||||
"prewarm_latency_ms": 100.0,
|
||||
},
|
||||
"resource": {
|
||||
"peak_reserved_vram_bytes": 1024,
|
||||
"gpu_name": "fixture-gpu",
|
||||
},
|
||||
}
|
||||
for index, candidate in enumerate(("ddrnet", "ppliteseg"), start=3)
|
||||
}
|
||||
source = {
|
||||
"shadow_session": "RAVNOVES004TREE",
|
||||
"shadow_camera": "sensor.camera.right",
|
||||
"shadow_frame_count": 6830,
|
||||
"video_shadow_frame_count": 6830,
|
||||
}
|
||||
identity = {
|
||||
"authority": authority,
|
||||
"base_result_id": "lab-v1-vegetation-shadow-" + "9" * 64,
|
||||
"route_full_review": review,
|
||||
"selected_candidate": "ddrnet",
|
||||
"candidate_metrics": candidates,
|
||||
"source": source,
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
result_id = f"lab-v1-vegetation-shadow-{identity_sha256}"
|
||||
result_root = runtime_root / "lab-v1-vegetation" / "results" / result_id
|
||||
result_root.mkdir(parents=True)
|
||||
document = {
|
||||
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"result_id": result_id,
|
||||
"identity": identity,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": "2026-08-29T18:05:11.329061+00:00",
|
||||
"ground_truth": False,
|
||||
"status": "visual-shadow-ready-policy-not-authorized",
|
||||
"authority": dict(authority),
|
||||
"source": source,
|
||||
"route_video": None,
|
||||
"route_review": None,
|
||||
"route_full_review": dict(review),
|
||||
"method": {
|
||||
"completeness": "complete",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
},
|
||||
"metrics": {"candidates": candidates},
|
||||
"decision": {
|
||||
"selected_candidate": "ddrnet",
|
||||
"visual_shadow_ready": True,
|
||||
"mission_policy_ready_for_configuration": True,
|
||||
"navigation_accepted": False,
|
||||
"production_accepted": False,
|
||||
},
|
||||
"limitations": ["fixture is observation-only"],
|
||||
"catalogs": {"goose": [], "ravnoves": []},
|
||||
"artifacts": [
|
||||
{
|
||||
**proof(path),
|
||||
"role": "fixture",
|
||||
"media_type": (
|
||||
"application/zip"
|
||||
if path.endswith(".zip")
|
||||
else "application/octet-stream"
|
||||
if path.endswith(".bin")
|
||||
else "application/json"
|
||||
),
|
||||
}
|
||||
for path in artifact_payloads
|
||||
],
|
||||
}
|
||||
for relative, payload in artifact_payloads.items():
|
||||
path = result_root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(payload)
|
||||
(result_root / "result.json").write_text(
|
||||
json.dumps(document, ensure_ascii=False, sort_keys=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result_root
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_uses_exact_session_binding_without_compute(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
store = CapturingStore()
|
||||
|
||||
published = publish_canonical_recorded_vegetation_result(
|
||||
store=store, # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
assert published["session_id"] == result_root.name
|
||||
assert published["source_session_id"] == "20260828T130511Z_viewer_live"
|
||||
assert published["source_result_id"] == "lab-v1-vegetation-shadow-" + "9" * 64
|
||||
assert published["config_sha256"] is None
|
||||
assert published["duration_seconds"] == pytest.approx(718.0)
|
||||
assert published["include_recorded_media"] is False
|
||||
assert published["expected_source_catalog_sha256"] == "a" * 64
|
||||
capability = published["replay_capability"]
|
||||
assert capability.as_dict() == {
|
||||
"schema_version": CANONICAL_REPLAY_CAPABILITY_SCHEMA,
|
||||
"kind": "canonical-recorded-rerun",
|
||||
"viewer_profile": "recorded-session",
|
||||
"timeline": "session_time",
|
||||
"activation": "explicit",
|
||||
"commands_enabled": False,
|
||||
}
|
||||
assert published["provenance"]["replay_capability"] == capability.as_dict()
|
||||
assert published["provenance"]["method"]["completeness"] == "legacy-partial"
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_authority_and_unregistered_root(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document["authority"]["commands_enabled"] = True
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="control authority"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
document["authority"]["commands_enabled"] = False
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
outside = tmp_path / result_root.name
|
||||
outside.mkdir()
|
||||
(outside / "result.json").write_text(
|
||||
document_path.read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="registered root"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=outside,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_non_boolean_authority(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document["authority"]["commands_enabled"] = 0
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="control authority"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_viewer_incomplete_result(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document.pop("metrics")
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
store = CapturingStore()
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="result metrics"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=store, # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
assert store.parameters is None
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_unbound_display_metrics(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
document_path = result_root / "result.json"
|
||||
document = json.loads(document_path.read_text(encoding="utf-8"))
|
||||
document["metrics"]["candidates"]["ddrnet"]["loaded_model_name"] = "tampered"
|
||||
document_path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="identity-bound"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_wrong_source_catalog_identity(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="source catalog identity"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(source_label="different source"), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_intermediate_symlink_escape(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
outside_runtime = tmp_path / "outside-runtime"
|
||||
outside_runtime.mkdir()
|
||||
actual_result = _sealed_result(outside_runtime)
|
||||
(runtime_root / "lab-v1-vegetation").symlink_to(
|
||||
outside_runtime / "lab-v1-vegetation",
|
||||
target_is_directory=True,
|
||||
)
|
||||
escaped_result = (
|
||||
runtime_root
|
||||
/ "lab-v1-vegetation"
|
||||
/ "results"
|
||||
/ actual_result.name
|
||||
)
|
||||
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="runtime root"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=CapturingStore(), # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=escaped_result,
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_recorded_projection_rejects_document_changed_after_proof(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime_root = tmp_path / "runtime"
|
||||
runtime_root.mkdir()
|
||||
result_root = _sealed_result(runtime_root)
|
||||
original = canonical_catalog.verify_laboratory_evidence_result
|
||||
|
||||
def mutate_after_verification(*args: object, **kwargs: object) -> dict[str, object]:
|
||||
proof = original(*args, **kwargs) # type: ignore[arg-type]
|
||||
path = result_root / "result.json"
|
||||
document = json.loads(path.read_text(encoding="utf-8"))
|
||||
document["method"]["pipeline_id"] = "tampered-after-proof/v1"
|
||||
path.write_text(json.dumps(document, sort_keys=True), encoding="utf-8")
|
||||
return proof
|
||||
|
||||
monkeypatch.setattr(
|
||||
canonical_catalog,
|
||||
"verify_laboratory_evidence_result",
|
||||
mutate_after_verification,
|
||||
)
|
||||
store = CapturingStore()
|
||||
with pytest.raises(CanonicalRecordedCatalogError, match="changed after verification"):
|
||||
publish_canonical_recorded_vegetation_result(
|
||||
store=store, # type: ignore[arg-type]
|
||||
runtime_root=runtime_root,
|
||||
result_root=result_root,
|
||||
)
|
||||
assert store.parameters is None
|
||||
@@ -12,9 +12,10 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import k1link.sessions.media as recorded_media_module
|
||||
import k1link.web.session_api as session_api_module
|
||||
@@ -22,6 +23,7 @@ from k1link.compute import RecordedPerceptionOverlayArtifact, RecordedPerception
|
||||
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
|
||||
from k1link.sessions import (
|
||||
LabReplayCapability,
|
||||
MaterializedRecording,
|
||||
RecordedMediaInspector,
|
||||
RecordedMediaManifest,
|
||||
@@ -347,6 +349,79 @@ def test_session_router_exposes_immutable_lab_provenance(tmp_path: Path) -> None
|
||||
assert_no_local_paths((item, detail), repository)
|
||||
|
||||
|
||||
def test_session_router_rolls_capability_projections_out_only_in_v2(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
legacy = store.publish_lab_instance(
|
||||
session_id="lab-e21-legacy",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB E21 · legacy",
|
||||
lab_id="LAB E21",
|
||||
result_kind="e21-realtime-envelope",
|
||||
result_id="e21-realtime-envelope-" + "1" * 64,
|
||||
run_created_at_utc="2026-07-23T15:55:15.548Z",
|
||||
provenance={"method": lab_method()},
|
||||
)
|
||||
capability = LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
canonical = store.publish_lab_instance(
|
||||
session_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
source_session_id=source.name,
|
||||
display_name="RAV004 · recorded",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
router = build_session_router(store)
|
||||
list_route = endpoint(router, "/api/v1/observation-sessions", "GET")
|
||||
|
||||
default_items = list_route(limit=20, cursor=None, scope="all")["items"]
|
||||
assert {item["id"] for item in default_items} == {source.name, legacy.session_id}
|
||||
default_legacy = next(item for item in default_items if item["id"] == legacy.session_id)
|
||||
assert "replay_capability" not in default_legacy["lab"]
|
||||
|
||||
v1_labs = list_route(
|
||||
limit=20,
|
||||
cursor=None,
|
||||
scope="laboratory",
|
||||
lab_contract="v1",
|
||||
)["items"]
|
||||
assert [item["id"] for item in v1_labs] == [legacy.session_id]
|
||||
v2_labs = list_route(
|
||||
limit=20,
|
||||
cursor=None,
|
||||
scope="laboratory",
|
||||
lab_contract="v2",
|
||||
)["items"]
|
||||
by_id = {item["id"]: item for item in v2_labs}
|
||||
assert set(by_id) == {legacy.session_id, canonical.session_id}
|
||||
assert by_id[legacy.session_id]["lab"]["replay_capability"] is None
|
||||
assert by_id[canonical.session_id]["lab"]["replay_capability"] == capability.as_dict()
|
||||
|
||||
application = FastAPI()
|
||||
application.include_router(router)
|
||||
assert TestClient(application).get(
|
||||
"/api/v1/observation-sessions?lab_contract=v3"
|
||||
).status_code == 422
|
||||
|
||||
|
||||
def test_delete_session_removes_evidence_and_cache_but_refuses_an_open_recording(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -16,6 +16,7 @@ from fastapi.routing import APIRoute
|
||||
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
|
||||
from k1link.sessions import (
|
||||
LabReplayCapability,
|
||||
SessionRecordingMaterializer,
|
||||
SessionRecordingPreparationManager,
|
||||
SessionStore,
|
||||
@@ -200,6 +201,60 @@ def test_startup_scan_baselines_historical_sessions_without_enqueuing(
|
||||
manager.close()
|
||||
|
||||
|
||||
def test_background_scan_never_prepares_explicit_capability_projection(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = _make_completed_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
monkeypatch.setattr(app_module, "session_store", store)
|
||||
baseline = set(app_module.finalized_replayable_recording_ids())
|
||||
capability = LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
projection = store.publish_lab_instance(
|
||||
session_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
source_session_id=source.name,
|
||||
display_name="RAV004 · explicit recorded review",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "8" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "fixture/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "fixture",
|
||||
"version": "v1",
|
||||
"role": "test",
|
||||
"identity_sha256": "9" * 64,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
current = set(app_module.finalized_replayable_recording_ids())
|
||||
assert baseline == {source.name}
|
||||
assert projection.session_id not in current
|
||||
assert app_module.newly_finalized_recording_ids(baseline, current) == ()
|
||||
|
||||
|
||||
def test_archive_revision_tracks_session_lifecycle_without_capture_churn(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -15,6 +15,7 @@ from k1link.device_plugins.xgrids_k1.mqtt.capture import (
|
||||
iter_capture_frames,
|
||||
)
|
||||
from k1link.sessions import (
|
||||
LabReplayCapability,
|
||||
LayoutConflictError,
|
||||
SessionIntegrityError,
|
||||
SessionNotFoundError,
|
||||
@@ -823,6 +824,7 @@ def test_lab_instance_is_independent_and_never_deletes_source_evidence(
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
assert binding.replay_capability is None
|
||||
|
||||
detail = store.get_session(binding.session_id)
|
||||
lab_command = store.prepare_replay(binding.session_id)
|
||||
@@ -879,6 +881,421 @@ def test_lab_instance_publication_is_idempotent_but_provenance_is_immutable(
|
||||
store.publish_lab_instance(**{**parameters, "config_sha256": "0" * 64})
|
||||
|
||||
|
||||
def test_lab_replay_capability_column_migrates_existing_catalog(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
data_dir = tmp_path / "data"
|
||||
initial = SessionStore(repository, data_dir=data_dir)
|
||||
initial.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
legacy = initial.publish_lab_instance(
|
||||
session_id="lab-e19-pre-capability",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB E19 · pre-capability",
|
||||
lab_id="LAB E19",
|
||||
result_kind="e19-legacy",
|
||||
result_id="e19-legacy-result",
|
||||
run_created_at_utc="2026-07-23T05:19:43.138Z",
|
||||
provenance={"method": lab_method()},
|
||||
)
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN replay_capability_json"
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN include_recorded_media"
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
migrated = SessionStore(repository, data_dir=data_dir)
|
||||
with sqlite3.connect(migrated.database_path) as connection:
|
||||
columns = {
|
||||
row[1]
|
||||
for row in connection.execute(
|
||||
"PRAGMA table_info(observation_lab_instances)"
|
||||
)
|
||||
}
|
||||
assert "replay_capability_json" in columns
|
||||
assert "include_recorded_media" in columns
|
||||
assert migrated.get_lab_instance(legacy.session_id).replay_capability is None
|
||||
|
||||
|
||||
def test_migration_types_only_the_exact_rolling_canonical_projection(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260828T130511Z_viewer_live")
|
||||
make_recorded_camera_source(source)
|
||||
data_dir = tmp_path / "data"
|
||||
initial = SessionStore(repository, data_dir=data_dir)
|
||||
initial.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET archive_id = ? WHERE session_id = ?",
|
||||
("xgrids-k1.viewer-live.evidence", source.name),
|
||||
)
|
||||
connection.commit()
|
||||
evidence_identity = "a" * 64
|
||||
capability = LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
provenance = {
|
||||
"schema_version": "missioncore.canonical-recorded-lab-projection/v1",
|
||||
"evidence_identity_sha256": evidence_identity,
|
||||
"result_document_sha256": "b" * 64,
|
||||
"replay_capability": capability.as_dict(),
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
},
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": evidence_identity,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
parameters = {
|
||||
"session_id": f"lab-v1-vegetation-shadow-{evidence_identity}",
|
||||
"source_session_id": source.name,
|
||||
"display_name": "RAVNOVES004TREE · полный маршрут восприятия",
|
||||
"lab_id": "LAB V1",
|
||||
"result_kind": "recorded-perception-qualification",
|
||||
"result_id": f"lab-v1-vegetation-shadow-{evidence_identity}",
|
||||
"source_result_id": "lab-v1-vegetation-shadow-" + "c" * 64,
|
||||
"config_sha256": None,
|
||||
"run_created_at_utc": "2026-08-29T18:05:11.329061+00:00",
|
||||
"duration_seconds": 718.0,
|
||||
"replay_capability": capability,
|
||||
"provenance": provenance,
|
||||
"include_recorded_media": False,
|
||||
}
|
||||
binding = initial.publish_lab_instance(**parameters)
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET modalities_json = ?, source_count = ?, "
|
||||
"total_bytes = ? WHERE session_id = ?",
|
||||
('["point-cloud","trajectory","video"]', 3, 999_999_999, binding.session_id),
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN replay_capability_json"
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN include_recorded_media"
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
migrated = SessionStore(repository, data_dir=data_dir)
|
||||
|
||||
assert migrated.get_lab_instance(binding.session_id).replay_capability == capability
|
||||
migrated_detail = migrated.get_session(binding.session_id)
|
||||
assert migrated_detail.summary.modalities == ("point-cloud", "trajectory")
|
||||
assert migrated_detail.summary.source_count == 2
|
||||
assert all(source.modality != "video" for source in migrated_detail.sources)
|
||||
assert migrated.list_recent(
|
||||
scope="laboratory",
|
||||
include_capability_projections=False,
|
||||
).items == ()
|
||||
assert [
|
||||
item.session_id
|
||||
for item in migrated.list_recent(
|
||||
scope="laboratory",
|
||||
include_capability_projections=True,
|
||||
).items
|
||||
] == [binding.session_id]
|
||||
assert migrated.publish_lab_instance(**parameters).session_id == binding.session_id
|
||||
|
||||
|
||||
def test_migration_rejects_a_non_replayable_canonical_projection(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260828T130511Z_viewer_live")
|
||||
make_recorded_camera_source(source)
|
||||
data_dir = tmp_path / "data"
|
||||
initial = SessionStore(repository, data_dir=data_dir)
|
||||
initial.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET archive_id = ? WHERE session_id = ?",
|
||||
("xgrids-k1.viewer-live.evidence", source.name),
|
||||
)
|
||||
connection.commit()
|
||||
capability = LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
evidence_identity = "a" * 64
|
||||
result_id = f"lab-v1-vegetation-shadow-{evidence_identity}"
|
||||
initial.publish_lab_instance(
|
||||
session_id=result_id,
|
||||
source_session_id=source.name,
|
||||
display_name="RAVNOVES004TREE · полный маршрут восприятия",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id=result_id,
|
||||
source_result_id="lab-v1-vegetation-shadow-" + "c" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
duration_seconds=718.0,
|
||||
include_recorded_media=False,
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"schema_version": "missioncore.canonical-recorded-lab-projection/v1",
|
||||
"evidence_identity_sha256": evidence_identity,
|
||||
"result_document_sha256": "b" * 64,
|
||||
"replay_capability": capability.as_dict(),
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
},
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": evidence_identity,
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
with sqlite3.connect(initial.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET replayable = 0, "
|
||||
"primary_replay_artifact_id = NULL, timeline_origin_epoch_ns = NULL, "
|
||||
"timeline_origin_monotonic_ns = NULL WHERE session_id = ?",
|
||||
(result_id,),
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN replay_capability_json"
|
||||
)
|
||||
connection.execute(
|
||||
"ALTER TABLE observation_lab_instances DROP COLUMN include_recorded_media"
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="LAB"):
|
||||
SessionStore(repository, data_dir=data_dir)
|
||||
|
||||
|
||||
def test_migration_rejects_boolean_aliases_in_rolling_authority(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
data_dir = tmp_path / "data"
|
||||
store = SessionStore(repository, data_dir=data_dir)
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
capability = {
|
||||
"schema_version": "missioncore.observation-lab-replay-capability/v1",
|
||||
"kind": "canonical-recorded-rerun",
|
||||
"viewer_profile": "recorded-session",
|
||||
"timeline": "session_time",
|
||||
"activation": "explicit",
|
||||
"commands_enabled": False,
|
||||
}
|
||||
evidence_identity = "a" * 64
|
||||
provenance = {
|
||||
"schema_version": "missioncore.canonical-recorded-lab-projection/v1",
|
||||
"evidence_identity_sha256": evidence_identity,
|
||||
"result_document_sha256": "b" * 64,
|
||||
"replay_capability": capability,
|
||||
"authority": {
|
||||
"commands_enabled": 0,
|
||||
"navigation_or_safety_accepted": 0,
|
||||
"actuation_accepted": 0,
|
||||
},
|
||||
"method": {
|
||||
"schema_version": "missioncore.laboratory-method/v1",
|
||||
"completeness": "legacy-partial",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
"components": [
|
||||
{
|
||||
"kind": "source",
|
||||
"name": "sealed full-route LAB result",
|
||||
"version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
"role": "immutable Session catalog projection",
|
||||
"identity_sha256": evidence_identity,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
result_id = f"lab-v1-vegetation-shadow-{evidence_identity}"
|
||||
connection.execute("PRAGMA foreign_keys = OFF")
|
||||
connection.execute(
|
||||
"INSERT INTO observation_sessions "
|
||||
"(session_id, plugin_id, archive_id, display_name, status, modalities_json, "
|
||||
"replayable, origin, source_count, total_bytes, allowed_root, session_root, "
|
||||
"created_at_utc, updated_at_utc) "
|
||||
"VALUES (?, ?, ?, ?, 'ready', '[]', 0, ?, 0, 0, ?, ?, ?, ?)",
|
||||
(
|
||||
result_id,
|
||||
"fixture.plugin",
|
||||
"missioncore.lab-instances",
|
||||
"invalid rolling authority",
|
||||
"missioncore.lab-instance/v1",
|
||||
str(repository),
|
||||
str(repository),
|
||||
"2026-08-30T00:00:00Z",
|
||||
"2026-08-30T00:00:00Z",
|
||||
),
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO observation_lab_instances "
|
||||
"(session_id, source_session_id, lab_id, result_kind, result_id, "
|
||||
"source_result_id, config_sha256, run_created_at_utc, published_at_utc, "
|
||||
"include_recorded_media, replay_capability_json, provenance_json) "
|
||||
"VALUES (?, ?, 'LAB V1', 'recorded-perception-qualification', ?, ?, "
|
||||
"NULL, ?, ?, NULL, NULL, ?)",
|
||||
(
|
||||
result_id,
|
||||
"20260828T130511Z_viewer_live",
|
||||
result_id,
|
||||
"lab-v1-vegetation-shadow-" + "c" * 64,
|
||||
"2026-08-29T18:05:11.329061+00:00",
|
||||
"2026-08-30T00:00:00Z",
|
||||
json.dumps(provenance, sort_keys=True),
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
migrated = SessionStore(repository, data_dir=data_dir)
|
||||
with sqlite3.connect(migrated.database_path) as connection:
|
||||
row = connection.execute(
|
||||
"SELECT replay_capability_json, include_recorded_media "
|
||||
"FROM observation_lab_instances WHERE session_id = ?",
|
||||
("lab-v1-vegetation-shadow-" + "a" * 64,),
|
||||
).fetchone()
|
||||
assert row == (None, None)
|
||||
|
||||
|
||||
def test_lab_instance_persists_typed_explicit_recorded_replay_capability(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
capability = LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
|
||||
binding = store.publish_lab_instance(
|
||||
session_id="lab-recorded-replay",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB V1 · recorded replay",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "a" * 64,
|
||||
source_result_id="lab-v1-vegetation-shadow-" + "b" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
|
||||
assert binding.replay_capability == capability
|
||||
assert "replay_capability" not in binding.as_dict()
|
||||
assert binding.as_dict()["provenance"]["replay_capability"] == capability.as_dict()
|
||||
assert store.get_lab_instance(binding.session_id) == binding
|
||||
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_lab_instances SET replay_capability_json = ? "
|
||||
"WHERE session_id = ?",
|
||||
('{"kind":"unknown"}', binding.session_id),
|
||||
)
|
||||
connection.commit()
|
||||
with pytest.raises(SessionIntegrityError, match="replay capability"):
|
||||
store.get_lab_instance(binding.session_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0, 1, None, True])
|
||||
def test_lab_replay_capability_rejects_non_literal_false(value: object) -> None:
|
||||
with pytest.raises(ValueError, match="replay capability"):
|
||||
LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=value, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def test_lab_instance_rejects_boolean_alias_in_capability_provenance(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
capability = LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
aliased = {**capability.as_dict(), "commands_enabled": 0}
|
||||
|
||||
with pytest.raises(ValueError, match="exactly match provenance"):
|
||||
store.publish_lab_instance(
|
||||
session_id="lab-recorded-replay-alias",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB V1 · recorded replay alias",
|
||||
lab_id="LAB V1",
|
||||
result_kind="recorded-perception-qualification",
|
||||
result_id="lab-v1-vegetation-shadow-" + "a" * 64,
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
replay_capability=capability,
|
||||
provenance={
|
||||
"replay_capability": aliased,
|
||||
"method": lab_method(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_lab_instance_rejects_publication_without_a_method_manifest(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -933,6 +1350,114 @@ def test_bounded_lab_instance_excludes_unbounded_recorded_media(
|
||||
assert store.prepare_replay(binding.session_id).primary_artifact.path.is_file()
|
||||
|
||||
|
||||
def test_capability_projection_summary_is_exact_and_idempotently_repairable(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
make_recorded_camera_source(source)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
_source_detail, source_snapshot_sha256 = (
|
||||
store.get_session_with_catalog_snapshot(source.name)
|
||||
)
|
||||
capability = LabReplayCapability(
|
||||
schema_version="missioncore.observation-lab-replay-capability/v1",
|
||||
kind="canonical-recorded-rerun",
|
||||
viewer_profile="recorded-session",
|
||||
timeline="session_time",
|
||||
activation="explicit",
|
||||
commands_enabled=False,
|
||||
)
|
||||
parameters = {
|
||||
"session_id": "lab-recorded-bounded",
|
||||
"source_session_id": source.name,
|
||||
"display_name": "LAB V1 · bounded recorded review",
|
||||
"lab_id": "LAB V1",
|
||||
"result_kind": "recorded-perception-qualification",
|
||||
"result_id": "lab-v1-vegetation-shadow-" + "1" * 64,
|
||||
"source_result_id": "lab-v1-vegetation-shadow-" + "2" * 64,
|
||||
"run_created_at_utc": "2026-08-29T18:05:11.329061+00:00",
|
||||
"duration_seconds": 59.962,
|
||||
"include_recorded_media": False,
|
||||
"expected_source_catalog_sha256": source_snapshot_sha256,
|
||||
"replay_capability": capability,
|
||||
"provenance": {
|
||||
"replay_capability": capability.as_dict(),
|
||||
"method": lab_method(),
|
||||
},
|
||||
}
|
||||
binding = store.publish_lab_instance(**parameters)
|
||||
detail = store.get_session(binding.session_id)
|
||||
assert detail.summary.modalities == ("point-cloud", "trajectory")
|
||||
assert detail.summary.source_count == len(detail.sources) == 2
|
||||
referenced_artifacts = {item.artifact_id for item in detail.sources}
|
||||
assert detail.summary.total_bytes == sum(
|
||||
artifact.byte_length
|
||||
for artifact in detail.artifacts
|
||||
if artifact.artifact_id in referenced_artifacts
|
||||
)
|
||||
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET modalities_json = ?, source_count = ?, "
|
||||
"total_bytes = ? WHERE session_id = ?",
|
||||
('["point-cloud","trajectory","video"]', 3, 999_999_999, binding.session_id),
|
||||
)
|
||||
connection.commit()
|
||||
assert store.publish_lab_instance(**parameters) == binding
|
||||
repaired = store.get_session(binding.session_id)
|
||||
assert repaired.summary.modalities == ("point-cloud", "trajectory")
|
||||
assert repaired.summary.source_count == 2
|
||||
assert repaired.summary.total_bytes == detail.summary.total_bytes
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="recorded-media policy"):
|
||||
store.publish_lab_instance(**{**parameters, "include_recorded_media": True})
|
||||
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_session_artifacts SET sha256 = ? "
|
||||
"WHERE session_id = ? AND artifact_id = 'raw-transport-primary'",
|
||||
("0" * 64, binding.session_id),
|
||||
)
|
||||
connection.commit()
|
||||
with pytest.raises(SessionIntegrityError, match="source snapshot"):
|
||||
store.publish_lab_instance(**parameters)
|
||||
|
||||
|
||||
def test_lab_publication_rejects_a_source_changed_after_snapshot(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
_detail, snapshot_sha256 = store.get_session_with_catalog_snapshot(source.name)
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_session_artifacts SET byte_length = byte_length + 1 "
|
||||
"WHERE session_id = ? AND artifact_id = 'raw-transport-primary'",
|
||||
(source.name,),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="changed after admission"):
|
||||
store.publish_lab_instance(
|
||||
session_id="lab-source-snapshot-race",
|
||||
source_session_id=source.name,
|
||||
display_name="LAB E21 · source snapshot race",
|
||||
lab_id="LAB E21",
|
||||
result_kind="source-snapshot-race",
|
||||
result_id="source-snapshot-race-result",
|
||||
run_created_at_utc="2026-08-29T18:05:11.329061+00:00",
|
||||
provenance={"method": lab_method()},
|
||||
expected_source_catalog_sha256=snapshot_sha256,
|
||||
)
|
||||
assert store.get_lab_instance("lab-source-snapshot-race") is None
|
||||
|
||||
|
||||
def test_delete_session_rejects_a_catalog_target_outside_its_allowed_root(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user